From 48da97d54d0f3379b60ea47e2757363b6f035946 Mon Sep 17 00:00:00 2001 From: Ryan-W31 Date: Sat, 13 Dec 2025 15:09:54 -0500 Subject: [PATCH 01/12] fix: pane splits --- cli/src/app.rs | 2 +- core/src/events.rs | 2 +- daemon/src/actors/client_connection.rs | 4 +-- daemon/src/actors/session.rs | 6 ++-- daemon/src/actors/session_manager.rs | 6 ++-- daemon/src/actors/window.rs | 46 +++++++++++++++++++------- 6 files changed, 44 insertions(+), 22 deletions(-) diff --git a/cli/src/app.rs b/cli/src/app.rs index 932817a..9e4aade 100644 --- a/cli/src/app.rs +++ b/cli/src/app.rs @@ -146,7 +146,7 @@ impl App { self.state.terminal.emulator.set_size(rows, cols); self.state.terminal.needs_resize = false; let (rows, cols) = self.state.terminal.size; - comm::send_event(&mut self.stream, CliEvent::TerminalResize { rows, cols }).await?; + comm::send_event(&mut self.stream, CliEvent::WindowResize { rows, cols }).await?; } tokio::select! { Some(input) = input_rx.recv() => { diff --git a/core/src/events.rs b/core/src/events.rs index d55bfad..15eb339 100644 --- a/core/src/events.rs +++ b/core/src/events.rs @@ -14,7 +14,7 @@ pub enum CliEvent { SwitchSession(String), // switch session - does nothing if session does not exist - TerminalResize { rows: u16, cols: u16 }, + WindowResize { rows: u16, cols: u16 }, Detach, } diff --git a/daemon/src/actors/client_connection.rs b/daemon/src/actors/client_connection.rs index d4d8a7a..9645011 100644 --- a/daemon/src/actors/client_connection.rs +++ b/daemon/src/actors/client_connection.rs @@ -145,8 +145,8 @@ impl ClientConnection { CliEvent::Raw(bytes) => { self.session_manager_handle.user_input(self.id, bytes).await.unwrap(); }, - CliEvent::TerminalResize{rows, cols} => { - self.session_manager_handle.terminal_resize(rows, cols).await.unwrap(); + CliEvent::WindowResize{rows, cols} => { + self.session_manager_handle.window_resize(rows, cols).await.unwrap(); }, CliEvent::Detach => { self.session_manager_handle.client_disconnect(self.id).await.unwrap(); diff --git a/daemon/src/actors/session.rs b/daemon/src/actors/session.rs index 6c0cf5c..5a88573 100644 --- a/daemon/src/actors/session.rs +++ b/daemon/src/actors/session.rs @@ -30,7 +30,7 @@ pub enum SessionEvent { // output WindowOutput(Bytes), - TerminalResize { rows: u16, cols: u16 }, + WindowResize { rows: u16, cols: u16 }, Kill, } use SessionEvent::*; @@ -102,8 +102,8 @@ impl Session { self.window_handle.kill().await.unwrap(); break; } - TerminalResize { rows, cols } => { - self.window_handle.terminal_resize(rows, cols).await.unwrap(); + WindowResize { rows, cols } => { + self.window_handle.window_resize(rows, cols).await.unwrap(); } RenameSession(name) => { let span = Span::current(); diff --git a/daemon/src/actors/session_manager.rs b/daemon/src/actors/session_manager.rs index 6704001..33c9322 100644 --- a/daemon/src/actors/session_manager.rs +++ b/daemon/src/actors/session_manager.rs @@ -58,7 +58,7 @@ pub enum SessionManagerEvent { session_id: u32, bytes: Bytes, }, - TerminalResize { + WindowResize { rows: u16, cols: u16, }, @@ -275,9 +275,9 @@ impl SessionManager { SessionSendOutput { session_id, bytes } => { self.handle_session_send_output(session_id, bytes).await.unwrap(); } - TerminalResize { rows, cols } => { + WindowResize { rows, cols } => { for SessionInfo { handle, .. } in self.state.sessions.values_mut() { - handle.terminal_resize(rows, cols).await.unwrap(); + handle.window_resize(rows, cols).await.unwrap(); } } } diff --git a/daemon/src/actors/window.rs b/daemon/src/actors/window.rs index ae318d3..bf80308 100644 --- a/daemon/src/actors/window.rs +++ b/daemon/src/actors/window.rs @@ -31,7 +31,7 @@ pub enum WindowEvent { }, KillPane, Redraw, - TerminalResize { + WindowResize { rows: u16, cols: u16, }, @@ -76,6 +76,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, @@ -146,17 +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(); - } + WindowResize { rows, cols } => { + self.handle_window_resize(rows, cols).await.unwrap(); } } } @@ -313,4 +305,34 @@ impl Window { Ok(()) } + async fn handle_window_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?; + } + } + + for pane in self.panes.values_mut() { + pane.resize(Rect { + x: 0, + y: 0, + width: cols, + height: rows, + }) + .await + .unwrap(); + } + + self.handle_redraw().await?; + Ok(()) + } } From fba51a8ca20144f8bc25bd9955ede5dcc4774a6f Mon Sep 17 00:00:00 2001 From: Ryan-W31 Date: Sat, 13 Dec 2025 17:25:05 -0500 Subject: [PATCH 02/12] feat: cells rerender --- daemon/src/actors/pane.rs | 82 +++++++++++++++++++++++------- daemon/src/cell.rs | 104 ++++++++++++++++++++++++++++++++++++++ daemon/src/main.rs | 1 + 3 files changed, 169 insertions(+), 18 deletions(-) create mode 100644 daemon/src/cell.rs diff --git a/daemon/src/actors/pane.rs b/daemon/src/actors/pane.rs index 1a8c298..b29e03c 100644 --- a/daemon/src/actors/pane.rs +++ b/daemon/src/actors/pane.rs @@ -1,4 +1,5 @@ use bytes::Bytes; +use color_eyre::owo_colors::OwoColorize; use handle_macro::Handle; use tokio::sync::mpsc; use tracing::Instrument; @@ -7,9 +8,7 @@ use crate::{ actors::{ pty::{Pty, PtyHandle}, window::WindowHandle, - }, - layout::Rect, - prelude::*, + }, cell::RemuxCell, layout::Rect, prelude::* }; #[derive(Handle, Debug)] @@ -38,6 +37,11 @@ pub struct Pane { rx: mpsc::Receiver, pane_state: PaneState, pty_handle: PtyHandle, + + // cells + curr_grid: Vec>, + prev_grid: Vec>, + // vte related vte: vt100::Parser, prev_screen_state: Option, @@ -53,6 +57,8 @@ impl Pane { let (tx, rx) = mpsc::channel(10); let handle = PaneHandle { tx }; + let curr_grid = vec![vec![RemuxCell::default(); rect.width as usize]; rect.height as usize]; + let prev_grid = vec![vec![RemuxCell::default(); rect.width as usize]; rect.height as usize]; let vte = vt100::Parser::new(rect.height, rect.width, 0); let pty_handle = Pty::spawn(handle.clone(), rect)?; Ok(Self { @@ -61,6 +67,8 @@ impl Pane { window_handle, pty_handle, rx, + curr_grid, + prev_grid, vte, pane_state: PaneState::Visible, prev_screen_state: None, @@ -151,26 +159,64 @@ impl Pane { } } - async fn handle_rerender(&mut self) -> Result<()> { - let screen = self.vte.screen(); - - trace!("RERENDER -- id: {} size {:?}", self.id, screen.size()); - self.prev_screen_state = Some(screen.clone()); - let mut output = Vec::new(); + // async fn handle_rerender(&mut self) -> Result<()> { + // let screen = self.vte.screen(); + // + // 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 erase_chars = format!("\x1b[{}X", self.rect.width); + // output.extend_from_slice(erase_chars.as_bytes()); + // output.extend_from_slice(&row); + // } + // + // output.extend_from_slice(b"\x1b[0m"); + // + // let (c_row, c_col) = screen.cursor_position(); + // let global_x = self.rect.x + 1 + c_col; + // let global_y = self.rect.y + 1 + c_row; + // + // self.window_handle + // .pane_output(self.id, Bytes::from(output), Some((global_x, global_y))) + // .await + // } - 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()); + async fn handle_rerender(&mut self) -> Result<()> { + let screen = self.vte.screen(); - let erase_chars = format!("\x1b[{}X", self.rect.width); - output.extend_from_slice(erase_chars.as_bytes()); - output.extend_from_slice(&row); + let rows = self.rect.height as usize; + let cols = self.rect.width as usize; + + let mut new_grid: Vec> = vec![vec![RemuxCell::default(); cols]; rows]; + + for r in 0..rows { + for c in 0..cols { + if let Some(cell) = screen.cell(r as u16, c as u16) { + new_grid[r][c] = RemuxCell { + contents: cell.contents().as_bytes().to_vec(), + fg_color: cell.fgcolor(), + bg_color: cell.bgcolor(), + bold: cell.bold(), + italic: cell.italic(), + underline: cell.underline(), + is_wide: cell.is_wide(), + is_wide_spacer: cell.is_wide_continuation() + } + } + } } - output.extend_from_slice(b"\x1b[0m"); + self.curr_grid = new_grid; + + let output = RemuxCell::render_diff(&self.prev_grid, &self.curr_grid, true); let (c_row, c_col) = screen.cursor_position(); let global_x = self.rect.x + 1 + c_col; diff --git a/daemon/src/cell.rs b/daemon/src/cell.rs new file mode 100644 index 0000000..6f9cbde --- /dev/null +++ b/daemon/src/cell.rs @@ -0,0 +1,104 @@ +use crate::prelude::*; +use std::io::Write; +const CONTENT_LENGTH: usize = 22; // size of vt100 cell content + +#[derive(Clone, Debug, PartialEq)] +pub struct RemuxCell { + pub contents: Vec, // change to fixed array + pub fg_color: vt100::Color, + pub bg_color: vt100::Color, + + // will switch to bit-packing later + pub bold: bool, + pub italic: bool, + pub underline: bool, + + pub is_wide: bool, + pub is_wide_spacer: bool, +} + +impl Default for RemuxCell { + fn default() -> Self { + Self { + contents: Default::default(), + fg_color: vt100::Color::Default, + bg_color: vt100::Color::Default, + bold: false, + italic: false, + underline: false, + is_wide: false, + is_wide_spacer: false, + } + } +} + +impl RemuxCell { + pub fn render_diff(prev_grid: &Vec>, curr_grid: &Vec>, is_rerender: bool) -> Vec { + let mut output = Vec::new(); + let mut current_fg_color = vt100::Color::Default; + let mut current_bg_color = vt100::Color::Default; + + let mut cursor_y = 0; + let mut cursor_x = 0; + let mut cursor_invalid = true; // Force a move on first draw + + for (r, row) in curr_grid.iter().enumerate() { + for (c, cell) in row.iter().enumerate() { + if cell.is_wide_spacer { + continue; + } + + // if the cell hasn't changed, skip it. + if !is_rerender && r < prev_grid.len() && c < prev_grid[0].len() { + if cell.eq(&prev_grid[r][c]) { + continue; + } + } + + // if the cursor is not currently at this cell, move it there + if cursor_invalid || cursor_y != r || cursor_x != c { + write!(output, "\x1b[{};{}H", r + 1, c + 1).unwrap(); // terminals are 1-indexed + cursor_y = r; + cursor_x = c; + cursor_invalid = false; + } + + if cell.fg_color != current_fg_color { + Self::write_sgr_color(&mut output, cell.fg_color, true).unwrap(); + current_fg_color = cell.fg_color; + } + + if cell.bg_color != current_bg_color { + Self::write_sgr_color(&mut output, cell.bg_color, false).unwrap(); + current_bg_color = cell.bg_color; + } + + let data = &cell.contents; + let len = data.iter().position(|&x| x == 0).unwrap_or(data.len()); + output.extend_from_slice(&data[..len]); + + cursor_x += if cell.is_wide { 2 } else { 1 }; + } + } + output.extend_from_slice(b"\x1b[0m"); + output + } + + fn write_sgr_color(output: &mut Vec, color: vt100::Color, is_fg: bool) -> Result<()> { + match color { + vt100::Color::Default => { + let code = if is_fg { 39 } else { 49 }; + write!(output, "\x1b[{}m", code).unwrap(); + } + vt100::Color::Idx(i) => { + let prefix = if is_fg { 38 } else { 48 }; + write!(output, "\x1b[{};5;{}m", prefix, i).unwrap(); + } + vt100::Color::Rgb(r, g, b) => { + let prefix = if is_fg { 38 } else { 48 }; + write!(output, "\x1b[{};2;{};{};{}m", prefix, r, g, b).unwrap(); + } + } + Ok(()) + } +} 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; From 6a00cdac347006c77e07285badd970637a8db7c1 Mon Sep 17 00:00:00 2001 From: Ryan-W31 Date: Sun, 14 Dec 2025 14:58:55 -0500 Subject: [PATCH 03/12] feat: splitting with cells --- daemon/src/actors/pane.rs | 7 ++----- daemon/src/cell.rs | 9 ++++++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/daemon/src/actors/pane.rs b/daemon/src/actors/pane.rs index b29e03c..8d0e033 100644 --- a/daemon/src/actors/pane.rs +++ b/daemon/src/actors/pane.rs @@ -40,7 +40,6 @@ pub struct Pane { // cells curr_grid: Vec>, - prev_grid: Vec>, // vte related vte: vt100::Parser, @@ -58,7 +57,6 @@ impl Pane { let handle = PaneHandle { tx }; let curr_grid = vec![vec![RemuxCell::default(); rect.width as usize]; rect.height as usize]; - let prev_grid = vec![vec![RemuxCell::default(); rect.width as usize]; rect.height as usize]; let vte = vt100::Parser::new(rect.height, rect.width, 0); let pty_handle = Pty::spawn(handle.clone(), rect)?; Ok(Self { @@ -68,7 +66,6 @@ impl Pane { pty_handle, rx, curr_grid, - prev_grid, vte, pane_state: PaneState::Visible, prev_screen_state: None, @@ -214,9 +211,9 @@ impl Pane { } } - self.curr_grid = new_grid; - let output = RemuxCell::render_diff(&self.prev_grid, &self.curr_grid, true); + let output = RemuxCell::render_diff(self.rect, &self.curr_grid, &new_grid, true); + self.curr_grid = new_grid; let (c_row, c_col) = screen.cursor_position(); let global_x = self.rect.x + 1 + c_col; diff --git a/daemon/src/cell.rs b/daemon/src/cell.rs index 6f9cbde..6325eb1 100644 --- a/daemon/src/cell.rs +++ b/daemon/src/cell.rs @@ -1,4 +1,4 @@ -use crate::prelude::*; +use crate::{layout::Rect, prelude::*}; use std::io::Write; const CONTENT_LENGTH: usize = 22; // size of vt100 cell content @@ -33,7 +33,7 @@ impl Default for RemuxCell { } impl RemuxCell { - pub fn render_diff(prev_grid: &Vec>, curr_grid: &Vec>, is_rerender: bool) -> Vec { + pub fn render_diff(rect: Rect, prev_grid: &Vec>, curr_grid: &Vec>, is_rerender: bool) -> Vec { let mut output = Vec::new(); let mut current_fg_color = vt100::Color::Default; let mut current_bg_color = vt100::Color::Default; @@ -55,9 +55,12 @@ impl RemuxCell { } } + let target_x = rect.x + 1 + c as u16; + let target_y = rect.y + 1 + r as u16; + // if the cursor is not currently at this cell, move it there if cursor_invalid || cursor_y != r || cursor_x != c { - write!(output, "\x1b[{};{}H", r + 1, c + 1).unwrap(); // terminals are 1-indexed + write!(output, "\x1b[{};{}H", target_y, target_x).unwrap(); // terminals are 1-indexed cursor_y = r; cursor_x = c; cursor_invalid = false; From a2587d1d24a37461be62b6e1e9860384d5fe9494 Mon Sep 17 00:00:00 2001 From: Ryan-W31 Date: Sun, 14 Dec 2025 17:08:34 -0500 Subject: [PATCH 04/12] feat: basic cell diffing --- daemon/src/actors/pane.rs | 29 +++++++++++++++++++++-------- daemon/src/actors/pty.rs | 4 ---- daemon/src/cell.rs | 12 +++++++----- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/daemon/src/actors/pane.rs b/daemon/src/actors/pane.rs index 8d0e033..da3e543 100644 --- a/daemon/src/actors/pane.rs +++ b/daemon/src/actors/pane.rs @@ -8,14 +8,13 @@ use crate::{ actors::{ pty::{Pty, PtyHandle}, window::WindowHandle, - }, cell::RemuxCell, layout::Rect, prelude::* + }, cell::{CONTENT_LENGTH, RemuxCell}, layout::Rect, prelude::* }; #[derive(Handle, Debug)] 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 }, @@ -39,6 +38,7 @@ pub struct Pane { pty_handle: PtyHandle, // cells + force_rerender: bool, curr_grid: Vec>, // vte related @@ -65,6 +65,7 @@ impl Pane { window_handle, pty_handle, rx, + force_rerender: true, curr_grid, vte, pane_state: PaneState::Visible, @@ -95,11 +96,9 @@ impl Pane { error!("Error while handling PTY output: {}", e); } } - PtyDied => { - break; - } Kill => { self.pty_handle.kill().await.unwrap(); + debug!("Pty died"); break; } Render => { @@ -197,8 +196,21 @@ impl Pane { for r in 0..rows { for c in 0..cols { if let Some(cell) = screen.cell(r as u16, c as u16) { + let content_str = cell.contents(); + let bytes = content_str.as_bytes(); + + let mut content_buf = [0u8; CONTENT_LENGTH]; + if bytes.is_empty() || bytes[0] == 0 { + content_buf[0] = b' '; + } else { + let len = bytes.len().min(CONTENT_LENGTH); + content_buf[..len].copy_from_slice(&bytes[..len]); + } + + let len = bytes.len().min(CONTENT_LENGTH); + content_buf[..len].copy_from_slice(&bytes[..len]); new_grid[r][c] = RemuxCell { - contents: cell.contents().as_bytes().to_vec(), + contents: content_buf, fg_color: cell.fgcolor(), bg_color: cell.bgcolor(), bold: cell.bold(), @@ -212,8 +224,9 @@ impl Pane { } - let output = RemuxCell::render_diff(self.rect, &self.curr_grid, &new_grid, true); + let output = RemuxCell::render_diff(self.rect, &self.curr_grid, &new_grid, self.force_rerender); self.curr_grid = new_grid; + self.force_rerender = false; let (c_row, c_col) = screen.cursor_position(); let global_x = self.rect.x + 1 + c_col; @@ -229,7 +242,7 @@ impl Pane { self.pty_handle.resize(rect).await?; self.vte.set_size(rect.height, rect.width); - self.handle_rerender().await?; + self.force_rerender = true; Ok(()) } } diff --git a/daemon/src/actors/pty.rs b/daemon/src/actors/pty.rs index 046aab8..3c2342f 100644 --- a/daemon/src/actors/pty.rs +++ b/daemon/src/actors/pty.rs @@ -164,10 +164,6 @@ 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/cell.rs b/daemon/src/cell.rs index 6325eb1..3c60cc3 100644 --- a/daemon/src/cell.rs +++ b/daemon/src/cell.rs @@ -1,10 +1,10 @@ use crate::{layout::Rect, prelude::*}; use std::io::Write; -const CONTENT_LENGTH: usize = 22; // size of vt100 cell content +pub const CONTENT_LENGTH: usize = 22; // size of vt100 cell content #[derive(Clone, Debug, PartialEq)] pub struct RemuxCell { - pub contents: Vec, // change to fixed array + pub contents: [u8; CONTENT_LENGTH], // change to fixed array pub fg_color: vt100::Color, pub bg_color: vt100::Color, @@ -19,8 +19,10 @@ pub struct RemuxCell { impl Default for RemuxCell { fn default() -> Self { + let mut content = [0u8; CONTENT_LENGTH]; + content[0] = b' '; Self { - contents: Default::default(), + contents: content, fg_color: vt100::Color::Default, bg_color: vt100::Color::Default, bold: false, @@ -33,7 +35,7 @@ impl Default for RemuxCell { } impl RemuxCell { - pub fn render_diff(rect: Rect, prev_grid: &Vec>, curr_grid: &Vec>, is_rerender: bool) -> Vec { + pub fn render_diff(rect: Rect, prev_grid: &Vec>, curr_grid: &Vec>, force_rerender: bool) -> Vec { let mut output = Vec::new(); let mut current_fg_color = vt100::Color::Default; let mut current_bg_color = vt100::Color::Default; @@ -49,7 +51,7 @@ impl RemuxCell { } // if the cell hasn't changed, skip it. - if !is_rerender && r < prev_grid.len() && c < prev_grid[0].len() { + if !force_rerender && r < prev_grid.len() && c < prev_grid[0].len() { if cell.eq(&prev_grid[r][c]) { continue; } From 317753a2d834aa90b71d216b0192a2d00daac51b Mon Sep 17 00:00:00 2001 From: Ryan-W31 Date: Sun, 14 Dec 2025 21:45:19 -0500 Subject: [PATCH 05/12] feat: fix splitting --- daemon/src/actors/pane.rs | 4 ---- daemon/src/actors/window.rs | 12 ------------ daemon/src/cell.rs | 35 +++++++++++++++++++++++++++++++++-- 3 files changed, 33 insertions(+), 18 deletions(-) diff --git a/daemon/src/actors/pane.rs b/daemon/src/actors/pane.rs index da3e543..ed8f5ca 100644 --- a/daemon/src/actors/pane.rs +++ b/daemon/src/actors/pane.rs @@ -1,5 +1,4 @@ use bytes::Bytes; -use color_eyre::owo_colors::OwoColorize; use handle_macro::Handle; use tokio::sync::mpsc; use tracing::Instrument; @@ -206,9 +205,6 @@ impl Pane { let len = bytes.len().min(CONTENT_LENGTH); content_buf[..len].copy_from_slice(&bytes[..len]); } - - let len = bytes.len().min(CONTENT_LENGTH); - content_buf[..len].copy_from_slice(&bytes[..len]); new_grid[r][c] = RemuxCell { contents: content_buf, fg_color: cell.fgcolor(), diff --git a/daemon/src/actors/window.rs b/daemon/src/actors/window.rs index bf80308..889e17b 100644 --- a/daemon/src/actors/window.rs +++ b/daemon/src/actors/window.rs @@ -1,7 +1,6 @@ use std::{collections::HashMap, mem}; use bytes::Bytes; -use crossterm::terminal; use handle_macro::Handle; use tokio::sync::mpsc; use tracing::Instrument; @@ -245,11 +244,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(()) } @@ -296,13 +290,7 @@ 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_window_resize(&mut self, rows: u16, cols: u16) -> Result<()> { diff --git a/daemon/src/cell.rs b/daemon/src/cell.rs index 3c60cc3..e688f5f 100644 --- a/daemon/src/cell.rs +++ b/daemon/src/cell.rs @@ -41,11 +41,42 @@ impl RemuxCell { let mut current_bg_color = vt100::Color::Default; let mut cursor_y = 0; - let mut cursor_x = 0; - let mut cursor_invalid = true; // Force a move on first draw + let mut cursor_x = 0; + let mut cursor_invalid = true; for (r, row) in curr_grid.iter().enumerate() { + // get the last useful column using a reverse path + let mut last_char_index = 0; + for (c, cell) in row.iter().enumerate().rev() { + let is_space = cell.contents[0] == b' '; + let is_default_bg = cell.bg_color == vt100::Color::Default; + let has_attributes = cell.bold || cell.italic || cell.underline || cell.is_wide || cell.is_wide_spacer; + + if !is_space || !is_default_bg || has_attributes { + last_char_index = c + 1; + break; + } + } + + // diff with forward pass for (c, cell) in row.iter().enumerate() { + if c >= last_char_index { + if cursor_invalid || cursor_y != r || cursor_x != c { + write!(output, "\x1b[{};{}H", rect.y + 1 + r as u16, rect.x + 1 + c as u16).unwrap(); + cursor_y = r; + cursor_x = c; + cursor_invalid = false; + } + + if current_bg_color != vt100::Color::Default { + write!(output, "\x1b[49m").unwrap(); + current_bg_color = vt100::Color::Default; + } + + output.extend_from_slice(b"\x1b[K"); + break; + } + if cell.is_wide_spacer { continue; } From 01d8587afae989681ab16f78c52671cafa3698f1 Mon Sep 17 00:00:00 2001 From: Ryan-W31 Date: Mon, 15 Dec 2025 18:32:57 -0500 Subject: [PATCH 06/12] feat: update cell diffing --- daemon/src/actors/pane.rs | 168 ++++++++++++++++-------------------- daemon/src/actors/window.rs | 42 +++------ daemon/src/cell.rs | 105 ++++++++++++---------- daemon/src/layout.rs | 4 +- 4 files changed, 148 insertions(+), 171 deletions(-) diff --git a/daemon/src/actors/pane.rs b/daemon/src/actors/pane.rs index ed8f5ca..1a4d97a 100644 --- a/daemon/src/actors/pane.rs +++ b/daemon/src/actors/pane.rs @@ -1,13 +1,18 @@ +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::{ actors::{ pty::{Pty, PtyHandle}, window::WindowHandle, - }, cell::{CONTENT_LENGTH, RemuxCell}, layout::Rect, prelude::* + }, + cell::{CONTENT_LENGTH, RemuxCell, SPACE}, + layout::Rect, + prelude::*, }; #[derive(Handle, Debug)] @@ -74,46 +79,71 @@ impl Pane { } 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 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; + } + Kill => { + self.pty_handle.kill().await.unwrap(); + debug!("Pty died"); + 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; } - } - Kill => { - self.pty_handle.kill().await.unwrap(); - debug!("Pty died"); - 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; } } } @@ -132,59 +162,10 @@ 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(); - // - // 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 erase_chars = format!("\x1b[{}X", self.rect.width); - // output.extend_from_slice(erase_chars.as_bytes()); - // output.extend_from_slice(&row); - // } - // - // output.extend_from_slice(b"\x1b[0m"); - // - // let (c_row, c_col) = screen.cursor_position(); - // let global_x = self.rect.x + 1 + c_col; - // let global_y = self.rect.y + 1 + c_row; - // - // self.window_handle - // .pane_output(self.id, Bytes::from(output), Some((global_x, global_y))) - // .await - // } - - - async fn handle_rerender(&mut self) -> Result<()> { let screen = self.vte.screen(); let rows = self.rect.height as usize; @@ -200,7 +181,7 @@ impl Pane { let mut content_buf = [0u8; CONTENT_LENGTH]; if bytes.is_empty() || bytes[0] == 0 { - content_buf[0] = b' '; + content_buf[0] = SPACE; } else { let len = bytes.len().min(CONTENT_LENGTH); content_buf[..len].copy_from_slice(&bytes[..len]); @@ -209,17 +190,16 @@ impl Pane { contents: content_buf, fg_color: cell.fgcolor(), bg_color: cell.bgcolor(), - bold: cell.bold(), - italic: cell.italic(), - underline: cell.underline(), - is_wide: cell.is_wide(), - is_wide_spacer: cell.is_wide_continuation() + // bold: cell.bold(), + // italic: cell.italic(), + // underline: cell.underline(), + // is_wide: cell.is_wide(), + // is_wide_spacer: cell.is_wide_continuation() } } } } - let output = RemuxCell::render_diff(self.rect, &self.curr_grid, &new_grid, self.force_rerender); self.curr_grid = new_grid; self.force_rerender = false; @@ -237,8 +217,6 @@ impl Pane { self.rect = rect; self.pty_handle.resize(rect).await?; self.vte.set_size(rect.height, rect.width); - - self.force_rerender = true; Ok(()) } } diff --git a/daemon/src/actors/window.rs b/daemon/src/actors/window.rs index 889e17b..de9d0c6 100644 --- a/daemon/src/actors/window.rs +++ b/daemon/src/actors/window.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, mem}; +use std::{collections::BTreeMap, mem}; use bytes::Bytes; use handle_macro::Handle; @@ -51,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, @@ -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, }) } @@ -182,18 +182,17 @@ 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() { + debug!("RENDERING PANE {}", pane.0); + 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 { @@ -264,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); @@ -276,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 @@ -301,7 +297,8 @@ impl Window { height: rows, }; - self.layout.calculate_layout(self.root_rect, &mut self.layout_sizing_map)?; + 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) { @@ -309,17 +306,6 @@ impl Window { } } - for pane in self.panes.values_mut() { - pane.resize(Rect { - x: 0, - y: 0, - width: cols, - height: rows, - }) - .await - .unwrap(); - } - self.handle_redraw().await?; Ok(()) } diff --git a/daemon/src/cell.rs b/daemon/src/cell.rs index e688f5f..f55e821 100644 --- a/daemon/src/cell.rs +++ b/daemon/src/cell.rs @@ -1,41 +1,48 @@ -use crate::{layout::Rect, prelude::*}; use std::io::Write; + +use crate::{layout::Rect, prelude::*}; + pub const CONTENT_LENGTH: usize = 22; // size of vt100 cell content +pub const SPACE: u8 = 0x20; #[derive(Clone, Debug, PartialEq)] pub struct RemuxCell { pub contents: [u8; CONTENT_LENGTH], // change to fixed array pub fg_color: vt100::Color, pub bg_color: vt100::Color, - // will switch to bit-packing later - pub bold: bool, - pub italic: bool, - pub underline: bool, - - pub is_wide: bool, - pub is_wide_spacer: bool, + // pub bold: bool, + // pub italic: bool, + // pub underline: bool, + // + // pub is_wide: bool, + // pub is_wide_spacer: bool, } impl Default for RemuxCell { fn default() -> Self { let mut content = [0u8; CONTENT_LENGTH]; - content[0] = b' '; + content[0] = SPACE; Self { contents: content, fg_color: vt100::Color::Default, bg_color: vt100::Color::Default, - bold: false, - italic: false, - underline: false, - is_wide: false, - is_wide_spacer: false, + // bold: false, + // italic: false, + // underline: false, + // is_wide: false, + // is_wide_spacer: false, } } } impl RemuxCell { - pub fn render_diff(rect: Rect, prev_grid: &Vec>, curr_grid: &Vec>, force_rerender: bool) -> Vec { + pub fn render_diff( + rect: Rect, + prev_grid: &Vec>, + curr_grid: &Vec>, + force_rerender: bool, + ) -> Vec { let mut output = Vec::new(); let mut current_fg_color = vt100::Color::Default; let mut current_bg_color = vt100::Color::Default; @@ -44,44 +51,51 @@ impl RemuxCell { let mut cursor_x = 0; let mut cursor_invalid = true; - for (r, row) in curr_grid.iter().enumerate() { - // get the last useful column using a reverse path - let mut last_char_index = 0; - for (c, cell) in row.iter().enumerate().rev() { - let is_space = cell.contents[0] == b' '; - let is_default_bg = cell.bg_color == vt100::Color::Default; - let has_attributes = cell.bold || cell.italic || cell.underline || cell.is_wide || cell.is_wide_spacer; + let rows = rect.height as usize; + let cols = rect.width as usize; - if !is_space || !is_default_bg || has_attributes { + for r in 0..rows { + let mut last_char_index = 0; + for c in (0..cols).rev() { + let cell = &curr_grid[r][c]; + if cell.contents[0] != SPACE || cell.bg_color != vt100::Color::Default { last_char_index = c + 1; break; } } - // diff with forward pass - for (c, cell) in row.iter().enumerate() { + for c in 0..cols { if c >= last_char_index { - if cursor_invalid || cursor_y != r || cursor_x != c { - write!(output, "\x1b[{};{}H", rect.y + 1 + r as u16, rect.x + 1 + c as u16).unwrap(); - cursor_y = r; - cursor_x = c; - cursor_invalid = false; - } - - if current_bg_color != vt100::Color::Default { - write!(output, "\x1b[49m").unwrap(); - current_bg_color = vt100::Color::Default; + let prev_has_content = if r < prev_grid.len() && c < prev_grid[0].len() { + let prev_cell = &prev_grid[r][c]; + prev_cell.contents[0] != SPACE || prev_cell.bg_color != vt100::Color::Default + } else { + true + }; + + if force_rerender || prev_has_content { + if current_bg_color != vt100::Color::Default { + Self::write_sgr_color(&mut output, vt100::Color::Default, false).unwrap(); + current_bg_color = vt100::Color::Default; + } + + let target_x = rect.x + 1 + c as u16; + let target_y = rect.y + 1 + r as u16; + write!(output, "\x1b[{};{}H", target_y, target_x).unwrap(); + + let count = cols - c; + + write!(output, "\x1b[{}X", count).unwrap(); + + cursor_invalid = true; + break; + } else { + continue; } - - output.extend_from_slice(b"\x1b[K"); - break; } - if cell.is_wide_spacer { - continue; - } + let cell = &curr_grid[r][c]; - // if the cell hasn't changed, skip it. if !force_rerender && r < prev_grid.len() && c < prev_grid[0].len() { if cell.eq(&prev_grid[r][c]) { continue; @@ -91,9 +105,8 @@ impl RemuxCell { let target_x = rect.x + 1 + c as u16; let target_y = rect.y + 1 + r as u16; - // if the cursor is not currently at this cell, move it there if cursor_invalid || cursor_y != r || cursor_x != c { - write!(output, "\x1b[{};{}H", target_y, target_x).unwrap(); // terminals are 1-indexed + write!(output, "\x1b[{};{}H", target_y, target_x).unwrap(); cursor_y = r; cursor_x = c; cursor_invalid = false; @@ -109,11 +122,11 @@ impl RemuxCell { current_bg_color = cell.bg_color; } - let data = &cell.contents; + let data = &cell.contents; let len = data.iter().position(|&x| x == 0).unwrap_or(data.len()); output.extend_from_slice(&data[..len]); - cursor_x += if cell.is_wide { 2 } else { 1 }; + cursor_x += 1; } } output.extend_from_slice(b"\x1b[0m"); 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); From 8a4025ef69b377036c39f8d5c685964e4d7a4ce7 Mon Sep 17 00:00:00 2001 From: Ryan-W31 Date: Mon, 15 Dec 2025 21:56:36 -0500 Subject: [PATCH 07/12] feat: cell optimization --- Cargo.lock | 110 +++++++++-------- Cargo.toml | 2 +- daemon/src/actors/pane.rs | 29 ++--- daemon/src/actors/window.rs | 1 - daemon/src/cell.rs | 227 ++++++++++++++++++++++++++++-------- 5 files changed, 244 insertions(+), 125 deletions(-) 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/pane.rs b/daemon/src/actors/pane.rs index 1a4d97a..9c516e2 100644 --- a/daemon/src/actors/pane.rs +++ b/daemon/src/actors/pane.rs @@ -10,7 +10,7 @@ use crate::{ pty::{Pty, PtyHandle}, window::WindowHandle, }, - cell::{CONTENT_LENGTH, RemuxCell, SPACE}, + cell::RemuxCell, layout::Rect, prelude::*, }; @@ -47,7 +47,6 @@ pub struct Pane { // vte related vte: vt100::Parser, - prev_screen_state: Option, rect: Rect, } impl Pane { @@ -73,7 +72,6 @@ impl Pane { curr_grid, vte, pane_state: PaneState::Visible, - prev_screen_state: None, rect, }) } @@ -176,26 +174,17 @@ impl Pane { for r in 0..rows { for c in 0..cols { if let Some(cell) = screen.cell(r as u16, c as u16) { + let mut remux_cell = RemuxCell::default(); + let content_str = cell.contents(); let bytes = content_str.as_bytes(); - let mut content_buf = [0u8; CONTENT_LENGTH]; - if bytes.is_empty() || bytes[0] == 0 { - content_buf[0] = SPACE; - } else { - let len = bytes.len().min(CONTENT_LENGTH); - content_buf[..len].copy_from_slice(&bytes[..len]); - } - new_grid[r][c] = RemuxCell { - contents: content_buf, - fg_color: cell.fgcolor(), - bg_color: cell.bgcolor(), - // bold: cell.bold(), - // italic: cell.italic(), - // underline: cell.underline(), - // is_wide: cell.is_wide(), - // is_wide_spacer: cell.is_wide_continuation() - } + remux_cell.set_content(bytes); + remux_cell.set_fg_color(cell.fgcolor()); + remux_cell.set_bg_color(cell.bgcolor()); + remux_cell.set_attributes_from_vt100(cell); + + new_grid[r][c] = remux_cell; } } } diff --git a/daemon/src/actors/window.rs b/daemon/src/actors/window.rs index de9d0c6..a871476 100644 --- a/daemon/src/actors/window.rs +++ b/daemon/src/actors/window.rs @@ -183,7 +183,6 @@ impl Window { } async fn handle_redraw(&mut self) -> Result<()> { for pane in self.panes.iter() { - debug!("RENDERING PANE {}", pane.0); pane.1.rerender().await?; } Ok(()) diff --git a/daemon/src/cell.rs b/daemon/src/cell.rs index f55e821..bce7fd9 100644 --- a/daemon/src/cell.rs +++ b/daemon/src/cell.rs @@ -1,38 +1,64 @@ use std::io::Write; -use crate::{layout::Rect, prelude::*}; +use bytes::buf; + +use crate::layout::Rect; pub const CONTENT_LENGTH: usize = 22; // size of vt100 cell content pub const SPACE: u8 = 0x20; -#[derive(Clone, Debug, PartialEq)] +// 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 { - pub contents: [u8; CONTENT_LENGTH], // change to fixed array - pub fg_color: vt100::Color, - pub bg_color: vt100::Color, - // will switch to bit-packing later - // pub bold: bool, - // pub italic: bool, - // pub underline: bool, - // - // pub is_wide: bool, - // pub is_wide_spacer: bool, + contents: [u8; CONTENT_LENGTH], // 22 Bytes + len: u8, // 1 byte + attributes: u8, // 1 byte + + // format: [Type: u8] [R: u8] [G: u8] [B: u8] + fg_color: u32, // 4 bytes + bg_color: u32, // 4 bytes } +// verify each cell is 32 bytes +const _: () = assert!(std::mem::size_of::() == 32); + impl Default for RemuxCell { fn default() -> Self { let mut content = [0u8; CONTENT_LENGTH]; content[0] = SPACE; Self { contents: content, - fg_color: vt100::Color::Default, - bg_color: vt100::Color::Default, - // bold: false, - // italic: false, - // underline: false, - // is_wide: false, - // is_wide_spacer: false, + len: 1, + fg_color: Self::color_to_bytes(vt100::Color::Default), + bg_color: Self::color_to_bytes(vt100::Color::Default), + attributes: 0, + } + } +} + +impl PartialEq for RemuxCell { + fn eq(&self, other: &Self) -> bool { + if self.len != other.len { + return false; + } + if self.fg_color != other.fg_color || self.bg_color != other.bg_color { + return false; + } + if self.attributes != other.attributes { + return false; } + let len = self.len(); + self.contents[..len] == other.contents[..len] } } @@ -43,9 +69,12 @@ impl RemuxCell { curr_grid: &Vec>, force_rerender: bool, ) -> Vec { - let mut output = Vec::new(); - let mut current_fg_color = vt100::Color::Default; - let mut current_bg_color = vt100::Color::Default; + let mut output = Vec::with_capacity((rect.width as usize * rect.height as usize) * 4); + + let default_color_bytes = Self::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; @@ -54,11 +83,18 @@ impl RemuxCell { let rows = rect.height as usize; let cols = rect.width as usize; + const VISIBLE_ON_WHITESPACE: u8 = INVERSE | UNDERLINE; + for r in 0..rows { let mut last_char_index = 0; for c in (0..cols).rev() { let cell = &curr_grid[r][c]; - if cell.contents[0] != SPACE || cell.bg_color != vt100::Color::Default { + + 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; } @@ -68,15 +104,21 @@ impl RemuxCell { 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][c]; - prev_cell.contents[0] != SPACE || prev_cell.bg_color != vt100::Color::Default + 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 force_rerender || prev_has_content { - if current_bg_color != vt100::Color::Default { - Self::write_sgr_color(&mut output, vt100::Color::Default, false).unwrap(); - current_bg_color = vt100::Color::Default; + 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; @@ -84,7 +126,6 @@ impl RemuxCell { write!(output, "\x1b[{};{}H", target_y, target_x).unwrap(); let count = cols - c; - write!(output, "\x1b[{}X", count).unwrap(); cursor_invalid = true; @@ -96,15 +137,18 @@ impl RemuxCell { let cell = &curr_grid[r][c]; + if cell.has_attribute(WIDE_SPACER) { + continue; + } + if !force_rerender && r < prev_grid.len() && c < prev_grid[0].len() { - if cell.eq(&prev_grid[r][c]) { + if cell == &prev_grid[r][c] { continue; } } 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 { write!(output, "\x1b[{};{}H", target_y, target_x).unwrap(); cursor_y = r; @@ -112,42 +156,133 @@ impl RemuxCell { cursor_invalid = false; } - if cell.fg_color != current_fg_color { - Self::write_sgr_color(&mut output, cell.fg_color, true).unwrap(); - current_fg_color = cell.fg_color; - } + if cell.attributes != current_attributes { + output.extend_from_slice(b"\x1b[0"); + Self::get_attributes_to_ansi(&mut output, cell); + output.push(b'm'); - if cell.bg_color != current_bg_color { - Self::write_sgr_color(&mut output, cell.bg_color, false).unwrap(); - current_bg_color = cell.bg_color; + current_attributes = cell.attributes; + + current_bg_color = default_color_bytes; + current_fg_color = default_color_bytes; + + if cell.fg_color != current_fg_color { + Self::write_packed_color(&mut output, cell.fg_color, true); + } + + if cell.bg_color != current_bg_color { + Self::write_packed_color(&mut output, cell.bg_color, false); + } + } else { + if cell.fg_color != current_fg_color { + Self::write_packed_color(&mut output, cell.fg_color, true); + current_fg_color = cell.fg_color; + } + if cell.bg_color != current_bg_color { + Self::write_packed_color(&mut output, cell.bg_color, false); + current_bg_color = cell.bg_color; + } } let data = &cell.contents; let len = data.iter().position(|&x| x == 0).unwrap_or(data.len()); output.extend_from_slice(&data[..len]); - cursor_x += 1; + cursor_x += if cell.has_attribute(WIDE) { 2 } else { 1 }; } } output.extend_from_slice(b"\x1b[0m"); output } - fn write_sgr_color(output: &mut Vec, color: vt100::Color, is_fg: bool) -> Result<()> { - match color { - vt100::Color::Default => { + 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), + } + } + + fn write_packed_color(output: &mut Vec, color: u32, is_fg: bool) { + let color_type = color >> 24; + let val1 = (color >> 16) & 0xFF; + let val2 = (color >> 8) & 0xFF; + let val3 = color & 0xFF; + + match color_type { + 0 => { let code = if is_fg { 39 } else { 49 }; write!(output, "\x1b[{}m", code).unwrap(); } - vt100::Color::Idx(i) => { + 1 => { let prefix = if is_fg { 38 } else { 48 }; - write!(output, "\x1b[{};5;{}m", prefix, i).unwrap(); + write!(output, "\x1b[{};5;{}m", prefix, val1).unwrap(); } - vt100::Color::Rgb(r, g, b) => { + 2 => { let prefix = if is_fg { 38 } else { 48 }; - write!(output, "\x1b[{};2;{};{};{}m", prefix, r, g, b).unwrap(); + write!(output, "\x1b[{};2;{};{};{}m", prefix, val1, val2, val3).unwrap(); } + _ => {} + } + } + + fn get_attributes_to_ansi(buffer: &mut Vec, cell: &RemuxCell) { + if cell.has_attribute(BOLD) { + buffer.extend_from_slice(b";1"); } - Ok(()) + 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]) { + if contents.is_empty() || contents[0] == 0 { + self.contents[0] = SPACE; + } 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 = Self::color_to_bytes(color); + } + + pub fn set_bg_color(&mut self, color: vt100::Color) { + self.bg_color = Self::color_to_bytes(color); } } From d2412aa892592d7ee5a6a5d88a24aa4a7bb1a9ff Mon Sep 17 00:00:00 2001 From: Ryan-W31 Date: Tue, 16 Dec 2025 12:39:43 -0500 Subject: [PATCH 08/12] perf: optimizations --- daemon/src/actors/pane.rs | 52 +++++---- daemon/src/cell.rs | 223 ++++++++++++++++++++++++++------------ 2 files changed, 188 insertions(+), 87 deletions(-) diff --git a/daemon/src/actors/pane.rs b/daemon/src/actors/pane.rs index 9c516e2..d83b5d5 100644 --- a/daemon/src/actors/pane.rs +++ b/daemon/src/actors/pane.rs @@ -43,7 +43,8 @@ pub struct Pane { // cells force_rerender: bool, - curr_grid: Vec>, + curr_grid: Vec, + prev_grid: Vec, // vte related vte: vt100::Parser, @@ -59,7 +60,10 @@ impl Pane { let (tx, rx) = mpsc::channel(10); let handle = PaneHandle { tx }; - let curr_grid = vec![vec![RemuxCell::default(); rect.width as usize]; rect.height as usize]; + 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 { @@ -70,6 +74,7 @@ impl Pane { rx, force_rerender: true, curr_grid, + prev_grid, vte, pane_state: PaneState::Visible, rect, @@ -87,11 +92,13 @@ impl Pane { loop { tokio::select! { _ = render_ticker.tick() => { - if self.force_rerender || is_dirty { - if let Err(e) = self.handle_render().await { - error!("Failed to render frame: {e}") + 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; } - is_dirty = false; } } event_result = self.rx.recv() => { @@ -147,7 +154,7 @@ impl Pane { } } } - .in_current_span(), + .in_current_span(), ); Ok(handle_clone) @@ -165,32 +172,34 @@ impl Pane { async fn handle_render(&mut self) -> Result<()> { let screen = self.vte.screen(); - let rows = self.rect.height as usize; let cols = self.rect.width as usize; - let mut new_grid: Vec> = vec![vec![RemuxCell::default(); cols]; rows]; + 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()); + } 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) { - let mut remux_cell = RemuxCell::default(); - - let content_str = cell.contents(); - let bytes = content_str.as_bytes(); - - remux_cell.set_content(bytes); + 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); - - new_grid[r][c] = remux_cell; + } else { + *remux_cell = RemuxCell::default(); } } } - let output = RemuxCell::render_diff(self.rect, &self.curr_grid, &new_grid, self.force_rerender); - self.curr_grid = new_grid; + 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(); @@ -204,6 +213,11 @@ 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); Ok(()) diff --git a/daemon/src/cell.rs b/daemon/src/cell.rs index bce7fd9..654a08b 100644 --- a/daemon/src/cell.rs +++ b/daemon/src/cell.rs @@ -1,7 +1,3 @@ -use std::io::Write; - -use bytes::buf; - use crate::layout::Rect; pub const CONTENT_LENGTH: usize = 22; // size of vt100 cell content @@ -23,15 +19,14 @@ pub struct RemuxCell { contents: [u8; CONTENT_LENGTH], // 22 Bytes len: u8, // 1 byte attributes: u8, // 1 byte - - // format: [Type: u8] [R: u8] [G: u8] [B: u8] - fg_color: u32, // 4 bytes - bg_color: u32, // 4 bytes + 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]; @@ -39,39 +34,39 @@ impl Default for RemuxCell { Self { contents: content, len: 1, - fg_color: Self::color_to_bytes(vt100::Color::Default), - bg_color: Self::color_to_bytes(vt100::Color::Default), + 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 { - return false; - } - if self.fg_color != other.fg_color || self.bg_color != other.bg_color { - return false; - } - if self.attributes != other.attributes { + if self.len != other.len + || self.fg_color != other.fg_color + || self.bg_color != other.bg_color + || self.attributes != other.attributes { return false; } - let len = self.len(); - self.contents[..len] == other.contents[..len] + + self.contents == other.contents } } impl RemuxCell { pub fn render_diff( rect: Rect, - prev_grid: &Vec>, - curr_grid: &Vec>, + prev_grid: &[RemuxCell], // 2D grid flattened to 1D + curr_grid: &[RemuxCell], // 2D grid flattened to 1D force_rerender: bool, ) -> Vec { - let mut output = Vec::with_capacity((rect.width as usize * rect.height as usize) * 4); + // average 10 bytes per cell + let mut output = Vec::with_capacity((rect.width as usize * rect.height as usize) * 10); - let default_color_bytes = Self::color_to_bytes(vt100::Color::Default); + // 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; @@ -86,9 +81,12 @@ impl RemuxCell { 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][c]; + let cell = &curr_grid[r * cols + c]; let is_visually_empty = cell.contents[0] == SPACE && cell.bg_color == default_color_bytes @@ -100,10 +98,13 @@ impl RemuxCell { } } + // 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][c]; + let prev_cell = &prev_grid[r * cols + c]; let prev_bg_default = prev_cell.bg_color == default_color_bytes; prev_cell.contents[0] != SPACE @@ -113,6 +114,9 @@ impl RemuxCell { 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"); @@ -123,10 +127,12 @@ impl RemuxCell { let target_x = rect.x + 1 + c as u16; let target_y = rect.y + 1 + r as u16; - write!(output, "\x1b[{};{}H", target_y, target_x).unwrap(); + set_cursor_position(&mut output, target_x, target_y); let count = cols - c; - write!(output, "\x1b[{}X", count).unwrap(); + output.extend_from_slice(b"\x1b["); + push_u16(&mut output, count as u16); + output.push(b'X'); cursor_invalid = true; break; @@ -135,27 +141,33 @@ impl RemuxCell { } } - let cell = &curr_grid[r][c]; - + // 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][c] { + 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 { - write!(output, "\x1b[{};{}H", target_y, target_x).unwrap(); + 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); @@ -167,65 +179,41 @@ impl RemuxCell { current_fg_color = default_color_bytes; if cell.fg_color != current_fg_color { - Self::write_packed_color(&mut output, cell.fg_color, true); + u32_color_to_ansi(&mut output, cell.fg_color, true); } if cell.bg_color != current_bg_color { - Self::write_packed_color(&mut output, cell.bg_color, false); + u32_color_to_ansi(&mut output, cell.bg_color, false); } } else { if cell.fg_color != current_fg_color { - Self::write_packed_color(&mut output, cell.fg_color, true); + u32_color_to_ansi(&mut output, cell.fg_color, true); current_fg_color = cell.fg_color; } if cell.bg_color != current_bg_color { - Self::write_packed_color(&mut output, cell.bg_color, false); + 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 = data.iter().position(|&x| x == 0).unwrap_or(data.len()); + 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 } - 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), - } - } - fn write_packed_color(output: &mut Vec, color: u32, is_fg: bool) { - let color_type = color >> 24; - let val1 = (color >> 16) & 0xFF; - let val2 = (color >> 8) & 0xFF; - let val3 = color & 0xFF; - - match color_type { - 0 => { - let code = if is_fg { 39 } else { 49 }; - write!(output, "\x1b[{}m", code).unwrap(); - } - 1 => { - let prefix = if is_fg { 38 } else { 48 }; - write!(output, "\x1b[{};5;{}m", prefix, val1).unwrap(); - } - 2 => { - let prefix = if is_fg { 38 } else { 48 }; - write!(output, "\x1b[{};2;{};{};{}m", prefix, val1, val2, val3).unwrap(); - } - _ => {} - } - } + // 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"); @@ -241,7 +229,6 @@ impl RemuxCell { } } } - // setters and getters impl RemuxCell { pub fn set_attributes_from_vt100(&mut self, cell: &vt100::Cell) { @@ -269,8 +256,11 @@ impl RemuxCell { } 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]); @@ -279,10 +269,107 @@ impl RemuxCell { } pub fn set_fg_color(&mut self, color: vt100::Color) { - self.fg_color = Self::color_to_bytes(color); + self.fg_color = color_to_bytes(color); } pub fn set_bg_color(&mut self, color: vt100::Color) { - self.bg_color = Self::color_to_bytes(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'); + } + _ => {} + } +} + From f0c78325dd8f60f2a5a128129dcc7463e869a0d0 Mon Sep 17 00:00:00 2001 From: Ryan-W31 Date: Wed, 17 Dec 2025 16:55:38 -0500 Subject: [PATCH 09/12] chore: formatting --- daemon/src/actors/pane.rs | 2 +- daemon/src/cell.rs | 40 +++++++++++++++++++++++---------------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/daemon/src/actors/pane.rs b/daemon/src/actors/pane.rs index d83b5d5..120e86f 100644 --- a/daemon/src/actors/pane.rs +++ b/daemon/src/actors/pane.rs @@ -154,7 +154,7 @@ impl Pane { } } } - .in_current_span(), + .in_current_span(), ); Ok(handle_clone) diff --git a/daemon/src/cell.rs b/daemon/src/cell.rs index 654a08b..2043754 100644 --- a/daemon/src/cell.rs +++ b/daemon/src/cell.rs @@ -44,10 +44,11 @@ impl Default for RemuxCell { // equality checks integers first, then compares contents impl PartialEq for RemuxCell { fn eq(&self, other: &Self) -> bool { - if self.len != other.len + if self.len != other.len || self.fg_color != other.fg_color || self.bg_color != other.bg_color - || self.attributes != other.attributes { + || self.attributes != other.attributes + { return false; } @@ -81,7 +82,6 @@ impl RemuxCell { 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; @@ -211,8 +211,6 @@ impl RemuxCell { output } - - // adds all attributes from RemuxCell as ANSI codes fn get_attributes_to_ansi(buffer: &mut Vec, cell: &RemuxCell) { if cell.has_attribute(BOLD) { @@ -277,7 +275,6 @@ impl RemuxCell { } } - // helper functions // push_u8 is faster than write!() due to under-the-hood Rust stuff #[inline] @@ -349,19 +346,31 @@ fn u32_color_to_ansi(output: &mut Vec, color: u32, is_fg: bool) { 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"); } + 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;"); } + 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;"); } + 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); @@ -372,4 +381,3 @@ fn u32_color_to_ansi(output: &mut Vec, color: u32, is_fg: bool) { _ => {} } } - From 8eed7cc57c86655c11b8517db2903c73073b8ac7 Mon Sep 17 00:00:00 2001 From: Prometheus1400 Date: Wed, 17 Dec 2025 17:14:12 -0800 Subject: [PATCH 10/12] chunk client response --- daemon/src/actors/client_connection.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/daemon/src/actors/client_connection.rs b/daemon/src/actors/client_connection.rs index 9645011..4a62617 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(); From 70274eb6fc29a55658c1eb6a5ca46e9cbbf744b0 Mon Sep 17 00:00:00 2001 From: Ryan-W31 Date: Wed, 17 Dec 2025 20:45:16 -0500 Subject: [PATCH 11/12] refactor: terminal resize --- cli/src/app.rs | 2 +- core/src/events.rs | 2 +- daemon/src/actors/client_connection.rs | 4 ++-- daemon/src/actors/session.rs | 6 +++--- daemon/src/actors/session_manager.rs | 6 +++--- daemon/src/actors/window.rs | 8 ++++---- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/cli/src/app.rs b/cli/src/app.rs index 9e4aade..932817a 100644 --- a/cli/src/app.rs +++ b/cli/src/app.rs @@ -146,7 +146,7 @@ impl App { self.state.terminal.emulator.set_size(rows, cols); self.state.terminal.needs_resize = false; let (rows, cols) = self.state.terminal.size; - comm::send_event(&mut self.stream, CliEvent::WindowResize { rows, cols }).await?; + comm::send_event(&mut self.stream, CliEvent::TerminalResize { rows, cols }).await?; } tokio::select! { Some(input) = input_rx.recv() => { diff --git a/core/src/events.rs b/core/src/events.rs index 15eb339..d55bfad 100644 --- a/core/src/events.rs +++ b/core/src/events.rs @@ -14,7 +14,7 @@ pub enum CliEvent { SwitchSession(String), // switch session - does nothing if session does not exist - WindowResize { rows: u16, cols: u16 }, + TerminalResize { rows: u16, cols: u16 }, Detach, } diff --git a/daemon/src/actors/client_connection.rs b/daemon/src/actors/client_connection.rs index 4a62617..024eaac 100644 --- a/daemon/src/actors/client_connection.rs +++ b/daemon/src/actors/client_connection.rs @@ -149,8 +149,8 @@ impl ClientConnection { CliEvent::Raw(bytes) => { self.session_manager_handle.user_input(self.id, bytes).await.unwrap(); }, - CliEvent::WindowResize{rows, cols} => { - self.session_manager_handle.window_resize(rows, cols).await.unwrap(); + CliEvent::TerminalResize{rows, cols} => { + self.session_manager_handle.terminal_resize(rows, cols).await.unwrap(); }, CliEvent::Detach => { self.session_manager_handle.client_disconnect(self.id).await.unwrap(); diff --git a/daemon/src/actors/session.rs b/daemon/src/actors/session.rs index 5a88573..6c0cf5c 100644 --- a/daemon/src/actors/session.rs +++ b/daemon/src/actors/session.rs @@ -30,7 +30,7 @@ pub enum SessionEvent { // output WindowOutput(Bytes), - WindowResize { rows: u16, cols: u16 }, + TerminalResize { rows: u16, cols: u16 }, Kill, } use SessionEvent::*; @@ -102,8 +102,8 @@ impl Session { self.window_handle.kill().await.unwrap(); break; } - WindowResize { rows, cols } => { - self.window_handle.window_resize(rows, cols).await.unwrap(); + TerminalResize { rows, cols } => { + self.window_handle.terminal_resize(rows, cols).await.unwrap(); } RenameSession(name) => { let span = Span::current(); diff --git a/daemon/src/actors/session_manager.rs b/daemon/src/actors/session_manager.rs index 33c9322..6704001 100644 --- a/daemon/src/actors/session_manager.rs +++ b/daemon/src/actors/session_manager.rs @@ -58,7 +58,7 @@ pub enum SessionManagerEvent { session_id: u32, bytes: Bytes, }, - WindowResize { + TerminalResize { rows: u16, cols: u16, }, @@ -275,9 +275,9 @@ impl SessionManager { SessionSendOutput { session_id, bytes } => { self.handle_session_send_output(session_id, bytes).await.unwrap(); } - WindowResize { rows, cols } => { + TerminalResize { rows, cols } => { for SessionInfo { handle, .. } in self.state.sessions.values_mut() { - handle.window_resize(rows, cols).await.unwrap(); + handle.terminal_resize(rows, cols).await.unwrap(); } } } diff --git a/daemon/src/actors/window.rs b/daemon/src/actors/window.rs index a871476..4f4690d 100644 --- a/daemon/src/actors/window.rs +++ b/daemon/src/actors/window.rs @@ -30,7 +30,7 @@ pub enum WindowEvent { }, KillPane, Redraw, - WindowResize { + TerminalResize { rows: u16, cols: u16, }, @@ -146,8 +146,8 @@ impl Window { } break; } - WindowResize { rows, cols } => { - self.handle_window_resize(rows, cols).await.unwrap(); + TerminalResize { rows, cols } => { + self.handle_terminal_resize(rows, cols).await.unwrap(); } } } @@ -288,7 +288,7 @@ impl Window { self.handle_redraw().await?; Ok(()) } - async fn handle_window_resize(&mut self, rows: u16, cols: u16) -> Result<()> { + async fn handle_terminal_resize(&mut self, rows: u16, cols: u16) -> Result<()> { self.root_rect = Rect { x: 0, y: 0, From a8ad3d27aa2f88fe6c9d31ecb0b1fc5a4571f89b Mon Sep 17 00:00:00 2001 From: Ryan-W31 Date: Wed, 17 Dec 2025 21:11:47 -0500 Subject: [PATCH 12/12] feat: add back pty exit logic --- daemon/src/actors/pane.rs | 8 +++++++- daemon/src/actors/pty.rs | 6 ++++++ daemon/src/actors/window.rs | 3 ++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/daemon/src/actors/pane.rs b/daemon/src/actors/pane.rs index 120e86f..d927d82 100644 --- a/daemon/src/actors/pane.rs +++ b/daemon/src/actors/pane.rs @@ -22,6 +22,7 @@ pub enum PaneEvent { 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, @@ -122,9 +123,14 @@ impl Pane { } 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"); + debug!("Pty died via pane kill"); break; } Render => { diff --git a/daemon/src/actors/pty.rs b/daemon/src/actors/pty.rs index 3c2342f..dcf343b 100644 --- a/daemon/src/actors/pty.rs +++ b/daemon/src/actors/pty.rs @@ -164,6 +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 4f4690d..a6b7adb 100644 --- a/daemon/src/actors/window.rs +++ b/daemon/src/actors/window.rs @@ -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,6 +147,7 @@ impl Window { break; } TerminalResize { rows, cols } => { + debug!("Window: TerminalResize"); self.handle_terminal_resize(rows, cols).await.unwrap(); } }