aboutsummaryrefslogtreecommitdiffstats
path: root/default-plugins
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
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')
-rw-r--r--default-plugins/about/src/main.rs1
-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
-rw-r--r--default-plugins/configuration/src/presets.rs24
-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
-rw-r--r--default-plugins/tab-bar/src/line.rs191
-rw-r--r--default-plugins/tab-bar/src/main.rs20
-rw-r--r--default-plugins/tab-bar/src/tab.rs30
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,
)
}