aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-11 20:08:23 +0200
committeruser <user@clank>2026-08-11 20:08:23 +0200
commit3418e8218d91d916b3b2a45ac81d3032c27575c6 (patch)
tree4853d3b368d18480efe7dc4ef994e9d7e99a94e0
parentsl0p: resolve responsive layouts from current state, not a remembered position (diff)
downloadsl0ppij-3418e8218d91d916b3b2a45ac81d3032c27575c6.tar.gz
sl0ppij-3418e8218d91d916b3b2a45ac81d3032c27575c6.tar.xz
sl0ppij-3418e8218d91d916b3b2a45ac81d3032c27575c6.zip
sl0p: add a minimal style for the stacked-pane picker
The boxed entries read as clutter: every entry bracketed in vertical bars, and the active one marked by a "> " that sits outside its own cell. `stack_list_style "minimal"` drops both. The active entry is shown by inverting it instead, which needs no glyph and no column, and the padding is moved inside the styled run so the inverted block covers the whole cell rather than leaving unstyled gaps at its edges. Four fewer columns of decoration per entry, so more fit per row in grid mode. Uses ribbon_selected rather than text_selected. Themes give ribbon_selected real contrast because it marks the selected item elsewhere in zellij's UI -- gruber yellow here, white-on-blue in the TempleOS theme -- whereas text_selected's background is 40,40,40 against a 24,24,24 terminal and read as essentially no highlight when tried. Decoration width comes from the style itself, so Tab's column arithmetic and the renderer cannot disagree about how wide an entry is. Default stays boxed, so nothing changes for anyone who does not ask.
-rw-r--r--README.md13
-rw-r--r--zellij-server/src/lib.rs1
-rw-r--r--zellij-server/src/screen.rs13
-rw-r--r--zellij-server/src/tab/mod.rs15
-rw-r--r--zellij-server/src/tab/unit/tab_integration_tests.rs34
-rw-r--r--zellij-server/src/ui/pane_boundaries_frame.rs72
-rw-r--r--zellij-server/src/ui/pane_contents_and_ui.rs6
-rw-r--r--zellij-server/src/unit/screen_tests.rs5
-rw-r--r--zellij-utils/src/input/config.rs1
-rw-r--r--zellij-utils/src/input/theme.rs44
-rw-r--r--zellij-utils/src/kdl/mod.rs21
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap1
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap1
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap1
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap1
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap1
16 files changed, 212 insertions, 18 deletions
diff --git a/README.md b/README.md
index e5a06114..3d8d73d6 100644
--- a/README.md
+++ b/README.md
@@ -167,6 +167,19 @@ budget** rather than a count: eight panes in a wide terminal fit on one row, and
count adapts to the titles rather than being configured; long titles simply
yield fewer columns, and one column is the floor.
+`stack_list_style` controls how entries look:
+
+| style | entry | active entry |
+|---|---|---|
+| `boxed` (default) | bracketed in vertical bars | prefixed with `> ` |
+| `minimal` | no brackets, one space of padding | inverted, using the theme's `ribbon_selected` |
+
+`minimal` reads better at a glance and costs 4 fewer columns per entry, so more
+entries fit per row in `grid` mode. It uses `ribbon_selected` rather than
+`text_selected` because themes give the former real contrast — it is what marks
+a selected item elsewhere in zellij — while the latter's background is often a
+hair off the terminal's own and would read as no highlight at all.
+
The list windows around the focused entry and reports the remainder as `+N more`
in the padding row it already reserves, so the indicator never costs a row or
steals one from a real entry. **Click that row to expand** the stack to its full
diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs
index 6a88ff58..02b2a87c 100644
--- a/zellij-server/src/lib.rs
+++ b/zellij-server/src/lib.rs
@@ -468,6 +468,7 @@ impl SessionMetaData {
responsive_min_cols: new_config.ui.pane_frames.responsive_min_cols,
stack_list_max_rows: new_config.ui.pane_frames.stack_list_max_rows,
stack_list_grid: new_config.ui.pane_frames.stack_list_grid,
+ stack_list_style: new_config.ui.pane_frames.stack_list_style,
responsive_layout_name: new_config.ui.pane_frames.responsive_layout.clone(),
stacked_resize: new_config.options.stacked_resize.unwrap_or(true),
stacked_pane_list: new_config.options.stacked_pane_list.unwrap_or(true),
diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs
index 5a96e652..0ec911f6 100644
--- a/zellij-server/src/screen.rs
+++ b/zellij-server/src/screen.rs
@@ -48,6 +48,7 @@ use zellij_utils::data::{
ResizeStrategy, SessionInfo, Styling, TabInfo, ThemeHue, WebSharing,
};
use zellij_utils::errors::prelude::*;
+use zellij_utils::input::theme::StackListStyle;
use zellij_utils::input::actions::Action;
use zellij_utils::input::command::RunCommand;
use zellij_utils::input::config::Config;
@@ -786,6 +787,7 @@ pub enum ScreenInstruction {
responsive_layout_name: String,
stack_list_max_rows: usize,
stack_list_grid: bool,
+ stack_list_style: StackListStyle,
stacked_resize: bool,
stacked_pane_list: bool,
default_editor: Option<PathBuf>,
@@ -1564,6 +1566,7 @@ pub(crate) struct Screen {
responsive_layout_name: String,
stack_list_max_rows: usize,
stack_list_grid: bool,
+ stack_list_style: StackListStyle,
currently_marking_pane_group: Rc<RefCell<HashMap<ClientId, bool>>>,
// the below are the configured values - the ones that will be set if and when the web server
// is brought online
@@ -1688,6 +1691,7 @@ impl Screen {
responsive_layout_name: String,
stack_list_max_rows: usize,
stack_list_grid: bool,
+ stack_list_style: StackListStyle,
) -> Self {
let session_name = mode_info.session_name.clone().unwrap_or_default();
let session_info = SessionInfo::new(session_name.clone());
@@ -1756,6 +1760,7 @@ impl Screen {
responsive_layout_name,
stack_list_max_rows,
stack_list_grid,
+ stack_list_style,
web_server_ip,
web_server_port,
render_blocker: RenderBlocker::new(100),
@@ -4520,6 +4525,7 @@ impl Screen {
);
tab.update_stack_list_max_rows(self.stack_list_max_rows);
tab.update_stack_list_grid(self.stack_list_grid);
+ tab.update_stack_list_style(self.stack_list_style);
for (client_id, mode_info) in &self.mode_info {
tab.change_mode_info(mode_info.clone(), *client_id);
}
@@ -6324,6 +6330,7 @@ impl Screen {
responsive_layout_name: String,
stack_list_max_rows: usize,
stack_list_grid: bool,
+ stack_list_style: StackListStyle,
stacked_resize: bool,
stacked_pane_list: bool,
default_editor: Option<PathBuf>,
@@ -6363,6 +6370,7 @@ impl Screen {
self.responsive_min_cols = responsive_min_cols;
self.stack_list_max_rows = stack_list_max_rows;
self.stack_list_grid = stack_list_grid;
+ self.stack_list_style = stack_list_style;
self.responsive_layout_name = responsive_layout_name.clone();
self.nested_session_handling = nested_session_handling;
self.default_mode_info
@@ -6390,6 +6398,7 @@ impl Screen {
tab.update_responsive_layout(responsive_min_cols, responsive_layout_name.clone());
tab.update_stack_list_max_rows(stack_list_max_rows);
tab.update_stack_list_grid(stack_list_grid);
+ tab.update_stack_list_style(stack_list_style);
tab.update_frame_layout(
pane_title_alignment,
pane_gap_x,
@@ -7497,6 +7506,7 @@ pub(crate) fn screen_thread_main(
let responsive_min_cols = config.ui.pane_frames.responsive_min_cols;
let stack_list_max_rows = config.ui.pane_frames.stack_list_max_rows;
let stack_list_grid = config.ui.pane_frames.stack_list_grid;
+ let stack_list_style = config.ui.pane_frames.stack_list_style;
let responsive_layout_name = config.ui.pane_frames.responsive_layout.clone();
let config_options = config.options;
@@ -7611,6 +7621,7 @@ pub(crate) fn screen_thread_main(
responsive_layout_name,
stack_list_max_rows,
stack_list_grid,
+ stack_list_style,
);
screen.host_theme_dark_styling = host_theme_dark_styling;
screen.host_theme_light_styling = host_theme_light_styling;
@@ -10936,6 +10947,7 @@ pub(crate) fn screen_thread_main(
responsive_layout_name,
stack_list_max_rows,
stack_list_grid,
+ stack_list_style,
stacked_resize,
stacked_pane_list,
default_editor,
@@ -10973,6 +10985,7 @@ pub(crate) fn screen_thread_main(
responsive_layout_name,
stack_list_max_rows,
stack_list_grid,
+ stack_list_style,
stacked_resize,
stacked_pane_list,
default_editor,
diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs
index 65b272f8..7cda7506 100644
--- a/zellij-server/src/tab/mod.rs
+++ b/zellij-server/src/tab/mod.rs
@@ -21,6 +21,7 @@ use zellij_utils::data::{
PluginPermission, RegexHighlight, ResizeStrategy, Style, StyledText, WebSharing,
};
use zellij_utils::errors::prelude::*;
+use zellij_utils::input::theme::StackListStyle;
use zellij_utils::input::command::RunCommand;
use zellij_utils::input::mouse::MouseEvent;
use zellij_utils::position::Position;
@@ -207,9 +208,6 @@ pub struct StackListLayout {
pub hidden: usize,
}
-/// Columns a picker entry spends on decoration: the "> " selection marker, and a
-/// border plus padding on each side.
-const STACK_LIST_ENTRY_DECORATION_COLS: usize = 6;
/// Below this an entry has no room for any label at all.
const STACK_LIST_MIN_ENTRY_COLS: usize = 8;
/// Blank columns between grid cells. Without a gap the focused entry's "> "
@@ -394,6 +392,8 @@ pub(crate) struct Tab {
expanded_stack_lists: HashSet<StackListId>,
/// Pack picker entries into a grid rather than one per row.
stack_list_grid: bool,
+ /// Visual treatment of picker entries.
+ stack_list_style: StackListStyle,
/// So the "no such swap layout" warning is logged once, not per resize.
responsive_missing_layout_warned: bool,
currently_marking_pane_group: Rc<RefCell<HashMap<ClientId, bool>>>,
@@ -1214,6 +1214,7 @@ impl Tab {
stack_list_max_rows: 0,
expanded_stack_lists: HashSet::new(),
stack_list_grid: false,
+ stack_list_style: StackListStyle::default(),
responsive_missing_layout_warned: false,
connected_clients_in_app,
web_server_ip,
@@ -1415,7 +1416,9 @@ impl Tab {
widest_label: usize,
) -> StackListLayout {
let expanded = self.expanded_stack_lists.contains(&stack_list_id);
- let natural_cell = (widest_label + STACK_LIST_ENTRY_DECORATION_COLS)
+ // from the style, so the column arithmetic and the renderer cannot
+ // disagree about how wide an entry's decoration is
+ let natural_cell = (widest_label + self.stack_list_style.decoration_cols())
.max(STACK_LIST_MIN_ENTRY_COLS);
let columns = if self.stack_list_grid {
// standard packing: n cells and n-1 gaps must fit
@@ -2115,6 +2118,7 @@ impl Tab {
*member == list.visible,
stack_is_focused,
Some(rank + 1),
+ self.stack_list_style,
);
for (client_id, client_mode) in &client_modes {
pane_contents_and_ui.render_pane_frame(
@@ -8084,6 +8088,9 @@ impl Tab {
pub fn update_stack_list_grid(&mut self, grid: bool) {
self.stack_list_grid = grid;
}
+ pub fn update_stack_list_style(&mut self, style: StackListStyle) {
+ self.stack_list_style = style;
+ }
pub fn update_focus_follows_mouse(&mut self, focus_follows_mouse: bool) {
self.focus_follows_mouse = focus_follows_mouse;
}
diff --git a/zellij-server/src/tab/unit/tab_integration_tests.rs b/zellij-server/src/tab/unit/tab_integration_tests.rs
index cfde77aa..d59e86b6 100644
--- a/zellij-server/src/tab/unit/tab_integration_tests.rs
+++ b/zellij-server/src/tab/unit/tab_integration_tests.rs
@@ -16712,3 +16712,37 @@ fn sl0p_responsive_collapses_even_when_the_named_layout_never_applies() {
widths(&tab)
);
}
+
+#[test]
+fn sl0p_stack_list_style_changes_the_cell_width_the_layout_reserves() {
+ // The renderer and Tab's column arithmetic must agree on how much room an
+ // entry's decoration takes, or the grid misaligns. Both read
+ // StackListStyle::decoration_cols, so assert the layout actually follows it.
+ let size = Size {
+ cols: 121,
+ rows: 40,
+ };
+ let mut tab = create_new_tab(size, ModeInfo::default());
+ tab.update_stack_list_grid(true);
+ tab.update_stack_list_max_rows(3);
+
+ let widest = 10usize;
+ let boxed = tab.stack_list_layout(0, 6, 0, 120, widest);
+ tab.update_stack_list_style(zellij_utils::input::theme::StackListStyle::Minimal);
+ let minimal = tab.stack_list_layout(0, 6, 0, 120, widest);
+
+ let boxed_width = boxed.cells.first().unwrap().width;
+ let minimal_width = minimal.cells.first().unwrap().width;
+ assert_eq!(boxed_width, widest + 6, "boxed spends 6 columns on decoration");
+ assert_eq!(
+ minimal_width,
+ widest + 2,
+ "minimal drops the marker and both borders, keeping only padding"
+ );
+ assert!(
+ minimal.cells.len() >= boxed.cells.len(),
+ "narrower cells must fit at least as many entries: boxed {} vs minimal {}",
+ boxed.cells.len(),
+ minimal.cells.len()
+ );
+}
diff --git a/zellij-server/src/ui/pane_boundaries_frame.rs b/zellij-server/src/ui/pane_boundaries_frame.rs
index a551ad05..ba6258f6 100644
--- a/zellij-server/src/ui/pane_boundaries_frame.rs
+++ b/zellij-server/src/ui/pane_boundaries_frame.rs
@@ -10,6 +10,7 @@ use crate::ui::hint_text::{
};
use crate::ClientId;
use zellij_utils::data::{client_id_to_colors, PaletteColor, PaneTitleAlignment, Style};
+use zellij_utils::input::theme::StackListStyle;
use zellij_utils::errors::prelude::*;
use zellij_utils::pane_size::{Offset, PaneGeom, Viewport};
use zellij_utils::position::Position;
@@ -106,6 +107,20 @@ fn dimmed_foreground_color(
})
}
+/// Foreground and background both set, so a run of text reads as a solid block.
+/// Used to mark the active picker entry without spending a column on a marker.
+fn inverted_color(
+ characters: &str,
+ background: PaletteColor,
+ foreground: PaletteColor,
+) -> Vec<TerminalCharacter> {
+ styled_characters(characters, |styles| {
+ styles.background = Some(AnsiCode::from(background));
+ styles.foreground = Some(AnsiCode::from(foreground));
+ styles.bold = Some(AnsiCode::On);
+ })
+}
+
fn background_color(characters: &str, color: Option<PaletteColor>) -> Vec<TerminalCharacter> {
styled_characters(characters, |styles| {
if let Some(palette_color) = color {
@@ -131,6 +146,7 @@ pub struct StackListEntry {
/// 1-based position in the stack, rendered as a prefix. See
/// stack_index_prefix.
pub index: Option<usize>,
+ pub style: StackListStyle,
}
/// Prefix shown before a stack list entry's title.
@@ -897,10 +913,17 @@ impl PaneFrame {
}
fn render_stack_list_entry(&self, entry: &StackListEntry) -> Vec<TerminalCharacter> {
let usable_cols = self.geom.cols;
- let selection_marker = "> ";
+ let minimal = entry.style == StackListStyle::Minimal;
+ // Minimal drops the marker and the borders entirely: the active entry is
+ // shown by inverting it instead, which needs no glyphs and no column.
+ let selection_marker = if minimal { "" } else { "> " };
let marker_width = selection_marker.width();
let corner_padding = 1;
- let corner_overhead = 2 * (1 + corner_padding);
+ let corner_overhead = if minimal {
+ 2 * corner_padding
+ } else {
+ 2 * (1 + corner_padding)
+ };
let inner_width = entry
.width
.min(usable_cols.saturating_sub(marker_width + corner_overhead));
@@ -934,8 +957,16 @@ impl PaneFrame {
line.push(EMPTY_TERMINAL_CHARACTER);
}
let unfocused_color = self.style.colors.frame_unselected.map(|frame| frame.base);
+ // ribbon_selected, not text_selected: themes give ribbon_selected real
+ // contrast because it marks the selected item in zellij's own UI, while
+ // text_selected's background is often a hair off the terminal's own and
+ // would read as no highlight at all.
+ let selected_colors = self.style.colors.ribbon_selected;
let style_entry_text = |text: &str| {
- if entry.is_selected || entry.is_emphasized {
+ if minimal && entry.is_selected {
+ // inverted: the entry's own block IS the indicator
+ inverted_color(text, selected_colors.background, selected_colors.base)
+ } else if entry.is_selected || entry.is_emphasized {
foreground_color(text, self.color)
} else if entry.stack_is_focused {
foreground_color(text, unfocused_color)
@@ -950,19 +981,34 @@ impl PaneFrame {
line.push(EMPTY_TERMINAL_CHARACTER);
}
}
- line.append(&mut foreground_color(boundary_type::VERTICAL, None));
- for _ in 0..corner_padding {
- line.push(EMPTY_TERMINAL_CHARACTER);
+ if !minimal {
+ line.append(&mut foreground_color(boundary_type::VERTICAL, None));
}
- line.append(&mut style_entry_text(&content));
let content_padding = inner_width.saturating_sub(content.width());
- for _ in 0..content_padding {
- line.push(EMPTY_TERMINAL_CHARACTER);
- }
- for _ in 0..corner_padding {
- line.push(EMPTY_TERMINAL_CHARACTER);
+ if minimal {
+ // pad INSIDE the styled run, so the inverted block covers the whole
+ // cell rather than leaving unstyled gaps at its edges
+ let padded = format!(
+ "{}{}{}{}",
+ " ".repeat(corner_padding),
+ content,
+ " ".repeat(content_padding),
+ " ".repeat(corner_padding)
+ );
+ line.append(&mut style_entry_text(&padded));
+ } else {
+ for _ in 0..corner_padding {
+ line.push(EMPTY_TERMINAL_CHARACTER);
+ }
+ line.append(&mut style_entry_text(&content));
+ for _ in 0..content_padding {
+ line.push(EMPTY_TERMINAL_CHARACTER);
+ }
+ for _ in 0..corner_padding {
+ line.push(EMPTY_TERMINAL_CHARACTER);
+ }
+ line.append(&mut foreground_color(boundary_type::VERTICAL, None));
}
- line.append(&mut foreground_color(boundary_type::VERTICAL, None));
let mut occupied_columns = entry_start + full_width_entry_length;
let (mut scroll_part, scroll_length) = self
.bracketed_scroll_indicator(usable_cols.saturating_sub(occupied_columns))
diff --git a/zellij-server/src/ui/pane_contents_and_ui.rs b/zellij-server/src/ui/pane_contents_and_ui.rs
index e7944036..8fd1d34c 100644
--- a/zellij-server/src/ui/pane_contents_and_ui.rs
+++ b/zellij-server/src/ui/pane_contents_and_ui.rs
@@ -8,6 +8,7 @@ use crate::ui::pane_boundaries_frame::{FrameParams, StackListEntry};
use crate::ClientId;
use std::collections::{HashMap, HashSet};
use zellij_utils::data::{client_id_to_colors, InputMode, PaletteColor, Style};
+use zellij_utils::input::theme::StackListStyle;
use zellij_utils::errors::prelude::*;
use zellij_utils::pane_size::PaneGeom;
@@ -40,6 +41,7 @@ pub struct PaneContentsAndUi<'a> {
stack_list_entry_is_selected: bool,
stack_list_entry_stack_is_focused: bool,
stack_list_entry_index: Option<usize>,
+ stack_list_entry_style: StackListStyle,
blank_title: bool,
mouse_scroll_resize: bool,
dimmed_for_clients: HashSet<ClientId>,
@@ -98,6 +100,7 @@ impl<'a> PaneContentsAndUi<'a> {
stack_list_entry_is_selected: false,
stack_list_entry_stack_is_focused: false,
stack_list_entry_index: None,
+ stack_list_entry_style: StackListStyle::default(),
blank_title: false,
mouse_scroll_resize,
dimmed_for_clients,
@@ -118,11 +121,13 @@ impl<'a> PaneContentsAndUi<'a> {
stack_list_entry_is_selected: bool,
stack_list_entry_stack_is_focused: bool,
stack_list_entry_index: Option<usize>,
+ stack_list_entry_style: StackListStyle,
) {
self.stack_list_entry_width = stack_list_entry_width;
self.stack_list_entry_is_selected = stack_list_entry_is_selected;
self.stack_list_entry_stack_is_focused = stack_list_entry_stack_is_focused;
self.stack_list_entry_index = stack_list_entry_index;
+ self.stack_list_entry_style = stack_list_entry_style;
}
pub fn render_pane_contents_to_multiple_clients(
&mut self,
@@ -361,6 +366,7 @@ impl<'a> PaneContentsAndUi<'a> {
is_selected: self.stack_list_entry_is_selected,
stack_is_focused: self.stack_list_entry_stack_is_focused,
index: self.stack_list_entry_index,
+ style: self.stack_list_entry_style,
is_emphasized: pane_is_in_group
|| (self
.mouse_is_hovering_over_pane_for_clients
diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs
index a4004b4d..87b4af2c 100644
--- a/zellij-server/src/unit/screen_tests.rs
+++ b/zellij-server/src/unit/screen_tests.rs
@@ -334,6 +334,7 @@ fn create_new_screen_with_kitty_graphics(
String::from("stacked"),
0,
false,
+ Default::default(),
);
screen
}
@@ -5546,6 +5547,7 @@ fn create_new_screen_with_message_capture(
String::from("stacked"),
0,
false,
+ Default::default(),
);
(screen, messages)
}
@@ -8648,6 +8650,7 @@ fn create_new_screen_with_forward_capture(size: Size) -> (Screen, ForwardCapture
String::from("stacked"),
0,
false,
+ Default::default(),
);
(
screen,
@@ -9325,6 +9328,7 @@ fn create_new_screen_with_theme_capture(size: Size) -> (Screen, ThemeCapture) {
String::from("stacked"),
0,
false,
+ Default::default(),
);
(
screen,
@@ -9859,6 +9863,7 @@ fn create_non_mirrored_screen(size: Size) -> Screen {
String::from("stacked"),
0,
false,
+ Default::default(),
)
}
diff --git a/zellij-utils/src/input/config.rs b/zellij-utils/src/input/config.rs
index 4cb315ca..08bfa4b0 100644
--- a/zellij-utils/src/input/config.rs
+++ b/zellij-utils/src/input/config.rs
@@ -1498,6 +1498,7 @@ mod config_test {
responsive_layout: String::from("stacked"),
stack_list_max_rows: 0,
stack_list_grid: false,
+ stack_list_style: Default::default(),
},
};
assert_eq!(config.ui, expected_ui_config, "Ui config defined in config");
diff --git a/zellij-utils/src/input/theme.rs b/zellij-utils/src/input/theme.rs
index 412aaae2..646fa0ed 100644
--- a/zellij-utils/src/input/theme.rs
+++ b/zellij-utils/src/input/theme.rs
@@ -9,6 +9,46 @@ use std::{
use crate::data::Styling;
+/// Visual treatment of the stacked-pane picker's entries.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
+pub enum StackListStyle {
+ /// Each entry bracketed in vertical bars, with a "> " marker on the active
+ /// one.
+ Boxed,
+ /// No brackets and no marker; the active entry is shown by inverting its
+ /// foreground and background instead.
+ Minimal,
+}
+
+impl Default for StackListStyle {
+ fn default() -> Self {
+ StackListStyle::Boxed
+ }
+}
+
+impl StackListStyle {
+ pub fn from_str(name: &str) -> Option<Self> {
+ match name.trim().to_lowercase().as_str() {
+ "boxed" => Some(StackListStyle::Boxed),
+ "minimal" => Some(StackListStyle::Minimal),
+ _ => None,
+ }
+ }
+ pub fn possible_values() -> &'static str {
+ "\"boxed\", \"minimal\""
+ }
+ /// Columns an entry spends on decoration. The picker's column arithmetic
+ /// lives in Tab and must agree with the renderer, so both read this.
+ pub fn decoration_cols(&self) -> usize {
+ match self {
+ // "> " marker, plus a border and a pad on each side
+ StackListStyle::Boxed => 6,
+ // one pad each side, so the inverted block has some air around it
+ StackListStyle::Minimal => 2,
+ }
+ }
+}
+
#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)]
pub struct UiConfig {
pub pane_frames: FrameConfig,
@@ -55,6 +95,8 @@ pub struct FrameConfig {
/// Pack picker entries into a grid instead of one per row. The row cap then
/// becomes a space budget rather than a count.
pub stack_list_grid: bool,
+ /// Visual treatment of picker entries.
+ pub stack_list_style: StackListStyle,
}
/// Hand-written rather than derived: `gap_aspect` must default to 2 to match
@@ -76,6 +118,7 @@ impl Default for FrameConfig {
responsive_layout: String::from("stacked"),
stack_list_max_rows: 0,
stack_list_grid: false,
+ stack_list_style: StackListStyle::Boxed,
}
}
}
@@ -108,6 +151,7 @@ impl FrameConfig {
merged.responsive_layout = other.responsive_layout.clone();
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
}
}
diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs
index dda62c87..3213b99d 100644
--- a/zellij-utils/src/kdl/mod.rs
+++ b/zellij-utils/src/kdl/mod.rs
@@ -15,7 +15,7 @@ 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, Theme, Themes, UiConfig};
+use crate::input::theme::{FrameConfig, StackListStyle, Theme, Themes, UiConfig};
use crate::input::web_client::WebClientConfig;
use kdl_layout_parser::KdlLayoutParser;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
@@ -5632,6 +5632,24 @@ impl UiConfig {
},
None => false,
};
+ let stack_list_style =
+ match kdl_children_property_first_arg_as_string!(pane_frames, "stack_list_style") {
+ Some(raw) => match StackListStyle::from_str(raw) {
+ Some(style) => style,
+ None => {
+ return Err(ConfigError::new_kdl_error(
+ format!(
+ "Unknown stack_list_style: \"{}\". Valid values are: {}",
+ raw,
+ StackListStyle::possible_values()
+ ),
+ pane_frames.span().offset(),
+ pane_frames.span().len(),
+ ))
+ },
+ },
+ None => StackListStyle::default(),
+ };
let frame_config = FrameConfig {
rounded_corners,
hide_session_name,
@@ -5645,6 +5663,7 @@ impl UiConfig {
responsive_layout,
stack_list_max_rows,
stack_list_grid,
+ stack_list_style,
};
ui_config.pane_frames = frame_config;
}
diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap
index c2c29be5..ae91dafc 100644
--- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap
+++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap
@@ -6245,6 +6245,7 @@ Config {
responsive_layout: "stacked",
stack_list_max_rows: 0,
stack_list_grid: false,
+ stack_list_style: Boxed,
},
},
env: {},
diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap
index bc58ffff..f7f87561 100644
--- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap
+++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap
@@ -6245,6 +6245,7 @@ Config {
responsive_layout: "stacked",
stack_list_max_rows: 0,
stack_list_grid: false,
+ stack_list_style: Boxed,
},
},
env: {
diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap
index 75cbc50e..692ffc41 100644
--- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap
+++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap
@@ -307,6 +307,7 @@ Config {
responsive_layout: "stacked",
stack_list_max_rows: 0,
stack_list_grid: false,
+ stack_list_style: Boxed,
},
},
env: {},
diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap
index d04f6e93..05b1553e 100644
--- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap
+++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap
@@ -8016,6 +8016,7 @@ Config {
responsive_layout: "stacked",
stack_list_max_rows: 0,
stack_list_grid: false,
+ stack_list_style: Boxed,
},
},
env: {},
diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap
index 966fed4e..67dade83 100644
--- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap
+++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap
@@ -6245,6 +6245,7 @@ Config {
responsive_layout: "stacked",
stack_list_max_rows: 0,
stack_list_grid: false,
+ stack_list_style: Boxed,
},
},
env: {},