diff --git a/docs/next/website/src/data/config-reference.json b/docs/next/website/src/data/config-reference.json index e7a0539b..a4732ffc 100644 --- a/docs/next/website/src/data/config-reference.json +++ b/docs/next/website/src/data/config-reference.json @@ -658,6 +658,12 @@ "default": "true", "description": "Draw borders around split panes." }, + { + "key": "ui.pane_outer_borders", + "type": "boolean", + "default": "true", + "description": "Draw borders along the outside edge of the pane area. Disable with pane gaps disabled for tmux-style internal splitters without an outside frame." + }, { "key": "ui.pane_scrollbars", "type": "boolean", diff --git a/src/app/agent_resume.rs b/src/app/agent_resume.rs index 1b459628..29c2c317 100644 --- a/src/app/agent_resume.rs +++ b/src/app/agent_resume.rs @@ -130,6 +130,7 @@ impl App { terminal_area, self.state.pane_borders, self.state.pane_gaps, + self.state.pane_outer_borders, ); if self.state.active == Some(ws_idx) @@ -290,15 +291,21 @@ fn derived_pending_agent_resume_pane_infos( terminal_area: Rect, pane_borders: bool, pane_gaps: bool, + pane_outer_borders: bool, ) -> Vec { - crate::ui::apply_pane_chrome(tab.layout.panes(terminal_area), pane_borders, pane_gaps) - .into_iter() - .map(|mut info| { - let pane_inner = crate::ui::pane_inner_rect(info.rect, info.borders); - info.inner_rect = stable_terminal_inner_rect(pane_inner); - info - }) - .collect() + crate::ui::apply_pane_chrome( + tab.layout.panes(terminal_area), + pane_borders, + pane_gaps, + pane_outer_borders, + ) + .into_iter() + .map(|mut info| { + let pane_inner = crate::ui::pane_inner_rect(info.rect, info.borders); + info.inner_rect = stable_terminal_inner_rect(pane_inner); + info + }) + .collect() } fn stable_terminal_inner_rect(pane_inner: Rect) -> Rect { diff --git a/src/app/api/panes.rs b/src/app/api/panes.rs index 4cbdd661..6615fa0a 100644 --- a/src/app/api/panes.rs +++ b/src/app/api/panes.rs @@ -1714,6 +1714,7 @@ impl App { tab.layout.panes(area), self.state.pane_borders, self.state.pane_gaps, + self.state.pane_outer_borders, ) .into_iter() .filter_map(|pane| { diff --git a/src/app/mod.rs b/src/app/mod.rs index 7fb8a01c..70d5fe93 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -631,6 +631,7 @@ impl App { prompt_new_tab_name: config.ui.prompt_new_tab_name, prompt_new_workspace_name: config.ui.prompt_new_workspace_name, pane_borders: config.ui.pane_borders, + pane_outer_borders: config.ui.pane_outer_borders, pane_scrollbars: config.ui.pane_scrollbars, pane_gaps: config.ui.pane_gaps, show_agent_labels_on_pane_borders: config.ui.show_agent_labels_on_pane_borders, @@ -1439,6 +1440,7 @@ impl App { self.state.prompt_new_tab_name = config.ui.prompt_new_tab_name; self.state.prompt_new_workspace_name = config.ui.prompt_new_workspace_name; self.state.pane_borders = config.ui.pane_borders; + self.state.pane_outer_borders = config.ui.pane_outer_borders; self.state.pane_scrollbars = config.ui.pane_scrollbars; self.state.pane_gaps = config.ui.pane_gaps; self.state.show_agent_labels_on_pane_borders = diff --git a/src/app/state.rs b/src/app/state.rs index 97266344..04876290 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -1464,6 +1464,7 @@ pub struct AppState { pub prompt_new_tab_name: bool, pub prompt_new_workspace_name: bool, pub pane_borders: bool, + pub pane_outer_borders: bool, pub pane_scrollbars: bool, pub pane_gaps: bool, pub show_agent_labels_on_pane_borders: bool, @@ -1833,6 +1834,7 @@ impl AppState { prompt_new_tab_name: true, prompt_new_workspace_name: false, pane_borders: true, + pane_outer_borders: true, pane_scrollbars: true, pane_gaps: false, show_agent_labels_on_pane_borders: false, diff --git a/src/config/model.rs b/src/config/model.rs index 5cbafaec..ba700731 100644 --- a/src/config/model.rs +++ b/src/config/model.rs @@ -838,6 +838,8 @@ pub struct UiConfig { pub prompt_new_workspace_name: bool, /// Draw borders around split panes. Default: true. pub pane_borders: bool, + /// Draw borders along the outside edge of the pane area. Default: true. + pub pane_outer_borders: bool, /// Draw interactive scrollbars beside terminal panes. Default: true. pub pane_scrollbars: bool, /// Keep split panes visually separated instead of sharing divider borders. Default: true. @@ -1052,6 +1054,7 @@ impl Default for UiConfig { prompt_new_tab_name: true, prompt_new_workspace_name: false, pane_borders: true, + pane_outer_borders: true, pane_scrollbars: true, pane_gaps: true, show_agent_labels_on_pane_borders: false, @@ -1304,6 +1307,7 @@ status_indicators = "symbols" fn pane_appearance_defaults_and_parse() { let default_config = Config::default(); assert!(default_config.ui.pane_borders); + assert!(default_config.ui.pane_outer_borders); assert!(default_config.ui.pane_scrollbars); assert!(default_config.ui.pane_gaps); assert!(!default_config.ui.show_agent_labels_on_pane_borders); @@ -1316,6 +1320,7 @@ status_indicators = "symbols" let toml = r#" [ui] pane_borders = false +pane_outer_borders = false pane_scrollbars = false pane_gaps = true show_agent_labels_on_pane_borders = true @@ -1324,6 +1329,7 @@ tab_bar_position = "bottom" "#; let config: Config = toml::from_str(toml).unwrap(); assert!(!config.ui.pane_borders); + assert!(!config.ui.pane_outer_borders); assert!(!config.ui.pane_scrollbars); assert!(config.ui.pane_gaps); assert!(config.ui.show_agent_labels_on_pane_borders); diff --git a/src/main.rs b/src/main.rs index 7f79730d..c3269675 100644 --- a/src/main.rs +++ b/src/main.rs @@ -306,6 +306,10 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration # Draw borders around split panes. # pane_borders = true +# Draw borders along the outside edge of the pane area. +# Disable for tmux-style internal splitters without an outside frame. +# pane_outer_borders = true + # Draw interactive scrollbars beside terminal panes. # Set false to reclaim the scrollbar column and keep it out of terminal-native selections. # pane_scrollbars = true diff --git a/src/ui/panes.rs b/src/ui/panes.rs index b904ce00..22daddf8 100644 --- a/src/ui/panes.rs +++ b/src/ui/panes.rs @@ -91,8 +91,21 @@ pub(crate) fn apply_pane_chrome( panes: Vec, pane_borders: bool, pane_gaps: bool, + pane_outer_borders: bool, ) -> Vec { let multi_pane = panes.len() > 1; + let outer_left = panes.iter().map(|info| info.rect.x).min().unwrap_or(0); + let outer_top = panes.iter().map(|info| info.rect.y).min().unwrap_or(0); + let outer_right = panes + .iter() + .map(|info| info.rect.x.saturating_add(info.rect.width)) + .max() + .unwrap_or(0); + let outer_bottom = panes + .iter() + .map(|info| info.rect.y.saturating_add(info.rect.height)) + .max() + .unwrap_or(0); panes .iter() .cloned() @@ -121,6 +134,20 @@ pub(crate) fn apply_pane_chrome( borders.remove(Borders::BOTTOM); } } + if !pane_outer_borders { + if info.rect.x == outer_left { + borders.remove(Borders::LEFT); + } + if info.rect.y == outer_top { + borders.remove(Borders::TOP); + } + if info.rect.x.saturating_add(info.rect.width) == outer_right { + borders.remove(Borders::RIGHT); + } + if info.rect.y.saturating_add(info.rect.height) == outer_bottom { + borders.remove(Borders::BOTTOM); + } + } borders }; info @@ -179,7 +206,7 @@ pub(super) fn resize_tab_panes( if tab.zoomed { let focused_id = tab.layout.focused(); if let Some((terminal_id, rt)) = runtime_for_tab_pane(terminal_runtimes, tab, focused_id) { - let borders = if multi_pane && app.pane_borders { + let borders = if multi_pane && app.pane_borders && app.pane_outer_borders { Borders::ALL } else { Borders::NONE @@ -198,7 +225,12 @@ pub(super) fn resize_tab_panes( return; } - for info in apply_pane_chrome(tab.layout.panes(area), app.pane_borders, app.pane_gaps) { + for info in apply_pane_chrome( + tab.layout.panes(area), + app.pane_borders, + app.pane_gaps, + app.pane_outer_borders, + ) { let pane_inner = pane_inner_rect(info.rect, info.borders); if let Some((terminal_id, rt)) = runtime_for_tab_pane(terminal_runtimes, tab, info.id) { @@ -234,7 +266,7 @@ pub(super) fn compute_pane_infos( if ws.zoomed { let focused_id = ws.layout.focused(); - let borders = if multi_pane && app.pane_borders { + let borders = if multi_pane && app.pane_borders && app.pane_outer_borders { Borders::ALL } else { Borders::NONE @@ -268,7 +300,12 @@ pub(super) fn compute_pane_infos( }]; } - let mut pane_infos = apply_pane_chrome(ws.layout.panes(area), app.pane_borders, app.pane_gaps); + let mut pane_infos = apply_pane_chrome( + ws.layout.panes(area), + app.pane_borders, + app.pane_gaps, + app.pane_outer_borders, + ); for info in &mut pane_infos { let pane_inner = pane_inner_rect(info.rect, info.borders); @@ -1054,6 +1091,7 @@ mod tests { workspace.tabs[0].layout.panes(Rect::new(0, 0, 100, 20)), true, false, + true, ); let left = infos.iter().find(|info| info.id == root).unwrap(); let right = infos.iter().find(|info| info.id == right).unwrap(); @@ -1074,6 +1112,7 @@ mod tests { workspace.tabs[0].layout.panes(Rect::new(0, 0, 100, 20)), true, false, + true, ); let top = infos.iter().find(|info| info.id == root).unwrap(); let bottom = infos.iter().find(|info| info.id == bottom).unwrap(); @@ -1083,6 +1122,26 @@ mod tests { assert!(bottom.borders.contains(Borders::TOP)); } + #[test] + fn disabled_outer_borders_keep_only_shared_pane_dividers() { + let mut workspace = Workspace::test_new("test"); + let root = workspace.tabs[0].root_pane; + let right = workspace.test_split(ratatui::layout::Direction::Horizontal); + workspace.tabs[0].layout.focus_pane(root); + + let infos = apply_pane_chrome( + workspace.tabs[0].layout.panes(Rect::new(0, 0, 100, 20)), + true, + false, + false, + ); + let left = infos.iter().find(|info| info.id == root).unwrap(); + let right = infos.iter().find(|info| info.id == right).unwrap(); + + assert_eq!(left.borders, Borders::NONE); + assert_eq!(right.borders, Borders::LEFT); + } + #[test] fn pane_gaps_keep_independent_bordered_panes() { let mut workspace = Workspace::test_new("test"); @@ -1094,6 +1153,7 @@ mod tests { workspace.tabs[0].layout.panes(Rect::new(0, 0, 100, 20)), true, true, + true, ); let left = infos.iter().find(|info| info.id == root).unwrap(); let right = infos.iter().find(|info| info.id == right).unwrap(); @@ -1114,6 +1174,7 @@ mod tests { workspace.tabs[0].layout.panes(Rect::new(0, 0, 100, 20)), false, true, + true, ); let left = infos.iter().find(|info| info.id == root).unwrap(); let right = infos.iter().find(|info| info.id == right).unwrap(); @@ -1133,6 +1194,7 @@ mod tests { workspace.tabs[0].layout.panes(Rect::new(0, 0, 100, 20)), false, false, + true, ); for info in infos {