aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-08-01 17:25:43 +0300
committerGitHub <noreply@github.com>2026-08-01 17:25:43 +0300
commitf6031f7fb062c89a47fd9fffbe641358f2bd0701 (patch)
tree4d9d61c7d7b63c05a48c81f2b54d9bf932470f9b /src
parentf459a0df315e0963aec5e4dcf2fc394a252ea320 (diff)
feat(ui): make pane scrollbars optional (#2166)
refs #2167
Diffstat (limited to 'src')
-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.rs38
5 files changed, 43 insertions, 9 deletions
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 2a996685..5d070afa 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -630,6 +630,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_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,
hide_tab_bar_when_single_tab: config.ui.hide_tab_bar_when_single_tab,
@@ -1437,6 +1438,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_scrollbars = config.ui.pane_scrollbars;
self.state.pane_gaps = config.ui.pane_gaps;
self.state.show_agent_labels_on_pane_borders =
config.ui.show_agent_labels_on_pane_borders;
diff --git a/src/app/state.rs b/src/app/state.rs
index ef903f65..e6c23c4c 100644
--- a/src/app/state.rs
+++ b/src/app/state.rs
@@ -1493,6 +1493,7 @@ pub struct AppState {
pub prompt_new_tab_name: bool,
pub prompt_new_workspace_name: bool,
pub pane_borders: bool,
+ pub pane_scrollbars: bool,
pub pane_gaps: bool,
pub show_agent_labels_on_pane_borders: bool,
pub hide_tab_bar_when_single_tab: bool,
@@ -1860,6 +1861,7 @@ impl AppState {
prompt_new_tab_name: true,
prompt_new_workspace_name: false,
pane_borders: true,
+ pane_scrollbars: true,
pane_gaps: false,
show_agent_labels_on_pane_borders: false,
hide_tab_bar_when_single_tab: false,
diff --git a/src/config/model.rs b/src/config/model.rs
index b1f0ebc3..e56ac087 100644
--- a/src/config/model.rs
+++ b/src/config/model.rs
@@ -814,6 +814,8 @@ pub struct UiConfig {
pub prompt_new_workspace_name: bool,
/// Draw borders around split panes. Default: true.
pub pane_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.
pub pane_gaps: bool,
/// Show agent labels in split pane borders when no manual pane label is set. Default: false.
@@ -1021,6 +1023,7 @@ impl Default for UiConfig {
prompt_new_tab_name: true,
prompt_new_workspace_name: false,
pane_borders: true,
+ pane_scrollbars: true,
pane_gaps: true,
show_agent_labels_on_pane_borders: false,
hide_tab_bar_when_single_tab: false,
@@ -1253,6 +1256,7 @@ agent_panel_scope = "current"
fn pane_appearance_defaults_and_parse() {
let default_config = Config::default();
assert!(default_config.ui.pane_borders);
+ assert!(default_config.ui.pane_scrollbars);
assert!(default_config.ui.pane_gaps);
assert!(!default_config.ui.show_agent_labels_on_pane_borders);
assert!(!default_config.ui.hide_tab_bar_when_single_tab);
@@ -1264,6 +1268,7 @@ agent_panel_scope = "current"
let toml = r#"
[ui]
pane_borders = false
+pane_scrollbars = false
pane_gaps = true
show_agent_labels_on_pane_borders = true
hide_tab_bar_when_single_tab = true
@@ -1271,6 +1276,7 @@ tab_bar_position = "bottom"
"#;
let config: Config = toml::from_str(toml).unwrap();
assert!(!config.ui.pane_borders);
+ assert!(!config.ui.pane_scrollbars);
assert!(config.ui.pane_gaps);
assert!(config.ui.show_agent_labels_on_pane_borders);
assert!(config.ui.hide_tab_bar_when_single_tab);
diff --git a/src/main.rs b/src/main.rs
index 7b4058d2..7b8613f8 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -305,6 +305,10 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration
# Draw borders around split panes.
# pane_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
+
# Keep split panes visually separated instead of sharing divider borders.
# pane_gaps = true
diff --git a/src/ui/panes.rs b/src/ui/panes.rs
index 5233060d..b904ce00 100644
--- a/src/ui/panes.rs
+++ b/src/ui/panes.rs
@@ -31,8 +31,8 @@ fn pane_border_title(label: &str, pane_width: u16, _focused: bool) -> Option<Str
Some(format!(" {} ", truncate_end(label, max_label_width)))
}
-fn stable_terminal_inner_rect(pane_inner: Rect) -> Rect {
- if pane_inner.width <= 4 {
+fn stable_terminal_inner_rect(pane_inner: Rect, pane_scrollbars: bool) -> Rect {
+ if !pane_scrollbars || pane_inner.width <= 4 {
return pane_inner;
}
@@ -143,8 +143,12 @@ fn runtime_for_tab_pane<'a>(
.map(|runtime| (terminal_id, runtime))
}
-fn stable_scrollbar_gutter(rt: &TerminalRuntime, pane_inner: Rect) -> (Rect, Option<Rect>) {
- let inner_rect = stable_terminal_inner_rect(pane_inner);
+fn stable_scrollbar_gutter(
+ rt: &TerminalRuntime,
+ pane_inner: Rect,
+ pane_scrollbars: bool,
+) -> (Rect, Option<Rect>) {
+ let inner_rect = stable_terminal_inner_rect(pane_inner, pane_scrollbars);
if inner_rect == pane_inner {
return (inner_rect, None);
}
@@ -181,7 +185,7 @@ pub(super) fn resize_tab_panes(
Borders::NONE
};
let pane_inner = pane_inner_rect(area, borders);
- let inner_rect = stable_terminal_inner_rect(pane_inner);
+ let inner_rect = stable_terminal_inner_rect(pane_inner, app.pane_scrollbars);
if !app.direct_attach_resize_locks.contains(terminal_id) {
rt.resize(
inner_rect.height,
@@ -198,7 +202,7 @@ pub(super) fn resize_tab_panes(
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) {
- let inner_rect = stable_terminal_inner_rect(pane_inner);
+ let inner_rect = stable_terminal_inner_rect(pane_inner, app.pane_scrollbars);
if !app.direct_attach_resize_locks.contains(terminal_id) {
rt.resize(
inner_rect.height,
@@ -239,7 +243,8 @@ pub(super) fn compute_pane_infos(
let mut inner_rect = pane_inner;
let mut scrollbar_rect = None;
if let Some(rt) = app.runtime_for_pane_in_workspace(terminal_runtimes, ws_idx, focused_id) {
- (inner_rect, scrollbar_rect) = stable_scrollbar_gutter(rt, pane_inner);
+ (inner_rect, scrollbar_rect) =
+ stable_scrollbar_gutter(rt, pane_inner, app.pane_scrollbars);
if resize_panes
&& ws.terminal_id(focused_id).is_some_and(|terminal_id| {
!app.direct_attach_resize_locks.contains(terminal_id)
@@ -271,7 +276,8 @@ pub(super) fn compute_pane_infos(
let mut inner_rect = pane_inner;
let mut scrollbar_rect = None;
if let Some(rt) = app.runtime_for_pane_in_workspace(terminal_runtimes, ws_idx, info.id) {
- (inner_rect, scrollbar_rect) = stable_scrollbar_gutter(rt, pane_inner);
+ (inner_rect, scrollbar_rect) =
+ stable_scrollbar_gutter(rt, pane_inner, app.pane_scrollbars);
if resize_panes
&& ws.terminal_id(info.id).is_some_and(|terminal_id| {
!app.direct_attach_resize_locks.contains(terminal_id)
@@ -1358,7 +1364,7 @@ mod tests {
}
#[tokio::test]
- async fn pane_scrollbar_reserves_last_column_from_terminal_area() {
+ async fn pane_scrollbar_setting_controls_reserved_column() {
let mut app = AppState::test_new();
let mut workspace = Workspace::test_new("test");
let root_pane = workspace.tabs[0].root_pane;
@@ -1388,6 +1394,20 @@ mod tests {
assert_eq!(info.rect, area);
assert_eq!(info.scrollbar_rect, Some(Rect::new(49, 3, 1, 8)));
assert_eq!(info.inner_rect, Rect::new(10, 3, 39, 8));
+
+ app.pane_scrollbars = false;
+ let infos = compute_pane_infos(
+ &app,
+ &terminal_runtimes,
+ area,
+ false,
+ crate::kitty_graphics::HostCellSize::default(),
+ );
+ let info = &infos[0];
+
+ assert_eq!(info.rect, area);
+ assert_eq!(info.scrollbar_rect, None);
+ assert_eq!(info.inner_rect, area);
}
#[test]