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
This commit is contained in:
parent
a9f8bbcd19
commit
7ef7cd5ecd
28 changed files with 1925 additions and 502 deletions
|
|
@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
|||
|
||||
## [Unreleased]
|
||||
* feat: multiple select and bulk pane actions (https://github.com/zellij-org/zellij/pull/4169 and https://github.com/zellij-org/zellij/pull/4171 and https://github.com/zellij-org/zellij/pull/4221)
|
||||
* feat: add an optional key tooltip to show the current keybindings for the compact bar (https://github.com/zellij-org/zellij/pull/4225)
|
||||
|
||||
## [0.42.2] - 2025-04-15
|
||||
* refactor(terminal): track scroll_region as tuple rather than Option (https://github.com/zellij-org/zellij/pull/4082)
|
||||
|
|
|
|||
129
default-plugins/compact-bar/src/action_types.rs
Normal file
129
default-plugins/compact-bar/src/action_types.rs
Normal file
|
|
@ -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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
27
default-plugins/compact-bar/src/clipboard_utils.rs
Normal file
27
default-plugins/compact-bar/src/clipboard_utils.rs
Normal file
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
499
default-plugins/compact-bar/src/keybind_utils.rs
Normal file
499
default-plugins/compact-bar/src/keybind_utils.rs
Normal file
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,235 +1,611 @@
|
|||
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)
|
||||
}
|
||||
|
||||
// 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>,
|
||||
#[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()
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
let total_size = collapsed_left.len + middle_size + collapsed_right.len;
|
||||
|
||||
if total_size > cols {
|
||||
// break and dont add collapsed tabs to tabs_to_render, they will not fit
|
||||
break;
|
||||
capabilities,
|
||||
}
|
||||
}
|
||||
|
||||
let left = if let Some(tab) = tabs_before_active.last() {
|
||||
tab.len
|
||||
} else {
|
||||
usize::MAX
|
||||
};
|
||||
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;
|
||||
|
||||
let right = if let Some(tab) = tabs_after_active.first() {
|
||||
tab.len
|
||||
} else {
|
||||
usize::MAX
|
||||
};
|
||||
loop {
|
||||
let left_count = tabs_before_active.len();
|
||||
let right_count = tabs_after_active.len();
|
||||
|
||||
// total size is shortened if the next tab to be added is the last one, as that will remove the collapsed tab
|
||||
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 fit_analysis = self.analyze_tab_fit(
|
||||
&tab_sizes,
|
||||
total_size,
|
||||
left_count,
|
||||
right_count,
|
||||
&collapsed_indicators,
|
||||
);
|
||||
|
||||
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;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
CollapsedIndicators {
|
||||
left: self.create_left_indicator(left_count, left_more_tab_index),
|
||||
right: self.create_right_indicator(right_count, right_more_tab_index),
|
||||
}
|
||||
}
|
||||
|
||||
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 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)
|
||||
}
|
||||
|
||||
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 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),
|
||||
];
|
||||
|
||||
LinePart {
|
||||
part: ANSIStrings(&styled_parts).to_string(),
|
||||
len: text_len,
|
||||
tab_index: Some(tab_index),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
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),
|
||||
}
|
||||
#[derive(Debug)]
|
||||
struct CollapsedIndicators {
|
||||
left: LinePart,
|
||||
right: LinePart,
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
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),
|
||||
}
|
||||
#[derive(Debug)]
|
||||
struct TabSizes {
|
||||
left: usize,
|
||||
right: usize,
|
||||
}
|
||||
|
||||
fn tab_line_prefix(
|
||||
session_name: Option<&str>,
|
||||
mode: InputMode,
|
||||
#[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();
|
||||
}
|
||||
|
||||
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;
|
||||
impl TabLinePrefixBuilder {
|
||||
fn new(palette: Styling, cols: usize) -> Self {
|
||||
Self { palette, cols }
|
||||
}
|
||||
|
||||
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 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();
|
||||
|
||||
LinePart {
|
||||
part: style!(colors.text, colors.background)
|
||||
.bold()
|
||||
.paint(prefix_text)
|
||||
.to_string(),
|
||||
len: prefix_text.chars().count(),
|
||||
tab_index: None,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RightSideElementsBuilder {
|
||||
palette: Styling,
|
||||
capabilities: PluginCapabilities,
|
||||
}
|
||||
|
||||
impl RightSideElementsBuilder {
|
||||
fn new(palette: Styling, capabilities: PluginCapabilities) -> Self {
|
||||
Self {
|
||||
palette,
|
||||
capabilities,
|
||||
}
|
||||
}
|
||||
|
||||
fn build(&self, config: &TabLineConfig, available_space: usize) -> Vec<LinePart> {
|
||||
let mut elements = Vec::new();
|
||||
|
||||
if let Some(ref tooltip_key) = config.toggle_tooltip_key {
|
||||
elements.push(self.create_tooltip_indicator(tooltip_key, config.tooltip_is_active));
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
} else if short_len <= max_len && config.mode != InputMode::Locked {
|
||||
Some(LinePart {
|
||||
part: indicator,
|
||||
len: short_len,
|
||||
tab_index: None,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SwapLayoutColors {
|
||||
bg: PaletteColor,
|
||||
fg: PaletteColor,
|
||||
green: PaletteColor,
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
parts
|
||||
}
|
||||
|
||||
pub fn tab_separator(capabilities: PluginCapabilities) -> &'static str {
|
||||
|
|
@ -239,137 +615,3 @@ pub fn tab_separator(capabilities: PluginCapabilities) -> &'static str {
|
|||
""
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tab_line(
|
||||
session_name: Option<&str>,
|
||||
mut all_tabs: Vec<LinePart>,
|
||||
active_tab_index: usize,
|
||||
cols: usize,
|
||||
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;
|
||||
}
|
||||
|
||||
let mut tabs_to_render = vec![active_tab];
|
||||
|
||||
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);
|
||||
|
||||
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,
|
||||
tab_index: None,
|
||||
});
|
||||
prefix.push(swap_layout_status);
|
||||
}
|
||||
}
|
||||
|
||||
prefix
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
self.handle_clipboard_copy(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);
|
||||
},
|
||||
};
|
||||
should_render
|
||||
Event::SystemClipboardFailure => self.handle_clipboard_failure(),
|
||||
Event::InputReceived => self.handle_input_received(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
|
||||
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 render(&mut self, rows: usize, cols: usize) {
|
||||
if self.is_tooltip {
|
||||
self.render_tooltip(rows, cols);
|
||||
} else {
|
||||
if self.tabs.is_empty() {
|
||||
return;
|
||||
}
|
||||
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();
|
||||
}
|
||||
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),
|
||||
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);
|
||||
},
|
||||
}
|
||||
self.render_tab_line(cols);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 styled_tab = tab_style(
|
||||
tab_name,
|
||||
tab,
|
||||
is_alternate_tab,
|
||||
self.mode_info.style.colors,
|
||||
self.mode_info.capabilities,
|
||||
);
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
136
default-plugins/compact-bar/src/tooltip.rs
Normal file
136
default-plugins/compact-bar/src/tooltip.rs
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -675,6 +675,7 @@ pub(crate) fn plugin_thread_main(
|
|||
} => {
|
||||
let should_float = floating.unwrap_or(true);
|
||||
let mut pipe_messages = vec![];
|
||||
let floating_pane_coordinates = None; // TODO: do we want to allow this?
|
||||
match plugin {
|
||||
Some(plugin_url) => {
|
||||
// send to specific plugin(s)
|
||||
|
|
@ -695,6 +696,7 @@ pub(crate) fn plugin_thread_main(
|
|||
&bus,
|
||||
&mut wasm_bridge,
|
||||
&plugin_aliases,
|
||||
floating_pane_coordinates,
|
||||
);
|
||||
},
|
||||
None => {
|
||||
|
|
@ -727,6 +729,7 @@ pub(crate) fn plugin_thread_main(
|
|||
} => {
|
||||
let should_float = floating.unwrap_or(true);
|
||||
let mut pipe_messages = vec![];
|
||||
let floating_pane_coordinates = None; // TODO: do we want to allow this?
|
||||
if let Some((plugin_id, client_id)) = plugin_and_client_id {
|
||||
let is_private = true;
|
||||
pipe_messages.push((
|
||||
|
|
@ -755,6 +758,7 @@ pub(crate) fn plugin_thread_main(
|
|||
&bus,
|
||||
&mut wasm_bridge,
|
||||
&plugin_aliases,
|
||||
floating_pane_coordinates,
|
||||
);
|
||||
},
|
||||
None => {
|
||||
|
|
@ -798,6 +802,7 @@ pub(crate) fn plugin_thread_main(
|
|||
.new_plugin_args
|
||||
.as_ref()
|
||||
.and_then(|n| n.pane_id_to_replace);
|
||||
let floating_pane_coordinates = message.floating_pane_coordinates;
|
||||
match (message.plugin_url, message.destination_plugin_id) {
|
||||
(Some(plugin_url), None) => {
|
||||
// send to specific plugin(s)
|
||||
|
|
@ -818,6 +823,7 @@ pub(crate) fn plugin_thread_main(
|
|||
&bus,
|
||||
&mut wasm_bridge,
|
||||
&plugin_aliases,
|
||||
floating_pane_coordinates,
|
||||
);
|
||||
},
|
||||
(None, Some(destination_plugin_id)) => {
|
||||
|
|
@ -997,6 +1003,7 @@ fn pipe_to_specific_plugins(
|
|||
bus: &Bus<PluginInstruction>,
|
||||
wasm_bridge: &mut WasmBridge,
|
||||
plugin_aliases: &PluginAliases,
|
||||
floating_pane_coordinates: Option<FloatingPaneCoordinates>,
|
||||
) {
|
||||
let is_private = true;
|
||||
let size = Size::default();
|
||||
|
|
@ -1018,6 +1025,7 @@ fn pipe_to_specific_plugins(
|
|||
pane_title.clone(),
|
||||
pane_id_to_replace.clone(),
|
||||
cli_client_id,
|
||||
floating_pane_coordinates,
|
||||
);
|
||||
for (plugin_id, client_id) in all_plugin_ids {
|
||||
pipe_messages.push((
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@ use std::{
|
|||
};
|
||||
use wasmtime::{Engine, Module};
|
||||
use zellij_utils::consts::{ZELLIJ_CACHE_DIR, ZELLIJ_TMP_DIR};
|
||||
use zellij_utils::data::{InputMode, PermissionStatus, PermissionType, PipeMessage, PipeSource};
|
||||
use zellij_utils::data::{
|
||||
FloatingPaneCoordinates, InputMode, PermissionStatus, PermissionType, PipeMessage, PipeSource,
|
||||
};
|
||||
use zellij_utils::downloader::Downloader;
|
||||
use zellij_utils::input::keybinds::Keybinds;
|
||||
use zellij_utils::input::permission::PermissionCache;
|
||||
|
|
@ -1409,6 +1411,7 @@ impl WasmBridge {
|
|||
pane_title: Option<String>,
|
||||
pane_id_to_replace: Option<PaneId>,
|
||||
cli_client_id: Option<ClientId>,
|
||||
floating_pane_coordinates: Option<FloatingPaneCoordinates>,
|
||||
) -> Vec<(PluginId, Option<ClientId>)> {
|
||||
let run_plugin = run_plugin_or_alias.get_run_plugin();
|
||||
match run_plugin {
|
||||
|
|
@ -1435,6 +1438,8 @@ impl WasmBridge {
|
|||
) {
|
||||
Ok((plugin_id, client_id)) => {
|
||||
let start_suppressed = false;
|
||||
let should_focus = Some(false); // we should not focus plugins that
|
||||
// were started from another plugin
|
||||
drop(self.senders.send_to_screen(ScreenInstruction::AddPlugin(
|
||||
Some(should_float),
|
||||
should_be_open_in_place,
|
||||
|
|
@ -1445,8 +1450,8 @@ impl WasmBridge {
|
|||
pane_id_to_replace,
|
||||
cwd,
|
||||
start_suppressed,
|
||||
None,
|
||||
None,
|
||||
floating_pane_coordinates,
|
||||
should_focus,
|
||||
Some(client_id),
|
||||
)));
|
||||
vec![(plugin_id, Some(client_id))]
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ use zellij_utils::{
|
|||
envs::set_session_name,
|
||||
input::command::TerminalAction,
|
||||
input::layout::{
|
||||
FloatingPaneLayout, Layout, Run, RunPluginOrAlias, SwapFloatingLayout, SwapTiledLayout,
|
||||
TiledPaneLayout,
|
||||
FloatingPaneLayout, Layout, Run, RunPluginOrAlias, SplitSize, SwapFloatingLayout,
|
||||
SwapTiledLayout, TiledPaneLayout,
|
||||
},
|
||||
position::Position,
|
||||
};
|
||||
|
|
@ -2407,8 +2407,9 @@ impl Screen {
|
|||
}
|
||||
// here we pass None instead of the client_id we have because we do not need to
|
||||
// necessarily trigger a relayout for this tab
|
||||
let pane_was_floating = tab.pane_id_is_floating(&pane_id);
|
||||
if let Some(pane) = tab.extract_pane(pane_id, true).take() {
|
||||
extracted_panes.push(pane);
|
||||
extracted_panes.push((pane_was_floating, pane));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -2422,11 +2423,27 @@ impl Screen {
|
|||
return Ok(());
|
||||
}
|
||||
if let Some(new_active_tab) = self.get_indexed_tab_mut(tab_index) {
|
||||
for pane in extracted_panes {
|
||||
for (pane_was_floating, pane) in extracted_panes {
|
||||
let pane_id = pane.pid();
|
||||
// here we pass None instead of the ClientId, because we do not want this pane to be
|
||||
// necessarily focused
|
||||
new_active_tab.add_tiled_pane(pane, pane_id, None)?;
|
||||
if pane_was_floating {
|
||||
let floating_pane_coordinates = FloatingPaneCoordinates {
|
||||
x: Some(SplitSize::Fixed(pane.x())),
|
||||
y: Some(SplitSize::Fixed(pane.y())),
|
||||
width: Some(SplitSize::Fixed(pane.cols())),
|
||||
height: Some(SplitSize::Fixed(pane.rows())),
|
||||
pinned: Some(pane.current_geom().is_pinned),
|
||||
};
|
||||
new_active_tab.add_floating_pane(
|
||||
pane,
|
||||
pane_id,
|
||||
Some(floating_pane_coordinates),
|
||||
false,
|
||||
)?;
|
||||
} else {
|
||||
// here we pass None instead of the ClientId, because we do not want this pane to be
|
||||
// necessarily focused
|
||||
new_active_tab.add_tiled_pane(pane, pane_id, None)?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::error!("Could not find tab with index: {:?}", tab_index);
|
||||
|
|
|
|||
|
|
@ -4538,6 +4538,7 @@ impl Tab {
|
|||
pane.set_active_at(Instant::now());
|
||||
pane.set_geom(new_pane_geom);
|
||||
pane.set_content_offset(Offset::frame(1)); // floating panes always have a frame
|
||||
pane.render_full_viewport(); // to make sure the frame is re-rendered
|
||||
resize_pty!(pane, self.os_api, self.senders, self.character_cell_size)
|
||||
.with_context(err_context)?;
|
||||
self.floating_panes.add_pane(pane_id, pane);
|
||||
|
|
|
|||
|
|
@ -169,7 +169,8 @@ impl PaneFrame {
|
|||
) -> Option<(Vec<TerminalCharacter>, usize)> {
|
||||
// string and length because of color
|
||||
let has_scroll = self.scroll_position.0 > 0 || self.scroll_position.1 > 0;
|
||||
if has_scroll {
|
||||
if has_scroll && self.is_selectable {
|
||||
// TODO: don't show SCROLL at all for plugins
|
||||
let pin_indication = if self.is_floating && self.is_selectable {
|
||||
self.render_pinned_indication(max_length)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1183,7 +1183,7 @@ pub fn break_panes_to_new_tab(
|
|||
unsafe { host_run_plugin_command() };
|
||||
}
|
||||
|
||||
/// Create a new tab that includes the specified pane ids
|
||||
/// Move the pane ids to the tab with the specified index
|
||||
pub fn break_panes_to_tab_with_index(
|
||||
pane_ids: &[PaneId],
|
||||
tab_index: usize,
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ impl Text {
|
|||
|
||||
while let Some(pos) = self.text[start..].find(substr) {
|
||||
let abs_pos = start + pos;
|
||||
self = self.color_range(index_level, abs_pos..abs_pos + substr.len());
|
||||
self = self.color_range(index_level, abs_pos..abs_pos + substr.chars().count());
|
||||
start = abs_pos + substr.len();
|
||||
}
|
||||
|
||||
|
|
@ -92,6 +92,9 @@ impl Text {
|
|||
|
||||
self
|
||||
}
|
||||
pub fn content(&self) -> &str {
|
||||
&self.text
|
||||
}
|
||||
fn pad_indices(&mut self, index_level: usize) {
|
||||
if self.indices.get(index_level).is_none() {
|
||||
for _ in self.indices.len()..=index_level {
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -590,6 +590,8 @@ pub struct MessageToPluginPayload {
|
|||
pub new_plugin_args: ::core::option::Option<NewPluginArgs>,
|
||||
#[prost(uint32, optional, tag="7")]
|
||||
pub destination_plugin_id: ::core::option::Option<u32>,
|
||||
#[prost(message, optional, tag="8")]
|
||||
pub floating_pane_coordinates: ::core::option::Option<FloatingPaneCoordinates>,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use crate::input::actions::Action;
|
|||
use crate::input::config::ConversionError;
|
||||
use crate::input::keybinds::Keybinds;
|
||||
use crate::input::layout::{RunPlugin, SplitSize};
|
||||
use crate::pane_size::PaneGeom;
|
||||
use crate::shared::colors as default_colors;
|
||||
use clap::ArgEnum;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -1883,6 +1884,7 @@ pub struct MessageToPlugin {
|
|||
/// these will only be used in case we need to launch a new plugin to send this message to,
|
||||
/// since none are running
|
||||
pub new_plugin_args: Option<NewPluginArgs>,
|
||||
pub floating_pane_coordinates: Option<FloatingPaneCoordinates>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
|
|
@ -1946,6 +1948,13 @@ impl MessageToPlugin {
|
|||
self.message_args = args;
|
||||
self
|
||||
}
|
||||
pub fn with_floating_pane_coordinates(
|
||||
mut self,
|
||||
floating_pane_coordinates: FloatingPaneCoordinates,
|
||||
) -> Self {
|
||||
self.floating_pane_coordinates = Some(floating_pane_coordinates);
|
||||
self
|
||||
}
|
||||
pub fn new_plugin_instance_should_float(mut self, should_float: bool) -> Self {
|
||||
let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
|
||||
new_plugin_args.should_float = Some(should_float);
|
||||
|
|
@ -2144,6 +2153,18 @@ impl FloatingPaneCoordinates {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<PaneGeom> for FloatingPaneCoordinates {
|
||||
fn from(pane_geom: PaneGeom) -> Self {
|
||||
FloatingPaneCoordinates {
|
||||
x: Some(SplitSize::Fixed(pane_geom.x)),
|
||||
y: Some(SplitSize::Fixed(pane_geom.y)),
|
||||
width: Some(SplitSize::Fixed(pane_geom.cols.as_usize())),
|
||||
height: Some(SplitSize::Fixed(pane_geom.rows.as_usize())),
|
||||
pinned: Some(pane_geom.is_pinned),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct OriginatingPlugin {
|
||||
pub plugin_id: u32,
|
||||
|
|
|
|||
|
|
@ -491,6 +491,7 @@ message MessageToPluginPayload {
|
|||
repeated ContextItem message_args = 5;
|
||||
optional NewPluginArgs new_plugin_args = 6;
|
||||
optional uint32 destination_plugin_id = 7;
|
||||
optional FloatingPaneCoordinates floating_pane_coordinates = 8;
|
||||
}
|
||||
|
||||
message NewPluginArgs {
|
||||
|
|
|
|||
|
|
@ -935,6 +935,7 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
|
|||
message_args,
|
||||
new_plugin_args,
|
||||
destination_plugin_id,
|
||||
floating_pane_coordinates,
|
||||
})) => {
|
||||
let plugin_config: BTreeMap<String, String> = plugin_config
|
||||
.into_iter()
|
||||
|
|
@ -962,6 +963,8 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
|
|||
})
|
||||
}),
|
||||
destination_plugin_id,
|
||||
floating_pane_coordinates: floating_pane_coordinates
|
||||
.and_then(|f| f.try_into().ok()),
|
||||
}))
|
||||
},
|
||||
_ => Err("Mismatched payload for MessageToPlugin"),
|
||||
|
|
@ -2151,6 +2154,9 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
|
|||
}
|
||||
}),
|
||||
destination_plugin_id: message_to_plugin.destination_plugin_id,
|
||||
floating_pane_coordinates: message_to_plugin
|
||||
.floating_pane_coordinates
|
||||
.and_then(|f| f.try_into().ok()),
|
||||
})),
|
||||
})
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue