use std::fmt::{Display, Error, Formatter}; use ansi_term::{ANSIStrings, Style}; use zellij_tile::prelude::actions::Action; use zellij_tile::prelude::*; use zellij_tile_utils::style; const TO_NORMAL: Action = Action::SwitchToMode(InputMode::Normal); #[derive(Default)] pub struct LinePart { part: String, len: usize, } impl Display for LinePart { fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { write!(f, "{}", self.part) } } /// Get key from action pattern(s). /// /// This function takes as arguments a `keymap` that is a `Vec<(Key, Vec<Action>)>` and contains /// all keybindings for the current mode and one or more `p` patterns which match a sequence of /// actions to search for. If within the keymap a sequence of actions matching `p` is found, all /// keys that trigger the action pattern are returned as vector of `Vec<Key>`. pub fn action_key(keymap: &[(Key, Vec<Action>)], action: &[Action]) -> Vec<Key> { keymap .iter() .filter_map(|(key, acvec)| { if acvec.as_slice() == action { Some(*key) } else { None } }) .collect::<Vec<Key>>() } #[derive(Clone, Copy)] pub struct SegmentStyle { pub char_shortcut: Style, } #[derive(Clone, Copy)] pub struct ColoredElements { pub selected: SegmentStyle, pub unselected: SegmentStyle, pub unselected_alternate: SegmentStyle, pub disabled: SegmentStyle, } fn color_elements(palette: Palette) -> ColoredElements { let background = match palette.theme_hue { ThemeHue::Dark => palette.black, ThemeHue::Light => palette.white, }; // let foreground = match palette.theme_hue { // ThemeHue::Dark => palette.white, // ThemeHue::Light => palette.black, // }; let alternate_background_color = palette.black; // if different_color_alternates { // match palette.theme_hue { // ThemeHue::Dark => palette.white, // ThemeHue::Light => palette.black, // } // } else { // palette.fg // }; match palette.source { PaletteSource::Default => ColoredElements { selected: SegmentStyle { char_shortcut: style!(palette.gold, background).bold(), }, unselected: SegmentStyle { char_shortcut: style!(palette.gray, palette.black).bold(), }, unselected_alternate: SegmentStyle { char_shortcut: style!(palette.gray, palette.black).bold(), }, disabled: SegmentStyle { char_shortcut: style!(background, palette.fg).dimmed().italic(), }, }, PaletteSource::Xresources => ColoredElements { selected: SegmentStyle { char_shortcut: style!(palette.red, palette.green).bold(), }, unselected: SegmentStyle { char_shortcut: style!(palette.red, palette.fg).bold(), }, unselected_alternate: SegmentStyle { char_shortcut: style!(palette.red, alternate_background_color).bold(), }, disabled: SegmentStyle { char_shortcut: style!(background, palette.fg).dimmed(), }, }, } } struct KeyShortcut { mode: KeyMode, action: KeyAction, key: Option<Key>, } #[derive(PartialEq)] enum KeyAction { Lock, Pane, Tab, Resize, Search, Quit, Session, Move, Tmux, } enum KeyMode { Unselected, UnselectedAlternate, Selected, Disabled, } impl KeyShortcut { pub fn new(mode: KeyMode, action: KeyAction, key: Option<Key>) -> Self { KeyShortcut { mode, action, key } } pub fn letter_shortcut(&self, with_prefix: bool) -> String { let key = match self.key { Some(k) => k, None => return String::from("?"), }; if with_prefix { format!("{}", key) } else { match key { Key::F(c) => format!("{}", c), Key::Ctrl(c) => format!("{}", c), Key::Char(_) => format!("{}", key), Key::Alt(c) => format!("{}", c), _ => String::from("??"), } } } } /// Generate short mode shortcut tile. /// /// A short mode shortcut tile consists of a leading and trailing `separator` and a keybinding. For /// example, the default short mode shortcut tile for "Locked" mode is: ` g `. /// /// # Arguments /// /// - `key`: A [`KeyShortcut`] that defines how the tile is displayed (active/disabled/...), what /// action it belongs to (roughly equivalent to [`InputMode`]s) and the keybinding to trigger /// this action. /// - `palette`: A structure holding styling information. /// - `separator`: The separator printed before and after the mode shortcut tile. The default is an /// arrow head-like separator. /// - `shared_super`: If set to true, all mode shortcut keybindings share a common modifier (see /// [`get_common_modifier`]) and the modifier belonging to the keybinding is **not** printed in /// the shortcut tile. /// - `first_tile`: If set to true, the leading separator for this tile will be ommited so no gap /// appears on the screen. fn short_mode_shortcut( key: &KeyShortcut, palette: ColoredElements, shared_super: bool, ) -> LinePart { let key_binding = match (&key.mode, &key.key) { (KeyMode::Disabled, None) => "".to_string(), (_, None) => return LinePart::default(), (_, Some(_)) => key.letter_shortcut(!shared_super), }; let colors = match key.mode { KeyMode::Unselected => palette.unselected, KeyMode::UnselectedAlternate => palette.unselected_alternate, KeyMode::Selected => palette.selected, KeyMode::Disabled => palette.disabled, }; let char_shortcut = colors.char_shortcut.paint(format!("{}", key_binding)); LinePart { part: ANSIStrings(&[char_shortcut]).to_string(), len: key_binding.chars().count(), // Key binding } } fn key_indicators(max_len: usize, keys: &[KeyShortcut], palette: ColoredElements) -> LinePart { // Print full-width hints let mut line_part = LinePart { part: String::from(""), len: 0, }; for ctrl_key in keys { let key = short_mode_shortcut(ctrl_key, palette, true); line_part.part = format!("{}{}", line_part.part, key.part); line_part.len += key.len; } if line_part.len < max_len { return line_part; } // Shortened doesn't fit, print nothing LinePart::default() } pub fn to_char(kv: Vec<Key>) -> Option<Key> { let key = kv .iter() .filter(|key| { // These are general "keybindings" to get back to normal, they aren't interesting here. !matches!(key, Key::Char('\n') | Key::Char(' ') | Key::Esc) }) .collect::<Vec<&Key>>() .into_iter() .next(); // Maybe the user bound one of the ignored keys? if key.is_none() { return kv.first().cloned(); } key.cloned() } /// Get the [`KeyShortcut`] for a specific [`InputMode`]. /// /// Iterates over the contents of `shortcuts` to find the [`KeyShortcut`] with the [`KeyAction`] /// matching the [`InputMode`]. Returns a mutable reference to the entry in `shortcuts` if a match /// is found or `None` otherwise. /// /// In case multiple entries in `shortcuts` match `mode` (which shouldn't happen), the first match /// is returned. fn get_key_shortcut_for_mode<'a>( shortcuts: &'a mut [KeyShortcut], mode: &InputMode, ) -> Option<&'a mut KeyShortcut> { let key_action = match mode { InputMode::Normal | InputMode::Prompt | InputMode::Tmux => return None, InputMode::Locked => KeyAction::Lock, InputMode::Pane | InputMode::RenamePane => KeyAction::Pane, InputMode::Tab | InputMode::RenameTab => KeyAction::Tab, InputMode::Resize => KeyAction::Resize, InputMode::Move => KeyAction::Move, InputMode::Scroll | InputMode::Search | InputMode::EnterSearch => KeyAction::Search, InputMode::Session => KeyAction::Session, }; for shortcut in shortcuts.iter_mut() { if shortcut.action == key_action { return Some(shortcut); } } None } pub fn modestatus(help: &ModeInfo, max_len: usize) -> LinePart { let colored_elements = color_elements(help.style.colors); let binds = &help.get_mode_keybinds(); // Unselect all by default let mut default_keys = vec![ KeyShortcut::new( KeyMode::Unselected, KeyAction::Lock, to_char(action_key( binds, &[Action::SwitchToMode(InputMode::Locked)], )), ), KeyShortcut::new( KeyMode::UnselectedAlternate, KeyAction::Pane, to_char(action_key(binds, &[Action::SwitchToMode(InputMode::Pane)])), ), KeyShortcut::new( KeyMode::Unselected, KeyAction::Tab, to_char(action_key(binds, &[Action::SwitchToMode(InputMode::Tab)])), ), KeyShortcut::new( KeyMode::UnselectedAlternate, KeyAction::Resize, to_char(action_key( binds, &[Action::SwitchToMode(InputMode::Resize)], )), ), KeyShortcut::new( KeyMode::Unselected, KeyAction::Move, to_char(action_key(binds, &[Action::SwitchToMode(InputMode::Move)])), ), KeyShortcut::new( KeyMode::UnselectedAlternate, KeyAction::Search, to_char(action_key( binds, &[Action::SwitchToMode(InputMode::Scroll)], )), ), KeyShortcut::new( KeyMode::Unselected, KeyAction::Session, to_char(action_key( binds, &[Action::SwitchToMode(InputMode::Session)], )), ), KeyShortcut::new( KeyMode::UnselectedAlternate, KeyAction::Quit, to_char(action_key(binds, &[Action::Quit])), ), ]; if let Some(key_shortcut) = get_key_shortcut_for_mode(&mut default_keys, &help.mode) { key_shortcut.mode = KeyMode::Selected; key_shortcut.key = to_char(action_key(binds, &[TO_NORMAL])); } // In locked mode we must disable all other mode keybindings if help.mode == InputMode::Locked { for key in default_keys.iter_mut().skip(1) { key.mode = KeyMode::Disabled; } } if help.mode == InputMode::Tmux { // Tmux tile is hidden by default default_keys.push(KeyShortcut::new( KeyMode::Selected, KeyAction::Tmux, to_char(action_key(binds, &[TO_NORMAL])), )); } key_indicators(max_len, &default_keys, colored_elements) }