diff options
| author | user <user@clank> | 2026-08-11 00:12:42 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-11 00:12:42 +0200 |
| commit | 26ef6c4e925981c86faed879204056cdc3ccbb93 (patch) | |
| tree | 34a1b96754fe7d6c9186fcd249d0692d210c641e | |
| parent | sl0p: collapse to a swap layout when the terminal gets narrow (diff) | |
| download | sl0ppij-26ef6c4e925981c86faed879204056cdc3ccbb93.tar.gz sl0ppij-26ef6c4e925981c86faed879204056cdc3ccbb93.tar.xz sl0ppij-26ef6c4e925981c86faed879204056cdc3ccbb93.zip | |
sl0p: keep stacked-layout entries identifiable
Two problems made the stack list unusable for panes nothing had named yet,
which is the common case right after opening them -- and the responsive
collapse walks straight into it, since collapsing is exactly when the list
becomes the only way to pick a pane.
`hide_default_pane_names` no longer applies to stack list entries. In a frame
an empty title is fine: the pane is still visible, sized and positioned, so
you can tell what it is by looking at it. In the stack list the label is the
ENTIRE affordance, so blanking it leaves rows that cannot be identified or
told apart. Entries fall back to the "Pane #N" placeholder even when frames
are configured to hide it.
Entries are also prefixed with their 1-based position, so they stay countable
and addressable even when several share a title or have none. The prefix is
built by one shared helper because the renderer and Tab's column-width
calculation must agree on its width or the list misaligns.
6 files changed, 126 insertions, 16 deletions
diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs index 77404f8d..735bdc9d 100644 --- a/zellij-server/src/panes/terminal_pane.rs +++ b/zellij-server/src/panes/terminal_pane.rs @@ -1235,7 +1235,19 @@ impl Pane for TerminalPane { self.pane_title.clone() } fn stack_list_entry_label(&self) -> String { - self.title_with_bell_indicator(self.current_title()) + // `hide_default_pane_names` deliberately does NOT apply here. + // + // In a frame, an empty title is fine: the pane is still visible, sized + // and positioned, so you can tell what it is by looking at it. In the + // stack list the label is the ENTIRE affordance -- blanking it leaves a + // row that cannot be identified or distinguished from its neighbours. + // So the stack list falls back to the "Pane #N" placeholder even when + // frames are configured to hide it. + let title = match self.current_title() { + title if title.is_empty() => self.pane_title.clone(), + title => title, + }; + self.title_with_bell_indicator(title) } fn custom_title(&self) -> Option<String> { if self.pane_name.is_empty() { diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index 2d8c00c8..cf413e63 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -35,7 +35,8 @@ use crate::ui::hint_text::{ held_hint_variants, hover_hint_variants, resize_hint_variants, HintExitStatus, }; use crate::ui::{ - loading_indication::LoadingIndication, pane_boundaries_frame::FrameParams, + loading_indication::LoadingIndication, + pane_boundaries_frame::{stack_index_prefix, FrameParams}, pane_contents_and_ui::PaneContentsAndUi, }; use layout_applier::LayoutApplier; @@ -1697,17 +1698,23 @@ impl Tab { .cols .as_usize() .saturating_sub(visible_content_offset.left + visible_content_offset.right); + // NB: measured INCLUDING the index prefix each entry will be + // rendered with, otherwise every entry is one prefix too narrow and + // the list misaligns. let widest_member_title = list .members .iter() - .filter_map(|member| { - if *member == list.visible { + .enumerate() + .filter_map(|(rank, member)| { + let pane = if *member == list.visible { self.tiled_panes.get_pane(*member).map(|p| &**p) } else { self.suppressed_panes.get(member).map(|(_, p)| &**p) - } + }; + pane.map(|pane| { + stack_index_prefix(rank + 1).width() + pane.stack_list_entry_label().width() + }) }) - .map(|pane| pane.stack_list_entry_label().width()) .max() .unwrap_or(0); let stack_is_focused = active_panes.values().any(|p_id| *p_id == list.visible); @@ -1749,6 +1756,7 @@ impl Tab { Some(widest_member_title), *member == list.visible, stack_is_focused, + Some(rank + 1), ); for (client_id, client_mode) in &client_modes { pane_contents_and_ui.render_pane_frame( diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_up_merging_stacks_preserves_all_panes.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_up_merging_stacks_preserves_all_panes.snap index e9214f5c..173a0a85 100644 --- a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_up_merging_stacks_preserves_all_panes.snap +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_up_merging_stacks_preserves_all_panes.snap @@ -1,10 +1,11 @@ --- source: zellij-server/src/tab/./unit/tab_integration_tests.rs +assertion_line: 13079 expression: snapshot --- -00 (C): > │ Pane #1 │ -01 (C): │ Pane #3 │ -02 (C): │ Pane #4 │ +00 (C): > │ 1: Pane #1 │ +01 (C): │ 2: Pane #3 │ +02 (C): │ 3: Pane #4 │ 03 (C): 04 (C): ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 05 (C): │ │ @@ -24,9 +25,9 @@ expression: snapshot 19 (C): │ │ 20 (C): │ │ 21 (C): └────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -22 (C): │ Pane #2 │ -23 (C): │ Pane #5 │ -24 (C): > │ Pane #6 │ +22 (C): │ 1: Pane #2 │ +23 (C): │ 2: Pane #5 │ +24 (C): > │ 3: Pane #6 │ 25 (C): 26 (C): ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 27 (C): │ │ @@ -42,4 +43,3 @@ expression: snapshot 37 (C): │ │ 38 (C): │ │ 39 (C): └────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ - diff --git a/zellij-server/src/tab/unit/tab_integration_tests.rs b/zellij-server/src/tab/unit/tab_integration_tests.rs index 145cb621..2449cac9 100644 --- a/zellij-server/src/tab/unit/tab_integration_tests.rs +++ b/zellij-server/src/tab/unit/tab_integration_tests.rs @@ -15931,3 +15931,68 @@ fn sl0p_responsive_layout_restores_even_if_the_layout_is_damaged_while_collapsed widths(&tab) ); } + +#[test] +fn sl0p_stack_list_entry_keeps_a_label_when_default_pane_names_are_hidden() { + // `hide_default_pane_names` blanks the "Pane #N" placeholder, which is what + // we want for a frame: the pane is still visible and positioned, so an empty + // title just looks clean. + // + // The stack list is the opposite case: the label is the entire affordance, + // so blanking it leaves rows that cannot be told apart -- which is exactly + // what happens to freshly created panes, since nothing has named them yet. + // The stack list must therefore keep a label regardless of the option. + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + for pid in [2, 3] { + tab.new_pane( + PaneId::Terminal(pid), + None, + None, + false, + true, + NewPanePlacement::default(), + Some(client_id), + None, + ) + .unwrap(); + } + + tab.update_frame_layout( + zellij_utils::data::PaneTitleAlignment::Left, + 0, + 0, + zellij_utils::data::PanePadding::default(), + 0, + true, // hide_default_pane_names + ); + + for (pane_id, pane) in tab.tiled_panes.panes.iter() { + // precondition: the option really is blanking the frame title, or this + // test would pass for the wrong reason + assert_eq!( + pane.current_title(), + "", + "precondition: {:?} should have a blank FRAME title while \ + hide_default_pane_names is on", + pane_id + ); + let label = pane.stack_list_entry_label(); + assert!( + !label.trim().is_empty(), + "{:?} must still get a stack list label while \ + hide_default_pane_names is on, got {:?}", + pane_id, + label + ); + assert!( + label.contains("Pane #"), + "the stack list label should fall back to the placeholder, got {:?}", + label + ); + } +} diff --git a/zellij-server/src/ui/pane_boundaries_frame.rs b/zellij-server/src/ui/pane_boundaries_frame.rs index 5e8fba65..a551ad05 100644 --- a/zellij-server/src/ui/pane_boundaries_frame.rs +++ b/zellij-server/src/ui/pane_boundaries_frame.rs @@ -128,6 +128,22 @@ pub struct StackListEntry { pub is_selected: bool, pub is_emphasized: bool, pub stack_is_focused: bool, + /// 1-based position in the stack, rendered as a prefix. See + /// stack_index_prefix. + pub index: Option<usize>, +} + +/// Prefix shown before a stack list entry's title. +/// +/// Stack entries are otherwise identified by title alone, so entries that share +/// a title -- or have none, which is the common case for freshly created panes -- +/// are indistinguishable and unaddressable. The index keeps every row countable +/// even when the titles do not help. +/// +/// Shared by the renderer and by the column-width calculation in Tab, which must +/// agree or the list will misalign. +pub fn stack_index_prefix(index: usize) -> String { + format!("{}: ", index) } pub struct FrameParams { @@ -888,12 +904,16 @@ impl PaneFrame { let inner_width = entry .width .min(usable_cols.saturating_sub(marker_width + corner_overhead)); - let content = if entry.label.width() <= inner_width { - entry.label.clone() + let label = match entry.index { + Some(index) => format!("{}{}", stack_index_prefix(index), entry.label), + None => entry.label.clone(), + }; + let content = if label.width() <= inner_width { + label.clone() } else { let truncation_budget = inner_width.saturating_sub(1); let mut truncated = String::new(); - for character in entry.label.chars() { + for character in label.chars() { if truncated.width() + character.width().unwrap_or(0) > truncation_budget { break; } diff --git a/zellij-server/src/ui/pane_contents_and_ui.rs b/zellij-server/src/ui/pane_contents_and_ui.rs index 9c9420a4..e7944036 100644 --- a/zellij-server/src/ui/pane_contents_and_ui.rs +++ b/zellij-server/src/ui/pane_contents_and_ui.rs @@ -39,6 +39,7 @@ pub struct PaneContentsAndUi<'a> { stack_list_entry_width: Option<usize>, stack_list_entry_is_selected: bool, stack_list_entry_stack_is_focused: bool, + stack_list_entry_index: Option<usize>, blank_title: bool, mouse_scroll_resize: bool, dimmed_for_clients: HashSet<ClientId>, @@ -96,6 +97,7 @@ impl<'a> PaneContentsAndUi<'a> { stack_list_entry_width: None, stack_list_entry_is_selected: false, stack_list_entry_stack_is_focused: false, + stack_list_entry_index: None, blank_title: false, mouse_scroll_resize, dimmed_for_clients, @@ -115,10 +117,12 @@ impl<'a> PaneContentsAndUi<'a> { stack_list_entry_width: Option<usize>, stack_list_entry_is_selected: bool, stack_list_entry_stack_is_focused: bool, + stack_list_entry_index: Option<usize>, ) { 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; } pub fn render_pane_contents_to_multiple_clients( &mut self, @@ -356,6 +360,7 @@ impl<'a> PaneContentsAndUi<'a> { label: self.pane.stack_list_entry_label(), is_selected: self.stack_list_entry_is_selected, stack_is_focused: self.stack_list_entry_stack_is_focused, + index: self.stack_list_entry_index, is_emphasized: pane_is_in_group || (self .mouse_is_hovering_over_pane_for_clients |
