aboutsummaryrefslogtreecommitdiffstats
path: root/sl0ppij/zellij-utils/src
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-12 16:15:19 +0200
committeruser <user@clank>2026-08-12 19:34:19 +0200
commitbc315b0c7bf1b0201a5164c8c52410a47871fc79 (patch)
tree7310c03cb575fa5e2b90e40bb1d4ff369306dd65 /sl0ppij/zellij-utils/src
parentsl0p: highlight the status bar button under the pointer (diff)
downloadsl0ppi-bc315b0c7bf1b0201a5164c8c52410a47871fc79.tar.gz
sl0ppi-bc315b0c7bf1b0201a5164c8c52410a47871fc79.tar.xz
sl0ppi-bc315b0c7bf1b0201a5164c8c52410a47871fc79.zip
sl0p: add an indexed style for status bar buttons
`status_bar_button_style "indexed"` renders `1:Approve 2:Reject` instead of `[Approve] [Reject]`, so the bar doubles as a key legend for a harness that binds alt+1..9 to the same actions. A config option rather than a protocol field: whether the numbers mean anything depends on what the USER has bound, not on what the program knows. It needs no wire change and so no version bump, and the click report still carries the id -- never the index -- so a program is unaffected by which style is in force. The style is threaded into layout_status_bar, the one function the renderer draws from and the hit-test resolves against. That is what makes this safe: changing a button's rendered width necessarily changes its clickable span too, verified by a test asserting span width equals the drawn width and that every drawn column resolves back, for both styles. Also fixes the web client's Style construction to read the option from config rather than defaulting it -- my first pass claimed that code had no pane_frames config available, which was wrong; it uses it two lines further down.
Diffstat (limited to 'sl0ppij/zellij-utils/src')
-rw-r--r--sl0ppij/zellij-utils/src/data.rs4
-rw-r--r--sl0ppij/zellij-utils/src/input/config.rs1
-rw-r--r--sl0ppij/zellij-utils/src/input/layout.rs7
-rw-r--r--sl0ppij/zellij-utils/src/input/theme.rs41
-rw-r--r--sl0ppij/zellij-utils/src/input/unit/layout_test.rs43
-rw-r--r--sl0ppij/zellij-utils/src/kdl/mod.rs25
-rw-r--r--sl0ppij/zellij-utils/src/plugin_api/event.rs1
-rw-r--r--sl0ppij/zellij-utils/src/plugin_api/style.rs1
-rw-r--r--sl0ppij/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap1
-rw-r--r--sl0ppij/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap1
-rw-r--r--sl0ppij/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap1
-rw-r--r--sl0ppij/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap1
-rw-r--r--sl0ppij/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap1
13 files changed, 121 insertions, 7 deletions
diff --git a/sl0ppij/zellij-utils/src/data.rs b/sl0ppij/zellij-utils/src/data.rs
index 2caedf8..fa18a36 100644
--- a/sl0ppij/zellij-utils/src/data.rs
+++ b/sl0ppij/zellij-utils/src/data.rs
@@ -1449,6 +1449,10 @@ pub struct Style {
/// the committed prost assets. Defaults to Left when coming back from a
/// ProtobufStyle.
pub pane_title_alignment: PaneTitleAlignment,
+ /// How status bar action buttons are decorated (OSC 5577). Not round-tripped
+ /// through the plugin protobuf API, for the same reason as
+ /// `pane_title_alignment` above.
+ pub status_bar_button_style: crate::input::theme::StatusBarButtonStyle,
/// OUTER gap: cells each pane gives up on every side, leaving empty
/// terminal between neighbouring panes. Two adjacent panes therefore show
/// 2 * pane_gap cells between them, and there is a gap at the screen edge.
diff --git a/sl0ppij/zellij-utils/src/input/config.rs b/sl0ppij/zellij-utils/src/input/config.rs
index 880b3fa..c3f33d2 100644
--- a/sl0ppij/zellij-utils/src/input/config.rs
+++ b/sl0ppij/zellij-utils/src/input/config.rs
@@ -1558,6 +1558,7 @@ mod config_test {
stack_list_max_rows: 0,
stack_list_grid: false,
stack_list_style: Default::default(),
+ status_bar_button_style: Default::default(),
},
};
assert_eq!(config.ui, expected_ui_config, "Ui config defined in config");
diff --git a/sl0ppij/zellij-utils/src/input/layout.rs b/sl0ppij/zellij-utils/src/input/layout.rs
index 6160bb2..7906338 100644
--- a/sl0ppij/zellij-utils/src/input/layout.rs
+++ b/sl0ppij/zellij-utils/src/input/layout.rs
@@ -216,12 +216,13 @@ pub fn layout_status_bar(
inner_cols: usize,
status: Option<&str>,
buttons: &[StatusBarButton],
+ button_style: crate::input::theme::StatusBarButtonStyle,
) -> StatusBarLayout {
let mut spans = vec![];
let mut buttons_width = 0;
- for button in buttons {
- // "[label]" plus a leading space to separate it from what precedes it
- let rendered = format!("[{}]", button.label);
+ for (index, button) in buttons.iter().enumerate() {
+ // plus a leading space, to separate it from whatever precedes it
+ let rendered = button_style.render(index + 1, &button.label);
let width = rendered.chars().count() + 1;
if buttons_width + width > inner_cols {
break; // no room for this one or any after it
diff --git a/sl0ppij/zellij-utils/src/input/theme.rs b/sl0ppij/zellij-utils/src/input/theme.rs
index 646fa0e..4a33db5 100644
--- a/sl0ppij/zellij-utils/src/input/theme.rs
+++ b/sl0ppij/zellij-utils/src/input/theme.rs
@@ -9,6 +9,43 @@ use std::{
use crate::data::Styling;
+/// How a status bar action button is decorated (OSC 5577).
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
+pub enum StatusBarButtonStyle {
+ /// `[Approve]` -- bracketed, no index.
+ Boxed,
+ /// `1:Approve` -- the button's 1-based position, then its label. Useful when
+ /// something else binds those numbers to the same actions, so the bar shows
+ /// which key does what.
+ Indexed,
+}
+
+impl Default for StatusBarButtonStyle {
+ fn default() -> Self {
+ StatusBarButtonStyle::Boxed
+ }
+}
+
+impl StatusBarButtonStyle {
+ pub fn from_str(name: &str) -> Option<Self> {
+ match name.trim().to_lowercase().as_str() {
+ "boxed" => Some(StatusBarButtonStyle::Boxed),
+ "indexed" => Some(StatusBarButtonStyle::Indexed),
+ _ => None,
+ }
+ }
+ pub fn possible_values() -> &'static str {
+ "\"boxed\", \"indexed\""
+ }
+ /// How this style renders a button. `index` is 1-based.
+ pub fn render(&self, index: usize, label: &str) -> String {
+ match self {
+ StatusBarButtonStyle::Boxed => format!("[{}]", label),
+ StatusBarButtonStyle::Indexed => format!("{}:{}", index, label),
+ }
+ }
+}
+
/// Visual treatment of the stacked-pane picker's entries.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum StackListStyle {
@@ -97,6 +134,8 @@ pub struct FrameConfig {
pub stack_list_grid: bool,
/// Visual treatment of picker entries.
pub stack_list_style: StackListStyle,
+ /// How status bar action buttons are decorated.
+ pub status_bar_button_style: StatusBarButtonStyle,
}
/// Hand-written rather than derived: `gap_aspect` must default to 2 to match
@@ -119,6 +158,7 @@ impl Default for FrameConfig {
stack_list_max_rows: 0,
stack_list_grid: false,
stack_list_style: StackListStyle::Boxed,
+ status_bar_button_style: StatusBarButtonStyle::Boxed,
}
}
}
@@ -152,6 +192,7 @@ impl FrameConfig {
merged.stack_list_max_rows = other.stack_list_max_rows;
merged.stack_list_grid = other.stack_list_grid;
merged.stack_list_style = other.stack_list_style;
+ merged.status_bar_button_style = other.status_bar_button_style;
merged
}
}
diff --git a/sl0ppij/zellij-utils/src/input/unit/layout_test.rs b/sl0ppij/zellij-utils/src/input/unit/layout_test.rs
index 41573e9..c48c17e 100644
--- a/sl0ppij/zellij-utils/src/input/unit/layout_test.rs
+++ b/sl0ppij/zellij-utils/src/input/unit/layout_test.rs
@@ -2805,7 +2805,7 @@ fn sl0p_status_bar_button_spans_are_where_they_are_drawn() {
},
];
for inner_cols in 20..120usize {
- let layout = layout_status_bar(inner_cols, Some("waiting for review"), &buttons);
+ let layout = layout_status_bar(inner_cols, Some("waiting for review"), &buttons, Default::default());
for span in &layout.buttons {
assert!(
span.start + span.width <= inner_cols,
@@ -2856,7 +2856,7 @@ fn sl0p_status_bar_drops_buttons_that_cannot_fit_rather_than_overflowing() {
label: String::from("LongishLabel"),
})
.collect();
- let layout = layout_status_bar(20, Some("status"), &buttons);
+ let layout = layout_status_bar(20, Some("status"), &buttons, Default::default());
assert!(
layout.buttons.len() < buttons.len(),
"expected some buttons to be dropped at 20 cols, got {:?}",
@@ -2876,7 +2876,7 @@ fn sl0p_status_bar_text_yields_space_to_buttons() {
label: String::from("Go"),
}];
let long = "x".repeat(200);
- let layout = layout_status_bar(30, Some(&long), &buttons);
+ let layout = layout_status_bar(30, Some(&long), &buttons, Default::default());
assert_eq!(layout.buttons.len(), 1, "the button must survive");
assert!(
layout.text.chars().count() < 30,
@@ -2903,3 +2903,40 @@ fn sl0p_status_bar_wire_format_round_trips() {
let parsed = parse_status_bar_buttons(wire.split(';'));
assert_eq!(parsed, buttons, "wire form: {}", wire);
}
+
+#[test]
+fn sl0p_status_bar_button_style_changes_rendering_and_spans_together() {
+ // The style changes how wide a button is, and the hit-test resolves against
+ // the same layout -- so an indexed button must be clickable across exactly
+ // the columns it is drawn on, not the columns a boxed one would have used.
+ use crate::input::theme::StatusBarButtonStyle;
+ let buttons: Vec<StatusBarButton> = ["Approve", "Reject", "Defer"]
+ .iter()
+ .enumerate()
+ .map(|(i, label)| StatusBarButton {
+ id: format!("id{}", i),
+ label: label.to_string(),
+ })
+ .collect();
+
+ let boxed = layout_status_bar(80, None, &buttons, StatusBarButtonStyle::Boxed);
+ let indexed = layout_status_bar(80, None, &buttons, StatusBarButtonStyle::Indexed);
+
+ assert_eq!(boxed.buttons[0].rendered, "[Approve]");
+ assert_eq!(indexed.buttons[0].rendered, "1:Approve");
+ assert_eq!(indexed.buttons[2].rendered, "3:Defer", "1-based index");
+
+ for layout in [&boxed, &indexed] {
+ for span in &layout.buttons {
+ assert_eq!(
+ span.width,
+ span.rendered.chars().count(),
+ "span width must match what is drawn: {:?}",
+ span
+ );
+ for col in span.start..span.start + span.width {
+ assert_eq!(layout.button_at(col), Some(span.id.as_str()));
+ }
+ }
+ }
+}
diff --git a/sl0ppij/zellij-utils/src/kdl/mod.rs b/sl0ppij/zellij-utils/src/kdl/mod.rs
index 88ea7db..358fa3e 100644
--- a/sl0ppij/zellij-utils/src/kdl/mod.rs
+++ b/sl0ppij/zellij-utils/src/kdl/mod.rs
@@ -15,7 +15,9 @@ use crate::input::layout::{
use crate::input::options::{Clipboard, OnForceClose, Options, PaneFrameStyle};
use crate::input::permission::{GrantedPermission, PermissionCache};
use crate::input::plugins::PluginAliases;
-use crate::input::theme::{FrameConfig, StackListStyle, Theme, Themes, UiConfig};
+use crate::input::theme::{
+ FrameConfig, StackListStyle, StatusBarButtonStyle, Theme, Themes, UiConfig,
+};
use crate::input::web_client::WebClientConfig;
use kdl_layout_parser::KdlLayoutParser;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
@@ -5688,6 +5690,26 @@ impl UiConfig {
},
None => StackListStyle::default(),
};
+ let status_bar_button_style = match kdl_children_property_first_arg_as_string!(
+ pane_frames,
+ "status_bar_button_style"
+ ) {
+ Some(raw) => match StatusBarButtonStyle::from_str(raw) {
+ Some(style) => style,
+ None => {
+ return Err(ConfigError::new_kdl_error(
+ format!(
+ "Unknown status_bar_button_style: \"{}\". Valid values are: {}",
+ raw,
+ StatusBarButtonStyle::possible_values()
+ ),
+ pane_frames.span().offset(),
+ pane_frames.span().len(),
+ ))
+ },
+ },
+ None => StatusBarButtonStyle::default(),
+ };
let frame_config = FrameConfig {
rounded_corners,
hide_session_name,
@@ -5702,6 +5724,7 @@ impl UiConfig {
stack_list_max_rows,
stack_list_grid,
stack_list_style,
+ status_bar_button_style,
};
ui_config.pane_frames = frame_config;
}
diff --git a/sl0ppij/zellij-utils/src/plugin_api/event.rs b/sl0ppij/zellij-utils/src/plugin_api/event.rs
index 0dafec6..2e905f2 100644
--- a/sl0ppij/zellij-utils/src/plugin_api/event.rs
+++ b/sl0ppij/zellij-utils/src/plugin_api/event.rs
@@ -2493,6 +2493,7 @@ fn serialize_mode_update_event_with_non_default_values() {
rounded_corners: true,
hide_session_name: false,
pane_title_alignment: Default::default(),
+ status_bar_button_style: Default::default(),
pane_gap_x: Default::default(),
pane_gap_y: Default::default(),
pane_padding: Default::default(),
diff --git a/sl0ppij/zellij-utils/src/plugin_api/style.rs b/sl0ppij/zellij-utils/src/plugin_api/style.rs
index cf55435..3fc1efe 100644
--- a/sl0ppij/zellij-utils/src/plugin_api/style.rs
+++ b/sl0ppij/zellij-utils/src/plugin_api/style.rs
@@ -26,6 +26,7 @@ impl TryFrom<ProtobufStyle> for Style {
// alignment, and adding a proto field would mean regenerating the
// committed prost assets.
pane_title_alignment: Default::default(),
+ status_bar_button_style: Default::default(),
pane_gap_x: Default::default(),
pane_gap_y: Default::default(),
pane_padding: Default::default(),
diff --git a/sl0ppij/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap b/sl0ppij/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap
index 5679824..bdcb910 100644
--- a/sl0ppij/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap
+++ b/sl0ppij/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap
@@ -6253,6 +6253,7 @@ Config {
stack_list_max_rows: 0,
stack_list_grid: false,
stack_list_style: Boxed,
+ status_bar_button_style: Boxed,
},
},
env: {},
diff --git a/sl0ppij/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap b/sl0ppij/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap
index 1541d67..39eebd8 100644
--- a/sl0ppij/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap
+++ b/sl0ppij/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap
@@ -6253,6 +6253,7 @@ Config {
stack_list_max_rows: 0,
stack_list_grid: false,
stack_list_style: Boxed,
+ status_bar_button_style: Boxed,
},
},
env: {
diff --git a/sl0ppij/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap b/sl0ppij/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap
index 126a727..bf08161 100644
--- a/sl0ppij/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap
+++ b/sl0ppij/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap
@@ -309,6 +309,7 @@ Config {
stack_list_max_rows: 0,
stack_list_grid: false,
stack_list_style: Boxed,
+ status_bar_button_style: Boxed,
},
},
env: {},
diff --git a/sl0ppij/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap b/sl0ppij/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap
index a83dd9f..1290980 100644
--- a/sl0ppij/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap
+++ b/sl0ppij/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap
@@ -8024,6 +8024,7 @@ Config {
stack_list_max_rows: 0,
stack_list_grid: false,
stack_list_style: Boxed,
+ status_bar_button_style: Boxed,
},
},
env: {},
diff --git a/sl0ppij/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap b/sl0ppij/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap
index 79f1c71..545b5b9 100644
--- a/sl0ppij/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap
+++ b/sl0ppij/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap
@@ -6253,6 +6253,7 @@ Config {
stack_list_max_rows: 0,
stack_list_grid: false,
stack_list_style: Boxed,
+ status_bar_button_style: Boxed,
},
},
env: {},