From 4cff31e9773b1433c22850668e14692c98ac1870 Mon Sep 17 00:00:00 2001 From: Ryan-W31 Date: Fri, 19 Dec 2025 16:55:18 -0500 Subject: [PATCH 1/3] feat: pane borders --- daemon/src/actors/window.rs | 80 +++++++++++++++++++++++++++++++++++-- daemon/src/cell.rs | 2 +- daemon/src/layout.rs | 16 +++++--- 3 files changed, 88 insertions(+), 10 deletions(-) diff --git a/daemon/src/actors/window.rs b/daemon/src/actors/window.rs index a6b7adb..e889b42 100644 --- a/daemon/src/actors/window.rs +++ b/daemon/src/actors/window.rs @@ -9,9 +9,7 @@ use crate::{ actors::{ pane::{Pane, PaneHandle}, session::SessionHandle, - }, - layout::{LayoutNode, Rect, SplitDirection}, - prelude::*, + }, cell::set_cursor_position, layout::{LayoutNode, Rect, SplitDirection}, prelude::* }; #[derive(Handle)] @@ -183,6 +181,8 @@ impl Window { Ok(()) } async fn handle_redraw(&mut self) -> Result<()> { + self.draw_pane_borders().await?; + for pane in self.panes.iter() { pane.1.rerender().await?; } @@ -309,4 +309,78 @@ impl Window { self.handle_redraw().await?; Ok(()) } + + async fn draw_pane_borders(&mut self) -> Result<()> { + let cols = self.root_rect.width; + let rows = self.root_rect.height; + + let mut output_buffer = Vec::with_capacity(cols as usize * rows as usize * 4); + + let is_content = |x: u16, y: u16, map: &BTreeMap| -> bool { + for rect in map.values() { + if x >= rect.x && x < rect.x + rect.width && + y >= rect.y && y < rect.y + rect.height { + return true; + } + } + false + }; + + output_buffer.extend_from_slice(b"\x1b[0m"); + + let mut cursor_row = 0; + let mut cursor_col = 0; + let mut cursor_invalid = true; + + for y in 0..rows { + for x in 0..cols { + if is_content(x, y, &self.layout_sizing_map) { + continue; + } + + let north = y > 0 && !is_content(x, y - 1, &self.layout_sizing_map); + let south = y < rows - 1 && !is_content(x, y + 1, &self.layout_sizing_map); + let west = x > 0 && !is_content(x - 1, y, &self.layout_sizing_map); + let east = x < cols - 1 && !is_content(x + 1, y, &self.layout_sizing_map); + + let border_char = match (north, south, east, west) { + (true, true, false, false) => '│', + (false, false, true, true) => '─', + (false, true, true, false) => '┌', + (false, true, false, true) => '┐', + (true, false, true, false) => '└', + (true, false, false, true) => '┘', + (true, true, true, false) => '├', + (true, true, false, true) => '┤', + (false, true, true, true) => '┬', + (true, false, true, true) => '┴', + (true, true, true, true) => '┼', + (true, false, false, false) => '│', + (false, true, false, false) => '│', + (false, false, true, false) => '─', + (false, false, false, true) => '─', + _ => ' ', + }; + + if cursor_invalid || y != cursor_row || x != cursor_col { + set_cursor_position(&mut output_buffer, x + 1, y + 1); + cursor_invalid = false; + cursor_row = y; + cursor_col = x; + } + + let mut border_char_buf = [0u8; 4]; + let str_slice = border_char.encode_utf8(&mut border_char_buf); + output_buffer.extend_from_slice(str_slice.as_bytes()); + + cursor_col += 1; + } + } + + if !output_buffer.is_empty() { + self.session_handle.window_output(Bytes::from(output_buffer)).await?; + } + + Ok(()) + } } diff --git a/daemon/src/cell.rs b/daemon/src/cell.rs index 2043754..b1b3257 100644 --- a/daemon/src/cell.rs +++ b/daemon/src/cell.rs @@ -319,7 +319,7 @@ fn push_u16(buf: &mut Vec, mut n: u16) { } #[inline] -fn set_cursor_position(buf: &mut Vec, x: u16, y: u16) { +pub fn set_cursor_position(buf: &mut Vec, x: u16, y: u16) { buf.extend_from_slice(b"\x1b["); push_u16(buf, y); buf.push(b';'); diff --git a/daemon/src/layout.rs b/daemon/src/layout.rs index 8651012..680043e 100644 --- a/daemon/src/layout.rs +++ b/daemon/src/layout.rs @@ -113,8 +113,10 @@ impl LayoutNode { match direction { SplitDirection::Vertical => { - let left_width = (area.width as u32 * left_weight / total_weight) as u16; - let right_width = area.width - left_width; + let available_width = area.width.saturating_sub(1); + + let left_width = (available_width as u32 * left_weight / total_weight) as u16; + let right_width = available_width - left_width; let left_rect = Rect { width: left_width, @@ -123,7 +125,7 @@ impl LayoutNode { let right_rect = Rect { width: right_width, - x: area.x + left_width, + x: area.x + left_width + 1, ..area }; @@ -135,8 +137,10 @@ impl LayoutNode { Ok(()) } SplitDirection::Horizontal => { - let top_height = (area.height as u32 * left_weight / total_weight) as u16; - let bottom_height = area.height - top_height; + let available_height = area.height.saturating_sub(1); + + let top_height = (available_height as u32 * left_weight / total_weight) as u16; + let bottom_height = available_height - top_height; let top_rect = Rect { height: top_height, @@ -145,7 +149,7 @@ impl LayoutNode { let bottom_rect = Rect { height: bottom_height, - y: area.y + top_height, + y: area.y + top_height + 1, ..area }; From 33c28b88e47bf09921d081b59664a856b7a7715d Mon Sep 17 00:00:00 2001 From: Ryan-W31 Date: Fri, 19 Dec 2025 17:33:18 -0500 Subject: [PATCH 2/3] feat: highlight active pane border --- daemon/src/actors/window.rs | 30 ++++++++++++++++++++++++++++++ daemon/src/layout.rs | 4 ++++ 2 files changed, 34 insertions(+) diff --git a/daemon/src/actors/window.rs b/daemon/src/actors/window.rs index e889b42..12481bb 100644 --- a/daemon/src/actors/window.rs +++ b/daemon/src/actors/window.rs @@ -218,6 +218,8 @@ impl Window { } }; + self.draw_pane_borders().await?; + let move_cursor = format!("\x1b[{};{}H", ty, tx); self.session_handle.window_output(Bytes::from(move_cursor)).await?; @@ -316,6 +318,10 @@ impl Window { let mut output_buffer = Vec::with_capacity(cols as usize * rows as usize * 4); + // grab active pane rectangle + let active_rect = self.layout_sizing_map.get(&self.active_pane_id); + + // checks if cell is in a pane or not let is_content = |x: u16, y: u16, map: &BTreeMap| -> bool { for rect in map.values() { if x >= rect.x && x < rect.x + rect.width && @@ -326,23 +332,28 @@ impl Window { false }; + // reset colors output_buffer.extend_from_slice(b"\x1b[0m"); + // set initial cursor position let mut cursor_row = 0; let mut cursor_col = 0; let mut cursor_invalid = true; for y in 0..rows { for x in 0..cols { + // skip cells in panes if is_content(x, y, &self.layout_sizing_map) { continue; } + // get surrounding borders let north = y > 0 && !is_content(x, y - 1, &self.layout_sizing_map); let south = y < rows - 1 && !is_content(x, y + 1, &self.layout_sizing_map); let west = x > 0 && !is_content(x - 1, y, &self.layout_sizing_map); let east = x < cols - 1 && !is_content(x + 1, y, &self.layout_sizing_map); + // pattern match to get correct border char let border_char = match (north, south, east, west) { (true, true, false, false) => '│', (false, false, true, true) => '─', @@ -362,6 +373,7 @@ impl Window { _ => ' ', }; + // move cursor if at the wrong spot if cursor_invalid || y != cursor_row || x != cursor_col { set_cursor_position(&mut output_buffer, x + 1, y + 1); cursor_invalid = false; @@ -369,6 +381,23 @@ impl Window { cursor_col = x; } + // if the border is on the active pane, set this to true + let mut is_active_border = false; + if let Some(rect) = active_rect { + if x >= rect.x.saturating_sub(1) && x < rect.x + rect.width + 1 && + y >= rect.y.saturating_sub(1) && y < rect.y + rect.height + 1 { + is_active_border = true; + } + } + + // configurable colors later + if is_active_border { + output_buffer.extend_from_slice(b"\x1b[96m"); + } else { + output_buffer.extend_from_slice(b"\x1b[90m"); + } + + // add ANSI let mut border_char_buf = [0u8; 4]; let str_slice = border_char.encode_utf8(&mut border_char_buf); output_buffer.extend_from_slice(str_slice.as_bytes()); @@ -377,6 +406,7 @@ impl Window { } } + // send to session if !output_buffer.is_empty() { self.session_handle.window_output(Bytes::from(output_buffer)).await?; } diff --git a/daemon/src/layout.rs b/daemon/src/layout.rs index 680043e..9d858f7 100644 --- a/daemon/src/layout.rs +++ b/daemon/src/layout.rs @@ -113,6 +113,7 @@ impl LayoutNode { match direction { SplitDirection::Vertical => { + // remove a column for the border let available_width = area.width.saturating_sub(1); let left_width = (available_width as u32 * left_weight / total_weight) as u16; @@ -123,6 +124,7 @@ impl LayoutNode { ..area }; + // +1 for the border let right_rect = Rect { width: right_width, x: area.x + left_width + 1, @@ -137,6 +139,7 @@ impl LayoutNode { Ok(()) } SplitDirection::Horizontal => { + // remove a row for the border let available_height = area.height.saturating_sub(1); let top_height = (available_height as u32 * left_weight / total_weight) as u16; @@ -147,6 +150,7 @@ impl LayoutNode { ..area }; + // +1 for the border let bottom_rect = Rect { height: bottom_height, y: area.y + top_height + 1, From f720431a504de5baef239df0abfc1f24494e0251 Mon Sep 17 00:00:00 2001 From: Ryan-W31 Date: Fri, 19 Dec 2025 17:35:46 -0500 Subject: [PATCH 3/3] chore: formatting --- daemon/src/actors/window.rs | 57 ++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/daemon/src/actors/window.rs b/daemon/src/actors/window.rs index 12481bb..5180661 100644 --- a/daemon/src/actors/window.rs +++ b/daemon/src/actors/window.rs @@ -9,7 +9,10 @@ use crate::{ actors::{ pane::{Pane, PaneHandle}, session::SessionHandle, - }, cell::set_cursor_position, layout::{LayoutNode, Rect, SplitDirection}, prelude::* + }, + cell::set_cursor_position, + layout::{LayoutNode, Rect, SplitDirection}, + prelude::*, }; #[derive(Handle)] @@ -315,7 +318,7 @@ impl Window { async fn draw_pane_borders(&mut self) -> Result<()> { let cols = self.root_rect.width; let rows = self.root_rect.height; - + let mut output_buffer = Vec::with_capacity(cols as usize * rows as usize * 4); // grab active pane rectangle @@ -324,8 +327,7 @@ impl Window { // checks if cell is in a pane or not let is_content = |x: u16, y: u16, map: &BTreeMap| -> bool { for rect in map.values() { - if x >= rect.x && x < rect.x + rect.width && - y >= rect.y && y < rect.y + rect.height { + if x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height { return true; } } @@ -333,7 +335,7 @@ impl Window { }; // reset colors - output_buffer.extend_from_slice(b"\x1b[0m"); + output_buffer.extend_from_slice(b"\x1b[0m"); // set initial cursor position let mut cursor_row = 0; @@ -350,26 +352,26 @@ impl Window { // get surrounding borders let north = y > 0 && !is_content(x, y - 1, &self.layout_sizing_map); let south = y < rows - 1 && !is_content(x, y + 1, &self.layout_sizing_map); - let west = x > 0 && !is_content(x - 1, y, &self.layout_sizing_map); - let east = x < cols - 1 && !is_content(x + 1, y, &self.layout_sizing_map); + let west = x > 0 && !is_content(x - 1, y, &self.layout_sizing_map); + let east = x < cols - 1 && !is_content(x + 1, y, &self.layout_sizing_map); // pattern match to get correct border char let border_char = match (north, south, east, west) { - (true, true, false, false) => '│', - (false, false, true, true) => '─', - (false, true, true, false) => '┌', - (false, true, false, true) => '┐', - (true, false, true, false) => '└', - (true, false, false, true) => '┘', - (true, true, true, false) => '├', - (true, true, false, true) => '┤', - (false, true, true, true) => '┬', - (true, false, true, true) => '┴', - (true, true, true, true) => '┼', - (true, false, false, false) => '│', - (false, true, false, false) => '│', - (false, false, true, false) => '─', - (false, false, false, true) => '─', + (true, true, false, false) => '│', + (false, false, true, true) => '─', + (false, true, true, false) => '┌', + (false, true, false, true) => '┐', + (true, false, true, false) => '└', + (true, false, false, true) => '┘', + (true, true, true, false) => '├', + (true, true, false, true) => '┤', + (false, true, true, true) => '┬', + (true, false, true, true) => '┴', + (true, true, true, true) => '┼', + (true, false, false, false) => '│', + (false, true, false, false) => '│', + (false, false, true, false) => '─', + (false, false, false, true) => '─', _ => ' ', }; @@ -384,8 +386,11 @@ impl Window { // if the border is on the active pane, set this to true let mut is_active_border = false; if let Some(rect) = active_rect { - if x >= rect.x.saturating_sub(1) && x < rect.x + rect.width + 1 && - y >= rect.y.saturating_sub(1) && y < rect.y + rect.height + 1 { + if x >= rect.x.saturating_sub(1) + && x < rect.x + rect.width + 1 + && y >= rect.y.saturating_sub(1) + && y < rect.y + rect.height + 1 + { is_active_border = true; } } @@ -398,7 +403,7 @@ impl Window { } // add ANSI - let mut border_char_buf = [0u8; 4]; + let mut border_char_buf = [0u8; 4]; let str_slice = border_char.encode_utf8(&mut border_char_buf); output_buffer.extend_from_slice(str_slice.as_bytes()); @@ -410,7 +415,7 @@ impl Window { if !output_buffer.is_empty() { self.session_handle.window_output(Bytes::from(output_buffer)).await?; } - + Ok(()) } }