aboutsummaryrefslogtreecommitdiffstats
path: root/default-plugins/compact-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/compact-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/compact-bar/src')
-rw-r--r--default-plugins/compact-bar/src/line.rs322
-rw-r--r--default-plugins/compact-bar/src/main.rs20
-rw-r--r--default-plugins/compact-bar/src/tab.rs28
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(