diff options
| author | Aram Drevekenin <aram@poor.dev> | 2024-07-05 15:13:51 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-07-05 15:13:51 +0200 |
| commit | a6d6c0e4ff03c35cafb6f04363ecab856ee3682d (patch) | |
| tree | eadf1c24c4c6a1e4b7499b3cd047351fd29e52de /default-plugins | |
| parent | docs(changelog): wasmer -> wasmtime (diff) | |
| download | sl0ppij-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.toml | 1 | ||||
| -rw-r--r-- | default-plugins/status-bar/src/first_line.rs | 47 | ||||
| -rw-r--r-- | default-plugins/status-bar/src/main.rs | 30 | ||||
| -rw-r--r-- | default-plugins/status-bar/src/one_line_ui.rs | 1440 | ||||
| -rw-r--r-- | default-plugins/tab-bar/src/line.rs | 211 | ||||
| -rw-r--r-- | default-plugins/tab-bar/src/main.rs | 17 | ||||
| -rw-r--r-- | default-plugins/tab-bar/src/tab.rs | 2 |
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() |
