aboutsummaryrefslogtreecommitdiffstats
path: root/default-plugins
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2025-06-10 17:23:46 +0200
committerGitHub <noreply@github.com>2025-06-10 17:23:46 +0200
commit7ef7cd5ecd13974b0f51eec87df164249c294a4c (patch)
tree234b3fdc1ad652a45c8aec3c8e957f4b1452f3e6 /default-plugins
parentfeat(ux): improve multiple select (#4221) (diff)
downloadsl0ppij-7ef7cd5ecd13974b0f51eec87df164249c294a4c.tar.gz
sl0ppij-7ef7cd5ecd13974b0f51eec87df164249c294a4c.tar.xz
sl0ppij-7ef7cd5ecd13974b0f51eec87df164249c294a4c.zip
feat(ui): add optional tooltip with key hints to the compact bar (#4225)
* initial implementation * some refactoring * refactor: separate some concerns and tidy up * fix: move tooltip to focused tab as needed * some ux adjustments * some refactoring * only show tooltip if we have configured a shortcut key * add plugin artifacts * fix tests * truncate tooltip if it exceeds width * change config name * remove comment * docs(changelog): add PR
Diffstat (limited to 'default-plugins')
-rw-r--r--default-plugins/compact-bar/src/action_types.rs129
-rw-r--r--default-plugins/compact-bar/src/clipboard_utils.rs27
-rw-r--r--default-plugins/compact-bar/src/keybind_utils.rs499
-rw-r--r--default-plugins/compact-bar/src/line.rs842
-rw-r--r--default-plugins/compact-bar/src/main.rs654
-rw-r--r--default-plugins/compact-bar/src/tooltip.rs136
6 files changed, 1822 insertions, 465 deletions
diff --git a/default-plugins/compact-bar/src/action_types.rs b/default-plugins/compact-bar/src/action_types.rs
new file mode 100644
index 00000000..7d1bd979
--- /dev/null
+++ b/default-plugins/compact-bar/src/action_types.rs
@@ -0,0 +1,129 @@
+use zellij_tile::prelude::actions::Action;
+use zellij_tile::prelude::*;
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum ActionType {
+ MoveFocus,
+ MovePaneWithDirection,
+ MovePaneWithoutDirection,
+ ResizeIncrease,
+ ResizeDecrease,
+ ResizeAny,
+ Search,
+ NewPaneWithDirection,
+ NewPaneWithoutDirection,
+ BreakPaneLeftOrRight,
+ GoToAdjacentTab,
+ Scroll,
+ PageScroll,
+ HalfPageScroll,
+ SessionManager,
+ Configuration,
+ PluginManager,
+ About,
+ SwitchToMode(InputMode),
+ TogglePaneEmbedOrFloating,
+ ToggleFocusFullscreen,
+ ToggleFloatingPanes,
+ CloseFocus,
+ CloseTab,
+ ToggleActiveSyncTab,
+ ToggleTab,
+ BreakPane,
+ EditScrollback,
+ NewTab,
+ Detach,
+ Quit,
+ Other(String), // Fallback for unhandled actions
+}
+
+impl ActionType {
+ pub fn description(&self) -> String {
+ match self {
+ ActionType::MoveFocus => "Move focus".to_string(),
+ ActionType::MovePaneWithDirection => "Move pane".to_string(),
+ ActionType::MovePaneWithoutDirection => "Move pane".to_string(),
+ ActionType::ResizeIncrease => "Increase size in direction".to_string(),
+ ActionType::ResizeDecrease => "Decrease size in direction".to_string(),
+ ActionType::ResizeAny => "Increase or decrease size".to_string(),
+ ActionType::Search => "Search".to_string(),
+ ActionType::NewPaneWithDirection => "Split right/down".to_string(),
+ ActionType::NewPaneWithoutDirection => "New pane".to_string(),
+ ActionType::BreakPaneLeftOrRight => "Break pane to adjacent tab".to_string(),
+ ActionType::GoToAdjacentTab => "Move tab focus".to_string(),
+ ActionType::Scroll => "Scroll".to_string(),
+ ActionType::PageScroll => "Scroll page".to_string(),
+ ActionType::HalfPageScroll => "Scroll half Page".to_string(),
+ ActionType::SessionManager => "Session manager".to_string(),
+ ActionType::PluginManager => "Plugin manager".to_string(),
+ ActionType::Configuration => "Configuration".to_string(),
+ ActionType::About => "About Zellij".to_string(),
+ ActionType::SwitchToMode(input_mode) if input_mode == &InputMode::RenamePane => {
+ "Rename pane".to_string()
+ },
+ ActionType::SwitchToMode(input_mode) if input_mode == &InputMode::RenameTab => {
+ "Rename tab".to_string()
+ },
+ ActionType::SwitchToMode(input_mode) if input_mode == &InputMode::EnterSearch => {
+ "Search".to_string()
+ },
+ ActionType::SwitchToMode(input_mode) if input_mode == &InputMode::Locked => {
+ "Lock".to_string()
+ },
+ ActionType::SwitchToMode(input_mode) if input_mode == &InputMode::Normal => {
+ "Unlock".to_string()
+ },
+ ActionType::SwitchToMode(input_mode) => format!("{:?}", input_mode),
+ ActionType::TogglePaneEmbedOrFloating => "Float or embed".to_string(),
+ ActionType::ToggleFocusFullscreen => "Toggle fullscreen".to_string(),
+ ActionType::ToggleFloatingPanes => "Show/hide floating panes".to_string(),
+ ActionType::CloseFocus => "Close pane".to_string(),
+ ActionType::CloseTab => "Close tab".to_string(),
+ ActionType::ToggleActiveSyncTab => "Sync panes in tab".to_string(),
+ ActionType::ToggleTab => "Circle tab focus".to_string(),
+ ActionType::BreakPane => "Break pane to new tab".to_string(),
+ ActionType::EditScrollback => "Open pane scrollback in editor".to_string(),
+ ActionType::NewTab => "New tab".to_string(),
+ ActionType::Detach => "Detach".to_string(),
+ ActionType::Quit => "Quit".to_string(),
+ ActionType::Other(_) => "Other action".to_string(),
+ }
+ }
+
+ pub fn from_action(action: &Action) -> Self {
+ match action {
+ Action::MoveFocus(_) => ActionType::MoveFocus,
+ Action::MovePane(Some(_)) => ActionType::MovePaneWithDirection,
+ Action::MovePane(None) => ActionType::MovePaneWithoutDirection,
+ Action::Resize(Resize::Increase, Some(_)) => ActionType::ResizeIncrease,
+ Action::Resize(Resize::Decrease, Some(_)) => ActionType::ResizeDecrease,
+ Action::Resize(_, None) => ActionType::ResizeAny,
+ Action::Search(_) => ActionType::Search,
+ Action::NewPane(Some(_), _, _) => ActionType::NewPaneWithDirection,
+ Action::NewPane(None, _, _) => ActionType::NewPaneWithoutDirection,
+ Action::BreakPaneLeft | Action::BreakPaneRight => ActionType::BreakPaneLeftOrRight,
+ Action::GoToPreviousTab | Action::GoToNextTab => ActionType::GoToAdjacentTab,
+ Action::ScrollUp | Action::ScrollDown => ActionType::Scroll,
+ Action::PageScrollUp | Action::PageScrollDown => ActionType::PageScroll,
+ Action::HalfPageScrollUp | Action::HalfPageScrollDown => ActionType::HalfPageScroll,
+ Action::SwitchToMode(input_mode) => ActionType::SwitchToMode(*input_mode),
+ Action::TogglePaneEmbedOrFloating => ActionType::TogglePaneEmbedOrFloating,
+ Action::ToggleFocusFullscreen => ActionType::ToggleFocusFullscreen,
+ Action::ToggleFloatingPanes => ActionType::ToggleFloatingPanes,
+ Action::CloseFocus => ActionType::CloseFocus,
+ Action::CloseTab => ActionType::CloseTab,
+ Action::ToggleActiveSyncTab => ActionType::ToggleActiveSyncTab,
+ Action::ToggleTab => ActionType::ToggleTab,
+ Action::BreakPane => ActionType::BreakPane,
+ Action::EditScrollback => ActionType::EditScrollback,
+ Action::Detach => ActionType::Detach,
+ Action::Quit => ActionType::Quit,
+ action if action.launches_plugin("session-manager") => ActionType::SessionManager,
+ action if action.launches_plugin("configuration") => ActionType::Configuration,
+ action if action.launches_plugin("plugin-manager") => ActionType::PluginManager,
+ action if action.launches_plugin("zellij:about") => ActionType::About,
+ action if matches!(action, Action::NewTab(..)) => ActionType::NewTab,
+ _ => ActionType::Other(format!("{:?}", action)),
+ }
+ }
+}
diff --git a/default-plugins/compact-bar/src/clipboard_utils.rs b/default-plugins/compact-bar/src/clipboard_utils.rs
new file mode 100644
index 00000000..0ce07936
--- /dev/null
+++ b/default-plugins/compact-bar/src/clipboard_utils.rs
@@ -0,0 +1,27 @@
+use crate::LinePart;
+use zellij_tile::prelude::*;
+
+pub fn text_copied_hint(copy_destination: CopyDestination) -> LinePart {
+ let hint = match copy_destination {
+ CopyDestination::Command => "Text piped to external command",
+ #[cfg(not(target_os = "macos"))]
+ CopyDestination::Primary => "Text copied to system primary selection",
+ #[cfg(target_os = "macos")] // primary selection does not exist on macos
+ CopyDestination::Primary => "Text copied to system clipboard",
+ CopyDestination::System => "Text copied to system clipboard",
+ };
+ LinePart {
+ part: serialize_text(&Text::new(&hint).color_range(2, ..).opaque()),
+ len: hint.len(),
+ tab_index: None,
+ }
+}
+
+pub fn system_clipboard_error() -> LinePart {
+ let hint = " Error using the system clipboard.";
+ LinePart {
+ part: serialize_text(&Text::new(&hint).color_range(2, ..).opaque()),
+ len: hint.len(),
+ tab_index: None,
+ }
+}
diff --git a/default-plugins/compact-bar/src/keybind_utils.rs b/default-plugins/compact-bar/src/keybind_utils.rs
new file mode 100644
index 00000000..c871d7c5
--- /dev/null
+++ b/default-plugins/compact-bar/src/keybind_utils.rs
@@ -0,0 +1,499 @@
+use crate::action_types::ActionType;
+use std::collections::HashSet;
+use zellij_tile::prelude::actions::Action;
+use zellij_tile::prelude::*;
+
+pub struct KeybindProcessor;
+
+impl KeybindProcessor {
+ /// Find predetermined actions based on predicates while maintaining order
+ pub fn find_predetermined_actions<F>(
+ mode_info: &ModeInfo,
+ mode: InputMode,
+ predicates: Vec<F>,
+ ) -> Vec<(String, String)>
+ where
+ F: Fn(&Action) -> bool,
+ {
+ let mut result = Vec::new();
+ let keybinds = mode_info.get_keybinds_for_mode(mode);
+ let mut processed_action_types = HashSet::new();
+
+ // Iterate through predicates in order to maintain the desired sequence
+ for predicate in predicates {
+ // Find the first matching action for this predicate
+ let mut found_match = false;
+ for (_key, actions) in &keybinds {
+ if let Some(first_action) = actions.first() {
+ if predicate(first_action) {
+ let action_type = ActionType::from_action(first_action);
+
+ // Skip if we've already processed this action type
+ if processed_action_types.contains(&action_type) {
+ found_match = true;
+ break;
+ }
+
+ let mut matching_keys = Vec::new();
+
+ // Find all keys that match this action type (including different directions)
+ for (inner_key, inner_actions) in &keybinds {
+ if let Some(inner_first_action) = inner_actions.first() {
+ if ActionType::from_action(inner_first_action) == action_type {
+ matching_keys.push(format!("{}", inner_key));
+ }
+ }
+ }
+
+ if !matching_keys.is_empty() {
+ let description = action_type.description();
+ let should_add_brackets_to_keys = mode != InputMode::Normal;
+
+ // Check if this is switching to normal mode
+ // let is_switching_to_locked = matches!(first_action, Action::SwitchToMode(InputMode::Normal));
+ let is_switching_to_locked =
+ matches!(first_action, Action::SwitchToMode(InputMode::Locked));
+
+ let grouped_keys = Self::group_key_sets(
+ &matching_keys,
+ should_add_brackets_to_keys,
+ is_switching_to_locked,
+ );
+ result.push((grouped_keys, description));
+ processed_action_types.insert(action_type);
+ }
+
+ found_match = true;
+ break;
+ }
+ }
+ }
+
+ // If we found a match for this predicate, we've processed it
+ if found_match {
+ continue;
+ }
+ }
+
+ result
+ }
+
+ /// Group keys into sets and separate different key types with '|'
+ fn group_key_sets(
+ keys: &[String],
+ should_add_brackets_to_keys: bool,
+ is_switching_to_locked: bool,
+ ) -> String {
+ if keys.is_empty() {
+ return String::new();
+ }
+
+ // Filter out Esc and Enter keys when switching to normal mode, but only if other keys exist
+ let filtered_keys: Vec<String> = if is_switching_to_locked {
+ let non_esc_enter_keys: Vec<String> = keys
+ .iter()
+ .filter(|k| k.as_str() != "ESC" && k.as_str() != "ENTER")
+ .cloned()
+ .collect();
+
+ if non_esc_enter_keys.is_empty() {
+ // If no other keys exist, keep the original keys
+ keys.to_vec()
+ } else {
+ // Use filtered keys (without Esc/Enter)
+ non_esc_enter_keys
+ }
+ } else {
+ keys.to_vec()
+ };
+
+ if filtered_keys.len() == 1 {
+ return if should_add_brackets_to_keys {
+ format!("<{}>", filtered_keys[0])
+ } else {
+ filtered_keys[0].clone()
+ };
+ }
+
+ // Group keys by type
+ let mut arrow_keys = Vec::new();
+ let mut hjkl_lower = Vec::new();
+ let mut hjkl_upper = Vec::new();
+ let mut square_bracket_keys = Vec::new();
+ let mut plus_minus_keys = Vec::new();
+ let mut pgup_pgdown = Vec::new();
+ let mut other_keys = Vec::new();
+
+ for key in &filtered_keys {
+ match key.as_str() {
+ "Left" | "←" => arrow_keys.push("←"),
+ "Down" | "↓" => arrow_keys.push("↓"),
+ "Up" | "↑" => arrow_keys.push("↑"),
+ "Right" | "→" => arrow_keys.push("→"),
+ "h" => hjkl_lower.push("h"),
+ "j" => hjkl_lower.push("j"),
+ "k" => hjkl_lower.push("k"),
+ "l" => hjkl_lower.push("l"),
+ "H" => hjkl_upper.push("H"),
+ "J" => hjkl_upper.push("J"),
+ "K" => hjkl_upper.push("K"),
+ "L" => hjkl_upper.push("L"),
+ "[" => square_bracket_keys.push("["),
+ "]" => square_bracket_keys.push("]"),
+ "+" => plus_minus_keys.push("+"),
+ "-" => plus_minus_keys.push("-"),
+ "=" => plus_minus_keys.push("="),
+ "PgUp" => pgup_pgdown.push("PgUp"),
+ "PgDn" => pgup_pgdown.push("PgDn"),
+ _ => {
+ if should_add_brackets_to_keys {
+ other_keys.push(format!("<{}>", key));
+ } else {
+ other_keys.push(key.clone());
+ }
+ },
+ }
+ }
+
+ let mut groups = Vec::new();
+
+ // Add hjkl group if present (prioritize hjkl over arrows)
+ if !hjkl_lower.is_empty() {
+ Self::sort_hjkl(&mut hjkl_lower);
+ groups.push(Self::format_key_group(
+ &hjkl_lower,
+ should_add_brackets_to_keys,
+ false,
+ ));
+ }
+
+ // Add HJKL group if present
+ if !hjkl_upper.is_empty() {
+ Self::sort_hjkl_upper(&mut hjkl_upper);
+ groups.push(Self::format_key_group(
+ &hjkl_upper,
+ should_add_brackets_to_keys,
+ false,
+ ));
+ }
+
+ // Add arrow keys group if present
+ if !arrow_keys.is_empty() {
+ Self::sort_arrows(&mut arrow_keys);
+ groups.push(Self::format_key_group(
+ &arrow_keys,
+ should_add_brackets_to_keys,
+ false,
+ ));
+ }
+
+ if !square_bracket_keys.is_empty() {
+ Self::sort_square_brackets(&mut square_bracket_keys);
+ groups.push(Self::format_key_group(
+ &square_bracket_keys,
+ should_add_brackets_to_keys,
+ false,
+ ));
+ }
+
+ if !plus_minus_keys.is_empty() {
+ Self::sort_plus_minus(&mut plus_minus_keys);
+ groups.push(Self::format_key_group(
+ &plus_minus_keys,
+ should_add_brackets_to_keys,
+ false,
+ ));
+ }
+
+ if !pgup_pgdown.is_empty() {
+ Self::sort_pgup_pgdown(&mut pgup_pgdown);
+ groups.push(Self::format_key_group(
+ &pgup_pgdown,
+ should_add_brackets_to_keys,
+ true,
+ ));
+ }
+
+ // Add other keys with / separator
+ if !other_keys.is_empty() {
+ groups.push(other_keys.join("/"));
+ }
+
+ groups.join("/")
+ }
+
+ fn sort_hjkl(keys: &mut Vec<&str>) {
+ keys.sort_by(|a, b| {
+ let order = ["h", "j", "k", "l"];
+ let pos_a = order.iter().position(|&x| &x == a).unwrap_or(usize::MAX);
+ let pos_b = order.iter().position(|&x| &x == b).unwrap_or(usize::MAX);
+ pos_a.cmp(&pos_b)
+ });
+ }
+
+ fn sort_hjkl_upper(keys: &mut Vec<&str>) {
+ keys.sort_by(|a, b| {
+ let order = ["H", "J", "K", "L"];
+ let pos_a = order.iter().position(|&x| &x == a).unwrap_or(usize::MAX);
+ let pos_b = order.iter().position(|&x| &x == b).unwrap_or(usize::MAX);
+ pos_a.cmp(&pos_b)
+ });
+ }
+
+ fn sort_arrows(keys: &mut Vec<&str>) {
+ keys.sort();
+ keys.dedup();
+ keys.sort_by(|a, b| {
+ let order = ["←", "↓", "↑", "→"];
+ let pos_a = order.iter().position(|&x| &x == a).unwrap_or(usize::MAX);
+ let pos_b = order.iter().position(|&x| &x == b).unwrap_or(usize::MAX);
+ pos_a.cmp(&pos_b)
+ });
+ }
+
+ fn sort_square_brackets(keys: &mut Vec<&str>) {
+ keys.sort_by(|a, b| {
+ let order = ["[", "]"];
+ let pos_a = order.iter().position(|&x| &x == a).unwrap_or(usize::MAX);
+ let pos_b = order.iter().position(|&x| &x == b).unwrap_or(usize::MAX);
+ pos_a.cmp(&pos_b)
+ });
+ }
+
+ fn sort_plus_minus(keys: &mut Vec<&str>) {
+ keys.sort_by(|a, b| {
+ let order = ["+", "-"];
+ let pos_a = order.iter().position(|&x| &x == a).unwrap_or(usize::MAX);
+ let pos_b = order.iter().position(|&x| &x == b).unwrap_or(usize::MAX);
+ pos_a.cmp(&pos_b)
+ });
+ // Remove "=" if both "+" and "=" are present
+ if keys.contains(&"+") && keys.contains(&"=") {
+ keys.retain(|k| k != &"=");
+ }
+ }
+
+ fn sort_pgup_pgdown(keys: &mut Vec<&str>) {
+ keys.sort_by(|a, b| {
+ let order = ["PgUp", "PgDn"];
+ let pos_a = order.iter().position(|&x| &x == a).unwrap_or(usize::MAX);
+ let pos_b = order.iter().position(|&x| &x == b).unwrap_or(usize::MAX);
+ pos_a.cmp(&pos_b)
+ });
+ }
+
+ fn format_key_group(
+ keys: &[&str],
+ should_add_brackets: bool,
+ use_pipe_separator: bool,
+ ) -> String {
+ let separator = if use_pipe_separator { "|" } else { "" };
+ let joined = keys.join(separator);
+
+ if should_add_brackets {
+ format!("<{}>", joined)
+ } else {
+ joined
+ }
+ }
+
+ /// Get predetermined actions for a specific mode
+ pub fn get_predetermined_actions(
+ mode_info: &ModeInfo,
+ mode: InputMode,
+ ) -> Vec<(String, String)> {
+ match mode {
+ InputMode::Locked => {
+ let ordered_predicates = vec![|action: &Action| {
+ matches!(action, Action::SwitchToMode(InputMode::Normal))
+ }];
+ Self::find_predetermined_actions(mode_info, mode, ordered_predicates)
+ },
+ InputMode::Normal => {
+ let ordered_predicates = vec![
+ |action: &Action| matches!(action, Action::SwitchToMode(InputMode::Locked)),
+ |action: &Action| matches!(action, Action::SwitchToMode(InputMode::Pane)),
+ |action: &Action| matches!(action, Action::SwitchToMode(InputMode::Tab)),
+ |action: &Action| matches!(action, Action::SwitchToMode(InputMode::Resize)),
+ |action: &Action| matches!(action, Action::SwitchToMode(InputMode::Move)),
+ |action: &Action| matches!(action, Action::SwitchToMode(InputMode::Scroll)),
+ |action: &Action| matches!(action, Action::SwitchToMode(InputMode::Session)),
+ |action: &Action| matches!(action, Action::Quit),
+ ];
+ Self::find_predetermined_actions(mode_info, mode, ordered_predicates)
+ },
+ InputMode::Pane => {
+ let ordered_predicates = vec![
+ |action: &Action| matches!(action, Action::NewPane(None, None, false)),
+ |action: &Action| matches!(action, Action::MoveFocus(Direction::Left)),
+ |action: &Action| matches!(action, Action::MoveFocus(Direction::Down)),
+ |action: &Action| matches!(action, Action::MoveFocus(Direction::Up)),
+ |action: &Action| matches!(action, Action::MoveFocus(Direction::Right)),
+ |action: &Action| matches!(action, Action::CloseFocus),
+ |action: &Action| matches!(action, Action::SwitchToMode(InputMode::RenamePane)),
+ |action: &Action| matches!(action, Action::ToggleFocusFullscreen),
+ |action: &Action| matches!(action, Action::ToggleFloatingPanes),
+ |action: &Action| matches!(action, Action::TogglePaneEmbedOrFloating),
+ |action: &Action| {
+ matches!(action, Action::NewPane(Some(Direction::Right), None, false))
+ },
+ |action: &Action| {
+ matches!(action, Action::NewPane(Some(Direction::Down), None, false))
+ },
+ ];
+ Self::find_predetermined_actions(mode_info, mode, ordered_predicates)
+ },
+ InputMode::Tab => {
+ let ordered_predicates = vec![
+ |action: &Action| matches!(action, Action::GoToPreviousTab),
+ |action: &Action| matches!(action, Action::GoToNextTab),
+ |action: &Action| {
+ matches!(action, Action::NewTab(None, _, None, None, None, true))
+ },
+ |action: &Action| matches!(action, Action::CloseTab),
+ |action: &Action| matches!(action, Action::SwitchToMode(InputMode::RenameTab)),
+ |action: &Action| matches!(action, Action::TabNameInput(_)),
+ |action: &Action| matches!(action, Action::ToggleActiveSyncTab),
+ |action: &Action| matches!(action, Action::BreakPane),
+ |action: &Action| matches!(action, Action::BreakPaneLeft),
+ |action: &Action| matches!(action, Action::BreakPaneRight),
+ |action: &Action| matches!(action, Action::ToggleTab),
+ ];
+ Self::find_predetermined_actions(mode_info, mode, ordered_predicates)
+ },
+ InputMode::Resize => {
+ let ordered_predicates = vec![
+ |action: &Action| matches!(action, Action::Resize(Resize::Increase, None)),
+ |action: &Action| matches!(action, Action::Resize(Resize::Decrease, None)),
+ |action: &Action| {
+ matches!(
+ action,
+ Action::Resize(Resize::Increase, Some(Direction::Left))
+ )
+ },
+ |action: &Action| {
+ matches!(
+ action,
+ Action::Resize(Resize::Increase, Some(Direction::Down))
+ )
+ },
+ |action: &Action| {
+ matches!(
+ action,
+ Action::Resize(Resize::Increase, Some(Direction::Up))
+ )
+ },
+ |action: &Action| {
+ matches!(
+ action,
+ Action::Resize(Resize::Increase, Some(Direction::Right))
+ )
+ },
+ |action: &Action| {
+ matches!(
+ action,
+ Action::Resize(Resize::Decrease, Some(Direction::Left))
+ )
+ },
+ |action: &Action| {
+ matches!(
+ action,
+ Action::Resize(Resize::Decrease, Some(Direction::Down))
+ )
+ },
+ |action: &Action| {
+ matches!(
+ action,
+ Action::Resize(Resize::Decrease, Some(Direction::Up))
+ )
+ },
+ |action: &Action| {
+ matches!(
+ action,
+ Action::Resize(Resize::Decrease, Some(Direction::Right))
+ )
+ },
+ ];
+ Self::find_predetermined_actions(mode_info, mode, ordered_predicates)
+ },
+ InputMode::Move => {
+ let ordered_predicates = vec![
+ |action: &Action| matches!(action, Action::MovePane(Some(Direction::Left))),
+ |action: &Action| matches!(action, Action::MovePane(Some(Direction::Down))),
+ |action: &Action| matches!(action, Action::MovePane(Some(Direction::Up))),
+ |action: &Action| matches!(action, Action::MovePane(Some(Direction::Right))),
+ ];
+ Self::find_predetermined_actions(mode_info, mode, ordered_predicates)
+ },
+ InputMode::Scroll => {
+ let ordered_predicates = vec![
+ |action: &Action| matches!(action, Action::ScrollDown),
+ |action: &Action| matches!(action, Action::ScrollUp),
+ |action: &Action| matches!(action, Action::HalfPageScrollDown),
+ |action: &Action| matches!(action, Action::HalfPageScrollUp),
+ |action: &Action| matches!(action, Action::PageScrollDown),
+ |action: &Action| matches!(action, Action::PageScrollUp),
+ |action: &Action| {
+ matches!(action, Action::SwitchToMode(InputMode::EnterSearch))
+ },
+ |action: &Action| matches!(action, Action::EditScrollback),
+ ];
+ Self::find_predetermined_actions(mode_info, mode, ordered_predicates)
+ },
+ InputMode::Search => {
+ let ordered_predicates = vec![
+ |action: &Action| {
+ matches!(action, Action::SwitchToMode(InputMode::EnterSearch))
+ },
+ |action: &Action| matches!(action, Action::SearchInput(_)),
+ |action: &Action| matches!(action, Action::ScrollDown),
+ |action: &Action| matches!(action, Action::ScrollUp),
+ |action: &Action| matches!(action, Action::PageScrollDown),
+ |action: &Action| matches!(action, Action::PageScrollUp),
+ |action: &Action| matches!(action, Action::HalfPageScrollDown),
+ |action: &Action| matches!(action, Action::HalfPageScrollUp),
+ |action: &Action| {
+ matches!(action, Action::Search(actions::SearchDirection::Down))
+ },
+ |action: &Action| {
+ matches!(action, Action::Search(actions::SearchDirection::Up))
+ },
+ |action: &Action| {
+ matches!(
+ action,
+ Action::SearchToggleOption(actions::SearchOption::CaseSensitivity)
+ )
+ },
+ |action: &Action| {
+ matches!(
+ action,
+ Action::SearchToggleOption(actions::SearchOption::Wrap)
+ )
+ },
+ |action: &Action| {
+ matches!(
+ action,
+ Action::SearchToggleOption(actions::SearchOption::WholeWord)
+ )
+ },
+ ];
+ Self::find_predetermined_actions(mode_info, mode, ordered_predicates)
+ },
+ InputMode::Session => {
+ let ordered_predicates = vec![
+ |action: &Action| matches!(action, Action::Detach),
+ |action: &Action| action.launches_plugin("session-manager"),
+ |action: &Action| action.launches_plugin("plugin-manager"),
+ |action: &Action| action.launches_plugin("configuration"),
+ |action: &Action| action.launches_plugin("zellij:about"),
+ ];
+ Self::find_predetermined_actions(mode_info, mode, ordered_predicates)
+ },
+ InputMode::EnterSearch
+ | InputMode::RenameTab
+ | InputMode::RenamePane
+ | InputMode::Prompt
+ | InputMode::Tmux => Vec::new(),
+ }
+ }
+}
diff --git a/default-plugins/compact-bar/src/line.rs b/default-plugins/compact-bar/src/line.rs
index 5d50d527..451cbeb7 100644
--- a/default-plugins/compact-bar/src/line.rs
+++ b/default-plugins/compact-bar/src/line.rs
@@ -1,375 +1,617 @@
use ansi_term::ANSIStrings;
use unicode_width::UnicodeWidthStr;
-use crate::{LinePart, ARROW_SEPARATOR};
+use crate::{LinePart, TabRenderData, ARROW_SEPARATOR};
use zellij_tile::prelude::*;
use zellij_tile_utils::style;
-fn get_current_title_len(current_title: &[LinePart]) -> usize {
- current_title.iter().map(|p| p.len).sum()
+pub fn tab_line(
+ mode_info: &ModeInfo,
+ tab_data: TabRenderData,
+ cols: usize,
+ toggle_tooltip_key: Option<String>,
+ tooltip_is_active: bool,
+) -> Vec<LinePart> {
+ let config = TabLineConfig {
+ session_name: mode_info.session_name.to_owned(),
+ hide_session_name: mode_info.style.hide_session_name,
+ mode: mode_info.mode,
+ active_swap_layout_name: tab_data.active_swap_layout_name,
+ is_swap_layout_dirty: tab_data.is_swap_layout_dirty,
+ toggle_tooltip_key,
+ tooltip_is_active,
+ };
+
+ let builder = TabLineBuilder::new(config, mode_info.style.colors, mode_info.capabilities, cols);
+ builder.build(tab_data.tabs, tab_data.active_tab_index)
+}
+
+#[derive(Debug, Clone)]
+pub struct TabLineConfig {
+ pub session_name: Option<String>,
+ pub hide_session_name: bool,
+ pub mode: InputMode,
+ pub active_swap_layout_name: Option<String>,
+ pub is_swap_layout_dirty: bool,
+ pub toggle_tooltip_key: Option<String>,
+ pub tooltip_is_active: bool,
+}
+
+fn calculate_total_length(parts: &[LinePart]) -> usize {
+ parts.iter().map(|p| p.len).sum()
}
-// 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(
- tabs_before_active: &mut Vec<LinePart>,
- tabs_after_active: &mut Vec<LinePart>,
- tabs_to_render: &mut Vec<LinePart>,
+struct TabLinePopulator {
cols: usize,
palette: Styling,
capabilities: PluginCapabilities,
-) {
- let mut middle_size = get_current_title_len(tabs_to_render);
-
- let mut total_left = 0;
- let mut total_right = 0;
- loop {
- let left_count = tabs_before_active.len();
- let right_count = tabs_after_active.len();
+}
- // left_more_tab_index is the tab to the left of the leftmost visible tab
- let left_more_tab_index = left_count.saturating_sub(1);
- let collapsed_left = left_more_message(
- left_count,
+impl TabLinePopulator {
+ fn new(cols: usize, palette: Styling, capabilities: PluginCapabilities) -> Self {
+ Self {
+ cols,
palette,
- tab_separator(capabilities),
- left_more_tab_index,
- );
- // right_more_tab_index is the tab to the right of the rightmost visible tab
- let right_more_tab_index = left_count + tabs_to_render.len();
- let collapsed_right = right_more_message(
- right_count,
- palette,
- tab_separator(capabilities),
- right_more_tab_index,
- );
+ capabilities,
+ }
+ }
+
+ fn populate_tabs(
+ &self,
+ tabs_before_active: &mut Vec<LinePart>,
+ tabs_after_active: &mut Vec<LinePart>,
+ tabs_to_render: &mut Vec<LinePart>,
+ ) {
+ let mut middle_size = calculate_total_length(tabs_to_render);
+ let mut total_left = 0;
+ let mut total_right = 0;
+
+ loop {
+ let left_count = tabs_before_active.len();
+ let right_count = tabs_after_active.len();
+
+ let collapsed_indicators =
+ self.create_collapsed_indicators(left_count, right_count, tabs_to_render.len());
+
+ let total_size =
+ collapsed_indicators.left.len + middle_size + collapsed_indicators.right.len;
+
+ if total_size > self.cols {
+ break;
+ }
+
+ let tab_sizes = TabSizes {
+ left: tabs_before_active.last().map_or(usize::MAX, |tab| tab.len),
+ right: tabs_after_active.get(0).map_or(usize::MAX, |tab| tab.len),
+ };
- let total_size = collapsed_left.len + middle_size + collapsed_right.len;
+ let fit_analysis = self.analyze_tab_fit(
+ &tab_sizes,
+ total_size,
+ left_count,
+ right_count,
+ &collapsed_indicators,
+ );
- if total_size > cols {
- // break and dont add collapsed tabs to tabs_to_render, they will not fit
- break;
+ match self.decide_next_action(&fit_analysis, total_left, total_right) {
+ TabAction::AddLeft => {
+ if let Some(tab) = tabs_before_active.pop() {
+ middle_size += tab.len;
+ total_left += tab.len;
+ tabs_to_render.insert(0, tab);
+ }
+ },
+ TabAction::AddRight => {
+ if !tabs_after_active.is_empty() {
+ let tab = tabs_after_active.remove(0);
+ middle_size += tab.len;
+ total_right += tab.len;
+ tabs_to_render.push(tab);
+ }
+ },
+ TabAction::Finish => {
+ tabs_to_render.insert(0, collapsed_indicators.left);
+ tabs_to_render.push(collapsed_indicators.right);
+ break;
+ },
+ }
}
+ }
- let left = if let Some(tab) = tabs_before_active.last() {
- tab.len
- } else {
- usize::MAX
- };
+ fn create_collapsed_indicators(
+ &self,
+ left_count: usize,
+ right_count: usize,
+ rendered_count: usize,
+ ) -> CollapsedIndicators {
+ let left_more_tab_index = left_count.saturating_sub(1);
+ let right_more_tab_index = left_count + rendered_count;
- let right = if let Some(tab) = tabs_after_active.first() {
- tab.len
- } else {
- usize::MAX
- };
+ CollapsedIndicators {
+ left: self.create_left_indicator(left_count, left_more_tab_index),
+ right: self.create_right_indicator(right_count, right_more_tab_index),
+ }
+ }
- // total size is shortened if the next tab to be added is the last one, as that will remove the collapsed tab
+ fn analyze_tab_fit(
+ &self,
+ tab_sizes: &TabSizes,
+ total_size: usize,
+ left_count: usize,
+ right_count: usize,
+ collapsed_indicators: &CollapsedIndicators,
+ ) -> TabFitAnalysis {
let size_by_adding_left =
- left.saturating_add(total_size)
+ tab_sizes
+ .left
+ .saturating_add(total_size)
.saturating_sub(if left_count == 1 {
- collapsed_left.len
+ collapsed_indicators.left.len
} else {
0
});
+
let size_by_adding_right =
- right
+ tab_sizes
+ .right
.saturating_add(total_size)
.saturating_sub(if right_count == 1 {
- collapsed_right.len
+ collapsed_indicators.right.len
} else {
0
});
- let left_fits = size_by_adding_left <= cols;
- let right_fits = size_by_adding_right <= cols;
- // active tab is kept in the middle by adding to the side that
- // has less width, or if the tab on the other side doesn't fit
- if (total_left <= total_right || !right_fits) && left_fits {
- // add left tab
- let tab = tabs_before_active.pop().unwrap();
- middle_size += tab.len;
- total_left += tab.len;
- tabs_to_render.insert(0, tab);
- } else if right_fits {
- // add right tab
- let tab = tabs_after_active.remove(0);
- middle_size += tab.len;
- total_right += tab.len;
- tabs_to_render.push(tab);
+ TabFitAnalysis {
+ left_fits: size_by_adding_left <= self.cols,
+ right_fits: size_by_adding_right <= self.cols,
+ }
+ }
+
+ fn decide_next_action(
+ &self,
+ fit_analysis: &TabFitAnalysis,
+ total_left: usize,
+ total_right: usize,
+ ) -> TabAction {
+ if (total_left <= total_right || !fit_analysis.right_fits) && fit_analysis.left_fits {
+ TabAction::AddLeft
+ } else if fit_analysis.right_fits {
+ TabAction::AddRight
} else {
- // there's either no space to add more tabs or no more tabs to add, so we're done
- tabs_to_render.insert(0, collapsed_left);
- tabs_to_render.push(collapsed_right);
- break;
+ TabAction::Finish
}
}
-}
-fn left_more_message(
- tab_count_to_the_left: usize,
- palette: Styling,
- separator: &str,
- tab_index: usize,
-) -> LinePart {
- if tab_count_to_the_left == 0 {
- return LinePart::default();
+ fn create_left_indicator(&self, tab_count: usize, tab_index: usize) -> LinePart {
+ if tab_count == 0 {
+ return LinePart::default();
+ }
+
+ let more_text = self.format_count_text(tab_count, "← +{}", " ← +many ");
+ self.create_styled_indicator(more_text, tab_index)
}
- let more_text = if tab_count_to_the_left < 10000 {
- format!(" ← +{} ", tab_count_to_the_left)
- } else {
- " ← +many ".to_string()
- };
- // 238
- // chars length plus separator length on both sides
- let more_text_len = more_text.width() + 2 * separator.width();
- let (text_color, sep_color) = (
- palette.ribbon_unselected.base,
- palette.text_unselected.background,
- );
- let plus_ribbon_bg = palette.text_selected.emphasis_0;
- let left_separator = style!(sep_color, plus_ribbon_bg).paint(separator);
- let more_styled_text = style!(text_color, plus_ribbon_bg).bold().paint(more_text);
- let right_separator = style!(plus_ribbon_bg, sep_color).paint(separator);
- let more_styled_text =
- ANSIStrings(&[left_separator, more_styled_text, right_separator]).to_string();
- LinePart {
- part: more_styled_text,
- len: more_text_len,
- tab_index: Some(tab_index),
+
+ fn create_right_indicator(&self, tab_count: usize, tab_index: usize) -> LinePart {
+ if tab_count == 0 {
+ return LinePart::default();
+ }
+
+ let more_text = self.format_count_text(tab_count, "+{} →", " +many → ");
+ self.create_styled_indicator(more_text, tab_index)
}
-}
-fn right_more_message(
- tab_count_to_the_right: usize,
- palette: Styling,
- separator: &str,
- tab_index: usize,
-) -> LinePart {
- if tab_count_to_the_right == 0 {
- return LinePart::default();
- };
- let more_text = if tab_count_to_the_right < 10000 {
- format!(" +{} → ", tab_count_to_the_right)
- } else {
- " +many → ".to_string()
- };
- // chars length plus separator length on both sides
- let more_text_len = more_text.width() + 2 * separator.width();
+ fn format_count_text(&self, count: usize, format_str: &str, fallback: &str) -> String {
+ if count < 10000 {
+ format!(" {} ", format_str.replace("{}", &count.to_string()))
+ } else {
+ fallback.to_string()
+ }
+ }
+
+ fn create_styled_indicator(&self, text: String, tab_index: usize) -> LinePart {
+ let separator = tab_separator(self.capabilities);
+ let text_len = text.width() + 2 * separator.width();
+
+ let colors = IndicatorColors {
+ text: self.palette.ribbon_unselected.base,
+ separator: self.palette.text_unselected.background,
+ background: self.palette.text_selected.emphasis_0,
+ };
+
+ let styled_parts = [
+ style!(colors.separator, colors.background).paint(separator),
+ style!(colors.text, colors.background).bold().paint(text),
+ style!(colors.background, colors.separator).paint(separator),
+ ];
- let (text_color, sep_color) = (
- palette.ribbon_unselected.base,
- palette.text_unselected.background,
- );
- let plus_ribbon_bg = palette.text_selected.emphasis_0;
- let left_separator = style!(sep_color, plus_ribbon_bg).paint(separator);
- let more_styled_text = style!(text_color, plus_ribbon_bg).bold().paint(more_text);
- let right_separator = style!(plus_ribbon_bg, sep_color).paint(separator);
- let more_styled_text =
- ANSIStrings(&[left_separator, more_styled_text, right_separator]).to_string();
- LinePart {
- part: more_styled_text,
- len: more_text_len,
- tab_index: Some(tab_index),
+ LinePart {
+ part: ANSIStrings(&styled_parts).to_string(),
+ len: text_len,
+ tab_index: Some(tab_index),
+ }
}
}
-fn tab_line_prefix(
- session_name: Option<&str>,
- mode: InputMode,
+#[derive(Debug)]
+struct CollapsedIndicators {
+ left: LinePart,
+ right: LinePart,
+}
+
+#[derive(Debug)]
+struct TabSizes {
+ left: usize,
+ right: usize,
+}
+
+#[derive(Debug)]
+struct TabFitAnalysis {
+ left_fits: bool,
+ right_fits: bool,
+}
+
+#[derive(Debug)]
+struct IndicatorColors {
+ text: PaletteColor,
+ separator: PaletteColor,
+ background: PaletteColor,
+}
+
+#[derive(Debug)]
+enum TabAction {
+ AddLeft,
+ AddRight,
+ Finish,
+}
+
+struct TabLinePrefixBuilder {
palette: Styling,
cols: usize,
-) -> Vec<LinePart> {
- let prefix_text = " Zellij ".to_string();
+}
+
+impl TabLinePrefixBuilder {
+ fn new(palette: Styling, cols: usize) -> Self {
+ Self { palette, cols }
+ }
+
+ fn build(&self, session_name: Option<&str>, mode: InputMode) -> Vec<LinePart> {
+ let mut parts = vec![self.create_zellij_part()];
+ let mut used_len = parts.get(0).map_or(0, |p| p.len);
+
+ if let Some(name) = session_name {
+ if let Some(name_part) = self.create_session_name_part(name, used_len) {
+ used_len += name_part.len;
+ parts.push(name_part);
+ }
+ }
+
+ if let Some(mode_part) = self.create_mode_part(mode, used_len) {
+ parts.push(mode_part);
+ }
+
+ parts
+ }
+
+ fn create_zellij_part(&self) -> LinePart {
+ let prefix_text = " Zellij ";
+ let colors = self.get_text_colors();
- let prefix_text_len = prefix_text.chars().count();
- let text_color = palette.text_unselected.base;
- let bg_color = palette.text_unselected.background;
- let locked_mode_color = palette.text_unselected.emphasis_3;
- let normal_mode_color = palette.text_unselected.emphasis_2;
- let other_modes_color = palette.text_unselected.emphasis_0;
+ LinePart {
+ part: style!(colors.text, colors.background)
+ .bold()
+ .paint(prefix_text)
+ .to_string(),
+ len: prefix_text.chars().count(),
+ tab_index: None,
+ }
+ }
- let prefix_styled_text = style!(text_color, bg_color).bold().paint(prefix_text);
- let mut parts = vec![LinePart {
- part: prefix_styled_text.to_string(),
- len: prefix_text_len,
- tab_index: None,
- }];
- if let Some(name) = session_name {
+ fn create_session_name_part(&self, name: &str, used_len: usize) -> Option<LinePart> {
let name_part = format!("({})", name);
let name_part_len = name_part.width();
- let name_part_styled_text = style!(text_color, bg_color).bold().paint(name_part);
- if cols.saturating_sub(prefix_text_len) >= name_part_len {
- parts.push(LinePart {
- part: name_part_styled_text.to_string(),
+
+ if self.cols.saturating_sub(used_len) >= name_part_len {
+ let colors = self.get_text_colors();
+ Some(LinePart {
+ part: style!(colors.text, colors.background)
+ .bold()
+ .paint(name_part)
+ .to_string(),
len: name_part_len,
tab_index: None,
})
+ } else {
+ None
}
}
- let mode_part = format!("{:?}", mode).to_uppercase();
- let mode_part_padded = format!(" {} ", mode_part);
- let mode_part_len = mode_part_padded.width();
- let mode_part_styled_text = if mode == InputMode::Locked {
- style!(locked_mode_color, bg_color)
- .bold()
- .paint(mode_part_padded)
- } else if mode == InputMode::Normal {
- style!(normal_mode_color, bg_color)
- .bold()
- .paint(mode_part_padded)
- } else {
- style!(other_modes_color, bg_color)
- .bold()
- .paint(mode_part_padded)
- };
- if cols.saturating_sub(prefix_text_len) >= mode_part_len {
- parts.push(LinePart {
- part: format!("{}", mode_part_styled_text),
- len: mode_part_len,
- tab_index: None,
- })
+
+ fn create_mode_part(&self, mode: InputMode, used_len: usize) -> Option<LinePart> {
+ let mode_text = format!(" {} ", format!("{:?}", mode).to_uppercase());
+ let mode_len = mode_text.width();
+
+ 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),
+ };
+
+ Some(LinePart {
+ part: style.bold().paint(mode_text).to_string(),
+ len: mode_len,
+ tab_index: None,
+ })
+ } else {
+ None
+ }
}
- parts
-}
-pub fn tab_separator(capabilities: PluginCapabilities) -> &'static str {
- if !capabilities.arrow_fonts {
- ARROW_SEPARATOR
- } else {
- ""
+ fn get_text_colors(&self) -> IndicatorColors {
+ IndicatorColors {
+ text: self.palette.text_unselected.base,
+ background: self.palette.text_unselected.background,
+ separator: self.palette.text_unselected.background,
+ }
}
}
-pub fn tab_line(
- session_name: Option<&str>,
- mut all_tabs: Vec<LinePart>,
- active_tab_index: usize,
- cols: usize,
+struct RightSideElementsBuilder {
palette: Styling,
capabilities: PluginCapabilities,
- hide_session_name: bool,
- mode: InputMode,
- active_swap_layout_name: &Option<String>,
- is_swap_layout_dirty: bool,
-) -> Vec<LinePart> {
- 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() {
- tabs_after_active.remove(0)
- } else {
- tabs_before_active.pop().unwrap()
- };
- let mut prefix = match hide_session_name {
- true => tab_line_prefix(None, mode, palette, cols),
- false => tab_line_prefix(session_name, mode, palette, cols),
- };
- let prefix_len = get_current_title_len(&prefix);
+}
- // if active tab alone won't fit in cols, don't draw any tabs
- if prefix_len + active_tab.len > cols {
- return prefix;
+impl RightSideElementsBuilder {
+ fn new(palette: Styling, capabilities: PluginCapabilities) -> Self {
+ Self {
+ palette,
+ capabilities,
+ }
}
- let mut tabs_to_render = vec![active_tab];
+ fn build(&self, config: &TabLineConfig, available_space: usize) -> Vec<LinePart> {
+ let mut elements = Vec::new();
- populate_tabs_in_tab_line(
- &mut tabs_before_active,
- &mut tabs_after_active,
- &mut tabs_to_render,
- cols.saturating_sub(prefix_len),
- palette,
- capabilities,
- );
- prefix.append(&mut tabs_to_render);
+ if let Some(ref tooltip_key) = config.toggle_tooltip_key {
+ elements.push(self.create_tooltip_indicator(tooltip_key, config.tooltip_is_active));
+ }
- let current_title_len = get_current_title_len(&prefix);
- if current_title_len < cols {
- let mut remaining_space = cols - current_title_len;
- let remaining_bg = palette.text_unselected.background;
- if let Some(swap_layout_status) = swap_layout_status(
- remaining_space,
- active_swap_layout_name,
- is_swap_layout_dirty,
- mode,
- &palette,
- tab_separator(capabilities),
- ) {
- remaining_space -= swap_layout_status.len;
- let mut buffer = String::new();
- for _ in 0..remaining_space {
- buffer.push_str(&style!(remaining_bg, remaining_bg).paint(" ").to_string());
- }
- prefix.push(LinePart {
- part: buffer,
- len: remaining_space,
+ if let Some(swap_status) = self.create_swap_layout_status(config, available_space) {
+ elements.push(swap_status);
+ }
+
+ elements
+ }
+
+ 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);
+ let ribbon_text = "Tooltip";
+ let mut ribbon = Text::new(ribbon_text);
+
+ if is_active {
+ ribbon = ribbon.selected();
+ }
+
+ LinePart {
+ part: format!("{} {}", serialize_text(&key), serialize_ribbon(&ribbon)),
+ len: key_text.chars().count() + ribbon_text.chars().count() + 6,
+ tab_index: None,
+ }
+ }
+
+ fn create_swap_layout_status(
+ &self,
+ config: &TabLineConfig,
+ max_len: usize,
+ ) -> Option<LinePart> {
+ let swap_layout_name = config.active_swap_layout_name.as_ref()?;
+
+ let mut layout_name = format!(" {} ", swap_layout_name);
+ layout_name.make_ascii_uppercase();
+ let layout_name_len = layout_name.len() + 3;
+
+ let colors = SwapLayoutColors {
+ bg: self.palette.text_unselected.background,
+ fg: self.palette.ribbon_unselected.background,
+ green: self.palette.ribbon_selected.background,
+ };
+
+ let separator = tab_separator(self.capabilities);
+ let styled_parts = self.create_swap_layout_styled_parts(
+ &layout_name,
+ config.mode,
+ config.is_swap_layout_dirty,
+ &colors,
+ separator,
+ );
+
+ let indicator = format!("{}{}{}", styled_parts.0, styled_parts.1, styled_parts.2);
+ let (part, full_len) = (indicator.clone(), layout_name_len);
+ let short_len = layout_name_len + 1;
+
+ if full_len <= max_len {
+ Some(LinePart {
+ part,
+ len: full_len,
tab_index: None,
- });
- prefix.push(swap_layout_status);
+ })
+ } else if short_len <= max_len && config.mode != InputMode::Locked {
+ Some(LinePart {
+ part: indicator,
+ len: short_len,
+ tab_index: None,
+ })
+ } else {
+ None
}
}
- prefix
+ fn create_swap_layout_styled_parts(
+ &self,
+ layout_name: &str,
+ mode: InputMode,
+ is_dirty: bool,
+ colors: &SwapLayoutColors,
+ separator: &str,
+ ) -> (String, String, String) {
+ match mode {
+ InputMode::Locked => (
+ style!(colors.bg, colors.fg).paint(separator).to_string(),
+ style!(colors.bg, colors.fg)
+ .italic()
+ .paint(layout_name)
+ .to_string(),
+ style!(colors.fg, colors.bg).paint(separator).to_string(),
+ ),
+ _ if is_dirty => (
+ style!(colors.bg, colors.fg).paint(separator).to_string(),
+ style!(colors.bg, colors.fg)
+ .bold()
+ .paint(layout_name)
+ .to_string(),
+ style!(colors.fg, colors.bg).paint(separator).to_string(),
+ ),
+ _ => (
+ style!(colors.bg, colors.green).paint(separator).to_string(),
+ style!(colors.bg, colors.green)
+ .bold()
+ .paint(layout_name)
+ .to_string(),
+ style!(colors.green, colors.bg).paint(separator).to_string(),
+ ),
+ }
+ }
}
-fn swap_layout_status(
- max_len: usize,
- swap_layout_name: &Option<String>,
- is_swap_layout_damaged: bool,
- input_mode: InputMode,
- palette: &Styling,
- separator: &str,
-) -> Option<LinePart> {
- match swap_layout_name {
- Some(swap_layout_name) => {
- let mut swap_layout_name = format!(" {} ", swap_layout_name);
- swap_layout_name.make_ascii_uppercase();
- let swap_layout_name_len = swap_layout_name.len() + 3;
- let bg = palette.text_unselected.background;
- let fg = palette.ribbon_unselected.background;
- let green = palette.ribbon_selected.background;
+#[derive(Debug)]
+struct SwapLayoutColors {
+ bg: PaletteColor,
+ fg: PaletteColor,
+ green: PaletteColor,
+}
- let (prefix_separator, swap_layout_name, suffix_separator) =
- if input_mode == InputMode::Locked {
- (
- style!(bg, fg).paint(separator),
- style!(bg, fg).italic().paint(&swap_layout_name),
- style!(fg, bg).paint(separator),
- )
- } else if is_swap_layout_damaged {
- (
- style!(bg, fg).paint(separator),
- style!(bg, fg).bold().paint(&swap_layout_name),
- style!(fg, bg).paint(separator),
- )
- } else {
- (
- style!(bg, green).paint(separator),
- style!(bg, green).bold().paint(&swap_layout_name),
- style!(green, bg).paint(separator),
- )
- };
- let swap_layout_indicator = format!(
- "{}{}{}",
- prefix_separator, swap_layout_name, suffix_separator
- );
- let (part, full_len) = (format!("{}", swap_layout_indicator), swap_layout_name_len);
- let short_len = swap_layout_name_len + 1; // 1 is the space between
- if full_len <= max_len {
- Some(LinePart {
- part,
- len: full_len,
- tab_index: None,
- })
- } else if short_len <= max_len && input_mode != InputMode::Locked {
- Some(LinePart {
- part: swap_layout_indicator,
- len: short_len,
- tab_index: None,
- })
- } else {
- None
+pub struct TabLineBuilder {
+ config: TabLineConfig,
+ palette: Styling,
+ capabilities: PluginCapabilities,
+ cols: usize,
+}
+
+impl TabLineBuilder {
+ pub fn new(
+ config: TabLineConfig,
+ palette: Styling,
+ capabilities: PluginCapabilities,
+ cols: usize,
+ ) -> Self {
+ Self {
+ config,
+ palette,
+ capabilities,
+ cols,
+ }
+ }
+
+ pub fn build(self, all_tabs: Vec<LinePart>, active_tab_index: usize) -> Vec<LinePart> {
+ 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 mut prefix = prefix_builder.build(session_name, self.config.mode);
+ let prefix_len = calculate_total_length(&prefix);
+
+ if prefix_len + active_tab.len > self.cols {
+ return prefix;
+ }
+
+ let mut tabs_to_render = vec![active_tab];
+ let populator = TabLinePopulator::new(
+ self.cols.saturating_sub(prefix_len),
+ self.palette,
+ self.capabilities,
+ );
+
+ let mut tabs_before = tabs_before_active;
+ let mut tabs_after = tabs_after_active;
+ populator.populate_tabs(&mut tabs_before, &mut tabs_after, &mut tabs_to_render);
+
+ prefix.append(&mut tabs_to_render);
+
+ self.add_right_side_elements(&mut prefix);
+ prefix
+ }
+
+ fn split_tabs(
+ &self,
+ mut all_tabs: Vec<LinePart>,
+ active_tab_index: usize,
+ ) -> (Vec<LinePart>, LinePart, Vec<LinePart>) {
+ 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() {
+ tabs_after_active.remove(0)
+ } else {
+ tabs_before_active.pop().unwrap_or_default()
+ };
+
+ (tabs_before_active, active_tab, tabs_after_active)
+ }
+
+ fn add_right_side_elements(&self, prefix: &mut Vec<LinePart>) {
+ let current_len = calculate_total_length(prefix);
+
+ if current_len < self.cols {
+ let right_builder = RightSideElementsBuilder::new(self.palette, self.capabilities);
+ let available_space = self.cols.saturating_sub(current_len);
+ let mut right_elements = right_builder.build(&self.config, available_space);
+
+ let right_len = calculate_total_length(&right_elements);
+
+ if current_len + right_len <= self.cols {
+ let remaining_space = self
+ .cols
+ .saturating_sub(current_len)
+ .saturating_sub(right_len);
+
+ if remaining_space > 0 {
+ prefix.push(self.create_spacer(remaining_space));
+ }
+
+ prefix.append(&mut right_elements);
}
- },
- None => None,
+ }
+ }
+
+ fn create_spacer(&self, space: usize) -> LinePart {
+ let bg = self.palette.text_unselected.background;
+ let buffer = (0..space)
+ .map(|_| style!(bg, bg).paint(" ").to_string())
+ .collect::<String>();
+
+ LinePart {
+ part: buffer,
+ len: space,
+ tab_index: None,
+ }
+ }
+}
+
+pub fn tab_separator(capabilities: PluginCapabilities) -> &'static str {
+ if !capabilities.arrow_fonts {
+ ARROW_SEPARATOR
+ } else {
+ ""
}
}
diff --git a/default-plugins/compact-bar/src/main.rs b/default-plugins/compact-bar/src/main.rs
index 91924211..9dde0e31 100644
--- a/default-plugins/compact-bar/src/main.rs
+++ b/default-plugins/compact-bar/src/main.rs
@@ -1,5 +1,9 @@
+mod action_types;
+mod clipboard_utils;
+mod keybind_utils;
mod line;
mod tab;
+mod tooltip;
use std::cmp::{max, min};
use std::collections::BTreeMap;
@@ -8,8 +12,18 @@ use std::convert::TryInto;
use tab::get_tab_to_focus;
use zellij_tile::prelude::*;
+use crate::clipboard_utils::{system_clipboard_error, text_copied_hint};
use crate::line::tab_line;
use crate::tab::tab_style;
+use crate::tooltip::TooltipRenderer;
+
+static ARROW_SEPARATOR: &str = "";
+
+const CONFIG_IS_TOOLTIP: &str = "is_tooltip";
+const CONFIG_TOGGLE_TOOLTIP_KEY: &str = "tooltip";
+const MSG_TOGGLE_TOOLTIP: &str = "toggle_tooltip";
+const MSG_TOGGLE_PERSISTED_TOOLTIP: &str = "toggle_persisted_tooltip";
+const MSG_LAUNCH_TOOLTIP: &str = "launch_tooltip_if_not_launched";
#[derive(Debug, Default)]
pub struct LinePart {
@@ -20,207 +34,517 @@ pub struct LinePart {
#[derive(Default)]
struct State {
+ // Tab state
tabs: Vec<TabInfo>,
active_tab_idx: usize,
+
+ // Display state
mode_info: ModeInfo,
tab_line: Vec<LinePart>,
+ display_area_rows: usize,
+ display_area_cols: usize,
+
+ // Clipboard state
text_copy_destination: Option<CopyDestination>,
display_system_clipboard_failure: bool,
+
+ // Plugin configuration
+ config: BTreeMap<String, String>,
+ own_plugin_id: Option<u32>,
+ toggle_tooltip_key: Option<String>,
+
+ // Tooltip state
+ is_tooltip: bool,
+ tooltip_is_active: bool,
+ persist: bool,
+ is_first_run: bool,
}
-static ARROW_SEPARATOR: &str = "";
+struct TabRenderData {
+ tabs: Vec<LinePart>,
+ active_tab_index: usize,
+ active_swap_layout_name: Option<String>,
+ is_swap_layout_dirty: bool,
+}
register_plugin!(State);
impl ZellijPlugin for State {
- fn load(&mut self, _configuration: BTreeMap<String, String>) {
- set_selectable(false);
- subscribe(&[
- EventType::TabUpdate,
- EventType::ModeUpdate,
- EventType::Mouse,
- EventType::CopyToClipboard,
- EventType::InputReceived,
- EventType::SystemClipboardFailure,
- EventType::PaneUpdate,
- ]);
+ fn load(&mut self, configuration: BTreeMap<String, String>) {
+ self.initialize_configuration(configuration);
+ self.setup_subscriptions();
+ self.configure_keybinds();
+ self.own_plugin_id = Some(get_plugin_ids().plugin_id);
}
fn update(&mut self, event: Event) -> bool {
- let mut should_render = false;
+ self.is_first_run = false;
+
match event {
- Event::ModeUpdate(mode_info) => {
- if self.mode_info != mode_info {
- should_render = true;
- }
- self.mode_info = mode_info
- },
- Event::TabUpdate(tabs) => {
- if let Some(active_tab_index) = tabs.iter().position(|t| t.active) {
- // tabs are indexed starting from 1 so we need to add 1
- let active_tab_idx = active_tab_index + 1;
- if self.active_tab_idx != active_tab_idx || self.tabs != tabs {
- should_render = true;
- }
- self.active_tab_idx = active_tab_idx;
- self.tabs = tabs;
- } else {
- eprintln!("Could not find active tab.");
- }
- },
- Event::Mouse(me) => match me {
- Mouse::LeftClick(_, col) => {
- let tab_to_focus = get_tab_to_focus(&self.tab_line, self.active_tab_idx, col);
- if let Some(idx) = tab_to_focus {
- switch_tab_to(idx.try_into().unwrap());
- }
- },
- Mouse::ScrollUp(_) => {
- switch_tab_to(min(self.active_tab_idx + 1, self.tabs.len()) as u32);
- },
- Mouse::ScrollDown(_) => {
- switch_tab_to(max(self.active_tab_idx.saturating_sub(1), 1) as u32);
- },
- _ => {},
+ Event::ModeUpdate(mode_info) => self.handle_mode_update(mode_info),
+ Event::TabUpdate(tabs) => self.handle_tab_update(tabs),
+ Event::PaneUpdate(pane_manifest) => self.handle_pane_update(pane_manifest),
+ Event::Mouse(mouse_event) => {
+ self.handle_mouse_event(mouse_event);
+ false
},
Event::CopyToClipboard(copy_destination) => {
- match self.text_copy_destination {
- Some(text_copy_destination) => {
- if text_copy_destination != copy_destination {
- should_render = true;
- }
- },
- None => {
- should_render = true;
- },
- }
- self.text_copy_destination = Some(copy_destination);
- },
- Event::SystemClipboardFailure => {
- should_render = true;
- self.display_system_clipboard_failure = true;
- },
- Event::InputReceived => {
- if self.text_copy_destination.is_some()
- || self.display_system_clipboard_failure == true
- {
- should_render = true;
- }
- self.text_copy_destination = None;
- self.display_system_clipboard_failure = false;
- },
- _ => {
- eprintln!("Got unrecognized event: {:?}", event);
+ self.handle_clipboard_copy(copy_destination)
},
+ Event::SystemClipboardFailure => self.handle_clipboard_failure(),
+ Event::InputReceived => self.handle_input_received(),
+ _ => false,
+ }
+ }
+
+ fn pipe(&mut self, message: PipeMessage) -> bool {
+ if self.is_tooltip && message.is_private {
+ self.handle_tooltip_pipe(message);
+ } else if message.name == MSG_TOGGLE_TOOLTIP
+ && message.is_private
+ && self.toggle_tooltip_key.is_some()
+ {
+ self.toggle_persisted_tooltip(self.mode_info.mode);
+ }
+ false
+ }
+
+ fn render(&mut self, rows: usize, cols: usize) {
+ if self.is_tooltip {
+ self.render_tooltip(rows, cols);
+ } else {
+ self.render_tab_line(cols);
+ }
+ }
+}
+
+impl State {
+ fn initialize_configuration(&mut self, configuration: BTreeMap<String, String>) {
+ self.config = configuration.clone();
+ self.is_tooltip = self.parse_bool_config(CONFIG_IS_TOOLTIP, false);
+
+ if !self.is_tooltip {
+ if let Some(tooltip_toggle_key) = configuration.get(CONFIG_TOGGLE_TOOLTIP_KEY) {
+ self.toggle_tooltip_key = Some(tooltip_toggle_key.clone());
+ }
+ }
+
+ if self.is_tooltip {
+ self.is_first_run = true;
+ }
+ }
+
+ fn setup_subscriptions(&self) {
+ set_selectable(false);
+
+ let events = if self.is_tooltip {
+ vec![EventType::ModeUpdate, EventType::TabUpdate]
+ } else {
+ vec![
+ EventType::TabUpdate,
+ EventType::PaneUpdate,
+ EventType::ModeUpdate,
+ EventType::Mouse,
+ EventType::CopyToClipboard,
+ EventType::InputReceived,
+ EventType::SystemClipboardFailure,
+ ]
};
+
+ subscribe(&events);
+ }
+
+ fn configure_keybinds(&self) {
+ if !self.is_tooltip && self.toggle_tooltip_key.is_some() {
+ if let Some(toggle_key) = &self.toggle_tooltip_key {
+ reconfigure(bind_toggle_key_config(toggle_key), false);
+ }
+ }
+ }
+
+ fn parse_bool_config(&self, key: &str, default: bool) -> bool {
+ self.config
+ .get(key)
+ .and_then(|v| v.parse().ok())
+ .unwrap_or(default)
+ }
+
+ // Event handlers
+ fn handle_mode_update(&mut self, mode_info: ModeInfo) -> bool {
+ let should_render = self.mode_info != mode_info;
+ let old_mode = self.mode_info.mode;
+ let new_mode = mode_info.mode;
+ let base_mode = mode_info.base_mode.unwrap_or(InputMode::Normal);
+
+ self.mode_info = mode_info;
+
+ if self.is_tooltip {
+ self.handle_tooltip_mode_update(old_mode, new_mode, base_mode);
+ } else {
+ self.handle_main_mode_update(new_mode, base_mode);
+ }
+
should_render
}
- fn render(&mut self, _rows: usize, cols: usize) {
- if let Some(copy_destination) = self.text_copy_destination {
- let hint = text_copied_hint(copy_destination).part;
+ fn handle_main_mode_update(&self, new_mode: InputMode, base_mode: InputMode) {
+ if self.toggle_tooltip_key.is_some()
+ && new_mode != base_mode
+ && !self.is_restricted_mode(new_mode)
+ {
+ self.launch_tooltip_if_not_launched(new_mode);
+ }
+ }
- let background = self.mode_info.style.colors.text_unselected.background;
- match background {
- PaletteColor::Rgb((r, g, b)) => {
- print!("{}\u{1b}[48;2;{};{};{}m\u{1b}[0K", hint, r, g, b);
- },
- PaletteColor::EightBit(color) => {
- print!("{}\u{1b}[48;5;{}m\u{1b}[0K", hint, color);
- },
- }
- } else if self.display_system_clipboard_failure {
- let hint = system_clipboard_error().part;
- let background = self.mode_info.style.colors.text_unselected.background;
- match background {
- PaletteColor::Rgb((r, g, b)) => {
- print!("{}\u{1b}[48;2;{};{};{}m\u{1b}[0K", hint, r, g, b);
- },
- PaletteColor::EightBit(color) => {
- print!("{}\u{1b}[48;5;{}m\u{1b}[0K", hint, color);
- },
+ fn handle_tooltip_mode_update(
+ &mut self,
+ old_mode: InputMode,
+ new_mode: InputMode,
+ base_mode: InputMode,
+ ) {
+ if !self.persist && (new_mode == base_mode || self.is_restricted_mode(new_mode)) {
+ close_self();
+ } else if new_mode != old_mode || self.persist {
+ self.update_tooltip_for_mode_change(new_mode);
+ }
+ }
+
+ fn handle_tab_update(&mut self, tabs: Vec<TabInfo>) -> bool {
+ self.update_display_area(&tabs);
+
+ if let Some(active_tab_index) = tabs.iter().position(|t| t.active) {
+ let active_tab_idx = active_tab_index + 1; // Convert to 1-based indexing
+ let should_render = self.active_tab_idx != active_tab_idx || self.tabs != tabs;
+
+ if self.is_tooltip && self.active_tab_idx != active_tab_idx {
+ self.move_tooltip_to_new_tab(active_tab_idx);
}
+
+ self.active_tab_idx = active_tab_idx;
+ self.tabs = tabs;
+ should_render
+ } else {
+ eprintln!("Could not find active tab.");
+ false
+ }
+ }
+
+ fn handle_pane_update(&mut self, pane_manifest: PaneManifest) -> bool {
+ if self.toggle_tooltip_key.is_some() {
+ let previous_tooltip_state = self.tooltip_is_active;
+ self.tooltip_is_active = self.detect_tooltip_presence(&pane_manifest);
+ previous_tooltip_state != self.tooltip_is_active
} else {
- if self.tabs.is_empty() {
- return;
+ false
+ }
+ }
+
+ fn handle_mouse_event(&mut self, mouse_event: Mouse) {
+ if self.is_tooltip {
+ return;
+ }
+
+ match mouse_event {
+ Mouse::LeftClick(_, col) => self.handle_tab_click(col),
+ Mouse::ScrollUp(_) => self.scroll_tab_up(),
+ Mouse::ScrollDown(_) => self.scroll_tab_down(),
+ _ => {},
+ }
+ }
+
+ fn handle_clipboard_copy(&mut self, copy_destination: CopyDestination) -> bool {
+ if self.is_tooltip {
+ return false;
+ }
+
+ let should_render = match self.text_copy_destination {
+ Some(current) => current != copy_destination,
+ None => true,
+ };
+
+ self.text_copy_destination = Some(copy_destination);
+ should_render
+ }
+
+ fn handle_clipboard_failure(&mut self) -> bool {
+ if self.is_tooltip {
+ return false;
+ }
+
+ self.display_system_clipboard_failure = true;
+ true
+ }
+
+ fn handle_input_received(&mut self) -> bool {
+ if self.is_tooltip {
+ return false;
+ }
+
+ let should_render =
+ self.text_copy_destination.is_some() || self.display_system_clipboard_failure;
+ self.clear_clipboard_state();
+ should_render
+ }
+
+ fn handle_tooltip_pipe(&mut self, message: PipeMessage) {
+ if message.name == MSG_TOGGLE_PERSISTED_TOOLTIP {
+ if self.is_first_run {
+ self.persist = true;
+ } else {
+ #[cfg(target_family = "wasm")]
+ close_self();
+ }
+ }
+ }
+
+ // Helper methods
+ fn update_display_area(&mut self, tabs: &[TabInfo]) {
+ for tab in tabs {
+ if tab.active {
+ self.display_area_rows = tab.display_area_rows;
+ self.display_area_cols = tab.display_area_columns;
+ break;
}
- let mut all_tabs: Vec<LinePart> = vec![];
- let mut active_tab_index = 0;
- let mut active_swap_layout_name = None;
- let mut is_swap_layout_dirty = false;
- let mut is_alternate_tab = false;
- for t in &mut self.tabs {
- let mut tabname = t.name.clone();
- if t.active && self.mode_info.mode == InputMode::RenameTab {
- if tabname.is_empty() {
- tabname = String::from("Enter name...");
- }
- active_tab_index = t.position;
- } else if t.active {
- active_tab_index = t.position;
- is_swap_layout_dirty = t.is_swap_layout_dirty;
- active_swap_layout_name = t.active_swap_layout_name.clone();
+ }
+ }
+
+ fn detect_tooltip_presence(&self, pane_manifest: &PaneManifest) -> bool {
+ for (_tab_index, panes) in &pane_manifest.panes {
+ for pane in panes {
+ if pane.plugin_url == Some("zellij:compact-bar".to_owned())
+ && pane.pane_x != pane.pane_content_x
+ {
+ return true;
+ }
+ }
+ }
+ false
+ }
+
+ fn handle_tab_click(&self, col: usize) {
+ if let Some(tab_idx) = get_tab_to_focus(&self.tab_line, self.active_tab_idx, col) {
+ switch_tab_to(tab_idx.try_into().unwrap());
+ }
+ }
+
+ fn scroll_tab_up(&self) {
+ let next_tab = min(self.active_tab_idx + 1, self.tabs.len());
+ switch_tab_to(next_tab as u32);
+ }
+
+ fn scroll_tab_down(&self) {
+ let prev_tab = max(self.active_tab_idx.saturating_sub(1), 1);
+ switch_tab_to(prev_tab as u32);
+ }
+
+ fn clear_clipboard_state(&mut self) {
+ self.text_copy_destination = None;
+ self.display_system_clipboard_failure = false;
+ }
+
+ fn is_restricted_mode(&self, mode: InputMode) -> bool {
+ matches!(
+ mode,
+ InputMode::Locked
+ | InputMode::EnterSearch
+ | InputMode::RenameTab
+ | InputMode::RenamePane
+ | InputMode::Prompt
+ | InputMode::Tmux
+ )
+ }
+
+ // Tooltip operations
+ fn toggle_persisted_tooltip(&self, new_mode: InputMode) {
+ let message = self
+ .create_tooltip_message(MSG_TOGGLE_PERSISTED_TOOLTIP, new_mode)
+ .with_args(self.create_persist_args());
+
+ #[cfg(target_family = "wasm")]
+ pipe_message_to_plugin(message);
+ }
+
+ fn launch_tooltip_if_not_launched(&self, new_mode: InputMode) {
+ let message = self.create_tooltip_message(MSG_LAUNCH_TOOLTIP, new_mode);
+ pipe_message_to_plugin(message);
+ }
+
+ fn create_tooltip_message(&self, name: &str, mode: InputMode) -> MessageToPlugin {
+ let mut tooltip_config = self.config.clone();
+ tooltip_config.insert(CONFIG_IS_TOOLTIP.to_string(), "true".to_string());
+
+ MessageToPlugin::new(name)
+ .with_plugin_url("zellij:OWN_URL")
+ .with_plugin_config(tooltip_config)
+ .with_floating_pane_coordinates(self.calculate_tooltip_coordinates())
+ .new_plugin_instance_should_have_pane_title(format!("{:?}", mode))
+ }
+
+ fn create_persist_args(&self) -> BTreeMap<String, String> {
+ let mut args = BTreeMap::new();
+ args.insert("persist".to_string(), String::new());
+ args
+ }
+
+ fn update_tooltip_for_mode_change(&self, new_mode: InputMode) {
+ if let Some(plugin_id) = self.own_plugin_id {
+ let coordinates = self.calculate_tooltip_coordinates();
+ change_floating_panes_coordinates(vec![(PaneId::Plugin(plugin_id), coordinates)]);
+ rename_plugin_pane(plugin_id, format!("{:?}", new_mode));
+ }
+ }
+
+ fn move_tooltip_to_new_tab(&self, new_tab_index: usize) {
+ if let Some(plugin_id) = self.own_plugin_id {
+ break_panes_to_tab_with_index(
+ &[PaneId::Plugin(plugin_id)],
+ new_tab_index.saturating_sub(1), // Convert to 0-based indexing
+ false,
+ );
+ }
+ }
+
+ fn calculate_tooltip_coordinates(&self) -> FloatingPaneCoordinates {
+ let tooltip_renderer = TooltipRenderer::new(&self.mode_info);
+ let (tooltip_rows, tooltip_cols) =
+ tooltip_renderer.calculate_dimensions(self.mode_info.mode);
+
+ let width = tooltip_cols + 4; // 2 for borders, 2 for padding
+ let height = tooltip_rows + 2; // 2 for borders
+ let x_position = 2;
+ let y_position = self.display_area_rows.saturating_sub(height + 2);
+
+ FloatingPaneCoordinates::new(
+ Some(x_position.to_string()),
+ Some(y_position.to_string()),
+ Some(width.to_string()),
+ Some(height.to_string()),
+ Some(true),
+ )
+ .unwrap_or_default()
+ }
+
+ // Rendering
+ fn render_tooltip(&self, rows: usize, cols: usize) {
+ let tooltip_renderer = TooltipRenderer::new(&self.mode_info);
+ tooltip_renderer.render(rows, cols);
+ }
+
+ fn render_tab_line(&mut self, cols: usize) {
+ if let Some(copy_destination) = self.text_copy_destination {
+ self.render_clipboard_hint(copy_destination);
+ } else if self.display_system_clipboard_failure {
+ self.render_clipboard_error();
+ } else {
+ self.render_tabs(cols);
+ }
+ }
+
+ fn render_clipboard_hint(&self, copy_destination: CopyDestination) {
+ let hint = text_copied_hint(copy_destination).part;
+ self.render_background_with_text(&hint);
+ }
+
+ fn render_clipboard_error(&self) {
+ let hint = system_clipboard_error().part;
+ self.render_background_with_text(&hint);
+ }
+
+ fn render_background_with_text(&self, text: &str) {
+ let background = self.mode_info.style.colors.text_unselected.background;
+ match background {
+ PaletteColor::Rgb((r, g, b)) => {
+ print!("{}\u{1b}[48;2;{};{};{}m\u{1b}[0K", text, r, g, b);
+ },
+ PaletteColor::EightBit(color) => {
+ print!("{}\u{1b}[48;5;{}m\u{1b}[0K", text, color);
+ },
+ }
+ }
+
+ fn render_tabs(&mut self, cols: usize) {
+ if self.tabs.is_empty() {
+ return;
+ }
+
+ let tab_data = self.prepare_tab_data();
+ self.tab_line = tab_line(
+ &self.mode_info,
+ tab_data,
+ cols,
+ self.toggle_tooltip_key.clone(),
+ self.tooltip_is_active,
+ );
+
+ let output = self
+ .tab_line
+ .iter()
+ .fold(String::new(), |acc, part| acc + &part.part);
+
+ self.render_background_with_text(&output);
+ }
+
+ fn prepare_tab_data(&self) -> TabRenderData {
+ let mut all_tabs = Vec::new();
+ let mut active_tab_index = 0;
+ let mut active_swap_layout_name = None;
+ let mut is_swap_layout_dirty = false;
+ let mut is_alternate_tab = false;
+
+ for tab in &self.tabs {
+ let tab_name = self.get_tab_display_name(tab);
+
+ if tab.active {
+ active_tab_index = tab.position;
+ if self.mode_info.mode != InputMode::RenameTab {
+ is_swap_layout_dirty = tab.is_swap_layout_dirty;
+ active_swap_layout_name = tab.active_swap_layout_name.clone();
}
- let tab = tab_style(
- tabname,
- t,
- is_alternate_tab,
- self.mode_info.style.colors,
- self.mode_info.capabilities,
- );
- is_alternate_tab = !is_alternate_tab;
- all_tabs.push(tab);
}
- self.tab_line = tab_line(
- self.mode_info.session_name.as_deref(),
- all_tabs,
- active_tab_index,
- cols.saturating_sub(1),
+
+ let styled_tab = tab_style(
+ tab_name,
+ tab,
+ is_alternate_tab,
self.mode_info.style.colors,
self.mode_info.capabilities,
- self.mode_info.style.hide_session_name,
- self.mode_info.mode,
- &active_swap_layout_name,
- is_swap_layout_dirty,
);
- let output = self
- .tab_line
- .iter()
- .fold(String::new(), |output, part| output + &part.part);
- let background = self.mode_info.style.colors.text_unselected.background;
- match background {
- PaletteColor::Rgb((r, g, b)) => {
- print!("{}\u{1b}[48;2;{};{};{}m\u{1b}[0K", output, r, g, b);
- },
- PaletteColor::EightBit(color) => {
- print!("{}\u{1b}[48;5;{}m\u{1b}[0K", output, color);
- },
- }
+
+ is_alternate_tab = !is_alternate_tab;
+ all_tabs.push(styled_tab);
+ }
+
+ TabRenderData {
+ tabs: all_tabs,
+ active_tab_index,
+ active_swap_layout_name,
+ is_swap_layout_dirty,
}
}
-}
-pub fn text_copied_hint(copy_destination: CopyDestination) -> LinePart {
- let hint = match copy_destination {
- CopyDestination::Command => "Text piped to external command",
- #[cfg(not(target_os = "macos"))]
- CopyDestination::Primary => "Text copied to system primary selection",
- #[cfg(target_os = "macos")] // primary selection does not exist on macos
- CopyDestination::Primary => "Text copied to system clipboard",
- CopyDestination::System => "Text copied to system clipboard",
- };
- LinePart {
- part: serialize_text(&Text::new(&hint).color_range(2, ..).opaque()),
- len: hint.len(),
- tab_index: None,
+ fn get_tab_display_name(&self, tab: &TabInfo) -> String {
+ let mut tab_name = tab.name.clone();
+ if tab.active && self.mode_info.mode == InputMode::RenameTab && tab_name.is_empty() {
+ tab_name = "Enter name...".to_string();
+ }
+ tab_name
}
}
-pub fn system_clipboard_error() -> LinePart {
- let hint = " Error using the system clipboard.";
- LinePart {
- part: serialize_text(&Text::new(&hint).color_range(2, ..).opaque()),
- len: hint.len(),
- tab_index: None,
- }
+fn bind_toggle_key_config(toggle_key: &str) -> String {
+ format!(
+ r#"
+ keybinds {{
+ shared {{
+ bind "{}" {{
+ MessagePlugin "compact-bar" {{
+ name "toggle_tooltip"
+ toggle_tooltip_key "{}"
+ }}
+ }}
+ }}
+ }}
+ "#,
+ toggle_key, toggle_key
+ )
}
diff --git a/default-plugins/compact-bar/src/tooltip.rs b/default-plugins/compact-bar/src/tooltip.rs
new file mode 100644
index 00000000..b068b3ed
--- /dev/null
+++ b/default-plugins/compact-bar/src/tooltip.rs
@@ -0,0 +1,136 @@
+use crate::keybind_utils::KeybindProcessor;
+use zellij_tile::prelude::*;
+
+pub struct TooltipRenderer<'a> {
+ mode_info: &'a ModeInfo,
+}
+
+impl<'a> TooltipRenderer<'a> {
+ pub fn new(mode_info: &'a ModeInfo) -> Self {
+ Self { mode_info }
+ }
+
+ pub fn render(&self, rows: usize, cols: usize) {
+ let current_mode = self.mode_info.mode;
+
+ if current_mode == InputMode::Normal {
+ let (text_components, tooltip_rows, tooltip_columns) =
+ self.normal_mode_tooltip(current_mode);
+ let base_x = cols.saturating_sub(tooltip_columns) / 2;
+ let base_y = rows.saturating_sub(tooltip_rows) / 2;
+
+ for (text, ribbon, x, y) in text_components {
+ let text_width = text.content().chars().count();
+ let ribbon_content_width = ribbon.content().chars().count();
+ let ribbon_total_width = ribbon_content_width + 4;
+ let total_element_width = text_width + ribbon_total_width + 1;
+
+ // Check if this element would exceed the available columns and render an ellipses
+ // if it does
+ if base_x + x + total_element_width > cols {
+ let remaining_space = cols.saturating_sub(base_x + x);
+ let ellipsis = Text::new("...");
+ print_text_with_coordinates(
+ ellipsis,
+ base_x + x,
+ base_y + y,
+ Some(remaining_space),
+ None,
+ );
+ break;
+ }
+
+ print_text_with_coordinates(text, base_x + x, base_y + y, None, None);
+ print_ribbon_with_coordinates(
+ ribbon,
+ base_x + x + text_width + 1,
+ base_y + y,
+ None,
+ None,
+ );
+ }
+ } else {
+ let (table, tooltip_rows, tooltip_columns) = self.other_mode_tooltip(current_mode);
+ let base_x = cols.saturating_sub(tooltip_columns) / 2;
+ let base_y = rows.saturating_sub(tooltip_rows) / 2;
+ print_table_with_coordinates(table, base_x, base_y, None, None);
+ }
+ }
+
+ pub fn calculate_dimensions(&self, current_mode: InputMode) -> (usize, usize) {
+ match current_mode {
+ InputMode::Normal => {
+ let (_, tooltip_rows, tooltip_cols) = self.normal_mode_tooltip(current_mode);
+ (tooltip_rows, tooltip_cols)
+ },
+ _ => {
+ let (_, tooltip_rows, tooltip_cols) = self.other_mode_tooltip(current_mode);
+ (tooltip_rows + 1, tooltip_cols) // + 1 for the invisible table title
+ },
+ }
+ }
+
+ fn normal_mode_tooltip(
+ &self,
+ current_mode: InputMode,
+ ) -> (Vec<(Text, Text, usize, usize)>, usize, usize) {
+ let actions = KeybindProcessor::get_predetermined_actions(self.mode_info, current_mode);
+ let y = 0;
+ let mut running_x = 0;
+ let mut components = Vec::new();
+ let mut max_columns = 0;
+
+ for (key, description) in actions {
+ let text = Text::new(&key).color_all(3);
+ let ribbon = Text::new(&description);
+
+ let line_length = key.chars().count() + 1 + description.chars().count();
+
+ components.push((text, ribbon, running_x, y));
+ running_x += line_length + 5;
+ max_columns = max_columns.max(running_x);
+ }
+
+ let total_rows = 1;
+ (components, total_rows, max_columns)
+ }
+
+ fn other_mode_tooltip(&self, current_mode: InputMode) -> (Table, usize, usize) {
+ let actions = KeybindProcessor::get_predetermined_actions(self.mode_info, current_mode);
+ let actions_vec: Vec<_> = actions.into_iter().collect();
+
+ let mut table = Table::new().add_row(vec![" ".to_owned(); 2]);
+ let mut row_count = 1; // Start with header row
+
+ if actions_vec.is_empty() {
+ let tooltip_text = match self.mode_info.mode {
+ InputMode::EnterSearch => "Entering search term...".to_owned(),
+ InputMode::RenameTab => "Renaming tab...".to_owned(),
+ InputMode::RenamePane => "Renaming pane...".to_owned(),
+ _ => {
+ format!("{:?}", self.mode_info.mode)
+ },
+ };
+ let total_width = tooltip_text.chars().count();
+ table = table.add_styled_row(vec![Text::new(tooltip_text).color_all(0)]);
+ row_count += 1;
+ (table, row_count, total_width)
+ } else {
+ let mut key_width = 0;
+ let mut action_width = 0;
+ for (key, description) in actions_vec.into_iter() {
+ let description_formatted = format!("- {}", description);
+ key_width = key_width.max(key.chars().count());
+ action_width = action_width.max(description_formatted.chars().count());
+ table = table.add_styled_row(vec![
+ Text::new(&key).color_all(3),
+ Text::new(description_formatted),
+ ]);
+ row_count += 1;
+ }
+
+ let total_width = key_width + action_width + 1; // +1 for separator
+ (table, row_count, total_width)
+ }
+ }
+}