diff options
| author | har7an <99636919+har7an@users.noreply.github.com> | 2022-07-27 14:48:35 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-07-27 16:48:35 +0200 |
| commit | f76b82820949067f66afdaf3f735e662bbacab12 (patch) | |
| tree | 446836796c30b229dbedfedfe151c3820b8a4a77 /default-plugins/status-bar | |
| parent | Log every panic to the logfile (#1602) (diff) | |
| download | sl0ppij-f76b82820949067f66afdaf3f735e662bbacab12.tar.gz sl0ppij-f76b82820949067f66afdaf3f735e662bbacab12.tar.xz sl0ppij-f76b82820949067f66afdaf3f735e662bbacab12.zip | |
fix(status-bar): reflect actual current keybindings (#1242)
* status-bar: first_line: Use more generic var names
Rename all `CtrlKey...` to the equivalent `Key...` to make the name less
specific. It implies that all key bindings use Ctrl as modifier key,
which needn't necessarily be the case.
* status-bar: first_line: Refactor `ctrl_keys`
Removes lots of code duplication by `Unselect`ing all keys by default
and only `Select`ing what is actually required for a given Input mode.
* utils: conditionally compile unix-specific code
In `zellij_utils`, the following modules each contained code that was
previously targeting only the unix platform:
- consts: Works with unix-specific filesystem attributes to set e.g.
special file permissions. Also relies on having a UID.
- shared: Uses unix-specific filesystem attributes to set file
permissions
These will never work when targeting wasm. Hence the concerning code
passages have been moved into private submodules that are only compiled
and re-exported when the target isn't `#[cfg(unix)]`. The re-export
makes sure that crates from the outside that use `zellij_utils` work as
before, since from their point of view nothing has changed.
* utils: Share more modules with wasm
that work on both wasm and unix natively. This requires factoring out
bits of code in the `setup` and `input` modules into a private submodule
that is re-exported when the compilation target is *not* "wasm". The
following modules are now available to the wasm target:
- cli
- consts
- data
- envs
- input (partial)
- actions
- command
- configs
- keybinds
- layout
- options
- plugins
- theme
- pane_size
- position
- setup (partial)
- shared
The remaining modules unavailable to wasm have dependencies on crates
that cannot compile against wasm, such as `async_std` or `termwiz`.
* utils/input/keybinds_test: Fix import
of the `CharOrArrow` struct which is now part of the `data` submodule.
* utils/layout: Use global serde crate
Previously the code was decorated with `#[serde(crate = "self::serde")]`
statements which cannot be shared with wasm. Use the regular serde
without specifying which serde is meant.
* utils/data: Implement `fmt::Display` for `Key`
so the Keybindings can be displayed via `format!` and friends in e.g.
the status bar.
* tile/prelude: Re-export `actions`
submodule of `zellij_utils` so the plugins can access the `ModeKeybinds`
struct with all of its members.
* utils/data: Fix `ModeInfo::keybinds` type
and transfer a vector of `(Key, Vec<Action>)` to the plugins so they can
parse it themselves, instead of passing strings around. Due to the
requirement of the `Eq` trait derive on `ModeInfo` this requires
deriving `Eq` on all the types included by `Key` and `Action` as well.
Note that `Action` includes the `layout::SplitSize` structure as a
member. We cannot derive `Eq` here since `SplitSize::Percent(f64)`
cannot satisfy `Eq` because `f64` doesn't implement this. So we add a
new type to hack around this limitation by storing the percentage as
`u64` internally, scaled by a factor of 10 000 and transforming it to
f64 when needed. Refer to the documentation of `layout::Percent` for
further information.
* utils/data: Make `Key` sortable
so the keybindings can be sorted after their keys.
* WIP: utils/input: Make keybinds accessible
when generating `ModeInfo` structs.
* utils/data: Handle unprintable chars in `Key`
when displaying via the `fmt::Display` trait. Handles `\t` and `\n` and
represents them as UTF-8 arrow glyphs.
* HACK: utils/layout: Use u64 for SplitSize::Percent
The previous workaround using a custom `Percent` type fails at the
absolute latest when confronted with user layouts, since these do not
know about the scaling factor and will thus break. It still breaks
currently because `Percent` now expects a u64 (i.e. `50`, not `50.0`)
but this is more easily explained and understood.
* status-bar: Add helper macros
that retrieve the key bound to execute a sequence of `Action` given a
specific Keybinding, and a shorthand that expands to
`Action::SwitchToMode(InputMode::Normal)` used for pattern matching with
the `matches!` macro.
* status-bar/first_line: Get shared superkey if any
from the `ModeKeybindings` in the current `ModeInfo` struct. If the
configured keybindings for switching the modes don't have a superkey in
common, do not print a common prefix.
* status-bar/first_line: Add key to KeyShortcut
which is the key that must be pressed in the current mode to execute the
given shortcut (i.e. switch to the given mode).
* status-bar/first_line: Dynamically set mode binds
Read the keybindings for switching the modes to print in the first line
from the actually configured keybindings for the current mode. Add some
logic to the code that:
- Prints only the "single letter" of the keybinding if all mode-switch
shortcuts *share the same modifier key*,
- Or prints the whole keybinding (with modified) into each segment if
there is no common modifier key.
* status-bar/second_line: Display configured binds
Instead of showing some hard-coded default values. For each mode, reads
the keybindings from the configured keybindings based on some sequence
of action. For example, the keybinding for `New` in the `Pane` menu is
now determined by looking into the configured keybindings and finding
what key is bound to the `Action::NewPane(None)` action.
If no keybinding is found for a given sequence of actions, it will not
show up in the segments either.
* WIP: utils/keybinds: Make key order deterministic
by using a BTreeMap which by default has all of its elements in sorted
order internally. As of currently this doesn't seem to impress the order
in which the keybindings are sent to the plugins, though.
* utils/data: Reorder `Key` variants
to have the Arrow keys sorted as "left", "down", "up", "right" in
accordance with the display in e.g. the status bar.
* status-bar/first_line: Fix inverted `matches!`
when trying to obtain the keybindings to switch between the input modes.
Its initial purpose was to filter out all ' ', '\n' and 'Esc'
keybindings for switching modes (As these are the default and not of
interest for the status bar display), but it was not negated and thus
only filtered out the aforementioned keys.
* status-bar: Don't get all modeswitch keybinds
but only those that are displayed in the status bar. This currently
excludes the keybindings for Entering the Pane/TabRename mode, Tmux mode
and Prompt mode. We must explicitly exclude these since they aren't
bound to the same Modifiers as the regular keys. Thus, if we e.g. enter
Pane or Tab mode, it will pick up the
`SwitchToMode(InputMode::TabRename)` action as being bound to `c`, hence
the `superkey` function cannot find a common modifier, etc. But we don't
display the `TabRename` input mode in the first line anyway, so we must
ignore it.
Therefore, we additionally add the keybinding to call the `Action::Quit`
action to terminate zellij to the vector we return. Also remove the
`(Key, InputMode)` tuple and convert the return type to a plain
`Vec<Key>`, since the never worked with the `InputMode` in the first
place.
* status-bar/first_line: Fix output for tight screen
Implement the "Squeezed" display variant where we do not display which
of the modes each keybinding switches to, but only the keybinding
itself.
* status-bar/second_line: Remove trailing " / "
* status-bar/second-line: Refactor key hints
Instead of determining the appropriate key hints for every case
separately (i.e. enough space to show all, show shortened, shot
best-effort), create a central function that returns for the current
`InputMode` a Vector with tuples of:
- A String to show in full-length mode
- A String to show in shortened/best-effort mode
- The vector of keys that goes with this key hint
This allows all functions that need the hints to iterate over the vector
and pick whatever hint suits them along with the Keys to display.
* status-bar/second-line: Implement shortened hints
* utils/data: Fix display for `Key::Alt`
which previously printed only the internal char but not the modifier.
* status-bar/first-line: Add hidden Tmux tile
that is only shown when in Tmux mode. Note that with the default config
this "breaks" the shared superkey display, because it correctly
identifies that one can switch to Scroll mode via `[`.
* status-bar: Print superkey as part of first line
Instead of first obtaining the superkey and then the rest of the first
line to display. This way we don't need to split up individual data
structures and carry a boolean flag around multiple functions.
It also has the advantage that when the available space is really tight,
the first line is entirely empty and doesn't display a stale superkey
without any other keybinding hints.
* status-bar: Rework keybinding theming
Previously there were individual functions to create the tiles in the
first line depending on whether:
- A tile was selected, unselected, unselected alternate (for theming) or disabled, and
- Tiles had full length or were displayed shortened
In the first case, the functions that previously handled the theming
only differed in what theme they apply to the otherwise identical
content. Since the theming information was drawn from a flat structure
that simulated hierarchy by giving hierarchical names to its theme
"members", this couldn't be handled in code. In the second case, some of
the theming information needed for the full-length shortcuts was
replicated for the shortened shortcuts.
Instead, rewrite the general Theming structure into a hierarchical one:
Adds a new structure `SegmentStyle` that contains the style for a single
segment depending on whether it is selected, unselected (alternate) or
disabled. Refactor the `first-line` module to use a single function to
generate either full-length or shortened tiles, that does functionally
the same but switches themes based on the selection status of the tile
it themes.
* status-bar/second-line: Return new `LinePart`s
from the `add_shortcut` function instead of modifying the input
parameters.
* status-bar/second-line: Implement adaptive behavior
and make the keyhints adapt when the screen runs out of space. The hints
first become shortened and when necessary partially disappear to display
a "..." hint instead.
* status-bar/second-line: Show float pane binding
based on the keycombination that's really bound to switching into the
"Pane" input mode.
* status-bar/get_keys_and_hints: Add more modes
for the keybindings in Tmux and the Pane/TabRename input modes.
* status-bar/second-line: Unify mode handling
and don't do extra shortcut handling for Tmux and the Pane/TabRename
modes any longer. Instead, assemble this like for all other modes from
the keybinding and hints vector.
* status-bar/first-line: Refactor common modifier
to a separate function so it can be used by other modules, too.
* status-bar/second-line: Display modifier in hints
when available. For example, for bindings to move between panes when in
PaneRename mode, now displays "Alt + <hjkl>" instead of
"<Alt+hAlt+j...>".
* utils/ipc: Remove `Copy` from `ClientAttributes`
as preparation to add `Keybinds` as a member to the `ClientAttributes`
struct. `Keybinds` contains a `HashMap`, for which the `std` doesn't
derive `Copy` but only `Clone`.
* utils/input/keybinds: Fix import path
Import `Key` and `InputMode` directly from `data`.
* utils/ipc: Add `Keybinds` to `ClientAttributes`
so we can keep track, pre-client, of the configured key bindings and
pass them around further in the code.
* server/lib: Store `ClientAttributes` over `Style`
in `SessionMetadata` to be able to pass Keybindings to other places in
the code, too. Since `Style` is also a member of `ClientAttributes`,
this works with minimal modifications.
* utils/input: Change `get_mode_info` parameters
to take a `ClientAttributes` struct instead of merely the `Style`
information. This way we can get the `Style` from the
`ClientAttributes`, and also have access to the `keybinds` member that
stores the keybinding configuration.
* utils/ipc: Use `rmp` for serde of IPC messages
instead of `bincode`, which seemingly has issues (de)serializing
`HashMap`s and `BTreeMap`s since `deserialize_any` isn't implemented for
these types.
* fix(nix): remove `assets` from `gitignore`
Remove `assets` from the gitignore of the plugins themselves,
since every single plugin now depends on the asset being accessible
in its source directory.
* tests/e2e: Fix status bar in snapshots
to reflect the current state of the dynamic keybindings.
* status_bar/first_line: Don't show unbound modes
If switching to a specific mode isn't bound to a key, don't show a
tile/ribbon for it either. E.g. in `LOCKED` mode, this will only show
the tile for the `LOCK` mode and ignore all others.
* utils/data: Make 'Key::Char(' ') visible as "␣"
so the user doesn't only see a blank char but has an idea that the space
key is meant.
* status_bar/second_line: Remove extra hints
generated by the `hint_producing_function` that would tell the user in
every input mode how to get back to normal mode. Instead, add this as
keybinding to the general keybindings vector.
This removes some lines of duplicated code but most of all now applies
the correct theming to this keybinding. Additionally, previously the
`RenameTab` and `RenamePane` input modes would show the keybinding to
get back to normal mode twice and both of them were hardcoded. This
binding is now dynamically displayed based on what the user configured
as keybinding.
* utils/data: format unprintable chars as words
instead of unicode symbols. E.g. write "SPACE" instead of "␣".
* utils/data: Fix display for `Ctrl`/`Alt` keys
previously their "inner" chars would be displayed with a regular
`fmt::Display` for the `&str` type they are. This doesn't match what we
want to output. So instead we wrap the inner chars into `Key::Char`
before printing them.
* utils/data: Change order of `Key`s
so that e.g. for the default bindings in `Scroll` mode we prefer to show
`PgDn|PgUp` rather than the arrow keys these actions are bound to as
well.
* status_bar/first_line: Don't ignore default char
bindings by default. These include the '\n', ' ' and 'Esc' bindings that
by default lead back to `Normal` input mode from all the modes.
Previously we would unconditionally ignore them and consequently not
print the tile when in fact the user may have bound this particular
action to either of the keys.
Instead now we first ignore the keys mentioned and if we turn up with an
undefined binding, we consider these default keys as well so we get
*something* to display in any case.
* status_bar/first_line: Add space when no modifier
is shared between the keybindings. This way there isn't a stray arrow at
the very border of the screen, but it is spaced just like the tab-bar
and the second line is.
* status_bar/second_line: Print separators
between consecutive keys bound to specific actions. This allows the user
to visually differ between different keys.
* status_bar/main: Don't return modifier if empty
* status_bar/first_line: Don't suppress Disabled tiles
Disabled is a special state that the keybindings only assume in locked
mode. It turns the respective tiles grey to signal to the user that
these are currently inactive. With respect to users new to zellij, it
may appear confusing that when entering locked mode all the other tiles
disappear (which they do because they have no valid keybinding
assigned). Since we have no keybinding for them, we still display them
but without any associated key (i.e. as `<>` for the binding).
* status_bar/first_line: Don't print leading triangle
on first tile, when there is no shared superkey.
* status_bar/second_line: Add exceptions
for inter-key separators. Keeps groups of `hjkl` and arrow keys intact
(doesn't add separators between the keys) but separates all others.
* status_bar/main: Refactor `action_key`
to a regular function instead of a macro. It turns out that while being
able to match patterns is a nice feature, we completely rely on the keys
that drop out of the pattern found this way to be sorted in a sensible
way. Since we sort the key vectors in the necessary places after the
keys, and not the actions, this of course doesn't apply when the user
changes "hjkl" to "zjkl", which would then become "jklz". Now this is of
course wrong, because "z" still means "Move focus left", and not "Move
focus right".
With the function we now assume a slice of Actions that we match the
action vectors from the keybindings against to obtain the necessary
keys. In order to avoid ugly `into_iter().chain(...)` constructs we had
before we also add a new function `action_key_group` that takes a sliced
array of slices to get a whole group of keys to display.
* status_bar/first_line: Fix "triangle" for short tiles
since we do not want to display a colored triangle at the start of the
line when in sortened mode (just as we do for the long tiles now).
Also fix a bug that would make the triangle reappear when the first
keybinding to be displayed didn't have a key assigned and thus wouldn't
be displayed at all.
* status_bar/second_line: Fix typo
that would cause single `Ctrl+?` bindings for actions in the second line
to be displayed as `Ctrl + <Ctrl+?>`.
* status_bar/second_line: Fix char count
when displaying groups of keys in a binding with or without a separator.
* status_bar: Use new `action_key` fn
instead of the previous macro to obtain the keys to display in the
status bar in a fixed given order. Also fix the display "bug" where tab
switching would be shows as "ArrowLeft/ArrowDown" instead of
"ArrowLeft/ArrowRight".
* status_bar/second_line: Fix floating pane hint
that tells the user what keybinding to press to suppress the currently
active floating panes. This was previously hardcoded.
* utils: Send full keybinds in `ModeInfo`
instead of the currently active `ModeKeybinds` for the active input
mode. Some of the UI issues cannot be solved without having access to
*all* keybindings.
* utils: Refactor keybinds vec into type
to make clippy happy.
* status_bar/first_line: Remove needless borrows
* status_bar: Factor out printing keybindings
into a separate function that takes a vector of keys and a palette and
returns the painted key groups, with correct inter-character separation
where necessary and factoring out common modifier keys.
* status_bar/tip: Use real keybindings
instead of printing hard-coded messages to the user.
* status_bar: abort early when keyvector is empty
in `style_key_with_modifier`.
* status_bar/tip: Fix all keybindings
and make them dynamic given the keybindings really active in the current
session. Also display **UNBOUND** is some keybinding is missing from the
users config.
* status_bar: Go clippy!
* status_bar: Add documentation
and add a new exception group to `action_key_group` that ensures that
`hl` and `jk` won't be separated with `|`.
* status_bar/tip: Detect when key aren't bound
correctly and show "UNBOUND" as keyhint instead, then. Previously we
would only check the length of the whole keybinding segment, but that
isn't a good indicator since most of the bindings require changing modes
first, which already adds a variable number of letters to the segment.
However, there is not point in showing how to get to a certain mode, if
the binding needed in that mode doesn't exist.
* status_bar/first_line: Show bindings when locked
if the user has any configured.
* status_bar: Don't consider 'hl', 'jk' groups
that don't need a separator in between the letters.
* status_bar/second_line: Add "search" keybindings
for the new Search functionality.
* tests/e2e: Fix snapshots
with what the status bar now really displays.
* status_bar: Remove old comments
* status_bar/first_line: Rename 'long_tile'
to the more descriptive name 'mode_shortcut', which better describes
what this function does.
* status_bar/first_line: Fix spacing in simple UI
where the modifier would be shows as `Ctrl +`, without a trailing space.
This isn't an issue in regular mode, where we have the spacing from the
arrow gaps (`>>`) that "simulates" this effect.
* status_bar: Refactor and rename `ctrl_keys`
so it doesn't rely on some "external" index for operation any more.
* status_bar: Add unit tests to shared functions
and fix a bug in the process where certain `Ctrl` keybindings would be
displayed wrong.
* status_bar/first_line: Rename functions
responsible for printing the long and short shortcut keyhint tiles. Also
add some documentation that explains their purpose and the arguments
they accept.
* status_bar/tips: Remove stray "/" in quicknav tip
* utils/layout: Remove old comments
introduced when rewriting `SplitSize::Percent` to not hold an `f64`
type.
* status_bar: Add "regex" as test dependency
We use regular expressions to strip all ANSI escape sequences in the
strings that are produced by the plugin functions during testing. We do
not test for the style information, but merely for the raw text.
* status_bar: Implement unit tests
* Makefile: Always run tests on host triple
This allows the unit tests for all plugins to be run on the host as well
(because their default compilation target is wasm32-wasi).
* tests/e2e: Add test for custom bindings
in the status bar. Makes sure that the modified bindings from a custom
configuration file are read and applied to the UI.
Co-authored-by: a-kenji <aks.kenji@protonmail.com>
Diffstat (limited to 'default-plugins/status-bar')
14 files changed, 2203 insertions, 1094 deletions
diff --git a/default-plugins/status-bar/Cargo.toml b/default-plugins/status-bar/Cargo.toml index 4c611dc8..a2ca3175 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 6c10b883..ae8fc70e 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<Key>, } -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<Key>) -> 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: ` <g> 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<Key> { + 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<Key>) -> Option<Key> { + let key = kv + .iter() + .filter(|key| { + // These are general "keybindings" to get back to normal, they aren't interesting here. + !matches!(key, Key::Char('\n') | Key::Char(' ') | Key::Esc) + }) + .collect::<Vec<&Key>>() + .into_iter() + .next(); + // Maybe the user bound one of the ignored keys? + if key.is_none() { + return kv.first().cloned(); + } + key.cloned() +} + +/// Get the [`KeyShortcut`] for a specific [`InputMode`]. +/// +/// Iterates over the contents of `shortcuts` to find the [`KeyShortcut`] with the [`KeyAction`] +/// matching the [`InputMode`]. Returns a mutable reference to the entry in `shortcuts` if a match +/// is found or `None` otherwise. +/// +/// In case multiple entries in `shortcuts` match `mode` (which shouldn't happen), the first match +/// is returned. +fn get_key_shortcut_for_mode<'a>( + shortcuts: &'a mut [KeyShortcut], + mode: &InputMode, +) -> Option<&'a mut KeyShortcut> { + let key_action = match mode { + InputMode::Normal | InputMode::Prompt | InputMode::Tmux => return None, + InputMode::Locked => KeyAction::Lock, + InputMode::Pane | InputMode::RenamePane => KeyAction::Pane, + InputMode::Tab | InputMode::RenameTab => KeyAction::Tab, + InputMode::Resize => KeyAction::Resize, + InputMode::Move => KeyAction::Move, + InputMode::Scroll | InputMode::Search | InputMode::EnterSearch => KeyAction::Search, + InputMode::Session => KeyAction::Session, + }; + for shortcut in shortcuts.iter_mut() { + if shortcut.action == key_action { + return Some(shortcut); + } } + None } -pub fn 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, "+ <Ctrl+0> 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 + >> <a> PANE >> <b> RESIZE >> <c> 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, + " <Ctrl+a> PANE >> <Ctrl+b> RESIZE >> <c> 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, + " <Ctrl+a> LOCK >> <BACKSPACE> PANE >> <ENTER> TAB >> <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 23ee3de7..38728d25 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<String> { + 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<Action>)>` and contains +/// all keybindings for the current mode and one or more `p` patterns which match a sequence of +/// actions to search for. If within the keymap a sequence of actions matching `p` is found, all +/// keys that trigger the action pattern are returned as vector of `Vec<Key>`. +pub fn action_key(keymap: &[(Key, Vec<Action>)], action: &[Action]) -> Vec<Key> { + keymap + .iter() + .filter_map(|(key, acvec)| { + if acvec.as_slice() == action { + Some(*key) + } else { + None + } + }) + .collect::<Vec<Key>>() +} + +/// 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<Action>)], actions: &[&[Action]]) -> Vec<Key> { + 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 `<KEYS>`, 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 + <KEYS>`. +/// +/// 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<ANSIString<'static>> { + // 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::<Vec<String>>(); + + // 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<Action>)> { + 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, "<a|b|c>".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, "<hjkl>".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, "<h|k|j|l>".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 + <a|b|c|d>".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 + <a|b|c|d>".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, "<Alt+a|Ctrl+b|c>".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, + "<BACKSPACE|ENTER|SPACE|TAB|PgDn|DEL|HOME|END|INS|TAB|ESC>".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 + <ENTER|SPACE|TAB>".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 + <ENTER|SPACE|TAB>".to_string()) + } +} diff --git a/default-plugins/status-bar/src/second_line.rs b/default-plugins/status-bar/src/second_line.rs index 40be3fcd..26afcd94 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<Key>, + 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<ANSIString> = 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::<Vec<_>>(); - LinePart { - part: ANSIStrings(&styled_text[..]).to_string(), - len, - } -} +fn add_shortcut(help: &ModeInfo, linepart: &LinePart, text: &str, keys: Vec<Key>) -> 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), - ("<ENTER>", 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<Key>` 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 + <hjkl>"), 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>` 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<Key>)> { + 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<Action>> = vec![]; + let mut km = vec![]; + for (key, acvec) in old_keymap { + if known_actions.contains(&acvec) { + // This action is known already + continue; + } else { + known_actions.push(acvec.to_vec()); + km.push((key, acvec)); } - 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<Key> = 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, " / <a> 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, " <a> 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, " / <ENTER> 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, " / <BACKSPACE> 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 + <a> 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 + <a> 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, " / <a|b|c> 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, " / <a|Ctrl+b|ENTER> 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 + <a|b|c> 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 / <n> New / <x> Close / <f> 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 / <n> 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 + <a|ENTER|1|SPACE> Move focus / <BACKSPACE> New / <ESC> Close / <END> 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 1632a836..dd67ab8f 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 + <p> + <z> - 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("<p>"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("<z>"), - ]) + ]; + 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 + <p> + <z> - 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("<p>"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("<z>"), - ]) + ]; + 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 + <p> + <z> - 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("<p>"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("<z>"), - ]) + ]; + bits.extend(add_keybinds(help)); + strings!(&bits) +} + +fn add_keybinds(help: &ModeInfo) -> Vec<ANSIString> { + 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 3807f55d..bef8acba 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 + <s> + <e> - 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("<s>"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("<e>"), - ]) + ]; + 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 + <s> + <e> - 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("<s>"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("<e>"), - ]) + ]; + 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 + <s> + <e> - 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("<s>"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("<e>"), - ]) + ]; + bits.extend(add_keybinds(help)); + strings!(&bits) +} + +fn add_keybinds(help: &ModeInfo) -> Vec<ANSIString> { + 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 64594987..d91c3ed8 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 + <p> + <w> 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("<p>"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("<w>"), - 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 + <p> + <w> - 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("<p>"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("<w>"), - ]) + ]; + 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 + <p> + <w> => 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<ANSIString> { + 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("<p>"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("<w>"), - 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 8e9f678c..b7a62ab8 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<ANSIString> { + 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 21d46fe8..59b604a7 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("<n>"), - 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("<n>"), - 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<ANSIString<'a>>, + move_focus: Vec<ANSIString<'a>>, + resize: Vec<ANSIString<'a>>, +} + +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 a4f7ee60..7daabeb6 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 + <mouse-click> 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("<mouse-click>"), - 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 + <mouse-click> 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("<mouse-click>"), - 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 + <mouse-click> => 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("<mouse-click>"), - 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 27d0ae7f..55331eb3 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 + <t> + <s> - 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("<t>"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("<s>"), - ]) + ]; + 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 + <t> + <s> - 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("<t>"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("<s>"), - ]) + ]; + 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 + <t> + <s> - 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<ANSIString> { + 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("<t>"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("<s>"), - ]) + 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 b9c8de58..4bbfbf94 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 c45deac2..3dd7b6d8 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 1249195f..9d4fbb70 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, |
