aboutsummaryrefslogtreecommitdiffstats
path: root/default-plugins/status-bar/src
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2026-07-27 18:24:37 +0200
committerGitHub <noreply@github.com>2026-07-27 18:24:37 +0200
commitea07e2d5b6bbc9ea6f9c765b0838b3e91156a58d (patch)
tree4a8fac8a9b27f2a8b32b3af61a3b5419b9937c81 /default-plugins/status-bar/src
parentSwitch from the unmaintained daemonize crate to a maintained fork (#4512) (diff)
downloadsl0ppij-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/status-bar/src')
-rw-r--r--default-plugins/status-bar/src/first_line.rs4
-rw-r--r--default-plugins/status-bar/src/main.rs50
-rw-r--r--default-plugins/status-bar/src/one_line_ui.rs131
-rw-r--r--default-plugins/status-bar/src/second_line.rs54
4 files changed, 206 insertions, 33 deletions
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",