aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDavid Heinemeier Hansson <david@hey.com>2026-08-09 00:37:57 +0200
committerGitHub <noreply@github.com>2026-08-09 01:37:57 +0300
commit36a8d36a7258e08b40c01bf521b1f4006525f143 (patch)
treeedaf35204db558c4d6b5a79ab78b6d6e5138c9dc /src
parent3a76fea2d858a7a5ef58615f1cc9aabaa30b80ee (diff)
feat: make outer pane borders configurable (#2535)
Diffstat (limited to 'src')
-rw-r--r--src/app/agent_resume.rs23
-rw-r--r--src/app/api/panes.rs1
-rw-r--r--src/app/mod.rs2
-rw-r--r--src/app/state.rs2
-rw-r--r--src/config/model.rs6
-rw-r--r--src/main.rs4
-rw-r--r--src/ui/panes.rs70
7 files changed, 96 insertions, 12 deletions
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::layout::PaneInfo> {
- 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<PaneInfo>,
pane_borders: bool,
pane_gaps: bool,
+ pane_outer_borders: bool,
) -> Vec<PaneInfo> {
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();
@@ -1084,6 +1123,26 @@ mod tests {
}
#[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");
let root = workspace.tabs[0].root_pane;
@@ -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 {