aboutsummaryrefslogtreecommitdiffstats
path: root/default-plugins
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2024-07-05 15:13:51 +0200
committerGitHub <noreply@github.com>2024-07-05 15:13:51 +0200
commita6d6c0e4ff03c35cafb6f04363ecab856ee3682d (patch)
treeeadf1c24c4c6a1e4b7499b3cd047351fd29e52de /default-plugins
parentdocs(changelog): wasmer -> wasmtime (diff)
downloadsl0ppij-a6d6c0e4ff03c35cafb6f04363ecab856ee3682d.tar.gz
sl0ppij-a6d6c0e4ff03c35cafb6f04363ecab856ee3682d.tar.xz
sl0ppij-a6d6c0e4ff03c35cafb6f04363ecab856ee3682d.zip
feat(ui): status bar redesign (#3475)
* work * work * working * get default mode from server and some ui responsiveness * work * finish design and get tests to pass * get e2e tests to pass * add classic layout * add classic layout assets * fix e2e tests * style(fmt): rustfmt * fix plugin system test * style(fmt): some cleanups
Diffstat (limited to 'default-plugins')
-rw-r--r--default-plugins/status-bar/Cargo.toml1
-rw-r--r--default-plugins/status-bar/src/first_line.rs47
-rw-r--r--default-plugins/status-bar/src/main.rs30
-rw-r--r--default-plugins/status-bar/src/one_line_ui.rs1440
-rw-r--r--default-plugins/tab-bar/src/line.rs211
-rw-r--r--default-plugins/tab-bar/src/main.rs17
-rw-r--r--default-plugins/tab-bar/src/tab.rs2
7 files changed, 1730 insertions, 18 deletions
diff --git a/default-plugins/status-bar/Cargo.toml b/default-plugins/status-bar/Cargo.toml
index a2ca3175..c1492c19 100644
--- a/default-plugins/status-bar/Cargo.toml
+++ b/default-plugins/status-bar/Cargo.toml
@@ -13,6 +13,7 @@ rand = "0.8.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0.30"
+unicode-width = "0.1.8"
zellij-tile = { path = "../../zellij-tile" }
zellij-tile-utils = { path = "../../zellij-tile-utils" }
diff --git a/default-plugins/status-bar/src/first_line.rs b/default-plugins/status-bar/src/first_line.rs
index 1fffd755..ee9b9478 100644
--- a/default-plugins/status-bar/src/first_line.rs
+++ b/default-plugins/status-bar/src/first_line.rs
@@ -8,14 +8,16 @@ use crate::{
};
use crate::{ColoredElements, LinePart};
-struct KeyShortcut {
- mode: KeyMode,
- action: KeyAction,
- key: Option<KeyWithModifier>,
+#[derive(Debug)]
+pub struct KeyShortcut {
+ pub mode: KeyMode,
+ pub action: KeyAction,
+ pub key: Option<KeyWithModifier>,
}
-#[derive(PartialEq)]
-enum KeyAction {
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub enum KeyAction {
+ Unlock,
Lock,
Pane,
Tab,
@@ -27,7 +29,8 @@ enum KeyAction {
Tmux,
}
-enum KeyMode {
+#[derive(Debug, Copy, Clone)]
+pub enum KeyMode {
Unselected,
UnselectedAlternate,
Selected,
@@ -42,6 +45,7 @@ impl KeyShortcut {
pub fn full_text(&self) -> String {
match self.action {
KeyAction::Lock => String::from("LOCK"),
+ KeyAction::Unlock => String::from("UNLOCK"),
KeyAction::Pane => String::from("PANE"),
KeyAction::Tab => String::from("TAB"),
KeyAction::Resize => String::from("RESIZE"),
@@ -82,6 +86,35 @@ impl KeyShortcut {
};
format!("{}", key)
}
+ pub fn get_key(&self) -> Option<KeyWithModifier> {
+ self.key.clone()
+ }
+ pub fn get_mode(&self) -> KeyMode {
+ self.mode
+ }
+ pub fn get_action(&self) -> KeyAction {
+ self.action
+ }
+ pub fn is_selected(&self) -> bool {
+ match self.mode {
+ KeyMode::Selected => true,
+ _ => false,
+ }
+ }
+ pub fn short_text(&self) -> String {
+ match self.action {
+ KeyAction::Lock => String::from("Lo"),
+ KeyAction::Unlock => String::from("Un"),
+ KeyAction::Pane => String::from("Pa"),
+ KeyAction::Tab => String::from("Ta"),
+ KeyAction::Resize => String::from("Re"),
+ KeyAction::Search => String::from("Se"),
+ KeyAction::Quit => String::from("Qu"),
+ KeyAction::Session => String::from("Se"),
+ KeyAction::Move => String::from("Mo"),
+ KeyAction::Tmux => String::from("Tm"),
+ }
+ }
}
/// Generate long mode shortcut tile.
diff --git a/default-plugins/status-bar/src/main.rs b/default-plugins/status-bar/src/main.rs
index 6fed529f..7db9b09d 100644
--- a/default-plugins/status-bar/src/main.rs
+++ b/default-plugins/status-bar/src/main.rs
@@ -1,4 +1,5 @@
mod first_line;
+mod one_line_ui;
mod second_line;
mod tip;
@@ -15,6 +16,7 @@ use zellij_tile::prelude::*;
use zellij_tile_utils::{palette_match, style};
use first_line::first_line;
+use one_line_ui::one_line_ui;
use second_line::{
floating_panes_are_visible, fullscreen_panes_to_hide, keybinds,
locked_floating_panes_are_visible, locked_fullscreen_panes_to_hide, system_clipboard_error,
@@ -35,6 +37,8 @@ struct State {
mode_info: ModeInfo,
text_copy_destination: Option<CopyDestination>,
display_system_clipboard_failure: bool,
+ classic_ui: bool,
+ base_mode_is_locked: bool,
}
register_plugin!(State);
@@ -177,9 +181,13 @@ fn color_elements(palette: Palette, different_color_alternates: bool) -> Colored
}
impl ZellijPlugin for State {
- fn load(&mut self, _configuration: BTreeMap<String, String>) {
+ fn load(&mut self, configuration: BTreeMap<String, String>) {
// TODO: Should be able to choose whether to use the cache through config.
self.tip_name = get_cached_tip_name();
+ self.classic_ui = configuration
+ .get("classic")
+ .map(|c| c == "true")
+ .unwrap_or(false);
set_selectable(false);
subscribe(&[
EventType::ModeUpdate,
@@ -198,6 +206,7 @@ impl ZellijPlugin for State {
should_render = true;
}
self.mode_info = mode_info;
+ self.base_mode_is_locked = self.mode_info.base_mode == Some(InputMode::Locked);
},
Event::TabUpdate(tabs) => {
if self.tabs != tabs {
@@ -244,6 +253,21 @@ impl ZellijPlugin for State {
""
};
+ if rows == 1 && !self.classic_ui {
+ let active_tab = self.tabs.iter().find(|t| t.active);
+ print!(
+ "{}",
+ one_line_ui(
+ &self.mode_info,
+ active_tab,
+ cols,
+ separator,
+ self.base_mode_is_locked
+ )
+ );
+ return;
+ }
+
let active_tab = self.tabs.iter().find(|t| t.active);
let first_line = first_line(&self.mode_info, active_tab, cols, separator);
let second_line = self.second_line(cols);
@@ -418,10 +442,6 @@ pub fn style_key_with_modifier(
let common_modifiers = get_common_modifiers(keyvec.iter().collect());
- // let modifier_str = match get_common_modifier(keyvec.iter().collect()) {
- // Some(modifier) => modifier,
- // None => "".to_string(),
- // };
let no_common_modifier = common_modifiers.is_empty();
let modifier_str = common_modifiers
.iter()
diff --git a/default-plugins/status-bar/src/one_line_ui.rs b/default-plugins/status-bar/src/one_line_ui.rs
new file mode 100644
index 00000000..87b1e2fd
--- /dev/null
+++ b/default-plugins/status-bar/src/one_line_ui.rs
@@ -0,0 +1,1440 @@
+use ansi_term::{ANSIString, ANSIStrings};
+use ansi_term::{
+ Color::{Fixed, RGB},
+ Style,
+};
+use std::collections::HashMap;
+use zellij_tile::prelude::actions::Action;
+use zellij_tile::prelude::*;
+use zellij_tile_utils::palette_match;
+
+use crate::first_line::{to_char, KeyAction, KeyMode, KeyShortcut};
+use crate::{action_key, action_key_group, color_elements, MORE_MSG, TO_NORMAL};
+use crate::{ColoredElements, LinePart};
+use unicode_width::UnicodeWidthStr;
+
+pub fn one_line_ui(
+ help: &ModeInfo,
+ tab_info: Option<&TabInfo>,
+ mut max_len: usize,
+ separator: &str,
+ base_mode_is_locked: bool,
+) -> LinePart {
+ let mut line_part_to_render = LinePart::default();
+ let mut append = |line_part: &LinePart, max_len: &mut usize| {
+ line_part_to_render.append(line_part);
+ *max_len = max_len.saturating_sub(line_part.len);
+ };
+
+ render_mode_key_indicators(help, max_len, separator, base_mode_is_locked)
+ .map(|mode_key_indicators| append(&mode_key_indicators, &mut max_len))
+ .and_then(|_| match help.mode {
+ InputMode::Normal | InputMode::Locked => render_secondary_info(help, tab_info, max_len)
+ .map(|secondary_info| append(&secondary_info, &mut max_len)),
+ _ => add_keygroup_separator(help, max_len)
+ .map(|key_group_separator| append(&key_group_separator, &mut max_len))
+ .and_then(|_| keybinds(help, max_len))
+ .map(|keybinds| append(&keybinds, &mut max_len)),
+ });
+ line_part_to_render
+}
+
+fn to_base_mode(base_mode: InputMode) -> Action {
+ Action::SwitchToMode(base_mode)
+}
+
+fn base_mode_locked_mode_indicators(help: &ModeInfo) -> HashMap<InputMode, Vec<KeyShortcut>> {
+ let locked_binds = &help.get_keybinds_for_mode(InputMode::Locked);
+ let normal_binds = &help.get_keybinds_for_mode(InputMode::Normal);
+ let pane_binds = &help.get_keybinds_for_mode(InputMode::Pane);
+ let tab_binds = &help.get_keybinds_for_mode(InputMode::Tab);
+ let resize_binds = &help.get_keybinds_for_mode(InputMode::Resize);
+ let move_binds = &help.get_keybinds_for_mode(InputMode::Move);
+ let scroll_binds = &help.get_keybinds_for_mode(InputMode::Scroll);
+ let session_binds = &help.get_keybinds_for_mode(InputMode::Session);
+ HashMap::from([
+ (
+ InputMode::Locked,
+ vec![KeyShortcut::new(
+ KeyMode::Unselected,
+ KeyAction::Unlock,
+ to_char(action_key(
+ locked_binds,
+ &[Action::SwitchToMode(InputMode::Normal)],
+ )),
+ )],
+ ),
+ (
+ InputMode::Normal,
+ vec![
+ KeyShortcut::new(
+ KeyMode::Selected,
+ KeyAction::Unlock,
+ to_char(action_key(
+ normal_binds,
+ &[Action::SwitchToMode(InputMode::Locked)],
+ )),
+ ),
+ KeyShortcut::new(
+ KeyMode::UnselectedAlternate,
+ KeyAction::Pane,
+ to_char(action_key(
+ normal_binds,
+ &[Action::SwitchToMode(InputMode::Pane)],
+ )),
+ ),
+ KeyShortcut::new(
+ KeyMode::Unselected,
+ KeyAction::Tab,
+ to_char(action_key(
+ normal_binds,
+ &[Action::SwitchToMode(InputMode::Tab)],
+ )),
+ ),
+ KeyShortcut::new(
+ KeyMode::UnselectedAlternate,
+ KeyAction::Resize,
+ to_char(action_key(
+ normal_binds,
+ &[Action::SwitchToMode(InputMode::Resize)],
+ )),
+ ),
+ KeyShortcut::new(
+ KeyMode::Unselected,
+ KeyAction::Move,
+ to_char(action_key(
+ normal_binds,
+ &[Action::SwitchToMode(InputMode::Move)],
+ )),
+ ),
+ KeyShortcut::new(
+ KeyMode::UnselectedAlternate,
+ KeyAction::Search,
+ to_char(action_key(
+ normal_binds,
+ &[Action::SwitchToMode(InputMode::Scroll)],
+ )),
+ ),
+ KeyShortcut::new(
+ KeyMode::Unselected,
+ KeyAction::Session,
+ to_char(action_key(
+ normal_binds,
+ &[Action::SwitchToMode(InputMode::Session)],
+ )),
+ ),
+ KeyShortcut::new(
+ KeyMode::UnselectedAlternate,
+ KeyAction::Quit,
+ to_char(action_key(normal_binds, &[Action::Quit])),
+ ),
+ ],
+ ),
+ (
+ InputMode::Pane,
+ vec![
+ KeyShortcut::new(
+ KeyMode::Selected,
+ KeyAction::Unlock,
+ to_char(action_key(
+ pane_binds,
+ &[Action::SwitchToMode(InputMode::Locked)],
+ )),
+ ),
+ KeyShortcut::new(
+ KeyMode::Selected,
+ KeyAction::Pane,
+ to_char(action_key(
+ pane_binds,
+ &[Action::SwitchToMode(InputMode::Normal)],
+ )),
+ ),
+ ],
+ ),
+ (
+ InputMode::Tab,
+ vec![
+ KeyShortcut::new(
+ KeyMode::Selected,
+ KeyAction::Unlock,
+ to_char(action_key(
+ tab_binds,
+ &[Action::SwitchToMode(InputMode::Locked)],
+ )),
+ ),
+ KeyShortcut::new(
+ KeyMode::Selected,
+ KeyAction::Tab,
+ to_char(action_key(
+ tab_binds,
+ &[Action::SwitchToMode(InputMode::Normal)],
+ )),
+ ),
+ ],
+ ),
+ (
+ InputMode::Resize,
+ vec![
+ KeyShortcut::new(
+ KeyMode::Selected,
+ KeyAction::Unlock,
+ to_char(action_key(
+ resize_binds,
+ &[Action::SwitchToMode(InputMode::Locked)],
+ )),
+ ),
+ KeyShortcut::new(
+ KeyMode::Selected,
+ KeyAction::Resize,
+ to_char(action_key(
+ resize_binds,
+ &[Action::SwitchToMode(InputMode::Normal)],
+ )),
+ ),
+ ],
+ ),
+ (
+ InputMode::Move,
+ vec![
+ KeyShortcut::new(
+ KeyMode::Selected,
+ KeyAction::Unlock,
+ to_char(action_key(
+ move_binds,
+ &[Action::SwitchToMode(InputMode::Locked)],
+ )),
+ ),
+ KeyShortcut::new(
+ KeyMode::Selected,
+ KeyAction::Move,
+ to_char(action_key(
+ move_binds,
+ &[Action::SwitchToMode(InputMode::Normal)],
+ )),
+ ),
+ ],
+ ),
+ (
+ InputMode::Scroll,
+ vec![
+ KeyShortcut::new(
+ KeyMode::Selected,
+ KeyAction::Unlock,
+ to_char(action_key(
+ scroll_binds,
+ &[Action::SwitchToMode(InputMode::Locked)],
+ )),
+ ),
+ KeyShortcut::new(
+ KeyMode::Selected,
+ KeyAction::Search,
+ to_char(action_key(
+ scroll_binds,
+ &[Action::SwitchToMode(InputMode::Normal)],
+ )),
+ ),
+ ],
+ ),
+ (
+ InputMode::Session,
+ vec![
+ KeyShortcut::new(
+ KeyMode::Selected,
+ KeyAction::Unlock,
+ to_char(action_key(
+ session_binds,
+ &[Action::SwitchToMode(InputMode::Locked)],
+ )),
+ ),
+ KeyShortcut::new(
+ KeyMode::Selected,
+ KeyAction::Session,
+ to_char(action_key(
+ session_binds,
+ &[Action::SwitchToMode(InputMode::Normal)],
+ )),
+ ),
+ ],
+ ),
+ ])
+}
+
+fn base_mode_normal_mode_indicators(help: &ModeInfo) -> HashMap<InputMode, Vec<KeyShortcut>> {
+ let locked_binds = &help.get_keybinds_for_mode(InputMode::Locked);
+ let normal_binds = &help.get_keybinds_for_mode(InputMode::Normal);
+ let pane_binds = &help.get_keybinds_for_mode(InputMode::Pane);
+ let tab_binds = &help.get_keybinds_for_mode(InputMode::Tab);
+ let resize_binds = &help.get_keybinds_for_mode(InputMode::Resize);
+ let move_binds = &help.get_keybinds_for_mode(InputMode::Move);
+ let scroll_binds = &help.get_keybinds_for_mode(InputMode::Scroll);
+ let session_binds = &help.get_keybinds_for_mode(InputMode::Session);
+ HashMap::from([
+ (
+ InputMode::Locked,
+ vec![KeyShortcut::new(
+ KeyMode::Selected,
+ KeyAction::Lock,
+ to_char(action_key(
+ locked_binds,
+ &[Action::SwitchToMode(InputMode::Normal)],
+ )),
+ )],
+ ),
+ (
+ InputMode::Normal,
+ vec![
+ KeyShortcut::new(
+ KeyMode::Unselected,
+ KeyAction::Lock,
+ to_char(action_key(
+ normal_binds,
+ &[Action::SwitchToMode(InputMode::Locked)],
+ )),
+ ),
+ KeyShortcut::new(
+ KeyMode::UnselectedAlternate,
+ KeyAction::Pane,
+ to_char(action_key(
+ normal_binds,
+ &[Action::SwitchToMode(InputMode::Pane)],
+ )),
+ ),
+ KeyShortcut::new(
+ KeyMode::Unselected,
+ KeyAction::Tab,
+ to_char(action_key(
+ normal_binds,
+ &[Action::SwitchToMode(InputMode::Tab)],
+ )),
+ ),
+ KeyShortcut::new(
+ KeyMode::UnselectedAlternate,
+ KeyAction::Resize,
+ to_char(action_key(
+ normal_binds,
+ &[Action::SwitchToMode(InputMode::Resize)],
+ )),
+ ),
+ KeyShortcut::new(
+ KeyMode::Unselected,
+ KeyAction::Move,
+ to_char(action_key(
+ normal_binds,
+ &[Action::SwitchToMode(InputMode::Move)],
+ )),
+ ),
+ KeyShortcut::new(
+ KeyMode::UnselectedAlternate,
+ KeyAction::Search,
+ to_char(action_key(
+ normal_binds,
+ &[Action::SwitchToMode(InputMode::Scroll)],
+ )),
+ ),
+ KeyShortcut::new(
+ KeyMode::Unselected,
+ KeyAction::Session,
+ to_char(action_key(
+ normal_binds,
+ &[Action::SwitchToMode(InputMode::Session)],
+ )),
+ ),
+ KeyShortcut::new(
+ KeyMode::UnselectedAlternate,
+ KeyAction::Quit,
+ to_char(action_key(normal_binds, &[Action::Quit])),
+ ),
+ ],
+ ),
+ (
+ InputMode::Pane,
+ vec![KeyShortcut::new(
+ KeyMode::Selected,
+ KeyAction::Pane,
+ to_char(action_key(
+ pane_binds,
+ &[Action::SwitchToMode(InputMode::Normal)],
+ )),
+ )],
+ ),
+ (
+ InputMode::Tab,
+ vec![KeyShortcut::new(
+ KeyMode::Selected,
+ KeyAction::Tab,
+ to_char(action_key(
+ tab_binds,
+ &[Action::SwitchToMode(InputMode::Normal)],
+ )),
+ )],
+ ),
+ (
+ InputMode::Resize,
+ vec![KeyShortcut::new(
+ KeyMode::Selected,
+ KeyAction::Resize,
+ to_char(action_key(
+ resize_binds,
+ &[Action::SwitchToMode(InputMode::Normal)],
+ )),
+ )],
+ ),
+ (
+ InputMode::Move,
+ vec![KeyShortcut::new(
+ KeyMode::Selected,
+ KeyAction::Move,
+ to_char(action_key(
+ move_binds,
+ &[Action::SwitchToMode(InputMode::Normal)],
+ )),
+ )],
+ ),
+ (
+ InputMode::Scroll,
+ vec![KeyShortcut::new(
+ KeyMode::Selected,
+ KeyAction::Search,
+ to_char(action_key(
+ scroll_binds,
+ &[Action::SwitchToMode(InputMode::Normal)],
+ )),
+ )],
+ ),
+ (
+ InputMode::Session,
+ vec![KeyShortcut::new(
+ KeyMode::Selected,
+ KeyAction::Session,
+ to_char(action_key(
+ session_binds,
+ &[Action::SwitchToMode(InputMode::Normal)],
+ )),
+ )],
+ ),
+ ])
+}
+
+fn render_mode_key_indicators(
+ help: &ModeInfo,
+ max_len: usize,
+ separator: &str,
+ base_mode_is_locked: bool,
+) -> Option<LinePart> {
+ let mut line_part_to_render = LinePart::default();
+ let supports_arrow_fonts = !help.capabilities.arrow_fonts;
+ let colored_elements = color_elements(help.style.colors, !supports_arrow_fonts);
+ let default_keys = if base_mode_is_locked {
+ base_mode_locked_mode_indicators(help)
+ } else {
+ base_mode_normal_mode_indicators(help)
+ };
+ match common_modifiers_in_all_modes(&default_keys) {
+ Some(modifiers) => {
+ if let Some(default_keys) = default_keys.get(&help.mode) {
+ let keys_without_common_modifiers: Vec<KeyShortcut> = default_keys
+ .iter()
+ .map(|key_shortcut| {
+ let key = key_shortcut
+ .get_key()
+ .map(|k| k.strip_common_modifiers(&modifiers));
+ let mode = key_shortcut.get_mode();
+ let action = key_shortcut.get_action();
+ KeyShortcut::new(mode, action, key)
+ })
+ .collect();
+ render_common_modifiers(
+ &colored_elements,
+ help,
+ &modifiers,
+ &mut line_part_to_render,
+ separator,
+ );
+
+ let full_shortcut_list =
+ full_inline_keys_modes_shortcut_list(&keys_without_common_modifiers, help);
+
+ if line_part_to_render.len + full_shortcut_list.len <= max_len {
+ line_part_to_render.append(&full_shortcut_list);
+ } else {
+ let shortened_shortcut_list = shortened_inline_keys_modes_shortcut_list(
+ &keys_without_common_modifiers,
+ help,
+ );
+ if line_part_to_render.len + shortened_shortcut_list.len <= max_len {
+ line_part_to_render.append(&shortened_shortcut_list);
+ }
+ }
+ }
+ },
+ None => {
+ if let Some(default_keys) = default_keys.get(&help.mode) {
+ let full_shortcut_list = full_modes_shortcut_list(&default_keys, help);
+ if line_part_to_render.len + full_shortcut_list.len <= max_len {
+ line_part_to_render.append(&full_shortcut_list);
+ } else {
+ let shortened_shortcut_list =
+ shortened_modes_shortcut_list(&default_keys, help);
+ if line_part_to_render.len + shortened_shortcut_list.len <= max_len {
+ line_part_to_render.append(&shortened_shortcut_list);
+ }
+ }
+ }
+ },
+ }
+ if line_part_to_render.len <= max_len {
+ Some(line_part_to_render)
+ } else {
+ None
+ }
+}
+
+fn full_inline_keys_modes_shortcut_list(
+ keys_without_common_modifiers: &Vec<KeyShortcut>,
+ help: &ModeInfo,
+) -> LinePart {
+ let mut full_shortcut_list = LinePart::default();
+ for key in keys_without_common_modifiers {
+ let is_selected = key.is_selected();
+ let shortcut = add_shortcut_with_inline_key(
+ help,
+ &key.full_text(),
+ key.key
+ .as_ref()
+ .map(|k| vec![k.clone()])
+ .unwrap_or_else(|| vec![]),
+ is_selected,
+ );
+ full_shortcut_list.append(&shortcut);
+ }
+ full_shortcut_list
+}
+
+fn shortened_inline_keys_modes_shortcut_list(
+ keys_without_common_modifiers: &Vec<KeyShortcut>,
+ help: &ModeInfo,
+) -> LinePart {
+ let mut shortened_shortcut_list = LinePart::default();
+ for key in keys_without_common_modifiers {
+ let is_selected = key.is_selected();
+ let shortcut = add_shortcut_with_key_only(
+ help,
+ key.key
+ .as_ref()
+ .map(|k| vec![k.clone()])
+ .unwrap_or_else(|| vec![]),
+ is_selected,
+ );
+ shortened_shortcut_list.append(&shortcut);
+ }
+ shortened_shortcut_list
+}
+
+fn full_modes_shortcut_list(default_keys: &Vec<KeyShortcut>, help: &ModeInfo) -> LinePart {
+ let mut full_shortcut_list = LinePart::default();
+ for key in default_keys {
+ let is_selected = key.is_selected();
+ full_shortcut_list.append(&add_shortcut(
+ help,
+ &key.full_text(),
+ &key.key
+ .as_ref()
+ .map(|k| vec![k.clone()])
+ .unwrap_or_else(|| vec![]),
+ is_selected,
+ Some(3),
+ ));
+ }
+ full_shortcut_list
+}
+
+fn shortened_modes_shortcut_list(default_keys: &Vec<KeyShortcut>, help: &ModeInfo) -> LinePart {
+ let mut shortened_shortcut_list = LinePart::default();
+ for key in default_keys {
+ let is_selected = key.is_selected();
+ shortened_shortcut_list.append(&add_shortcut(
+ help,
+ &key.short_text(),
+ &key.key
+ .as_ref()
+ .map(|k| vec![k.clone()])
+ .unwrap_or_else(|| vec![]),
+ is_selected,
+ Some(3),
+ ));
+ }
+ shortened_shortcut_list
+}
+
+fn common_modifiers_in_all_modes(
+ key_shortcuts: &HashMap<InputMode, Vec<KeyShortcut>>,
+) -> Option<Vec<KeyModifier>> {
+ let Some(mut common_modifiers) = key_shortcuts.iter().next().and_then(|k| {
+ k.1.iter()
+ .next()
+ .and_then(|k| k.get_key().map(|k| k.key_modifiers.clone()))
+ }) else {
+ return None;
+ };
+ for (_mode, key_shortcuts) in key_shortcuts {
+ if key_shortcuts.is_empty() {
+ return None;
+ }
+ let Some(mut common_modifiers_for_mode) = key_shortcuts
+ .iter()
+ .next()
+ .unwrap()
+ .get_key()
+ .map(|k| k.key_modifiers.clone())
+ else {
+ return None;
+ };
+ for key in key_shortcuts {
+ let Some(key) = key.get_key() else {
+ return None;
+ };
+ common_modifiers_for_mode = common_modifiers_for_mode
+ .intersection(&key.key_modifiers)
+ .cloned()
+ .collect();
+ }
+ common_modifiers = common_modifiers
+ .intersection(&common_modifiers_for_mode)
+ .cloned()
+ .collect();
+ }
+ if common_modifiers.is_empty() {
+ return None;
+ }
+ Some(common_modifiers.into_iter().collect())
+}
+
+fn render_common_modifiers(
+ palette: &ColoredElements,
+ mode_info: &ModeInfo,
+ common_modifiers: &Vec<KeyModifier>,
+ line_part_to_render: &mut LinePart,
+ separator: &str,
+) {
+ let prefix_text = if mode_info.capabilities.arrow_fonts {
+ // Add extra space in simplified ui
+ format!(
+ " {} + ",
+ common_modifiers
+ .iter()
+ .map(|m| m.to_string())
+ .collect::<Vec<_>>()
+ .join("-")
+ )
+ } else {
+ format!(
+ " {} +",
+ common_modifiers
+ .iter()
+ .map(|m| m.to_string())
+ .collect::<Vec<_>>()
+ .join("-")
+ )
+ };
+
+ let suffix_separator = palette.superkey_suffix_separator.paint(separator);
+ line_part_to_render.part = format!(
+ "{}{}{}",
+ line_part_to_render.part,
+ serialize_text(&Text::new(&prefix_text)),
+ suffix_separator
+ );
+ line_part_to_render.len += prefix_text.chars().count() + separator.chars().count();
+}
+
+fn render_secondary_info(
+ help: &ModeInfo,
+ tab_info: Option<&TabInfo>,
+ max_len: usize,
+) -> Option<LinePart> {
+ let mut secondary_info = LinePart::default();
+ let supports_arrow_fonts = !help.capabilities.arrow_fonts;
+ let colored_elements = color_elements(help.style.colors, !supports_arrow_fonts);
+ let secondary_keybinds = secondary_keybinds(&help, tab_info, max_len);
+ secondary_info.append(&secondary_keybinds);
+ let remaining_space = max_len.saturating_sub(secondary_info.len).saturating_sub(1); // 1 for the end padding of the line
+ let mut padding = String::new();
+ let mut padding_len = 0;
+ for _ in 0..remaining_space {
+ padding.push_str(&ANSIStrings(&[colored_elements.superkey_prefix.paint(" ")]).to_string());
+ padding_len += 1;
+ }
+ secondary_info.part = format!("{}{}", padding, secondary_info.part);
+ secondary_info.len += padding_len;
+ if secondary_info.len <= max_len {
+ Some(secondary_info)
+ } else {
+ None
+ }
+}
+
+fn secondary_keybinds(help: &ModeInfo, tab_info: Option<&TabInfo>, max_len: usize) -> LinePart {
+ let mut secondary_info = LinePart::default();
+ let binds = &help.get_mode_keybinds();
+ // New Pane
+ let new_pane_action_key = action_key(binds, &[Action::NewPane(None, None)]);
+ let mut new_pane_key_to_display = new_pane_action_key
+ .iter()
+ .find(|k| k.is_key_with_alt_modifier(BareKey::Char('n')))
+ .or_else(|| new_pane_action_key.iter().next());
+ let new_pane_key_to_display =
+ if let Some(new_pane_key_to_display) = new_pane_key_to_display.take() {
+ vec![new_pane_key_to_display.clone()]
+ } else {
+ vec![]
+ };
+
+ // Move focus
+ let mut move_focus_shortcuts: Vec<KeyWithModifier> = vec![];
+
+ // Left
+ let move_focus_left_action_key = action_key(binds, &[Action::MoveFocusOrTab(Direction::Left)]);
+ let move_focus_left_key = move_focus_left_action_key
+ .iter()
+ .find(|k| k.is_key_with_alt_modifier(BareKey::Left))
+ .or_else(|| move_focus_left_action_key.iter().next());
+ if let Some(move_focus_left_key) = move_focus_left_key {
+ move_focus_shortcuts.push(move_focus_left_key.clone());
+ }
+ // Down
+ let move_focus_left_action_key = action_key(binds, &[Action::MoveFocus(Direction::Down)]);
+ let move_focus_left_key = move_focus_left_action_key
+ .iter()
+ .find(|k| k.is_key_with_alt_modifier(BareKey::Down))
+ .or_else(|| move_focus_left_action_key.iter().next());
+ if let Some(move_focus_left_key) = move_focus_left_key {
+ move_focus_shortcuts.push(move_focus_left_key.clone());
+ }
+ // Up
+ let move_focus_left_action_key = action_key(binds, &[Action::MoveFocus(Direction::Up)]);
+ let move_focus_left_key = move_focus_left_action_key
+ .iter()
+ .find(|k| k.is_key_with_alt_modifier(BareKey::Up))
+ .or_else(|| move_focus_left_action_key.iter().next());
+ if let Some(move_focus_left_key) = move_focus_left_key {
+ move_focus_shortcuts.push(move_focus_left_key.clone());
+ }
+ // Right
+ let move_focus_left_action_key = action_key(binds, &[Action::MoveFocusOrTab(Direction::Right)]);
+ let move_focus_left_key = move_focus_left_action_key
+ .iter()
+ .find(|k| k.is_key_with_alt_modifier(BareKey::Right))
+ .or_else(|| move_focus_left_action_key.iter().next());
+ if let Some(move_focus_left_key) = move_focus_left_key {
+ move_focus_shortcuts.push(move_focus_left_key.clone());
+ }
+
+ let toggle_floating_action_key = action_key(binds, &[Action::ToggleFloatingPanes]);
+ let mut toggle_floating_action_key = toggle_floating_action_key
+ .iter()
+ .find(|k| k.is_key_with_alt_modifier(BareKey::Char('f')))
+ .or_else(|| toggle_floating_action_key.iter().next());
+ let toggle_floating_key_to_display =
+ if let Some(toggle_floating_key_to_display) = toggle_floating_action_key.take() {
+ vec![toggle_floating_key_to_display.clone()]
+ } else {
+ vec![]
+ };
+ let are_floating_panes_visible = tab_info
+ .map(|t| t.are_floating_panes_visible)
+ .unwrap_or(false);
+
+ let common_modifiers = get_common_modifiers(
+ [
+ new_pane_key_to_display.clone(),
+ move_focus_shortcuts.clone(),
+ toggle_floating_key_to_display.clone(),
+ ]
+ .iter()
+ .flatten()
+ .collect(),
+ );
+ let no_common_modifier = common_modifiers.is_empty();
+
+ if no_common_modifier {
+ secondary_info.append(&add_shortcut(
+ help,
+ "New Pane",
+ &new_pane_key_to_display,
+ false,
+ Some(0),
+ ));
+ secondary_info.append(&add_shortcut(
+ help,
+ "Change Focus",
+ &move_focus_shortcuts,
+ false,
+ Some(0),
+ ));
+ secondary_info.append(&add_shortcut(
+ help,
+ "Floating",
+ &toggle_floating_key_to_display,
+ are_floating_panes_visible,
+ Some(0),
+ ));
+ } else {
+ let modifier_str = text_as_line_part_with_emphasis(
+ format!(
+ "{} + ",
+ common_modifiers
+ .iter()
+ .map(|m| m.to_string())
+ .collect::<Vec<_>>()
+ .join("-")
+ ),
+ 0,
+ );
+ secondary_info.append(&modifier_str);
+ let new_pane_key_to_display: Vec<KeyWithModifier> = new_pane_key_to_display
+ .iter()
+ .map(|k| k.strip_common_modifiers(&common_modifiers))
+ .collect();
+ let move_focus_shortcuts: Vec<KeyWithModifier> = move_focus_shortcuts
+ .iter()
+ .map(|k| k.strip_common_modifiers(&common_modifiers))
+ .collect();
+ let toggle_floating_key_to_display: Vec<KeyWithModifier> = toggle_floating_key_to_display
+ .iter()
+ .map(|k| k.strip_common_modifiers(&common_modifiers))
+ .collect();
+ secondary_info.append(&add_shortcut_with_inline_key(
+ help,
+ "New Pane",
+ new_pane_key_to_display,
+ false,
+ ));
+ secondary_info.append(&add_shortcut_with_inline_key(
+ help,
+ "Change Focus",
+ move_focus_shortcuts,
+ false,
+ ));
+ secondary_info.append(&add_shortcut_with_inline_key(
+ help,
+ "Floating",
+ toggle_floating_key_to_display,
+ are_floating_panes_visible,
+ ));
+ }
+
+ if secondary_info.len <= max_len {
+ secondary_info
+ } else {
+ let mut short_line = LinePart::default();
+ if no_common_modifier {
+ short_line.append(&add_shortcut(
+ help,
+ "New",
+ &new_pane_key_to_display,
+ false,
+ Some(0),
+ ));
+ short_line.append(&add_shortcut(
+ help,
+ "Focus",
+ &move_focus_shortcuts,
+ false,
+ Some(0),
+ ));
+ short_line.append(&add_shortcut(
+ help,
+ "Floating",
+ &toggle_floating_key_to_display,
+ are_floating_panes_visible,
+ Some(0),
+ ));
+ } else {
+ let modifier_str = text_as_line_part_with_emphasis(
+ format!(
+ "{} + ",
+ common_modifiers
+ .iter()
+ .map(|m| m.to_string())
+ .collect::<Vec<_>>()
+ .join("-")
+ ),
+ 0,
+ );
+ short_line.append(&modifier_str);
+ let new_pane_key_to_display: Vec<KeyWithModifier> = new_pane_key_to_display
+ .iter()
+ .map(|k| k.strip_common_modifiers(&common_modifiers))
+ .collect();
+ let move_focus_shortcuts: Vec<KeyWithModifier> = move_focus_shortcuts
+ .iter()
+ .map(|k| k.strip_common_modifiers(&common_modifiers))
+ .collect();
+ let toggle_floating_key_to_display: Vec<KeyWithModifier> =
+ toggle_floating_key_to_display
+ .iter()
+ .map(|k| k.strip_common_modifiers(&common_modifiers))
+ .collect();
+ short_line.append(&add_shortcut_with_inline_key(
+ help,
+ "New",
+ new_pane_key_to_display,
+ false,
+ ));
+ short_line.append(&add_shortcut_with_inline_key(
+ help,
+ "Focus",
+ move_focus_shortcuts,
+ false,
+ ));
+ short_line.append(&add_shortcut_with_inline_key(
+ help,
+ "Floating",
+ toggle_floating_key_to_display,
+ are_floating_panes_visible,
+ ));
+ }
+ short_line
+ }
+}
+
+fn text_as_line_part_with_emphasis(text: String, emphases_index: usize) -> LinePart {
+ let part = serialize_text(&Text::new(&text).color_range(emphases_index, ..));
+ LinePart {
+ part,
+ len: text.width(),
+ }
+}
+
+fn keybinds(help: &ModeInfo, max_width: usize) -> Option<LinePart> {
+ let full_shortcut_list = full_shortcut_list(help);
+ if full_shortcut_list.len <= max_width {
+ return Some(full_shortcut_list);
+ }
+ let shortened_shortcut_list = shortened_shortcut_list(help);
+ if shortened_shortcut_list.len <= max_width {
+ return Some(shortened_shortcut_list);
+ }
+ Some(best_effort_shortcut_list(help, max_width))
+}
+
+fn add_shortcut(
+ help: &ModeInfo,
+ text: &str,
+ keys: &Vec<KeyWithModifier>,
+ selected: bool,
+ key_color_index: Option<usize>,
+) -> LinePart {
+ let mut ret = LinePart::default();
+ if keys.is_empty() {
+ return ret;
+ }
+
+ ret.append(&style_key_with_modifier(&keys, key_color_index)); // TODO: alternate
+ //
+ let ribbon = if selected {
+ serialize_ribbon(&Text::new(format!("{}", text)).selected())
+ } else {
+ serialize_ribbon(&Text::new(format!("{}", text)))
+ };
+ ret.part = format!("{}{}", ret.part, ribbon);
+ let supports_arrow_fonts = !help.capabilities.arrow_fonts;
+ ret.len += if supports_arrow_fonts {
+ text.width() + 4 // padding and arrow fonts
+ } else {
+ text.width() + 2 // padding
+ };
+ ret
+}
+
+fn add_shortcut_with_inline_key(
+ help: &ModeInfo,
+ text: &str,
+ key: Vec<KeyWithModifier>,
+ is_selected: bool,
+) -> LinePart {
+ let capabilities = help.capabilities;
+
+ let mut ret = LinePart::default();
+ if key.is_empty() {
+ return ret;
+ }
+
+ let key_separator = match key
+ .iter()
+ .map(|k| k.to_string())
+ .collect::<Vec<_>>()
+ .join("")
+ .as_str()
+ {
+ "HJKL" => "",
+ "hjkl" => "",
+ "←↓↑→" => "",
+ "←→" => "",
+ "↓↑" => "",
+ "[]" => "",
+ _ => "|",
+ };
+
+ let key_string = format!(
+ "{}",
+ key.iter()
+ .map(|k| k.to_string())
+ .collect::<Vec<_>>()
+ .join(key_separator)
+ );
+
+ let ribbon = if is_selected {
+ serialize_ribbon(
+ &Text::new(format!("<{}> {}", key_string, text))
+ .color_range(0, 1..key_string.width() + 1)
+ .selected(),
+ )
+ } else {
+ serialize_ribbon(
+ &Text::new(format!("<{}> {}", key_string, text))
+ .color_range(0, 1..key_string.width() + 1),
+ )
+ };
+ ret.part = ribbon;
+ let supports_arrow_fonts = !capabilities.arrow_fonts;
+ ret.len += if supports_arrow_fonts {
+ text.width() + key_string.width() + 7 // padding, group boundaries and arrow fonts
+ } else {
+ text.width() + key_string.width() + 5 // padding and group boundaries
+ };
+
+ ret
+}
+
+fn add_shortcut_with_key_only(
+ help: &ModeInfo,
+ key: Vec<KeyWithModifier>,
+ is_selected: bool,
+) -> LinePart {
+ let mut ret = LinePart::default();
+ if key.is_empty() {
+ return ret;
+ }
+
+ let key_string = format!(
+ "{}",
+ key.iter()
+ .map(|k| k.to_string())
+ .collect::<Vec<_>>()
+ .join("-")
+ );
+
+ let ribbon = if is_selected {
+ serialize_ribbon(
+ &Text::new(format!("{}", key_string))
+ .color_range(0, ..)
+ .selected(),
+ )
+ } else {
+ serialize_ribbon(&Text::new(format!("{}", key_string)).color_range(0, ..))
+ };
+ ret.part = ribbon;
+ let supports_arrow_fonts = !help.capabilities.arrow_fonts;
+ ret.len += if supports_arrow_fonts {
+ key_string.width() + 4 // 4 => arrow fonts + padding
+ } else {
+ key_string.width() + 2 // 2 => padding
+ };
+ ret
+}
+
+fn add_keygroup_separator(help: &ModeInfo, max_len: usize) -> Option<LinePart> {
+ let supports_arrow_fonts = !help.capabilities.arrow_fonts;
+ let separator = if supports_arrow_fonts {
+ crate::ARROW_SEPARATOR
+ } else {
+ " "
+ };
+ let palette = help.style.colors;
+
+ let mut ret = LinePart::default();
+
+ let separator_color = palette_match!(palette.orange);
+ let bg_color = palette_match!(palette.black);
+ let mut bits: Vec<ANSIString> = vec![];
+ let mode_help_text = match help.mode {
+ InputMode::RenamePane => Some("RENAMING PANE"),
+ InputMode::RenameTab => Some("RENAMING TAB"),
+ InputMode::EnterSearch => Some("ENTERING SEARCH TERM"),
+ InputMode::Search => Some("SEARCHING"),
+ _ => None,
+ };
+ if let Some(mode_help_text) = mode_help_text {
+ bits.push(
+ Style::new()
+ .fg(separator_color)
+ .bold()
+ .paint(format!(" {} ", mode_help_text)),
+ );
+ ret.len += mode_help_text.width() + 2; // 2 => padding
+ }
+ bits.push(
+ Style::new()
+ .fg(bg_color)
+ .on(separator_color)
+ .bold()
+ .paint(format!("{}", separator)),
+ );
+ bits.push(
+ Style::new()
+ .fg(separator_color)
+ .on(separator_color)
+ .bold()
+ .paint(format!(" ")),
+ );
+ bits.push(
+ Style::new()
+ .fg(separator_color)
+ .on(bg_color)
+ .bold()
+ .paint(format!("{}", separator)),
+ );
+ ret.part = format!("{}{}", ret.part, ANSIStrings(&bits));
+ ret.len += 3; // padding and arrow fonts
+
+ if ret.len <= max_len {
+ Some(ret)
+ } else {
+ None
+ }
+}
+
+fn full_shortcut_list(help: &ModeInfo) -> LinePart {
+ match help.mode {
+ InputMode::Normal => LinePart::default(),
+ InputMode::Locked => LinePart::default(),
+ _ => full_shortcut_list_nonstandard_mode(help),
+ }
+}
+
+fn full_shortcut_list_nonstandard_mode(help: &ModeInfo) -> LinePart {
+ let mut line_part = LinePart::default();
+ let keys_and_hints = get_keys_and_hints(help);
+
+ for (long, _short, keys) in keys_and_hints.into_iter() {
+ line_part.append(&add_shortcut(help, &long, &keys.to_vec(), false, Some(2)));
+ }
+ line_part
+}
+
+#[rustfmt::skip]
+fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec<KeyWithModifier>)> {
+ use Action as A;
+ use InputMode as IM;
+ use Direction as Dir;
+ use actions::SearchDirection as SDir;
+ use actions::SearchOption as SOpt;
+
+ let mut old_keymap = mi.get_mode_keybinds();
+ let s = |string: &str| string.to_string();
+
+ // Find a keybinding to get back to "Normal" input mode. In this case we prefer '\n' over other
+ // choices. Do it here before we dedupe the keymap below!
+ let base_mode = mi.base_mode;
+ let to_basemode_keys = base_mode.map(|b| action_key(&old_keymap, &[to_base_mode(b)])).unwrap_or_else(|| action_key(&old_keymap, &[TO_NORMAL]));
+ let to_basemode_key = if to_basemode_keys.contains(&KeyWithModifier::new(BareKey::Enter)) {
+ vec![KeyWithModifier::new(BareKey::Enter)]
+ } else {
+ // Yield `vec![key]` if `to_normal_keys` has at least one key, or an empty vec otherwise.
+ to_basemode_keys.into_iter().take(1).collect()
+ };
+
+ // Sort and deduplicate the keybindings first. We sort after the `Key`s, and deduplicate by
+ // their `Action` vectors. An unstable sort is fine here because if the user maps anything to
+ // the same key again, anything will happen...
+ old_keymap.sort_unstable_by(|(keya, _), (keyb, _)| keya.partial_cmp(keyb).unwrap());
+
+ let mut known_actions: Vec<Vec<Action>> = vec![];
+ let mut km = vec![];
+ for (key, acvec) in old_keymap {
+ if known_actions.contains(&acvec) {
+ // This action is known already
+ continue;
+ } else {
+ known_actions.push(acvec.to_vec());
+ km.push((key, acvec));
+ }
+ }
+
+ if mi.mode == IM::Pane { vec![
+ (s("New"), s("New"), single_action_key(&km, &[A::NewPane(None, None), TO_NORMAL])),
+ (s("Change Focus"), s("Move"),
+ action_key_group(&km, &[&[A::MoveFocus(Dir::Left)], &[A::MoveFocus(Dir::Down)],
+ &[A::MoveFocus(Dir::Up)], &[A::MoveFocus(Dir::Right)]])),
+ (s("Close"), s("Close"), single_action_key(&km, &[A::CloseFocus, TO_NORMAL])),
+ (s("Rename"), s("Rename"),
+ single_action_key(&km, &[A::SwitchToMode(IM::RenamePane), A::PaneNameInput(vec![0])])),
+ (s("Toggle Fullscreen"), s("Fullscreen"), single_action_key(&km, &[A::ToggleFocusFullscreen, TO_NORMAL])),
+ (s("Toggle Floating"), s("Floating"),
+ single_action_key(&km, &[A::ToggleFloatingPanes, TO_NORMAL])),
+ (s("Toggle Embed"), s("Embed"), single_action_key(&km, &[A::TogglePaneEmbedOrFloating, TO_NORMAL])),
+ (s("Select pane"), s("Select"), to_basemode_key),
+ ]} else if mi.mode == IM::Tab {
+ // With the default bindings, "Move focus" for tabs is tricky: It binds all the arrow keys
+ // to moving tabs focus (left/up go left, right/down go right). Since we sort the keys
+ // above and then dedpulicate based on the actions, we will end up with LeftArrow for
+ // "left" and DownArrow for "right". What we really expect is to see LeftArrow and
+ // RightArrow.
+ // FIXME: So for lack of a better idea we just check this case manually here.
+ let old_keymap = mi.get_mode_keybinds();
+ let focus_keys_full: Vec<KeyWithModifier> = action_key_group(&old_keymap,
+ &[&[A::GoToPreviousTab], &[A::GoToNextTab]]);
+ let focus_keys = if focus_keys_full.contains(&KeyWithModifier::new(BareKey::Left))
+ && focus_keys_full.contains(&KeyWithModifier::new(BareKey::Right)) {
+ vec![KeyWithModifier::new(BareKey::Left), KeyWithModifier::new(BareKey::Right)]
+ } else {
+ action_key_group(&km, &[&[A::GoToPreviousTab], &[A::GoToNextTab]])
+ };
+
+ vec![
+ (s("New"), s("New"), single_action_key(&km, &[A::NewTab(None, vec![], None, None, None), TO_NORMAL])),
+ (s("Change focus"), s("Move"), focus_keys),
+ (s("Close"), s("Close"), single_action_key(&km, &[A::CloseTab, TO_NORMAL])),
+ (s("Rename"), s("Rename"),
+ single_action_key(&km, &[A::SwitchToMode(IM::RenameTab), A::TabNameInput(vec![0])])),
+ (s("Sync"), s("Sync"), single_action_key(&km, &[A::ToggleActiveSyncTab, TO_NORMAL])),
+ (s("Break pane to new tab"), s("Break out"), single_action_key(&km, &[A::BreakPane, TO_NORMAL])),
+ (s("Break pane left/right"), s("Break"), action_key_group(&km, &[
+ &[Action::BreakPaneLeft, TO_NORMAL],
+ &[Action::BreakPaneRight, TO_NORMAL],
+ ])),
+ (s("Toggle"), s("Toggle"), single_action_key(&km, &[A::ToggleTab])),
+ (s("Select pane"), s("Select"), to_basemode_key),
+ ]} else if mi.mode == IM::Resize { vec![
+ (s("Increase/Decrease size"), s("Increase/Decrease"),
+ action_key_group(&km, &[
+ &[A::Resize(Resize::Increase, None)],
+ &[A::Resize(Resize::Decrease, None)]
+ ])),
+ (s("Increase to"), s("Increase"), action_key_group(&km, &[
+ &[A::Resize(Resize::Increase, Some(Dir::Left))],
+ &[A::Resize(Resize::Increase, Some(Dir::Down))],
+ &[A::Resize(Resize::Increase, Some(Dir::Up))],
+ &[A::Resize(Resize::Increase, Some(Dir::Right))]
+ ])),
+ (s("Decrease from"), s("Decrease"), action_key_group(&km, &[
+ &[A::Resize(Resize::Decrease, Some(Dir::Left))],
+ &[A::Resize(Resize::Decrease, Some(Dir::Down))],
+ &[A::Resize(Resize::Decrease, Some(Dir::Up))],
+ &[A::Resize(Resize::Decrease, Some(Dir::Right))]
+ ])),
+ (s("Select pane"), s("Select"), to_basemode_key),
+ ]} else if mi.mode == IM::Move { vec![
+ (s("Switch Location"), s("Move"), action_key_group(&km, &[
+ &[Action::MovePane(Some(Dir::Left))], &[Action::MovePane(Some(Dir::Down))],
+ &[Action::MovePane(Some(Dir::Up))], &[Action::MovePane(Some(Dir::Right))]])),
+ ]} else if mi.mode == IM::Scroll { vec![
+ (s("Enter search term"), s("Search"),
+ action_key(&km, &[A::SwitchToMode(IM::EnterSearch), A::SearchInput(vec![0])])),
+ (s("Scroll"), s("Scroll"),
+ action_key_group(&km, &[&[Action::ScrollDown], &[Action::ScrollUp]])),
+ (s("Scroll page"), s("Scroll"),
+ action_key_group(&km, &[&[Action::PageScrollDown], &[Action::PageScrollUp]])),
+ (s("Scroll half page"), s("Scroll"),
+ action_key_group(&km, &[&[Action::HalfPageScrollDown], &[Action::HalfPageScrollUp]])),
+ (s("Edit scrollback in default editor"), s("Edit"),
+ single_action_key(&km, &[Action::EditScrollback, TO_NORMAL])),
+ (s("Select pane"), s("Select"), to_basemode_key),
+ ]} else if mi.mode == IM::EnterSearch { vec![
+ (s("When done"), s("Done"), action_key(&km, &[A::SwitchToMode(IM::Search)])),
+ (s("Cancel"), s("Cancel"),
+ action_key(&km, &[A::SearchInput(vec![27]), A::SwitchToMode(IM::Scroll)])),
+ ]} else if mi.mode == IM::Search { vec![
+ (s("Enter Search term"), s("Search"),
+ action_key(&km, &[A::SwitchToMode(IM::EnterSearch), A::SearchInput(vec![0])])),
+ (s("Scroll"), s("Scroll"),
+ action_key_group(&km, &[&[Action::ScrollDown], &[Action::ScrollUp]])),
+ (s("Scroll page"), s("Scroll"),
+ action_key_group(&km, &[&[Action::PageScrollDown], &[Action::PageScrollUp]])),
+ (s("Scroll half page"), s("Scroll"),
+ action_key_group(&km, &[&[Action::HalfPageScrollDown], &[Action::HalfPageScrollUp]])),
+ (s("Search down"), s("Down"), action_key(&km, &[A::Search(SDir::Down)])),
+ (s("Search up"), s("Up"), action_key(&km, &[A::Search(SDir::Up)])),
+ (s("Case sensitive"), s("Case"),
+ action_key(&km, &[A::SearchToggleOption(SOpt::CaseSensitivity)])),
+ (s("Wrap"), s("Wrap"),
+ action_key(&km, &[A::SearchToggleOption(SOpt::Wrap)])),
+ (s("Whole words"), s("Whole"),
+ action_key(&km, &[A::SearchToggleOption(SOpt::WholeWord)])),
+ ]} else if mi.mode == IM::Session { vec![
+ (s("Detach"), s("Detach"), action_key(&km, &[Action::Detach])),
+ (s("Session Manager"), s("Manager"), session_manager_key(&km)),
+ (s("Select pane"), s("Select"), to_basemode_key),
+ ]} else if mi.mode == IM::Tmux { vec![
+ (s("Move focus"), s("Move"), action_key_group(&km, &[
+ &[A::MoveFocus(Dir::Left)], &[A::MoveFocus(Dir::Down)],
+ &[A::MoveFocus(Dir::Up)], &[A::MoveFocus(Dir::Right)]])),
+ (s("Split down"), s("Down"), action_key(&km, &[A::NewPane(Some(Dir::Down), None), TO_NORMAL])),
+ (s("Split right"), s("Right"), action_key(&km, &[A::NewPane(Some(Dir::Right), None), TO_NORMAL])),
+ (s("Fullscreen"), s("Fullscreen"), action_key(&km, &[A::ToggleFocusFullscreen, TO_NORMAL])),
+ (s("New tab"), s("New"), action_key(&km, &[A::NewTab(None, vec![], None, None, None), TO_NORMAL])),
+ (s("Rename tab"), s("Rename"),
+ action_key(&km, &[A::SwitchToMode(IM::RenameTab), A::TabNameInput(vec![0])])),
+ (s("Previous Tab"), s("Previous"), action_key(&km, &[A::GoToPreviousTab, TO_NORMAL])),
+ (s("Next Tab"), s("Next"), action_key(&km, &[A::GoToNextTab, TO_NORMAL])),
+ (s("Select pane"), s("Select"), to_basemode_key),
+ ]} else if matches!(mi.mode, IM::RenamePane | IM::RenameTab) { vec![
+ (s("When done"), s("Done"), to_basemode_key),
+ ]} else { vec![] }
+}
+
+fn shortened_shortcut_list_nonstandard_mode(help: &ModeInfo) -> LinePart {
+ let mut line_part = LinePart::default();
+ let keys_and_hints = get_keys_and_hints(help);
+
+ for (_, short, keys) in keys_and_hints.into_iter() {
+ line_part.append(&add_shortcut(help, &short, &keys.to_vec(), false, Some(2)));
+ }
+ line_part
+}
+
+fn shortened_shortcut_list(help: &ModeInfo) -> LinePart {
+ match help.mode {
+ InputMode::Normal => LinePart::default(),
+ InputMode::Locked => LinePart::default(),
+ _ => shortened_shortcut_list_nonstandard_mode(help),
+ }
+}
+
+fn best_effort_shortcut_list(help: &ModeInfo, max_len: usize) -> LinePart {
+ let mut line_part = LinePart::default();
+ let keys_and_hints = get_keys_and_hints(help);
+ for (_, short, keys) in keys_and_hints.into_iter() {
+ let shortcut = add_shortcut(help, &short, &keys.to_vec(), false, Some(2));
+ if line_part.len + shortcut.len + MORE_MSG.chars().count() > max_len {
+ line_part.part = format!("{}{}", line_part.part, MORE_MSG);
+ line_part.len += MORE_MSG.chars().count();
+ break;
+ } else {
+ line_part.append(&shortcut);
+ }
+ }
+ line_part
+}
+
+fn single_action_key(
+ keymap: &[(KeyWithModifier, Vec<Action>)],
+ action: &[Action],
+) -> Vec<KeyWithModifier> {
+ let mut matching = keymap.iter().find_map(|(key, acvec)| {
+ if acvec.iter().next() == action.iter().next() {
+ Some(key.clone())
+ } else {
+ None
+ }
+ });
+ if let Some(matching) = matching.take() {
+ vec![matching]
+ } else {
+ vec![]
+ }
+}
+
+fn session_manager_key(keymap: &[(KeyWithModifier, Vec<Action>)]) -> Vec<KeyWithModifier> {
+ let mut matching = keymap.iter().find_map(|(key, acvec)| {
+ let has_match = acvec
+ .iter()
+ .find(|a| a.launches_plugin("session-manager"))
+ .is_some();
+ if has_match {
+ Some(key.clone())
+ } else {
+ None
+ }
+ });
+ if let Some(matching) = matching.take() {
+ vec![matching]
+ } else {
+ vec![]
+ }
+}
+
+fn style_key_with_modifier(keyvec: &[KeyWithModifier], color_index: Option<usize>) -> LinePart {
+ if keyvec.is_empty() {
+ return LinePart::default();
+ }
+
+ let common_modifiers = get_common_modifiers(keyvec.iter().collect());
+
+ let no_common_modifier = common_modifiers.is_empty();
+ let modifier_str = common_modifiers
+ .iter()
+ .map(|m| m.to_string())
+ .collect::<Vec<_>>()
+ .join("-");
+
+ // Prints the keys
+ let key = keyvec
+ .iter()
+ .map(|key| {
+ if no_common_modifier || keyvec.len() == 1 {
+ format!("{}", key)
+ } else {
+ format!("{}", key.strip_common_modifiers(&common_modifiers))
+ }
+ })
+ .collect::<Vec<String>>();
+
+ // Special handling of some pre-defined keygroups
+ let key_string = key.join("");
+ let key_separator = match &key_string[..] {
+ "HJKL" => "",
+ "hjkl" => "",
+ "←↓↑→" => "",
+ "←→" => "",
+ "↓↑" => "",
+ "[]" => "",
+ _ => "|",
+ };
+
+ if no_common_modifier || key.len() == 1 {
+ let key_string_text = format!(" {} ", key.join(key_separator));
+ let text = if let Some(color_index) = color_index {
+ Text::new(&key_string_text).color_range(color_index, ..)
+ } else {
+ Text::new(&key_string_text)
+ };
+ LinePart {
+ part: serialize_text(&text),
+ len: key_string_text.width(),
+ }
+ } else {
+ let key_string_without_modifier = format!("{}", key.join(key_separator));
+ let key_string_text = format!(" {} <{}> ", modifier_str, key_string_without_modifier);
+ let text = if let Some(color_index) = color_index {
+ Text::new(&key_string_text)
+ .color_range(color_index, ..modifier_str.width() + 1)
+ .color_range(
+ color_index,
+ modifier_str.width() + 3
+ ..modifier_str.width() + 3 + key_string_without_modifier.width(),
+ )
+ } else {
+ Text::new(&key_string_text)
+ };
+ LinePart {
+ part: serialize_text(&text),
+ len: key_string_text.width(),
+ }
+ }
+}
+
+fn get_common_modifiers(mut keyvec: Vec<&KeyWithModifier>) -> Vec<KeyModifier> {
+ if keyvec.is_empty() {
+ return vec![];
+ }
+ let mut common_modifiers = keyvec.pop().unwrap().key_modifiers.clone();
+ for key in keyvec {
+ common_modifiers = common_modifiers
+ .intersection(&key.key_modifiers)
+ .cloned()
+ .collect();
+ }
+ common_modifiers.into_iter().collect()
+}
diff --git a/default-plugins/tab-bar/src/line.rs b/default-plugins/tab-bar/src/line.rs
index 668507a0..349981f4 100644
--- a/default-plugins/tab-bar/src/line.rs
+++ b/default-plugins/tab-bar/src/line.rs
@@ -1,9 +1,14 @@
use ansi_term::ANSIStrings;
+use ansi_term::{
+ Color::{Fixed, RGB},
+ Style,
+};
use unicode_width::UnicodeWidthStr;
use crate::{LinePart, ARROW_SEPARATOR};
+use zellij_tile::prelude::actions::Action;
use zellij_tile::prelude::*;
-use zellij_tile_utils::style;
+use zellij_tile_utils::{palette_match, style};
fn get_current_title_len(current_title: &[LinePart]) -> usize {
current_title.iter().map(|p| p.len).sum()
@@ -224,6 +229,9 @@ pub fn tab_line(
palette: Palette,
capabilities: PluginCapabilities,
hide_session_name: bool,
+ tab_info: Option<&TabInfo>,
+ mode_info: &ModeInfo,
+ hide_swap_layout_indicator: bool,
) -> Vec<LinePart> {
let mut tabs_after_active = all_tabs.split_off(active_tab_index);
let mut tabs_before_active = all_tabs;
@@ -236,10 +244,26 @@ pub fn tab_line(
true => tab_line_prefix(None, palette, cols),
false => tab_line_prefix(session_name, palette, cols),
};
- let prefix_len = get_current_title_len(&prefix);
+
+ let mut swap_layout_indicator = if hide_swap_layout_indicator {
+ None
+ } else {
+ tab_info.and_then(|tab_info| {
+ swap_layout_status(
+ cols,
+ &tab_info.active_swap_layout_name,
+ tab_info.is_swap_layout_dirty,
+ mode_info,
+ !capabilities.arrow_fonts,
+ )
+ })
+ };
+
+ let non_tab_len =
+ get_current_title_len(&prefix) + swap_layout_indicator.as_ref().map(|s| s.len).unwrap_or(0);
// if active tab alone won't fit in cols, don't draw any tabs
- if prefix_len + active_tab.len > cols {
+ if non_tab_len + active_tab.len > cols {
return prefix;
}
@@ -249,10 +273,189 @@ pub fn tab_line(
&mut tabs_before_active,
&mut tabs_after_active,
&mut tabs_to_render,
- cols.saturating_sub(prefix_len),
+ cols.saturating_sub(non_tab_len),
palette,
capabilities,
);
prefix.append(&mut tabs_to_render);
+
+ if let Some(mut swap_layout_indicator) = swap_layout_indicator.take() {
+ let remaining_space = cols
+ .saturating_sub(prefix.iter().fold(0, |len, part| len + part.len))
+ .saturating_sub(swap_layout_indicator.len);
+ let mut padding = String::new();
+ let mut padding_len = 0;
+ for _ in 0..remaining_space {
+ padding.push_str(" ");
+ padding_len += 1;
+ }
+ swap_layout_indicator.part = format!("{}{}", padding, swap_layout_indicator.part);
+ swap_layout_indicator.len += padding_len;
+ prefix.push(swap_layout_indicator);
+ }
+
prefix
}
+
+fn swap_layout_status(
+ cols: usize,
+ swap_layout_name: &Option<String>,
+ is_swap_layout_dirty: bool,
+ mode_info: &ModeInfo,
+ supports_arrow_fonts: bool,
+) -> Option<LinePart> {
+ match swap_layout_name {
+ Some(swap_layout_name) => {
+ let mode_keybinds = mode_info.get_mode_keybinds();
+ let prev_next_keys = action_key_group(
+ &mode_keybinds,
+ &[&[Action::PreviousSwapLayout], &[Action::NextSwapLayout]],
+ );
+ let mut text = style_key_with_modifier(&prev_next_keys, Some(0));
+ text.append(&ribbon_as_line_part(
+ &swap_layout_name.to_uppercase(),
+ !is_swap_layout_dirty,
+ supports_arrow_fonts,
+ ));
+ Some(text)
+ },
+ None => None,
+ }
+}
+
+pub fn ribbon_as_line_part(text: &str, is_selected: bool, supports_arrow_fonts: bool) -> LinePart {
+ let ribbon_text = if is_selected {
+ Text::new(text).selected()
+ } else {
+ Text::new(text)
+ };
+ let part = serialize_ribbon(&ribbon_text);
+ let mut len = text.width() + 2;
+ if supports_arrow_fonts {
+ len += 2;
+ };
+ LinePart {
+ part,
+ len,
+ tab_index: None,
+ }
+}
+
+pub fn style_key_with_modifier(keyvec: &[KeyWithModifier], color_index: Option<usize>) -> LinePart {
+ if keyvec.is_empty() {
+ return LinePart::default();
+ }
+
+ let common_modifiers = get_common_modifiers(keyvec.iter().collect());
+
+ let no_common_modifier = common_modifiers.is_empty();
+ let modifier_str = common_modifiers
+ .iter()
+ .map(|m| m.to_string())
+ .collect::<Vec<_>>()
+ .join("-");
+
+ // Prints the keys
+ let key = keyvec
+ .iter()
+ .map(|key| {
+ if no_common_modifier || keyvec.len() == 1 {
+ format!("{}", key)
+ } else {
+ format!("{}", key.strip_common_modifiers(&common_modifiers))
+ }
+ })
+ .collect::<Vec<String>>();
+
+ // Special handling of some pre-defined keygroups
+ let key_string = key.join("");
+ let key_separator = match &key_string[..] {
+ "HJKL" => "",
+ "hjkl" => "",
+ "←↓↑→" => "",
+ "←→" => "",
+ "↓↑" => "",
+ "[]" => "",
+ _ => "|",
+ };
+
+ if no_common_modifier || key.len() == 1 {
+ let key_string_text = format!(" {} ", key.join(key_separator));
+ let text = if let Some(color_index) = color_index {
+ Text::new(&key_string_text).color_range(color_index, ..)
+ } else {
+ Text::new(&key_string_text)
+ };
+ LinePart {
+ part: serialize_text(&text),
+ len: key_string_text.width(),
+ ..Default::default()
+ }
+ } else {
+ let key_string_without_modifier = format!("{}", key.join(key_separator));
+ let key_string_text = format!(" {} <{}> ", modifier_str, key_string_without_modifier);
+ let text = if let Some(color_index) = color_index {
+ Text::new(&key_string_text)
+ .color_range(color_index, ..modifier_str.width() + 1)
+ .color_range(
+ color_index,
+ modifier_str.width() + 3
+ ..modifier_str.width() + 3 + key_string_without_modifier.width(),
+ )
+ } else {
+ Text::new(&key_string_text)
+ };
+ LinePart {
+ part: serialize_text(&text),
+ len: key_string_text.width(),
+ ..Default::default()
+ }
+ }
+}
+
+pub fn get_common_modifiers(mut keyvec: Vec<&KeyWithModifier>) -> Vec<KeyModifier> {
+ if keyvec.is_empty() {
+ return vec![];
+ }
+ let mut common_modifiers = keyvec.pop().unwrap().key_modifiers.clone();
+ for key in keyvec {
+ common_modifiers = common_modifiers
+ .intersection(&key.key_modifiers)
+ .cloned()
+ .collect();
+ }
+ common_modifiers.into_iter().collect()
+}
+
+pub fn action_key_group(
+ keymap: &[(KeyWithModifier, Vec<Action>)],
+ actions: &[&[Action]],
+) -> Vec<KeyWithModifier> {
+ let mut ret = vec![];
+ for action in actions {
+ ret.extend(action_key(keymap, action));
+ }
+ ret
+}
+
+pub fn action_key(
+ keymap: &[(KeyWithModifier, Vec<Action>)],
+ action: &[Action],
+) -> Vec<KeyWithModifier> {
+ keymap
+ .iter()
+ .filter_map(|(key, acvec)| {
+ let matching = acvec
+ .iter()
+ .zip(action)
+ .filter(|(a, b)| a.shallow_eq(b))
+ .count();
+
+ if matching == acvec.len() && matching == action.len() {
+ Some(key.clone())
+ } else {
+ None
+ }
+ })
+ .collect::<Vec<KeyWithModifier>>()
+}
diff --git a/default-plugins/tab-bar/src/main.rs b/default-plugins/tab-bar/src/main.rs
index 20855825..8230467d 100644
--- a/default-plugins/tab-bar/src/main.rs
+++ b/default-plugins/tab-bar/src/main.rs
@@ -18,12 +18,20 @@ pub struct LinePart {
tab_index: Option<usize>,
}
+impl LinePart {
+ pub fn append(&mut self, to_append: &LinePart) {
+ self.part.push_str(&to_append.part);
+ self.len += to_append.len;
+ }
+}
+
#[derive(Default)]
struct State {
tabs: Vec<TabInfo>,
active_tab_idx: usize,
mode_info: ModeInfo,
tab_line: Vec<LinePart>,
+ hide_swap_layout_indication: bool,
}
static ARROW_SEPARATOR: &str = "";
@@ -31,7 +39,11 @@ static ARROW_SEPARATOR: &str = "";
register_plugin!(State);
impl ZellijPlugin for State {
- fn load(&mut self, _configuration: BTreeMap<String, String>) {
+ fn load(&mut self, configuration: BTreeMap<String, String>) {
+ self.hide_swap_layout_indication = configuration
+ .get("hide_swap_layout_indication")
+ .map(|s| s == "true")
+ .unwrap_or(false);
set_selectable(false);
subscribe(&[
EventType::TabUpdate,
@@ -120,6 +132,9 @@ impl ZellijPlugin for State {
self.mode_info.style.colors,
self.mode_info.capabilities,
self.mode_info.style.hide_session_name,
+ self.tabs.iter().find(|t| t.active),
+ &self.mode_info,
+ self.hide_swap_layout_indication,
);
let output = self
diff --git a/default-plugins/tab-bar/src/tab.rs b/default-plugins/tab-bar/src/tab.rs
index 4b2ae777..5044ac87 100644
--- a/default-plugins/tab-bar/src/tab.rs
+++ b/default-plugins/tab-bar/src/tab.rs
@@ -51,7 +51,7 @@ pub fn render_tab(
let right_separator = style!(background_color, foreground_color).paint(separator);
let tab_styled_text = if !focused_clients.is_empty() {
let (cursor_section, extra_length) = cursors(focused_clients, palette);
- tab_text_len += extra_length;
+ tab_text_len += extra_length + 2; // 2 for cursor_beginning and cursor_end
let mut s = String::new();
let cursor_beginning = style!(foreground_color, background_color)
.bold()