diff --git a/Cargo.lock b/Cargo.lock index c167887..e63e9fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -177,9 +177,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.47" +version = "1.2.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" dependencies = [ "find-msvc-tools", "shlex", @@ -286,9 +286,9 @@ dependencies = [ [[package]] name = "convert_case" -version = "0.7.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" dependencies = [ "unicode-segmentation", ] @@ -408,22 +408,23 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" dependencies = [ "convert_case", "proc-macro2", "quote", + "rustc_version", "syn 2.0.111", "unicode-xid", ] @@ -640,9 +641,9 @@ dependencies = [ [[package]] name = "instability" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" +checksum = "6778b0196eefee7df739db78758e5cf9b37412268bfa5650bfeed028aed20d9c" dependencies = [ "darling", "indoc", @@ -709,9 +710,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "linux-raw-sys" @@ -742,9 +743,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru" @@ -766,9 +767,9 @@ dependencies = [ [[package]] name = "luajit-src" -version = "210.6.4+e17ee83" +version = "210.6.5+7152e15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35a0ceb2a395ffa403a863adcf365e82cc8d8338ac7f5f949b9df5ca3de251e1" +checksum = "29e64ac463f01a02ee793423f9b351369cf244c5ee8bb9e2729a75b2eb404181" dependencies = [ "cc", "which", @@ -800,9 +801,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "log", @@ -1125,7 +1126,7 @@ dependencies = [ "rand", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror", "tokio", "uuid", ] @@ -1165,6 +1166,15 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.44" @@ -1209,6 +1219,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -1387,40 +1403,20 @@ dependencies = [ [[package]] name = "terminput" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86a8a2c741a146c297baff16b315401ab7ec4b4817eff783606e89734242408a" +checksum = "fb2e8e835e040588586eb480bd465b41e1164d507190cce0c7153bb56b27614d" dependencies = [ "bitflags", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.17", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", + "thiserror-impl", ] [[package]] @@ -1504,9 +1500,9 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -1515,21 +1511,21 @@ dependencies = [ [[package]] name = "tracing-appender" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" dependencies = [ "crossbeam-channel", - "thiserror 1.0.69", + "thiserror", "time", "tracing-subscriber", ] [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -1538,9 +1534,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", "valuable", @@ -1569,9 +1565,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", "nu-ansi-term", @@ -1992,18 +1988,18 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "zerocopy" -version = "0.8.28" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.28" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 4f7fe25..e7b84e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,4 +20,4 @@ tracing-appender = { version = "0.2" } uuid = { version = "1.0", features = ["v4", "serde"] } tracing-error = { version = "0.2" } tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } -vt100 = { version = "0" } +vt100 = "0.15.2" diff --git a/daemon/src/actors/client_connection.rs b/daemon/src/actors/client_connection.rs index d4d8a7a..024eaac 100644 --- a/daemon/src/actors/client_connection.rs +++ b/daemon/src/actors/client_connection.rs @@ -118,7 +118,11 @@ impl ClientConnection { comm::send_event(&mut self.stream, DaemonEvent::Disconnected).await.unwrap(); } SessionOutput(bytes) => { - comm::send_event(&mut self.stream, DaemonEvent::Raw(bytes)).await.unwrap(); + // comm::send_event(&mut self.stream, DaemonEvent::Raw(bytes)).await.unwrap(); + let chunk_size = 1024; + for chunk in bytes.chunks(chunk_size) { + comm::send_event(&mut self.stream, DaemonEvent::Raw(Bytes::copy_from_slice(chunk))).await.unwrap(); + } } NewSession(session_id, session_name) => { comm::send_event(&mut self.stream, DaemonEvent::NewSession(session_id, session_name)).await.unwrap(); diff --git a/daemon/src/actors/pane.rs b/daemon/src/actors/pane.rs index 1a8c298..d927d82 100644 --- a/daemon/src/actors/pane.rs +++ b/daemon/src/actors/pane.rs @@ -1,6 +1,8 @@ +use std::time::Duration; + use bytes::Bytes; use handle_macro::Handle; -use tokio::sync::mpsc; +use tokio::{sync::mpsc, time::MissedTickBehavior}; use tracing::Instrument; use crate::{ @@ -8,6 +10,7 @@ use crate::{ pty::{Pty, PtyHandle}, window::WindowHandle, }, + cell::RemuxCell, layout::Rect, prelude::*, }; @@ -16,10 +19,10 @@ use crate::{ pub enum PaneEvent { UserInput(Bytes), PtyOutput(Bytes), - PtyDied, Render, // uses the diff from prev state to get to desired state (falls back to rerender if no prev state) Rerender, // full rerender Resize { rect: Rect }, + PtyDied, Hide, Reveal, Kill, @@ -38,9 +41,14 @@ pub struct Pane { rx: mpsc::Receiver, pane_state: PaneState, pty_handle: PtyHandle, + + // cells + force_rerender: bool, + curr_grid: Vec, + prev_grid: Vec, + // vte related vte: vt100::Parser, - prev_screen_state: Option, rect: Rect, } impl Pane { @@ -53,6 +61,10 @@ impl Pane { let (tx, rx) = mpsc::channel(10); let handle = PaneHandle { tx }; + let total_cells = (rect.width * rect.height) as usize; + let curr_grid = vec![RemuxCell::default(); total_cells]; + let prev_grid = vec![RemuxCell::default(); total_cells]; + let vte = vt100::Parser::new(rect.height, rect.width, 0); let pty_handle = Pty::spawn(handle.clone(), rect)?; Ok(Self { @@ -61,56 +73,88 @@ impl Pane { window_handle, pty_handle, rx, + force_rerender: true, + curr_grid, + prev_grid, vte, pane_state: PaneState::Visible, - prev_screen_state: None, rect, }) } fn run(mut self) -> Result { let handle_clone = self.handle.clone(); + + let mut render_ticker = tokio::time::interval(Duration::from_millis(16)); // 60 fps + render_ticker.set_missed_tick_behavior(MissedTickBehavior::Skip); + + let mut is_dirty = true; let _task = tokio::spawn( async move { loop { - if let Some(event) = self.rx.recv().await { - match &event { - UserInput(..) | PtyOutput(..) => { - trace!(event=?event); - } - _ => { - info!(event=?event); + tokio::select! { + _ = render_ticker.tick() => { + if let PaneState::Visible = self.pane_state { + if self.force_rerender || is_dirty { + if let Err(e) = self.handle_render().await { + error!("Failed to render frame: {e}") + } + is_dirty = false; + } } } - match event { - UserInput(bytes) => { - self.handle_input(bytes).await.unwrap(); - } - PtyOutput(bytes) => { - if let Err(e) = self.handle_pty_output(bytes).await { - error!("Error while handling PTY output: {}", e); + event_result = self.rx.recv() => { + match event_result { + Some(event) => { + match &event { + UserInput(..) | PtyOutput(..) => { + trace!(event=?event); + } + _ => { + info!(event=?event); + } + } + match event { + UserInput(bytes) => { + self.handle_input(bytes).await.unwrap(); + } + PtyOutput(bytes) => { + if let Err(e) = self.handle_pty_output(bytes).await { + error!("Error while handling PTY output: {}", e); + } + is_dirty = true; + } + PtyDied => { + debug!("Pty died via exit"); + self.window_handle.kill_pane().await.unwrap(); + break; + } + Kill => { + self.pty_handle.kill().await.unwrap(); + debug!("Pty died via pane kill"); + break; + } + Render => { + is_dirty = true; + } + Rerender => { + self.force_rerender = true; + } + Resize { rect } => { + self.handle_resize(rect).await.unwrap(); + self.force_rerender = true; + } + Hide => { + self.pane_state = PaneState::Hidden; + } + Reveal => { + self.pane_state = PaneState::Visible; + } + } + } + None => { + error!("Channel closed"); + break; } - } - PtyDied => { - break; - } - Kill => { - self.pty_handle.kill().await.unwrap(); - break; - } - Render => { - self.handle_render().await.unwrap(); - } - Rerender => { - self.handle_rerender().await.unwrap(); - } - Resize { rect } => { - self.handle_resize(rect).await.unwrap(); - } - Hide => { - self.pane_state = PaneState::Hidden; - } - Reveal => { - self.pane_state = PaneState::Visible; } } } @@ -129,48 +173,40 @@ impl Pane { async fn handle_pty_output(&mut self, bytes: Bytes) -> Result<()> { self.vte.process(&bytes); - self.handle_rerender().await + Ok(()) } - // TODO: below code is bad and unused, need better diffing solution async fn handle_render(&mut self) -> Result<()> { - match &self.prev_screen_state { - Some(prev) => { - let cur_screen_state = self.vte.screen(); - let diff = cur_screen_state.state_diff(prev); - self.prev_screen_state = Some(cur_screen_state.clone()); - let (c_row, c_col) = cur_screen_state.cursor_position(); - let global_x = self.rect.x + 1 + c_col; - let global_y = self.rect.y + 1 + c_row; - Ok(self - .window_handle - .pane_output(self.id, Bytes::copy_from_slice(&diff), Some((global_x, global_y))) - .await?) - } - None => self.handle_rerender().await, - } - } - - async fn handle_rerender(&mut self) -> Result<()> { let screen = self.vte.screen(); + let rows = self.rect.height as usize; + let cols = self.rect.width as usize; - trace!("RERENDER -- id: {} size {:?}", self.id, screen.size()); - self.prev_screen_state = Some(screen.clone()); - let mut output = Vec::new(); - - for (i, row) in screen.rows_formatted(0, self.rect.width).enumerate() { - let cx = self.rect.x + 1; - let cy = self.rect.y + 1 + (i as u16); - - let move_cursor = format!("\x1b[{};{}H", cy, cx); - output.extend_from_slice(move_cursor.as_bytes()); + let total_cells = rows * cols; + if self.curr_grid.len() != total_cells { + self.curr_grid.resize(total_cells, RemuxCell::default()); + } + if self.prev_grid.len() != total_cells { + self.prev_grid.resize(total_cells, RemuxCell::default()); + } - let erase_chars = format!("\x1b[{}X", self.rect.width); - output.extend_from_slice(erase_chars.as_bytes()); - output.extend_from_slice(&row); + for r in 0..rows { + for c in 0..cols { + let idx = r * cols + c; + let remux_cell = &mut self.curr_grid[idx]; + if let Some(cell) = screen.cell(r as u16, c as u16) { + remux_cell.set_content(cell.contents().as_bytes()); + remux_cell.set_fg_color(cell.fgcolor()); + remux_cell.set_bg_color(cell.bgcolor()); + remux_cell.set_attributes_from_vt100(cell); + } else { + *remux_cell = RemuxCell::default(); + } + } } - output.extend_from_slice(b"\x1b[0m"); + let output = RemuxCell::render_diff(self.rect, &self.prev_grid, &self.curr_grid, self.force_rerender); + std::mem::swap(&mut self.prev_grid, &mut self.curr_grid); + self.force_rerender = false; let (c_row, c_col) = screen.cursor_position(); let global_x = self.rect.x + 1 + c_col; @@ -183,10 +219,13 @@ impl Pane { async fn handle_resize(&mut self, rect: Rect) -> Result<()> { self.rect = rect; + + let new_size = (rect.width * rect.height) as usize; + self.prev_grid.resize(new_size, RemuxCell::default()); + self.curr_grid.resize(new_size, RemuxCell::default()); + self.pty_handle.resize(rect).await?; self.vte.set_size(rect.height, rect.width); - - self.handle_rerender().await?; Ok(()) } } diff --git a/daemon/src/actors/pty.rs b/daemon/src/actors/pty.rs index 046aab8..dcf343b 100644 --- a/daemon/src/actors/pty.rs +++ b/daemon/src/actors/pty.rs @@ -164,10 +164,12 @@ impl Pty { Err(Errno::ECHILD) => info!("No such child process: {}", child), Err(err) => error!("waitpid failed: {}", err), } + debug!("stopping PtyProcess run"); if let Err(e) = self.pane_handle.pty_died().await { warn!("Could not notify pane that PTY died (Pane has likely already died) {}", e); } + Ok(()) }.in_current_span() }); diff --git a/daemon/src/actors/window.rs b/daemon/src/actors/window.rs index ae318d3..a6b7adb 100644 --- a/daemon/src/actors/window.rs +++ b/daemon/src/actors/window.rs @@ -1,7 +1,6 @@ -use std::{collections::HashMap, mem}; +use std::{collections::BTreeMap, mem}; use bytes::Bytes; -use crossterm::terminal; use handle_macro::Handle; use tokio::sync::mpsc; use tracing::Instrument; @@ -52,9 +51,9 @@ pub struct Window { rx: mpsc::Receiver, layout: LayoutNode, - layout_sizing_map: HashMap, - panes: HashMap, - pane_cursors: HashMap, + layout_sizing_map: BTreeMap, + panes: BTreeMap, + pane_cursors: BTreeMap, active_pane_id: usize, next_pane_id: usize, root_rect: Rect, @@ -76,6 +75,7 @@ impl Window { let init_pane_id = 0; let init_layout_node = LayoutNode::Pane { id: init_pane_id }; + // Default size, overridden when client connects and send new size let (cols, rows) = (80, 24); let root_rect = Rect { x: 0, @@ -83,11 +83,11 @@ impl Window { width: cols, height: rows, }; - let mut layout_sizing_map = HashMap::new(); + let mut layout_sizing_map = BTreeMap::new(); layout_sizing_map.insert(init_pane_id, root_rect); init_layout_node.calculate_layout(root_rect, &mut layout_sizing_map)?; - let mut panes = HashMap::new(); + let mut panes = BTreeMap::new(); if let Some(rect) = layout_sizing_map.get(&init_pane_id) { let pane_handle = Pane::spawn(handle.clone(), init_pane_id, *rect)?; panes.insert(init_pane_id, pane_handle); @@ -103,7 +103,7 @@ impl Window { active_pane_id: init_pane_id, next_pane_id: init_pane_id + 1, window_state: WindowState::Focused, - pane_cursors: HashMap::new(), + pane_cursors: BTreeMap::new(), root_rect, }) } @@ -132,7 +132,7 @@ impl Window { self.handle_split_pane(direction).await.unwrap(); } KillPane => { - debug!("Window: IteratePane"); + debug!("Window: KillPane"); self.handle_kill_pane().await.unwrap(); } Redraw => { @@ -147,16 +147,8 @@ impl Window { break; } TerminalResize { rows, cols } => { - for pane in self.panes.values_mut() { - pane.resize(Rect { - x: 0, - y: 0, - width: cols, - height: rows, - }) - .await - .unwrap(); - } + debug!("Window: TerminalResize"); + self.handle_terminal_resize(rows, cols).await.unwrap(); } } } @@ -191,18 +183,16 @@ impl Window { Ok(()) } async fn handle_redraw(&mut self) -> Result<()> { - for pane in self.panes.values() { - pane.rerender().await?; + for pane in self.panes.iter() { + pane.1.rerender().await?; } Ok(()) } async fn handle_iterate_pane(&mut self, is_next: bool) -> Result<()> { - let mut ids: Vec = self.panes.keys().copied().collect(); + let ids: Vec = self.panes.keys().copied().collect(); if ids.is_empty() { return Ok(()); } - ids.sort(); - let current_idx = ids.iter().position(|&id| id == self.active_pane_id).unwrap_or(0); let new_idx = if is_next { @@ -253,11 +243,6 @@ impl Window { } } - // self.session_handle - // .window_output(Bytes::from( - // crossterm::terminal::Clear(crossterm::terminal::ClearType::All).to_string(), - // )) - // .await?; self.handle_redraw().await?; Ok(()) } @@ -278,7 +263,6 @@ impl Window { self.panes.remove(&dead_pane_id); self.pane_cursors.remove(&dead_pane_id); - self.layout_sizing_map.remove(&dead_pane_id); let dummy_node = LayoutNode::Pane { id: 0 }; let old_layout = mem::replace(&mut self.layout, dummy_node); @@ -290,9 +274,7 @@ impl Window { return Ok(()); } - if let Some(&new_id) = self.panes.keys().next() { - self.active_pane_id = new_id; - } + self.handle_iterate_pane(false).await?; self.layout_sizing_map.clear(); self.layout @@ -304,13 +286,27 @@ impl Window { } } - self.session_handle - .window_output(Bytes::from( - crossterm::terminal::Clear(crossterm::terminal::ClearType::All).to_string(), - )) - .await?; self.handle_redraw().await?; + Ok(()) + } + async fn handle_terminal_resize(&mut self, rows: u16, cols: u16) -> Result<()> { + self.root_rect = Rect { + x: 0, + y: 0, + width: cols, + height: rows, + }; + self.layout + .calculate_layout(self.root_rect, &mut self.layout_sizing_map)?; + + for (id, pane) in self.panes.iter() { + if let Some(new_rect) = self.layout_sizing_map.get(id) { + pane.resize(*new_rect).await?; + } + } + + self.handle_redraw().await?; Ok(()) } } diff --git a/daemon/src/cell.rs b/daemon/src/cell.rs new file mode 100644 index 0000000..2043754 --- /dev/null +++ b/daemon/src/cell.rs @@ -0,0 +1,383 @@ +use crate::layout::Rect; + +pub const CONTENT_LENGTH: usize = 22; // size of vt100 cell content +pub const SPACE: u8 = 0x20; + +// attributes +const BOLD: u8 = 0b0000_0001; +const ITALIC: u8 = 0b0000_0010; +const UNDERLINE: u8 = 0b0000_0100; +const INVERSE: u8 = 0b0000_1000; +const WIDE: u8 = 0b0001_0000; +const WIDE_SPACER: u8 = 0b0010_0000; + +const LEN_BITS: u8 = 0b0001_1111; + +#[repr(C)] +#[derive(Clone, Debug)] +pub struct RemuxCell { + contents: [u8; CONTENT_LENGTH], // 22 Bytes + len: u8, // 1 byte + attributes: u8, // 1 byte + fg_color: u32, // 4 bytes format: [Type: u8] [R: u8] [G: u8] [B: u8] + bg_color: u32, // 4 bytes format: [Type: u8] [R: u8] [G: u8] [B: u8] +} + +// verify each cell is 32 bytes +const _: () = assert!(std::mem::size_of::() == 32); + +// default cell has 1 space and no styles +impl Default for RemuxCell { + fn default() -> Self { + let mut content = [0u8; CONTENT_LENGTH]; + content[0] = SPACE; + Self { + contents: content, + len: 1, + fg_color: color_to_bytes(vt100::Color::Default), + bg_color: color_to_bytes(vt100::Color::Default), + attributes: 0, + } + } +} + +// equality checks integers first, then compares contents +impl PartialEq for RemuxCell { + fn eq(&self, other: &Self) -> bool { + if self.len != other.len + || self.fg_color != other.fg_color + || self.bg_color != other.bg_color + || self.attributes != other.attributes + { + return false; + } + + self.contents == other.contents + } +} + +impl RemuxCell { + pub fn render_diff( + rect: Rect, + prev_grid: &[RemuxCell], // 2D grid flattened to 1D + curr_grid: &[RemuxCell], // 2D grid flattened to 1D + force_rerender: bool, + ) -> Vec { + // average 10 bytes per cell + let mut output = Vec::with_capacity((rect.width as usize * rect.height as usize) * 10); + + // set intitial values + let default_color_bytes = color_to_bytes(vt100::Color::Default); + let mut current_fg_color = default_color_bytes; + let mut current_bg_color = default_color_bytes; + let mut current_attributes: u8 = 0; + + let mut cursor_y = 0; + let mut cursor_x = 0; + let mut cursor_invalid = true; + + let rows = rect.height as usize; + let cols = rect.width as usize; + + const VISIBLE_ON_WHITESPACE: u8 = INVERSE | UNDERLINE; + + for r in 0..rows { + // first pass is a backwards pass to find the last index with a visible change + // we use this to clear empty space + let mut last_char_index = 0; + for c in (0..cols).rev() { + let cell = &curr_grid[r * cols + c]; + + let is_visually_empty = cell.contents[0] == SPACE + && cell.bg_color == default_color_bytes + && (cell.attributes & VISIBLE_ON_WHITESPACE) == 0; + + if !is_visually_empty { + last_char_index = c + 1; + break; + } + } + + // second pass is a forward pass + for c in 0..cols { + // if we are past the last useful char, we check if there is content already there + // if there is, we check if it was visible content + if c >= last_char_index { + let prev_has_content = if r < prev_grid.len() && c < prev_grid[0].len() { + let prev_cell = &prev_grid[r * cols + c]; + let prev_bg_default = prev_cell.bg_color == default_color_bytes; + + prev_cell.contents[0] != SPACE + || !prev_bg_default + || (prev_cell.attributes & VISIBLE_ON_WHITESPACE) != 0 + } else { + true + }; + + // if its a full rerender, or if there was content there previously, + // we reset everything, move the cursor, remove all characters to the end of + // the row, then set the cursor as invalid + if force_rerender || prev_has_content { + if current_bg_color != default_color_bytes || current_attributes != 0 { + output.extend_from_slice(b"\x1b[0m"); + current_bg_color = default_color_bytes; + current_fg_color = default_color_bytes; + current_attributes = 0; + } + + let target_x = rect.x + 1 + c as u16; + let target_y = rect.y + 1 + r as u16; + set_cursor_position(&mut output, target_x, target_y); + + let count = cols - c; + output.extend_from_slice(b"\x1b["); + push_u16(&mut output, count as u16); + output.push(b'X'); + + cursor_invalid = true; + break; + } else { + continue; + } + } + + // if we are at a useful cell, grab it + // then we check if its a spacer, if it is we skip it + let cell = &curr_grid[r * cols + c]; + if cell.has_attribute(WIDE_SPACER) { + continue; + } + + // if this is a NOT full rerender and its within the bound of the previous + // grid, we do an equality check and skip processing if nothing has changed + if !force_rerender && r < prev_grid.len() && c < prev_grid[0].len() { + if cell == &prev_grid[r * cols + c] { + continue; + } + } + + // if we are not at the correct place already, move the cursor + let target_x = rect.x + 1 + c as u16; + let target_y = rect.y + 1 + r as u16; + if cursor_invalid || cursor_y != r || cursor_x != c { + set_cursor_position(&mut output, target_x, target_y); + cursor_y = r; + cursor_x = c; + cursor_invalid = false; + } + + // check if the current attributes are the same as the new ones, + // if they are, just check colors, otherwise change the attributes + if cell.attributes != current_attributes { + output.extend_from_slice(b"\x1b[0"); + Self::get_attributes_to_ansi(&mut output, cell); + output.push(b'm'); + + current_attributes = cell.attributes; + + current_bg_color = default_color_bytes; + current_fg_color = default_color_bytes; + + if cell.fg_color != current_fg_color { + u32_color_to_ansi(&mut output, cell.fg_color, true); + } + + if cell.bg_color != current_bg_color { + u32_color_to_ansi(&mut output, cell.bg_color, false); + } + } else { + if cell.fg_color != current_fg_color { + u32_color_to_ansi(&mut output, cell.fg_color, true); + current_fg_color = cell.fg_color; + } + if cell.bg_color != current_bg_color { + u32_color_to_ansi(&mut output, cell.bg_color, false); + current_bg_color = cell.bg_color; + } + } + + // finally add the actual content of the cell, usually a char + let data = &cell.contents; + let len = cell.len(); + output.extend_from_slice(&data[..len]); + + // move the cursor after adding content + cursor_x += if cell.has_attribute(WIDE) { 2 } else { 1 }; + } + } + + // reset styles (clean up thing) and return the buffer + output.extend_from_slice(b"\x1b[0m"); + output + } + + // adds all attributes from RemuxCell as ANSI codes + fn get_attributes_to_ansi(buffer: &mut Vec, cell: &RemuxCell) { + if cell.has_attribute(BOLD) { + buffer.extend_from_slice(b";1"); + } + if cell.has_attribute(ITALIC) { + buffer.extend_from_slice(b";3"); + } + if cell.has_attribute(UNDERLINE) { + buffer.extend_from_slice(b";4"); + } + if cell.has_attribute(INVERSE) { + buffer.extend_from_slice(b";7"); + } + } +} +// setters and getters +impl RemuxCell { + pub fn set_attributes_from_vt100(&mut self, cell: &vt100::Cell) { + self.set_attribute(BOLD, cell.bold()); + self.set_attribute(ITALIC, cell.italic()); + self.set_attribute(UNDERLINE, cell.underline()); + self.set_attribute(INVERSE, cell.inverse()); + self.set_attribute(WIDE, cell.is_wide()); + self.set_attribute(WIDE_SPACER, cell.is_wide_continuation()); + } + pub fn set_attribute(&mut self, attribute: u8, enable: bool) { + if enable { + self.attributes |= attribute; + } else { + self.attributes &= !attribute; + } + } + + pub fn has_attribute(&self, attribute: u8) -> bool { + (self.attributes & attribute) != 0 + } + + pub fn len(&self) -> usize { + usize::from(self.len & LEN_BITS) + } + + pub fn set_content(&mut self, contents: &[u8]) { + self.contents = [0u8; CONTENT_LENGTH]; + + if contents.is_empty() || contents[0] == 0 { + self.contents[0] = SPACE; + self.len = 1; + } else { + let len = contents.len().min(CONTENT_LENGTH); + self.contents[..len].copy_from_slice(&contents[..len]); + self.len = len as u8; + } + } + + pub fn set_fg_color(&mut self, color: vt100::Color) { + self.fg_color = color_to_bytes(color); + } + + pub fn set_bg_color(&mut self, color: vt100::Color) { + self.bg_color = color_to_bytes(color); + } +} + +// helper functions +// push_u8 is faster than write!() due to under-the-hood Rust stuff +#[inline] +fn push_u8(buf: &mut Vec, n: u8) { + if n == 0 { + buf.push(b'0'); + return; + } + if n < 10 { + buf.push(b'0' + n); + return; + } + if n < 100 { + buf.push(b'0' + (n / 10)); + buf.push(b'0' + (n % 10)); + return; + } + buf.push(b'0' + (n / 100)); + buf.push(b'0' + ((n / 10) % 10)); + buf.push(b'0' + (n % 10)); +} + +// push_u16 is faster than write!() due to under-the-hood Rust stuff +#[inline] +fn push_u16(buf: &mut Vec, mut n: u16) { + if n == 0 { + buf.push(b'0'); + return; + } + + // create temp buffer and write backwards + let mut buffer = [0u8; 5]; + let mut i = 5; + + while n > 0 { + i -= 1; + buffer[i] = b'0' + (n % 10) as u8; + n /= 10; + } + + buf.extend_from_slice(&buffer[i..]); +} + +#[inline] +fn set_cursor_position(buf: &mut Vec, x: u16, y: u16) { + buf.extend_from_slice(b"\x1b["); + push_u16(buf, y); + buf.push(b';'); + push_u16(buf, x); + buf.push(b'H'); +} + +// convert vt100 color to bytes, the 0, 1, 2 are different modes +#[inline] +fn color_to_bytes(c: vt100::Color) -> u32 { + match c { + vt100::Color::Default => 0, + vt100::Color::Idx(i) => 0x0100_0000 | ((i as u32) << 16), + vt100::Color::Rgb(r, g, b) => 0x0200_0000 | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32), + } +} + +// converts the colors as bytes to ANSI codes +#[inline] +fn u32_color_to_ansi(output: &mut Vec, color: u32, is_fg: bool) { + let color_type = color >> 24; + let val1 = ((color >> 16) & 0xFF) as u8; + let val2 = ((color >> 8) & 0xFF) as u8; + let val3 = (color & 0xFF) as u8; + + match color_type { + 0 => { + // Default + if is_fg { + output.extend_from_slice(b"\x1b[39m"); + } else { + output.extend_from_slice(b"\x1b[49m"); + } + } + 1 => { + // Indexed + if is_fg { + output.extend_from_slice(b"\x1b[38;5;"); + } else { + output.extend_from_slice(b"\x1b[48;5;"); + } + push_u8(output, val1); + output.push(b'm'); + } + 2 => { + // RGB + if is_fg { + output.extend_from_slice(b"\x1b[38;2;"); + } else { + output.extend_from_slice(b"\x1b[48;2;"); + } + push_u8(output, val1); + output.push(b';'); + push_u8(output, val2); + output.push(b';'); + push_u8(output, val3); + output.push(b'm'); + } + _ => {} + } +} diff --git a/daemon/src/layout.rs b/daemon/src/layout.rs index d39e583..8651012 100644 --- a/daemon/src/layout.rs +++ b/daemon/src/layout.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::BTreeMap; use crate::prelude::*; @@ -96,7 +96,7 @@ impl LayoutNode { } } - pub fn calculate_layout(&self, area: Rect, results: &mut HashMap) -> Result<()> { + pub fn calculate_layout(&self, area: Rect, results: &mut BTreeMap) -> Result<()> { match self { LayoutNode::Pane { id } => { results.insert(*id, area); diff --git a/daemon/src/main.rs b/daemon/src/main.rs index 206324d..0fb38fa 100644 --- a/daemon/src/main.rs +++ b/daemon/src/main.rs @@ -1,4 +1,5 @@ mod actors; +mod cell; mod control_signals; mod daemon; mod layout;