diff options
| author | Aram Drevekenin <aram@poor.dev> | 2026-07-27 18:24:37 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-27 18:24:37 +0200 |
| commit | ea07e2d5b6bbc9ea6f9c765b0838b3e91156a58d (patch) | |
| tree | 4a8fac8a9b27f2a8b32b3af61a3b5419b9937c81 /default-plugins/compact-bar/src | |
| parent | Switch from the unmaintained daemonize crate to a maintained fork (#4512) (diff) | |
| download | sl0ppij-ea07e2d5b6bbc9ea6f9c765b0838b3e91156a58d.tar.gz sl0ppij-ea07e2d5b6bbc9ea6f9c765b0838b3e91156a58d.tar.xz sl0ppij-ea07e2d5b6bbc9ea6f9c765b0838b3e91156a58d.zip | |
Nested session handling (#5417)
* fix: theme query and updated pixel dimensions
* add no-ui-fullscreen key/action/capability
* protocol core
* integration test harness
* fix integration tests
* fix flaky tests
* nested ui (dimming) and directional focus initial implementation
* integration tests for nested ui dimming and directional focus
* initial full screen nesteed implementation
* fix: keep hidden floating panes hidden across nested no-ui fullscreen
* fix integration tests
* no ui fullscreen integration tests
* hardening behavior/tests
* fix(nested): re-derive descend/dim on host tab switch and new-tab so clicking dimmed chrome ascends
* initial modal implementation
* initial ui improvements
* fix integration tests
* improve nested selection ui
* nested session modal refactoring and tests
* improve integration test speed somewhat
* some ui adjustments
* simplify full/auto nested session handling
* hints in compact-bar
* guest stdin leak fix
* allow floating panes to be fullscreen
* make nested session handling configurable
* protocol reannounce
* refactor: naming confusion
* improve ux somewhat
* rustfmt
* merge fixes
* improve integration test rate limiting (fix ci)
* add pr
Diffstat (limited to 'default-plugins/compact-bar/src')
| -rw-r--r-- | default-plugins/compact-bar/src/line.rs | 322 | ||||
| -rw-r--r-- | default-plugins/compact-bar/src/main.rs | 20 | ||||
| -rw-r--r-- | default-plugins/compact-bar/src/tab.rs | 28 |
3 files changed, 320 insertions, 50 deletions
diff --git a/default-plugins/compact-bar/src/line.rs b/default-plugins/compact-bar/src/line.rs index 089b6ed3..aae8aec0 100644 --- a/default-plugins/compact-bar/src/line.rs +++ b/default-plugins/compact-bar/src/line.rs @@ -5,13 +5,25 @@ use crate::{LinePart, TabRenderData, ARROW_SEPARATOR}; use zellij_tile::prelude::*; use zellij_tile_utils::style; +pub struct TabLineOutput { + pub parts: Vec<LinePart>, + pub breadcrumb_range: Option<(usize, usize)>, +} + pub fn tab_line( mode_info: &ModeInfo, tab_data: TabRenderData, cols: usize, toggle_tooltip_key: Option<String>, tooltip_is_active: bool, -) -> Vec<LinePart> { +) -> TabLineOutput { + let dimmed = mode_info.session_ascended == Some(true) || mode_info.session_dimmed == Some(true); + let breadcrumb_ancestry = if mode_info.host_fullscreen == Some(true) { + mode_info.session_ancestry.clone() + } else { + Vec::new() + }; + let nested_hint = NestedSessionHint::from_mode_info(mode_info); let config = TabLineConfig { session_name: mode_info.session_name.to_owned(), hide_session_name: mode_info.style.hide_session_name, @@ -20,6 +32,9 @@ pub fn tab_line( is_swap_layout_dirty: tab_data.is_swap_layout_dirty, toggle_tooltip_key, tooltip_is_active, + dimmed, + breadcrumb_ancestry, + nested_hint, }; let builder = TabLineBuilder::new(config, mode_info.style.colors, mode_info.capabilities, cols); @@ -27,6 +42,29 @@ pub fn tab_line( } #[derive(Debug, Clone)] +pub enum NestedSessionHint { + None, + Ascend(Vec<KeyWithModifier>), + Descend(Vec<KeyWithModifier>), +} + +impl NestedSessionHint { + fn from_mode_info(mode_info: &ModeInfo) -> Self { + if mode_info.session_dimmed == Some(true) { + NestedSessionHint::Ascend(mode_info.nested_ascend_keys.clone()) + } else if mode_info.session_ascended == Some(true) { + NestedSessionHint::Descend(mode_info.nested_descend_keys.clone()) + } else { + NestedSessionHint::None + } + } + + fn is_active(&self) -> bool { + !matches!(self, NestedSessionHint::None) + } +} + +#[derive(Debug, Clone)] pub struct TabLineConfig { pub session_name: Option<String>, pub hide_session_name: bool, @@ -35,24 +73,38 @@ pub struct TabLineConfig { pub is_swap_layout_dirty: bool, pub toggle_tooltip_key: Option<String>, pub tooltip_is_active: bool, + pub dimmed: bool, + pub breadcrumb_ancestry: Vec<String>, + pub nested_hint: NestedSessionHint, } fn calculate_total_length(parts: &[LinePart]) -> usize { parts.iter().map(|p| p.len).sum() } +fn dim_style(bg: PaletteColor) -> ansi_term::Style { + ansi_term::Style::new() + .on(match bg { + PaletteColor::Rgb((r, g, b)) => ansi_term::Color::RGB(r, g, b), + PaletteColor::EightBit(c) => ansi_term::Color::Fixed(c), + }) + .dimmed() +} + struct TabLinePopulator { cols: usize, palette: Styling, capabilities: PluginCapabilities, + dimmed: bool, } impl TabLinePopulator { - fn new(cols: usize, palette: Styling, capabilities: PluginCapabilities) -> Self { + fn new(cols: usize, palette: Styling, capabilities: PluginCapabilities, dimmed: bool) -> Self { Self { cols, palette, capabilities, + dimmed, } } @@ -218,9 +270,15 @@ impl TabLinePopulator { background: self.palette.text_selected.emphasis_0, }; + let text_style = if self.dimmed { + style!(colors.text, colors.background).italic() + } else { + style!(colors.text, colors.background).bold() + }; + let styled_parts = [ style!(colors.separator, colors.background).paint(separator), - style!(colors.text, colors.background).bold().paint(text), + text_style.paint(text), style!(colors.background, colors.separator).paint(separator), ]; @@ -264,22 +322,43 @@ enum TabAction { Finish, } -struct TabLinePrefixBuilder { +struct TabLinePrefixBuilder<'a> { palette: Styling, cols: usize, + dimmed: bool, + breadcrumb_ancestry: &'a [String], +} + +struct TabLinePrefix { + parts: Vec<LinePart>, + breadcrumb_range: Option<(usize, usize)>, } -impl TabLinePrefixBuilder { - fn new(palette: Styling, cols: usize) -> Self { - Self { palette, cols } +impl<'a> TabLinePrefixBuilder<'a> { + fn new(palette: Styling, cols: usize, dimmed: bool, breadcrumb_ancestry: &'a [String]) -> Self { + Self { + palette, + cols, + dimmed, + breadcrumb_ancestry, + } } - fn build(&self, session_name: Option<&str>, mode: InputMode) -> Vec<LinePart> { + fn build(&self, session_name: Option<&str>, mode: InputMode) -> TabLinePrefix { let mut parts = vec![self.create_zellij_part()]; let mut used_len = parts.get(0).map_or(0, |p| p.len); + let mut breadcrumb_range = None; if let Some(name) = session_name { - if let Some(name_part) = self.create_session_name_part(name, used_len) { + if !self.breadcrumb_ancestry.is_empty() { + if let Some((mut breadcrumb_parts, range, breadcrumb_len)) = + self.create_breadcrumb_parts(name, used_len) + { + used_len += breadcrumb_len; + breadcrumb_range = Some(range); + parts.append(&mut breadcrumb_parts); + } + } else if let Some(name_part) = self.create_session_name_part(name, used_len) { used_len += name_part.len; parts.push(name_part); } @@ -289,18 +368,81 @@ impl TabLinePrefixBuilder { parts.push(mode_part); } - parts + TabLinePrefix { + parts, + breadcrumb_range, + } + } + + fn create_breadcrumb_parts( + &self, + name: &str, + used_len: usize, + ) -> Option<(Vec<LinePart>, (usize, usize), usize)> { + let bg_color = self.palette.text_unselected.background; + let text_color = self.palette.text_unselected.base; + let ancestor_text = format!("({} ▸ ", self.breadcrumb_ancestry.join(" ▸ ")); + let ancestor_len = ancestor_text.width(); + let name_text = name.to_string(); + let name_len = name_text.width(); + let closing_text = ") ".to_string(); + let closing_len = closing_text.width(); + let total_len = ancestor_len + name_len + closing_len; + + if self.cols.saturating_sub(used_len) < total_len { + return None; + } + + let ancestor_style = if self.dimmed { + dim_style(bg_color) + } else { + style!(text_color, bg_color).italic() + }; + let name_style = if self.dimmed { + dim_style(bg_color) + } else { + style!(self.palette.text_unselected.emphasis_0, bg_color).bold() + }; + let closing_style = if self.dimmed { + dim_style(bg_color) + } else { + style!(text_color, bg_color).bold() + }; + + let ancestor_start = used_len; + let ancestor_end = ancestor_start + ancestor_len; + let parts = vec![ + LinePart { + part: ancestor_style.paint(ancestor_text).to_string(), + len: ancestor_len, + tab_index: None, + }, + LinePart { + part: name_style.paint(name_text).to_string(), + len: name_len, + tab_index: None, + }, + LinePart { + part: closing_style.paint(closing_text).to_string(), + len: closing_len, + tab_index: None, + }, + ]; + + Some((parts, (ancestor_start, ancestor_end), total_len)) } fn create_zellij_part(&self) -> LinePart { let prefix_text = " Zellij "; let colors = self.get_text_colors(); + let text_style = if self.dimmed { + style!(colors.text, colors.background).italic() + } else { + style!(colors.text, colors.background).bold() + }; LinePart { - part: style!(colors.text, colors.background) - .bold() - .paint(prefix_text) - .to_string(), + part: text_style.paint(prefix_text).to_string(), len: prefix_text.chars().count(), tab_index: None, } @@ -312,11 +454,13 @@ impl TabLinePrefixBuilder { if self.cols.saturating_sub(used_len) >= name_part_len { let colors = self.get_text_colors(); + let text_style = if self.dimmed { + style!(colors.text, colors.background).italic() + } else { + style!(colors.text, colors.background).bold() + }; Some(LinePart { - part: style!(colors.text, colors.background) - .bold() - .paint(name_part) - .to_string(), + part: text_style.paint(name_part).to_string(), len: name_part_len, tab_index: None, }) @@ -331,18 +475,23 @@ impl TabLinePrefixBuilder { if self.cols.saturating_sub(used_len) >= mode_len { let colors = self.get_text_colors(); - let style = match mode { - InputMode::Locked => { - style!(self.palette.text_unselected.emphasis_3, colors.background) - }, - InputMode::Normal => { - style!(self.palette.text_unselected.emphasis_2, colors.background) - }, - _ => style!(self.palette.text_unselected.emphasis_0, colors.background), + let style = if self.dimmed { + style!(colors.text, colors.background).italic() + } else { + match mode { + InputMode::Locked => { + style!(self.palette.text_unselected.emphasis_3, colors.background) + }, + InputMode::Normal => { + style!(self.palette.text_unselected.emphasis_2, colors.background) + }, + _ => style!(self.palette.text_unselected.emphasis_0, colors.background), + } + .bold() }; Some(LinePart { - part: style.bold().paint(mode_text).to_string(), + part: style.paint(mode_text).to_string(), len: mode_len, tab_index: None, }) @@ -363,17 +512,26 @@ impl TabLinePrefixBuilder { struct RightSideElementsBuilder { palette: Styling, capabilities: PluginCapabilities, + dimmed: bool, } impl RightSideElementsBuilder { - fn new(palette: Styling, capabilities: PluginCapabilities) -> Self { + fn new(palette: Styling, capabilities: PluginCapabilities, dimmed: bool) -> Self { Self { palette, capabilities, + dimmed, } } fn build(&self, config: &TabLineConfig, available_space: usize) -> Vec<LinePart> { + if config.nested_hint.is_active() { + if let Some(hint) = self.create_nested_hint(&config.nested_hint, available_space) { + return vec![hint]; + } + return Vec::new(); + } + let mut elements = Vec::new(); if let Some(ref tooltip_key) = config.toggle_tooltip_key { @@ -387,13 +545,77 @@ impl RightSideElementsBuilder { elements } + fn create_nested_hint(&self, hint: &NestedSessionHint, max_len: usize) -> Option<LinePart> { + let (prefix, keys) = match hint { + NestedSessionHint::Ascend(keys) => ("Ascend: ", keys), + NestedSessionHint::Descend(keys) => ("Descend: ", keys), + NestedSessionHint::None => return None, + }; + + let bg = self.palette.text_unselected.background; + let key_color = self.palette.text_unselected.emphasis_0; + let sep_color = self.palette.text_unselected.base; + let keys_text: Vec<String> = if keys.is_empty() { + vec!["<unbound>".to_string()] + } else { + keys.iter().map(|key| format!("<{}>", key)).collect() + }; + + let prefix_len = prefix.width() + 1; + let keys_display_len: usize = keys_text.iter().map(|k| k.width()).sum::<usize>() + + keys_text.len().saturating_sub(1) * 3; + + if !keys_text.is_empty() && prefix_len + keys_display_len <= max_len { + let prefix_style = dim_style(bg).italic(); + let mut part = prefix_style.paint(format!(" {}", prefix)).to_string(); + for (i, key) in keys_text.iter().enumerate() { + if i > 0 { + part.push_str(&style!(sep_color, bg).paint(" + ").to_string()); + } + if keys.is_empty() { + part.push_str(&dim_style(bg).italic().paint(key).to_string()); + } else { + part.push_str(&style!(key_color, bg).bold().paint(key).to_string()); + } + } + return Some(LinePart { + part, + len: prefix_len + keys_display_len, + tab_index: None, + }); + } + + let message = match hint { + NestedSessionHint::Ascend(_) => " nested session", + NestedSessionHint::Descend(_) => " host session", + NestedSessionHint::None => return None, + }; + let message_len = message.width(); + if message_len <= max_len { + let style = dim_style(bg).italic(); + return Some(LinePart { + part: style.paint(message).to_string(), + len: message_len, + tab_index: None, + }); + } + + None + } + fn create_tooltip_indicator(&self, toggle_key: &str, is_active: bool) -> LinePart { let key_text = toggle_key; - let key = Text::new(key_text).color_all(3).opaque(); + let key = if self.dimmed { + Text::new(key_text).disabled().opaque() + } else { + Text::new(key_text).color_all(3).opaque() + }; let ribbon_text = "Tooltip"; let mut ribbon = Text::new(ribbon_text); - if is_active { + if self.dimmed { + ribbon = ribbon.disabled(); + } else if is_active { ribbon = ribbon.selected(); } @@ -459,6 +681,14 @@ impl RightSideElementsBuilder { colors: &SwapLayoutColors, separator: &str, ) -> (String, String, String) { + if self.dimmed { + let text_style = style!(colors.bg, colors.fg).italic(); + return ( + style!(colors.bg, colors.fg).paint(separator).to_string(), + text_style.paint(layout_name).to_string(), + style!(colors.fg, colors.bg).paint(separator).to_string(), + ); + } match mode { InputMode::Locked => ( style!(colors.bg, colors.fg).paint(separator).to_string(), @@ -517,22 +747,37 @@ impl TabLineBuilder { } } - pub fn build(self, all_tabs: Vec<LinePart>, active_tab_index: usize) -> Vec<LinePart> { + pub fn build(self, all_tabs: Vec<LinePart>, active_tab_index: usize) -> TabLineOutput { let (tabs_before_active, active_tab, tabs_after_active) = self.split_tabs(all_tabs, active_tab_index); - let prefix_builder = TabLinePrefixBuilder::new(self.palette, self.cols); let session_name = if self.config.hide_session_name { None } else { self.config.session_name.as_deref() }; + let breadcrumb_ancestry: &[String] = if self.config.hide_session_name { + &[] + } else { + &self.config.breadcrumb_ancestry + }; + let prefix_builder = TabLinePrefixBuilder::new( + self.palette, + self.cols, + self.config.dimmed, + breadcrumb_ancestry, + ); - let mut prefix = prefix_builder.build(session_name, self.config.mode); + let prefix_result = prefix_builder.build(session_name, self.config.mode); + let mut prefix = prefix_result.parts; + let breadcrumb_range = prefix_result.breadcrumb_range; let prefix_len = calculate_total_length(&prefix); if prefix_len + active_tab.len > self.cols { - return prefix; + return TabLineOutput { + parts: prefix, + breadcrumb_range, + }; } let mut tabs_to_render = vec![active_tab]; @@ -540,6 +785,7 @@ impl TabLineBuilder { self.cols.saturating_sub(prefix_len), self.palette, self.capabilities, + self.config.dimmed, ); let mut tabs_before = tabs_before_active; @@ -549,7 +795,10 @@ impl TabLineBuilder { prefix.append(&mut tabs_to_render); self.add_right_side_elements(&mut prefix); - prefix + TabLineOutput { + parts: prefix, + breadcrumb_range, + } } fn split_tabs( @@ -573,7 +822,8 @@ impl TabLineBuilder { let current_len = calculate_total_length(prefix); if current_len < self.cols { - let right_builder = RightSideElementsBuilder::new(self.palette, self.capabilities); + let right_builder = + RightSideElementsBuilder::new(self.palette, self.capabilities, self.config.dimmed); let available_space = self.cols.saturating_sub(current_len); let mut right_elements = right_builder.build(&self.config, available_space); diff --git a/default-plugins/compact-bar/src/main.rs b/default-plugins/compact-bar/src/main.rs index 3a8446a5..29975d9a 100644 --- a/default-plugins/compact-bar/src/main.rs +++ b/default-plugins/compact-bar/src/main.rs @@ -41,6 +41,7 @@ struct State { // Display state mode_info: ModeInfo, tab_line: Vec<LinePart>, + breadcrumb_range: Option<(usize, usize)>, display_area_rows: usize, display_area_cols: usize, @@ -278,13 +279,23 @@ impl State { } match mouse_event { - Mouse::LeftClick(_, col) => self.handle_tab_click(col), + Mouse::LeftClick(_, col) => { + if self.click_is_on_breadcrumb(col) { + focus_host_session(); + } else { + self.handle_tab_click(col); + } + }, Mouse::ScrollUp(_) => self.scroll_tab_up(), Mouse::ScrollDown(_) => self.scroll_tab_down(), _ => {}, } } + fn click_is_on_breadcrumb(&self, col: usize) -> bool { + matches!(self.breadcrumb_range, Some((start, end)) if col >= start && col < end) + } + fn handle_clipboard_copy(&mut self, copy_destination: CopyDestination) -> bool { if self.is_tooltip { return false; @@ -514,13 +525,15 @@ impl State { } let tab_data = self.prepare_tab_data(); - self.tab_line = tab_line( + let tab_line_output = tab_line( &self.mode_info, tab_data, cols, self.toggle_tooltip_key.clone(), self.tooltip_is_active, ); + self.tab_line = tab_line_output.parts; + self.breadcrumb_range = tab_line_output.breadcrumb_range; let output = self .tab_line @@ -536,6 +549,8 @@ impl State { let mut active_swap_layout_name = None; let mut is_swap_layout_dirty = false; let mut is_alternate_tab = false; + let dimmed = self.mode_info.session_ascended == Some(true) + || self.mode_info.session_dimmed == Some(true); for tab in &self.tabs { let tab_name = self.get_tab_display_name(tab); @@ -554,6 +569,7 @@ impl State { is_alternate_tab, self.mode_info.style.colors, self.mode_info.capabilities, + dimmed, ); is_alternate_tab = !is_alternate_tab; diff --git a/default-plugins/compact-bar/src/tab.rs b/default-plugins/compact-bar/src/tab.rs index 8d4ada96..ac1602c3 100644 --- a/default-plugins/compact-bar/src/tab.rs +++ b/default-plugins/compact-bar/src/tab.rs @@ -27,6 +27,7 @@ pub fn render_tab( is_alternate_tab: bool, palette: Styling, separator: &str, + dimmed: bool, ) -> LinePart { let focused_clients = tab.other_focused_clients.as_slice(); let separator_width = separator.width(); @@ -54,12 +55,20 @@ pub fn render_tab( palette.ribbon_unselected.base }; let separator_fill_color = palette.text_unselected.background; + let background_color = if dimmed { + palette.ribbon_unselected.background + } else { + background_color + }; + let text_style = if dimmed { + style!(palette.ribbon_unselected.base, background_color).italic() + } else { + style!(foreground_color, background_color).bold() + }; let left_separator = style!(separator_fill_color, background_color).paint(separator); let mut tab_text_len = text.width() + (separator_width * 2) + 2; // + 2 for padding - let tab_styled_text = style!(foreground_color, background_color) - .bold() - .paint(format!(" {} ", text)); + let tab_styled_text = text_style.paint(format!(" {} ", text)); let right_separator = style!(background_color, separator_fill_color).paint(separator); let tab_styled_text = if !focused_clients.is_empty() { @@ -67,15 +76,9 @@ pub fn render_tab( cursors(focused_clients, palette.multiplayer_user_colors); tab_text_len += extra_length; let mut s = String::new(); - let cursor_beginning = style!(foreground_color, background_color) - .bold() - .paint("[") - .to_string(); + let cursor_beginning = text_style.paint("[").to_string(); let cursor_section = ANSIStrings(&cursor_section).to_string(); - let cursor_end = style!(foreground_color, background_color) - .bold() - .paint("]") - .to_string(); + let cursor_end = text_style.paint("]").to_string(); s.push_str(&left_separator.to_string()); s.push_str(&tab_styled_text.to_string()); s.push_str(&cursor_beginning); @@ -100,6 +103,7 @@ pub fn tab_style( mut is_alternate_tab: bool, palette: Styling, capabilities: PluginCapabilities, + dimmed: bool, ) -> LinePart { let separator = tab_separator(capabilities); @@ -116,7 +120,7 @@ pub fn tab_style( is_alternate_tab = false; } - render_tab(tabname, tab, is_alternate_tab, palette, separator) + render_tab(tabname, tab, is_alternate_tab, palette, separator, dimmed) } pub(crate) fn get_tab_to_focus( |
