diff --git a/.nix/default.nix b/.nix/default.nix index c87221d814..07c0f5c185 100644 --- a/.nix/default.nix +++ b/.nix/default.nix @@ -68,6 +68,8 @@ flake-utils.lib.eachSystem [ }; cargo = rustToolchainToml; rustc = rustToolchainToml; + cargoMSRV = msrvToolchain; + rustcMSRV = msrvToolchain; buildInputs = [ # in order to run tests @@ -163,6 +165,7 @@ flake-utils.lib.eachSystem [ license = [licenses.mit]; }; in rec { + packages.default = packages.zellij-native; # crate2nix - better incremental builds, but uses ifd packages.zellij = crate2nixPkgs.callPackage ./crate2nix.nix { inherit @@ -177,7 +180,7 @@ in rec { nativeBuildInputs = nativeBuildInputs ++ defaultPlugins; }; - packages.zellij-msrv = crate2nixMsrvPkgs.callPackage ./crate2nix.nix { + packages.zellij-crate-msrv = crate2nixMsrvPkgs.callPackage ./crate2nix.nix { inherit name src @@ -204,7 +207,25 @@ in rec { ; nativeBuildInputs = nativeBuildInputs ++ defaultPlugins; }; - packages.default = packages.zellij; + # native nixpkgs support - msrv + packages.zellij-msrv = + (pkgs.makeRustPlatform { + cargo = cargoMSRV; + rustc = rustcMSRV; + }) + .buildRustPackage { + inherit + src + name + cargoLock + buildInputs + postInstall + patchPhase + desktopItems + meta + ; + nativeBuildInputs = nativeBuildInputs ++ defaultPlugins; + }; packages.plugins-compact = plugins.compact-bar; packages.plugins-status-bar = plugins.status-bar; diff --git a/.nix/plugins.nix b/.nix/plugins.nix index 270544c956..f3e1c8b7ae 100644 --- a/.nix/plugins.nix +++ b/.nix/plugins.nix @@ -10,7 +10,6 @@ ignoreSource = [ ".git" ".github" - "assets" "docs" "example" "target" diff --git a/Cargo.lock b/Cargo.lock index 4a3d08ab51..eca15cb00a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1677,6 +1677,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "paste" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" + [[package]] name = "percent-encoding" version = "2.1.0" @@ -2049,6 +2055,28 @@ dependencies = [ "winapi", ] +[[package]] +name = "rmp" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44519172358fd6d58656c86ab8e7fbc9e1490c3e8f14d35ed78ca0dd07403c9f" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25786b0d276110195fa3d6f3f31299900cf71dfbd6c28450f3f58a0e7f7a347e" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + [[package]] name = "rustc-demangle" version = "0.1.21" @@ -2301,6 +2329,7 @@ dependencies = [ "colored", "lazy_static", "rand 0.8.5", + "regex", "serde", "serde_json", "thiserror", @@ -3348,7 +3377,6 @@ dependencies = [ "anyhow", "async-std", "backtrace", - "bincode", "clap", "clap_complete", "colored", @@ -3364,6 +3392,7 @@ dependencies = [ "nix", "once_cell", "regex", + "rmp-serde", "serde", "serde_json", "serde_yaml", diff --git a/Makefile.toml b/Makefile.toml index 16057604d0..f8ade7e644 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -18,18 +18,9 @@ dependencies = [ "clippy", ] -# Patching the default flows to skip testing of wasm32-wasi targets -[tasks.pre-test] -condition = { env = { "CARGO_MAKE_CRATE_TARGET_TRIPLE" = "wasm32-wasi" } } -env = { "SKIP_TEST" = true } - [tasks.test] -condition = { env_false = ["SKIP_TEST"] } -dependencies = ["pre-test"] -args = ["test", "--", "@@split(CARGO_MAKE_TASK_ARGS,;)"] - -[tasks.post-test] -env = { "SKIP_TEST" = false } +dependencies = ["get-host-triple"] +args = ["test", "--target", "${CARGO_HOST_TRIPLE}", "--", "@@split(CARGO_MAKE_TASK_ARGS,;)"] # Running Zellij using the development data directory [tasks.run] @@ -125,6 +116,25 @@ env = { "CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS" = [ ] } run_task = { name = "build", fork = true } +[tasks.get-host-triple] +script_runner = "@duckscript" +script = ''' +output = exec rustc -v -V +lines = split ${output.stdout} \n +triple = set "" +for line in ${lines} + if starts_with ${line} "host:" && not is_empty ${line} + bits = split ${line} " " + triple = array_get ${bits} 1 + triple = set ${triple} + end +end + +if not is_empty ${triple} + set_env CARGO_HOST_TRIPLE "${triple}" +end +''' + [tasks.wasm-opt-plugins] dependencies = ["build-plugins-release"] script_runner = "@duckscript" diff --git a/default-plugins/status-bar/Cargo.toml b/default-plugins/status-bar/Cargo.toml index 4c611dc881..a2ca3175a4 100644 --- a/default-plugins/status-bar/Cargo.toml +++ b/default-plugins/status-bar/Cargo.toml @@ -15,3 +15,6 @@ serde_json = "1.0" thiserror = "1.0.30" zellij-tile = { path = "../../zellij-tile" } zellij-tile-utils = { path = "../../zellij-tile-utils" } + +[dev-dependencies] +regex = "1" diff --git a/default-plugins/status-bar/src/first_line.rs b/default-plugins/status-bar/src/first_line.rs index 6c10b8832c..ae8fc70e73 100644 --- a/default-plugins/status-bar/src/first_line.rs +++ b/default-plugins/status-bar/src/first_line.rs @@ -1,21 +1,19 @@ use ansi_term::ANSIStrings; +use zellij_tile::prelude::actions::Action; use zellij_tile::prelude::*; use crate::color_elements; +use crate::{action_key, get_common_modifier, TO_NORMAL}; use crate::{ColoredElements, LinePart}; -struct CtrlKeyShortcut { - mode: CtrlKeyMode, - action: CtrlKeyAction, +struct KeyShortcut { + mode: KeyMode, + action: KeyAction, + key: Option, } -impl CtrlKeyShortcut { - pub fn new(mode: CtrlKeyMode, action: CtrlKeyAction) -> Self { - CtrlKeyShortcut { mode, action } - } -} - -enum CtrlKeyAction { +#[derive(PartialEq)] +enum KeyAction { Lock, Pane, Tab, @@ -24,114 +22,103 @@ enum CtrlKeyAction { Quit, Session, Move, + Tmux, } -enum CtrlKeyMode { +enum KeyMode { Unselected, UnselectedAlternate, Selected, Disabled, } -impl CtrlKeyShortcut { +impl KeyShortcut { + pub fn new(mode: KeyMode, action: KeyAction, key: Option) -> Self { + KeyShortcut { mode, action, key } + } + pub fn full_text(&self) -> String { match self.action { - CtrlKeyAction::Lock => String::from("LOCK"), - CtrlKeyAction::Pane => String::from("PANE"), - CtrlKeyAction::Tab => String::from("TAB"), - CtrlKeyAction::Resize => String::from("RESIZE"), - CtrlKeyAction::Search => String::from("SEARCH"), - CtrlKeyAction::Quit => String::from("QUIT"), - CtrlKeyAction::Session => String::from("SESSION"), - CtrlKeyAction::Move => String::from("MOVE"), + KeyAction::Lock => String::from("LOCK"), + KeyAction::Pane => String::from("PANE"), + KeyAction::Tab => String::from("TAB"), + KeyAction::Resize => String::from("RESIZE"), + KeyAction::Search => String::from("SEARCH"), + KeyAction::Quit => String::from("QUIT"), + KeyAction::Session => String::from("SESSION"), + KeyAction::Move => String::from("MOVE"), + KeyAction::Tmux => String::from("TMUX"), } } - pub fn letter_shortcut(&self) -> char { - match self.action { - CtrlKeyAction::Lock => 'g', - CtrlKeyAction::Pane => 'p', - CtrlKeyAction::Tab => 't', - CtrlKeyAction::Resize => 'n', - CtrlKeyAction::Search => 's', - CtrlKeyAction::Quit => 'q', - CtrlKeyAction::Session => 'o', - CtrlKeyAction::Move => 'h', + 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("??"), + } } } } -fn unselected_mode_shortcut( - letter: char, - text: &str, +/// Generate long mode shortcut tile. +/// +/// A long mode shortcut tile consists of a leading and trailing `separator`, a keybinding enclosed +/// in `<>` brackets and the name of the mode displayed in capitalized letters next to it. For +/// example, the default long mode shortcut tile for "Locked" mode is: ` LOCK `. +/// +/// # 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 long_mode_shortcut( + key: &KeyShortcut, palette: ColoredElements, separator: &str, + shared_super: bool, + first_tile: bool, ) -> LinePart { - let prefix_separator = palette.unselected_prefix_separator.paint(separator); - let char_left_separator = palette.unselected_char_left_separator.paint(" <"); - let char_shortcut = palette.unselected_char_shortcut.paint(letter.to_string()); - let char_right_separator = palette.unselected_char_right_separator.paint(">"); - let styled_text = palette.unselected_styled_text.paint(format!("{} ", text)); - let suffix_separator = palette.unselected_suffix_separator.paint(separator); - LinePart { - part: ANSIStrings(&[ - prefix_separator, - char_left_separator, - char_shortcut, - char_right_separator, - styled_text, - suffix_separator, - ]) - .to_string(), - len: text.chars().count() + 7, // 2 for the arrows, 3 for the char separators, 1 for the character, 1 for the text padding - } -} - -fn unselected_alternate_mode_shortcut( - letter: char, - text: &str, - palette: ColoredElements, - separator: &str, -) -> LinePart { - let prefix_separator = palette - .unselected_alternate_prefix_separator - .paint(separator); - let char_left_separator = palette.unselected_alternate_char_left_separator.paint(" <"); - let char_shortcut = palette - .unselected_alternate_char_shortcut - .paint(letter.to_string()); - let char_right_separator = palette.unselected_alternate_char_right_separator.paint(">"); - let styled_text = palette - .unselected_alternate_styled_text - .paint(format!("{} ", text)); - let suffix_separator = palette - .unselected_alternate_suffix_separator - .paint(separator); - LinePart { - part: ANSIStrings(&[ - prefix_separator, - char_left_separator, - char_shortcut, - char_right_separator, - styled_text, - suffix_separator, - ]) - .to_string(), - len: text.chars().count() + 7, // 2 for the arrows, 3 for the char separators, 1 for the character, 1 for the text padding - } -} + let key_hint = key.full_text(); + let key_binding = match (&key.mode, &key.key) { + (KeyMode::Disabled, None) => "".to_string(), + (_, None) => return LinePart::default(), + (_, Some(_)) => key.letter_shortcut(!shared_super), + }; -fn selected_mode_shortcut( - letter: char, - text: &str, - palette: ColoredElements, - separator: &str, -) -> LinePart { - let prefix_separator = palette.selected_prefix_separator.paint(separator); - let char_left_separator = palette.selected_char_left_separator.paint(" <".to_string()); - let char_shortcut = palette.selected_char_shortcut.paint(letter.to_string()); - let char_right_separator = palette.selected_char_right_separator.paint(">".to_string()); - let styled_text = palette.selected_styled_text.paint(format!("{} ", text)); - let suffix_separator = palette.selected_suffix_separator.paint(separator); + let colors = match key.mode { + KeyMode::Unselected => palette.unselected, + KeyMode::UnselectedAlternate => palette.unselected_alternate, + KeyMode::Selected => palette.selected, + KeyMode::Disabled => palette.disabled, + }; + let start_separator = if !shared_super && first_tile { + "" + } else { + separator + }; + let prefix_separator = colors.prefix_separator.paint(start_separator); + let char_left_separator = colors.char_left_separator.paint(" <".to_string()); + let char_shortcut = colors.char_shortcut.paint(key_binding.to_string()); + let char_right_separator = colors.char_right_separator.paint("> ".to_string()); + let styled_text = colors.styled_text.paint(format!("{} ", key_hint)); + let suffix_separator = colors.suffix_separator.paint(separator); LinePart { part: ANSIStrings(&[ prefix_separator, @@ -142,318 +129,713 @@ fn selected_mode_shortcut( suffix_separator, ]) .to_string(), - len: text.chars().count() + 7, // 2 for the arrows, 3 for the char separators, 1 for the character, 1 for the text padding + len: start_separator.chars().count() // Separator + + 2 // " <" + + key_binding.chars().count() // Key binding + + 2 // "> " + + key_hint.chars().count() // Key hint (mode) + + 1 // " " + + separator.chars().count(), // Separator } } -fn disabled_mode_shortcut(text: &str, palette: ColoredElements, separator: &str) -> LinePart { - let prefix_separator = palette.disabled_prefix_separator.paint(separator); - let styled_text = palette.disabled_styled_text.paint(format!("{} ", text)); - let suffix_separator = palette.disabled_suffix_separator.paint(separator); - LinePart { - part: format!("{}{}{}", prefix_separator, styled_text, suffix_separator), - len: text.chars().count() + 2 + 1, // 2 for the arrows, 1 for the padding in the end - } -} - -fn selected_mode_shortcut_single_letter( - letter: char, - palette: ColoredElements, - separator: &str, -) -> LinePart { - let char_shortcut_text = format!(" {} ", letter); - let len = char_shortcut_text.chars().count() + 4; // 2 for the arrows, 2 for the padding - let prefix_separator = palette - .selected_single_letter_prefix_separator - .paint(separator); - let char_shortcut = palette - .selected_single_letter_char_shortcut - .paint(char_shortcut_text); - let suffix_separator = palette - .selected_single_letter_suffix_separator - .paint(separator); - LinePart { - part: ANSIStrings(&[prefix_separator, char_shortcut, suffix_separator]).to_string(), - len, - } -} - -fn unselected_mode_shortcut_single_letter( - letter: char, +/// 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, separator: &str, + shared_super: bool, + first_tile: bool, ) -> LinePart { - let char_shortcut_text = format!(" {} ", letter); - let len = char_shortcut_text.chars().count() + 4; // 2 for the arrows, 2 for the padding - let prefix_separator = palette - .unselected_single_letter_prefix_separator - .paint(separator); - let char_shortcut = palette - .unselected_single_letter_char_shortcut - .paint(char_shortcut_text); - let suffix_separator = palette - .unselected_single_letter_suffix_separator - .paint(separator); - LinePart { - part: ANSIStrings(&[prefix_separator, char_shortcut, suffix_separator]).to_string(), - len, - } -} + let key_binding = match (&key.mode, &key.key) { + (KeyMode::Disabled, None) => "".to_string(), + (_, None) => return LinePart::default(), + (_, Some(_)) => key.letter_shortcut(!shared_super), + }; -fn unselected_alternate_mode_shortcut_single_letter( - letter: char, - palette: ColoredElements, - separator: &str, -) -> LinePart { - let char_shortcut_text = format!(" {} ", letter); - let len = char_shortcut_text.chars().count() + 4; // 2 for the arrows, 2 for the padding - let prefix_separator = palette - .unselected_alternate_single_letter_prefix_separator - .paint(separator); - let char_shortcut = palette - .unselected_alternate_single_letter_char_shortcut - .paint(char_shortcut_text); - let suffix_separator = palette - .unselected_alternate_single_letter_suffix_separator - .paint(separator); + let colors = match key.mode { + KeyMode::Unselected => palette.unselected, + KeyMode::UnselectedAlternate => palette.unselected_alternate, + KeyMode::Selected => palette.selected, + KeyMode::Disabled => palette.disabled, + }; + let start_separator = if !shared_super && first_tile { + "" + } else { + separator + }; + let prefix_separator = colors.prefix_separator.paint(start_separator); + let char_shortcut = colors.char_shortcut.paint(format!(" {} ", key_binding)); + let suffix_separator = colors.suffix_separator.paint(separator); LinePart { part: ANSIStrings(&[prefix_separator, char_shortcut, suffix_separator]).to_string(), - len, - } -} - -fn full_ctrl_key(key: &CtrlKeyShortcut, palette: ColoredElements, separator: &str) -> LinePart { - let full_text = key.full_text(); - let letter_shortcut = key.letter_shortcut(); - match key.mode { - CtrlKeyMode::Unselected => unselected_mode_shortcut( - letter_shortcut, - &format!(" {}", full_text), - palette, - separator, - ), - CtrlKeyMode::UnselectedAlternate => unselected_alternate_mode_shortcut( - letter_shortcut, - &format!(" {}", full_text), - palette, - separator, - ), - CtrlKeyMode::Selected => selected_mode_shortcut( - letter_shortcut, - &format!(" {}", full_text), - palette, - separator, - ), - CtrlKeyMode::Disabled => disabled_mode_shortcut( - &format!(" <{}> {}", letter_shortcut, full_text), - palette, - separator, - ), - } -} - -fn single_letter_ctrl_key( - key: &CtrlKeyShortcut, - palette: ColoredElements, - separator: &str, -) -> LinePart { - let letter_shortcut = key.letter_shortcut(); - match key.mode { - CtrlKeyMode::Unselected => { - unselected_mode_shortcut_single_letter(letter_shortcut, palette, separator) - }, - CtrlKeyMode::UnselectedAlternate => { - unselected_alternate_mode_shortcut_single_letter(letter_shortcut, palette, separator) - }, - CtrlKeyMode::Selected => { - selected_mode_shortcut_single_letter(letter_shortcut, palette, separator) - }, - CtrlKeyMode::Disabled => { - disabled_mode_shortcut(&format!(" {}", letter_shortcut), palette, separator) - }, + len: separator.chars().count() // Separator + + 1 // " " + + key_binding.chars().count() // Key binding + + 1 // " " + + separator.chars().count(), // Separator } } fn key_indicators( max_len: usize, - keys: &[CtrlKeyShortcut], + keys: &[KeyShortcut], palette: ColoredElements, separator: &str, + mode_info: &ModeInfo, ) -> LinePart { - let mut line_part = LinePart::default(); + // Print full-width hints + let mut line_part = superkey(palette, separator, mode_info); + let shared_super = line_part.len > 0; for ctrl_key in keys { - let key = full_ctrl_key(ctrl_key, palette, separator); + let line_empty = line_part.len == 0; + let key = long_mode_shortcut(ctrl_key, palette, separator, shared_super, line_empty); line_part.part = format!("{}{}", line_part.part, key.part); line_part.len += key.len; } if line_part.len < max_len { return line_part; } - line_part = LinePart::default(); + + // Full-width doesn't fit, try shortened hints (just keybindings, no meanings/actions) + line_part = superkey(palette, separator, mode_info); + let shared_super = line_part.len > 0; for ctrl_key in keys { - let key = single_letter_ctrl_key(ctrl_key, palette, separator); + let line_empty = line_part.len == 0; + let key = short_mode_shortcut(ctrl_key, palette, separator, shared_super, line_empty); 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 line_part = LinePart::default(); line_part } -pub fn superkey(palette: ColoredElements, separator: &str) -> LinePart { - let prefix_text = if separator.is_empty() { - " Ctrl + " - } else { - " Ctrl +" +/// Get the keybindings for switching `InputMode`s and `Quit` visible in status bar. +/// +/// Return a Vector of `Key`s where each `Key` is a shortcut to switch to some `InputMode` or Quit +/// zellij. Given the vast amount of things a user can configure in their zellij config, this +/// function has some limitations to keep in mind: +/// +/// - The vector is not deduplicated: If switching to a certain `InputMode` is bound to multiple +/// `Key`s, all of these bindings will be part of the returned vector. There is also no +/// guaranteed sort order. Which key ends up in the status bar in such a situation isn't defined. +/// - The vector will **not** contain the ' ', '\n' and 'Esc' keys: These are the default bindings +/// to get back to normal mode from any input mode, but they aren't of interest when searching +/// for the super key. If for any input mode the user has bound only these keys to switching back +/// to `InputMode::Normal`, a '?' will be displayed as keybinding instead. +pub fn mode_switch_keys(mode_info: &ModeInfo) -> Vec { + mode_info + .get_mode_keybinds() + .iter() + .filter_map(|(key, vac)| match vac.first() { + // No actions defined, ignore + None => None, + Some(vac) => { + // We ignore certain "default" keybindings that switch back to normal InputMode. + // These include: ' ', '\n', 'Esc' + if matches!(key, Key::Char(' ') | Key::Char('\n') | Key::Esc) { + return None; + } + if let actions::Action::SwitchToMode(mode) = vac { + return match mode { + // Store the keys that switch to displayed modes + InputMode::Normal + | InputMode::Locked + | InputMode::Pane + | InputMode::Tab + | InputMode::Resize + | InputMode::Move + | InputMode::Scroll + | InputMode::Session => Some(*key), + _ => None, + }; + } + if let actions::Action::Quit = vac { + return Some(*key); + } + // Not a `SwitchToMode` or `Quit` action, ignore + None + }, + }) + .collect() +} + +pub fn superkey(palette: ColoredElements, separator: &str, mode_info: &ModeInfo) -> LinePart { + // Find a common modifier if any + let prefix_text = match get_common_modifier(mode_switch_keys(mode_info).iter().collect()) { + Some(text) => { + if mode_info.capabilities.arrow_fonts { + // Add extra space in simplified ui + format!(" {} + ", text) + } else { + format!(" {} +", text) + } + }, + _ => return LinePart::default(), }; - let prefix = palette.superkey_prefix.paint(prefix_text); + + let prefix = palette.superkey_prefix.paint(&prefix_text); let suffix_separator = palette.superkey_suffix_separator.paint(separator); LinePart { part: ANSIStrings(&[prefix, suffix_separator]).to_string(), - len: prefix_text.chars().count(), + len: prefix_text.chars().count() + separator.chars().count(), + } +} + +pub fn to_char(kv: Vec) -> Option { + 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::>() + .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 ctrl_keys(help: &ModeInfo, max_len: usize, separator: &str) -> LinePart { +pub fn first_line(help: &ModeInfo, max_len: usize, separator: &str) -> LinePart { let supports_arrow_fonts = !help.capabilities.arrow_fonts; let colored_elements = color_elements(help.style.colors, !supports_arrow_fonts); - match &help.mode { - InputMode::Locked => key_indicators( - max_len, - &[ - CtrlKeyShortcut::new(CtrlKeyMode::Selected, CtrlKeyAction::Lock), - CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Pane), - CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Tab), - CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Resize), - CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Move), - CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Search), - CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Session), - CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Quit), - ], - colored_elements, - separator, + 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)], + )), ), - InputMode::Resize => key_indicators( - max_len, - &[ - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Lock), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Pane), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Tab), - CtrlKeyShortcut::new(CtrlKeyMode::Selected, CtrlKeyAction::Resize), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Move), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Search), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Quit), - ], - colored_elements, - separator, + KeyShortcut::new( + KeyMode::UnselectedAlternate, + KeyAction::Pane, + to_char(action_key(binds, &[Action::SwitchToMode(InputMode::Pane)])), ), - InputMode::Pane | InputMode::RenamePane => key_indicators( - max_len, - &[ - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Lock), - CtrlKeyShortcut::new(CtrlKeyMode::Selected, CtrlKeyAction::Pane), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Tab), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Resize), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Move), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Search), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Quit), - ], - colored_elements, - separator, + KeyShortcut::new( + KeyMode::Unselected, + KeyAction::Tab, + to_char(action_key(binds, &[Action::SwitchToMode(InputMode::Tab)])), ), - InputMode::Tab | InputMode::RenameTab => key_indicators( - max_len, - &[ - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Lock), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Pane), - CtrlKeyShortcut::new(CtrlKeyMode::Selected, CtrlKeyAction::Tab), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Resize), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Move), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Search), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Quit), - ], - colored_elements, - separator, + KeyShortcut::new( + KeyMode::UnselectedAlternate, + KeyAction::Resize, + to_char(action_key( + binds, + &[Action::SwitchToMode(InputMode::Resize)], + )), ), - InputMode::EnterSearch | InputMode::Scroll | InputMode::Search => key_indicators( - max_len, - &[ - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Lock), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Pane), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Tab), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Resize), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Move), - CtrlKeyShortcut::new(CtrlKeyMode::Selected, CtrlKeyAction::Search), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Quit), - ], - colored_elements, - separator, + KeyShortcut::new( + KeyMode::Unselected, + KeyAction::Move, + to_char(action_key(binds, &[Action::SwitchToMode(InputMode::Move)])), ), - InputMode::Move => key_indicators( - max_len, - &[ - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Lock), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Pane), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Tab), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Resize), - CtrlKeyShortcut::new(CtrlKeyMode::Selected, CtrlKeyAction::Move), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Search), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Quit), - ], - colored_elements, - separator, + KeyShortcut::new( + KeyMode::UnselectedAlternate, + KeyAction::Search, + to_char(action_key( + binds, + &[Action::SwitchToMode(InputMode::Scroll)], + )), ), - InputMode::Normal | InputMode::Prompt => key_indicators( - max_len, - &[ - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Lock), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Pane), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Tab), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Resize), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Move), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Search), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Quit), - ], - colored_elements, - separator, + KeyShortcut::new( + KeyMode::Unselected, + KeyAction::Session, + to_char(action_key( + binds, + &[Action::SwitchToMode(InputMode::Session)], + )), ), - InputMode::Session => key_indicators( - max_len, - &[ - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Lock), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Pane), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Tab), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Resize), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Move), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Search), - CtrlKeyShortcut::new(CtrlKeyMode::Selected, CtrlKeyAction::Session), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Quit), - ], - colored_elements, - separator, + KeyShortcut::new( + KeyMode::UnselectedAlternate, + KeyAction::Quit, + to_char(action_key(binds, &[Action::Quit])), ), - InputMode::Tmux => key_indicators( - max_len, - &[ - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Lock), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Pane), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Tab), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Resize), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Move), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Search), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::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, separator, help) +} + +#[cfg(test)] +/// Unit tests. +/// +/// Note that we cheat a little here, because the number of things one may want to test is endless, +/// and creating a Mockup of [`ModeInfo`] by hand for all these testcases is nothing less than +/// torture. Hence, we test the most atomic units thoroughly ([`long_mode_shortcut`] and +/// [`short_mode_shortcut`]) and then test the public API ([`first_line`]) to ensure correct +/// operation. +mod tests { + use super::*; + + fn colored_elements() -> ColoredElements { + let palette = Palette::default(); + color_elements(palette, false) + } + + // Strip style information from `LinePart` and return a raw String instead + fn unstyle(line_part: LinePart) -> String { + let string = line_part.to_string(); + + let re = regex::Regex::new(r"\x1b\[[0-9;]*m").unwrap(); + let string = re.replace_all(&string, "".to_string()); + + string.to_string() + } + + #[test] + fn long_mode_shortcut_selected_with_binding() { + let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, Some(Key::Char('0'))); + let color = colored_elements(); + + let ret = long_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "+ <0> SESSION +".to_string()); + } + + #[test] + // Displayed like selected(alternate), but different styling + fn long_mode_shortcut_unselected_with_binding() { + let key = KeyShortcut::new( + KeyMode::Unselected, + KeyAction::Session, + Some(Key::Char('0')), + ); + let color = colored_elements(); + + let ret = long_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "+ <0> SESSION +".to_string()); + } + + #[test] + // Treat exactly like "unselected" variant + fn long_mode_shortcut_unselected_alternate_with_binding() { + let key = KeyShortcut::new( + KeyMode::UnselectedAlternate, + KeyAction::Session, + Some(Key::Char('0')), + ); + let color = colored_elements(); + + let ret = long_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "+ <0> SESSION +".to_string()); + } + + #[test] + // KeyShortcuts without binding are only displayed when "disabled" (for locked mode indications) + fn long_mode_shortcut_selected_without_binding() { + let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, None); + let color = colored_elements(); + + let ret = long_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "".to_string()); + } + + #[test] + // First tile doesn't print a starting separator + fn long_mode_shortcut_selected_with_binding_first_tile() { + let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, Some(Key::Char('0'))); + let color = colored_elements(); + + let ret = long_mode_shortcut(&key, color, "+", false, true); + let ret = unstyle(ret); + + assert_eq!(ret, " <0> SESSION +".to_string()); + } + + #[test] + // Modifier is the superkey, mustn't appear in angled brackets + fn long_mode_shortcut_selected_with_ctrl_binding_shared_superkey() { + let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, Some(Key::Ctrl('0'))); + let color = colored_elements(); + + let ret = long_mode_shortcut(&key, color, "+", true, false); + let ret = unstyle(ret); + + assert_eq!(ret, "+ <0> SESSION +".to_string()); + } + + #[test] + // Modifier must be in the angled brackets + fn long_mode_shortcut_selected_with_ctrl_binding_no_shared_superkey() { + let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, Some(Key::Ctrl('0'))); + let color = colored_elements(); + + let ret = long_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "+ SESSION +".to_string()); + } + + #[test] + // Must be displayed as usual, but it is styled to be greyed out which we don't test here + fn long_mode_shortcut_disabled_with_binding() { + let key = KeyShortcut::new(KeyMode::Disabled, KeyAction::Session, Some(Key::Char('0'))); + let color = colored_elements(); + + let ret = long_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "+ <0> SESSION +".to_string()); + } + + #[test] + // Must be displayed but without keybinding + fn long_mode_shortcut_disabled_without_binding() { + let key = KeyShortcut::new(KeyMode::Disabled, KeyAction::Session, None); + let color = colored_elements(); + + let ret = long_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "+ <> SESSION +".to_string()); + } + + #[test] + // Test all at once + // Note that when "shared_super" is true, the tile **cannot** be the first on the line, so we + // ignore **first** here. + fn long_mode_shortcut_selected_with_ctrl_binding_and_shared_super_and_first_tile() { + let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, Some(Key::Ctrl('0'))); + let color = colored_elements(); + + let ret = long_mode_shortcut(&key, color, "+", true, true); + let ret = unstyle(ret); + + assert_eq!(ret, "+ <0> SESSION +".to_string()); + } + + #[test] + fn short_mode_shortcut_selected_with_binding() { + let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, Some(Key::Char('0'))); + let color = colored_elements(); + + let ret = short_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "+ 0 +".to_string()); + } + + #[test] + fn short_mode_shortcut_selected_with_ctrl_binding_no_shared_super() { + let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, Some(Key::Ctrl('0'))); + let color = colored_elements(); + + let ret = short_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "+ Ctrl+0 +".to_string()); + } + + #[test] + fn short_mode_shortcut_selected_with_ctrl_binding_shared_super() { + let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, Some(Key::Ctrl('0'))); + let color = colored_elements(); + + let ret = short_mode_shortcut(&key, color, "+", true, false); + let ret = unstyle(ret); + + assert_eq!(ret, "+ 0 +".to_string()); + } + + #[test] + fn short_mode_shortcut_selected_with_binding_first_tile() { + let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, Some(Key::Char('0'))); + let color = colored_elements(); + + let ret = short_mode_shortcut(&key, color, "+", false, true); + let ret = unstyle(ret); + + assert_eq!(ret, " 0 +".to_string()); + } + + #[test] + fn short_mode_shortcut_unselected_with_binding() { + let key = KeyShortcut::new( + KeyMode::Unselected, + KeyAction::Session, + Some(Key::Char('0')), + ); + let color = colored_elements(); + + let ret = short_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "+ 0 +".to_string()); + } + + #[test] + fn short_mode_shortcut_unselected_alternate_with_binding() { + let key = KeyShortcut::new( + KeyMode::UnselectedAlternate, + KeyAction::Session, + Some(Key::Char('0')), + ); + let color = colored_elements(); + + let ret = short_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "+ 0 +".to_string()); + } + + #[test] + fn short_mode_shortcut_disabled_with_binding() { + let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, Some(Key::Char('0'))); + let color = colored_elements(); + + let ret = short_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "+ 0 +".to_string()); + } + + #[test] + fn short_mode_shortcut_selected_without_binding() { + let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, None); + let color = colored_elements(); + + let ret = short_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "".to_string()); + } + + #[test] + fn short_mode_shortcut_unselected_without_binding() { + let key = KeyShortcut::new(KeyMode::Unselected, KeyAction::Session, None); + let color = colored_elements(); + + let ret = short_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "".to_string()); + } + + #[test] + fn short_mode_shortcut_unselected_alternate_without_binding() { + let key = KeyShortcut::new(KeyMode::UnselectedAlternate, KeyAction::Session, None); + let color = colored_elements(); + + let ret = short_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "".to_string()); + } + + #[test] + fn short_mode_shortcut_disabled_without_binding() { + let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, None); + let color = colored_elements(); + + let ret = short_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "".to_string()); + } + + #[test] + // Observe: Modes missing in between aren't displayed! + fn first_line_default_layout_shared_super() { + #[rustfmt::skip] + let mode_info = ModeInfo{ + mode: InputMode::Normal, + keybinds : vec![ + (InputMode::Normal, vec![ + (Key::Ctrl('a'), vec![Action::SwitchToMode(InputMode::Pane)]), + (Key::Ctrl('b'), vec![Action::SwitchToMode(InputMode::Resize)]), + (Key::Ctrl('c'), vec![Action::SwitchToMode(InputMode::Move)]), + ]), ], - colored_elements, - separator, - ), + ..ModeInfo::default() + }; + + let ret = first_line(&mode_info, 500, ">"); + let ret = unstyle(ret); + + assert_eq!( + ret, + " Ctrl + >> PANE >> RESIZE >> MOVE >".to_string() + ); + } + + #[test] + fn first_line_default_layout_no_shared_super() { + #[rustfmt::skip] + let mode_info = ModeInfo{ + mode: InputMode::Normal, + keybinds : vec![ + (InputMode::Normal, vec![ + (Key::Ctrl('a'), vec![Action::SwitchToMode(InputMode::Pane)]), + (Key::Ctrl('b'), vec![Action::SwitchToMode(InputMode::Resize)]), + (Key::Char('c'), vec![Action::SwitchToMode(InputMode::Move)]), + ]), + ], + ..ModeInfo::default() + }; + + let ret = first_line(&mode_info, 500, ">"); + let ret = unstyle(ret); + + assert_eq!( + ret, + " PANE >> RESIZE >> MOVE >".to_string() + ); + } + + #[test] + fn first_line_default_layout_unprintables() { + #[rustfmt::skip] + let mode_info = ModeInfo{ + mode: InputMode::Normal, + keybinds : vec![ + (InputMode::Normal, vec![ + (Key::Ctrl('a'), vec![Action::SwitchToMode(InputMode::Locked)]), + (Key::Backspace, vec![Action::SwitchToMode(InputMode::Pane)]), + (Key::Char('\n'), vec![Action::SwitchToMode(InputMode::Tab)]), + (Key::Char('\t'), vec![Action::SwitchToMode(InputMode::Resize)]), + (Key::Left, vec![Action::SwitchToMode(InputMode::Move)]), + ]), + ], + ..ModeInfo::default() + }; + + let ret = first_line(&mode_info, 500, ">"); + let ret = unstyle(ret); + + assert_eq!( + ret, + " LOCK >> PANE >> TAB >> RESIZE >> <←> MOVE >" + .to_string() + ); + } + + #[test] + fn first_line_short_layout_shared_super() { + #[rustfmt::skip] + let mode_info = ModeInfo{ + mode: InputMode::Normal, + keybinds : vec![ + (InputMode::Normal, vec![ + (Key::Ctrl('a'), vec![Action::SwitchToMode(InputMode::Locked)]), + (Key::Ctrl('b'), vec![Action::SwitchToMode(InputMode::Pane)]), + (Key::Ctrl('c'), vec![Action::SwitchToMode(InputMode::Tab)]), + (Key::Ctrl('d'), vec![Action::SwitchToMode(InputMode::Resize)]), + (Key::Ctrl('e'), vec![Action::SwitchToMode(InputMode::Move)]), + ]), + ], + ..ModeInfo::default() + }; + + let ret = first_line(&mode_info, 50, ">"); + let ret = unstyle(ret); + + assert_eq!(ret, " Ctrl + >> a >> b >> c >> d >> e >".to_string()); + } + + #[test] + fn first_line_short_simplified_ui_shared_super() { + #[rustfmt::skip] + let mode_info = ModeInfo{ + mode: InputMode::Normal, + keybinds : vec![ + (InputMode::Normal, vec![ + (Key::Ctrl('a'), vec![Action::SwitchToMode(InputMode::Pane)]), + (Key::Ctrl('b'), vec![Action::SwitchToMode(InputMode::Resize)]), + (Key::Ctrl('c'), vec![Action::SwitchToMode(InputMode::Move)]), + ]), + ], + ..ModeInfo::default() + }; + + let ret = first_line(&mode_info, 30, ""); + let ret = unstyle(ret); + + assert_eq!(ret, " Ctrl + a b c ".to_string()); } } diff --git a/default-plugins/status-bar/src/main.rs b/default-plugins/status-bar/src/main.rs index 23ee3de769..38728d254c 100644 --- a/default-plugins/status-bar/src/main.rs +++ b/default-plugins/status-bar/src/main.rs @@ -2,13 +2,18 @@ mod first_line; mod second_line; mod tip; -use ansi_term::Style; +use ansi_term::{ + ANSIString, + Colour::{Fixed, RGB}, + Style, +}; use std::fmt::{Display, Error, Formatter}; +use zellij_tile::prelude::actions::Action; use zellij_tile::prelude::*; -use zellij_tile_utils::style; +use zellij_tile_utils::{palette_match, style}; -use first_line::{ctrl_keys, superkey}; +use first_line::first_line; 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, @@ -19,6 +24,8 @@ use tip::utils::get_cached_tip_name; // for more of these, copy paste from: https://en.wikipedia.org/wiki/Box-drawing_character static ARROW_SEPARATOR: &str = ""; static MORE_MSG: &str = " ... "; +/// Shorthand for `Action::SwitchToMode(InputMode::Normal)`. +const TO_NORMAL: Action = Action::SwitchToMode(InputMode::Normal); #[derive(Default)] struct State { @@ -45,48 +52,25 @@ impl Display for LinePart { #[derive(Clone, Copy)] pub struct ColoredElements { - // selected mode - pub selected_prefix_separator: Style, - pub selected_char_left_separator: Style, - pub selected_char_shortcut: Style, - pub selected_char_right_separator: Style, - pub selected_styled_text: Style, - pub selected_suffix_separator: Style, - // unselected mode - pub unselected_prefix_separator: Style, - pub unselected_char_left_separator: Style, - pub unselected_char_shortcut: Style, - pub unselected_char_right_separator: Style, - pub unselected_styled_text: Style, - pub unselected_suffix_separator: Style, - // unselected mode alternate color - pub unselected_alternate_prefix_separator: Style, - pub unselected_alternate_char_left_separator: Style, - pub unselected_alternate_char_shortcut: Style, - pub unselected_alternate_char_right_separator: Style, - pub unselected_alternate_styled_text: Style, - pub unselected_alternate_suffix_separator: Style, - // disabled mode - pub disabled_prefix_separator: Style, - pub disabled_styled_text: Style, - pub disabled_suffix_separator: Style, - // selected single letter - pub selected_single_letter_prefix_separator: Style, - pub selected_single_letter_char_shortcut: Style, - pub selected_single_letter_suffix_separator: Style, - // unselected single letter - pub unselected_single_letter_prefix_separator: Style, - pub unselected_single_letter_char_shortcut: Style, - pub unselected_single_letter_suffix_separator: Style, - // unselected alternate single letter - pub unselected_alternate_single_letter_prefix_separator: Style, - pub unselected_alternate_single_letter_char_shortcut: Style, - pub unselected_alternate_single_letter_suffix_separator: Style, + pub selected: SegmentStyle, + pub unselected: SegmentStyle, + pub unselected_alternate: SegmentStyle, + pub disabled: SegmentStyle, // superkey pub superkey_prefix: Style, pub superkey_suffix_separator: Style, } +#[derive(Clone, Copy)] +pub struct SegmentStyle { + pub prefix_separator: Style, + pub char_left_separator: Style, + pub char_shortcut: Style, + pub char_right_separator: Style, + pub styled_text: Style, + pub suffix_separator: Style, +} + // I really hate this, but I can't come up with a good solution for this, // we need different colors from palette for the default theme // plus here we can add new sources in the future, like Theme @@ -110,109 +94,74 @@ fn color_elements(palette: Palette, different_color_alternates: bool) -> Colored }; match palette.source { PaletteSource::Default => ColoredElements { - selected_prefix_separator: style!(background, palette.green), - selected_char_left_separator: style!(background, palette.green).bold(), - selected_char_shortcut: style!(palette.red, palette.green).bold(), - selected_char_right_separator: style!(background, palette.green).bold(), - selected_styled_text: style!(background, palette.green).bold(), - selected_suffix_separator: style!(palette.green, background).bold(), - - unselected_prefix_separator: style!(background, palette.fg), - unselected_char_left_separator: style!(background, palette.fg).bold(), - unselected_char_shortcut: style!(palette.red, palette.fg).bold(), - unselected_char_right_separator: style!(background, palette.fg).bold(), - unselected_styled_text: style!(background, palette.fg).bold(), - unselected_suffix_separator: style!(palette.fg, background), - - unselected_alternate_prefix_separator: style!(background, alternate_background_color), - unselected_alternate_char_left_separator: style!( - background, - alternate_background_color - ) - .bold(), - unselected_alternate_char_shortcut: style!(palette.red, alternate_background_color) - .bold(), - unselected_alternate_char_right_separator: style!( - background, - alternate_background_color - ) - .bold(), - unselected_alternate_styled_text: style!(background, alternate_background_color).bold(), - unselected_alternate_suffix_separator: style!(alternate_background_color, background), - - disabled_prefix_separator: style!(background, palette.fg), - disabled_styled_text: style!(background, palette.fg).dimmed().italic(), - disabled_suffix_separator: style!(palette.fg, background), - selected_single_letter_prefix_separator: style!(background, palette.green), - selected_single_letter_char_shortcut: style!(palette.red, palette.green).bold(), - selected_single_letter_suffix_separator: style!(palette.green, background), - - unselected_single_letter_prefix_separator: style!(background, palette.fg), - unselected_single_letter_char_shortcut: style!(palette.red, palette.fg).bold().dimmed(), - unselected_single_letter_suffix_separator: style!(palette.fg, background), - - unselected_alternate_single_letter_prefix_separator: style!(background, palette.fg), - unselected_alternate_single_letter_char_shortcut: style!( - palette.red, - alternate_background_color - ) - .bold() - .dimmed(), - unselected_alternate_single_letter_suffix_separator: style!(palette.fg, background), - + selected: SegmentStyle { + prefix_separator: style!(background, palette.green), + char_left_separator: style!(background, palette.green).bold(), + char_shortcut: style!(palette.red, palette.green).bold(), + char_right_separator: style!(background, palette.green).bold(), + styled_text: style!(background, palette.green).bold(), + suffix_separator: style!(palette.green, background).bold(), + }, + unselected: SegmentStyle { + prefix_separator: style!(background, palette.fg), + char_left_separator: style!(background, palette.fg).bold(), + char_shortcut: style!(palette.red, palette.fg).bold(), + char_right_separator: style!(background, palette.fg).bold(), + styled_text: style!(background, palette.fg).bold(), + suffix_separator: style!(palette.fg, background), + }, + unselected_alternate: SegmentStyle { + prefix_separator: style!(background, alternate_background_color), + char_left_separator: style!(background, alternate_background_color).bold(), + char_shortcut: style!(palette.red, alternate_background_color).bold(), + char_right_separator: style!(background, alternate_background_color).bold(), + styled_text: style!(background, alternate_background_color).bold(), + suffix_separator: style!(alternate_background_color, background), + }, + disabled: SegmentStyle { + prefix_separator: style!(background, palette.fg), + char_left_separator: style!(background, palette.fg).dimmed().italic(), + char_shortcut: style!(background, palette.fg).dimmed().italic(), + char_right_separator: style!(background, palette.fg).dimmed().italic(), + styled_text: style!(background, palette.fg).dimmed().italic(), + suffix_separator: style!(palette.fg, background), + }, superkey_prefix: style!(foreground, background).bold(), superkey_suffix_separator: style!(background, background), }, PaletteSource::Xresources => ColoredElements { - selected_prefix_separator: style!(background, palette.green), - selected_char_left_separator: style!(palette.fg, palette.green).bold(), - selected_char_shortcut: style!(palette.red, palette.green).bold(), - selected_char_right_separator: style!(palette.fg, palette.green).bold(), - selected_styled_text: style!(background, palette.green).bold(), - selected_suffix_separator: style!(palette.green, background).bold(), - unselected_prefix_separator: style!(background, palette.fg), - unselected_char_left_separator: style!(background, palette.fg).bold(), - unselected_char_shortcut: style!(palette.red, palette.fg).bold(), - unselected_char_right_separator: style!(background, palette.fg).bold(), - unselected_styled_text: style!(background, palette.fg).bold(), - unselected_suffix_separator: style!(palette.fg, background), - - unselected_alternate_prefix_separator: style!(background, alternate_background_color), - unselected_alternate_char_left_separator: style!( - background, - alternate_background_color - ) - .bold(), - unselected_alternate_char_shortcut: style!(palette.red, alternate_background_color) - .bold(), - unselected_alternate_char_right_separator: style!( - background, - alternate_background_color - ) - .bold(), - unselected_alternate_styled_text: style!(background, alternate_background_color).bold(), - unselected_alternate_suffix_separator: style!(alternate_background_color, background), - - disabled_prefix_separator: style!(background, palette.fg), - disabled_styled_text: style!(background, palette.fg).dimmed(), - disabled_suffix_separator: style!(palette.fg, background), - selected_single_letter_prefix_separator: style!(palette.fg, palette.green), - selected_single_letter_char_shortcut: style!(palette.red, palette.green).bold(), - selected_single_letter_suffix_separator: style!(palette.green, palette.fg), - - unselected_single_letter_prefix_separator: style!(palette.fg, background), - unselected_single_letter_char_shortcut: style!(palette.red, palette.fg).bold(), - unselected_single_letter_suffix_separator: style!(palette.fg, background), - - unselected_alternate_single_letter_prefix_separator: style!(background, palette.fg), - unselected_alternate_single_letter_char_shortcut: style!( - palette.red, - alternate_background_color - ) - .bold() - .dimmed(), - unselected_alternate_single_letter_suffix_separator: style!(palette.fg, background), - + selected: SegmentStyle { + prefix_separator: style!(background, palette.green), + char_left_separator: style!(palette.fg, palette.green).bold(), + char_shortcut: style!(palette.red, palette.green).bold(), + char_right_separator: style!(palette.fg, palette.green).bold(), + styled_text: style!(background, palette.green).bold(), + suffix_separator: style!(palette.green, background).bold(), + }, + unselected: SegmentStyle { + prefix_separator: style!(background, palette.fg), + char_left_separator: style!(background, palette.fg).bold(), + char_shortcut: style!(palette.red, palette.fg).bold(), + char_right_separator: style!(background, palette.fg).bold(), + styled_text: style!(background, palette.fg).bold(), + suffix_separator: style!(palette.fg, background), + }, + unselected_alternate: SegmentStyle { + prefix_separator: style!(background, alternate_background_color), + char_left_separator: style!(background, alternate_background_color).bold(), + char_shortcut: style!(palette.red, alternate_background_color).bold(), + char_right_separator: style!(background, alternate_background_color).bold(), + styled_text: style!(background, alternate_background_color).bold(), + suffix_separator: style!(alternate_background_color, background), + }, + disabled: SegmentStyle { + prefix_separator: style!(background, palette.fg), + char_left_separator: style!(background, palette.fg).dimmed(), + char_shortcut: style!(background, palette.fg).dimmed(), + char_right_separator: style!(background, palette.fg).dimmed(), + styled_text: style!(background, palette.fg).dimmed(), + suffix_separator: style!(palette.fg, background), + }, superkey_prefix: style!(background, palette.fg).bold(), superkey_suffix_separator: style!(palette.fg, background), }, @@ -263,15 +212,7 @@ impl ZellijPlugin for State { "" }; - let colored_elements = color_elements(self.mode_info.style.colors, !supports_arrow_fonts); - let superkey = superkey(colored_elements, separator); - let ctrl_keys = ctrl_keys( - &self.mode_info, - cols.saturating_sub(superkey.len), - separator, - ); - - let first_line = format!("{}{}", superkey, ctrl_keys); + let first_line = first_line(&self.mode_info, cols, separator); let second_line = self.second_line(cols); let background = match self.mode_info.style.colors.theme_hue { @@ -316,7 +257,7 @@ impl State { } } else if active_tab.are_floating_panes_visible { match self.mode_info.mode { - InputMode::Normal => floating_panes_are_visible(&self.mode_info.style.colors), + InputMode::Normal => floating_panes_are_visible(&self.mode_info), InputMode::Locked => { locked_floating_panes_are_visible(&self.mode_info.style.colors) }, @@ -330,3 +271,496 @@ impl State { } } } + +/// Get a common modifier key from a key vector. +/// +/// Iterates over all keys and returns any found common modifier key. Possible modifiers that will +/// be detected are "Ctrl" and "Alt". +pub fn get_common_modifier(keyvec: Vec<&Key>) -> Option { + let mut modifier = ""; + let mut new_modifier; + for key in keyvec.iter() { + match key { + Key::Ctrl(_) => new_modifier = "Ctrl", + Key::Alt(_) => new_modifier = "Alt", + _ => return None, + } + if modifier.is_empty() { + modifier = new_modifier; + } else if modifier != new_modifier { + // Prefix changed! + return None; + } + } + match modifier.is_empty() { + true => None, + false => Some(modifier.to_string()), + } +} + +/// Get key from action pattern(s). +/// +/// This function takes as arguments a `keymap` that is a `Vec<(Key, Vec)>` 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`. +pub fn action_key(keymap: &[(Key, Vec)], action: &[Action]) -> Vec { + keymap + .iter() + .filter_map(|(key, acvec)| { + if acvec.as_slice() == action { + Some(*key) + } else { + None + } + }) + .collect::>() +} + +/// Get multiple keys for multiple actions. +/// +/// An extension of [`action_key`] that iterates over all action tuples and collects the results. +pub fn action_key_group(keymap: &[(Key, Vec)], actions: &[&[Action]]) -> Vec { + let mut ret = vec![]; + for action in actions { + ret.extend(action_key(keymap, action)); + } + ret +} + +/// Style a vector of [`Key`]s with the given [`Palette`]. +/// +/// Creates a line segment of style ``, with correct theming applied: The brackets have the +/// regular text color, the enclosed keys are painted green and bold. If the keys share a common +/// modifier (See [`get_common_modifier`]), it is printed in front of the keys, painted green and +/// bold, separated with a `+`: `MOD + `. +/// +/// If multiple [`Key`]s are given, the individual keys are separated with a `|` char. This does +/// not apply to the following groups of keys which are treated specially and don't have a +/// separator between them: +/// +/// - "hjkl" +/// - "←↓↑→" +/// - "←→" +/// - "↓↑" +/// +/// The returned Vector of [`ANSIString`] is suitable for transformation into an [`ANSIStrings`] +/// type. +pub fn style_key_with_modifier(keyvec: &[Key], palette: &Palette) -> Vec> { + // Nothing to do, quit... + if keyvec.is_empty() { + return vec![]; + } + + let text_color = palette_match!(match palette.theme_hue { + ThemeHue::Dark => palette.white, + ThemeHue::Light => palette.black, + }); + let green_color = palette_match!(palette.green); + let orange_color = palette_match!(palette.orange); + let mut ret = vec![]; + + // Prints modifier key + let modifier_str = match get_common_modifier(keyvec.iter().collect()) { + Some(modifier) => modifier, + None => "".to_string(), + }; + let no_modifier = modifier_str.is_empty(); + let painted_modifier = if modifier_str.is_empty() { + Style::new().paint("") + } else { + Style::new().fg(orange_color).bold().paint(modifier_str) + }; + ret.push(painted_modifier); + + // Prints key group start + let group_start_str = if no_modifier { "<" } else { " + <" }; + ret.push(Style::new().fg(text_color).paint(group_start_str)); + + // Prints the keys + let key = keyvec + .iter() + .map(|key| { + if no_modifier { + format!("{}", key) + } else { + match key { + Key::Ctrl(c) => format!("{}", Key::Char(*c)), + Key::Alt(c) => format!("{}", c), + _ => format!("{}", key), + } + } + }) + .collect::>(); + + // Special handling of some pre-defined keygroups + let key_string = key.join(""); + let key_separator = match &key_string[..] { + "hjkl" => "", + "←↓↑→" => "", + "←→" => "", + "↓↑" => "", + _ => "|", + }; + + for (idx, key) in key.iter().enumerate() { + if idx > 0 && !key_separator.is_empty() { + ret.push(Style::new().fg(text_color).paint(key_separator)); + } + ret.push(Style::new().fg(green_color).bold().paint(key.clone())); + } + + let group_end_str = ">"; + ret.push(Style::new().fg(text_color).paint(group_end_str)); + + ret +} + +#[cfg(test)] +pub mod tests { + use super::*; + use ansi_term::unstyle; + use ansi_term::ANSIStrings; + use zellij_tile::prelude::CharOrArrow; + use zellij_tile::prelude::Direction; + + fn big_keymap() -> Vec<(Key, Vec)> { + vec![ + (Key::Char('a'), vec![Action::Quit]), + (Key::Ctrl('b'), vec![Action::ScrollUp]), + (Key::Ctrl('d'), vec![Action::ScrollDown]), + ( + Key::Alt(CharOrArrow::Char('c')), + vec![Action::ScrollDown, Action::SwitchToMode(InputMode::Normal)], + ), + ( + Key::Char('1'), + vec![TO_NORMAL, Action::SwitchToMode(InputMode::Locked)], + ), + ] + } + + #[test] + fn common_modifier_with_ctrl_keys() { + let keyvec = vec![Key::Ctrl('a'), Key::Ctrl('b'), Key::Ctrl('c')]; + let ret = get_common_modifier(keyvec.iter().collect()); + assert_eq!(ret, Some("Ctrl".to_string())); + } + + #[test] + fn common_modifier_with_alt_keys_chars() { + let keyvec = vec![ + Key::Alt(CharOrArrow::Char('1')), + Key::Alt(CharOrArrow::Char('t')), + Key::Alt(CharOrArrow::Char('z')), + ]; + let ret = get_common_modifier(keyvec.iter().collect()); + assert_eq!(ret, Some("Alt".to_string())); + } + + #[test] + fn common_modifier_with_alt_keys_arrows() { + let keyvec = vec![ + Key::Alt(CharOrArrow::Direction(Direction::Left)), + Key::Alt(CharOrArrow::Direction(Direction::Right)), + ]; + let ret = get_common_modifier(keyvec.iter().collect()); + assert_eq!(ret, Some("Alt".to_string())); + } + + #[test] + fn common_modifier_with_alt_keys_arrows_and_chars() { + let keyvec = vec![ + Key::Alt(CharOrArrow::Direction(Direction::Left)), + Key::Alt(CharOrArrow::Direction(Direction::Right)), + Key::Alt(CharOrArrow::Char('t')), + Key::Alt(CharOrArrow::Char('z')), + ]; + let ret = get_common_modifier(keyvec.iter().collect()); + assert_eq!(ret, Some("Alt".to_string())); + } + + #[test] + fn common_modifier_with_mixed_alt_ctrl_keys() { + let keyvec = vec![ + Key::Alt(CharOrArrow::Direction(Direction::Left)), + Key::Alt(CharOrArrow::Char('z')), + Key::Ctrl('a'), + Key::Ctrl('1'), + ]; + let ret = get_common_modifier(keyvec.iter().collect()); + assert_eq!(ret, None); + } + + #[test] + fn common_modifier_with_any_keys() { + let keyvec = vec![Key::Backspace, Key::Char('f'), Key::Down]; + let ret = get_common_modifier(keyvec.iter().collect()); + assert_eq!(ret, None); + } + + #[test] + fn common_modifier_with_ctrl_and_normal_keys() { + let keyvec = vec![Key::Ctrl('a'), Key::Char('f'), Key::Down]; + let ret = get_common_modifier(keyvec.iter().collect()); + assert_eq!(ret, None); + } + + #[test] + fn common_modifier_with_alt_and_normal_keys() { + let keyvec = vec![Key::Alt(CharOrArrow::Char('a')), Key::Char('f'), Key::Down]; + let ret = get_common_modifier(keyvec.iter().collect()); + assert_eq!(ret, None); + } + + #[test] + fn action_key_simple_pattern_match_exact() { + let keymap = &[(Key::Char('f'), vec![Action::Quit])]; + let ret = action_key(keymap, &[Action::Quit]); + assert_eq!(ret, vec![Key::Char('f')]); + } + + #[test] + fn action_key_simple_pattern_match_pattern_too_long() { + let keymap = &[(Key::Char('f'), vec![Action::Quit])]; + let ret = action_key(keymap, &[Action::Quit, Action::ScrollUp]); + assert_eq!(ret, Vec::new()); + } + + #[test] + fn action_key_simple_pattern_match_pattern_empty() { + let keymap = &[(Key::Char('f'), vec![Action::Quit])]; + let ret = action_key(keymap, &[]); + assert_eq!(ret, Vec::new()); + } + + #[test] + fn action_key_long_pattern_match_exact() { + let keymap = big_keymap(); + let ret = action_key(&keymap, &[Action::ScrollDown, TO_NORMAL]); + assert_eq!(ret, vec![Key::Alt(CharOrArrow::Char('c'))]); + } + + #[test] + fn action_key_long_pattern_match_too_short() { + let keymap = big_keymap(); + let ret = action_key(&keymap, &[TO_NORMAL]); + assert_eq!(ret, Vec::new()); + } + + #[test] + fn action_key_group_single_pattern() { + let keymap = big_keymap(); + let ret = action_key_group(&keymap, &[&[Action::Quit]]); + assert_eq!(ret, vec![Key::Char('a')]); + } + + #[test] + fn action_key_group_two_patterns() { + let keymap = big_keymap(); + let ret = action_key_group(&keymap, &[&[Action::ScrollDown], &[Action::ScrollUp]]); + // Mind the order! + assert_eq!(ret, vec![Key::Ctrl('d'), Key::Ctrl('b')]); + } + + fn get_palette() -> Palette { + Palette::default() + } + + #[test] + fn style_key_with_modifier_only_chars() { + let keyvec = vec![Key::Char('a'), Key::Char('b'), Key::Char('c')]; + let palette = get_palette(); + + let ret = style_key_with_modifier(&keyvec, &palette); + let ret = unstyle(&ANSIStrings(&ret)); + + assert_eq!(ret, "".to_string()) + } + + #[test] + fn style_key_with_modifier_special_group_hjkl() { + let keyvec = vec![ + Key::Char('h'), + Key::Char('j'), + Key::Char('k'), + Key::Char('l'), + ]; + let palette = get_palette(); + + let ret = style_key_with_modifier(&keyvec, &palette); + let ret = unstyle(&ANSIStrings(&ret)); + + assert_eq!(ret, "".to_string()) + } + + #[test] + fn style_key_with_modifier_special_group_hjkl_broken() { + // Sorted the wrong way + let keyvec = vec![ + Key::Char('h'), + Key::Char('k'), + Key::Char('j'), + Key::Char('l'), + ]; + let palette = get_palette(); + + let ret = style_key_with_modifier(&keyvec, &palette); + let ret = unstyle(&ANSIStrings(&ret)); + + assert_eq!(ret, "".to_string()) + } + + #[test] + fn style_key_with_modifier_special_group_all_arrows() { + let keyvec = vec![ + Key::Char('←'), + Key::Char('↓'), + Key::Char('↑'), + Key::Char('→'), + ]; + let palette = get_palette(); + + let ret = style_key_with_modifier(&keyvec, &palette); + let ret = unstyle(&ANSIStrings(&ret)); + + assert_eq!(ret, "<←↓↑→>".to_string()) + } + + #[test] + fn style_key_with_modifier_special_group_left_right_arrows() { + let keyvec = vec![Key::Char('←'), Key::Char('→')]; + let palette = get_palette(); + + let ret = style_key_with_modifier(&keyvec, &palette); + let ret = unstyle(&ANSIStrings(&ret)); + + assert_eq!(ret, "<←→>".to_string()) + } + + #[test] + fn style_key_with_modifier_special_group_down_up_arrows() { + let keyvec = vec![Key::Char('↓'), Key::Char('↑')]; + let palette = get_palette(); + + let ret = style_key_with_modifier(&keyvec, &palette); + let ret = unstyle(&ANSIStrings(&ret)); + + assert_eq!(ret, "<↓↑>".to_string()) + } + + #[test] + fn style_key_with_modifier_common_ctrl_modifier_chars() { + let keyvec = vec![ + Key::Ctrl('a'), + Key::Ctrl('b'), + Key::Ctrl('c'), + Key::Ctrl('d'), + ]; + let palette = get_palette(); + + let ret = style_key_with_modifier(&keyvec, &palette); + let ret = unstyle(&ANSIStrings(&ret)); + + assert_eq!(ret, "Ctrl + ".to_string()) + } + + #[test] + fn style_key_with_modifier_common_alt_modifier_chars() { + let keyvec = vec![ + Key::Alt(CharOrArrow::Char('a')), + Key::Alt(CharOrArrow::Char('b')), + Key::Alt(CharOrArrow::Char('c')), + Key::Alt(CharOrArrow::Char('d')), + ]; + let palette = get_palette(); + + let ret = style_key_with_modifier(&keyvec, &palette); + let ret = unstyle(&ANSIStrings(&ret)); + + assert_eq!(ret, "Alt + ".to_string()) + } + + #[test] + fn style_key_with_modifier_common_alt_modifier_with_special_group_all_arrows() { + let keyvec = vec![ + Key::Alt(CharOrArrow::Direction(Direction::Left)), + Key::Alt(CharOrArrow::Direction(Direction::Down)), + Key::Alt(CharOrArrow::Direction(Direction::Up)), + Key::Alt(CharOrArrow::Direction(Direction::Right)), + ]; + let palette = get_palette(); + + let ret = style_key_with_modifier(&keyvec, &palette); + let ret = unstyle(&ANSIStrings(&ret)); + + assert_eq!(ret, "Alt + <←↓↑→>".to_string()) + } + + #[test] + fn style_key_with_modifier_ctrl_alt_char_mixed() { + let keyvec = vec![ + Key::Alt(CharOrArrow::Char('a')), + Key::Ctrl('b'), + Key::Char('c'), + ]; + let palette = get_palette(); + + let ret = style_key_with_modifier(&keyvec, &palette); + let ret = unstyle(&ANSIStrings(&ret)); + + assert_eq!(ret, "".to_string()) + } + + #[test] + fn style_key_with_modifier_unprintables() { + let keyvec = vec![ + Key::Backspace, + Key::Char('\n'), + Key::Char(' '), + Key::Char('\t'), + Key::PageDown, + Key::Delete, + Key::Home, + Key::End, + Key::Insert, + Key::BackTab, + Key::Esc, + ]; + let palette = get_palette(); + + let ret = style_key_with_modifier(&keyvec, &palette); + let ret = unstyle(&ANSIStrings(&ret)); + + assert_eq!( + ret, + "".to_string() + ) + } + + #[test] + fn style_key_with_modifier_unprintables_with_common_ctrl_modifier() { + let keyvec = vec![Key::Ctrl('\n'), Key::Ctrl(' '), Key::Ctrl('\t')]; + let palette = get_palette(); + + let ret = style_key_with_modifier(&keyvec, &palette); + let ret = unstyle(&ANSIStrings(&ret)); + + assert_eq!(ret, "Ctrl + ".to_string()) + } + + #[test] + fn style_key_with_modifier_unprintables_with_common_alt_modifier() { + let keyvec = vec![ + Key::Alt(CharOrArrow::Char('\n')), + Key::Alt(CharOrArrow::Char(' ')), + Key::Alt(CharOrArrow::Char('\t')), + ]; + let palette = get_palette(); + + let ret = style_key_with_modifier(&keyvec, &palette); + let ret = unstyle(&ANSIStrings(&ret)); + + assert_eq!(ret, "Alt + ".to_string()) + } +} diff --git a/default-plugins/status-bar/src/second_line.rs b/default-plugins/status-bar/src/second_line.rs index 40be3fcd5f..26afcd94f2 100644 --- a/default-plugins/status-bar/src/second_line.rs +++ b/default-plugins/status-bar/src/second_line.rs @@ -1,96 +1,47 @@ use ansi_term::{ - ANSIStrings, + unstyled_len, ANSIString, ANSIStrings, Color::{Fixed, RGB}, Style, }; +use zellij_tile::prelude::actions::Action; use zellij_tile::prelude::*; use zellij_tile_utils::palette_match; use crate::{ + action_key, action_key_group, style_key_with_modifier, tip::{data::TIPS, TipFn}, - LinePart, MORE_MSG, + LinePart, MORE_MSG, TO_NORMAL, }; -#[derive(Clone, Copy)] -enum StatusBarTextColor { - White, - Green, - Orange, -} - -#[derive(Clone, Copy)] -enum StatusBarTextBoldness { - Bold, - NotBold, -} - fn full_length_shortcut( is_first_shortcut: bool, - letter: &str, - description: &str, + key: Vec, + action: &str, palette: Palette, ) -> LinePart { - let text_color = palette_match!(match palette.theme_hue { - ThemeHue::Dark => palette.white, - ThemeHue::Light => palette.black, - }); - let green_color = palette_match!(palette.green); - let separator = if is_first_shortcut { " " } else { " / " }; - let separator = Style::new().fg(text_color).paint(separator); - let shortcut_len = letter.chars().count() + 3; // 2 for <>'s around shortcut, 1 for the space - let shortcut_left_separator = Style::new().fg(text_color).paint("<"); - let shortcut = Style::new().fg(green_color).bold().paint(letter); - let shortcut_right_separator = Style::new().fg(text_color).paint("> "); - let description_len = description.chars().count(); - let description = Style::new().fg(text_color).bold().paint(description); - let len = shortcut_len + description_len + separator.chars().count(); - LinePart { - part: ANSIStrings(&[ - separator, - shortcut_left_separator, - shortcut, - shortcut_right_separator, - description, - ]) - .to_string(), - len, + if key.is_empty() { + return LinePart::default(); } -} -fn first_word_shortcut( - is_first_shortcut: bool, - letter: &str, - description: &str, - palette: Palette, -) -> LinePart { let text_color = palette_match!(match palette.theme_hue { ThemeHue::Dark => palette.white, ThemeHue::Light => palette.black, }); - let green_color = palette_match!(palette.green); + let separator = if is_first_shortcut { " " } else { " / " }; - let separator = Style::new().fg(text_color).paint(separator); - let shortcut_len = letter.chars().count() + 3; // 2 for <>'s around shortcut, 1 for the space - let shortcut_left_separator = Style::new().fg(text_color).paint("<"); - let shortcut = Style::new().fg(green_color).bold().paint(letter); - let shortcut_right_separator = Style::new().fg(text_color).paint("> "); - let description_first_word = description.split(' ').next().unwrap_or(""); - let description_first_word_length = description_first_word.chars().count(); - let description_first_word = Style::new() - .fg(text_color) - .bold() - .paint(description_first_word); - let len = shortcut_len + description_first_word_length + separator.chars().count(); + let mut bits: Vec = vec![Style::new().fg(text_color).paint(separator)]; + bits.extend(style_key_with_modifier(&key, &palette)); + bits.push( + Style::new() + .fg(text_color) + .bold() + .paint(format!(" {}", action)), + ); + let part = ANSIStrings(&bits); + LinePart { - part: ANSIStrings(&[ - separator, - shortcut_left_separator, - shortcut, - shortcut_right_separator, - description_first_word, - ]) - .to_string(), - len, + part: part.to_string(), + len: unstyled_len(&part), } } @@ -108,168 +59,241 @@ fn locked_interface_indication(palette: Palette) -> LinePart { } } -fn show_extra_hints( - palette: Palette, - text_with_style: Vec<(&str, StatusBarTextColor, StatusBarTextBoldness)>, -) -> LinePart { - use StatusBarTextBoldness::*; - use StatusBarTextColor::*; - // get the colors - let white_color = palette_match!(palette.white); - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); - // calculate length of tipp - let len = text_with_style - .iter() - .fold(0, |len_sum, (text, _, _)| len_sum + text.chars().count()); - // apply the styles defined above - let styled_text = text_with_style - .into_iter() - .map(|(text, color, is_bold)| { - let color = match color { - White => white_color, - Green => green_color, - Orange => orange_color, - }; - match is_bold { - Bold => Style::new().fg(color).bold().paint(text), - NotBold => Style::new().fg(color).paint(text), - } - }) - .collect::>(); - LinePart { - part: ANSIStrings(&styled_text[..]).to_string(), - len, - } -} +fn add_shortcut(help: &ModeInfo, linepart: &LinePart, text: &str, keys: Vec) -> LinePart { + let shortcut = if linepart.len == 0 { + full_length_shortcut(true, keys, text, help.style.colors) + } else { + full_length_shortcut(false, keys, text, help.style.colors) + }; -/// Creates hints for usage of Pane Mode -fn confirm_pane_selection(palette: Palette) -> LinePart { - use StatusBarTextBoldness::*; - use StatusBarTextColor::*; - let text_with_style = [ - (" / ", White, NotBold), - ("", Green, Bold), - (" Select pane", White, Bold), - ]; - show_extra_hints(palette, text_with_style.to_vec()) + let mut new_linepart = LinePart::default(); + new_linepart.len += linepart.len + shortcut.len; + new_linepart.part = format!("{}{}", linepart.part, shortcut); + new_linepart } -/// Creates hints for usage of Rename Mode in Pane Mode -fn select_pane_shortcut(palette: Palette) -> LinePart { - use StatusBarTextBoldness::*; - use StatusBarTextColor::*; - let text_with_style = [ - (" / ", White, NotBold), - ("Alt", Orange, Bold), - (" + ", White, NotBold), - ("<", Green, Bold), - ("[]", Green, Bold), - (" or ", White, NotBold), - ("hjkl", Green, Bold), - (">", Green, Bold), - (" Select pane", White, Bold), - ]; - show_extra_hints(palette, text_with_style.to_vec()) +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 = add_shortcut(help, &line_part, &long, keys.to_vec()); + } + line_part } -fn full_shortcut_list_nonstandard_mode( - extra_hint_producing_function: fn(Palette) -> LinePart, -) -> impl FnOnce(&ModeInfo) -> LinePart { - move |help| { - let mut line_part = LinePart::default(); - for (i, (letter, description)) in help.keybinds.iter().enumerate() { - let shortcut = full_length_shortcut(i == 0, letter, description, help.style.colors); - line_part.len += shortcut.len; - line_part.part = format!("{}{}", line_part.part, shortcut,); +/// Collect all relevant keybindings and hints to display. +/// +/// Creates a vector with tuples containing the following entries: +/// +/// - A String to display for this keybinding when there are no size restrictions, +/// - A shortened String (where sensible) to display if the whole second line becomes too long, +/// - A `Vec` of the keys that map to this keyhint +/// +/// This vector is created by iterating over the keybindings for the current [`InputMode`] and +/// storing all Keybindings that match pre-defined patterns of `Action`s. For example, the +/// `InputMode::Pane` input mode determines which keys to display for the "Move focus" hint by +/// searching the keybindings for anything that matches the `Action::MoveFocus(_)` action. Since by +/// default multiple keybindings map to some action patterns (e.g. `Action::MoveFocus(_)` is bound +/// to "hjkl", the arrow keys and "Alt + "), we deduplicate the vector of all keybindings +/// before processing it. +/// +/// Therefore we sort it by the [`Key`]s of the current keymap and deduplicate the resulting sorted +/// vector by the `Vec` action vectors bound to the keys. As such, when multiple keys map +/// to the same sequence of actions, the keys that appear first in the [`Key`] structure will be +/// displayed. +// Please don't let rustfmt play with the formatting. It will stretch out the function to about +// three times the length and all the keybinding vectors we generate become virtually unreadable +// for humans. +#[rustfmt::skip] +fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec)> { + use Action as A; + use InputMode as IM; + use actions::Direction as Dir; + use actions::ResizeDirection as RDir; + 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 to_normal_keys = action_key(&old_keymap, &[TO_NORMAL]); + let to_normal_key = if to_normal_keys.contains(&Key::Char('\n')) { + vec![Key::Char('\n')] + } else { + // Yield `vec![key]` if `to_normal_keys` has at least one key, or an empty vec otherwise. + to_normal_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![]; + 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)); } - let select_pane_shortcut = extra_hint_producing_function(help.style.colors); - line_part.len += select_pane_shortcut.len; - line_part.part = format!("{}{}", line_part.part, select_pane_shortcut,); - line_part } + + if mi.mode == IM::Pane { 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("New"), s("New"), action_key(&km, &[A::NewPane(None), TO_NORMAL])), + (s("Close"), s("Close"), action_key(&km, &[A::CloseFocus, TO_NORMAL])), + (s("Rename"), s("Rename"), + action_key(&km, &[A::SwitchToMode(IM::RenamePane), A::PaneNameInput(vec![0])])), + (s("Split down"), s("Down"), action_key(&km, &[A::NewPane(Some(Dir::Down)), TO_NORMAL])), + (s("Split right"), s("Right"), action_key(&km, &[A::NewPane(Some(Dir::Right)), TO_NORMAL])), + (s("Fullscreen"), s("Fullscreen"), action_key(&km, &[A::ToggleFocusFullscreen, TO_NORMAL])), + (s("Frames"), s("Frames"), action_key(&km, &[A::TogglePaneFrames, TO_NORMAL])), + (s("Floating toggle"), s("Floating"), + action_key(&km, &[A::ToggleFloatingPanes, TO_NORMAL])), + (s("Embed pane"), s("Embed"), action_key(&km, &[A::TogglePaneEmbedOrFloating, TO_NORMAL])), + (s("Next"), s("Next"), action_key(&km, &[A::SwitchFocus])), + (s("Select pane"), s("Select"), to_normal_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 = action_key_group(&old_keymap, + &[&[A::GoToPreviousTab], &[A::GoToNextTab]]); + let focus_keys = if focus_keys_full.contains(&Key::Left) + && focus_keys_full.contains(&Key::Right) { + vec![Key::Left, Key::Right] + } else { + action_key_group(&km, &[&[A::GoToPreviousTab], &[A::GoToNextTab]]) + }; + + vec![ + (s("Move focus"), s("Move"), focus_keys), + (s("New"), s("New"), action_key(&km, &[A::NewTab(None), TO_NORMAL])), + (s("Close"), s("Close"), action_key(&km, &[A::CloseTab, TO_NORMAL])), + (s("Rename"), s("Rename"), + action_key(&km, &[A::SwitchToMode(IM::RenameTab), A::TabNameInput(vec![0])])), + (s("Sync"), s("Sync"), action_key(&km, &[A::ToggleActiveSyncTab, TO_NORMAL])), + (s("Toggle"), s("Toggle"), action_key(&km, &[A::ToggleTab])), + (s("Select pane"), s("Select"), to_normal_key), + ]} else if mi.mode == IM::Resize { vec![ + (s("Resize"), s("Resize"), action_key_group(&km, &[ + &[A::Resize(RDir::Left)], &[A::Resize(RDir::Down)], + &[A::Resize(RDir::Up)], &[A::Resize(RDir::Right)]])), + (s("Increase/Decrease size"), s("Increase/Decrease"), + action_key_group(&km, &[&[A::Resize(RDir::Increase)], &[A::Resize(RDir::Decrease)]])), + (s("Select pane"), s("Select"), to_normal_key), + ]} else if mi.mode == IM::Move { vec![ + (s("Move"), 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))]])), + (s("Next pane"), s("Next"), action_key(&km, &[Action::MovePane(None)])), + ]} else if mi.mode == IM::Scroll { vec![ + (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"), + action_key(&km, &[Action::EditScrollback, TO_NORMAL])), + (s("Enter search term"), s("Search"), + action_key(&km, &[A::SwitchToMode(IM::EnterSearch), A::SearchInput(vec![0])])), + (s("Select pane"), s("Select"), to_normal_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("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("Enter term"), s("Search"), + action_key(&km, &[A::SwitchToMode(IM::EnterSearch), A::SearchInput(vec![0])])), + (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("Select pane"), s("Select"), to_normal_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)), TO_NORMAL])), + (s("Split right"), s("Right"), action_key(&km, &[A::NewPane(Some(Dir::Right)), TO_NORMAL])), + (s("Fullscreen"), s("Fullscreen"), action_key(&km, &[A::ToggleFocusFullscreen, TO_NORMAL])), + (s("New tab"), s("New"), action_key(&km, &[A::NewTab(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_normal_key), + ]} else if matches!(mi.mode, IM::RenamePane | IM::RenameTab) { vec![ + (s("When done"), s("Done"), to_normal_key), + (s("Select pane"), s("Select"), action_key_group(&km, &[ + &[A::MoveFocus(Dir::Left)], &[A::MoveFocus(Dir::Down)], + &[A::MoveFocus(Dir::Up)], &[A::MoveFocus(Dir::Right)]])), + ]} else { vec![] } } fn full_shortcut_list(help: &ModeInfo, tip: TipFn) -> LinePart { match help.mode { - InputMode::Normal => tip(help.style.colors), + InputMode::Normal => tip(help), InputMode::Locked => locked_interface_indication(help.style.colors), - InputMode::Tmux => full_tmux_mode_indication(help), - InputMode::RenamePane => full_shortcut_list_nonstandard_mode(select_pane_shortcut)(help), - InputMode::EnterSearch => full_shortcut_list_nonstandard_mode(select_pane_shortcut)(help), - _ => full_shortcut_list_nonstandard_mode(confirm_pane_selection)(help), + _ => full_shortcut_list_nonstandard_mode(help), } } -fn shortened_shortcut_list_nonstandard_mode( - extra_hint_producing_function: fn(Palette) -> LinePart, -) -> impl FnOnce(&ModeInfo) -> LinePart { - move |help| { - let mut line_part = LinePart::default(); - for (i, (letter, description)) in help.keybinds.iter().enumerate() { - let shortcut = first_word_shortcut(i == 0, letter, description, help.style.colors); - line_part.len += shortcut.len; - line_part.part = format!("{}{}", line_part.part, shortcut,); - } - let select_pane_shortcut = extra_hint_producing_function(help.style.colors); - line_part.len += select_pane_shortcut.len; - line_part.part = format!("{}{}", line_part.part, select_pane_shortcut,); - line_part +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 = add_shortcut(help, &line_part, &short, keys.to_vec()); } + line_part } fn shortened_shortcut_list(help: &ModeInfo, tip: TipFn) -> LinePart { match help.mode { - InputMode::Normal => tip(help.style.colors), + InputMode::Normal => tip(help), InputMode::Locked => locked_interface_indication(help.style.colors), - InputMode::Tmux => short_tmux_mode_indication(help), - InputMode::RenamePane => { - shortened_shortcut_list_nonstandard_mode(select_pane_shortcut)(help) - }, - InputMode::EnterSearch => { - shortened_shortcut_list_nonstandard_mode(select_pane_shortcut)(help) - }, - _ => shortened_shortcut_list_nonstandard_mode(confirm_pane_selection)(help), + _ => shortened_shortcut_list_nonstandard_mode(help), } } -fn best_effort_shortcut_list_nonstandard_mode( - extra_hint_producing_function: fn(Palette) -> LinePart, -) -> impl FnOnce(&ModeInfo, usize) -> LinePart { - move |help, max_len| { - let mut line_part = LinePart::default(); - for (i, (letter, description)) in help.keybinds.iter().enumerate() { - let shortcut = first_word_shortcut(i == 0, letter, description, help.style.colors); - if line_part.len + shortcut.len + MORE_MSG.chars().count() > max_len { - // TODO: better - line_part.part = format!("{}{}", line_part.part, MORE_MSG); - line_part.len += MORE_MSG.chars().count(); - break; - } - line_part.len += shortcut.len; - line_part.part = format!("{}{}", line_part.part, shortcut); - } - let select_pane_shortcut = extra_hint_producing_function(help.style.colors); - if line_part.len + select_pane_shortcut.len <= max_len { - line_part.len += select_pane_shortcut.len; - line_part.part = format!("{}{}", line_part.part, select_pane_shortcut,); - } - line_part - } -} +fn best_effort_shortcut_list_nonstandard_mode(help: &ModeInfo, max_len: usize) -> LinePart { + let mut line_part = LinePart::default(); + let keys_and_hints = get_keys_and_hints(help); -fn best_effort_tmux_shortcut_list(help: &ModeInfo, max_len: usize) -> LinePart { - let mut line_part = tmux_mode_indication(help); - for (i, (letter, description)) in help.keybinds.iter().enumerate() { - let shortcut = first_word_shortcut(i == 0, letter, description, help.style.colors); - if line_part.len + shortcut.len + MORE_MSG.chars().count() > max_len { - // TODO: better + for (_, short, keys) in keys_and_hints.into_iter() { + let new_line_part = add_shortcut(help, &line_part, &short, keys.to_vec()); + if new_line_part.len + MORE_MSG.chars().count() > max_len { line_part.part = format!("{}{}", line_part.part, MORE_MSG); line_part.len += MORE_MSG.chars().count(); break; } - line_part.len += shortcut.len; - line_part.part = format!("{}{}", line_part.part, shortcut); + line_part = new_line_part; } line_part } @@ -277,7 +301,7 @@ fn best_effort_tmux_shortcut_list(help: &ModeInfo, max_len: usize) -> LinePart { fn best_effort_shortcut_list(help: &ModeInfo, tip: TipFn, max_len: usize) -> LinePart { match help.mode { InputMode::Normal => { - let line_part = tip(help.style.colors); + let line_part = tip(help); if line_part.len <= max_len { line_part } else { @@ -292,11 +316,7 @@ fn best_effort_shortcut_list(help: &ModeInfo, tip: TipFn, max_len: usize) -> Lin LinePart::default() } }, - InputMode::Tmux => best_effort_tmux_shortcut_list(help, max_len), - InputMode::RenamePane => { - best_effort_shortcut_list_nonstandard_mode(select_pane_shortcut)(help, max_len) - }, - _ => best_effort_shortcut_list_nonstandard_mode(confirm_pane_selection)(help, max_len), + _ => best_effort_shortcut_list_nonstandard_mode(help, max_len), } } @@ -374,7 +394,9 @@ pub fn fullscreen_panes_to_hide(palette: &Palette, panes_to_hide: usize) -> Line } } -pub fn floating_panes_are_visible(palette: &Palette) -> LinePart { +pub fn floating_panes_are_visible(mode_info: &ModeInfo) -> LinePart { + let palette = mode_info.style.colors; + let km = &mode_info.get_mode_keybinds(); let white_color = match palette.white { PaletteColor::Rgb((r, g, b)) => RGB(r, g, b), PaletteColor::EightBit(color) => Fixed(color), @@ -391,16 +413,29 @@ pub fn floating_panes_are_visible(palette: &Palette) -> LinePart { let shortcut_right_separator = Style::new().fg(white_color).bold().paint("): "); let floating_panes = "FLOATING PANES VISIBLE"; let press = "Press "; - let ctrl = "Ctrl-p "; - let plus = "+ "; + let pane_mode = format!( + "{}", + action_key(km, &[Action::SwitchToMode(InputMode::Pane)]) + .first() + .unwrap_or(&Key::Char('?')) + ); + let plus = ", "; let p_left_separator = "<"; - let p = "w"; + let p = format!( + "{}", + action_key( + &mode_info.get_keybinds_for_mode(InputMode::Pane), + &[Action::ToggleFloatingPanes, TO_NORMAL] + ) + .first() + .unwrap_or(&Key::Char('?')) + ); let p_right_separator = "> "; let to_hide = "to hide."; let len = floating_panes.chars().count() + press.chars().count() - + ctrl.chars().count() + + pane_mode.chars().count() + plus.chars().count() + p_left_separator.chars().count() + p.chars().count() @@ -414,7 +449,7 @@ pub fn floating_panes_are_visible(palette: &Palette) -> LinePart { Style::new().fg(orange_color).bold().paint(floating_panes), shortcut_right_separator, Style::new().fg(white_color).bold().paint(press), - Style::new().fg(green_color).bold().paint(ctrl), + Style::new().fg(green_color).bold().paint(pane_mode), Style::new().fg(white_color).bold().paint(plus), Style::new().fg(white_color).bold().paint(p_left_separator), Style::new().fg(green_color).bold().paint(p), @@ -425,90 +460,6 @@ pub fn floating_panes_are_visible(palette: &Palette) -> LinePart { } } -pub fn tmux_mode_indication(help: &ModeInfo) -> LinePart { - let white_color = match help.style.colors.white { - PaletteColor::Rgb((r, g, b)) => RGB(r, g, b), - PaletteColor::EightBit(color) => Fixed(color), - }; - let orange_color = match help.style.colors.orange { - PaletteColor::Rgb((r, g, b)) => RGB(r, g, b), - PaletteColor::EightBit(color) => Fixed(color), - }; - - let shortcut_left_separator = Style::new().fg(white_color).bold().paint(" ("); - let shortcut_right_separator = Style::new().fg(white_color).bold().paint("): "); - let tmux_mode_text = "TMUX MODE"; - let tmux_mode_indicator = Style::new().fg(orange_color).bold().paint(tmux_mode_text); - let line_part = LinePart { - part: format!( - "{}{}{}", - shortcut_left_separator, tmux_mode_indicator, shortcut_right_separator - ), - len: tmux_mode_text.chars().count() + 5, // 2 for the separators, 3 for the colon and following space - }; - line_part -} - -pub fn full_tmux_mode_indication(help: &ModeInfo) -> LinePart { - let white_color = match help.style.colors.white { - PaletteColor::Rgb((r, g, b)) => RGB(r, g, b), - PaletteColor::EightBit(color) => Fixed(color), - }; - let orange_color = match help.style.colors.orange { - PaletteColor::Rgb((r, g, b)) => RGB(r, g, b), - PaletteColor::EightBit(color) => Fixed(color), - }; - - let shortcut_left_separator = Style::new().fg(white_color).bold().paint(" ("); - let shortcut_right_separator = Style::new().fg(white_color).bold().paint("): "); - let tmux_mode_text = "TMUX MODE"; - let tmux_mode_indicator = Style::new().fg(orange_color).bold().paint(tmux_mode_text); - let mut line_part = LinePart { - part: format!( - "{}{}{}", - shortcut_left_separator, tmux_mode_indicator, shortcut_right_separator - ), - len: tmux_mode_text.chars().count() + 5, // 2 for the separators, 3 for the colon and following space - }; - - for (i, (letter, description)) in help.keybinds.iter().enumerate() { - let shortcut = full_length_shortcut(i == 0, letter, description, help.style.colors); - line_part.len += shortcut.len; - line_part.part = format!("{}{}", line_part.part, shortcut,); - } - line_part -} - -pub fn short_tmux_mode_indication(help: &ModeInfo) -> LinePart { - let white_color = match help.style.colors.white { - PaletteColor::Rgb((r, g, b)) => RGB(r, g, b), - PaletteColor::EightBit(color) => Fixed(color), - }; - let orange_color = match help.style.colors.orange { - PaletteColor::Rgb((r, g, b)) => RGB(r, g, b), - PaletteColor::EightBit(color) => Fixed(color), - }; - - let shortcut_left_separator = Style::new().fg(white_color).bold().paint(" ("); - let shortcut_right_separator = Style::new().fg(white_color).bold().paint("): "); - let tmux_mode_text = "TMUX MODE"; - let tmux_mode_indicator = Style::new().fg(orange_color).bold().paint(tmux_mode_text); - let mut line_part = LinePart { - part: format!( - "{}{}{}", - shortcut_left_separator, tmux_mode_indicator, shortcut_right_separator - ), - len: tmux_mode_text.chars().count() + 5, // 2 for the separators, 3 for the colon and following space - }; - - for (i, (letter, description)) in help.keybinds.iter().enumerate() { - let shortcut = first_word_shortcut(i == 0, letter, description, help.style.colors); - line_part.len += shortcut.len; - line_part.part = format!("{}{}", line_part.part, shortcut); - } - line_part -} - pub fn locked_fullscreen_panes_to_hide(palette: &Palette, panes_to_hide: usize) -> LinePart { let text_color = palette_match!(match palette.theme_hue { ThemeHue::Dark => palette.white, @@ -570,3 +521,244 @@ pub fn locked_floating_panes_are_visible(palette: &Palette) -> LinePart { len, } } + +#[cfg(test)] +/// Unit tests. +/// +/// Note that we cheat a little here, because the number of things one may want to test is endless, +/// and creating a Mockup of [`ModeInfo`] by hand for all these testcases is nothing less than +/// torture. Hence, we test the most atomic unit thoroughly ([`full_length_shortcut`] and then test +/// the public API ([`keybinds`]) to ensure correct operation. +mod tests { + use super::*; + + // Strip style information from `LinePart` and return a raw String instead + fn unstyle(line_part: LinePart) -> String { + let string = line_part.to_string(); + + let re = regex::Regex::new(r"\x1b\[[0-9;]*m").unwrap(); + let string = re.replace_all(&string, "".to_string()); + + string.to_string() + } + + fn get_palette() -> Palette { + Palette::default() + } + + #[test] + fn full_length_shortcut_with_key() { + let keyvec = vec![Key::Char('a')]; + let palette = get_palette(); + + let ret = full_length_shortcut(false, keyvec, "Foobar", palette); + let ret = unstyle(ret); + + assert_eq!(ret, " / Foobar"); + } + + #[test] + fn full_length_shortcut_with_key_first_element() { + let keyvec = vec![Key::Char('a')]; + let palette = get_palette(); + + let ret = full_length_shortcut(true, keyvec, "Foobar", palette); + let ret = unstyle(ret); + + assert_eq!(ret, " Foobar"); + } + + #[test] + // When there is no binding, we print no shortcut either + fn full_length_shortcut_without_key() { + let keyvec = vec![]; + let palette = get_palette(); + + let ret = full_length_shortcut(false, keyvec, "Foobar", palette); + let ret = unstyle(ret); + + assert_eq!(ret, ""); + } + + #[test] + fn full_length_shortcut_with_key_unprintable_1() { + let keyvec = vec![Key::Char('\n')]; + let palette = get_palette(); + + let ret = full_length_shortcut(false, keyvec, "Foobar", palette); + let ret = unstyle(ret); + + assert_eq!(ret, " / Foobar"); + } + + #[test] + fn full_length_shortcut_with_key_unprintable_2() { + let keyvec = vec![Key::Backspace]; + let palette = get_palette(); + + let ret = full_length_shortcut(false, keyvec, "Foobar", palette); + let ret = unstyle(ret); + + assert_eq!(ret, " / Foobar"); + } + + #[test] + fn full_length_shortcut_with_ctrl_key() { + let keyvec = vec![Key::Ctrl('a')]; + let palette = get_palette(); + + let ret = full_length_shortcut(false, keyvec, "Foobar", palette); + let ret = unstyle(ret); + + assert_eq!(ret, " / Ctrl + Foobar"); + } + + #[test] + fn full_length_shortcut_with_alt_key() { + let keyvec = vec![Key::Alt(CharOrArrow::Char('a'))]; + let palette = get_palette(); + + let ret = full_length_shortcut(false, keyvec, "Foobar", palette); + let ret = unstyle(ret); + + assert_eq!(ret, " / Alt + Foobar"); + } + + #[test] + fn full_length_shortcut_with_homogenous_key_group() { + let keyvec = vec![Key::Char('a'), Key::Char('b'), Key::Char('c')]; + let palette = get_palette(); + + let ret = full_length_shortcut(false, keyvec, "Foobar", palette); + let ret = unstyle(ret); + + assert_eq!(ret, " / Foobar"); + } + + #[test] + fn full_length_shortcut_with_heterogenous_key_group() { + let keyvec = vec![Key::Char('a'), Key::Ctrl('b'), Key::Char('\n')]; + let palette = get_palette(); + + let ret = full_length_shortcut(false, keyvec, "Foobar", palette); + let ret = unstyle(ret); + + assert_eq!(ret, " / Foobar"); + } + + #[test] + fn full_length_shortcut_with_key_group_shared_ctrl_modifier() { + let keyvec = vec![Key::Ctrl('a'), Key::Ctrl('b'), Key::Ctrl('c')]; + let palette = get_palette(); + + let ret = full_length_shortcut(false, keyvec, "Foobar", palette); + let ret = unstyle(ret); + + assert_eq!(ret, " / Ctrl + Foobar"); + } + //pub fn keybinds(help: &ModeInfo, tip_name: &str, max_width: usize) -> LinePart { + + #[test] + // Note how it leaves out elements that don't exist! + fn keybinds_wide() { + let mode_info = ModeInfo { + mode: InputMode::Pane, + keybinds: vec![( + InputMode::Pane, + vec![ + (Key::Left, vec![Action::MoveFocus(actions::Direction::Left)]), + (Key::Down, vec![Action::MoveFocus(actions::Direction::Down)]), + (Key::Up, vec![Action::MoveFocus(actions::Direction::Up)]), + ( + Key::Right, + vec![Action::MoveFocus(actions::Direction::Right)], + ), + (Key::Char('n'), vec![Action::NewPane(None), TO_NORMAL]), + (Key::Char('x'), vec![Action::CloseFocus, TO_NORMAL]), + ( + Key::Char('f'), + vec![Action::ToggleFocusFullscreen, TO_NORMAL], + ), + ], + )], + ..ModeInfo::default() + }; + + let ret = keybinds(&mode_info, "quicknav", 500); + let ret = unstyle(ret); + + assert_eq!( + ret, + " <←↓↑→> Move focus / New / Close / Fullscreen" + ); + } + + #[test] + // Note how "Move focus" becomes "Move" + fn keybinds_tight_width() { + let mode_info = ModeInfo { + mode: InputMode::Pane, + keybinds: vec![( + InputMode::Pane, + vec![ + (Key::Left, vec![Action::MoveFocus(actions::Direction::Left)]), + (Key::Down, vec![Action::MoveFocus(actions::Direction::Down)]), + (Key::Up, vec![Action::MoveFocus(actions::Direction::Up)]), + ( + Key::Right, + vec![Action::MoveFocus(actions::Direction::Right)], + ), + (Key::Char('n'), vec![Action::NewPane(None), TO_NORMAL]), + (Key::Char('x'), vec![Action::CloseFocus, TO_NORMAL]), + ( + Key::Char('f'), + vec![Action::ToggleFocusFullscreen, TO_NORMAL], + ), + ], + )], + ..ModeInfo::default() + }; + + let ret = keybinds(&mode_info, "quicknav", 35); + let ret = unstyle(ret); + + assert_eq!(ret, " <←↓↑→> Move / New ... "); + } + + #[test] + fn keybinds_wide_weird_keys() { + let mode_info = ModeInfo { + mode: InputMode::Pane, + keybinds: vec![( + InputMode::Pane, + vec![ + ( + Key::Ctrl('a'), + vec![Action::MoveFocus(actions::Direction::Left)], + ), + ( + Key::Ctrl('\n'), + vec![Action::MoveFocus(actions::Direction::Down)], + ), + ( + Key::Ctrl('1'), + vec![Action::MoveFocus(actions::Direction::Up)], + ), + ( + Key::Ctrl(' '), + vec![Action::MoveFocus(actions::Direction::Right)], + ), + (Key::Backspace, vec![Action::NewPane(None), TO_NORMAL]), + (Key::Esc, vec![Action::CloseFocus, TO_NORMAL]), + (Key::End, vec![Action::ToggleFocusFullscreen, TO_NORMAL]), + ], + )], + ..ModeInfo::default() + }; + + let ret = keybinds(&mode_info, "quicknav", 500); + let ret = unstyle(ret); + + assert_eq!(ret, " Ctrl + Move focus / New / Close / Fullscreen"); + } +} diff --git a/default-plugins/status-bar/src/tip/data/compact_layout.rs b/default-plugins/status-bar/src/tip/data/compact_layout.rs index 1632a836ee..dd67ab8f8c 100644 --- a/default-plugins/status-bar/src/tip/data/compact_layout.rs +++ b/default-plugins/status-bar/src/tip/data/compact_layout.rs @@ -5,12 +5,13 @@ use ansi_term::{ }; use crate::LinePart; -use zellij_tile::prelude::*; +use crate::{action_key, style_key_with_modifier}; +use zellij_tile::prelude::{actions::Action, *}; use zellij_tile_utils::palette_match; macro_rules! strings { ($ANSIStrings:expr) => {{ - let strings: &[ANSIString<'static>] = $ANSIStrings; + let strings: &[ANSIString] = $ANSIStrings; let ansi_strings = ANSIStrings(strings); @@ -21,13 +22,12 @@ macro_rules! strings { }}; } -pub fn compact_layout_full(palette: Palette) -> LinePart { +pub fn compact_layout_full(help: &ModeInfo) -> LinePart { // Tip: UI taking up too much space? Start Zellij with // zellij -l compact or remove pane frames with Ctrl +

+ - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); + let green_color = palette_match!(help.style.colors.green); - strings!(&[ + let mut bits = vec![ Style::new().paint(" Tip: "), Style::new().paint("UI taking up too much space? Start Zellij with "), Style::new() @@ -35,21 +35,17 @@ pub fn compact_layout_full(palette: Palette) -> LinePart { .bold() .paint("zellij -l compact"), Style::new().paint(" or remove pane frames with "), - Style::new().fg(orange_color).bold().paint("Ctrl"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("

"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - ]) + ]; + bits.extend(add_keybinds(help)); + strings!(&bits) } -pub fn compact_layout_medium(palette: Palette) -> LinePart { +pub fn compact_layout_medium(help: &ModeInfo) -> LinePart { // Tip: To save screen space, start Zellij with // zellij -l compact or remove pane frames with Ctrl +

+ - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); + let green_color = palette_match!(help.style.colors.green); - strings!(&[ + let mut bits = vec![ Style::new().paint(" Tip: "), Style::new().paint("To save screen space, start Zellij with "), Style::new() @@ -57,31 +53,48 @@ pub fn compact_layout_medium(palette: Palette) -> LinePart { .bold() .paint("zellij -l compact"), Style::new().paint(" or remove frames with "), - Style::new().fg(orange_color).bold().paint("Ctrl"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("

"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - ]) + ]; + bits.extend(add_keybinds(help)); + strings!(&bits) } -pub fn compact_layout_short(palette: Palette) -> LinePart { +pub fn compact_layout_short(help: &ModeInfo) -> LinePart { // Save screen space, start Zellij with // zellij -l compact or remove pane frames with Ctrl +

+ - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); + let green_color = palette_match!(help.style.colors.green); - strings!(&[ + let mut bits = vec![ Style::new().paint(" Save screen space, start with: "), Style::new() .fg(green_color) .bold() .paint("zellij -l compact"), Style::new().paint(" or remove frames with "), - Style::new().fg(orange_color).bold().paint("Ctrl"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("

"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - ]) + ]; + bits.extend(add_keybinds(help)); + strings!(&bits) +} + +fn add_keybinds(help: &ModeInfo) -> Vec { + let to_pane = action_key( + &help.get_mode_keybinds(), + &[Action::SwitchToMode(InputMode::Pane)], + ); + let pane_frames = action_key( + &help.get_keybinds_for_mode(InputMode::Pane), + &[ + Action::TogglePaneFrames, + Action::SwitchToMode(InputMode::Normal), + ], + ); + + if pane_frames.is_empty() { + return vec![Style::new().bold().paint("UNBOUND")]; + } + + let mut bits = vec![]; + bits.extend(style_key_with_modifier(&to_pane, &help.style.colors)); + bits.push(Style::new().paint(", ")); + bits.extend(style_key_with_modifier(&pane_frames, &help.style.colors)); + bits } diff --git a/default-plugins/status-bar/src/tip/data/edit_scrollbuffer.rs b/default-plugins/status-bar/src/tip/data/edit_scrollbuffer.rs index 3807f55db2..bef8acba2a 100644 --- a/default-plugins/status-bar/src/tip/data/edit_scrollbuffer.rs +++ b/default-plugins/status-bar/src/tip/data/edit_scrollbuffer.rs @@ -4,13 +4,13 @@ use ansi_term::{ Style, }; -use crate::LinePart; -use zellij_tile::prelude::*; +use crate::{action_key, style_key_with_modifier, LinePart}; +use zellij_tile::prelude::{actions::Action, *}; use zellij_tile_utils::palette_match; macro_rules! strings { ($ANSIStrings:expr) => {{ - let strings: &[ANSIString<'static>] = $ANSIStrings; + let strings: &[ANSIString] = $ANSIStrings; let ansi_strings = ANSIStrings(strings); @@ -21,58 +21,70 @@ macro_rules! strings { }}; } -pub fn edit_scrollbuffer_full(palette: Palette) -> LinePart { +pub fn edit_scrollbuffer_full(help: &ModeInfo) -> LinePart { // Tip: Search through the scrollbuffer using your default $EDITOR with // Ctrl + + - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); + let green_color = palette_match!(help.style.colors.green); - strings!(&[ + let mut bits = vec![ Style::new().paint(" Tip: "), Style::new().paint("Search through the scrollbuffer using your default "), Style::new().fg(green_color).bold().paint("$EDITOR"), Style::new().paint(" with "), - Style::new().fg(orange_color).bold().paint("Ctrl"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - ]) + ]; + bits.extend(add_keybinds(help)); + strings!(&bits) } -pub fn edit_scrollbuffer_medium(palette: Palette) -> LinePart { +pub fn edit_scrollbuffer_medium(help: &ModeInfo) -> LinePart { // Tip: Search the scrollbuffer using your $EDITOR with // Ctrl + + - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); + let green_color = palette_match!(help.style.colors.green); - strings!(&[ + let mut bits = vec![ Style::new().paint(" Tip: "), Style::new().paint("Search the scrollbuffer using your "), Style::new().fg(green_color).bold().paint("$EDITOR"), Style::new().paint(" with "), - Style::new().fg(orange_color).bold().paint("Ctrl"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - ]) + ]; + bits.extend(add_keybinds(help)); + strings!(&bits) } -pub fn edit_scrollbuffer_short(palette: Palette) -> LinePart { +pub fn edit_scrollbuffer_short(help: &ModeInfo) -> LinePart { // Search using $EDITOR with // Ctrl + + - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); + let green_color = palette_match!(help.style.colors.green); - strings!(&[ + let mut bits = vec![ Style::new().paint(" Search using "), Style::new().fg(green_color).bold().paint("$EDITOR"), Style::new().paint(" with "), - Style::new().fg(orange_color).bold().paint("Ctrl"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - ]) + ]; + bits.extend(add_keybinds(help)); + strings!(&bits) +} + +fn add_keybinds(help: &ModeInfo) -> Vec { + let to_pane = action_key( + &help.get_mode_keybinds(), + &[Action::SwitchToMode(InputMode::Scroll)], + ); + let edit_buffer = action_key( + &help.get_keybinds_for_mode(InputMode::Scroll), + &[ + Action::EditScrollback, + Action::SwitchToMode(InputMode::Normal), + ], + ); + + if edit_buffer.is_empty() { + return vec![Style::new().bold().paint("UNBOUND")]; + } + + let mut bits = vec![]; + bits.extend(style_key_with_modifier(&to_pane, &help.style.colors)); + bits.push(Style::new().paint(", ")); + bits.extend(style_key_with_modifier(&edit_buffer, &help.style.colors)); + bits } diff --git a/default-plugins/status-bar/src/tip/data/floating_panes_mouse.rs b/default-plugins/status-bar/src/tip/data/floating_panes_mouse.rs index 64594987b0..d91c3ed8b4 100644 --- a/default-plugins/status-bar/src/tip/data/floating_panes_mouse.rs +++ b/default-plugins/status-bar/src/tip/data/floating_panes_mouse.rs @@ -1,16 +1,11 @@ -use ansi_term::{ - unstyled_len, ANSIString, ANSIStrings, - Color::{Fixed, RGB}, - Style, -}; +use ansi_term::{unstyled_len, ANSIString, ANSIStrings, Style}; -use crate::LinePart; -use zellij_tile::prelude::*; -use zellij_tile_utils::palette_match; +use crate::{action_key, style_key_with_modifier, LinePart}; +use zellij_tile::prelude::{actions::Action, *}; macro_rules! strings { ($ANSIStrings:expr) => {{ - let strings: &[ANSIString<'static>] = $ANSIStrings; + let strings: &[ANSIString] = $ANSIStrings; let ansi_strings = ANSIStrings(strings); @@ -21,49 +16,57 @@ macro_rules! strings { }}; } -pub fn floating_panes_mouse_full(palette: Palette) -> LinePart { +pub fn floating_panes_mouse_full(help: &ModeInfo) -> LinePart { // Tip: Toggle floating panes with Ctrl +

+ and move them with keyboard or mouse - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); - - strings!(&[ + let mut bits = vec![ Style::new().paint(" Tip: "), Style::new().paint("Toggle floating panes with "), - Style::new().fg(orange_color).bold().paint("Ctrl"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("

"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - Style::new().paint(" and move them with keyboard or mouse"), - ]) + ]; + bits.extend(add_keybinds(help)); + bits.push(Style::new().paint(" and move them with keyboard or mouse")); + strings!(&bits) } -pub fn floating_panes_mouse_medium(palette: Palette) -> LinePart { +pub fn floating_panes_mouse_medium(help: &ModeInfo) -> LinePart { // Tip: Toggle floating panes with Ctrl +

+ - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); - strings!(&[ + let mut bits = vec![ Style::new().paint(" Tip: "), Style::new().paint("Toggle floating panes with "), - Style::new().fg(orange_color).bold().paint("Ctrl"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("

"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - ]) + ]; + bits.extend(add_keybinds(help)); + strings!(&bits) } -pub fn floating_panes_mouse_short(palette: Palette) -> LinePart { +pub fn floating_panes_mouse_short(help: &ModeInfo) -> LinePart { // Ctrl +

+ => floating panes - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); + let mut bits = add_keybinds(help); + bits.push(Style::new().paint(" => floating panes")); + strings!(&bits) +} + +fn add_keybinds(help: &ModeInfo) -> Vec { + let to_pane = action_key( + &help.get_mode_keybinds(), + &[Action::SwitchToMode(InputMode::Pane)], + ); + let floating_toggle = action_key( + &help.get_keybinds_for_mode(InputMode::Pane), + &[ + Action::ToggleFloatingPanes, + Action::SwitchToMode(InputMode::Normal), + ], + ); + + if floating_toggle.is_empty() { + return vec![Style::new().bold().paint("UNBOUND")]; + } - strings!(&[ - Style::new().fg(orange_color).bold().paint(" Ctrl"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("

"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - Style::new().paint(" => floating panes"), - ]) + let mut bits = vec![]; + bits.extend(style_key_with_modifier(&to_pane, &help.style.colors)); + bits.push(Style::new().paint(", ")); + bits.extend(style_key_with_modifier( + &floating_toggle, + &help.style.colors, + )); + bits } diff --git a/default-plugins/status-bar/src/tip/data/move_focus_hjkl_tab_switch.rs b/default-plugins/status-bar/src/tip/data/move_focus_hjkl_tab_switch.rs index 8e9f678c6e..b7a62ab8b3 100644 --- a/default-plugins/status-bar/src/tip/data/move_focus_hjkl_tab_switch.rs +++ b/default-plugins/status-bar/src/tip/data/move_focus_hjkl_tab_switch.rs @@ -1,16 +1,14 @@ -use ansi_term::{ - unstyled_len, ANSIString, ANSIStrings, - Color::{Fixed, RGB}, - Style, -}; +use ansi_term::{unstyled_len, ANSIString, ANSIStrings, Style}; -use crate::LinePart; -use zellij_tile::prelude::*; -use zellij_tile_utils::palette_match; +use crate::{action_key_group, style_key_with_modifier, LinePart}; +use zellij_tile::prelude::{ + actions::{Action, Direction}, + *, +}; macro_rules! strings { ($ANSIStrings:expr) => {{ - let strings: &[ANSIString<'static>] = $ANSIStrings; + let strings: &[ANSIString] = $ANSIStrings; let ansi_strings = ANSIStrings(strings); @@ -21,44 +19,71 @@ macro_rules! strings { }}; } -pub fn move_focus_hjkl_tab_switch_full(palette: Palette) -> LinePart { +pub fn move_focus_hjkl_tab_switch_full(help: &ModeInfo) -> LinePart { // Tip: When changing focus with Alt + <←↓↑→> moving off screen left/right focuses the next tab. - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); - - strings!(&[ + let mut bits = vec![ Style::new().paint(" Tip: "), Style::new().paint("When changing focus with "), - Style::new().fg(orange_color).bold().paint("Alt"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("<←↓↑→>"), - Style::new().paint(" moving off screen left/right focuses the next tab."), - ]) + ]; + bits.extend(add_keybinds(help)); + bits.push(Style::new().paint(" moving off screen left/right focuses the next tab.")); + strings!(&bits) } -pub fn move_focus_hjkl_tab_switch_medium(palette: Palette) -> LinePart { +pub fn move_focus_hjkl_tab_switch_medium(help: &ModeInfo) -> LinePart { // Tip: Changing focus with Alt + <←↓↑→> off screen focuses the next tab. - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); - strings!(&[ + let mut bits = vec![ Style::new().paint(" Tip: "), Style::new().paint("Changing focus with "), - Style::new().fg(orange_color).bold().paint("Alt"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("<←↓↑→>"), - Style::new().paint(" off screen focuses the next tab."), - ]) + ]; + bits.extend(add_keybinds(help)); + bits.push(Style::new().paint(" off screen focuses the next tab.")); + strings!(&bits) } -pub fn move_focus_hjkl_tab_switch_short(palette: Palette) -> LinePart { +pub fn move_focus_hjkl_tab_switch_short(help: &ModeInfo) -> LinePart { // Alt + <←↓↑→> off screen edge focuses next tab. - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); + let mut bits = add_keybinds(help); + bits.push(Style::new().paint(" off screen edge focuses next tab.")); + strings!(&bits) +} + +fn add_keybinds(help: &ModeInfo) -> Vec { + let pane_keymap = help.get_keybinds_for_mode(InputMode::Pane); + let move_focus_keys = action_key_group( + &pane_keymap, + &[ + &[Action::MoveFocusOrTab(Direction::Left)], + &[Action::MoveFocusOrTab(Direction::Right)], + ], + ); - strings!(&[ - Style::new().fg(orange_color).bold().paint(" Alt"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("<←↓↑→>"), - Style::new().paint(" off screen edge focuses next tab."), - ]) + // Let's see if we have some pretty groups in common here + let mut arrows = vec![]; + let mut letters = vec![]; + for key in move_focus_keys.into_iter() { + let key_str = key.to_string(); + if key_str.contains('←') + || key_str.contains('↓') + || key_str.contains('↑') + || key_str.contains('→') + { + arrows.push(key); + } else { + letters.push(key); + } + } + let arrows = style_key_with_modifier(&arrows, &help.style.colors); + let letters = style_key_with_modifier(&letters, &help.style.colors); + if arrows.is_empty() && letters.is_empty() { + vec![Style::new().bold().paint("UNBOUND")] + } else if arrows.is_empty() || letters.is_empty() { + arrows.into_iter().chain(letters.into_iter()).collect() + } else { + arrows + .into_iter() + .chain(vec![Style::new().paint(" or ")].into_iter()) + .chain(letters.into_iter()) + .collect() + } } diff --git a/default-plugins/status-bar/src/tip/data/quicknav.rs b/default-plugins/status-bar/src/tip/data/quicknav.rs index 21d46fe8ff..59b604a701 100644 --- a/default-plugins/status-bar/src/tip/data/quicknav.rs +++ b/default-plugins/status-bar/src/tip/data/quicknav.rs @@ -1,16 +1,14 @@ -use ansi_term::{ - unstyled_len, ANSIString, ANSIStrings, - Color::{Fixed, RGB}, - Style, -}; +use ansi_term::{unstyled_len, ANSIString, ANSIStrings, Style}; -use crate::LinePart; -use zellij_tile::prelude::*; -use zellij_tile_utils::palette_match; +use crate::{action_key, action_key_group, style_key_with_modifier, LinePart}; +use zellij_tile::prelude::{ + actions::{Action, Direction, ResizeDirection}, + *, +}; macro_rules! strings { ($ANSIStrings:expr) => {{ - let strings: &[ANSIString<'static>] = $ANSIStrings; + let strings: &[ANSIString] = $ANSIStrings; let ansi_strings = ANSIStrings(strings); @@ -21,66 +19,115 @@ macro_rules! strings { }}; } -pub fn quicknav_full(palette: Palette) -> LinePart { - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); +pub fn quicknav_full(help: &ModeInfo) -> LinePart { + let groups = add_keybinds(help); + + let mut bits = vec![Style::new().paint(" Tip: ")]; + bits.extend(groups.new_pane); + bits.push(Style::new().paint(" => open new pane. ")); + bits.extend(groups.move_focus); + bits.push(Style::new().paint(" => navigate between panes. ")); + bits.extend(groups.resize); + bits.push(Style::new().paint(" => increase/decrease pane size.")); + strings!(&bits) +} + +pub fn quicknav_medium(help: &ModeInfo) -> LinePart { + let groups = add_keybinds(help); - strings!(&[ - Style::new().paint(" Tip: "), - Style::new().fg(orange_color).bold().paint("Alt"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - Style::new().paint(" => open new pane. "), - Style::new().fg(orange_color).bold().paint("Alt"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("<←↓↑→"), - Style::new().paint(" or "), - Style::new().fg(green_color).bold().paint("hjkl>"), - Style::new().paint(" => navigate between panes. "), - Style::new().fg(orange_color).bold().paint("Alt"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("<+->"), - Style::new().paint(" => increase/decrease pane size."), - ]) + let mut bits = vec![Style::new().paint(" Tip: ")]; + bits.extend(groups.new_pane); + bits.push(Style::new().paint(" => new pane. ")); + bits.extend(groups.move_focus); + bits.push(Style::new().paint(" => navigate. ")); + bits.extend(groups.resize); + bits.push(Style::new().paint(" => resize pane.")); + strings!(&bits) } -pub fn quicknav_medium(palette: Palette) -> LinePart { - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); +pub fn quicknav_short(help: &ModeInfo) -> LinePart { + let groups = add_keybinds(help); - strings!(&[ - Style::new().paint(" Tip: "), - Style::new().fg(orange_color).bold().paint("Alt"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - Style::new().paint(" => new pane. "), - Style::new().fg(orange_color).bold().paint("Alt"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("<←↓↑→"), - Style::new().paint(" or "), - Style::new().fg(green_color).bold().paint("hjkl>"), - Style::new().paint(" => navigate. "), - Style::new().fg(orange_color).bold().paint("Alt"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("<+->"), - Style::new().paint(" => resize pane."), - ]) + let mut bits = vec![Style::new().paint(" QuickNav: ")]; + bits.extend(groups.new_pane); + bits.push(Style::new().paint(" / ")); + bits.extend(groups.move_focus); + bits.push(Style::new().paint(" / ")); + bits.extend(groups.resize); + strings!(&bits) } -pub fn quicknav_short(palette: Palette) -> LinePart { - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); +struct Keygroups<'a> { + new_pane: Vec>, + move_focus: Vec>, + resize: Vec>, +} + +fn add_keybinds(help: &ModeInfo) -> Keygroups { + let normal_keymap = help.get_mode_keybinds(); + let new_pane_keys = action_key(&normal_keymap, &[Action::NewPane(None)]); + let new_pane = if new_pane_keys.is_empty() { + vec![Style::new().bold().paint("UNBOUND")] + } else { + style_key_with_modifier(&new_pane_keys, &help.style.colors) + }; + + let resize_keys = action_key_group( + &normal_keymap, + &[ + &[Action::Resize(ResizeDirection::Increase)], + &[Action::Resize(ResizeDirection::Decrease)], + ], + ); + let resize = if resize_keys.is_empty() { + vec![Style::new().bold().paint("UNBOUND")] + } else { + style_key_with_modifier(&resize_keys, &help.style.colors) + }; + + let move_focus_keys = action_key_group( + &normal_keymap, + &[ + &[Action::MoveFocus(Direction::Left)], + &[Action::MoveFocusOrTab(Direction::Left)], + &[Action::MoveFocus(Direction::Down)], + &[Action::MoveFocus(Direction::Up)], + &[Action::MoveFocus(Direction::Right)], + &[Action::MoveFocusOrTab(Direction::Right)], + ], + ); + // Let's see if we have some pretty groups in common here + let mut arrows = vec![]; + let mut letters = vec![]; + for key in move_focus_keys.into_iter() { + let key_str = key.to_string(); + if key_str.contains('←') + || key_str.contains('↓') + || key_str.contains('↑') + || key_str.contains('→') + { + arrows.push(key); + } else { + letters.push(key); + } + } + let arrows = style_key_with_modifier(&arrows, &help.style.colors); + let letters = style_key_with_modifier(&letters, &help.style.colors); + let move_focus = if arrows.is_empty() && letters.is_empty() { + vec![Style::new().bold().paint("UNBOUND")] + } else if arrows.is_empty() || letters.is_empty() { + arrows.into_iter().chain(letters.into_iter()).collect() + } else { + arrows + .into_iter() + .chain(vec![Style::new().paint(" or ")].into_iter()) + .chain(letters.into_iter()) + .collect() + }; - strings!(&[ - Style::new().paint(" QuickNav: "), - Style::new().fg(orange_color).bold().paint("Alt"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("n"), - Style::new().paint("/"), - Style::new().fg(green_color).bold().paint("<←↓↑→"), - Style::new().paint("/"), - Style::new().fg(green_color).bold().paint("hjkl"), - Style::new().paint("/"), - Style::new().fg(green_color).bold().paint("+->"), - ]) + Keygroups { + new_pane, + move_focus, + resize, + } } diff --git a/default-plugins/status-bar/src/tip/data/send_mouse_click_to_terminal.rs b/default-plugins/status-bar/src/tip/data/send_mouse_click_to_terminal.rs index a4f7ee60a8..7daabeb6dd 100644 --- a/default-plugins/status-bar/src/tip/data/send_mouse_click_to_terminal.rs +++ b/default-plugins/status-bar/src/tip/data/send_mouse_click_to_terminal.rs @@ -10,7 +10,7 @@ use zellij_tile_utils::palette_match; macro_rules! strings { ($ANSIStrings:expr) => {{ - let strings: &[ANSIString<'static>] = $ANSIStrings; + let strings: &[ANSIString] = $ANSIStrings; let ansi_strings = ANSIStrings(strings); @@ -21,43 +21,43 @@ macro_rules! strings { }}; } -pub fn mouse_click_to_terminal_full(palette: Palette) -> LinePart { +pub fn mouse_click_to_terminal_full(help: &ModeInfo) -> LinePart { // Tip: SHIFT + bypasses Zellij and sends the mouse click directly to the terminal - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); + let green_color = palette_match!(help.style.colors.green); + let orange_color = palette_match!(help.style.colors.orange); strings!(&[ Style::new().paint(" Tip: "), - Style::new().fg(orange_color).bold().paint("SHIFT"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - Style::new().paint(" bypasses Zellij and sends the mouse click directly to the terminal."), + Style::new().fg(orange_color).bold().paint("Shift"), + Style::new().paint(" + <"), + Style::new().fg(green_color).bold().paint("mouse-click"), + Style::new().paint("> bypasses Zellij and sends the mouse click directly to the terminal."), ]) } -pub fn mouse_click_to_terminal_medium(palette: Palette) -> LinePart { +pub fn mouse_click_to_terminal_medium(help: &ModeInfo) -> LinePart { // Tip: SHIFT + sends the click directly to the terminal - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); + let green_color = palette_match!(help.style.colors.green); + let orange_color = palette_match!(help.style.colors.orange); strings!(&[ Style::new().paint(" Tip: "), - Style::new().fg(orange_color).bold().paint("SHIFT"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - Style::new().paint(" sends the click directly to the terminal."), + Style::new().fg(orange_color).bold().paint("Shift"), + Style::new().paint(" + <"), + Style::new().fg(green_color).bold().paint("mouse-click"), + Style::new().paint("> sends the click directly to the terminal."), ]) } -pub fn mouse_click_to_terminal_short(palette: Palette) -> LinePart { +pub fn mouse_click_to_terminal_short(help: &ModeInfo) -> LinePart { // Tip: SHIFT + => sends click to terminal. - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); + let green_color = palette_match!(help.style.colors.green); + let orange_color = palette_match!(help.style.colors.orange); strings!(&[ Style::new().paint(" Tip: "), - Style::new().fg(orange_color).bold().paint("SHIFT"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - Style::new().paint(" => sends click to terminal."), + Style::new().fg(orange_color).bold().paint("Shift"), + Style::new().paint(" + <"), + Style::new().fg(green_color).bold().paint("mouse-click"), + Style::new().paint("> => sends click to terminal."), ]) } diff --git a/default-plugins/status-bar/src/tip/data/sync_tab.rs b/default-plugins/status-bar/src/tip/data/sync_tab.rs index 27d0ae7f06..55331eb3e7 100644 --- a/default-plugins/status-bar/src/tip/data/sync_tab.rs +++ b/default-plugins/status-bar/src/tip/data/sync_tab.rs @@ -1,16 +1,11 @@ -use ansi_term::{ - unstyled_len, ANSIString, ANSIStrings, - Color::{Fixed, RGB}, - Style, -}; +use ansi_term::{unstyled_len, ANSIString, ANSIStrings, Style}; -use crate::LinePart; -use zellij_tile::prelude::*; -use zellij_tile_utils::palette_match; +use crate::{action_key, style_key_with_modifier, LinePart}; +use zellij_tile::prelude::{actions::Action, *}; macro_rules! strings { ($ANSIStrings:expr) => {{ - let strings: &[ANSIString<'static>] = $ANSIStrings; + let strings: &[ANSIString] = $ANSIStrings; let ansi_strings = ANSIStrings(strings); @@ -21,49 +16,53 @@ macro_rules! strings { }}; } -pub fn sync_tab_full(palette: Palette) -> LinePart { +pub fn sync_tab_full(help: &ModeInfo) -> LinePart { // Tip: Sync a tab and write keyboard input to all panes with Ctrl + + - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); - - strings!(&[ + let mut bits = vec![ Style::new().paint(" Tip: "), Style::new().paint("Sync a tab and write keyboard input to all its panes with "), - Style::new().fg(orange_color).bold().paint("Ctrl"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - ]) + ]; + bits.extend(add_keybinds(help)); + strings!(&bits) } -pub fn sync_tab_medium(palette: Palette) -> LinePart { +pub fn sync_tab_medium(help: &ModeInfo) -> LinePart { // Tip: Sync input to panes in a tab with Ctrl + + - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); - - strings!(&[ + let mut bits = vec![ Style::new().paint(" Tip: "), Style::new().paint("Sync input to panes in a tab with "), - Style::new().fg(orange_color).bold().paint("Ctrl"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - ]) + ]; + bits.extend(add_keybinds(help)); + strings!(&bits) } -pub fn sync_tab_short(palette: Palette) -> LinePart { +pub fn sync_tab_short(help: &ModeInfo) -> LinePart { // Sync input in a tab with Ctrl + + - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); + let mut bits = vec![Style::new().paint(" Sync input in a tab with ")]; + bits.extend(add_keybinds(help)); + strings!(&bits) +} + +fn add_keybinds(help: &ModeInfo) -> Vec { + let to_tab = action_key( + &help.get_mode_keybinds(), + &[Action::SwitchToMode(InputMode::Tab)], + ); + let sync_tabs = action_key( + &help.get_keybinds_for_mode(InputMode::Tab), + &[ + Action::ToggleActiveSyncTab, + Action::SwitchToMode(InputMode::Normal), + ], + ); + + if sync_tabs.is_empty() { + return vec![Style::new().bold().paint("UNBOUND")]; + } - strings!(&[ - Style::new().paint(" Sync input in a tab with "), - Style::new().fg(orange_color).bold().paint("Ctrl"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - ]) + let mut bits = vec![]; + bits.extend(style_key_with_modifier(&to_tab, &help.style.colors)); + bits.push(Style::new().paint(", ")); + bits.extend(style_key_with_modifier(&sync_tabs, &help.style.colors)); + bits } diff --git a/default-plugins/status-bar/src/tip/data/use_mouse.rs b/default-plugins/status-bar/src/tip/data/use_mouse.rs index b9c8de580f..4bbfbf948d 100644 --- a/default-plugins/status-bar/src/tip/data/use_mouse.rs +++ b/default-plugins/status-bar/src/tip/data/use_mouse.rs @@ -10,7 +10,7 @@ use zellij_tile_utils::palette_match; macro_rules! strings { ($ANSIStrings:expr) => {{ - let strings: &[ANSIString<'static>] = $ANSIStrings; + let strings: &[ANSIString] = $ANSIStrings; let ansi_strings = ANSIStrings(strings); @@ -21,10 +21,10 @@ macro_rules! strings { }}; } -pub fn use_mouse_full(palette: Palette) -> LinePart { +pub fn use_mouse_full(help: &ModeInfo) -> LinePart { // Tip: Use the mouse to switch pane focus, scroll through the pane // scrollbuffer, switch or scroll through tabs - let green_color = palette_match!(palette.green); + let green_color = palette_match!(help.style.colors.green); strings!(&[ Style::new().paint(" Tip: "), @@ -33,10 +33,10 @@ pub fn use_mouse_full(palette: Palette) -> LinePart { ]) } -pub fn use_mouse_medium(palette: Palette) -> LinePart { +pub fn use_mouse_medium(help: &ModeInfo) -> LinePart { // Tip: Use the mouse to switch panes/tabs or scroll through the pane // scrollbuffer - let green_color = palette_match!(palette.green); + let green_color = palette_match!(help.style.colors.green); strings!(&[ Style::new().paint(" Tip: "), @@ -45,9 +45,9 @@ pub fn use_mouse_medium(palette: Palette) -> LinePart { ]) } -pub fn use_mouse_short(palette: Palette) -> LinePart { +pub fn use_mouse_short(help: &ModeInfo) -> LinePart { // Tip: Use the mouse to switch panes/tabs or scroll - let green_color = palette_match!(palette.green); + let green_color = palette_match!(help.style.colors.green); strings!(&[ Style::new().fg(green_color).bold().paint(" Use the mouse"), diff --git a/default-plugins/status-bar/src/tip/data/zellij_setup_check.rs b/default-plugins/status-bar/src/tip/data/zellij_setup_check.rs index c45deac205..3dd7b6d853 100644 --- a/default-plugins/status-bar/src/tip/data/zellij_setup_check.rs +++ b/default-plugins/status-bar/src/tip/data/zellij_setup_check.rs @@ -10,7 +10,7 @@ use zellij_tile_utils::palette_match; macro_rules! strings { ($ANSIStrings:expr) => {{ - let strings: &[ANSIString<'static>] = $ANSIStrings; + let strings: &[ANSIString] = $ANSIStrings; let ansi_strings = ANSIStrings(strings); @@ -21,9 +21,9 @@ macro_rules! strings { }}; } -pub fn zellij_setup_check_full(palette: Palette) -> LinePart { +pub fn zellij_setup_check_full(help: &ModeInfo) -> LinePart { // Tip: Having issues with Zellij? Try running "zellij setup --check" - let orange_color = palette_match!(palette.orange); + let orange_color = palette_match!(help.style.colors.orange); strings!(&[ Style::new().paint(" Tip: "), @@ -35,9 +35,9 @@ pub fn zellij_setup_check_full(palette: Palette) -> LinePart { ]) } -pub fn zellij_setup_check_medium(palette: Palette) -> LinePart { +pub fn zellij_setup_check_medium(help: &ModeInfo) -> LinePart { // Tip: Run "zellij setup --check" to find issues - let orange_color = palette_match!(palette.orange); + let orange_color = palette_match!(help.style.colors.orange); strings!(&[ Style::new().paint(" Tip: "), @@ -50,9 +50,9 @@ pub fn zellij_setup_check_medium(palette: Palette) -> LinePart { ]) } -pub fn zellij_setup_check_short(palette: Palette) -> LinePart { +pub fn zellij_setup_check_short(help: &ModeInfo) -> LinePart { // Run "zellij setup --check" to find issues - let orange_color = palette_match!(palette.orange); + let orange_color = palette_match!(help.style.colors.orange); strings!(&[ Style::new().paint(" Run "), diff --git a/default-plugins/status-bar/src/tip/mod.rs b/default-plugins/status-bar/src/tip/mod.rs index 1249195f9e..9d4fbb70a4 100644 --- a/default-plugins/status-bar/src/tip/mod.rs +++ b/default-plugins/status-bar/src/tip/mod.rs @@ -6,9 +6,8 @@ pub mod utils; use crate::LinePart; use zellij_tile::prelude::*; -pub type TipFn = fn(Palette) -> LinePart; +pub type TipFn = fn(&ModeInfo) -> LinePart; -#[derive(Debug)] pub struct TipBody { pub short: TipFn, pub medium: TipFn, diff --git a/src/tests/e2e/cases.rs b/src/tests/e2e/cases.rs index 47e7edd44b..7457ed544c 100644 --- a/src/tests/e2e/cases.rs +++ b/src/tests/e2e/cases.rs @@ -983,6 +983,41 @@ pub fn accepts_basic_layout() { assert_snapshot!(last_snapshot); } +#[test] +#[ignore] +pub fn status_bar_loads_custom_keybindings() { + let fake_win_size = Size { + cols: 120, + rows: 24, + }; + let config_file_name = "changed_keys.yaml"; + let mut test_attempts = 10; + let last_snapshot = loop { + RemoteRunner::kill_running_sessions(fake_win_size); + let mut runner = RemoteRunner::new_with_config(fake_win_size, config_file_name); + runner.run_all_steps(); + let last_snapshot = runner.take_snapshot_after(Step { + name: "Wait for app to load", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(3, 1) + && remote_terminal.snapshot_contains("$ █ ││$") + && remote_terminal.snapshot_contains("$ ") { + step_is_complete = true; + } + step_is_complete + }, + }); + if runner.test_timed_out && test_attempts > 0 { + test_attempts -= 1; + continue; + } else { + break last_snapshot; + } + }; + assert_snapshot!(last_snapshot); +} + #[test] #[ignore] fn focus_pane_with_mouse() { diff --git a/src/tests/e2e/remote_runner.rs b/src/tests/e2e/remote_runner.rs index 21c91e5134..03fb619e11 100644 --- a/src/tests/e2e/remote_runner.rs +++ b/src/tests/e2e/remote_runner.rs @@ -20,6 +20,7 @@ use std::rc::Rc; const ZELLIJ_EXECUTABLE_LOCATION: &str = "/usr/src/zellij/x86_64-unknown-linux-musl/release/zellij"; const SET_ENV_VARIABLES: &str = "EDITOR=/usr/bin/vi"; const ZELLIJ_LAYOUT_PATH: &str = "/usr/src/zellij/fixtures/layouts"; +const ZELLIJ_CONFIG_PATH: &str = "/usr/src/zellij/fixtures/configs"; const ZELLIJ_DATA_DIR: &str = "/usr/src/zellij/e2e-data"; const ZELLIJ_FIXTURE_PATH: &str = "/usr/src/zellij/fixtures"; const CONNECTION_STRING: &str = "127.0.0.1:2222"; @@ -163,6 +164,25 @@ fn start_zellij_with_layout(channel: &mut ssh2::Channel, layout_path: &str) { std::thread::sleep(std::time::Duration::from_secs(1)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN } +fn start_zellij_with_config(channel: &mut ssh2::Channel, config_path: &str) { + stop_zellij(channel); + channel + .write_all( + format!( + "{} {} --config {} --session {} --data-dir {}\n", + SET_ENV_VARIABLES, + ZELLIJ_EXECUTABLE_LOCATION, + config_path, + SESSION_NAME, + ZELLIJ_DATA_DIR + ) + .as_bytes(), + ) + .unwrap(); + channel.flush().unwrap(); + std::thread::sleep(std::time::Duration::from_secs(1)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN +} + fn read_from_channel( channel: &Arc>, last_snapshot: &Arc>, @@ -587,6 +607,42 @@ impl RemoteRunner { reader_thread, } } + pub fn new_with_config(win_size: Size, config_file_name: &'static str) -> Self { + let remote_path = Path::new(ZELLIJ_CONFIG_PATH).join(config_file_name); + let sess = ssh_connect(); + let mut channel = sess.channel_session().unwrap(); + let mut rows = Dimension::fixed(win_size.rows); + let mut cols = Dimension::fixed(win_size.cols); + rows.set_inner(win_size.rows); + cols.set_inner(win_size.cols); + let pane_geom = PaneGeom { + x: 0, + y: 0, + rows, + cols, + }; + setup_remote_environment(&mut channel, win_size); + start_zellij_with_config(&mut channel, &remote_path.to_string_lossy()); + let channel = Arc::new(Mutex::new(channel)); + let last_snapshot = Arc::new(Mutex::new(String::new())); + let cursor_coordinates = Arc::new(Mutex::new((0, 0))); + sess.set_blocking(false); + let reader_thread = + read_from_channel(&channel, &last_snapshot, &cursor_coordinates, &pane_geom); + RemoteRunner { + steps: vec![], + channel, + currently_running_step: None, + current_step_index: 0, + retries_left: RETRIES, + retry_pause_ms: 100, + test_timed_out: false, + panic_on_no_retries_left: true, + last_snapshot, + cursor_coordinates, + reader_thread, + } + } pub fn dont_panic(mut self) -> Self { self.panic_on_no_retries_left = false; self diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__bracketed_paste.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__bracketed_paste.snap index 842eaed82c..8b66d5ace2 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__bracketed_paste.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__bracketed_paste.snap @@ -26,4 +26,4 @@ expression: last_snapshot │ │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - Tip: Alt + => new pane. Alt + <←↓↑→ or hjkl> => navigate. Alt + <+-> => resize pane. + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|=|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap index ed01a3b93a..a5e456d25d 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap @@ -21,5 +21,5 @@ expression: last_snapshot │ │ │ │ └──────┘ - Ctrl + + diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__close_pane.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__close_pane.snap index 8a6f9f4001..0d431c3292 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__close_pane.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__close_pane.snap @@ -26,4 +26,4 @@ expression: last_snapshot │ │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - Tip: Alt + => new pane. Alt + <←↓↑→ or hjkl> => navigate. Alt + <+-> => resize pane. + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|=|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__close_tab.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__close_tab.snap index 078f7d4eea..603432fdfe 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__close_tab.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__close_tab.snap @@ -26,4 +26,4 @@ expression: last_snapshot │ ││ │ └──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - Tip: Alt + => new pane. Alt + <←↓↑→ or hjkl> => navigate. Alt + <+-> => resize pane. + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|=|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__detach_and_attach_session.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__detach_and_attach_session.snap index e4f8f1d314..92d2a0378c 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__detach_and_attach_session.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__detach_and_attach_session.snap @@ -26,4 +26,4 @@ expression: last_snapshot │ ││ │ └──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - Tip: Alt + => new pane. Alt + <←↓↑→ or hjkl> => navigate. Alt + <+-> => resize pane. + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|=|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__focus_pane_with_mouse.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__focus_pane_with_mouse.snap index 21e1e7b11e..ef923aa2ed 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__focus_pane_with_mouse.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__focus_pane_with_mouse.snap @@ -26,4 +26,4 @@ expression: last_snapshot │ ││ │ └──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - Tip: Alt + => new pane. Alt + <←↓↑→ or hjkl> => navigate. Alt + <+-> => resize pane. + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|=|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__focus_tab_with_layout.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__focus_tab_with_layout.snap index d07e3f2a44..920fde6336 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__focus_tab_with_layout.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__focus_tab_with_layout.snap @@ -26,4 +26,4 @@ expression: last_snapshot │ ││ │ └──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - Tip: Alt + => new pane. Alt + <←↓↑→ or hjkl> => navigate. Alt + <+-> => resize pane. + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|=|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__lock_mode.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__lock_mode.snap index f47db23e8a..5583430dab 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__lock_mode.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__lock_mode.snap @@ -25,5 +25,5 @@ expression: last_snapshot │ │ │ │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ - Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  + Ctrl + LOCK  <> PANE  <> TAB  <> RESIZE  <> MOVE  <> SEARCH  <> SESSION  <> QUIT  -- INTERFACE LOCKED -- diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions-2.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions-2.snap index c50ca954e3..f5c9f44c43 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions-2.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions-2.snap @@ -26,4 +26,4 @@ expression: second_runner_snapshot │ ││ │ └──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - <←↓↑→> Move focus / New / Close / Rename / Sync / Toggle / Select pane + <←→> Move focus / New / Close / Rename / Sync / Toggle / Select pane diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions.snap index a12aa434ba..68965f48e9 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions.snap @@ -26,4 +26,4 @@ expression: first_runner_snapshot │ ││ │ └──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - Tip: Alt + => new pane. Alt + <←↓↑→ or hjkl> => navigate. Alt + <+-> => resize pane. + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|=|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_panes_and_same_tab-2.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_panes_and_same_tab-2.snap index 3ad31dcf11..ffcb6b2fd0 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_panes_and_same_tab-2.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_panes_and_same_tab-2.snap @@ -26,4 +26,4 @@ expression: second_runner_snapshot │ ││ │ └──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - Tip: Alt + => new pane. Alt + <←↓↑→ or hjkl> => navigate. Alt + <+-> => resize pane. + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|=|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_panes_and_same_tab.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_panes_and_same_tab.snap index 4abaae5907..797448a6a1 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_panes_and_same_tab.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_panes_and_same_tab.snap @@ -26,4 +26,4 @@ expression: first_runner_snapshot │ ││ │ └──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - Tip: Alt + => new pane. Alt + <←↓↑→ or hjkl> => navigate. Alt + <+-> => resize pane. + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|=|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_tabs-2.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_tabs-2.snap index 8c6b4face8..cd4006d5b6 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_tabs-2.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_tabs-2.snap @@ -26,4 +26,4 @@ expression: second_runner_snapshot │ │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - Tip: Alt + => new pane. Alt + <←↓↑→ or hjkl> => navigate. Alt + <+-> => resize pane. + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|=|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_tabs.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_tabs.snap index 19f23c390f..8856f1f9f3 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_tabs.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_tabs.snap @@ -26,4 +26,4 @@ expression: first_runner_snapshot │ │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - Tip: Alt + => new pane. Alt + <←↓↑→ or hjkl> => navigate. Alt + <+-> => resize pane. + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|=|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_same_pane_and_tab-2.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_same_pane_and_tab-2.snap index 74fb1a5710..d7328b8db3 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_same_pane_and_tab-2.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_same_pane_and_tab-2.snap @@ -26,4 +26,4 @@ expression: second_runner_snapshot │ │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - Tip: Alt + => new pane. Alt + <←↓↑→ or hjkl> => navigate. Alt + <+-> => resize pane. + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|=|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_same_pane_and_tab.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_same_pane_and_tab.snap index 2aec1c4d62..6bc0369c93 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_same_pane_and_tab.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_same_pane_and_tab.snap @@ -26,4 +26,4 @@ expression: first_runner_snapshot │ │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - Tip: Alt + => new pane. Alt + <←↓↑→ or hjkl> => navigate. Alt + <+-> => resize pane. + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|=|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__open_new_tab.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__open_new_tab.snap index 032a03caa5..2d84b3f39c 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__open_new_tab.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__open_new_tab.snap @@ -26,4 +26,4 @@ expression: last_snapshot │ │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - Tip: Alt + => new pane. Alt + <←↓↑→ or hjkl> => navigate. Alt + <+-> => resize pane. + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|=|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__resize_pane.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__resize_pane.snap index d78a61df48..f03656301c 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__resize_pane.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__resize_pane.snap @@ -26,4 +26,4 @@ expression: last_snapshot │ ││ │ └────────────────────────────────────────────────────┘└────────────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - Tip: Alt + => new pane. Alt + <←↓↑→ or hjkl> => navigate. Alt + <+-> => resize pane. + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|=|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__resize_terminal_window.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__resize_terminal_window.snap index 513b0ec67c..de1ef639a1 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__resize_terminal_window.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__resize_terminal_window.snap @@ -26,4 +26,4 @@ expression: last_snapshot │ ││ │ └────────────────────────────────────────────────┘└────────────────────────────────────────────────┘ Ctrl + g  p  t  n  h  s  o  q  - Tip: Alt + => new pane. Alt + <←↓↑→ or hjkl> => navigate. Alt + <+-> => resize pane. + QuickNav: Alt + / Alt + <←↓↑→> or Alt + / Alt + <+|=|-> diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane.snap index b66ba6c569..ff4ce54413 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane.snap @@ -26,4 +26,4 @@ expression: last_snapshot │ ││li█e21 │ └──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - <↓↑> Scroll / Scroll / Scroll / Edit / Enter / Select pane + <↓↑> Scroll / Scroll / Scroll / Edit / Search / Select diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane_with_mouse.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane_with_mouse.snap index 2bcdfaede9..ec416eeab4 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane_with_mouse.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane_with_mouse.snap @@ -26,4 +26,4 @@ expression: last_snapshot │ ││li█e19 │ └──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - Tip: Alt + => new pane. Alt + <←↓↑→ or hjkl> => navigate. Alt + <+-> => resize pane. + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|=|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__split_terminals_vertically.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__split_terminals_vertically.snap index 76411adb64..22b796279e 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__split_terminals_vertically.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__split_terminals_vertically.snap @@ -26,4 +26,4 @@ expression: last_snapshot │ ││ │ └──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - Tip: Alt + => new pane. Alt + <←↓↑→ or hjkl> => navigate. Alt + <+-> => resize pane. + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|=|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__start_without_pane_frames.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__start_without_pane_frames.snap index 51289dbb22..f066d20782 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__start_without_pane_frames.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__start_without_pane_frames.snap @@ -26,4 +26,4 @@ $ │$ █ │ │ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - Tip: Alt + => new pane. Alt + <←↓↑→ or hjkl> => navigate. Alt + <+-> => resize pane. + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|=|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__starts_with_one_terminal.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__starts_with_one_terminal.snap index c99691342c..1f4ff4f02d 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__starts_with_one_terminal.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__starts_with_one_terminal.snap @@ -26,4 +26,4 @@ expression: last_snapshot │ │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - Tip: Alt + => new pane. Alt + <←↓↑→ or hjkl> => navigate. Alt + <+-> => resize pane. + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|=|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__status_bar_loads_custom_keybindings.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__status_bar_loads_custom_keybindings.snap new file mode 100644 index 0000000000..5a88e98a78 --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__status_bar_loads_custom_keybindings.snap @@ -0,0 +1,29 @@ +--- +source: src/tests/e2e/cases.rs +assertion_line: 398 +expression: last_snapshot +--- + Zellij (e2e-test)  Tab #1  +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + LOCK  PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  + Tip: UNBOUND => open new pane. UNBOUND => navigate between panes. UNBOUND => increase/decrease pane size. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__tmux_mode.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__tmux_mode.snap index 1c85d159ec..05985a5f27 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__tmux_mode.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__tmux_mode.snap @@ -26,4 +26,4 @@ expression: last_snapshot │ ││ │ └──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - Tip: Alt + => new pane. Alt + <←↓↑→ or hjkl> => navigate. Alt + <+-> => resize pane. + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|=|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__toggle_floating_panes.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__toggle_floating_panes.snap index b30448fe32..81c4b4fe1d 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__toggle_floating_panes.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__toggle_floating_panes.snap @@ -26,4 +26,4 @@ expression: last_snapshot │ │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - (FLOATING PANES VISIBLE): Press Ctrl-p + to hide. + (FLOATING PANES VISIBLE): Press Ctrl+p, to hide. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__typing_exit_closes_pane.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__typing_exit_closes_pane.snap index 81b3e55473..3a1643b16d 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__typing_exit_closes_pane.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__typing_exit_closes_pane.snap @@ -26,4 +26,4 @@ expression: last_snapshot │ │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - Tip: Alt + => new pane. Alt + <←↓↑→ or hjkl> => navigate. Alt + <+-> => resize pane. + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|=|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__undo_rename_pane.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__undo_rename_pane.snap index c5c77a31c2..03b44ebb1e 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__undo_rename_pane.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__undo_rename_pane.snap @@ -26,4 +26,4 @@ expression: last_snapshot │ │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - Tip: Alt + => new pane. Alt + <←↓↑→ or hjkl> => navigate. Alt + <+-> => resize pane. + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|=|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__undo_rename_tab.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__undo_rename_tab.snap index f80671e137..ed9a8e20e8 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__undo_rename_tab.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__undo_rename_tab.snap @@ -26,4 +26,4 @@ expression: last_snapshot │ │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - Tip: Alt + => new pane. Alt + <←↓↑→ or hjkl> => navigate. Alt + <+-> => resize pane. + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|=|-> => resize pane. diff --git a/src/tests/fixtures/configs/changed_keys.yaml b/src/tests/fixtures/configs/changed_keys.yaml new file mode 100644 index 0000000000..7d2a8c714d --- /dev/null +++ b/src/tests/fixtures/configs/changed_keys.yaml @@ -0,0 +1,26 @@ +--- +# Configuration for zellij. + +# In order to troubleshoot your configuration try using the following command: +# `zellij setup --check` +# It should show current config locations and features that are enabled. + +keybinds: + unbind: true + normal: + - action: [SwitchToMode: Locked,] + key: [F: 1] + - action: [SwitchToMode: Pane,] + key: [F: 2] + - action: [SwitchToMode: Tab,] + key: [F: 3] + - action: [SwitchToMode: Resize,] + key: [F: 4] + - action: [SwitchToMode: Move,] + key: [F: 5] + - action: [SwitchToMode: Scroll,] + key: [F: 6] + - action: [SwitchToMode: Session,] + key: [F: 7] + - action: [Quit,] + key: [F: 8] diff --git a/zellij-client/src/fake_client.rs b/zellij-client/src/fake_client.rs index 88b41c9e89..475298e958 100644 --- a/zellij-client/src/fake_client.rs +++ b/zellij-client/src/fake_client.rs @@ -49,6 +49,7 @@ pub fn start_fake_client( colors: palette, rounded_corners: config.ui.unwrap_or_default().pane_frames.rounded_corners, }, + keybinds: config.keybinds.clone(), }; let first_msg = ClientToServerMsg::AttachClient(client_attributes, config_options.clone()); diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs index 3b81694b41..c9c68db352 100644 --- a/zellij-client/src/lib.rs +++ b/zellij-client/src/lib.rs @@ -159,6 +159,7 @@ pub fn start_client( colors: palette, rounded_corners: config.ui.unwrap_or_default().pane_frames.rounded_corners, }, + keybinds: config.keybinds.clone(), }; let first_msg = match info { diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index 7b9bf798bc..93ea824d57 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -39,7 +39,7 @@ use zellij_utils::{ channels::{self, ChannelWithContext, SenderWithContext}, cli::CliArgs, consts::{DEFAULT_SCROLL_BUFFER_SIZE, SCROLL_BUFFER_SIZE}, - data::{Event, PluginCapabilities, Style}, + data::{Event, PluginCapabilities}, errors::{ContextType, ErrorInstruction, ServerContext}, input::{ command::{RunCommand, TerminalAction}, @@ -104,7 +104,7 @@ impl ErrorInstruction for ServerInstruction { pub(crate) struct SessionMetaData { pub senders: ThreadSenders, pub capabilities: PluginCapabilities, - pub style: Style, + pub client_attributes: ClientAttributes, pub default_shell: Option, screen_thread: Option>, pty_thread: Option>, @@ -285,7 +285,7 @@ pub fn start_server(mut os_input: Box, socket_path: PathBuf) { let session = init_session( os_input.clone(), to_server.clone(), - client_attributes, + client_attributes.clone(), SessionOptions { opts, layout: layout.clone(), @@ -378,7 +378,7 @@ pub fn start_server(mut os_input: Box, socket_path: PathBuf) { .send_to_plugin(PluginInstruction::AddClient(client_id)) .unwrap(); let default_mode = options.default_mode.unwrap_or_default(); - let mode_info = get_mode_info(default_mode, attrs.style, session_data.capabilities); + let mode_info = get_mode_info(default_mode, &attrs, session_data.capabilities); let mode = mode_info.mode; session_data .senders @@ -654,8 +654,14 @@ fn init_session( ); let max_panes = opts.max_panes; + let client_attributes_clone = client_attributes.clone(); move || { - screen_thread_main(screen_bus, max_panes, client_attributes, config_options); + screen_thread_main( + screen_bus, + max_panes, + client_attributes_clone, + config_options, + ); } }) .unwrap(); @@ -705,7 +711,7 @@ fn init_session( }, capabilities, default_shell, - style: client_attributes.style, + client_attributes, screen_thread: Some(screen_thread), pty_thread: Some(pty_thread), wasm_thread: Some(wasm_thread), diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index dc80814bb0..2505c4cbe9 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -75,22 +75,23 @@ fn route_action( .unwrap(); }, Action::SwitchToMode(mode) => { - let style = session.style; + let attrs = &session.client_attributes; // TODO: use the palette from the client and remove it from the server os api // this is left here as a stop gap measure until we shift some code around // to allow for this + // TODO: Need access to `ClientAttributes` here session .senders .send_to_plugin(PluginInstruction::Update( None, Some(client_id), - Event::ModeUpdate(get_mode_info(mode, style, session.capabilities)), + Event::ModeUpdate(get_mode_info(mode, attrs, session.capabilities)), )) .unwrap(); session .senders .send_to_screen(ScreenInstruction::ChangeMode( - get_mode_info(mode, style, session.capabilities), + get_mode_info(mode, attrs, session.capabilities), client_id, )) .unwrap(); diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 7b354c16d9..79cbdb5706 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -915,7 +915,7 @@ pub(crate) fn screen_thread_main( max_panes, get_mode_info( config_options.default_mode.unwrap_or_default(), - client_attributes.style, + &client_attributes, PluginCapabilities { arrow_fonts: capabilities.unwrap_or_default(), }, diff --git a/zellij-tile/src/prelude.rs b/zellij-tile/src/prelude.rs index 196696a4c9..8190206673 100644 --- a/zellij-tile/src/prelude.rs +++ b/zellij-tile/src/prelude.rs @@ -1,3 +1,4 @@ pub use crate::shim::*; pub use crate::*; pub use zellij_utils::data::*; +pub use zellij_utils::input::actions; diff --git a/zellij-utils/Cargo.toml b/zellij-utils/Cargo.toml index 28f309d338..18051ba59c 100644 --- a/zellij-utils/Cargo.toml +++ b/zellij-utils/Cargo.toml @@ -11,7 +11,7 @@ license = "MIT" [dependencies] anyhow = "1.0.45" backtrace = "0.3.55" -bincode = "1.3.1" +rmp-serde = "1.1.0" clap = { version = "3.2.2", features = ["derive", "env"] } clap_complete = "3.2.1" colored = "2.0.0" diff --git a/zellij-utils/src/consts.rs b/zellij-utils/src/consts.rs index 39d125c781..cb11315fc2 100644 --- a/zellij-utils/src/consts.rs +++ b/zellij-utils/src/consts.rs @@ -1,13 +1,9 @@ //! Zellij program-wide constants. -use crate::envs; -use crate::shared::set_permissions; use directories_next::ProjectDirs; use lazy_static::lazy_static; -use nix::unistd::Uid; use once_cell::sync::OnceCell; use std::path::PathBuf; -use std::{env, fs}; pub const ZELLIJ_CONFIG_FILE_ENV: &str = "ZELLIJ_CONFIG_FILE"; pub const ZELLIJ_CONFIG_DIR_ENV: &str = "ZELLIJ_CONFIG_DIR"; @@ -28,31 +24,8 @@ const fn system_default_data_dir() -> &'static str { } lazy_static! { - static ref UID: Uid = Uid::current(); pub static ref ZELLIJ_PROJ_DIR: ProjectDirs = ProjectDirs::from("org", "Zellij Contributors", "Zellij").unwrap(); - pub static ref ZELLIJ_SOCK_DIR: PathBuf = { - let mut ipc_dir = envs::get_socket_dir().map_or_else( - |_| { - ZELLIJ_PROJ_DIR - .runtime_dir() - .map_or_else(|| ZELLIJ_TMP_DIR.clone(), |p| p.to_owned()) - }, - PathBuf::from, - ); - ipc_dir.push(VERSION); - ipc_dir - }; - pub static ref ZELLIJ_IPC_PIPE: PathBuf = { - let mut sock_dir = ZELLIJ_SOCK_DIR.clone(); - fs::create_dir_all(&sock_dir).unwrap(); - set_permissions(&sock_dir, 0o700).unwrap(); - sock_dir.push(envs::get_session_name().unwrap()); - sock_dir - }; - pub static ref ZELLIJ_TMP_DIR: PathBuf = PathBuf::from(format!("/tmp/zellij-{}", *UID)); - pub static ref ZELLIJ_TMP_LOG_DIR: PathBuf = ZELLIJ_TMP_DIR.join("zellij-log"); - pub static ref ZELLIJ_TMP_LOG_FILE: PathBuf = ZELLIJ_TMP_LOG_DIR.join("zellij.log"); pub static ref ZELLIJ_CACHE_DIR: PathBuf = ZELLIJ_PROJ_DIR.cache_dir().to_path_buf(); } @@ -60,3 +33,42 @@ pub const FEATURES: &[&str] = &[ #[cfg(feature = "disable_automatic_asset_installation")] "disable_automatic_asset_installation", ]; + +#[cfg(unix)] +pub use unix_only::*; + +#[cfg(unix)] +mod unix_only { + use super::*; + use crate::envs; + use crate::shared::set_permissions; + use lazy_static::lazy_static; + use nix::unistd::Uid; + use std::fs; + + lazy_static! { + static ref UID: Uid = Uid::current(); + pub static ref ZELLIJ_IPC_PIPE: PathBuf = { + let mut sock_dir = ZELLIJ_SOCK_DIR.clone(); + fs::create_dir_all(&sock_dir).unwrap(); + set_permissions(&sock_dir, 0o700).unwrap(); + sock_dir.push(envs::get_session_name().unwrap()); + sock_dir + }; + pub static ref ZELLIJ_TMP_DIR: PathBuf = PathBuf::from(format!("/tmp/zellij-{}", *UID)); + pub static ref ZELLIJ_TMP_LOG_DIR: PathBuf = ZELLIJ_TMP_DIR.join("zellij-log"); + pub static ref ZELLIJ_TMP_LOG_FILE: PathBuf = ZELLIJ_TMP_LOG_DIR.join("zellij.log"); + pub static ref ZELLIJ_SOCK_DIR: PathBuf = { + let mut ipc_dir = envs::get_socket_dir().map_or_else( + |_| { + ZELLIJ_PROJ_DIR + .runtime_dir() + .map_or_else(|| ZELLIJ_TMP_DIR.clone(), |p| p.to_owned()) + }, + PathBuf::from, + ); + ipc_dir.push(VERSION); + ipc_dir + }; + } +} diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs index 82ed5da9e3..b28972920c 100644 --- a/zellij-utils/src/data.rs +++ b/zellij-utils/src/data.rs @@ -1,3 +1,4 @@ +use crate::input::actions::Action; use clap::ArgEnum; use serde::{Deserialize, Serialize}; use std::fmt; @@ -30,37 +31,81 @@ pub fn single_client_color(colors: Palette) -> (PaletteColor, PaletteColor) { (colors.green, colors.black) } -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +// TODO: Add a shortened string representation (beyond `Display::fmt` below) that can be used when +// screen space is scarce. Useful for e.g. "ENTER", "SPACE", "TAB" to display as Unicode +// representations instead. +// NOTE: Do not reorder the key variants since that influences what the `status_bar` plugin +// displays! +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] pub enum Key { - Backspace, + PageDown, + PageUp, Left, - Right, - Up, Down, + Up, + Right, Home, End, - PageUp, - PageDown, - BackTab, + Backspace, Delete, Insert, F(u8), Char(char), Alt(CharOrArrow), Ctrl(char), + BackTab, Null, Esc, } -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +impl fmt::Display for Key { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Key::Backspace => write!(f, "BACKSPACE"), + Key::Left => write!(f, "{}", Direction::Left), + Key::Right => write!(f, "{}", Direction::Right), + Key::Up => write!(f, "{}", Direction::Up), + Key::Down => write!(f, "{}", Direction::Down), + Key::Home => write!(f, "HOME"), + Key::End => write!(f, "END"), + Key::PageUp => write!(f, "PgUp"), + Key::PageDown => write!(f, "PgDn"), + Key::BackTab => write!(f, "TAB"), + Key::Delete => write!(f, "DEL"), + Key::Insert => write!(f, "INS"), + Key::F(n) => write!(f, "F{}", n), + Key::Char(c) => match c { + '\n' => write!(f, "ENTER"), + '\t' => write!(f, "TAB"), + ' ' => write!(f, "SPACE"), + _ => write!(f, "{}", c), + }, + Key::Alt(c) => write!(f, "Alt+{}", c), + Key::Ctrl(c) => write!(f, "Ctrl+{}", Key::Char(*c)), + Key::Null => write!(f, "NULL"), + Key::Esc => write!(f, "ESC"), + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] #[serde(untagged)] pub enum CharOrArrow { Char(char), Direction(Direction), } +impl fmt::Display for CharOrArrow { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CharOrArrow::Char(c) => write!(f, "{}", Key::Char(*c)), + CharOrArrow::Direction(d) => write!(f, "{}", d), + } + } +} + /// The four directions (left, right, up, down). -#[derive(Eq, Clone, Copy, Debug, PartialEq, Hash, Deserialize, Serialize)] +#[derive(Eq, Clone, Copy, Debug, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] pub enum Direction { Left, Right, @@ -68,6 +113,17 @@ pub enum Direction { Down, } +impl fmt::Display for Direction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Direction::Left => write!(f, "←"), + Direction::Right => write!(f, "→"), + Direction::Up => write!(f, "↑"), + Direction::Down => write!(f, "↓"), + } + } +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] // FIXME: This should be extended to handle different button clicks (not just // left click) and the `ScrollUp` and `ScrollDown` events could probably be @@ -237,19 +293,36 @@ pub struct Style { pub rounded_corners: bool, } +// FIXME: Poor devs hashtable since HashTable can't derive `Default`... +pub type KeybindsVec = Vec<(InputMode, Vec<(Key, Vec)>)>; + /// Represents the contents of the help message that is printed in the status bar, /// which indicates the current [`InputMode`] and what the keybinds for that mode /// are. Related to the default `status-bar` plugin. -#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ModeInfo { pub mode: InputMode, - // FIXME: This should probably return Keys and Actions, then sort out strings plugin-side - pub keybinds: Vec<(String, String)>, // => + pub keybinds: KeybindsVec, pub style: Style, pub capabilities: PluginCapabilities, pub session_name: Option, } +impl ModeInfo { + pub fn get_mode_keybinds(&self) -> Vec<(Key, Vec)> { + self.get_keybinds_for_mode(self.mode) + } + + pub fn get_keybinds_for_mode(&self, mode: InputMode) -> Vec<(Key, Vec)> { + for (vec_mode, map) in &self.keybinds { + if mode == *vec_mode { + return map.to_vec(); + } + } + vec![] + } +} + #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] pub struct TabInfo { /* subset of fields to publish to plugins */ diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index 17778c4e95..a20d490dd5 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -17,7 +17,7 @@ pub enum Direction { Down, } -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub enum ResizeDirection { Left, Right, @@ -27,13 +27,13 @@ pub enum ResizeDirection { Decrease, } -#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)] pub enum SearchDirection { Down, Up, } -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub enum SearchOption { CaseSensitivity, WholeWord, @@ -45,7 +45,7 @@ pub enum SearchOption { // They might need to be adjusted in the default config // as well `../../assets/config/default.yaml` /// Actions that can be bound to keys. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub enum Action { /// Quit Zellij. Quit, diff --git a/zellij-utils/src/input/keybinds.rs b/zellij-utils/src/input/keybinds.rs index 9f948f898c..6e6f2ab99b 100644 --- a/zellij-utils/src/input/keybinds.rs +++ b/zellij-utils/src/input/keybinds.rs @@ -1,9 +1,9 @@ //! Mapping of inputs to sequences of actions. -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use super::actions::Action; use super::config; -use crate::input::{InputMode, Key}; +use crate::data::{InputMode, Key, KeybindsVec}; use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; @@ -12,7 +12,7 @@ use strum::IntoEnumIterator; #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct Keybinds(HashMap); #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct ModeKeybinds(HashMap>); +pub struct ModeKeybinds(BTreeMap>); /// Intermediate struct used for deserialisation /// Used in the config file. @@ -84,6 +84,20 @@ impl Keybinds { .keybinds } + pub fn to_keybinds_vec(&self) -> KeybindsVec { + let mut ret = vec![]; + for (mode, mode_binds) in &self.0 { + ret.push((*mode, mode_binds.to_cloned_vec())); + } + ret + } + + pub fn get_mode_keybinds(&self, mode: &InputMode) -> &ModeKeybinds { + self.0 + .get(mode) + .expect("Failed to get Keybinds for current mode") + } + /// Entrypoint from the config module pub fn get_default_keybinds_with_config(from_yaml: Option) -> Keybinds { let default_keybinds = match from_yaml.clone() { @@ -221,7 +235,7 @@ impl Keybinds { impl ModeKeybinds { fn new() -> ModeKeybinds { - ModeKeybinds(HashMap::>::new()) + ModeKeybinds(BTreeMap::>::new()) } /// Merges `self` with `other`, if keys are the same, `other` overwrites. @@ -239,6 +253,13 @@ impl ModeKeybinds { } keymap } + + pub fn to_cloned_vec(&self) -> Vec<(Key, Vec)> { + self.0 + .iter() + .map(|(key, vac)| (*key, vac.clone())) + .collect() + } } impl From for Keybinds { @@ -269,7 +290,7 @@ impl From for ModeKeybinds { .key .into_iter() .map(|k| (k, actions.clone())) - .collect::>>(), + .collect::>>(), ) } } diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs index b7eff7bda2..3f0b392957 100644 --- a/zellij-utils/src/input/layout.rs +++ b/zellij-utils/src/input/layout.rs @@ -16,7 +16,6 @@ use crate::{ pane_size::{Dimension, PaneGeom}, setup, }; -use crate::{serde, serde_yaml}; use super::{ config::ConfigFromYaml, @@ -35,7 +34,6 @@ use std::{fs::File, io::prelude::*}; use url::Url; #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Copy)] -#[serde(crate = "self::serde")] pub enum Direction { #[serde(alias = "horizontal")] Horizontal, @@ -54,17 +52,15 @@ impl Not for Direction { } } -#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)] -#[serde(crate = "self::serde")] +#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] pub enum SplitSize { #[serde(alias = "percent")] - Percent(f64), // 1 to 100 + Percent(u64), // 1 to 100 #[serde(alias = "fixed")] Fixed(usize), // An absolute number of columns or rows } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(crate = "self::serde")] pub enum Run { #[serde(rename = "plugin")] Plugin(RunPlugin), @@ -73,7 +69,6 @@ pub enum Run { } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(crate = "self::serde")] pub enum RunFromYaml { #[serde(rename = "plugin")] Plugin(RunPluginFromYaml), @@ -82,7 +77,6 @@ pub enum RunFromYaml { } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(crate = "self::serde")] pub struct RunPluginFromYaml { #[serde(default)] pub _allow_exec_host_cmd: bool, @@ -90,7 +84,6 @@ pub struct RunPluginFromYaml { } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(crate = "self::serde")] pub struct RunPlugin { #[serde(default)] pub _allow_exec_host_cmd: bool, @@ -98,7 +91,6 @@ pub struct RunPlugin { } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(crate = "self::serde")] pub enum RunPluginLocation { File(PathBuf), Zellij(PluginTag), @@ -133,7 +125,6 @@ impl fmt::Display for RunPluginLocation { // The layout struct ultimately used to build the layouts. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(crate = "self::serde")] pub struct Layout { pub direction: Direction, #[serde(default)] @@ -152,7 +143,6 @@ pub struct Layout { // https://github.com/bincode-org/bincode/issues/245 // flattened fields don't retain size information. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(crate = "self::serde")] #[serde(default)] pub struct LayoutFromYamlIntermediate { #[serde(default)] @@ -170,7 +160,6 @@ pub struct LayoutFromYamlIntermediate { // The struct that is used to deserialize the layout from // a yaml configuration file #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(crate = "self::serde")] #[serde(default)] pub struct LayoutFromYaml { #[serde(default)] @@ -422,7 +411,6 @@ impl LayoutFromYaml { // The struct that is used to deserialize the session from // a yaml configuration file #[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] -#[serde(crate = "self::serde")] pub struct SessionFromYaml { pub name: Option, #[serde(default = "default_as_some_true")] @@ -436,7 +424,6 @@ fn default_as_some_true() -> Option { // The struct that carries the information template that is used to // construct the layout #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(crate = "self::serde")] pub struct LayoutTemplate { pub direction: Direction, #[serde(default)] @@ -481,8 +468,7 @@ impl LayoutTemplate { } // The tab-layout struct used to specify each individual tab. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(crate = "self::serde")] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct TabLayout { #[serde(default)] pub direction: Direction, @@ -606,7 +592,7 @@ fn split_space(space_to_split: &PaneGeom, layout: &Layout) -> Vec<(Layout, PaneG for (&size, part) in sizes.iter().zip(&layout.parts) { let split_dimension = match size { - Some(SplitSize::Percent(percent)) => Dimension::percent(percent), + Some(SplitSize::Percent(percent)) => Dimension::percent(percent as f64), Some(SplitSize::Fixed(size)) => Dimension::fixed(size), None => { let free_percent = if let Some(p) = split_dimension_space.as_percent() { @@ -614,7 +600,7 @@ fn split_space(space_to_split: &PaneGeom, layout: &Layout) -> Vec<(Layout, PaneG .iter() .map(|&s| { if let Some(SplitSize::Percent(ip)) = s { - ip + ip as f64 } else { 0.0 } diff --git a/zellij-utils/src/input/mod.rs b/zellij-utils/src/input/mod.rs index 268a4e2c87..29a7816cd6 100644 --- a/zellij-utils/src/input/mod.rs +++ b/zellij-utils/src/input/mod.rs @@ -1,176 +1,122 @@ //! The way terminal input is handled. - pub mod actions; pub mod command; pub mod config; pub mod keybinds; pub mod layout; -pub mod mouse; pub mod options; pub mod plugins; pub mod theme; -use super::{ - data::{CharOrArrow, Direction, Style}, - data::{InputMode, Key, ModeInfo, PluginCapabilities}, -}; -use crate::envs; -use termwiz::input::{InputEvent, InputParser, KeyCode, KeyEvent, Modifiers}; +// Can't use this in wasm due to dependency on the `termwiz` crate. +#[cfg(not(target_family = "wasm"))] +pub mod mouse; -/// Creates a [`ModeInfo`] struct indicating the current [`InputMode`] and its keybinds -/// (as pairs of [`String`]s). -pub fn get_mode_info(mode: InputMode, style: Style, capabilities: PluginCapabilities) -> ModeInfo { - let keybinds = match mode { - InputMode::Normal | InputMode::Locked | InputMode::Prompt => Vec::new(), - InputMode::Resize => vec![ - ("←↓↑→".to_string(), "Resize".to_string()), - ("+-".to_string(), "Increase/Decrease size".to_string()), - ], - InputMode::Move => vec![ - ("←↓↑→".to_string(), "Move".to_string()), - ("n/Tab".to_string(), "Next Pane".to_string()), - ], - InputMode::Pane => vec![ - ("←↓↑→".to_string(), "Move focus".to_string()), - ("n".to_string(), "New".to_string()), - ("d".to_string(), "Down split".to_string()), - ("r".to_string(), "Right split".to_string()), - ("x".to_string(), "Close".to_string()), - ("f".to_string(), "Fullscreen".to_string()), - ("z".to_string(), "Frames".to_string()), - ("c".to_string(), "Rename".to_string()), - ("w".to_string(), "Floating Toggle".to_string()), - ("e".to_string(), "Embed Pane".to_string()), - ("p".to_string(), "Next".to_string()), - ], - InputMode::Tab => vec![ - ("←↓↑→".to_string(), "Move focus".to_string()), - ("n".to_string(), "New".to_string()), - ("x".to_string(), "Close".to_string()), - ("r".to_string(), "Rename".to_string()), - ("s".to_string(), "Sync".to_string()), - ("Tab".to_string(), "Toggle".to_string()), - ], - InputMode::Scroll => vec![ - ("↓↑".to_string(), "Scroll".to_string()), - ("PgDn/PgUp".to_string(), "Scroll Page".to_string()), - ("d/u".to_string(), "Scroll Half Page".to_string()), - ( - "e".to_string(), - "Edit Scrollback in Default Editor".to_string(), - ), - ("s".to_string(), "Enter search term".to_string()), - ], - InputMode::EnterSearch => vec![("Enter".to_string(), "when done".to_string())], - InputMode::Search => vec![ - ("↓↑".to_string(), "Scroll".to_string()), - ("PgUp/PgDn".to_string(), "Scroll Page".to_string()), - ("u/d".to_string(), "Scroll Half Page".to_string()), - ("n".to_string(), "Search down".to_string()), - ("p".to_string(), "Search up".to_string()), - ("c".to_string(), "Case sensitivity".to_string()), - ("w".to_string(), "Wrap".to_string()), - ("o".to_string(), "Whole words".to_string()), - ], - InputMode::RenameTab => vec![("Enter".to_string(), "when done".to_string())], - InputMode::RenamePane => vec![("Enter".to_string(), "when done".to_string())], - InputMode::Session => vec![("d".to_string(), "Detach".to_string())], - InputMode::Tmux => vec![ - ("←↓↑→".to_string(), "Move focus".to_string()), - ("\"".to_string(), "Split Down".to_string()), - ("%".to_string(), "Split Right".to_string()), - ("z".to_string(), "Fullscreen".to_string()), - ("c".to_string(), "New Tab".to_string()), - (",".to_string(), "Rename Tab".to_string()), - ("p".to_string(), "Previous Tab".to_string()), - ("n".to_string(), "Next Tab".to_string()), - ], +#[cfg(not(target_family = "wasm"))] +pub use not_wasm::*; + +#[cfg(not(target_family = "wasm"))] +mod not_wasm { + use crate::{ + data::{CharOrArrow, Direction, InputMode, Key, ModeInfo, PluginCapabilities}, + envs, + ipc::ClientAttributes, }; + use termwiz::input::{InputEvent, InputParser, KeyCode, KeyEvent, Modifiers}; - let session_name = envs::get_session_name().ok(); + /// Creates a [`ModeInfo`] struct indicating the current [`InputMode`] and its keybinds + /// (as pairs of [`String`]s). + pub fn get_mode_info( + mode: InputMode, + attributes: &ClientAttributes, + capabilities: PluginCapabilities, + ) -> ModeInfo { + let keybinds = attributes.keybinds.to_keybinds_vec(); + let session_name = envs::get_session_name().ok(); - ModeInfo { - mode, - keybinds, - style, - capabilities, - session_name, + ModeInfo { + mode, + keybinds, + style: attributes.style, + capabilities, + session_name, + } } -} -pub fn parse_keys(input_bytes: &[u8]) -> Vec { - let mut ret = vec![]; - let mut input_parser = InputParser::new(); // this is the termwiz InputParser - let maybe_more = false; - let parse_input_event = |input_event: InputEvent| { - if let InputEvent::Key(key_event) = input_event { - ret.push(cast_termwiz_key(key_event, input_bytes)); - } - }; - input_parser.parse(input_bytes, parse_input_event, maybe_more); - ret -} + pub fn parse_keys(input_bytes: &[u8]) -> Vec { + let mut ret = vec![]; + let mut input_parser = InputParser::new(); // this is the termwiz InputParser + let maybe_more = false; + let parse_input_event = |input_event: InputEvent| { + if let InputEvent::Key(key_event) = input_event { + ret.push(cast_termwiz_key(key_event, input_bytes)); + } + }; + input_parser.parse(input_bytes, parse_input_event, maybe_more); + ret + } -// FIXME: This is an absolutely cursed function that should be destroyed as soon -// as an alternative that doesn't touch zellij-tile can be developed... -pub fn cast_termwiz_key(event: KeyEvent, raw_bytes: &[u8]) -> Key { - let modifiers = event.modifiers; + // FIXME: This is an absolutely cursed function that should be destroyed as soon + // as an alternative that doesn't touch zellij-tile can be developed... + pub fn cast_termwiz_key(event: KeyEvent, raw_bytes: &[u8]) -> Key { + let modifiers = event.modifiers; - // *** THIS IS WHERE WE SHOULD WORK AROUND ISSUES WITH TERMWIZ *** - if raw_bytes == [8] { - return Key::Ctrl('h'); - }; + // *** THIS IS WHERE WE SHOULD WORK AROUND ISSUES WITH TERMWIZ *** + if raw_bytes == [8] { + return Key::Ctrl('h'); + }; - match event.key { - KeyCode::Char(c) => { - if modifiers.contains(Modifiers::CTRL) { - Key::Ctrl(c.to_lowercase().next().unwrap_or_default()) - } else if modifiers.contains(Modifiers::ALT) { - Key::Alt(CharOrArrow::Char(c)) - } else { - Key::Char(c) - } - }, - KeyCode::Backspace => Key::Backspace, - KeyCode::LeftArrow | KeyCode::ApplicationLeftArrow => { - if modifiers.contains(Modifiers::ALT) { - Key::Alt(CharOrArrow::Direction(Direction::Left)) - } else { - Key::Left - } - }, - KeyCode::RightArrow | KeyCode::ApplicationRightArrow => { - if modifiers.contains(Modifiers::ALT) { - Key::Alt(CharOrArrow::Direction(Direction::Right)) - } else { - Key::Right - } - }, - KeyCode::UpArrow | KeyCode::ApplicationUpArrow => { - if modifiers.contains(Modifiers::ALT) { - //Key::AltPlusUpArrow - Key::Alt(CharOrArrow::Direction(Direction::Up)) - } else { - Key::Up - } - }, - KeyCode::DownArrow | KeyCode::ApplicationDownArrow => { - if modifiers.contains(Modifiers::ALT) { - Key::Alt(CharOrArrow::Direction(Direction::Down)) - } else { - Key::Down - } - }, - KeyCode::Home => Key::Home, - KeyCode::End => Key::End, - KeyCode::PageUp => Key::PageUp, - KeyCode::PageDown => Key::PageDown, - KeyCode::Tab => Key::BackTab, // TODO: ??? - KeyCode::Delete => Key::Delete, - KeyCode::Insert => Key::Insert, - KeyCode::Function(n) => Key::F(n), - KeyCode::Escape => Key::Esc, - KeyCode::Enter => Key::Char('\n'), - _ => Key::Esc, // there are other keys we can implement here, but we might need additional terminal support to implement them, not just exhausting this enum + match event.key { + KeyCode::Char(c) => { + if modifiers.contains(Modifiers::CTRL) { + Key::Ctrl(c.to_lowercase().next().unwrap_or_default()) + } else if modifiers.contains(Modifiers::ALT) { + Key::Alt(CharOrArrow::Char(c)) + } else { + Key::Char(c) + } + }, + KeyCode::Backspace => Key::Backspace, + KeyCode::LeftArrow | KeyCode::ApplicationLeftArrow => { + if modifiers.contains(Modifiers::ALT) { + Key::Alt(CharOrArrow::Direction(Direction::Left)) + } else { + Key::Left + } + }, + KeyCode::RightArrow | KeyCode::ApplicationRightArrow => { + if modifiers.contains(Modifiers::ALT) { + Key::Alt(CharOrArrow::Direction(Direction::Right)) + } else { + Key::Right + } + }, + KeyCode::UpArrow | KeyCode::ApplicationUpArrow => { + if modifiers.contains(Modifiers::ALT) { + //Key::AltPlusUpArrow + Key::Alt(CharOrArrow::Direction(Direction::Up)) + } else { + Key::Up + } + }, + KeyCode::DownArrow | KeyCode::ApplicationDownArrow => { + if modifiers.contains(Modifiers::ALT) { + Key::Alt(CharOrArrow::Direction(Direction::Down)) + } else { + Key::Down + } + }, + KeyCode::Home => Key::Home, + KeyCode::End => Key::End, + KeyCode::PageUp => Key::PageUp, + KeyCode::PageDown => Key::PageDown, + KeyCode::Tab => Key::BackTab, // TODO: ??? + KeyCode::Delete => Key::Delete, + KeyCode::Insert => Key::Insert, + KeyCode::Function(n) => Key::F(n), + KeyCode::Escape => Key::Esc, + KeyCode::Enter => Key::Char('\n'), + _ => Key::Esc, // there are other keys we can implement here, but we might need additional terminal support to implement them, not just exhausting this enum + } } } diff --git a/zellij-utils/src/input/unit/keybinds_test.rs b/zellij-utils/src/input/unit/keybinds_test.rs index ed13289819..1579f0139e 100644 --- a/zellij-utils/src/input/unit/keybinds_test.rs +++ b/zellij-utils/src/input/unit/keybinds_test.rs @@ -1,7 +1,6 @@ use super::super::actions::*; use super::super::keybinds::*; -use crate::data::Key; -use crate::input::CharOrArrow; +use crate::data::{CharOrArrow, Key}; #[test] fn merge_keybinds_merges_different_keys() { diff --git a/zellij-utils/src/input/unit/layout_test.rs b/zellij-utils/src/input/unit/layout_test.rs index 292b6b01dc..22a10c6fb1 100644 --- a/zellij-utils/src/input/unit/layout_test.rs +++ b/zellij-utils/src/input/unit/layout_test.rs @@ -198,7 +198,7 @@ fn three_panes_with_tab_merged_correctly() { pane_name: None, focus: None, parts: vec![], - split_size: Some(SplitSize::Percent(50.0)), + split_size: Some(SplitSize::Percent(50)), run: None, }, Layout { @@ -213,7 +213,7 @@ fn three_panes_with_tab_merged_correctly() { pane_name: None, focus: None, parts: vec![], - split_size: Some(SplitSize::Percent(50.0)), + split_size: Some(SplitSize::Percent(50)), run: None, }, Layout { @@ -222,7 +222,7 @@ fn three_panes_with_tab_merged_correctly() { pane_name: None, focus: None, parts: vec![], - split_size: Some(SplitSize::Percent(50.0)), + split_size: Some(SplitSize::Percent(50)), run: None, }, ], @@ -319,7 +319,7 @@ fn three_panes_with_tab_and_default_plugins_merged_correctly() { pane_name: None, focus: None, parts: vec![], - split_size: Some(SplitSize::Percent(50.0)), + split_size: Some(SplitSize::Percent(50)), run: None, }, Layout { @@ -334,7 +334,7 @@ fn three_panes_with_tab_and_default_plugins_merged_correctly() { pane_name: None, focus: None, parts: vec![], - split_size: Some(SplitSize::Percent(50.0)), + split_size: Some(SplitSize::Percent(50)), run: None, }, Layout { @@ -343,7 +343,7 @@ fn three_panes_with_tab_and_default_plugins_merged_correctly() { pane_name: None, focus: None, parts: vec![], - split_size: Some(SplitSize::Percent(50.0)), + split_size: Some(SplitSize::Percent(50)), run: None, }, ], @@ -467,7 +467,7 @@ fn deeply_nested_tab_merged_correctly() { pane_name: None, focus: None, parts: vec![], - split_size: Some(SplitSize::Percent(21.0)), + split_size: Some(SplitSize::Percent(21)), run: None, }, Layout { @@ -482,7 +482,7 @@ fn deeply_nested_tab_merged_correctly() { pane_name: None, focus: None, parts: vec![], - split_size: Some(SplitSize::Percent(22.0)), + split_size: Some(SplitSize::Percent(22)), run: None, }, Layout { @@ -497,7 +497,7 @@ fn deeply_nested_tab_merged_correctly() { pane_name: None, focus: None, parts: vec![], - split_size: Some(SplitSize::Percent(23.0)), + split_size: Some(SplitSize::Percent(23)), run: None, }, Layout { @@ -506,19 +506,19 @@ fn deeply_nested_tab_merged_correctly() { pane_name: None, focus: None, parts: vec![], - split_size: Some(SplitSize::Percent(24.0)), + split_size: Some(SplitSize::Percent(24)), run: None, }, ], - split_size: Some(SplitSize::Percent(78.0)), + split_size: Some(SplitSize::Percent(78)), run: None, }, ], - split_size: Some(SplitSize::Percent(79.0)), + split_size: Some(SplitSize::Percent(79)), run: None, }, ], - split_size: Some(SplitSize::Percent(90.0)), + split_size: Some(SplitSize::Percent(90)), run: None, }, Layout { @@ -527,7 +527,7 @@ fn deeply_nested_tab_merged_correctly() { pane_name: None, focus: None, parts: vec![], - split_size: Some(SplitSize::Percent(15.0)), + split_size: Some(SplitSize::Percent(15)), run: None, }, Layout { @@ -536,7 +536,7 @@ fn deeply_nested_tab_merged_correctly() { pane_name: None, focus: None, parts: vec![], - split_size: Some(SplitSize::Percent(15.0)), + split_size: Some(SplitSize::Percent(15)), run: None, }, Layout { @@ -545,7 +545,7 @@ fn deeply_nested_tab_merged_correctly() { pane_name: None, focus: None, parts: vec![], - split_size: Some(SplitSize::Percent(15.0)), + split_size: Some(SplitSize::Percent(15)), run: None, }, ], @@ -591,7 +591,7 @@ fn three_tabs_tab_one_merged_correctly() { pane_name: None, focus: None, parts: vec![], - split_size: Some(SplitSize::Percent(50.0)), + split_size: Some(SplitSize::Percent(50)), run: None, }, Layout { @@ -638,7 +638,7 @@ fn three_tabs_tab_two_merged_correctly() { pane_name: None, focus: None, parts: vec![], - split_size: Some(SplitSize::Percent(50.0)), + split_size: Some(SplitSize::Percent(50)), run: None, }, Layout { @@ -651,7 +651,7 @@ fn three_tabs_tab_two_merged_correctly() { run: None, }, ], - split_size: Some(SplitSize::Percent(50.0)), + split_size: Some(SplitSize::Percent(50)), run: None, }, Layout { @@ -698,7 +698,7 @@ fn three_tabs_tab_three_merged_correctly() { pane_name: None, focus: None, parts: vec![], - split_size: Some(SplitSize::Percent(50.0)), + split_size: Some(SplitSize::Percent(50)), run: None, }, Layout { @@ -711,7 +711,7 @@ fn three_tabs_tab_three_merged_correctly() { run: None, }, ], - split_size: Some(SplitSize::Percent(50.0)), + split_size: Some(SplitSize::Percent(50)), run: None, }, Layout { diff --git a/zellij-utils/src/ipc.rs b/zellij-utils/src/ipc.rs index a526897987..c0a7e26999 100644 --- a/zellij-utils/src/ipc.rs +++ b/zellij-utils/src/ipc.rs @@ -1,10 +1,12 @@ //! IPC stuff for starting to split things into a client and server model. - use crate::{ cli::CliArgs, data::{ClientId, InputMode, Style}, errors::{get_current_ctx, ErrorContext}, - input::{actions::Action, layout::LayoutFromYaml, options::Options, plugins::PluginsConfig}, + input::{ + actions::Action, keybinds::Keybinds, layout::LayoutFromYaml, options::Options, + plugins::PluginsConfig, + }, pane_size::{Size, SizeInPixels}, }; use interprocess::local_socket::LocalSocketStream; @@ -37,10 +39,11 @@ pub enum ClientType { Writer, } -#[derive(Default, Serialize, Deserialize, Debug, Clone, Copy)] +#[derive(Default, Serialize, Deserialize, Debug, Clone)] pub struct ClientAttributes { pub size: Size, pub style: Style, + pub keybinds: Keybinds, } #[derive(Default, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] @@ -155,7 +158,7 @@ impl IpcSenderWithContext { /// Sends an event, along with the current [`ErrorContext`], on this [`IpcSenderWithContext`]'s socket. pub fn send(&mut self, msg: T) { let err_ctx = get_current_ctx(); - bincode::serialize_into(&mut self.sender, &(msg, err_ctx)).unwrap(); + rmp_serde::encode::write(&mut self.sender, &(msg, err_ctx)).unwrap(); // TODO: unwrapping here can cause issues when the server disconnects which we don't mind // do we need to handle errors here in other cases? let _ = self.sender.flush(); @@ -193,7 +196,7 @@ where /// Receives an event, along with the current [`ErrorContext`], on this [`IpcReceiverWithContext`]'s socket. pub fn recv(&mut self) -> Option<(T, ErrorContext)> { - match bincode::deserialize_from(&mut self.receiver) { + match rmp_serde::decode::from_read(&mut self.receiver) { Ok(msg) => Some(msg), Err(e) => { warn!("Error in IpcReceiver.recv(): {:?}", e); diff --git a/zellij-utils/src/lib.rs b/zellij-utils/src/lib.rs index 94c4f18dbf..3cff1f046a 100644 --- a/zellij-utils/src/lib.rs +++ b/zellij-utils/src/lib.rs @@ -1,30 +1,23 @@ -pub mod data; - -#[cfg(not(target_family = "wasm"))] -pub mod channels; -#[cfg(not(target_family = "wasm"))] pub mod cli; -#[cfg(not(target_family = "wasm"))] pub mod consts; -#[cfg(not(target_family = "wasm"))] +pub mod data; pub mod envs; -#[cfg(not(target_family = "wasm"))] -pub mod errors; -#[cfg(not(target_family = "wasm"))] pub mod input; -#[cfg(not(target_family = "wasm"))] -pub mod ipc; -#[cfg(not(target_family = "wasm"))] -pub mod logging; -#[cfg(not(target_family = "wasm"))] pub mod pane_size; -#[cfg(not(target_family = "wasm"))] pub mod position; -#[cfg(not(target_family = "wasm"))] pub mod setup; -#[cfg(not(target_family = "wasm"))] pub mod shared; +// The following modules can't be used when targeting wasm +#[cfg(not(target_family = "wasm"))] +pub mod channels; // Requires async_std +#[cfg(not(target_family = "wasm"))] +pub mod errors; // Requires async_std (via channels) +#[cfg(not(target_family = "wasm"))] +pub mod ipc; // Requires interprocess +#[cfg(not(target_family = "wasm"))] +pub mod logging; // Requires log4rs + #[cfg(not(target_family = "wasm"))] pub use ::{ anyhow, async_std, clap, interprocess, lazy_static, libc, nix, regex, serde, serde_yaml, diff --git a/zellij-utils/src/position.rs b/zellij-utils/src/position.rs index 89b91f8225..783421c574 100644 --- a/zellij-utils/src/position.rs +++ b/zellij-utils/src/position.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Deserialize, Serialize)] +#[derive(Debug, Hash, Copy, Clone, PartialEq, Eq, PartialOrd, Deserialize, Serialize)] pub struct Position { pub line: Line, pub column: Column, @@ -30,7 +30,7 @@ impl Position { } } -#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, PartialOrd)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, PartialOrd)] pub struct Line(pub isize); -#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, PartialOrd)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, PartialOrd)] pub struct Column(pub usize); diff --git a/zellij-utils/src/setup.rs b/zellij-utils/src/setup.rs index 209dc18975..17e2f3145f 100644 --- a/zellij-utils/src/setup.rs +++ b/zellij-utils/src/setup.rs @@ -1,94 +1,13 @@ -use crate::{ - cli::{CliArgs, Command}, - consts::{ - FEATURES, SYSTEM_DEFAULT_CONFIG_DIR, SYSTEM_DEFAULT_DATA_DIR_PREFIX, VERSION, - ZELLIJ_PROJ_DIR, - }, - input::{ - config::{Config, ConfigError}, - layout::{LayoutFromYaml, LayoutFromYamlIntermediate}, - options::Options, - theme::ThemesFromYaml, - }, -}; -use clap::{Args, IntoApp}; -use clap_complete::Shell; +use crate::consts::{SYSTEM_DEFAULT_CONFIG_DIR, SYSTEM_DEFAULT_DATA_DIR_PREFIX, ZELLIJ_PROJ_DIR}; +use clap::Args; use directories_next::BaseDirs; use serde::{Deserialize, Serialize}; use std::{ - convert::TryFrom, fmt::Write as FmtWrite, io::Write, path::Path, path::PathBuf, process, + io::Write, + path::{Path, PathBuf}, }; const CONFIG_LOCATION: &str = ".config/zellij"; -const CONFIG_NAME: &str = "config.yaml"; -static ARROW_SEPARATOR: &str = ""; - -#[cfg(not(test))] -/// Goes through a predefined list and checks for an already -/// existing config directory, returns the first match -pub fn find_default_config_dir() -> Option { - default_config_dirs() - .into_iter() - .filter(|p| p.is_some()) - .find(|p| p.clone().unwrap().exists()) - .flatten() -} - -#[cfg(test)] -pub fn find_default_config_dir() -> Option { - None -} - -/// Order in which config directories are checked -fn default_config_dirs() -> Vec> { - vec![ - home_config_dir(), - Some(xdg_config_dir()), - Some(Path::new(SYSTEM_DEFAULT_CONFIG_DIR).to_path_buf()), - ] -} - -/// Looks for an existing dir, uses that, else returns a -/// dir matching the config spec. -pub fn get_default_data_dir() -> PathBuf { - [ - xdg_data_dir(), - Path::new(SYSTEM_DEFAULT_DATA_DIR_PREFIX).join("share/zellij"), - ] - .into_iter() - .find(|p| p.exists()) - .unwrap_or_else(xdg_data_dir) -} - -pub fn xdg_config_dir() -> PathBuf { - ZELLIJ_PROJ_DIR.config_dir().to_owned() -} - -pub fn xdg_data_dir() -> PathBuf { - ZELLIJ_PROJ_DIR.data_dir().to_owned() -} - -pub fn home_config_dir() -> Option { - if let Some(user_dirs) = BaseDirs::new() { - let config_dir = user_dirs.home_dir().join(CONFIG_LOCATION); - Some(config_dir) - } else { - None - } -} - -pub fn get_layout_dir(config_dir: Option) -> Option { - config_dir.map(|dir| dir.join("layouts")) -} - -pub fn get_theme_dir(config_dir: Option) -> Option { - config_dir.map(|dir| dir.join("themes")) -} - -pub fn dump_asset(asset: &[u8]) -> std::io::Result<()> { - std::io::stdout().write_all(asset)?; - Ok(()) -} pub const DEFAULT_CONFIG: &[u8] = include_bytes!(concat!( env!("CARGO_MANIFEST_DIR"), @@ -144,6 +63,15 @@ pub const ZSH_AUTO_START_SCRIPT: &[u8] = include_bytes!(concat!( "assets/shell/auto-start.zsh" )); +pub fn get_theme_dir(config_dir: Option) -> Option { + config_dir.map(|dir| dir.join("themes")) +} + +pub fn dump_asset(asset: &[u8]) -> std::io::Result<()> { + std::io::stdout().write_all(asset)?; + Ok(()) +} + pub fn dump_default_config() -> std::io::Result<()> { dump_asset(DEFAULT_CONFIG) } @@ -190,413 +118,501 @@ pub struct Setup { pub generate_auto_start: Option, } -impl Setup { - /// Entrypoint from main - /// Merges options from the config file and the command line options - /// into `[Options]`, the command line options superceeding the layout - /// file options, superceeding the config file options: - /// 1. command line options (`zellij options`) - /// 2. layout options - /// (`layout.yaml` / `zellij --layout`) - /// 3. config options (`config.yaml`) - pub fn from_options( - opts: &CliArgs, - ) -> Result<(Config, Option, Options), ConfigError> { - let clean = match &opts.command { - Some(Command::Setup(ref setup)) => setup.clean, - _ => false, - }; +#[cfg(test)] +pub fn find_default_config_dir() -> Option { + None +} - // setup functions that don't require deserialisation of the config - if let Some(Command::Setup(ref setup)) = &opts.command { - setup.from_cli().map_or_else( - |e| { - eprintln!("{:?}", e); - process::exit(1); - }, - |_| {}, - ); - }; +#[cfg(not(test))] +/// Goes through a predefined list and checks for an already +/// existing config directory, returns the first match +pub fn find_default_config_dir() -> Option { + default_config_dirs() + .into_iter() + .filter(|p| p.is_some()) + .find(|p| p.clone().unwrap().exists()) + .flatten() +} - let mut config = if !clean { - match Config::try_from(opts) { - Ok(config) => config, - Err(e) => { - return Err(e); - }, - } - } else { - Config::default() - }; +/// Order in which config directories are checked +#[allow(dead_code)] +fn default_config_dirs() -> Vec> { + vec![ + home_config_dir(), + Some(xdg_config_dir()), + Some(Path::new(SYSTEM_DEFAULT_CONFIG_DIR).to_path_buf()), + ] +} - let config_options = Options::from_cli(&config.options, opts.command.clone()); - - let layout_dir = config_options - .layout_dir - .clone() - .or_else(|| get_layout_dir(opts.config_dir.clone().or_else(find_default_config_dir))); - let chosen_layout = opts - .layout - .clone() - .or_else(|| config_options.default_layout.clone()); - let layout_result = - LayoutFromYamlIntermediate::from_path_or_default(chosen_layout.as_ref(), layout_dir); - let layout = match layout_result { - None => None, - Some(Ok(layout)) => Some(layout), - Some(Err(e)) => { - return Err(e); - }, - }; +/// Looks for an existing dir, uses that, else returns a +/// dir matching the config spec. +pub fn get_default_data_dir() -> PathBuf { + [ + xdg_data_dir(), + Path::new(SYSTEM_DEFAULT_DATA_DIR_PREFIX).join("share/zellij"), + ] + .into_iter() + .find(|p| p.exists()) + .unwrap_or_else(xdg_data_dir) +} - if let Some(theme_dir) = config_options - .theme_dir - .clone() - .or_else(|| get_theme_dir(opts.config_dir.clone().or_else(find_default_config_dir))) - { - if theme_dir.is_dir() { - for entry in (theme_dir.read_dir()?).flatten() { - if let Some(extension) = entry.path().extension() { - if extension == "yaml" || extension == "yml" { - if let Ok(themes) = ThemesFromYaml::from_path(&entry.path()) { - config.themes = config.themes.map(|t| t.merge(themes.into())); - } - } - } - } - } - } +pub fn xdg_config_dir() -> PathBuf { + ZELLIJ_PROJ_DIR.config_dir().to_owned() +} - if let Some(Command::Setup(ref setup)) = &opts.command { - setup - .from_cli_with_options(opts, &config_options) - .map_or_else( +pub fn xdg_data_dir() -> PathBuf { + ZELLIJ_PROJ_DIR.data_dir().to_owned() +} + +pub fn home_config_dir() -> Option { + if let Some(user_dirs) = BaseDirs::new() { + let config_dir = user_dirs.home_dir().join(CONFIG_LOCATION); + Some(config_dir) + } else { + None + } +} + +pub fn get_layout_dir(config_dir: Option) -> Option { + config_dir.map(|dir| dir.join("layouts")) +} + +#[cfg(not(target_family = "wasm"))] +pub use not_wasm::*; + +#[cfg(not(target_family = "wasm"))] +mod not_wasm { + use super::*; + use crate::{ + cli::{CliArgs, Command}, + consts::{FEATURES, SYSTEM_DEFAULT_DATA_DIR_PREFIX, VERSION}, + input::{ + config::{Config, ConfigError}, + layout::{LayoutFromYaml, LayoutFromYamlIntermediate}, + options::Options, + theme::ThemesFromYaml, + }, + }; + use clap::IntoApp; + use clap_complete::Shell; + use std::{convert::TryFrom, fmt::Write as FmtWrite, io::Write, path::PathBuf, process}; + + const CONFIG_NAME: &str = "config.yaml"; + static ARROW_SEPARATOR: &str = ""; + + impl Setup { + /// Entrypoint from main + /// Merges options from the config file and the command line options + /// into `[Options]`, the command line options superceeding the layout + /// file options, superceeding the config file options: + /// 1. command line options (`zellij options`) + /// 2. layout options + /// (`layout.yaml` / `zellij --layout`) + /// 3. config options (`config.yaml`) + pub fn from_options( + opts: &CliArgs, + ) -> Result<(Config, Option, Options), ConfigError> { + let clean = match &opts.command { + Some(Command::Setup(ref setup)) => setup.clean, + _ => false, + }; + + // setup functions that don't require deserialisation of the config + if let Some(Command::Setup(ref setup)) = &opts.command { + setup.from_cli().map_or_else( |e| { eprintln!("{:?}", e); process::exit(1); }, |_| {}, ); - }; + }; - Setup::merge_config_with_layout(config, layout, config_options) - } + let mut config = if !clean { + match Config::try_from(opts) { + Ok(config) => config, + Err(e) => { + return Err(e); + }, + } + } else { + Config::default() + }; - /// General setup helpers - pub fn from_cli(&self) -> std::io::Result<()> { - if self.clean { - return Ok(()); - } + let config_options = Options::from_cli(&config.options, opts.command.clone()); + + let layout_dir = config_options.layout_dir.clone().or_else(|| { + get_layout_dir(opts.config_dir.clone().or_else(find_default_config_dir)) + }); + let chosen_layout = opts + .layout + .clone() + .or_else(|| config_options.default_layout.clone()); + let layout_result = LayoutFromYamlIntermediate::from_path_or_default( + chosen_layout.as_ref(), + layout_dir, + ); + let layout = match layout_result { + None => None, + Some(Ok(layout)) => Some(layout), + Some(Err(e)) => { + return Err(e); + }, + }; - if self.dump_config { - dump_default_config()?; - std::process::exit(0); - } + if let Some(theme_dir) = config_options + .theme_dir + .clone() + .or_else(|| get_theme_dir(opts.config_dir.clone().or_else(find_default_config_dir))) + { + if theme_dir.is_dir() { + for entry in (theme_dir.read_dir()?).flatten() { + if let Some(extension) = entry.path().extension() { + if extension == "yaml" || extension == "yml" { + if let Ok(themes) = ThemesFromYaml::from_path(&entry.path()) { + config.themes = config.themes.map(|t| t.merge(themes.into())); + } + } + } + } + } + } - if let Some(shell) = &self.generate_completion { - Self::generate_completion(shell); - std::process::exit(0); - } + if let Some(Command::Setup(ref setup)) = &opts.command { + setup + .from_cli_with_options(opts, &config_options) + .map_or_else( + |e| { + eprintln!("{:?}", e); + process::exit(1); + }, + |_| {}, + ); + }; - if let Some(shell) = &self.generate_auto_start { - Self::generate_auto_start(shell); - std::process::exit(0); + Setup::merge_config_with_layout(config, layout, config_options) } - if let Some(layout) = &self.dump_layout { - dump_specified_layout(layout)?; - std::process::exit(0); - } + /// General setup helpers + pub fn from_cli(&self) -> std::io::Result<()> { + if self.clean { + return Ok(()); + } - Ok(()) - } + if self.dump_config { + dump_default_config()?; + std::process::exit(0); + } - /// Checks the merged configuration - pub fn from_cli_with_options( - &self, - opts: &CliArgs, - config_options: &Options, - ) -> std::io::Result<()> { - if self.check { - Setup::check_defaults_config(opts, config_options)?; - std::process::exit(0); + if let Some(shell) = &self.generate_completion { + Self::generate_completion(shell); + std::process::exit(0); + } + + if let Some(shell) = &self.generate_auto_start { + Self::generate_auto_start(shell); + std::process::exit(0); + } + + if let Some(layout) = &self.dump_layout { + dump_specified_layout(layout)?; + std::process::exit(0); + } + + Ok(()) } - Ok(()) - } - fn merge_config_with_layout( - config: Config, - layout: Option, - config_options: Options, - ) -> Result<(Config, Option, Options), ConfigError> { - let (layout, layout_config) = match layout.map(|l| l.to_layout_and_config()) { - None => (None, None), - Some((layout, layout_config)) => (Some(layout), layout_config), - }; + /// Checks the merged configuration + pub fn from_cli_with_options( + &self, + opts: &CliArgs, + config_options: &Options, + ) -> std::io::Result<()> { + if self.check { + Setup::check_defaults_config(opts, config_options)?; + std::process::exit(0); + } + Ok(()) + } + + fn merge_config_with_layout( + config: Config, + layout: Option, + config_options: Options, + ) -> Result<(Config, Option, Options), ConfigError> { + let (layout, layout_config) = match layout.map(|l| l.to_layout_and_config()) { + None => (None, None), + Some((layout, layout_config)) => (Some(layout), layout_config), + }; - let (config, config_options) = if let Some(layout_config) = layout_config { - let config_options = if let Some(options) = layout_config.options.clone() { - config_options.merge(options) + let (config, config_options) = if let Some(layout_config) = layout_config { + let config_options = if let Some(options) = layout_config.options.clone() { + config_options.merge(options) + } else { + config_options + }; + let config = config.merge(layout_config.try_into()?); + (config, config_options) } else { - config_options + (config, config_options) }; - let config = config.merge(layout_config.try_into()?); - (config, config_options) - } else { - (config, config_options) - }; - Ok((config, layout, config_options)) - } + Ok((config, layout, config_options)) + } - pub fn check_defaults_config(opts: &CliArgs, config_options: &Options) -> std::io::Result<()> { - let data_dir = opts.data_dir.clone().unwrap_or_else(get_default_data_dir); - let config_dir = opts.config_dir.clone().or_else(find_default_config_dir); - let plugin_dir = data_dir.join("plugins"); - let layout_dir = config_options - .layout_dir - .clone() - .or_else(|| get_layout_dir(config_dir.clone())); - let theme_dir = config_options - .theme_dir - .clone() - .or_else(|| get_theme_dir(config_dir.clone())); - let system_data_dir = PathBuf::from(SYSTEM_DEFAULT_DATA_DIR_PREFIX).join("share/zellij"); - let config_file = opts - .config - .clone() - .or_else(|| config_dir.clone().map(|p| p.join(CONFIG_NAME))); - - // according to - // https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda - let hyperlink_start = "\u{1b}]8;;"; - let hyperlink_mid = "\u{1b}\\"; - let hyperlink_end = "\u{1b}]8;;\u{1b}\\"; - - let mut message = String::new(); - - writeln!(&mut message, "[Version]: {:?}", VERSION).unwrap(); - if let Some(config_dir) = config_dir { - writeln!(&mut message, "[CONFIG DIR]: {:?}", config_dir).unwrap(); - } else { - message.push_str("[CONFIG DIR]: Not Found\n"); - let mut default_config_dirs = default_config_dirs() - .iter() - .filter_map(|p| p.clone()) - .collect::>(); - default_config_dirs.dedup(); - message.push_str( - " On your system zellij looks in the following config directories by default:\n", - ); - for dir in default_config_dirs { - writeln!(&mut message, " {:?}", dir).unwrap(); + pub fn check_defaults_config( + opts: &CliArgs, + config_options: &Options, + ) -> std::io::Result<()> { + let data_dir = opts.data_dir.clone().unwrap_or_else(get_default_data_dir); + let config_dir = opts.config_dir.clone().or_else(find_default_config_dir); + let plugin_dir = data_dir.join("plugins"); + let layout_dir = config_options + .layout_dir + .clone() + .or_else(|| get_layout_dir(config_dir.clone())); + let theme_dir = config_options + .theme_dir + .clone() + .or_else(|| get_theme_dir(config_dir.clone())); + let system_data_dir = + PathBuf::from(SYSTEM_DEFAULT_DATA_DIR_PREFIX).join("share/zellij"); + let config_file = opts + .config + .clone() + .or_else(|| config_dir.clone().map(|p| p.join(CONFIG_NAME))); + + // according to + // https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda + let hyperlink_start = "\u{1b}]8;;"; + let hyperlink_mid = "\u{1b}\\"; + let hyperlink_end = "\u{1b}]8;;\u{1b}\\"; + + let mut message = String::new(); + + writeln!(&mut message, "[Version]: {:?}", VERSION).unwrap(); + if let Some(config_dir) = config_dir { + writeln!(&mut message, "[CONFIG DIR]: {:?}", config_dir).unwrap(); + } else { + message.push_str("[CONFIG DIR]: Not Found\n"); + let mut default_config_dirs = default_config_dirs() + .iter() + .filter_map(|p| p.clone()) + .collect::>(); + default_config_dirs.dedup(); + message.push_str( + " On your system zellij looks in the following config directories by default:\n", + ); + for dir in default_config_dirs { + writeln!(&mut message, " {:?}", dir).unwrap(); + } } - } - if let Some(config_file) = config_file { - writeln!(&mut message, "[CONFIG FILE]: {:?}", config_file).unwrap(); - match Config::new(&config_file) { - Ok(_) => message.push_str("[CONFIG FILE]: Well defined.\n"), - Err(e) => writeln!(&mut message, "[CONFIG ERROR]: {}", e).unwrap(), + if let Some(config_file) = config_file { + writeln!(&mut message, "[CONFIG FILE]: {:?}", config_file).unwrap(); + match Config::new(&config_file) { + Ok(_) => message.push_str("[CONFIG FILE]: Well defined.\n"), + Err(e) => writeln!(&mut message, "[CONFIG ERROR]: {}", e).unwrap(), + } + } else { + message.push_str("[CONFIG FILE]: Not Found\n"); + writeln!( + &mut message, + " By default zellij looks for a file called [{}] in the configuration directory", + CONFIG_NAME + ) + .unwrap(); } - } else { - message.push_str("[CONFIG FILE]: Not Found\n"); - writeln!( + writeln!(&mut message, "[DATA DIR]: {:?}", data_dir).unwrap(); + message.push_str(&format!("[PLUGIN DIR]: {:?}\n", plugin_dir)); + if let Some(layout_dir) = layout_dir { + writeln!(&mut message, "[LAYOUT DIR]: {:?}", layout_dir).unwrap(); + } else { + message.push_str("[LAYOUT DIR]: Not Found\n"); + } + if let Some(theme_dir) = theme_dir { + writeln!(&mut message, "[THEME DIR]: {:?}", theme_dir).unwrap(); + } else { + message.push_str("[THEME DIR]: Not Found\n"); + } + writeln!(&mut message, "[SYSTEM DATA DIR]: {:?}", system_data_dir).unwrap(); + + writeln!(&mut message, "[ARROW SEPARATOR]: {}", ARROW_SEPARATOR).unwrap(); + message.push_str(" Is the [ARROW_SEPARATOR] displayed correctly?\n"); + message.push_str(" If not you may want to either start zellij with a compatible mode: 'zellij options --simplified-ui true'\n"); + let mut hyperlink_compat = String::new(); + hyperlink_compat.push_str(hyperlink_start); + hyperlink_compat.push_str("https://zellij.dev/documentation/compatibility.html#the-status-bar-fonts-dont-render-correctly"); + hyperlink_compat.push_str(hyperlink_mid); + hyperlink_compat.push_str("https://zellij.dev/documentation/compatibility.html#the-status-bar-fonts-dont-render-correctly"); + hyperlink_compat.push_str(hyperlink_end); + write!( &mut message, - " By default zellij looks for a file called [{}] in the configuration directory", - CONFIG_NAME + " Or check the font that is in use:\n {}\n", + hyperlink_compat ) .unwrap(); + message.push_str("[MOUSE INTERACTION]: \n"); + message.push_str(" Can be temporarily disabled through pressing the [SHIFT] key.\n"); + message.push_str(" If that doesn't fix any issues consider to disable the mouse handling of zellij: 'zellij options --disable-mouse-mode'\n"); + + let default_editor = std::env::var("EDITOR") + .or_else(|_| std::env::var("VISUAL")) + .unwrap_or_else(|_| String::from("Not set, checked $EDITOR and $VISUAL")); + writeln!(&mut message, "[DEFAULT EDITOR]: {}", default_editor).unwrap(); + writeln!(&mut message, "[FEATURES]: {:?}", FEATURES).unwrap(); + let mut hyperlink = String::new(); + hyperlink.push_str(hyperlink_start); + hyperlink.push_str("https://www.zellij.dev/documentation/"); + hyperlink.push_str(hyperlink_mid); + hyperlink.push_str("zellij.dev/documentation"); + hyperlink.push_str(hyperlink_end); + writeln!(&mut message, "[DOCUMENTATION]: {}", hyperlink).unwrap(); + //printf '\e]8;;http://example.com\e\\This is a link\e]8;;\e\\\n' + + std::io::stdout().write_all(message.as_bytes())?; + + Ok(()) } - writeln!(&mut message, "[DATA DIR]: {:?}", data_dir).unwrap(); - message.push_str(&format!("[PLUGIN DIR]: {:?}\n", plugin_dir)); - if let Some(layout_dir) = layout_dir { - writeln!(&mut message, "[LAYOUT DIR]: {:?}", layout_dir).unwrap(); - } else { - message.push_str("[LAYOUT DIR]: Not Found\n"); - } - if let Some(theme_dir) = theme_dir { - writeln!(&mut message, "[THEME DIR]: {:?}", theme_dir).unwrap(); - } else { - message.push_str("[THEME DIR]: Not Found\n"); + fn generate_completion(shell: &str) { + let shell: Shell = match shell.to_lowercase().parse() { + Ok(shell) => shell, + _ => { + eprintln!("Unsupported shell: {}", shell); + std::process::exit(1); + }, + }; + let mut out = std::io::stdout(); + clap_complete::generate(shell, &mut CliArgs::command(), "zellij", &mut out); + // add shell dependent extra completion + match shell { + Shell::Bash => {}, + Shell::Elvish => {}, + Shell::Fish => { + let _ = out.write_all(FISH_EXTRA_COMPLETION); + }, + Shell::PowerShell => {}, + Shell::Zsh => {}, + _ => {}, + }; } - writeln!(&mut message, "[SYSTEM DATA DIR]: {:?}", system_data_dir).unwrap(); - - writeln!(&mut message, "[ARROW SEPARATOR]: {}", ARROW_SEPARATOR).unwrap(); - message.push_str(" Is the [ARROW_SEPARATOR] displayed correctly?\n"); - message.push_str(" If not you may want to either start zellij with a compatible mode: 'zellij options --simplified-ui true'\n"); - let mut hyperlink_compat = String::new(); - hyperlink_compat.push_str(hyperlink_start); - hyperlink_compat.push_str("https://zellij.dev/documentation/compatibility.html#the-status-bar-fonts-dont-render-correctly"); - hyperlink_compat.push_str(hyperlink_mid); - hyperlink_compat.push_str("https://zellij.dev/documentation/compatibility.html#the-status-bar-fonts-dont-render-correctly"); - hyperlink_compat.push_str(hyperlink_end); - write!( - &mut message, - " Or check the font that is in use:\n {}\n", - hyperlink_compat - ) - .unwrap(); - message.push_str("[MOUSE INTERACTION]: \n"); - message.push_str(" Can be temporarily disabled through pressing the [SHIFT] key.\n"); - message.push_str(" If that doesn't fix any issues consider to disable the mouse handling of zellij: 'zellij options --disable-mouse-mode'\n"); - - let default_editor = std::env::var("EDITOR") - .or_else(|_| std::env::var("VISUAL")) - .unwrap_or_else(|_| String::from("Not set, checked $EDITOR and $VISUAL")); - writeln!(&mut message, "[DEFAULT EDITOR]: {}", default_editor).unwrap(); - writeln!(&mut message, "[FEATURES]: {:?}", FEATURES).unwrap(); - let mut hyperlink = String::new(); - hyperlink.push_str(hyperlink_start); - hyperlink.push_str("https://www.zellij.dev/documentation/"); - hyperlink.push_str(hyperlink_mid); - hyperlink.push_str("zellij.dev/documentation"); - hyperlink.push_str(hyperlink_end); - writeln!(&mut message, "[DOCUMENTATION]: {}", hyperlink).unwrap(); - //printf '\e]8;;http://example.com\e\\This is a link\e]8;;\e\\\n' - - std::io::stdout().write_all(message.as_bytes())?; - - Ok(()) - } - fn generate_completion(shell: &str) { - let shell: Shell = match shell.to_lowercase().parse() { - Ok(shell) => shell, - _ => { - eprintln!("Unsupported shell: {}", shell); - std::process::exit(1); - }, - }; - let mut out = std::io::stdout(); - clap_complete::generate(shell, &mut CliArgs::command(), "zellij", &mut out); - // add shell dependent extra completion - match shell { - Shell::Bash => {}, - Shell::Elvish => {}, - Shell::Fish => { - let _ = out.write_all(FISH_EXTRA_COMPLETION); - }, - Shell::PowerShell => {}, - Shell::Zsh => {}, - _ => {}, - }; - } - fn generate_auto_start(shell: &str) { - let shell: Shell = match shell.to_lowercase().parse() { - Ok(shell) => shell, - _ => { - eprintln!("Unsupported shell: {}", shell); - std::process::exit(1); - }, - }; + fn generate_auto_start(shell: &str) { + let shell: Shell = match shell.to_lowercase().parse() { + Ok(shell) => shell, + _ => { + eprintln!("Unsupported shell: {}", shell); + std::process::exit(1); + }, + }; - let mut out = std::io::stdout(); - match shell { - Shell::Bash => { - let _ = out.write_all(BASH_AUTO_START_SCRIPT); - }, - Shell::Fish => { - let _ = out.write_all(FISH_AUTO_START_SCRIPT); - }, - Shell::Zsh => { - let _ = out.write_all(ZSH_AUTO_START_SCRIPT); - }, - _ => {}, + let mut out = std::io::stdout(); + match shell { + Shell::Bash => { + let _ = out.write_all(BASH_AUTO_START_SCRIPT); + }, + Shell::Fish => { + let _ = out.write_all(FISH_AUTO_START_SCRIPT); + }, + Shell::Zsh => { + let _ = out.write_all(ZSH_AUTO_START_SCRIPT); + }, + _ => {}, + } } } -} -#[cfg(test)] -mod setup_test { - use super::Setup; - use crate::data::InputMode; - use crate::input::{ - config::{Config, ConfigError}, - layout::LayoutFromYamlIntermediate, - options::Options, - }; + #[cfg(test)] + mod setup_test { + use super::Setup; + use crate::data::InputMode; + use crate::input::{ + config::{Config, ConfigError}, + layout::LayoutFromYamlIntermediate, + options::Options, + }; - fn deserialise_config_and_layout( - config: &str, - layout: &str, - ) -> Result<(Config, LayoutFromYamlIntermediate), ConfigError> { - let config = Config::from_yaml(config)?; - let layout = LayoutFromYamlIntermediate::from_yaml(layout)?; - Ok((config, layout)) - } + fn deserialise_config_and_layout( + config: &str, + layout: &str, + ) -> Result<(Config, LayoutFromYamlIntermediate), ConfigError> { + let config = Config::from_yaml(config)?; + let layout = LayoutFromYamlIntermediate::from_yaml(layout)?; + Ok((config, layout)) + } - #[test] - fn empty_config_empty_layout() { - let goal = Config::default(); - let config = r""; - let layout = r""; - let config_layout_result = deserialise_config_and_layout(config, layout); - let (config, layout) = config_layout_result.unwrap(); - let config_options = Options::default(); - let (config, _layout, _config_options) = - Setup::merge_config_with_layout(config, Some(layout), config_options).unwrap(); - assert_eq!(config, goal); - } + #[test] + fn empty_config_empty_layout() { + let goal = Config::default(); + let config = r""; + let layout = r""; + let config_layout_result = deserialise_config_and_layout(config, layout); + let (config, layout) = config_layout_result.unwrap(); + let config_options = Options::default(); + let (config, _layout, _config_options) = + Setup::merge_config_with_layout(config, Some(layout), config_options).unwrap(); + assert_eq!(config, goal); + } - #[test] - fn config_empty_layout() { - let mut goal = Config::default(); - goal.options.default_shell = Some(std::path::PathBuf::from("fish")); - let config = r"--- - default_shell: fish"; - let layout = r""; - let config_layout_result = deserialise_config_and_layout(config, layout); - let (config, layout) = config_layout_result.unwrap(); - let config_options = Options::default(); - let (config, _layout, _config_options) = - Setup::merge_config_with_layout(config, Some(layout), config_options).unwrap(); - assert_eq!(config, goal); - } + #[test] + fn config_empty_layout() { + let mut goal = Config::default(); + goal.options.default_shell = Some(std::path::PathBuf::from("fish")); + let config = r"--- + default_shell: fish"; + let layout = r""; + let config_layout_result = deserialise_config_and_layout(config, layout); + let (config, layout) = config_layout_result.unwrap(); + let config_options = Options::default(); + let (config, _layout, _config_options) = + Setup::merge_config_with_layout(config, Some(layout), config_options).unwrap(); + assert_eq!(config, goal); + } - #[test] - fn layout_overwrites_config() { - let mut goal = Config::default(); - goal.options.default_shell = Some(std::path::PathBuf::from("bash")); - let config = r"--- - default_shell: fish"; - let layout = r"--- - default_shell: bash"; - let config_layout_result = deserialise_config_and_layout(config, layout); - let (config, layout) = config_layout_result.unwrap(); - let config_options = Options::default(); - let (config, _layout, _config_options) = - Setup::merge_config_with_layout(config, Some(layout), config_options).unwrap(); - assert_eq!(config, goal); - } + #[test] + fn layout_overwrites_config() { + let mut goal = Config::default(); + goal.options.default_shell = Some(std::path::PathBuf::from("bash")); + let config = r"--- + default_shell: fish"; + let layout = r"--- + default_shell: bash"; + let config_layout_result = deserialise_config_and_layout(config, layout); + let (config, layout) = config_layout_result.unwrap(); + let config_options = Options::default(); + let (config, _layout, _config_options) = + Setup::merge_config_with_layout(config, Some(layout), config_options).unwrap(); + assert_eq!(config, goal); + } - #[test] - fn empty_config_nonempty_layout() { - let mut goal = Config::default(); - goal.options.default_shell = Some(std::path::PathBuf::from("bash")); - let config = r""; - let layout = r"--- - default_shell: bash"; - let config_layout_result = deserialise_config_and_layout(config, layout); - let (config, layout) = config_layout_result.unwrap(); - let config_options = Options::default(); - let (config, _layout, _config_options) = - Setup::merge_config_with_layout(config, Some(layout), config_options).unwrap(); - assert_eq!(config, goal); - } + #[test] + fn empty_config_nonempty_layout() { + let mut goal = Config::default(); + goal.options.default_shell = Some(std::path::PathBuf::from("bash")); + let config = r""; + let layout = r"--- + default_shell: bash"; + let config_layout_result = deserialise_config_and_layout(config, layout); + let (config, layout) = config_layout_result.unwrap(); + let config_options = Options::default(); + let (config, _layout, _config_options) = + Setup::merge_config_with_layout(config, Some(layout), config_options).unwrap(); + assert_eq!(config, goal); + } - #[test] - fn nonempty_config_nonempty_layout() { - let mut goal = Config::default(); - goal.options.default_shell = Some(std::path::PathBuf::from("bash")); - goal.options.default_mode = Some(InputMode::Locked); - let config = r"--- - default_mode: locked"; - let layout = r"--- - default_shell: bash"; - let config_layout_result = deserialise_config_and_layout(config, layout); - let (config, layout) = config_layout_result.unwrap(); - let config_options = Options::default(); - let (config, _layout, _config_options) = - Setup::merge_config_with_layout(config, Some(layout), config_options).unwrap(); - assert_eq!(config, goal); + #[test] + fn nonempty_config_nonempty_layout() { + let mut goal = Config::default(); + goal.options.default_shell = Some(std::path::PathBuf::from("bash")); + goal.options.default_mode = Some(InputMode::Locked); + let config = r"--- + default_mode: locked"; + let layout = r"--- + default_shell: bash"; + let config_layout_result = deserialise_config_and_layout(config, layout); + let (config, layout) = config_layout_result.unwrap(); + let config_options = Options::default(); + let (config, _layout, _config_options) = + Setup::merge_config_with_layout(config, Some(layout), config_options).unwrap(); + assert_eq!(config, goal); + } } } diff --git a/zellij-utils/src/shared.rs b/zellij-utils/src/shared.rs index b916c4119d..37d999d1a1 100644 --- a/zellij-utils/src/shared.rs +++ b/zellij-utils/src/shared.rs @@ -5,16 +5,23 @@ use std::{iter, str::from_utf8}; use crate::data::{Palette, PaletteColor, PaletteSource, ThemeHue}; use crate::envs::get_session_name; use colorsys::Rgb; -use std::os::unix::fs::PermissionsExt; -use std::path::Path; -use std::{fs, io}; use strip_ansi_escapes::strip; use unicode_width::UnicodeWidthStr; -pub fn set_permissions(path: &Path, mode: u32) -> io::Result<()> { - let mut permissions = fs::metadata(path)?.permissions(); - permissions.set_mode(mode); - fs::set_permissions(path, permissions) +#[cfg(unix)] +pub use unix_only::*; + +#[cfg(unix)] +mod unix_only { + use std::os::unix::fs::PermissionsExt; + use std::path::Path; + use std::{fs, io}; + + pub fn set_permissions(path: &Path, mode: u32) -> io::Result<()> { + let mut permissions = fs::metadata(path)?.permissions(); + permissions.set_mode(mode); + fs::set_permissions(path, permissions) + } } pub fn ansi_len(s: &str) -> usize {