diff options
| author | user <user@clank> | 2026-08-11 20:08:23 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-11 20:08:23 +0200 |
| commit | 3418e8218d91d916b3b2a45ac81d3032c27575c6 (patch) | |
| tree | 4853d3b368d18480efe7dc4ef994e9d7e99a94e0 /zellij-server | |
| parent | sl0p: resolve responsive layouts from current state, not a remembered position (diff) | |
| download | sl0ppij-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.
Diffstat (limited to 'zellij-server')
| -rw-r--r-- | zellij-server/src/lib.rs | 1 | ||||
| -rw-r--r-- | zellij-server/src/screen.rs | 13 | ||||
| -rw-r--r-- | zellij-server/src/tab/mod.rs | 15 | ||||
| -rw-r--r-- | zellij-server/src/tab/unit/tab_integration_tests.rs | 34 | ||||
| -rw-r--r-- | zellij-server/src/ui/pane_boundaries_frame.rs | 72 | ||||
| -rw-r--r-- | zellij-server/src/ui/pane_contents_and_ui.rs | 6 | ||||
| -rw-r--r-- | zellij-server/src/unit/screen_tests.rs | 5 |
7 files changed, 129 insertions, 17 deletions
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(), ) } |
