feat: make outer pane borders configurable (#2535)
This commit is contained in:
parent
3a76fea2d8
commit
36a8d36a72
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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| {
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue