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 | |
| 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')
| -rw-r--r-- | default-plugins/about/src/main.rs | 1 | ||||
| -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 | ||||
| -rw-r--r-- | default-plugins/configuration/src/presets.rs | 24 | ||||
| -rw-r--r-- | default-plugins/status-bar/src/first_line.rs | 4 | ||||
| -rw-r--r-- | default-plugins/status-bar/src/main.rs | 50 | ||||
| -rw-r--r-- | default-plugins/status-bar/src/one_line_ui.rs | 131 | ||||
| -rw-r--r-- | default-plugins/status-bar/src/second_line.rs | 54 | ||||
| -rw-r--r-- | default-plugins/tab-bar/src/line.rs | 191 | ||||
| -rw-r--r-- | default-plugins/tab-bar/src/main.rs | 20 | ||||
| -rw-r--r-- | default-plugins/tab-bar/src/tab.rs | 30 |
12 files changed, 746 insertions, 129 deletions
diff --git a/default-plugins/about/src/main.rs b/default-plugins/about/src/main.rs index 42f87055..c0d1f398 100644 --- a/default-plugins/about/src/main.rs +++ b/default-plugins/about/src/main.rs @@ -311,7 +311,6 @@ impl App { ) .unwrap(), )]); - let _ = show_floating_panes(None); } } } 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( diff --git a/default-plugins/configuration/src/presets.rs b/default-plugins/configuration/src/presets.rs index 4eb65042..a577baf2 100644 --- a/default-plugins/configuration/src/presets.rs +++ b/default-plugins/configuration/src/presets.rs @@ -34,6 +34,7 @@ keybinds clear-defaults=true {{ bind "s" {{ NewPane "stacked"; SwitchToMode "Locked"; }} bind "x" {{ CloseFocus; SwitchToMode "Locked"; }} bind "f" {{ ToggleFocusFullscreen; SwitchToMode "Locked"; }} + bind "Shift f" {{ ToggleFocusNoUiFullscreen; SwitchToMode "Locked"; }} bind "z" {{ TogglePaneFrames; SwitchToMode "Locked"; }} bind "w" {{ ToggleFloatingPanes; SwitchToMode "Locked"; }} bind "e" {{ TogglePaneEmbedOrFloating; SwitchToMode "Locked"; }} @@ -120,6 +121,9 @@ keybinds clear-defaults=true {{ session {{ bind "o" {{ SwitchToMode "Normal"; }} bind "d" {{ Detach; }} + bind "]" {{ FocusHostSession; SwitchToMode "Normal"; }} + bind "[" {{ FocusGuestSession; SwitchToMode "Normal"; }} + bind "f" {{ ToggleHostFullscreen; SwitchToMode "Normal"; }} bind "w" {{ LaunchOrFocusPlugin "session-manager" {{ floating true @@ -255,6 +259,7 @@ keybinds clear-defaults=true {{ bind "s" {{ NewPane "stacked"; SwitchToMode "Normal"; }} bind "x" {{ CloseFocus; SwitchToMode "Normal"; }} bind "f" {{ ToggleFocusFullscreen; SwitchToMode "Normal"; }} + bind "Shift f" {{ ToggleFocusNoUiFullscreen; SwitchToMode "Normal"; }} bind "z" {{ TogglePaneFrames; SwitchToMode "Normal"; }} bind "w" {{ ToggleFloatingPanes; SwitchToMode "Normal"; }} bind "e" {{ TogglePaneEmbedOrFloating; SwitchToMode "Normal"; }} @@ -343,6 +348,9 @@ keybinds clear-defaults=true {{ bind "{primary_modifier} o" {{ SwitchToMode "Normal"; }} bind "{primary_modifier} s" {{ SwitchToMode "Scroll"; }} bind "d" {{ Detach; }} + bind "]" {{ FocusHostSession; SwitchToMode "Normal"; }} + bind "[" {{ FocusGuestSession; SwitchToMode "Normal"; }} + bind "f" {{ ToggleHostFullscreen; SwitchToMode "Normal"; }} bind "w" {{ LaunchOrFocusPlugin "session-manager" {{ floating true @@ -487,6 +495,7 @@ keybinds clear-defaults=true {{ bind "s" {{ NewPane "stacked"; SwitchToMode "Normal"; }} bind "x" {{ CloseFocus; SwitchToMode "Normal"; }} bind "f" {{ ToggleFocusFullscreen; SwitchToMode "Normal"; }} + bind "Shift f" {{ ToggleFocusNoUiFullscreen; SwitchToMode "Normal"; }} bind "z" {{ TogglePaneFrames; SwitchToMode "Normal"; }} bind "w" {{ ToggleFloatingPanes; SwitchToMode "Normal"; }} bind "e" {{ TogglePaneEmbedOrFloating; SwitchToMode "Normal"; }} @@ -566,6 +575,9 @@ keybinds clear-defaults=true {{ }} session {{ bind "d" {{ Detach; }} + bind "]" {{ FocusHostSession; SwitchToMode "Normal"; }} + bind "[" {{ FocusGuestSession; SwitchToMode "Normal"; }} + bind "f" {{ ToggleHostFullscreen; SwitchToMode "Normal"; }} bind "w" {{ LaunchOrFocusPlugin "session-manager" {{ floating true @@ -690,6 +702,7 @@ keybinds clear-defaults=true {{ bind "s" {{ NewPane "stacked"; SwitchToMode "Normal"; }} bind "x" {{ CloseFocus; SwitchToMode "Normal"; }} bind "f" {{ ToggleFocusFullscreen; SwitchToMode "Normal"; }} + bind "Shift f" {{ ToggleFocusNoUiFullscreen; SwitchToMode "Normal"; }} bind "z" {{ TogglePaneFrames; SwitchToMode "Normal"; }} bind "w" {{ ToggleFloatingPanes; SwitchToMode "Normal"; }} bind "e" {{ TogglePaneEmbedOrFloating; SwitchToMode "Normal"; }} @@ -770,6 +783,9 @@ keybinds clear-defaults=true {{ bind "{primary_modifier} o" {{ SwitchToMode "Normal"; }} bind "{primary_modifier} s" {{ SwitchToMode "Scroll"; }} bind "d" {{ Detach; }} + bind "]" {{ FocusHostSession; SwitchToMode "Normal"; }} + bind "[" {{ FocusGuestSession; SwitchToMode "Normal"; }} + bind "f" {{ ToggleHostFullscreen; SwitchToMode "Normal"; }} bind "w" {{ LaunchOrFocusPlugin "session-manager" {{ floating true @@ -900,6 +916,7 @@ keybinds clear-defaults=true {{ bind "s" {{ NewPane "stacked"; SwitchToMode "Normal"; }} bind "x" {{ CloseFocus; SwitchToMode "Normal"; }} bind "f" {{ ToggleFocusFullscreen; SwitchToMode "Normal"; }} + bind "Shift f" {{ ToggleFocusNoUiFullscreen; SwitchToMode "Normal"; }} bind "z" {{ TogglePaneFrames; SwitchToMode "Normal"; }} bind "w" {{ ToggleFloatingPanes; SwitchToMode "Normal"; }} bind "e" {{ TogglePaneEmbedOrFloating; SwitchToMode "Normal"; }} @@ -974,6 +991,9 @@ keybinds clear-defaults=true {{ }} session {{ bind "d" {{ Detach; }} + bind "]" {{ FocusHostSession; SwitchToMode "Normal"; }} + bind "[" {{ FocusGuestSession; SwitchToMode "Normal"; }} + bind "f" {{ ToggleHostFullscreen; SwitchToMode "Normal"; }} bind "w" {{ LaunchOrFocusPlugin "session-manager" {{ floating true @@ -1085,6 +1105,7 @@ keybinds clear-defaults=true {{ bind "s" {{ NewPane "stacked"; SwitchToMode "Normal"; }} bind "x" {{ CloseFocus; SwitchToMode "Normal"; }} bind "f" {{ ToggleFocusFullscreen; SwitchToMode "Normal"; }} + bind "Shift f" {{ ToggleFocusNoUiFullscreen; SwitchToMode "Normal"; }} bind "z" {{ TogglePaneFrames; SwitchToMode "Normal"; }} bind "w" {{ ToggleFloatingPanes; SwitchToMode "Normal"; }} bind "e" {{ TogglePaneEmbedOrFloating; SwitchToMode "Normal"; }} @@ -1173,6 +1194,9 @@ keybinds clear-defaults=true {{ bind "{primary_modifier} o" {{ SwitchToMode "Normal"; }} bind "{primary_modifier} s" {{ SwitchToMode "Scroll"; }} bind "d" {{ Detach; }} + bind "]" {{ FocusHostSession; SwitchToMode "Normal"; }} + bind "[" {{ FocusGuestSession; SwitchToMode "Normal"; }} + bind "f" {{ ToggleHostFullscreen; SwitchToMode "Normal"; }} bind "w" {{ LaunchOrFocusPlugin "session-manager" {{ floating true diff --git a/default-plugins/status-bar/src/first_line.rs b/default-plugins/status-bar/src/first_line.rs index b5c688ad..c2f5b477 100644 --- a/default-plugins/status-bar/src/first_line.rs +++ b/default-plugins/status-bar/src/first_line.rs @@ -613,7 +613,7 @@ pub fn first_line( separator: &str, ) -> LinePart { let supports_arrow_fonts = !help.capabilities.arrow_fonts; - let colored_elements = color_elements(help.style.colors, !supports_arrow_fonts); + let colored_elements = color_elements(help.style.colors, !supports_arrow_fonts, false); let binds = &help.get_mode_keybinds(); // Unselect all by default let mut default_keys = vec![ @@ -755,7 +755,7 @@ mod tests { fn colored_elements() -> ColoredElements { let palette = Styling::default(); - color_elements(palette, false) + color_elements(palette, false, false) } // Strip style information from `LinePart` and return a raw String instead diff --git a/default-plugins/status-bar/src/main.rs b/default-plugins/status-bar/src/main.rs index 673d7c8c..f12da1cc 100644 --- a/default-plugins/status-bar/src/main.rs +++ b/default-plugins/status-bar/src/main.rs @@ -18,9 +18,9 @@ use zellij_tile_utils::{palette_match, style}; use first_line::first_line; use one_line_ui::one_line_ui; use second_line::{ - floating_panes_are_visible, fullscreen_panes_to_hide, keybinds, - locked_floating_panes_are_visible, locked_fullscreen_panes_to_hide, system_clipboard_error, - text_copied_hint, + ascended_to_host_session_hint, descended_into_nested_session_hint, floating_panes_are_visible, + fullscreen_panes_to_hide, keybinds, locked_floating_panes_are_visible, + locked_fullscreen_panes_to_hide, system_clipboard_error, text_copied_hint, }; use tip::utils::get_cached_tip_name; @@ -94,7 +94,33 @@ pub struct SegmentStyle { // we need different colors from palette for the default theme // plus here we can add new sources in the future, like Theme // that can be defined in the config perhaps -fn color_elements(palette: Styling, different_color_alternates: bool) -> ColoredElements { +fn color_elements( + palette: Styling, + different_color_alternates: bool, + dimmed: bool, +) -> ColoredElements { + if dimmed { + let background = palette.text_unselected.background; + let foreground = palette.text_unselected.base; + let ribbon_background = palette.ribbon_unselected.background; + let italic_on_ribbon = style!(palette.ribbon_unselected.base, ribbon_background).italic(); + let dim_segment = SegmentStyle { + prefix_separator: style!(background, ribbon_background), + char_left_separator: italic_on_ribbon, + char_shortcut: italic_on_ribbon, + char_right_separator: italic_on_ribbon, + styled_text: italic_on_ribbon, + suffix_separator: style!(ribbon_background, background), + }; + return ColoredElements { + selected: dim_segment, + unselected: dim_segment, + unselected_alternate: dim_segment, + disabled: dim_segment, + superkey_prefix: style!(foreground, background).italic(), + superkey_suffix_separator: style!(background, background), + }; + } let background = palette.text_unselected.background; let foreground = palette.text_unselected.base; let alternate_background_color = if different_color_alternates { @@ -344,6 +370,8 @@ impl ZellijPlugin for State { let active_tab = self.tabs.iter().find(|t| t.active); let first_line = first_line(&self.mode_info, active_tab, cols, separator); let second_line = self.second_line(cols); + let show_nested_session_hint = self.mode_info.session_dimmed.unwrap_or(false) + || self.mode_info.session_ascended.unwrap_or(false); // [48;5;238m is white background, [0K is so that it fills the rest of the line // [m is background reset, [0K is so that it clears the rest of the line @@ -352,7 +380,9 @@ impl ZellijPlugin for State { if rows > 1 { println!("{}\u{1b}[48;2;{};{};{}m\u{1b}[0K", first_line, r, g, b); } else { - if self.mode_info.mode == InputMode::Normal { + if show_nested_session_hint { + print!("\u{1b}[m{}\u{1b}[0K", second_line); + } else if self.mode_info.mode == InputMode::Normal { print!("{}\u{1b}[48;2;{};{};{}m\u{1b}[0K", first_line, r, g, b); } else { print!("\u{1b}[m{}\u{1b}[0K", second_line); @@ -363,7 +393,9 @@ impl ZellijPlugin for State { if rows > 1 { println!("{}\u{1b}[48;5;{}m\u{1b}[0K", first_line, color); } else { - if self.mode_info.mode == InputMode::Normal { + if show_nested_session_hint { + print!("\u{1b}[m{}\u{1b}[0K", second_line); + } else if self.mode_info.mode == InputMode::Normal { print!("{}\u{1b}[48;5;{}m\u{1b}[0K", first_line, color); } else { print!("\u{1b}[m{}\u{1b}[0K", second_line); @@ -382,6 +414,12 @@ impl State { fn second_line(&self, cols: usize) -> LinePart { let active_tab = self.tabs.iter().find(|t| t.active); + if self.mode_info.session_dimmed.unwrap_or(false) { + return descended_into_nested_session_hint(&self.mode_info, cols); + } + if self.mode_info.session_ascended.unwrap_or(false) { + return ascended_to_host_session_hint(&self.mode_info, cols); + } if let Some(copy_destination) = self.text_copy_destination { text_copied_hint(copy_destination) } else if self.display_system_clipboard_failure { diff --git a/default-plugins/status-bar/src/one_line_ui.rs b/default-plugins/status-bar/src/one_line_ui.rs index ae57a223..62992af6 100644 --- a/default-plugins/status-bar/src/one_line_ui.rs +++ b/default-plugins/status-bar/src/one_line_ui.rs @@ -9,7 +9,10 @@ use zellij_tile::prelude::*; use zellij_tile_utils::{palette_match, style}; use crate::first_line::{to_char, KeyAction, KeyMode, KeyShortcut}; -use crate::second_line::{system_clipboard_error, text_copied_hint}; +use crate::second_line::{ + ascended_to_host_session_hint, descended_into_nested_session_hint, system_clipboard_error, + text_copied_hint, +}; use crate::{action_key, action_key_group, color_elements, MORE_MSG, TO_NORMAL}; use crate::{ColoredElements, LinePart}; use unicode_width::UnicodeWidthStr; @@ -25,6 +28,16 @@ pub fn one_line_ui( new_pane_hovered: bool, floating_hovered: bool, ) -> (LinePart, Option<(usize, usize)>, Option<(usize, usize)>) { + if help.session_dimmed.unwrap_or(false) { + return ( + descended_into_nested_session_hint(help, max_len), + None, + None, + ); + } + if help.session_ascended.unwrap_or(false) { + return (ascended_to_host_session_hint(help, max_len), None, None); + } if let Some(text_copied_to_clipboard_destination) = text_copied_to_clipboard_destination { return ( text_copied_hint(text_copied_to_clipboard_destination), @@ -528,7 +541,11 @@ fn render_mode_key_indicators( ) -> Option<LinePart> { let mut line_part_to_render = LinePart::default(); let supports_arrow_fonts = !help.capabilities.arrow_fonts; - let colored_elements = color_elements(help.style.colors, !supports_arrow_fonts); + let colored_elements = color_elements( + help.style.colors, + !supports_arrow_fonts, + help.session_dimmed.unwrap_or(false), + ); let default_keys = if base_mode_is_locked { base_mode_locked_mode_indicators(help) } else { @@ -742,13 +759,14 @@ fn render_common_modifiers( ) }; + let prefix = if mode_info.session_dimmed.unwrap_or(false) { + serialize_text(&Text::new(&prefix_text).disabled().opaque()) + } else { + serialize_text(&Text::new(&prefix_text).opaque()) + }; let suffix_separator = palette.superkey_suffix_separator.paint(separator); - line_part_to_render.part = format!( - "{}{}{}", - line_part_to_render.part, - serialize_text(&Text::new(&prefix_text).opaque()), - suffix_separator - ); + line_part_to_render.part = + format!("{}{}{}", line_part_to_render.part, prefix, suffix_separator); line_part_to_render.len += prefix_text.chars().count() + separator.chars().count(); } @@ -761,7 +779,11 @@ fn render_secondary_info( ) -> Option<(LinePart, Option<(usize, usize)>, Option<(usize, usize)>)> { let mut secondary_info = LinePart::default(); let supports_arrow_fonts = !help.capabilities.arrow_fonts; - let colored_elements = color_elements(help.style.colors, !supports_arrow_fonts); + let colored_elements = color_elements( + help.style.colors, + !supports_arrow_fonts, + help.session_dimmed.unwrap_or(false), + ); let (secondary_keybinds, new_pane_range, floating_range) = secondary_keybinds(&help, tab_info, max_len, new_pane_hovered, floating_hovered); secondary_info.append(&secondary_keybinds); @@ -1011,6 +1033,7 @@ fn secondary_keybinds( .join("-") ), 0, + help.session_dimmed.unwrap_or(false), ); secondary_info.append(&modifier_str); let new_pane_key_to_display: Vec<KeyWithModifier> = new_pane_key_to_display @@ -1145,6 +1168,7 @@ fn secondary_keybinds( .join("-") ), 0, + help.session_dimmed.unwrap_or(false), ); short_line.append(&modifier_str); let new_pane_key_to_display: Vec<KeyWithModifier> = new_pane_key_to_display @@ -1213,11 +1237,12 @@ fn secondary_keybinds( if short_line.len <= max_len { (short_line, new_pane_range, floating_range) } else if max_len >= 3 { - let part = serialize_text( - &Text::new(format!("{:>width$}", "...", width = max_len)) - .color_range(0, ..) - .opaque(), - ); + let overflow_text = Text::new(format!("{:>width$}", "...", width = max_len)); + let part = if help.session_dimmed.unwrap_or(false) { + serialize_text(&overflow_text.disabled().opaque()) + } else { + serialize_text(&overflow_text.color_range(0, ..).opaque()) + }; let len = max_len; (LinePart { part, len }, None, None) } else { @@ -1232,8 +1257,12 @@ fn secondary_keybinds( } } -fn text_as_line_part_with_emphasis(text: String, emphases_index: usize) -> LinePart { - let part = serialize_text(&Text::new(&text).color_range(emphases_index, ..).opaque()); +fn text_as_line_part_with_emphasis(text: String, emphases_index: usize, dimmed: bool) -> LinePart { + let part = if dimmed { + serialize_text(&Text::new(&text).disabled().opaque()) + } else { + serialize_text(&Text::new(&text).color_range(emphases_index, ..).opaque()) + }; LinePart { part, len: text.width(), @@ -1279,11 +1308,14 @@ fn add_shortcut_hovered( keys: &Vec<KeyWithModifier>, key_color_index: Option<usize>, ) -> LinePart { + if help.session_dimmed.unwrap_or(false) { + return add_shortcut(help, text, keys, false, key_color_index); + } let mut ret = LinePart::default(); if keys.is_empty() { return ret; } - ret.append(&style_key_with_modifier(&keys, key_color_index)); + ret.append(&style_key_with_modifier(&keys, key_color_index, false)); let palette = help.style.colors; let supports_arrow_fonts = !help.capabilities.arrow_fonts; let ribbon_bg = palette.ribbon_unselected.emphasis_1; @@ -1306,6 +1338,9 @@ fn add_shortcut_with_inline_key_hovered( text: &str, key: Vec<KeyWithModifier>, ) -> LinePart { + if help.session_dimmed.unwrap_or(false) { + return add_shortcut_with_inline_key(help, text, key, false); + } let mut ret = LinePart::default(); if key.is_empty() { return ret; @@ -1360,14 +1395,17 @@ fn add_shortcut( selected: bool, key_color_index: Option<usize>, ) -> LinePart { + let dimmed = help.session_dimmed.unwrap_or(false); let mut ret = LinePart::default(); if keys.is_empty() { return ret; } - ret.append(&style_key_with_modifier(&keys, key_color_index)); // TODO: alternate - // - let ribbon = if selected { + ret.append(&style_key_with_modifier(&keys, key_color_index, dimmed)); // TODO: alternate + // + let ribbon = if dimmed { + serialize_ribbon(&Text::new(format!("{}", text)).disabled()) + } else if selected { serialize_ribbon(&Text::new(format!("{}", text)).selected()) } else { serialize_ribbon(&Text::new(format!("{}", text))) @@ -1420,7 +1458,9 @@ fn add_shortcut_with_inline_key( .join(key_separator) ); - let ribbon = if is_selected { + let ribbon = if help.session_dimmed.unwrap_or(false) { + serialize_ribbon(&Text::new(format!("<{}> {}", key_string, text)).disabled()) + } else if is_selected { serialize_ribbon( &Text::new(format!("<{}> {}", key_string, text)) .color_range(0, 1..key_string.width() + 1) @@ -1461,7 +1501,9 @@ fn add_shortcut_with_key_only( .join("-") ); - let ribbon = if is_selected { + let ribbon = if help.session_dimmed.unwrap_or(false) { + serialize_ribbon(&Text::new(format!("{}", key_string)).disabled()) + } else if is_selected { serialize_ribbon( &Text::new(format!("{}", key_string)) .color_range(0, ..) @@ -1489,6 +1531,37 @@ fn add_keygroup_separator(help: &ModeInfo, max_len: usize) -> Option<LinePart> { }; let palette = help.style.colors; + if help.session_dimmed.unwrap_or(false) { + let mut ret = LinePart::default(); + let dim_style = Style::new() + .fg(palette_match!(palette.text_unselected.base)) + .on(palette_match!(palette.text_unselected.background)) + .italic(); + let mut bits: Vec<ANSIString> = vec![]; + let mode_help_text = match help.mode { + InputMode::RenamePane => Some("RENAMING PANE"), + InputMode::RenameTab => Some("RENAMING TAB"), + InputMode::EnterSearch => Some("ENTERING SEARCH TERM"), + InputMode::Search => Some("SEARCHING"), + _ => None, + }; + if let Some(mode_help_text) = mode_help_text { + bits.push(dim_style.paint(format!(" {} ", mode_help_text))); + ret.len += mode_help_text.width() + 2; + } + bits.push(dim_style.paint(format!("{}", separator))); + bits.push(dim_style.paint(format!(" "))); + bits.push(dim_style.paint(format!("{}", separator))); + ret.part = format!("{}{}", ret.part, ANSIStrings(&bits)); + ret.len += 3; + + if ret.len <= max_len { + return Some(ret); + } else { + return None; + } + } + let mut ret = LinePart::default(); let separator_color = palette_match!(palette.text_unselected.emphasis_0); @@ -1916,7 +1989,11 @@ fn configuration_key(keymap: &[(KeyWithModifier, Vec<Action>)]) -> Vec<KeyWithMo } } -fn style_key_with_modifier(keyvec: &[KeyWithModifier], color_index: Option<usize>) -> LinePart { +fn style_key_with_modifier( + keyvec: &[KeyWithModifier], + color_index: Option<usize>, + dimmed: bool, +) -> LinePart { if keyvec.is_empty() { return LinePart::default(); } @@ -1956,7 +2033,9 @@ fn style_key_with_modifier(keyvec: &[KeyWithModifier], color_index: Option<usize if no_common_modifier || key.len() == 1 { let key_string_text = format!(" {} ", key.join(key_separator)); - let text = if let Some(color_index) = color_index { + let text = if dimmed { + Text::new(&key_string_text).disabled().opaque() + } else if let Some(color_index) = color_index { Text::new(&key_string_text) .color_range(color_index, ..) .opaque() @@ -1970,7 +2049,9 @@ fn style_key_with_modifier(keyvec: &[KeyWithModifier], color_index: Option<usize } else { let key_string_without_modifier = format!("{}", key.join(key_separator)); let key_string_text = format!(" {} <{}> ", modifier_str, key_string_without_modifier); - let text = if let Some(color_index) = color_index { + let text = if dimmed { + Text::new(&key_string_text).disabled().opaque() + } else if let Some(color_index) = color_index { Text::new(&key_string_text) .color_range(color_index, ..modifier_str.width() + 1) .color_range( diff --git a/default-plugins/status-bar/src/second_line.rs b/default-plugins/status-bar/src/second_line.rs index 921f3c75..63f845a6 100644 --- a/default-plugins/status-bar/src/second_line.rs +++ b/default-plugins/status-bar/src/second_line.rs @@ -368,6 +368,60 @@ pub fn keybinds(help: &ModeInfo, tip_name: &str, max_width: usize) -> LinePart { best_effort_shortcut_list(help, tip_body.short, max_width) } +pub fn descended_into_nested_session_hint(help: &ModeInfo, max_len: usize) -> LinePart { + nested_session_status_hint(help, "Ascend: ", &help.nested_ascend_keys, max_len) +} + +pub fn ascended_to_host_session_hint(help: &ModeInfo, max_len: usize) -> LinePart { + nested_session_status_hint(help, "Descend: ", &help.nested_descend_keys, max_len) +} + +fn nested_session_status_hint( + help: &ModeInfo, + label: &str, + keys: &[KeyWithModifier], + max_len: usize, +) -> LinePart { + let palette = help.style.colors; + let text_color = palette_match!(palette.text_unselected.base); + let styled_label = Style::new().dimmed().italic().paint(format!(" {}", label)); + let mut key_bits: Vec<ANSIString> = vec![]; + if keys.is_empty() { + key_bits.push(Style::new().dimmed().italic().paint("<unbound>")); + } else { + for (i, key) in keys.iter().enumerate() { + if i > 0 { + key_bits.push(Style::new().fg(text_color).paint(" + ")); + } + key_bits.extend(style_key_with_modifier( + std::slice::from_ref(key), + &palette, + None, + )); + } + } + let mut bits: Vec<ANSIString> = vec![styled_label]; + bits.extend(key_bits.clone()); + let part = ANSIStrings(&bits); + let len = unstyled_len(&part); + if len <= max_len { + return LinePart { + part: part.to_string(), + len, + }; + } + let part = ANSIStrings(&key_bits); + let len = unstyled_len(&part); + if len <= max_len { + LinePart { + part: part.to_string(), + len, + } + } else { + LinePart::default() + } +} + pub fn text_copied_hint(copy_destination: CopyDestination) -> LinePart { let hint = match copy_destination { CopyDestination::Command => "Text piped to external command", diff --git a/default-plugins/tab-bar/src/line.rs b/default-plugins/tab-bar/src/line.rs index 84c5b3aa..d9f7a8da 100644 --- a/default-plugins/tab-bar/src/line.rs +++ b/default-plugins/tab-bar/src/line.rs @@ -11,6 +11,15 @@ fn get_current_title_len(current_title: &[LinePart]) -> usize { current_title.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() +} + // move elements from before_active and after_active into tabs_to_render while they fit in cols // adds collapsed_tabs to the left and right if there's left over tabs that don't fit fn populate_tabs_in_tab_line( @@ -20,6 +29,7 @@ fn populate_tabs_in_tab_line( cols: usize, palette: Styling, capabilities: PluginCapabilities, + dimmed: bool, ) { let mut middle_size = get_current_title_len(tabs_to_render); @@ -36,6 +46,7 @@ fn populate_tabs_in_tab_line( palette, tab_separator(capabilities), left_more_tab_index, + dimmed, ); // right_more_tab_index is the first tab to the right of the rightmost visible tab @@ -45,6 +56,7 @@ fn populate_tabs_in_tab_line( palette, tab_separator(capabilities), right_more_tab_index, + dimmed, ); let total_size = collapsed_left.len + middle_size + collapsed_right.len; @@ -113,6 +125,7 @@ fn left_more_message( palette: Styling, separator: &str, tab_index: usize, + dimmed: bool, ) -> LinePart { if tab_count_to_the_left == 0 { return LinePart::default(); @@ -129,10 +142,13 @@ fn left_more_message( palette.ribbon_unselected.base, palette.text_unselected.background, ); + let text_style = if dimmed { + style!(text_color, palette.ribbon_unselected.background).italic() + } else { + style!(text_color, palette.ribbon_unselected.background).bold() + }; let left_separator = style!(sep_color, palette.ribbon_unselected.background).paint(separator); - let more_styled_text = style!(text_color, palette.ribbon_unselected.background) - .bold() - .paint(more_text); + let more_styled_text = text_style.paint(more_text); let right_separator = style!(palette.ribbon_unselected.background, sep_color).paint(separator); let more_styled_text = ANSIStrings(&[left_separator, more_styled_text, right_separator]).to_string(); @@ -148,6 +164,7 @@ fn right_more_message( palette: Styling, separator: &str, tab_index: usize, + dimmed: bool, ) -> LinePart { if tab_count_to_the_right == 0 { return LinePart::default(); @@ -163,10 +180,13 @@ fn right_more_message( palette.ribbon_unselected.base, palette.text_unselected.background, ); + let text_style = if dimmed { + style!(text_color, palette.ribbon_unselected.background).italic() + } else { + style!(text_color, palette.ribbon_unselected.background).bold() + }; let left_separator = style!(sep_color, palette.ribbon_unselected.background).paint(separator); - let more_styled_text = style!(text_color, palette.ribbon_unselected.background) - .bold() - .paint(more_text); + let more_styled_text = text_style.paint(more_text); let right_separator = style!(palette.ribbon_unselected.background, sep_color).paint(separator); let more_styled_text = ANSIStrings(&[left_separator, more_styled_text, right_separator]).to_string(); @@ -177,23 +197,80 @@ fn right_more_message( } } -fn tab_line_prefix(session_name: Option<&str>, palette: Styling, cols: usize) -> Vec<LinePart> { +fn tab_line_prefix( + session_name: Option<&str>, + palette: Styling, + cols: usize, + dimmed: bool, + breadcrumb_ancestry: &[String], +) -> (Vec<LinePart>, Option<(usize, usize)>) { let prefix_text = " Zellij ".to_string(); let running_text_len = prefix_text.chars().count(); let text_color = palette.text_unselected.base; let bg_color = palette.text_unselected.background; - let prefix_styled_text = style!(text_color, bg_color).bold().paint(prefix_text); + let prefix_style = if dimmed { + dim_style(bg_color) + } else { + style!(text_color, bg_color).bold() + }; + let prefix_styled_text = prefix_style.paint(prefix_text); let mut parts = vec![LinePart { part: prefix_styled_text.to_string(), len: running_text_len, tab_index: None, }]; + let mut breadcrumb_range = None; + if !breadcrumb_ancestry.is_empty() { + if let Some(name) = session_name { + let ancestor_text = format!("({} ▸ ", 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 ancestor_style = if dimmed { + dim_style(bg_color) + } else { + style!(text_color, bg_color).italic() + }; + let name_style = if dimmed { + dim_style(bg_color) + } else { + style!(palette.text_unselected.emphasis_0, bg_color).bold() + }; + let closing_style = if dimmed { + dim_style(bg_color) + } else { + style!(text_color, bg_color).bold() + }; + if cols.saturating_sub(running_text_len) >= ancestor_len + name_len + closing_len { + let ancestor_start = running_text_len; + let ancestor_end = ancestor_start + ancestor_len; + breadcrumb_range = Some((ancestor_start, ancestor_end)); + parts.push(LinePart { + part: ancestor_style.paint(ancestor_text).to_string(), + len: ancestor_len, + tab_index: None, + }); + parts.push(LinePart { + part: name_style.paint(name_text).to_string(), + len: name_len, + tab_index: None, + }); + parts.push(LinePart { + part: closing_style.paint(closing_text).to_string(), + len: closing_len, + tab_index: None, + }); + } + } + return (parts, breadcrumb_range); + } if let Some(name) = session_name { let name_part = format!("({}) ", name); let name_part_len = name_part.width(); - let text_color = palette.text_unselected.base; - let name_part_styled_text = style!(text_color, bg_color).bold().paint(name_part); + let name_part_styled_text = prefix_style.paint(name_part); if cols.saturating_sub(running_text_len) >= name_part_len { parts.push(LinePart { part: name_part_styled_text.to_string(), @@ -202,7 +279,7 @@ fn tab_line_prefix(session_name: Option<&str>, palette: Styling, cols: usize) -> }) } } - parts + (parts, breadcrumb_range) } pub fn tab_separator(capabilities: PluginCapabilities) -> &'static str { @@ -229,7 +306,13 @@ pub fn tab_line( hint: Option<&BTreeMap<usize, StyledText>>, is_alternate_tab: bool, new_tab_button_is_hovered: bool, -) -> (Vec<LinePart>, Option<(usize, usize)>) { + dimmed: bool, + breadcrumb_ancestry: &[String], +) -> ( + Vec<LinePart>, + Option<(usize, usize)>, + Option<(usize, usize)>, +) { let mut tabs_after_active = all_tabs.split_off(active_tab_index); let mut tabs_before_active = all_tabs; let active_tab = if !tabs_after_active.is_empty() { @@ -237,9 +320,9 @@ pub fn tab_line( } else { tabs_before_active.pop().unwrap() }; - let mut prefix = match hide_session_name { - true => tab_line_prefix(None, palette, cols), - false => tab_line_prefix(session_name, palette, cols), + let (mut prefix, breadcrumb_range) = match hide_session_name { + true => tab_line_prefix(None, palette, cols, dimmed, &[]), + false => tab_line_prefix(session_name, palette, cols, dimmed, breadcrumb_ancestry), }; let mut swap_layout_indicator = if hide_swap_layout_indicator { @@ -251,17 +334,19 @@ pub fn tab_line( tab_info.is_swap_layout_dirty, mode_info, !capabilities.arrow_fonts, + dimmed, ) }) }; if swap_layout_indicator.is_none() { - swap_layout_indicator = active_pane_scroll.map(|scroll| scroll_status(scroll, palette)); + swap_layout_indicator = + active_pane_scroll.map(|scroll| scroll_status(scroll, palette, dimmed)); } // Drop indicator if it would cause wrapping (active tab always rendered unconditionally) let prefix_len = get_current_title_len(&prefix); let hint_budget = cols.saturating_sub(prefix_len + active_tab.len); - if let Some(hint_part) = hint.and_then(|hint| hint_line_part(hint, hint_budget)) { + if let Some(hint_part) = hint.and_then(|hint| hint_line_part(hint, hint_budget, dimmed)) { swap_layout_indicator = Some(hint_part); } let indicator_len = swap_layout_indicator.as_ref().map(|s| s.len).unwrap_or(0); @@ -280,6 +365,7 @@ pub fn tab_line( cols.saturating_sub(non_tab_len), palette, capabilities, + dimmed, ); prefix.append(&mut tabs_to_render); prefix.append(&mut vec![LinePart { @@ -303,7 +389,8 @@ pub fn tab_line( } else { palette.ribbon_unselected.background }; - let new_tab_button = new_tab_button_line_part(palette, capabilities, new_tab_button_background); + let new_tab_button = + new_tab_button_line_part(palette, capabilities, new_tab_button_background, dimmed); let used = prefix.iter().map(|p| p.len).sum::<usize>(); let current_indicator_len = swap_layout_indicator.as_ref().map(|s| s.len).unwrap_or(0); let room = cols @@ -340,7 +427,7 @@ pub fn tab_line( prefix.push(swap_layout_indicator); } - (prefix, new_tab_button_range) + (prefix, new_tab_button_range, breadcrumb_range) } fn swap_layout_status( @@ -348,6 +435,7 @@ fn swap_layout_status( is_swap_layout_dirty: bool, mode_info: &ModeInfo, supports_arrow_fonts: bool, + dimmed: bool, ) -> Option<LinePart> { match swap_layout_name { Some(swap_layout_name) => { @@ -356,11 +444,12 @@ fn swap_layout_status( &mode_keybinds, &[&[Action::PreviousSwapLayout], &[Action::NextSwapLayout]], ); - let mut text = style_key_with_modifier(&prev_next_keys, Some(0)); + let mut text = style_key_with_modifier(&prev_next_keys, Some(0), dimmed); text.append(&ribbon_as_line_part( &swap_layout_name.to_uppercase(), !is_swap_layout_dirty, supports_arrow_fonts, + dimmed, )); Some(text) }, @@ -368,13 +457,22 @@ fn swap_layout_status( } } -fn hint_line_part(hint: &BTreeMap<usize, StyledText>, max_len: usize) -> Option<LinePart> { +fn hint_line_part( + hint: &BTreeMap<usize, StyledText>, + max_len: usize, + dimmed: bool, +) -> Option<LinePart> { let fitting_variant = hint .range(..=max_len) .next_back() .map(|(_width, styled_text)| styled_text)?; let len = fitting_variant.text.width(); - let part = serialize_text(&Text::from(fitting_variant.clone())); + let text = if dimmed { + Text::from(fitting_variant.clone()).disabled() + } else { + Text::from(fitting_variant.clone()) + }; + let part = serialize_text(&text); Some(LinePart { part, len, @@ -382,12 +480,17 @@ fn hint_line_part(hint: &BTreeMap<usize, StyledText>, max_len: usize) -> Option< }) } -fn scroll_status(scroll: (usize, usize), palette: Styling) -> LinePart { +fn scroll_status(scroll: (usize, usize), palette: Styling, dimmed: bool) -> LinePart { let (position, length) = scroll; let text = format!("[ SCROLL {}/{} ]", position, length); let text_color = palette.text_unselected.base; let bg_color = palette.text_unselected.background; - let styled_text = style!(text_color, bg_color).bold().paint(&text); + let text_style = if dimmed { + style!(text_color, bg_color).italic() + } else { + style!(text_color, bg_color).bold() + }; + let styled_text = text_style.paint(&text); LinePart { part: styled_text.to_string(), len: text.width(), @@ -399,17 +502,26 @@ fn new_tab_button_line_part( palette: Styling, capabilities: PluginCapabilities, background_color: PaletteColor, + dimmed: bool, ) -> LinePart { let separator = tab_separator(capabilities); let separator_width = separator.width(); let foreground_color = 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!(foreground_color, background_color).italic() + } else { + style!(foreground_color, background_color).bold() + }; let left_separator = style!(separator_fill_color, background_color).paint(separator); let right_separator = style!(background_color, separator_fill_color).paint(separator); let text = "+"; - let styled_text = style!(foreground_color, background_color) - .bold() - .paint(format!(" {} ", text)); + let styled_text = text_style.paint(format!(" {} ", text)); let part = ANSIStrings(&[left_separator, styled_text, right_separator]).to_string(); let len = text.width() + (separator_width * 2) + 2; LinePart { @@ -419,8 +531,15 @@ fn new_tab_button_line_part( } } -pub fn ribbon_as_line_part(text: &str, is_selected: bool, supports_arrow_fonts: bool) -> LinePart { - let ribbon_text = if is_selected { +pub fn ribbon_as_line_part( + text: &str, + is_selected: bool, + supports_arrow_fonts: bool, + dimmed: bool, +) -> LinePart { + let ribbon_text = if dimmed { + Text::new(text).disabled() + } else if is_selected { Text::new(text).selected() } else { Text::new(text) @@ -437,7 +556,11 @@ pub fn ribbon_as_line_part(text: &str, is_selected: bool, supports_arrow_fonts: } } -pub fn style_key_with_modifier(keyvec: &[KeyWithModifier], color_index: Option<usize>) -> LinePart { +pub fn style_key_with_modifier( + keyvec: &[KeyWithModifier], + color_index: Option<usize>, + dimmed: bool, +) -> LinePart { if keyvec.is_empty() { return LinePart::default(); } @@ -477,7 +600,9 @@ pub fn style_key_with_modifier(keyvec: &[KeyWithModifier], color_index: Option<u if no_common_modifier || key.len() == 1 { let key_string_text = format!(" {} ", key.join(key_separator)); - let text = if let Some(color_index) = color_index { + let text = if dimmed { + Text::new(&key_string_text).disabled().opaque() + } else if let Some(color_index) = color_index { Text::new(&key_string_text) .color_range(color_index, ..) .opaque() @@ -492,7 +617,9 @@ pub fn style_key_with_modifier(keyvec: &[KeyWithModifier], color_index: Option<u } else { let key_string_without_modifier = format!("{}", key.join(key_separator)); let key_string_text = format!(" {} <{}> ", modifier_str, key_string_without_modifier); - let text = if let Some(color_index) = color_index { + let text = if dimmed { + Text::new(&key_string_text).disabled().opaque() + } else if let Some(color_index) = color_index { Text::new(&key_string_text) .color_range(color_index, ..modifier_str.width() + 1) .color_range( diff --git a/default-plugins/tab-bar/src/main.rs b/default-plugins/tab-bar/src/main.rs index 4b521969..d55cd800 100644 --- a/default-plugins/tab-bar/src/main.rs +++ b/default-plugins/tab-bar/src/main.rs @@ -35,6 +35,7 @@ struct State { cached_keybinds: KeybindsVec, active_pane_scroll: Option<(usize, usize)>, new_tab_button_range: Option<(usize, usize)>, + breadcrumb_range: Option<(usize, usize)>, hovered_tab_idx: Option<usize>, hovered_new_tab_button: bool, hint_text: Option<BTreeMap<usize, StyledText>>, @@ -133,6 +134,12 @@ impl ZellijPlugin for State { }, Event::Mouse(me) => match me { Mouse::LeftClick(_, col) => { + if let Some((start, end)) = self.breadcrumb_range { + if col >= start && col < end { + focus_host_session(); + return should_render; + } + } if let Some((start, end)) = self.new_tab_button_range { if col >= start && col < end { new_tab::<&str>(None, None); @@ -191,6 +198,8 @@ impl ZellijPlugin for State { if self.tabs.is_empty() { return; } + let dimmed = self.mode_info.session_ascended == Some(true) + || self.mode_info.session_dimmed == Some(true); let mut all_tabs: Vec<LinePart> = vec![]; let mut active_tab_index = 0; let mut is_alternate_tab = false; @@ -212,6 +221,7 @@ impl ZellijPlugin for State { is_hovered, self.mode_info.style.colors, self.mode_info.capabilities, + dimmed, ); is_alternate_tab = !is_alternate_tab; all_tabs.push(tab); @@ -225,7 +235,12 @@ impl ZellijPlugin for State { } else { self.hint_text.as_ref() }; - let (line, new_tab_button_range) = tab_line( + let breadcrumb_ancestry: Vec<String> = if self.mode_info.host_fullscreen == Some(true) { + self.mode_info.session_ancestry.clone() + } else { + vec![] + }; + let (line, new_tab_button_range, breadcrumb_range) = tab_line( self.mode_info.session_name.as_deref(), all_tabs, active_tab_index, @@ -241,9 +256,12 @@ impl ZellijPlugin for State { hint_text, is_alternate_tab, self.hovered_new_tab_button, + dimmed, + &breadcrumb_ancestry, ); self.tab_line = line; self.new_tab_button_range = new_tab_button_range; + self.breadcrumb_range = breadcrumb_range; let output = self .tab_line diff --git a/default-plugins/tab-bar/src/tab.rs b/default-plugins/tab-bar/src/tab.rs index 3db1bbb6..df44dbad 100644 --- a/default-plugins/tab-bar/src/tab.rs +++ b/default-plugins/tab-bar/src/tab.rs @@ -27,6 +27,7 @@ pub fn render_tab( is_hovered: bool, palette: Styling, separator: &str, + dimmed: bool, ) -> LinePart { let focused_clients = tab.other_focused_clients.as_slice(); let separator_width = separator.width(); @@ -58,27 +59,30 @@ pub fn render_tab( }; 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 right_separator_style = style!(background_color, separator_fill_color); 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 right_separator = right_separator_style.paint(separator); let tab_styled_text = if !focused_clients.is_empty() { let (cursor_section, extra_length) = cursors(focused_clients, palette.multiplayer_user_colors); tab_text_len += extra_length + 2; // 2 for cursor_beginning and cursor_end 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); @@ -104,6 +108,7 @@ pub fn tab_style( is_hovered: bool, palette: Styling, capabilities: PluginCapabilities, + dimmed: bool, ) -> LinePart { let separator = tab_separator(capabilities); @@ -127,6 +132,7 @@ pub fn tab_style( is_hovered, palette, separator, + dimmed, ) } |
