From f76b82820949067f66afdaf3f735e662bbacab12 Mon Sep 17 00:00:00 2001 From: har7an <99636919+har7an@users.noreply.github.com> Date: Wed, 27 Jul 2022 14:48:35 +0000 Subject: [PATCH] fix(status-bar): reflect actual current keybindings (#1242) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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)` 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`, 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 + " instead of "". * 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 + `. * 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 --- .nix/default.nix | 25 +- .nix/plugins.nix | 1 - Cargo.lock | 31 +- Makefile.toml | 32 +- default-plugins/status-bar/Cargo.toml | 3 + default-plugins/status-bar/src/first_line.rs | 1100 +++++++++++------ default-plugins/status-bar/src/main.rs | 732 ++++++++--- default-plugins/status-bar/src/second_line.rs | 790 +++++++----- .../status-bar/src/tip/data/compact_layout.rs | 77 +- .../src/tip/data/edit_scrollbuffer.rs | 78 +- .../src/tip/data/floating_panes_mouse.rs | 89 +- .../tip/data/move_focus_hjkl_tab_switch.rs | 101 +- .../status-bar/src/tip/data/quicknav.rs | 175 ++- .../tip/data/send_mouse_click_to_terminal.rs | 44 +- .../status-bar/src/tip/data/sync_tab.rs | 85 +- .../status-bar/src/tip/data/use_mouse.rs | 14 +- .../src/tip/data/zellij_setup_check.rs | 14 +- default-plugins/status-bar/src/tip/mod.rs | 3 +- src/tests/e2e/cases.rs | 35 + src/tests/e2e/remote_runner.rs | 56 + ...j__tests__e2e__cases__bracketed_paste.snap | 2 +- ...lly_when_active_terminal_is_too_small.snap | 2 +- ...zellij__tests__e2e__cases__close_pane.snap | 2 +- .../zellij__tests__e2e__cases__close_tab.snap | 2 +- ...e2e__cases__detach_and_attach_session.snap | 2 +- ...ts__e2e__cases__focus_pane_with_mouse.snap | 2 +- ...ts__e2e__cases__focus_tab_with_layout.snap | 2 +- .../zellij__tests__e2e__cases__lock_mode.snap | 2 +- ...ests__e2e__cases__mirrored_sessions-2.snap | 2 +- ..._tests__e2e__cases__mirrored_sessions.snap | 2 +- ...ers_in_different_panes_and_same_tab-2.snap | 2 +- ...users_in_different_panes_and_same_tab.snap | 2 +- ...s__multiple_users_in_different_tabs-2.snap | 2 +- ...ses__multiple_users_in_different_tabs.snap | 2 +- ...multiple_users_in_same_pane_and_tab-2.snap | 2 +- ...__multiple_users_in_same_pane_and_tab.snap | 2 +- ...llij__tests__e2e__cases__open_new_tab.snap | 2 +- ...ellij__tests__e2e__cases__resize_pane.snap | 2 +- ...s__e2e__cases__resize_terminal_window.snap | 2 +- ...__e2e__cases__scrolling_inside_a_pane.snap | 2 +- ...s__scrolling_inside_a_pane_with_mouse.snap | 2 +- ...2e__cases__split_terminals_vertically.snap | 2 +- ...e2e__cases__start_without_pane_frames.snap | 2 +- ..._e2e__cases__starts_with_one_terminal.snap | 2 +- ...__status_bar_loads_custom_keybindings.snap | 29 + .../zellij__tests__e2e__cases__tmux_mode.snap | 2 +- ...ts__e2e__cases__toggle_floating_panes.snap | 2 +- ...__e2e__cases__typing_exit_closes_pane.snap | 2 +- ...__tests__e2e__cases__undo_rename_pane.snap | 2 +- ...j__tests__e2e__cases__undo_rename_tab.snap | 2 +- src/tests/fixtures/configs/changed_keys.yaml | 26 + zellij-client/src/fake_client.rs | 1 + zellij-client/src/lib.rs | 1 + zellij-server/src/lib.rs | 18 +- zellij-server/src/route.rs | 7 +- zellij-server/src/screen.rs | 2 +- zellij-tile/src/prelude.rs | 1 + zellij-utils/Cargo.toml | 2 +- zellij-utils/src/consts.rs | 66 +- zellij-utils/src/data.rs | 97 +- zellij-utils/src/input/actions.rs | 8 +- zellij-utils/src/input/keybinds.rs | 31 +- zellij-utils/src/input/layout.rs | 24 +- zellij-utils/src/input/mod.rs | 258 ++-- zellij-utils/src/input/unit/keybinds_test.rs | 3 +- zellij-utils/src/input/unit/layout_test.rs | 42 +- zellij-utils/src/ipc.rs | 13 +- zellij-utils/src/lib.rs | 29 +- zellij-utils/src/position.rs | 6 +- zellij-utils/src/setup.rs | 940 +++++++------- zellij-utils/src/shared.rs | 21 +- 71 files changed, 3274 insertions(+), 1894 deletions(-) create mode 100644 src/tests/e2e/snapshots/zellij__tests__e2e__cases__status_bar_loads_custom_keybindings.snap create mode 100644 src/tests/fixtures/configs/changed_keys.yaml diff --git a/.nix/default.nix b/.nix/default.nix index c87221d8..07c0f5c1 100644 --- a/.nix/default.nix +++ b/.nix/default.nix @@ -68,6 +68,8 @@ flake-utils.lib.eachSystem [ }; cargo = rustToolchainToml; rustc = rustToolchainToml; + cargoMSRV = msrvToolchain; + rustcMSRV = msrvToolchain; buildInputs = [ # in order to run tests @@ -163,6 +165,7 @@ flake-utils.lib.eachSystem [ license = [licenses.mit]; }; in rec { + packages.default = packages.zellij-native; # crate2nix - better incremental builds, but uses ifd packages.zellij = crate2nixPkgs.callPackage ./crate2nix.nix { inherit @@ -177,7 +180,7 @@ in rec { nativeBuildInputs = nativeBuildInputs ++ defaultPlugins; }; - packages.zellij-msrv = crate2nixMsrvPkgs.callPackage ./crate2nix.nix { + packages.zellij-crate-msrv = crate2nixMsrvPkgs.callPackage ./crate2nix.nix { inherit name src @@ -204,7 +207,25 @@ in rec { ; nativeBuildInputs = nativeBuildInputs ++ defaultPlugins; }; - packages.default = packages.zellij; + # native nixpkgs support - msrv + packages.zellij-msrv = + (pkgs.makeRustPlatform { + cargo = cargoMSRV; + rustc = rustcMSRV; + }) + .buildRustPackage { + inherit + src + name + cargoLock + buildInputs + postInstall + patchPhase + desktopItems + meta + ; + nativeBuildInputs = nativeBuildInputs ++ defaultPlugins; + }; packages.plugins-compact = plugins.compact-bar; packages.plugins-status-bar = plugins.status-bar; diff --git a/.nix/plugins.nix b/.nix/plugins.nix index 270544c9..f3e1c8b7 100644 --- a/.nix/plugins.nix +++ b/.nix/plugins.nix @@ -10,7 +10,6 @@ ignoreSource = [ ".git" ".github" - "assets" "docs" "example" "target" diff --git a/Cargo.lock b/Cargo.lock index 4a3d08ab..eca15cb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1677,6 +1677,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "paste" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" + [[package]] name = "percent-encoding" version = "2.1.0" @@ -2049,6 +2055,28 @@ dependencies = [ "winapi", ] +[[package]] +name = "rmp" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44519172358fd6d58656c86ab8e7fbc9e1490c3e8f14d35ed78ca0dd07403c9f" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25786b0d276110195fa3d6f3f31299900cf71dfbd6c28450f3f58a0e7f7a347e" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + [[package]] name = "rustc-demangle" version = "0.1.21" @@ -2301,6 +2329,7 @@ dependencies = [ "colored", "lazy_static", "rand 0.8.5", + "regex", "serde", "serde_json", "thiserror", @@ -3348,7 +3377,6 @@ dependencies = [ "anyhow", "async-std", "backtrace", - "bincode", "clap", "clap_complete", "colored", @@ -3364,6 +3392,7 @@ dependencies = [ "nix", "once_cell", "regex", + "rmp-serde", "serde", "serde_json", "serde_yaml", diff --git a/Makefile.toml b/Makefile.toml index 16057604..f8ade7e6 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -18,18 +18,9 @@ dependencies = [ "clippy", ] -# Patching the default flows to skip testing of wasm32-wasi targets -[tasks.pre-test] -condition = { env = { "CARGO_MAKE_CRATE_TARGET_TRIPLE" = "wasm32-wasi" } } -env = { "SKIP_TEST" = true } - [tasks.test] -condition = { env_false = ["SKIP_TEST"] } -dependencies = ["pre-test"] -args = ["test", "--", "@@split(CARGO_MAKE_TASK_ARGS,;)"] - -[tasks.post-test] -env = { "SKIP_TEST" = false } +dependencies = ["get-host-triple"] +args = ["test", "--target", "${CARGO_HOST_TRIPLE}", "--", "@@split(CARGO_MAKE_TASK_ARGS,;)"] # Running Zellij using the development data directory [tasks.run] @@ -125,6 +116,25 @@ env = { "CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS" = [ ] } run_task = { name = "build", fork = true } +[tasks.get-host-triple] +script_runner = "@duckscript" +script = ''' +output = exec rustc -v -V +lines = split ${output.stdout} \n +triple = set "" +for line in ${lines} + if starts_with ${line} "host:" && not is_empty ${line} + bits = split ${line} " " + triple = array_get ${bits} 1 + triple = set ${triple} + end +end + +if not is_empty ${triple} + set_env CARGO_HOST_TRIPLE "${triple}" +end +''' + [tasks.wasm-opt-plugins] dependencies = ["build-plugins-release"] script_runner = "@duckscript" diff --git a/default-plugins/status-bar/Cargo.toml b/default-plugins/status-bar/Cargo.toml index 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, } -impl CtrlKeyShortcut { - pub fn new(mode: CtrlKeyMode, action: CtrlKeyAction) -> Self { - CtrlKeyShortcut { mode, action } - } -} - -enum CtrlKeyAction { +#[derive(PartialEq)] +enum KeyAction { Lock, Pane, Tab, @@ -24,54 +22,103 @@ enum CtrlKeyAction { Quit, Session, Move, + Tmux, } -enum CtrlKeyMode { +enum KeyMode { Unselected, UnselectedAlternate, Selected, Disabled, } -impl CtrlKeyShortcut { +impl KeyShortcut { + pub fn new(mode: KeyMode, action: KeyAction, key: Option) -> Self { + KeyShortcut { mode, action, key } + } + pub fn full_text(&self) -> String { match self.action { - CtrlKeyAction::Lock => String::from("LOCK"), - CtrlKeyAction::Pane => String::from("PANE"), - CtrlKeyAction::Tab => String::from("TAB"), - CtrlKeyAction::Resize => String::from("RESIZE"), - CtrlKeyAction::Search => String::from("SEARCH"), - CtrlKeyAction::Quit => String::from("QUIT"), - CtrlKeyAction::Session => String::from("SESSION"), - CtrlKeyAction::Move => String::from("MOVE"), + KeyAction::Lock => String::from("LOCK"), + KeyAction::Pane => String::from("PANE"), + KeyAction::Tab => String::from("TAB"), + KeyAction::Resize => String::from("RESIZE"), + KeyAction::Search => String::from("SEARCH"), + KeyAction::Quit => String::from("QUIT"), + KeyAction::Session => String::from("SESSION"), + KeyAction::Move => String::from("MOVE"), + KeyAction::Tmux => String::from("TMUX"), } } - pub fn letter_shortcut(&self) -> char { - match self.action { - CtrlKeyAction::Lock => 'g', - CtrlKeyAction::Pane => 'p', - CtrlKeyAction::Tab => 't', - CtrlKeyAction::Resize => 'n', - CtrlKeyAction::Search => 's', - CtrlKeyAction::Quit => 'q', - CtrlKeyAction::Session => 'o', - CtrlKeyAction::Move => 'h', + pub fn letter_shortcut(&self, with_prefix: bool) -> String { + let key = match self.key { + Some(k) => k, + None => return String::from("?"), + }; + if with_prefix { + format!("{}", key) + } else { + match key { + Key::F(c) => format!("{}", c), + Key::Ctrl(c) => format!("{}", c), + Key::Char(_) => format!("{}", key), + Key::Alt(c) => format!("{}", c), + _ => String::from("??"), + } } } } -fn unselected_mode_shortcut( - letter: char, - text: &str, +/// Generate long mode shortcut tile. +/// +/// A long mode shortcut tile consists of a leading and trailing `separator`, a keybinding enclosed +/// in `<>` brackets and the name of the mode displayed in capitalized letters next to it. For +/// example, the default long mode shortcut tile for "Locked" mode is: ` LOCK `. +/// +/// # Arguments +/// +/// - `key`: A [`KeyShortcut`] that defines how the tile is displayed (active/disabled/...), what +/// action it belongs to (roughly equivalent to [`InputMode`]s) and the keybinding to trigger +/// this action. +/// - `palette`: A structure holding styling information. +/// - `separator`: The separator printed before and after the mode shortcut tile. The default is an +/// arrow head-like separator. +/// - `shared_super`: If set to true, all mode shortcut keybindings share a common modifier (see +/// [`get_common_modifier`]) and the modifier belonging to the keybinding is **not** printed in +/// the shortcut tile. +/// - `first_tile`: If set to true, the leading separator for this tile will be ommited so no gap +/// appears on the screen. +fn long_mode_shortcut( + key: &KeyShortcut, palette: ColoredElements, separator: &str, + shared_super: bool, + first_tile: bool, ) -> LinePart { - let prefix_separator = palette.unselected_prefix_separator.paint(separator); - let char_left_separator = palette.unselected_char_left_separator.paint(" <"); - let char_shortcut = palette.unselected_char_shortcut.paint(letter.to_string()); - let char_right_separator = palette.unselected_char_right_separator.paint(">"); - let styled_text = palette.unselected_styled_text.paint(format!("{} ", text)); - let suffix_separator = palette.unselected_suffix_separator.paint(separator); + 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), + }; + + 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, @@ -82,378 +129,713 @@ fn unselected_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 unselected_alternate_mode_shortcut( - letter: char, - text: &str, +/// 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 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_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); - 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 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); + 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 unselected_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_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, - } -} - -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); - LinePart { - part: ANSIStrings(&[prefix_separator, char_shortcut, suffix_separator]).to_string(), - len, - } -} - -fn full_ctrl_key(key: &CtrlKeyShortcut, palette: ColoredElements, separator: &str) -> LinePart { - let full_text = key.full_text(); - let letter_shortcut = key.letter_shortcut(); - match key.mode { - CtrlKeyMode::Unselected => unselected_mode_shortcut( - letter_shortcut, - &format!(" {}", full_text), - palette, - separator, - ), - CtrlKeyMode::UnselectedAlternate => unselected_alternate_mode_shortcut( - letter_shortcut, - &format!(" {}", full_text), - palette, - separator, - ), - CtrlKeyMode::Selected => selected_mode_shortcut( - letter_shortcut, - &format!(" {}", full_text), - palette, - separator, - ), - CtrlKeyMode::Disabled => disabled_mode_shortcut( - &format!(" <{}> {}", letter_shortcut, full_text), - palette, - separator, - ), - } -} - -fn single_letter_ctrl_key( - key: &CtrlKeyShortcut, - palette: ColoredElements, - separator: &str, -) -> LinePart { - let letter_shortcut = key.letter_shortcut(); - match key.mode { - CtrlKeyMode::Unselected => { - unselected_mode_shortcut_single_letter(letter_shortcut, palette, separator) - }, - CtrlKeyMode::UnselectedAlternate => { - unselected_alternate_mode_shortcut_single_letter(letter_shortcut, palette, separator) - }, - CtrlKeyMode::Selected => { - selected_mode_shortcut_single_letter(letter_shortcut, palette, separator) - }, - CtrlKeyMode::Disabled => { - disabled_mode_shortcut(&format!(" {}", letter_shortcut), palette, separator) - }, + len: separator.chars().count() // Separator + + 1 // " " + + key_binding.chars().count() // Key binding + + 1 // " " + + separator.chars().count(), // Separator } } fn key_indicators( max_len: usize, - keys: &[CtrlKeyShortcut], + keys: &[KeyShortcut], palette: ColoredElements, separator: &str, + mode_info: &ModeInfo, ) -> LinePart { - let mut line_part = LinePart::default(); + // Print full-width hints + let mut line_part = superkey(palette, separator, mode_info); + let shared_super = line_part.len > 0; for ctrl_key in keys { - let key = full_ctrl_key(ctrl_key, palette, separator); + let line_empty = line_part.len == 0; + let key = long_mode_shortcut(ctrl_key, palette, separator, shared_super, line_empty); line_part.part = format!("{}{}", line_part.part, key.part); line_part.len += key.len; } if line_part.len < max_len { return line_part; } - line_part = LinePart::default(); + + // Full-width doesn't fit, try shortened hints (just keybindings, no meanings/actions) + line_part = superkey(palette, separator, mode_info); + let shared_super = line_part.len > 0; for ctrl_key in keys { - let key = single_letter_ctrl_key(ctrl_key, palette, separator); + let line_empty = line_part.len == 0; + let key = short_mode_shortcut(ctrl_key, palette, separator, shared_super, line_empty); line_part.part = format!("{}{}", line_part.part, key.part); line_part.len += key.len; } if line_part.len < max_len { return line_part; } + + // Shortened doesn't fit, print nothing line_part = LinePart::default(); line_part } -pub fn superkey(palette: ColoredElements, separator: &str) -> LinePart { - let prefix_text = if separator.is_empty() { - " Ctrl + " - } else { - " Ctrl +" +/// Get the keybindings for switching `InputMode`s and `Quit` visible in status bar. +/// +/// Return a Vector of `Key`s where each `Key` is a shortcut to switch to some `InputMode` or Quit +/// zellij. Given the vast amount of things a user can configure in their zellij config, this +/// function has some limitations to keep in mind: +/// +/// - The vector is not deduplicated: If switching to a certain `InputMode` is bound to multiple +/// `Key`s, all of these bindings will be part of the returned vector. There is also no +/// guaranteed sort order. Which key ends up in the status bar in such a situation isn't defined. +/// - The vector will **not** contain the ' ', '\n' and 'Esc' keys: These are the default bindings +/// to get back to normal mode from any input mode, but they aren't of interest when searching +/// for the super key. If for any input mode the user has bound only these keys to switching back +/// to `InputMode::Normal`, a '?' will be displayed as keybinding instead. +pub fn mode_switch_keys(mode_info: &ModeInfo) -> Vec { + mode_info + .get_mode_keybinds() + .iter() + .filter_map(|(key, vac)| match vac.first() { + // No actions defined, ignore + None => None, + Some(vac) => { + // We ignore certain "default" keybindings that switch back to normal InputMode. + // These include: ' ', '\n', 'Esc' + if matches!(key, Key::Char(' ') | Key::Char('\n') | Key::Esc) { + return None; + } + if let actions::Action::SwitchToMode(mode) = vac { + return match mode { + // Store the keys that switch to displayed modes + InputMode::Normal + | InputMode::Locked + | InputMode::Pane + | InputMode::Tab + | InputMode::Resize + | InputMode::Move + | InputMode::Scroll + | InputMode::Session => Some(*key), + _ => None, + }; + } + if let actions::Action::Quit = vac { + return Some(*key); + } + // Not a `SwitchToMode` or `Quit` action, ignore + None + }, + }) + .collect() +} + +pub fn superkey(palette: ColoredElements, separator: &str, mode_info: &ModeInfo) -> LinePart { + // Find a common modifier if any + let prefix_text = match get_common_modifier(mode_switch_keys(mode_info).iter().collect()) { + Some(text) => { + if mode_info.capabilities.arrow_fonts { + // Add extra space in simplified ui + format!(" {} + ", text) + } else { + format!(" {} +", text) + } + }, + _ => return LinePart::default(), }; - let prefix = palette.superkey_prefix.paint(prefix_text); + + let prefix = palette.superkey_prefix.paint(&prefix_text); let suffix_separator = palette.superkey_suffix_separator.paint(separator); LinePart { part: ANSIStrings(&[prefix, suffix_separator]).to_string(), - len: prefix_text.chars().count(), + len: prefix_text.chars().count() + separator.chars().count(), } } -pub fn ctrl_keys(help: &ModeInfo, max_len: usize, separator: &str) -> LinePart { +pub fn to_char(kv: Vec) -> Option { + let key = kv + .iter() + .filter(|key| { + // These are general "keybindings" to get back to normal, they aren't interesting here. + !matches!(key, Key::Char('\n') | Key::Char(' ') | Key::Esc) + }) + .collect::>() + .into_iter() + .next(); + // Maybe the user bound one of the ignored keys? + if key.is_none() { + return kv.first().cloned(); + } + key.cloned() +} + +/// Get the [`KeyShortcut`] for a specific [`InputMode`]. +/// +/// Iterates over the contents of `shortcuts` to find the [`KeyShortcut`] with the [`KeyAction`] +/// matching the [`InputMode`]. Returns a mutable reference to the entry in `shortcuts` if a match +/// is found or `None` otherwise. +/// +/// In case multiple entries in `shortcuts` match `mode` (which shouldn't happen), the first match +/// is returned. +fn get_key_shortcut_for_mode<'a>( + shortcuts: &'a mut [KeyShortcut], + mode: &InputMode, +) -> Option<&'a mut KeyShortcut> { + let key_action = match mode { + InputMode::Normal | InputMode::Prompt | InputMode::Tmux => return None, + InputMode::Locked => KeyAction::Lock, + InputMode::Pane | InputMode::RenamePane => KeyAction::Pane, + InputMode::Tab | InputMode::RenameTab => KeyAction::Tab, + InputMode::Resize => KeyAction::Resize, + InputMode::Move => KeyAction::Move, + InputMode::Scroll | InputMode::Search | InputMode::EnterSearch => KeyAction::Search, + InputMode::Session => KeyAction::Session, + }; + for shortcut in shortcuts.iter_mut() { + if shortcut.action == key_action { + return Some(shortcut); + } + } + None +} + +pub fn first_line(help: &ModeInfo, max_len: usize, separator: &str) -> LinePart { let supports_arrow_fonts = !help.capabilities.arrow_fonts; let colored_elements = color_elements(help.style.colors, !supports_arrow_fonts); - match &help.mode { - InputMode::Locked => key_indicators( - max_len, - &[ - CtrlKeyShortcut::new(CtrlKeyMode::Selected, CtrlKeyAction::Lock), - CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Pane), - CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Tab), - CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Resize), - CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Move), - CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Search), - CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Session), - CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Quit), - ], - colored_elements, - separator, + let binds = &help.get_mode_keybinds(); + // Unselect all by default + let mut default_keys = vec![ + KeyShortcut::new( + KeyMode::Unselected, + KeyAction::Lock, + to_char(action_key( + binds, + &[Action::SwitchToMode(InputMode::Locked)], + )), ), - InputMode::Resize => key_indicators( - max_len, - &[ - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Lock), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Pane), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Tab), - CtrlKeyShortcut::new(CtrlKeyMode::Selected, CtrlKeyAction::Resize), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Move), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Search), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Quit), - ], - colored_elements, - separator, + KeyShortcut::new( + KeyMode::UnselectedAlternate, + KeyAction::Pane, + to_char(action_key(binds, &[Action::SwitchToMode(InputMode::Pane)])), ), - InputMode::Pane | InputMode::RenamePane => key_indicators( - max_len, - &[ - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Lock), - CtrlKeyShortcut::new(CtrlKeyMode::Selected, CtrlKeyAction::Pane), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Tab), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Resize), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Move), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Search), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Quit), - ], - colored_elements, - separator, + KeyShortcut::new( + KeyMode::Unselected, + KeyAction::Tab, + to_char(action_key(binds, &[Action::SwitchToMode(InputMode::Tab)])), ), - InputMode::Tab | InputMode::RenameTab => key_indicators( - max_len, - &[ - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Lock), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Pane), - CtrlKeyShortcut::new(CtrlKeyMode::Selected, CtrlKeyAction::Tab), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Resize), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Move), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Search), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Quit), - ], - colored_elements, - separator, + KeyShortcut::new( + KeyMode::UnselectedAlternate, + KeyAction::Resize, + to_char(action_key( + binds, + &[Action::SwitchToMode(InputMode::Resize)], + )), ), - InputMode::EnterSearch | InputMode::Scroll | InputMode::Search => key_indicators( - max_len, - &[ - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Lock), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Pane), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Tab), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Resize), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Move), - CtrlKeyShortcut::new(CtrlKeyMode::Selected, CtrlKeyAction::Search), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Quit), - ], - colored_elements, - separator, + KeyShortcut::new( + KeyMode::Unselected, + KeyAction::Move, + to_char(action_key(binds, &[Action::SwitchToMode(InputMode::Move)])), ), - InputMode::Move => key_indicators( - max_len, - &[ - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Lock), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Pane), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Tab), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Resize), - CtrlKeyShortcut::new(CtrlKeyMode::Selected, CtrlKeyAction::Move), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Search), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Quit), - ], - colored_elements, - separator, + KeyShortcut::new( + KeyMode::UnselectedAlternate, + KeyAction::Search, + to_char(action_key( + binds, + &[Action::SwitchToMode(InputMode::Scroll)], + )), ), - InputMode::Normal | InputMode::Prompt => key_indicators( - max_len, - &[ - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Lock), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Pane), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Tab), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Resize), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Move), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Search), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Quit), - ], - colored_elements, - separator, + KeyShortcut::new( + KeyMode::Unselected, + KeyAction::Session, + to_char(action_key( + binds, + &[Action::SwitchToMode(InputMode::Session)], + )), ), - InputMode::Session => key_indicators( - max_len, - &[ - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Lock), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Pane), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Tab), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Resize), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Move), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Search), - CtrlKeyShortcut::new(CtrlKeyMode::Selected, CtrlKeyAction::Session), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Quit), - ], - colored_elements, - separator, + KeyShortcut::new( + KeyMode::UnselectedAlternate, + KeyAction::Quit, + to_char(action_key(binds, &[Action::Quit])), ), - InputMode::Tmux => key_indicators( - max_len, - &[ - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Lock), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Pane), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Tab), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Resize), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Move), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Search), - CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session), - CtrlKeyShortcut::new(CtrlKeyMode::UnselectedAlternate, CtrlKeyAction::Quit), + ]; + + if let Some(key_shortcut) = get_key_shortcut_for_mode(&mut default_keys, &help.mode) { + key_shortcut.mode = KeyMode::Selected; + key_shortcut.key = to_char(action_key(binds, &[TO_NORMAL])); + } + + // In locked mode we must disable all other mode keybindings + if help.mode == InputMode::Locked { + for key in default_keys.iter_mut().skip(1) { + key.mode = KeyMode::Disabled; + } + } + + if help.mode == InputMode::Tmux { + // Tmux tile is hidden by default + default_keys.push(KeyShortcut::new( + KeyMode::Selected, + KeyAction::Tmux, + to_char(action_key(binds, &[TO_NORMAL])), + )); + } + + key_indicators(max_len, &default_keys, colored_elements, separator, help) +} + +#[cfg(test)] +/// Unit tests. +/// +/// Note that we cheat a little here, because the number of things one may want to test is endless, +/// and creating a Mockup of [`ModeInfo`] by hand for all these testcases is nothing less than +/// torture. Hence, we test the most atomic units thoroughly ([`long_mode_shortcut`] and +/// [`short_mode_shortcut`]) and then test the public API ([`first_line`]) to ensure correct +/// operation. +mod tests { + use super::*; + + fn colored_elements() -> ColoredElements { + let palette = Palette::default(); + color_elements(palette, false) + } + + // Strip style information from `LinePart` and return a raw String instead + fn unstyle(line_part: LinePart) -> String { + let string = line_part.to_string(); + + let re = regex::Regex::new(r"\x1b\[[0-9;]*m").unwrap(); + let string = re.replace_all(&string, "".to_string()); + + string.to_string() + } + + #[test] + fn long_mode_shortcut_selected_with_binding() { + let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, Some(Key::Char('0'))); + let color = colored_elements(); + + let ret = long_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "+ <0> SESSION +".to_string()); + } + + #[test] + // Displayed like selected(alternate), but different styling + fn long_mode_shortcut_unselected_with_binding() { + let key = KeyShortcut::new( + KeyMode::Unselected, + KeyAction::Session, + Some(Key::Char('0')), + ); + let color = colored_elements(); + + let ret = long_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "+ <0> SESSION +".to_string()); + } + + #[test] + // Treat exactly like "unselected" variant + fn long_mode_shortcut_unselected_alternate_with_binding() { + let key = KeyShortcut::new( + KeyMode::UnselectedAlternate, + KeyAction::Session, + Some(Key::Char('0')), + ); + let color = colored_elements(); + + let ret = long_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "+ <0> SESSION +".to_string()); + } + + #[test] + // KeyShortcuts without binding are only displayed when "disabled" (for locked mode indications) + fn long_mode_shortcut_selected_without_binding() { + let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, None); + let color = colored_elements(); + + let ret = long_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "".to_string()); + } + + #[test] + // First tile doesn't print a starting separator + fn long_mode_shortcut_selected_with_binding_first_tile() { + let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, Some(Key::Char('0'))); + let color = colored_elements(); + + let ret = long_mode_shortcut(&key, color, "+", false, true); + let ret = unstyle(ret); + + assert_eq!(ret, " <0> SESSION +".to_string()); + } + + #[test] + // Modifier is the superkey, mustn't appear in angled brackets + fn long_mode_shortcut_selected_with_ctrl_binding_shared_superkey() { + let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, Some(Key::Ctrl('0'))); + let color = colored_elements(); + + let ret = long_mode_shortcut(&key, color, "+", true, false); + let ret = unstyle(ret); + + assert_eq!(ret, "+ <0> SESSION +".to_string()); + } + + #[test] + // Modifier must be in the angled brackets + fn long_mode_shortcut_selected_with_ctrl_binding_no_shared_superkey() { + let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, Some(Key::Ctrl('0'))); + let color = colored_elements(); + + let ret = long_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "+ SESSION +".to_string()); + } + + #[test] + // Must be displayed as usual, but it is styled to be greyed out which we don't test here + fn long_mode_shortcut_disabled_with_binding() { + let key = KeyShortcut::new(KeyMode::Disabled, KeyAction::Session, Some(Key::Char('0'))); + let color = colored_elements(); + + let ret = long_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "+ <0> SESSION +".to_string()); + } + + #[test] + // Must be displayed but without keybinding + fn long_mode_shortcut_disabled_without_binding() { + let key = KeyShortcut::new(KeyMode::Disabled, KeyAction::Session, None); + let color = colored_elements(); + + let ret = long_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "+ <> SESSION +".to_string()); + } + + #[test] + // Test all at once + // Note that when "shared_super" is true, the tile **cannot** be the first on the line, so we + // ignore **first** here. + fn long_mode_shortcut_selected_with_ctrl_binding_and_shared_super_and_first_tile() { + let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, Some(Key::Ctrl('0'))); + let color = colored_elements(); + + let ret = long_mode_shortcut(&key, color, "+", true, true); + let ret = unstyle(ret); + + assert_eq!(ret, "+ <0> SESSION +".to_string()); + } + + #[test] + fn short_mode_shortcut_selected_with_binding() { + let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, Some(Key::Char('0'))); + let color = colored_elements(); + + let ret = short_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "+ 0 +".to_string()); + } + + #[test] + fn short_mode_shortcut_selected_with_ctrl_binding_no_shared_super() { + let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, Some(Key::Ctrl('0'))); + let color = colored_elements(); + + let ret = short_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "+ Ctrl+0 +".to_string()); + } + + #[test] + fn short_mode_shortcut_selected_with_ctrl_binding_shared_super() { + let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, Some(Key::Ctrl('0'))); + let color = colored_elements(); + + let ret = short_mode_shortcut(&key, color, "+", true, false); + let ret = unstyle(ret); + + assert_eq!(ret, "+ 0 +".to_string()); + } + + #[test] + fn short_mode_shortcut_selected_with_binding_first_tile() { + let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, Some(Key::Char('0'))); + let color = colored_elements(); + + let ret = short_mode_shortcut(&key, color, "+", false, true); + let ret = unstyle(ret); + + assert_eq!(ret, " 0 +".to_string()); + } + + #[test] + fn short_mode_shortcut_unselected_with_binding() { + let key = KeyShortcut::new( + KeyMode::Unselected, + KeyAction::Session, + Some(Key::Char('0')), + ); + let color = colored_elements(); + + let ret = short_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "+ 0 +".to_string()); + } + + #[test] + fn short_mode_shortcut_unselected_alternate_with_binding() { + let key = KeyShortcut::new( + KeyMode::UnselectedAlternate, + KeyAction::Session, + Some(Key::Char('0')), + ); + let color = colored_elements(); + + let ret = short_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "+ 0 +".to_string()); + } + + #[test] + fn short_mode_shortcut_disabled_with_binding() { + let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, Some(Key::Char('0'))); + let color = colored_elements(); + + let ret = short_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "+ 0 +".to_string()); + } + + #[test] + fn short_mode_shortcut_selected_without_binding() { + let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, None); + let color = colored_elements(); + + let ret = short_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "".to_string()); + } + + #[test] + fn short_mode_shortcut_unselected_without_binding() { + let key = KeyShortcut::new(KeyMode::Unselected, KeyAction::Session, None); + let color = colored_elements(); + + let ret = short_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "".to_string()); + } + + #[test] + fn short_mode_shortcut_unselected_alternate_without_binding() { + let key = KeyShortcut::new(KeyMode::UnselectedAlternate, KeyAction::Session, None); + let color = colored_elements(); + + let ret = short_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "".to_string()); + } + + #[test] + fn short_mode_shortcut_disabled_without_binding() { + let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, None); + let color = colored_elements(); + + let ret = short_mode_shortcut(&key, color, "+", false, false); + let ret = unstyle(ret); + + assert_eq!(ret, "".to_string()); + } + + #[test] + // Observe: Modes missing in between aren't displayed! + fn first_line_default_layout_shared_super() { + #[rustfmt::skip] + let mode_info = ModeInfo{ + mode: InputMode::Normal, + keybinds : vec![ + (InputMode::Normal, vec![ + (Key::Ctrl('a'), vec![Action::SwitchToMode(InputMode::Pane)]), + (Key::Ctrl('b'), vec![Action::SwitchToMode(InputMode::Resize)]), + (Key::Ctrl('c'), vec![Action::SwitchToMode(InputMode::Move)]), + ]), ], - colored_elements, - separator, - ), + ..ModeInfo::default() + }; + + let ret = first_line(&mode_info, 500, ">"); + let ret = unstyle(ret); + + assert_eq!( + ret, + " Ctrl + >> PANE >> RESIZE >> MOVE >".to_string() + ); + } + + #[test] + fn first_line_default_layout_no_shared_super() { + #[rustfmt::skip] + let mode_info = ModeInfo{ + mode: InputMode::Normal, + keybinds : vec![ + (InputMode::Normal, vec![ + (Key::Ctrl('a'), vec![Action::SwitchToMode(InputMode::Pane)]), + (Key::Ctrl('b'), vec![Action::SwitchToMode(InputMode::Resize)]), + (Key::Char('c'), vec![Action::SwitchToMode(InputMode::Move)]), + ]), + ], + ..ModeInfo::default() + }; + + let ret = first_line(&mode_info, 500, ">"); + let ret = unstyle(ret); + + assert_eq!( + ret, + " PANE >> RESIZE >> MOVE >".to_string() + ); + } + + #[test] + fn first_line_default_layout_unprintables() { + #[rustfmt::skip] + let mode_info = ModeInfo{ + mode: InputMode::Normal, + keybinds : vec![ + (InputMode::Normal, vec![ + (Key::Ctrl('a'), vec![Action::SwitchToMode(InputMode::Locked)]), + (Key::Backspace, vec![Action::SwitchToMode(InputMode::Pane)]), + (Key::Char('\n'), vec![Action::SwitchToMode(InputMode::Tab)]), + (Key::Char('\t'), vec![Action::SwitchToMode(InputMode::Resize)]), + (Key::Left, vec![Action::SwitchToMode(InputMode::Move)]), + ]), + ], + ..ModeInfo::default() + }; + + let ret = first_line(&mode_info, 500, ">"); + let ret = unstyle(ret); + + assert_eq!( + ret, + " LOCK >> PANE >> TAB >> RESIZE >> <←> MOVE >" + .to_string() + ); + } + + #[test] + fn first_line_short_layout_shared_super() { + #[rustfmt::skip] + let mode_info = ModeInfo{ + mode: InputMode::Normal, + keybinds : vec![ + (InputMode::Normal, vec![ + (Key::Ctrl('a'), vec![Action::SwitchToMode(InputMode::Locked)]), + (Key::Ctrl('b'), vec![Action::SwitchToMode(InputMode::Pane)]), + (Key::Ctrl('c'), vec![Action::SwitchToMode(InputMode::Tab)]), + (Key::Ctrl('d'), vec![Action::SwitchToMode(InputMode::Resize)]), + (Key::Ctrl('e'), vec![Action::SwitchToMode(InputMode::Move)]), + ]), + ], + ..ModeInfo::default() + }; + + let ret = first_line(&mode_info, 50, ">"); + let ret = unstyle(ret); + + assert_eq!(ret, " Ctrl + >> a >> b >> c >> d >> e >".to_string()); + } + + #[test] + fn first_line_short_simplified_ui_shared_super() { + #[rustfmt::skip] + let mode_info = ModeInfo{ + mode: InputMode::Normal, + keybinds : vec![ + (InputMode::Normal, vec![ + (Key::Ctrl('a'), vec![Action::SwitchToMode(InputMode::Pane)]), + (Key::Ctrl('b'), vec![Action::SwitchToMode(InputMode::Resize)]), + (Key::Ctrl('c'), vec![Action::SwitchToMode(InputMode::Move)]), + ]), + ], + ..ModeInfo::default() + }; + + let ret = first_line(&mode_info, 30, ""); + let ret = unstyle(ret); + + assert_eq!(ret, " Ctrl + a b c ".to_string()); } } diff --git a/default-plugins/status-bar/src/main.rs b/default-plugins/status-bar/src/main.rs index 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 { + let mut modifier = ""; + let mut new_modifier; + for key in keyvec.iter() { + match key { + Key::Ctrl(_) => new_modifier = "Ctrl", + Key::Alt(_) => new_modifier = "Alt", + _ => return None, + } + if modifier.is_empty() { + modifier = new_modifier; + } else if modifier != new_modifier { + // Prefix changed! + return None; + } + } + match modifier.is_empty() { + true => None, + false => Some(modifier.to_string()), + } +} + +/// Get key from action pattern(s). +/// +/// This function takes as arguments a `keymap` that is a `Vec<(Key, Vec)>` and contains +/// all keybindings for the current mode and one or more `p` patterns which match a sequence of +/// actions to search for. If within the keymap a sequence of actions matching `p` is found, all +/// keys that trigger the action pattern are returned as vector of `Vec`. +pub fn action_key(keymap: &[(Key, Vec)], action: &[Action]) -> Vec { + keymap + .iter() + .filter_map(|(key, acvec)| { + if acvec.as_slice() == action { + Some(*key) + } else { + None + } + }) + .collect::>() +} + +/// Get multiple keys for multiple actions. +/// +/// An extension of [`action_key`] that iterates over all action tuples and collects the results. +pub fn action_key_group(keymap: &[(Key, Vec)], actions: &[&[Action]]) -> Vec { + let mut ret = vec![]; + for action in actions { + ret.extend(action_key(keymap, action)); + } + ret +} + +/// Style a vector of [`Key`]s with the given [`Palette`]. +/// +/// Creates a line segment of style ``, with correct theming applied: The brackets have the +/// regular text color, the enclosed keys are painted green and bold. If the keys share a common +/// modifier (See [`get_common_modifier`]), it is printed in front of the keys, painted green and +/// bold, separated with a `+`: `MOD + `. +/// +/// If multiple [`Key`]s are given, the individual keys are separated with a `|` char. This does +/// not apply to the following groups of keys which are treated specially and don't have a +/// separator between them: +/// +/// - "hjkl" +/// - "←↓↑→" +/// - "←→" +/// - "↓↑" +/// +/// The returned Vector of [`ANSIString`] is suitable for transformation into an [`ANSIStrings`] +/// type. +pub fn style_key_with_modifier(keyvec: &[Key], palette: &Palette) -> Vec> { + // Nothing to do, quit... + if keyvec.is_empty() { + return vec![]; + } + + let text_color = palette_match!(match palette.theme_hue { + ThemeHue::Dark => palette.white, + ThemeHue::Light => palette.black, + }); + let green_color = palette_match!(palette.green); + let orange_color = palette_match!(palette.orange); + let mut ret = vec![]; + + // Prints modifier key + let modifier_str = match get_common_modifier(keyvec.iter().collect()) { + Some(modifier) => modifier, + None => "".to_string(), + }; + let no_modifier = modifier_str.is_empty(); + let painted_modifier = if modifier_str.is_empty() { + Style::new().paint("") + } else { + Style::new().fg(orange_color).bold().paint(modifier_str) + }; + ret.push(painted_modifier); + + // Prints key group start + let group_start_str = if no_modifier { "<" } else { " + <" }; + ret.push(Style::new().fg(text_color).paint(group_start_str)); + + // Prints the keys + let key = keyvec + .iter() + .map(|key| { + if no_modifier { + format!("{}", key) + } else { + match key { + Key::Ctrl(c) => format!("{}", Key::Char(*c)), + Key::Alt(c) => format!("{}", c), + _ => format!("{}", key), + } + } + }) + .collect::>(); + + // Special handling of some pre-defined keygroups + let key_string = key.join(""); + let key_separator = match &key_string[..] { + "hjkl" => "", + "←↓↑→" => "", + "←→" => "", + "↓↑" => "", + _ => "|", + }; + + for (idx, key) in key.iter().enumerate() { + if idx > 0 && !key_separator.is_empty() { + ret.push(Style::new().fg(text_color).paint(key_separator)); + } + ret.push(Style::new().fg(green_color).bold().paint(key.clone())); + } + + let group_end_str = ">"; + ret.push(Style::new().fg(text_color).paint(group_end_str)); + + ret +} + +#[cfg(test)] +pub mod tests { + use super::*; + use ansi_term::unstyle; + use ansi_term::ANSIStrings; + use zellij_tile::prelude::CharOrArrow; + use zellij_tile::prelude::Direction; + + fn big_keymap() -> Vec<(Key, Vec)> { + vec![ + (Key::Char('a'), vec![Action::Quit]), + (Key::Ctrl('b'), vec![Action::ScrollUp]), + (Key::Ctrl('d'), vec![Action::ScrollDown]), + ( + Key::Alt(CharOrArrow::Char('c')), + vec![Action::ScrollDown, Action::SwitchToMode(InputMode::Normal)], + ), + ( + Key::Char('1'), + vec![TO_NORMAL, Action::SwitchToMode(InputMode::Locked)], + ), + ] + } + + #[test] + fn common_modifier_with_ctrl_keys() { + let keyvec = vec![Key::Ctrl('a'), Key::Ctrl('b'), Key::Ctrl('c')]; + let ret = get_common_modifier(keyvec.iter().collect()); + assert_eq!(ret, Some("Ctrl".to_string())); + } + + #[test] + fn common_modifier_with_alt_keys_chars() { + let keyvec = vec![ + Key::Alt(CharOrArrow::Char('1')), + Key::Alt(CharOrArrow::Char('t')), + Key::Alt(CharOrArrow::Char('z')), + ]; + let ret = get_common_modifier(keyvec.iter().collect()); + assert_eq!(ret, Some("Alt".to_string())); + } + + #[test] + fn common_modifier_with_alt_keys_arrows() { + let keyvec = vec![ + Key::Alt(CharOrArrow::Direction(Direction::Left)), + Key::Alt(CharOrArrow::Direction(Direction::Right)), + ]; + let ret = get_common_modifier(keyvec.iter().collect()); + assert_eq!(ret, Some("Alt".to_string())); + } + + #[test] + fn common_modifier_with_alt_keys_arrows_and_chars() { + let keyvec = vec![ + Key::Alt(CharOrArrow::Direction(Direction::Left)), + Key::Alt(CharOrArrow::Direction(Direction::Right)), + Key::Alt(CharOrArrow::Char('t')), + Key::Alt(CharOrArrow::Char('z')), + ]; + let ret = get_common_modifier(keyvec.iter().collect()); + assert_eq!(ret, Some("Alt".to_string())); + } + + #[test] + fn common_modifier_with_mixed_alt_ctrl_keys() { + let keyvec = vec![ + Key::Alt(CharOrArrow::Direction(Direction::Left)), + Key::Alt(CharOrArrow::Char('z')), + Key::Ctrl('a'), + Key::Ctrl('1'), + ]; + let ret = get_common_modifier(keyvec.iter().collect()); + assert_eq!(ret, None); + } + + #[test] + fn common_modifier_with_any_keys() { + let keyvec = vec![Key::Backspace, Key::Char('f'), Key::Down]; + let ret = get_common_modifier(keyvec.iter().collect()); + assert_eq!(ret, None); + } + + #[test] + fn common_modifier_with_ctrl_and_normal_keys() { + let keyvec = vec![Key::Ctrl('a'), Key::Char('f'), Key::Down]; + let ret = get_common_modifier(keyvec.iter().collect()); + assert_eq!(ret, None); + } + + #[test] + fn common_modifier_with_alt_and_normal_keys() { + let keyvec = vec![Key::Alt(CharOrArrow::Char('a')), Key::Char('f'), Key::Down]; + let ret = get_common_modifier(keyvec.iter().collect()); + assert_eq!(ret, None); + } + + #[test] + fn action_key_simple_pattern_match_exact() { + let keymap = &[(Key::Char('f'), vec![Action::Quit])]; + let ret = action_key(keymap, &[Action::Quit]); + assert_eq!(ret, vec![Key::Char('f')]); + } + + #[test] + fn action_key_simple_pattern_match_pattern_too_long() { + let keymap = &[(Key::Char('f'), vec![Action::Quit])]; + let ret = action_key(keymap, &[Action::Quit, Action::ScrollUp]); + assert_eq!(ret, Vec::new()); + } + + #[test] + fn action_key_simple_pattern_match_pattern_empty() { + let keymap = &[(Key::Char('f'), vec![Action::Quit])]; + let ret = action_key(keymap, &[]); + assert_eq!(ret, Vec::new()); + } + + #[test] + fn action_key_long_pattern_match_exact() { + let keymap = big_keymap(); + let ret = action_key(&keymap, &[Action::ScrollDown, TO_NORMAL]); + assert_eq!(ret, vec![Key::Alt(CharOrArrow::Char('c'))]); + } + + #[test] + fn action_key_long_pattern_match_too_short() { + let keymap = big_keymap(); + let ret = action_key(&keymap, &[TO_NORMAL]); + assert_eq!(ret, Vec::new()); + } + + #[test] + fn action_key_group_single_pattern() { + let keymap = big_keymap(); + let ret = action_key_group(&keymap, &[&[Action::Quit]]); + assert_eq!(ret, vec![Key::Char('a')]); + } + + #[test] + fn action_key_group_two_patterns() { + let keymap = big_keymap(); + let ret = action_key_group(&keymap, &[&[Action::ScrollDown], &[Action::ScrollUp]]); + // Mind the order! + assert_eq!(ret, vec![Key::Ctrl('d'), Key::Ctrl('b')]); + } + + fn get_palette() -> Palette { + Palette::default() + } + + #[test] + fn style_key_with_modifier_only_chars() { + let keyvec = vec![Key::Char('a'), Key::Char('b'), Key::Char('c')]; + let palette = get_palette(); + + let ret = style_key_with_modifier(&keyvec, &palette); + let ret = unstyle(&ANSIStrings(&ret)); + + assert_eq!(ret, "".to_string()) + } + + #[test] + fn style_key_with_modifier_special_group_hjkl() { + let keyvec = vec![ + Key::Char('h'), + Key::Char('j'), + Key::Char('k'), + Key::Char('l'), + ]; + let palette = get_palette(); + + let ret = style_key_with_modifier(&keyvec, &palette); + let ret = unstyle(&ANSIStrings(&ret)); + + assert_eq!(ret, "".to_string()) + } + + #[test] + fn style_key_with_modifier_special_group_hjkl_broken() { + // Sorted the wrong way + let keyvec = vec![ + Key::Char('h'), + Key::Char('k'), + Key::Char('j'), + Key::Char('l'), + ]; + let palette = get_palette(); + + let ret = style_key_with_modifier(&keyvec, &palette); + let ret = unstyle(&ANSIStrings(&ret)); + + assert_eq!(ret, "".to_string()) + } + + #[test] + fn style_key_with_modifier_special_group_all_arrows() { + let keyvec = vec![ + Key::Char('←'), + Key::Char('↓'), + Key::Char('↑'), + Key::Char('→'), + ]; + let palette = get_palette(); + + let ret = style_key_with_modifier(&keyvec, &palette); + let ret = unstyle(&ANSIStrings(&ret)); + + assert_eq!(ret, "<←↓↑→>".to_string()) + } + + #[test] + fn style_key_with_modifier_special_group_left_right_arrows() { + let keyvec = vec![Key::Char('←'), Key::Char('→')]; + let palette = get_palette(); + + let ret = style_key_with_modifier(&keyvec, &palette); + let ret = unstyle(&ANSIStrings(&ret)); + + assert_eq!(ret, "<←→>".to_string()) + } + + #[test] + fn style_key_with_modifier_special_group_down_up_arrows() { + let keyvec = vec![Key::Char('↓'), Key::Char('↑')]; + let palette = get_palette(); + + let ret = style_key_with_modifier(&keyvec, &palette); + let ret = unstyle(&ANSIStrings(&ret)); + + assert_eq!(ret, "<↓↑>".to_string()) + } + + #[test] + fn style_key_with_modifier_common_ctrl_modifier_chars() { + let keyvec = vec![ + Key::Ctrl('a'), + Key::Ctrl('b'), + Key::Ctrl('c'), + Key::Ctrl('d'), + ]; + let palette = get_palette(); + + let ret = style_key_with_modifier(&keyvec, &palette); + let ret = unstyle(&ANSIStrings(&ret)); + + assert_eq!(ret, "Ctrl + ".to_string()) + } + + #[test] + fn style_key_with_modifier_common_alt_modifier_chars() { + let keyvec = vec![ + Key::Alt(CharOrArrow::Char('a')), + Key::Alt(CharOrArrow::Char('b')), + Key::Alt(CharOrArrow::Char('c')), + Key::Alt(CharOrArrow::Char('d')), + ]; + let palette = get_palette(); + + let ret = style_key_with_modifier(&keyvec, &palette); + let ret = unstyle(&ANSIStrings(&ret)); + + assert_eq!(ret, "Alt + ".to_string()) + } + + #[test] + fn style_key_with_modifier_common_alt_modifier_with_special_group_all_arrows() { + let keyvec = vec![ + Key::Alt(CharOrArrow::Direction(Direction::Left)), + Key::Alt(CharOrArrow::Direction(Direction::Down)), + Key::Alt(CharOrArrow::Direction(Direction::Up)), + Key::Alt(CharOrArrow::Direction(Direction::Right)), + ]; + let palette = get_palette(); + + let ret = style_key_with_modifier(&keyvec, &palette); + let ret = unstyle(&ANSIStrings(&ret)); + + assert_eq!(ret, "Alt + <←↓↑→>".to_string()) + } + + #[test] + fn style_key_with_modifier_ctrl_alt_char_mixed() { + let keyvec = vec![ + Key::Alt(CharOrArrow::Char('a')), + Key::Ctrl('b'), + Key::Char('c'), + ]; + let palette = get_palette(); + + let ret = style_key_with_modifier(&keyvec, &palette); + let ret = unstyle(&ANSIStrings(&ret)); + + assert_eq!(ret, "".to_string()) + } + + #[test] + fn style_key_with_modifier_unprintables() { + let keyvec = vec![ + Key::Backspace, + Key::Char('\n'), + Key::Char(' '), + Key::Char('\t'), + Key::PageDown, + Key::Delete, + Key::Home, + Key::End, + Key::Insert, + Key::BackTab, + Key::Esc, + ]; + let palette = get_palette(); + + let ret = style_key_with_modifier(&keyvec, &palette); + let ret = unstyle(&ANSIStrings(&ret)); + + assert_eq!( + ret, + "".to_string() + ) + } + + #[test] + fn style_key_with_modifier_unprintables_with_common_ctrl_modifier() { + let keyvec = vec![Key::Ctrl('\n'), Key::Ctrl(' '), Key::Ctrl('\t')]; + let palette = get_palette(); + + let ret = style_key_with_modifier(&keyvec, &palette); + let ret = unstyle(&ANSIStrings(&ret)); + + assert_eq!(ret, "Ctrl + ".to_string()) + } + + #[test] + fn style_key_with_modifier_unprintables_with_common_alt_modifier() { + let keyvec = vec![ + Key::Alt(CharOrArrow::Char('\n')), + Key::Alt(CharOrArrow::Char(' ')), + Key::Alt(CharOrArrow::Char('\t')), + ]; + let palette = get_palette(); + + let ret = style_key_with_modifier(&keyvec, &palette); + let ret = unstyle(&ANSIStrings(&ret)); + + assert_eq!(ret, "Alt + ".to_string()) + } +} diff --git a/default-plugins/status-bar/src/second_line.rs b/default-plugins/status-bar/src/second_line.rs index 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, + action: &str, palette: Palette, ) -> LinePart { - let text_color = palette_match!(match palette.theme_hue { - ThemeHue::Dark => palette.white, - ThemeHue::Light => palette.black, - }); - let green_color = palette_match!(palette.green); - let separator = if is_first_shortcut { " " } else { " / " }; - let separator = Style::new().fg(text_color).paint(separator); - let shortcut_len = letter.chars().count() + 3; // 2 for <>'s around shortcut, 1 for the space - let shortcut_left_separator = Style::new().fg(text_color).paint("<"); - let shortcut = Style::new().fg(green_color).bold().paint(letter); - let shortcut_right_separator = Style::new().fg(text_color).paint("> "); - let description_len = description.chars().count(); - let description = Style::new().fg(text_color).bold().paint(description); - let len = shortcut_len + description_len + separator.chars().count(); - LinePart { - part: ANSIStrings(&[ - separator, - shortcut_left_separator, - shortcut, - shortcut_right_separator, - description, - ]) - .to_string(), - len, + if key.is_empty() { + return LinePart::default(); } -} -fn first_word_shortcut( - is_first_shortcut: bool, - letter: &str, - description: &str, - palette: Palette, -) -> LinePart { let text_color = palette_match!(match palette.theme_hue { ThemeHue::Dark => palette.white, ThemeHue::Light => palette.black, }); - let green_color = palette_match!(palette.green); + let separator = if is_first_shortcut { " " } else { " / " }; - let separator = Style::new().fg(text_color).paint(separator); - let shortcut_len = letter.chars().count() + 3; // 2 for <>'s around shortcut, 1 for the space - let shortcut_left_separator = Style::new().fg(text_color).paint("<"); - let shortcut = Style::new().fg(green_color).bold().paint(letter); - let shortcut_right_separator = Style::new().fg(text_color).paint("> "); - let description_first_word = description.split(' ').next().unwrap_or(""); - let description_first_word_length = description_first_word.chars().count(); - let description_first_word = Style::new() - .fg(text_color) - .bold() - .paint(description_first_word); - let len = shortcut_len + description_first_word_length + separator.chars().count(); + let mut bits: Vec = vec![Style::new().fg(text_color).paint(separator)]; + bits.extend(style_key_with_modifier(&key, &palette)); + bits.push( + Style::new() + .fg(text_color) + .bold() + .paint(format!(" {}", action)), + ); + let part = ANSIStrings(&bits); + LinePart { - part: ANSIStrings(&[ - separator, - shortcut_left_separator, - shortcut, - shortcut_right_separator, - description_first_word, - ]) - .to_string(), - len, + part: part.to_string(), + len: unstyled_len(&part), } } @@ -108,168 +59,241 @@ fn locked_interface_indication(palette: Palette) -> LinePart { } } -fn show_extra_hints( - palette: Palette, - text_with_style: Vec<(&str, StatusBarTextColor, StatusBarTextBoldness)>, -) -> LinePart { - use StatusBarTextBoldness::*; - use StatusBarTextColor::*; - // get the colors - let white_color = palette_match!(palette.white); - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); - // calculate length of tipp - let len = text_with_style - .iter() - .fold(0, |len_sum, (text, _, _)| len_sum + text.chars().count()); - // apply the styles defined above - let styled_text = text_with_style - .into_iter() - .map(|(text, color, is_bold)| { - let color = match color { - White => white_color, - Green => green_color, - Orange => orange_color, - }; - match is_bold { - Bold => Style::new().fg(color).bold().paint(text), - NotBold => Style::new().fg(color).paint(text), - } - }) - .collect::>(); - LinePart { - part: ANSIStrings(&styled_text[..]).to_string(), - len, +fn add_shortcut(help: &ModeInfo, linepart: &LinePart, text: &str, keys: Vec) -> LinePart { + let shortcut = if linepart.len == 0 { + full_length_shortcut(true, keys, text, help.style.colors) + } else { + full_length_shortcut(false, keys, text, help.style.colors) + }; + + let mut new_linepart = LinePart::default(); + new_linepart.len += linepart.len + shortcut.len; + new_linepart.part = format!("{}{}", linepart.part, shortcut); + new_linepart +} + +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 } -/// Creates hints for usage of Pane Mode -fn confirm_pane_selection(palette: Palette) -> LinePart { - use StatusBarTextBoldness::*; - use StatusBarTextColor::*; - let text_with_style = [ - (" / ", White, NotBold), - ("", Green, Bold), - (" Select pane", White, Bold), - ]; - show_extra_hints(palette, text_with_style.to_vec()) -} +/// Collect all relevant keybindings and hints to display. +/// +/// Creates a vector with tuples containing the following entries: +/// +/// - A String to display for this keybinding when there are no size restrictions, +/// - A shortened String (where sensible) to display if the whole second line becomes too long, +/// - A `Vec` of the keys that map to this keyhint +/// +/// This vector is created by iterating over the keybindings for the current [`InputMode`] and +/// storing all Keybindings that match pre-defined patterns of `Action`s. For example, the +/// `InputMode::Pane` input mode determines which keys to display for the "Move focus" hint by +/// searching the keybindings for anything that matches the `Action::MoveFocus(_)` action. Since by +/// default multiple keybindings map to some action patterns (e.g. `Action::MoveFocus(_)` is bound +/// to "hjkl", the arrow keys and "Alt + "), we deduplicate the vector of all keybindings +/// before processing it. +/// +/// Therefore we sort it by the [`Key`]s of the current keymap and deduplicate the resulting sorted +/// vector by the `Vec` action vectors bound to the keys. As such, when multiple keys map +/// to the same sequence of actions, the keys that appear first in the [`Key`] structure will be +/// displayed. +// Please don't let rustfmt play with the formatting. It will stretch out the function to about +// three times the length and all the keybinding vectors we generate become virtually unreadable +// for humans. +#[rustfmt::skip] +fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec)> { + use Action as A; + use InputMode as IM; + use actions::Direction as Dir; + use actions::ResizeDirection as RDir; + use actions::SearchDirection as SDir; + use actions::SearchOption as SOpt; -/// 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()) -} + let mut old_keymap = mi.get_mode_keybinds(); + let s = |string: &str| string.to_string(); -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,); + // Find a keybinding to get back to "Normal" input mode. In this case we prefer '\n' over other + // choices. Do it here before we dedupe the keymap below! + let to_normal_keys = action_key(&old_keymap, &[TO_NORMAL]); + let to_normal_key = if to_normal_keys.contains(&Key::Char('\n')) { + vec![Key::Char('\n')] + } else { + // Yield `vec![key]` if `to_normal_keys` has at least one key, or an empty vec otherwise. + to_normal_keys.into_iter().take(1).collect() + }; + + // Sort and deduplicate the keybindings first. We sort after the `Key`s, and deduplicate by + // their `Action` vectors. An unstable sort is fine here because if the user maps anything to + // the same key again, anything will happen... + old_keymap.sort_unstable_by(|(keya, _), (keyb, _)| keya.partial_cmp(keyb).unwrap()); + + let mut known_actions: Vec> = vec![]; + let mut km = vec![]; + for (key, acvec) in old_keymap { + if known_actions.contains(&acvec) { + // This action is known already + continue; + } else { + known_actions.push(acvec.to_vec()); + km.push((key, acvec)); } - let select_pane_shortcut = extra_hint_producing_function(help.style.colors); - line_part.len += select_pane_shortcut.len; - line_part.part = format!("{}{}", line_part.part, select_pane_shortcut,); - line_part } + + if mi.mode == IM::Pane { vec![ + (s("Move focus"), s("Move"), + action_key_group(&km, &[&[A::MoveFocus(Dir::Left)], &[A::MoveFocus(Dir::Down)], + &[A::MoveFocus(Dir::Up)], &[A::MoveFocus(Dir::Right)]])), + (s("New"), s("New"), action_key(&km, &[A::NewPane(None), TO_NORMAL])), + (s("Close"), s("Close"), action_key(&km, &[A::CloseFocus, TO_NORMAL])), + (s("Rename"), s("Rename"), + action_key(&km, &[A::SwitchToMode(IM::RenamePane), A::PaneNameInput(vec![0])])), + (s("Split down"), s("Down"), action_key(&km, &[A::NewPane(Some(Dir::Down)), TO_NORMAL])), + (s("Split right"), s("Right"), action_key(&km, &[A::NewPane(Some(Dir::Right)), TO_NORMAL])), + (s("Fullscreen"), s("Fullscreen"), action_key(&km, &[A::ToggleFocusFullscreen, TO_NORMAL])), + (s("Frames"), s("Frames"), action_key(&km, &[A::TogglePaneFrames, TO_NORMAL])), + (s("Floating toggle"), s("Floating"), + action_key(&km, &[A::ToggleFloatingPanes, TO_NORMAL])), + (s("Embed pane"), s("Embed"), action_key(&km, &[A::TogglePaneEmbedOrFloating, TO_NORMAL])), + (s("Next"), s("Next"), action_key(&km, &[A::SwitchFocus])), + (s("Select pane"), s("Select"), to_normal_key), + ]} else if mi.mode == IM::Tab { + // With the default bindings, "Move focus" for tabs is tricky: It binds all the arrow keys + // to moving tabs focus (left/up go left, right/down go right). Since we sort the keys + // above and then dedpulicate based on the actions, we will end up with LeftArrow for + // "left" and DownArrow for "right". What we really expect is to see LeftArrow and + // RightArrow. + // FIXME: So for lack of a better idea we just check this case manually here. + let old_keymap = mi.get_mode_keybinds(); + let focus_keys_full: Vec = action_key_group(&old_keymap, + &[&[A::GoToPreviousTab], &[A::GoToNextTab]]); + let focus_keys = if focus_keys_full.contains(&Key::Left) + && focus_keys_full.contains(&Key::Right) { + vec![Key::Left, Key::Right] + } else { + action_key_group(&km, &[&[A::GoToPreviousTab], &[A::GoToNextTab]]) + }; + + vec![ + (s("Move focus"), s("Move"), focus_keys), + (s("New"), s("New"), action_key(&km, &[A::NewTab(None), TO_NORMAL])), + (s("Close"), s("Close"), action_key(&km, &[A::CloseTab, TO_NORMAL])), + (s("Rename"), s("Rename"), + action_key(&km, &[A::SwitchToMode(IM::RenameTab), A::TabNameInput(vec![0])])), + (s("Sync"), s("Sync"), action_key(&km, &[A::ToggleActiveSyncTab, TO_NORMAL])), + (s("Toggle"), s("Toggle"), action_key(&km, &[A::ToggleTab])), + (s("Select pane"), s("Select"), to_normal_key), + ]} else if mi.mode == IM::Resize { vec![ + (s("Resize"), s("Resize"), action_key_group(&km, &[ + &[A::Resize(RDir::Left)], &[A::Resize(RDir::Down)], + &[A::Resize(RDir::Up)], &[A::Resize(RDir::Right)]])), + (s("Increase/Decrease size"), s("Increase/Decrease"), + action_key_group(&km, &[&[A::Resize(RDir::Increase)], &[A::Resize(RDir::Decrease)]])), + (s("Select pane"), s("Select"), to_normal_key), + ]} else if mi.mode == IM::Move { vec![ + (s("Move"), s("Move"), action_key_group(&km, &[ + &[Action::MovePane(Some(Dir::Left))], &[Action::MovePane(Some(Dir::Down))], + &[Action::MovePane(Some(Dir::Up))], &[Action::MovePane(Some(Dir::Right))]])), + (s("Next pane"), s("Next"), action_key(&km, &[Action::MovePane(None)])), + ]} else if mi.mode == IM::Scroll { vec![ + (s("Scroll"), s("Scroll"), + action_key_group(&km, &[&[Action::ScrollDown], &[Action::ScrollUp]])), + (s("Scroll page"), s("Scroll"), + action_key_group(&km, &[&[Action::PageScrollDown], &[Action::PageScrollUp]])), + (s("Scroll half page"), s("Scroll"), + action_key_group(&km, &[&[Action::HalfPageScrollDown], &[Action::HalfPageScrollUp]])), + (s("Edit scrollback in default editor"), s("Edit"), + action_key(&km, &[Action::EditScrollback, TO_NORMAL])), + (s("Enter search term"), s("Search"), + action_key(&km, &[A::SwitchToMode(IM::EnterSearch), A::SearchInput(vec![0])])), + (s("Select pane"), s("Select"), to_normal_key), + ]} else if mi.mode == IM::EnterSearch { vec![ + (s("When done"), s("Done"), action_key(&km, &[A::SwitchToMode(IM::Search)])), + (s("Cancel"), s("Cancel"), + action_key(&km, &[A::SearchInput(vec![27]), A::SwitchToMode(IM::Scroll)])), + ]} else if mi.mode == IM::Search { vec![ + (s("Scroll"), s("Scroll"), + action_key_group(&km, &[&[Action::ScrollDown], &[Action::ScrollUp]])), + (s("Scroll page"), s("Scroll"), + action_key_group(&km, &[&[Action::PageScrollDown], &[Action::PageScrollUp]])), + (s("Scroll half page"), s("Scroll"), + action_key_group(&km, &[&[Action::HalfPageScrollDown], &[Action::HalfPageScrollUp]])), + (s("Enter term"), s("Search"), + action_key(&km, &[A::SwitchToMode(IM::EnterSearch), A::SearchInput(vec![0])])), + (s("Search down"), s("Down"), action_key(&km, &[A::Search(SDir::Down)])), + (s("Search up"), s("Up"), action_key(&km, &[A::Search(SDir::Up)])), + (s("Case sensitive"), s("Case"), + action_key(&km, &[A::SearchToggleOption(SOpt::CaseSensitivity)])), + (s("Wrap"), s("Wrap"), + action_key(&km, &[A::SearchToggleOption(SOpt::Wrap)])), + (s("Whole words"), s("Whole"), + action_key(&km, &[A::SearchToggleOption(SOpt::WholeWord)])), + ]} else if mi.mode == IM::Session { vec![ + (s("Detach"), s("Detach"), action_key(&km, &[Action::Detach])), + (s("Select pane"), s("Select"), to_normal_key), + ]} else if mi.mode == IM::Tmux { vec![ + (s("Move focus"), s("Move"), action_key_group(&km, &[ + &[A::MoveFocus(Dir::Left)], &[A::MoveFocus(Dir::Down)], + &[A::MoveFocus(Dir::Up)], &[A::MoveFocus(Dir::Right)]])), + (s("Split down"), s("Down"), action_key(&km, &[A::NewPane(Some(Dir::Down)), TO_NORMAL])), + (s("Split right"), s("Right"), action_key(&km, &[A::NewPane(Some(Dir::Right)), TO_NORMAL])), + (s("Fullscreen"), s("Fullscreen"), action_key(&km, &[A::ToggleFocusFullscreen, TO_NORMAL])), + (s("New tab"), s("New"), action_key(&km, &[A::NewTab(None), TO_NORMAL])), + (s("Rename tab"), s("Rename"), + action_key(&km, &[A::SwitchToMode(IM::RenameTab), A::TabNameInput(vec![0])])), + (s("Previous Tab"), s("Previous"), action_key(&km, &[A::GoToPreviousTab, TO_NORMAL])), + (s("Next Tab"), s("Next"), action_key(&km, &[A::GoToNextTab, TO_NORMAL])), + (s("Select pane"), s("Select"), to_normal_key), + ]} else if matches!(mi.mode, IM::RenamePane | IM::RenameTab) { vec![ + (s("When done"), s("Done"), to_normal_key), + (s("Select pane"), s("Select"), action_key_group(&km, &[ + &[A::MoveFocus(Dir::Left)], &[A::MoveFocus(Dir::Down)], + &[A::MoveFocus(Dir::Up)], &[A::MoveFocus(Dir::Right)]])), + ]} else { vec![] } } fn full_shortcut_list(help: &ModeInfo, tip: TipFn) -> LinePart { match help.mode { - InputMode::Normal => tip(help.style.colors), + InputMode::Normal => tip(help), InputMode::Locked => locked_interface_indication(help.style.colors), - InputMode::Tmux => full_tmux_mode_indication(help), - InputMode::RenamePane => full_shortcut_list_nonstandard_mode(select_pane_shortcut)(help), - InputMode::EnterSearch => full_shortcut_list_nonstandard_mode(select_pane_shortcut)(help), - _ => full_shortcut_list_nonstandard_mode(confirm_pane_selection)(help), + _ => full_shortcut_list_nonstandard_mode(help), } } -fn shortened_shortcut_list_nonstandard_mode( - extra_hint_producing_function: fn(Palette) -> LinePart, -) -> impl FnOnce(&ModeInfo) -> LinePart { - move |help| { - let mut line_part = LinePart::default(); - for (i, (letter, description)) in help.keybinds.iter().enumerate() { - let shortcut = first_word_shortcut(i == 0, letter, description, help.style.colors); - line_part.len += shortcut.len; - line_part.part = format!("{}{}", line_part.part, shortcut,); - } - let select_pane_shortcut = extra_hint_producing_function(help.style.colors); - line_part.len += select_pane_shortcut.len; - line_part.part = format!("{}{}", line_part.part, select_pane_shortcut,); - line_part +fn shortened_shortcut_list_nonstandard_mode(help: &ModeInfo) -> LinePart { + let mut line_part = LinePart::default(); + let keys_and_hints = get_keys_and_hints(help); + + for (_, short, keys) in keys_and_hints.into_iter() { + line_part = add_shortcut(help, &line_part, &short, keys.to_vec()); } + line_part } fn shortened_shortcut_list(help: &ModeInfo, tip: TipFn) -> LinePart { match help.mode { - InputMode::Normal => tip(help.style.colors), + InputMode::Normal => tip(help), InputMode::Locked => locked_interface_indication(help.style.colors), - InputMode::Tmux => short_tmux_mode_indication(help), - InputMode::RenamePane => { - shortened_shortcut_list_nonstandard_mode(select_pane_shortcut)(help) - }, - InputMode::EnterSearch => { - shortened_shortcut_list_nonstandard_mode(select_pane_shortcut)(help) - }, - _ => shortened_shortcut_list_nonstandard_mode(confirm_pane_selection)(help), + _ => shortened_shortcut_list_nonstandard_mode(help), } } -fn best_effort_shortcut_list_nonstandard_mode( - extra_hint_producing_function: fn(Palette) -> LinePart, -) -> impl FnOnce(&ModeInfo, usize) -> LinePart { - move |help, max_len| { - let mut line_part = LinePart::default(); - for (i, (letter, description)) in help.keybinds.iter().enumerate() { - let shortcut = first_word_shortcut(i == 0, letter, description, help.style.colors); - if line_part.len + shortcut.len + MORE_MSG.chars().count() > max_len { - // TODO: better - line_part.part = format!("{}{}", line_part.part, MORE_MSG); - line_part.len += MORE_MSG.chars().count(); - break; - } - line_part.len += shortcut.len; - line_part.part = format!("{}{}", line_part.part, shortcut); - } - let select_pane_shortcut = extra_hint_producing_function(help.style.colors); - if line_part.len + select_pane_shortcut.len <= max_len { - line_part.len += select_pane_shortcut.len; - line_part.part = format!("{}{}", line_part.part, select_pane_shortcut,); - } - line_part - } -} +fn best_effort_shortcut_list_nonstandard_mode(help: &ModeInfo, max_len: usize) -> LinePart { + let mut line_part = LinePart::default(); + let keys_and_hints = get_keys_and_hints(help); -fn best_effort_tmux_shortcut_list(help: &ModeInfo, max_len: usize) -> LinePart { - let mut line_part = tmux_mode_indication(help); - for (i, (letter, description)) in help.keybinds.iter().enumerate() { - let shortcut = first_word_shortcut(i == 0, letter, description, help.style.colors); - if line_part.len + shortcut.len + MORE_MSG.chars().count() > max_len { - // TODO: better + for (_, short, keys) in keys_and_hints.into_iter() { + let new_line_part = add_shortcut(help, &line_part, &short, keys.to_vec()); + if new_line_part.len + MORE_MSG.chars().count() > max_len { line_part.part = format!("{}{}", line_part.part, MORE_MSG); line_part.len += MORE_MSG.chars().count(); break; } - line_part.len += shortcut.len; - line_part.part = format!("{}{}", line_part.part, shortcut); + line_part = new_line_part; } line_part } @@ -277,7 +301,7 @@ fn best_effort_tmux_shortcut_list(help: &ModeInfo, max_len: usize) -> LinePart { fn best_effort_shortcut_list(help: &ModeInfo, tip: TipFn, max_len: usize) -> LinePart { match help.mode { InputMode::Normal => { - let line_part = tip(help.style.colors); + let line_part = tip(help); if line_part.len <= max_len { line_part } else { @@ -292,11 +316,7 @@ fn best_effort_shortcut_list(help: &ModeInfo, tip: TipFn, max_len: usize) -> Lin LinePart::default() } }, - InputMode::Tmux => best_effort_tmux_shortcut_list(help, max_len), - InputMode::RenamePane => { - best_effort_shortcut_list_nonstandard_mode(select_pane_shortcut)(help, max_len) - }, - _ => best_effort_shortcut_list_nonstandard_mode(confirm_pane_selection)(help, max_len), + _ => best_effort_shortcut_list_nonstandard_mode(help, max_len), } } @@ -374,7 +394,9 @@ pub fn fullscreen_panes_to_hide(palette: &Palette, panes_to_hide: usize) -> Line } } -pub fn floating_panes_are_visible(palette: &Palette) -> LinePart { +pub fn floating_panes_are_visible(mode_info: &ModeInfo) -> LinePart { + let palette = mode_info.style.colors; + let km = &mode_info.get_mode_keybinds(); let white_color = match palette.white { PaletteColor::Rgb((r, g, b)) => RGB(r, g, b), PaletteColor::EightBit(color) => Fixed(color), @@ -391,16 +413,29 @@ pub fn floating_panes_are_visible(palette: &Palette) -> LinePart { let shortcut_right_separator = Style::new().fg(white_color).bold().paint("): "); let floating_panes = "FLOATING PANES VISIBLE"; let press = "Press "; - let ctrl = "Ctrl-p "; - let plus = "+ "; + let pane_mode = format!( + "{}", + action_key(km, &[Action::SwitchToMode(InputMode::Pane)]) + .first() + .unwrap_or(&Key::Char('?')) + ); + let plus = ", "; let p_left_separator = "<"; - let p = "w"; + let p = format!( + "{}", + action_key( + &mode_info.get_keybinds_for_mode(InputMode::Pane), + &[Action::ToggleFloatingPanes, TO_NORMAL] + ) + .first() + .unwrap_or(&Key::Char('?')) + ); let p_right_separator = "> "; let to_hide = "to hide."; let len = floating_panes.chars().count() + press.chars().count() - + ctrl.chars().count() + + pane_mode.chars().count() + plus.chars().count() + p_left_separator.chars().count() + p.chars().count() @@ -414,7 +449,7 @@ pub fn floating_panes_are_visible(palette: &Palette) -> LinePart { Style::new().fg(orange_color).bold().paint(floating_panes), shortcut_right_separator, Style::new().fg(white_color).bold().paint(press), - Style::new().fg(green_color).bold().paint(ctrl), + Style::new().fg(green_color).bold().paint(pane_mode), Style::new().fg(white_color).bold().paint(plus), Style::new().fg(white_color).bold().paint(p_left_separator), Style::new().fg(green_color).bold().paint(p), @@ -425,90 +460,6 @@ pub fn floating_panes_are_visible(palette: &Palette) -> LinePart { } } -pub fn tmux_mode_indication(help: &ModeInfo) -> LinePart { - let white_color = match help.style.colors.white { - PaletteColor::Rgb((r, g, b)) => RGB(r, g, b), - PaletteColor::EightBit(color) => Fixed(color), - }; - let orange_color = match help.style.colors.orange { - PaletteColor::Rgb((r, g, b)) => RGB(r, g, b), - PaletteColor::EightBit(color) => Fixed(color), - }; - - let shortcut_left_separator = Style::new().fg(white_color).bold().paint(" ("); - let shortcut_right_separator = Style::new().fg(white_color).bold().paint("): "); - let tmux_mode_text = "TMUX MODE"; - let tmux_mode_indicator = Style::new().fg(orange_color).bold().paint(tmux_mode_text); - let line_part = LinePart { - part: format!( - "{}{}{}", - shortcut_left_separator, tmux_mode_indicator, shortcut_right_separator - ), - len: tmux_mode_text.chars().count() + 5, // 2 for the separators, 3 for the colon and following space - }; - line_part -} - -pub fn full_tmux_mode_indication(help: &ModeInfo) -> LinePart { - let white_color = match help.style.colors.white { - PaletteColor::Rgb((r, g, b)) => RGB(r, g, b), - PaletteColor::EightBit(color) => Fixed(color), - }; - let orange_color = match help.style.colors.orange { - PaletteColor::Rgb((r, g, b)) => RGB(r, g, b), - PaletteColor::EightBit(color) => Fixed(color), - }; - - let shortcut_left_separator = Style::new().fg(white_color).bold().paint(" ("); - let shortcut_right_separator = Style::new().fg(white_color).bold().paint("): "); - let tmux_mode_text = "TMUX MODE"; - let tmux_mode_indicator = Style::new().fg(orange_color).bold().paint(tmux_mode_text); - let mut line_part = LinePart { - part: format!( - "{}{}{}", - shortcut_left_separator, tmux_mode_indicator, shortcut_right_separator - ), - len: tmux_mode_text.chars().count() + 5, // 2 for the separators, 3 for the colon and following space - }; - - for (i, (letter, description)) in help.keybinds.iter().enumerate() { - let shortcut = full_length_shortcut(i == 0, letter, description, help.style.colors); - line_part.len += shortcut.len; - line_part.part = format!("{}{}", line_part.part, shortcut,); - } - line_part -} - -pub fn short_tmux_mode_indication(help: &ModeInfo) -> LinePart { - let white_color = match help.style.colors.white { - PaletteColor::Rgb((r, g, b)) => RGB(r, g, b), - PaletteColor::EightBit(color) => Fixed(color), - }; - let orange_color = match help.style.colors.orange { - PaletteColor::Rgb((r, g, b)) => RGB(r, g, b), - PaletteColor::EightBit(color) => Fixed(color), - }; - - let shortcut_left_separator = Style::new().fg(white_color).bold().paint(" ("); - let shortcut_right_separator = Style::new().fg(white_color).bold().paint("): "); - let tmux_mode_text = "TMUX MODE"; - let tmux_mode_indicator = Style::new().fg(orange_color).bold().paint(tmux_mode_text); - let mut line_part = LinePart { - part: format!( - "{}{}{}", - shortcut_left_separator, tmux_mode_indicator, shortcut_right_separator - ), - len: tmux_mode_text.chars().count() + 5, // 2 for the separators, 3 for the colon and following space - }; - - for (i, (letter, description)) in help.keybinds.iter().enumerate() { - let shortcut = first_word_shortcut(i == 0, letter, description, help.style.colors); - line_part.len += shortcut.len; - line_part.part = format!("{}{}", line_part.part, shortcut); - } - line_part -} - pub fn locked_fullscreen_panes_to_hide(palette: &Palette, panes_to_hide: usize) -> LinePart { let text_color = palette_match!(match palette.theme_hue { ThemeHue::Dark => palette.white, @@ -570,3 +521,244 @@ pub fn locked_floating_panes_are_visible(palette: &Palette) -> LinePart { len, } } + +#[cfg(test)] +/// Unit tests. +/// +/// Note that we cheat a little here, because the number of things one may want to test is endless, +/// and creating a Mockup of [`ModeInfo`] by hand for all these testcases is nothing less than +/// torture. Hence, we test the most atomic unit thoroughly ([`full_length_shortcut`] and then test +/// the public API ([`keybinds`]) to ensure correct operation. +mod tests { + use super::*; + + // Strip style information from `LinePart` and return a raw String instead + fn unstyle(line_part: LinePart) -> String { + let string = line_part.to_string(); + + let re = regex::Regex::new(r"\x1b\[[0-9;]*m").unwrap(); + let string = re.replace_all(&string, "".to_string()); + + string.to_string() + } + + fn get_palette() -> Palette { + Palette::default() + } + + #[test] + fn full_length_shortcut_with_key() { + let keyvec = vec![Key::Char('a')]; + let palette = get_palette(); + + let ret = full_length_shortcut(false, keyvec, "Foobar", palette); + let ret = unstyle(ret); + + assert_eq!(ret, " / Foobar"); + } + + #[test] + fn full_length_shortcut_with_key_first_element() { + let keyvec = vec![Key::Char('a')]; + let palette = get_palette(); + + let ret = full_length_shortcut(true, keyvec, "Foobar", palette); + let ret = unstyle(ret); + + assert_eq!(ret, " Foobar"); + } + + #[test] + // When there is no binding, we print no shortcut either + fn full_length_shortcut_without_key() { + let keyvec = vec![]; + let palette = get_palette(); + + let ret = full_length_shortcut(false, keyvec, "Foobar", palette); + let ret = unstyle(ret); + + assert_eq!(ret, ""); + } + + #[test] + fn full_length_shortcut_with_key_unprintable_1() { + let keyvec = vec![Key::Char('\n')]; + let palette = get_palette(); + + let ret = full_length_shortcut(false, keyvec, "Foobar", palette); + let ret = unstyle(ret); + + assert_eq!(ret, " / Foobar"); + } + + #[test] + fn full_length_shortcut_with_key_unprintable_2() { + let keyvec = vec![Key::Backspace]; + let palette = get_palette(); + + let ret = full_length_shortcut(false, keyvec, "Foobar", palette); + let ret = unstyle(ret); + + assert_eq!(ret, " / Foobar"); + } + + #[test] + fn full_length_shortcut_with_ctrl_key() { + let keyvec = vec![Key::Ctrl('a')]; + let palette = get_palette(); + + let ret = full_length_shortcut(false, keyvec, "Foobar", palette); + let ret = unstyle(ret); + + assert_eq!(ret, " / Ctrl + Foobar"); + } + + #[test] + fn full_length_shortcut_with_alt_key() { + let keyvec = vec![Key::Alt(CharOrArrow::Char('a'))]; + let palette = get_palette(); + + let ret = full_length_shortcut(false, keyvec, "Foobar", palette); + let ret = unstyle(ret); + + assert_eq!(ret, " / Alt + Foobar"); + } + + #[test] + fn full_length_shortcut_with_homogenous_key_group() { + let keyvec = vec![Key::Char('a'), Key::Char('b'), Key::Char('c')]; + let palette = get_palette(); + + let ret = full_length_shortcut(false, keyvec, "Foobar", palette); + let ret = unstyle(ret); + + assert_eq!(ret, " / Foobar"); + } + + #[test] + fn full_length_shortcut_with_heterogenous_key_group() { + let keyvec = vec![Key::Char('a'), Key::Ctrl('b'), Key::Char('\n')]; + let palette = get_palette(); + + let ret = full_length_shortcut(false, keyvec, "Foobar", palette); + let ret = unstyle(ret); + + assert_eq!(ret, " / Foobar"); + } + + #[test] + fn full_length_shortcut_with_key_group_shared_ctrl_modifier() { + let keyvec = vec![Key::Ctrl('a'), Key::Ctrl('b'), Key::Ctrl('c')]; + let palette = get_palette(); + + let ret = full_length_shortcut(false, keyvec, "Foobar", palette); + let ret = unstyle(ret); + + assert_eq!(ret, " / Ctrl + Foobar"); + } + //pub fn keybinds(help: &ModeInfo, tip_name: &str, max_width: usize) -> LinePart { + + #[test] + // Note how it leaves out elements that don't exist! + fn keybinds_wide() { + let mode_info = ModeInfo { + mode: InputMode::Pane, + keybinds: vec![( + InputMode::Pane, + vec![ + (Key::Left, vec![Action::MoveFocus(actions::Direction::Left)]), + (Key::Down, vec![Action::MoveFocus(actions::Direction::Down)]), + (Key::Up, vec![Action::MoveFocus(actions::Direction::Up)]), + ( + Key::Right, + vec![Action::MoveFocus(actions::Direction::Right)], + ), + (Key::Char('n'), vec![Action::NewPane(None), TO_NORMAL]), + (Key::Char('x'), vec![Action::CloseFocus, TO_NORMAL]), + ( + Key::Char('f'), + vec![Action::ToggleFocusFullscreen, TO_NORMAL], + ), + ], + )], + ..ModeInfo::default() + }; + + let ret = keybinds(&mode_info, "quicknav", 500); + let ret = unstyle(ret); + + assert_eq!( + ret, + " <←↓↑→> Move focus / New / Close / Fullscreen" + ); + } + + #[test] + // Note how "Move focus" becomes "Move" + fn keybinds_tight_width() { + let mode_info = ModeInfo { + mode: InputMode::Pane, + keybinds: vec![( + InputMode::Pane, + vec![ + (Key::Left, vec![Action::MoveFocus(actions::Direction::Left)]), + (Key::Down, vec![Action::MoveFocus(actions::Direction::Down)]), + (Key::Up, vec![Action::MoveFocus(actions::Direction::Up)]), + ( + Key::Right, + vec![Action::MoveFocus(actions::Direction::Right)], + ), + (Key::Char('n'), vec![Action::NewPane(None), TO_NORMAL]), + (Key::Char('x'), vec![Action::CloseFocus, TO_NORMAL]), + ( + Key::Char('f'), + vec![Action::ToggleFocusFullscreen, TO_NORMAL], + ), + ], + )], + ..ModeInfo::default() + }; + + let ret = keybinds(&mode_info, "quicknav", 35); + let ret = unstyle(ret); + + assert_eq!(ret, " <←↓↑→> Move / New ... "); + } + + #[test] + fn keybinds_wide_weird_keys() { + let mode_info = ModeInfo { + mode: InputMode::Pane, + keybinds: vec![( + InputMode::Pane, + vec![ + ( + Key::Ctrl('a'), + vec![Action::MoveFocus(actions::Direction::Left)], + ), + ( + Key::Ctrl('\n'), + vec![Action::MoveFocus(actions::Direction::Down)], + ), + ( + Key::Ctrl('1'), + vec![Action::MoveFocus(actions::Direction::Up)], + ), + ( + Key::Ctrl(' '), + vec![Action::MoveFocus(actions::Direction::Right)], + ), + (Key::Backspace, vec![Action::NewPane(None), TO_NORMAL]), + (Key::Esc, vec![Action::CloseFocus, TO_NORMAL]), + (Key::End, vec![Action::ToggleFocusFullscreen, TO_NORMAL]), + ], + )], + ..ModeInfo::default() + }; + + let ret = keybinds(&mode_info, "quicknav", 500); + let ret = unstyle(ret); + + assert_eq!(ret, " Ctrl + Move focus / New / Close / Fullscreen"); + } +} diff --git a/default-plugins/status-bar/src/tip/data/compact_layout.rs b/default-plugins/status-bar/src/tip/data/compact_layout.rs index 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 +

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

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

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

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

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

"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - ]) + ]; + bits.extend(add_keybinds(help)); + strings!(&bits) +} + +fn add_keybinds(help: &ModeInfo) -> Vec { + let to_pane = action_key( + &help.get_mode_keybinds(), + &[Action::SwitchToMode(InputMode::Pane)], + ); + let pane_frames = action_key( + &help.get_keybinds_for_mode(InputMode::Pane), + &[ + Action::TogglePaneFrames, + Action::SwitchToMode(InputMode::Normal), + ], + ); + + if pane_frames.is_empty() { + return vec![Style::new().bold().paint("UNBOUND")]; + } + + let mut bits = vec![]; + bits.extend(style_key_with_modifier(&to_pane, &help.style.colors)); + bits.push(Style::new().paint(", ")); + bits.extend(style_key_with_modifier(&pane_frames, &help.style.colors)); + bits } diff --git a/default-plugins/status-bar/src/tip/data/edit_scrollbuffer.rs b/default-plugins/status-bar/src/tip/data/edit_scrollbuffer.rs index 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 + + - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); + let green_color = palette_match!(help.style.colors.green); - strings!(&[ + let mut bits = vec![ Style::new().paint(" Tip: "), Style::new().paint("Search through the scrollbuffer using your default "), Style::new().fg(green_color).bold().paint("$EDITOR"), Style::new().paint(" with "), - Style::new().fg(orange_color).bold().paint("Ctrl"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - ]) + ]; + bits.extend(add_keybinds(help)); + strings!(&bits) } -pub fn edit_scrollbuffer_medium(palette: Palette) -> LinePart { +pub fn edit_scrollbuffer_medium(help: &ModeInfo) -> LinePart { // Tip: Search the scrollbuffer using your $EDITOR with // Ctrl + + - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); + let green_color = palette_match!(help.style.colors.green); - strings!(&[ + let mut bits = vec![ Style::new().paint(" Tip: "), Style::new().paint("Search the scrollbuffer using your "), Style::new().fg(green_color).bold().paint("$EDITOR"), Style::new().paint(" with "), - Style::new().fg(orange_color).bold().paint("Ctrl"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - ]) + ]; + bits.extend(add_keybinds(help)); + strings!(&bits) } -pub fn edit_scrollbuffer_short(palette: Palette) -> LinePart { +pub fn edit_scrollbuffer_short(help: &ModeInfo) -> LinePart { // Search using $EDITOR with // Ctrl + + - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); + let green_color = palette_match!(help.style.colors.green); - strings!(&[ + let mut bits = vec![ Style::new().paint(" Search using "), Style::new().fg(green_color).bold().paint("$EDITOR"), Style::new().paint(" with "), - Style::new().fg(orange_color).bold().paint("Ctrl"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - ]) + ]; + bits.extend(add_keybinds(help)); + strings!(&bits) +} + +fn add_keybinds(help: &ModeInfo) -> Vec { + let to_pane = action_key( + &help.get_mode_keybinds(), + &[Action::SwitchToMode(InputMode::Scroll)], + ); + let edit_buffer = action_key( + &help.get_keybinds_for_mode(InputMode::Scroll), + &[ + Action::EditScrollback, + Action::SwitchToMode(InputMode::Normal), + ], + ); + + if edit_buffer.is_empty() { + return vec![Style::new().bold().paint("UNBOUND")]; + } + + let mut bits = vec![]; + bits.extend(style_key_with_modifier(&to_pane, &help.style.colors)); + bits.push(Style::new().paint(", ")); + bits.extend(style_key_with_modifier(&edit_buffer, &help.style.colors)); + bits } diff --git a/default-plugins/status-bar/src/tip/data/floating_panes_mouse.rs b/default-plugins/status-bar/src/tip/data/floating_panes_mouse.rs index 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 +

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

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

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

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

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

"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - Style::new().paint(" => floating panes"), - ]) + let mut bits = add_keybinds(help); + bits.push(Style::new().paint(" => floating panes")); + strings!(&bits) +} + +fn add_keybinds(help: &ModeInfo) -> Vec { + let to_pane = action_key( + &help.get_mode_keybinds(), + &[Action::SwitchToMode(InputMode::Pane)], + ); + let floating_toggle = action_key( + &help.get_keybinds_for_mode(InputMode::Pane), + &[ + Action::ToggleFloatingPanes, + Action::SwitchToMode(InputMode::Normal), + ], + ); + + if floating_toggle.is_empty() { + return vec![Style::new().bold().paint("UNBOUND")]; + } + + 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); - - 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 mut bits = add_keybinds(help); + bits.push(Style::new().paint(" off screen edge focuses next tab.")); + strings!(&bits) +} + +fn add_keybinds(help: &ModeInfo) -> Vec { + let pane_keymap = help.get_keybinds_for_mode(InputMode::Pane); + let move_focus_keys = action_key_group( + &pane_keymap, + &[ + &[Action::MoveFocusOrTab(Direction::Left)], + &[Action::MoveFocusOrTab(Direction::Right)], + ], + ); + + // 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); - strings!(&[ - Style::new().paint(" Tip: "), - Style::new().fg(orange_color).bold().paint("Alt"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - Style::new().paint(" => open new pane. "), - Style::new().fg(orange_color).bold().paint("Alt"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("<←↓↑→"), - Style::new().paint(" or "), - Style::new().fg(green_color).bold().paint("hjkl>"), - Style::new().paint(" => navigate between panes. "), - Style::new().fg(orange_color).bold().paint("Alt"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint("<+->"), - Style::new().paint(" => increase/decrease pane size."), - ]) + let mut bits = vec![Style::new().paint(" Tip: ")]; + bits.extend(groups.new_pane); + bits.push(Style::new().paint(" => 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(palette: Palette) -> LinePart { - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); +pub fn quicknav_medium(help: &ModeInfo) -> LinePart { + let groups = add_keybinds(help); - strings!(&[ - Style::new().paint(" Tip: "), - Style::new().fg(orange_color).bold().paint("Alt"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - Style::new().paint(" => 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(" 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_short(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(" 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("+->"), - ]) + 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) +} + +struct Keygroups<'a> { + new_pane: Vec>, + move_focus: Vec>, + resize: Vec>, +} + +fn add_keybinds(help: &ModeInfo) -> Keygroups { + let normal_keymap = help.get_mode_keybinds(); + let new_pane_keys = action_key(&normal_keymap, &[Action::NewPane(None)]); + let new_pane = if new_pane_keys.is_empty() { + vec![Style::new().bold().paint("UNBOUND")] + } else { + style_key_with_modifier(&new_pane_keys, &help.style.colors) + }; + + let resize_keys = action_key_group( + &normal_keymap, + &[ + &[Action::Resize(ResizeDirection::Increase)], + &[Action::Resize(ResizeDirection::Decrease)], + ], + ); + let resize = if resize_keys.is_empty() { + vec![Style::new().bold().paint("UNBOUND")] + } else { + style_key_with_modifier(&resize_keys, &help.style.colors) + }; + + let move_focus_keys = action_key_group( + &normal_keymap, + &[ + &[Action::MoveFocus(Direction::Left)], + &[Action::MoveFocusOrTab(Direction::Left)], + &[Action::MoveFocus(Direction::Down)], + &[Action::MoveFocus(Direction::Up)], + &[Action::MoveFocus(Direction::Right)], + &[Action::MoveFocusOrTab(Direction::Right)], + ], + ); + // Let's see if we have some pretty groups in common here + let mut arrows = vec![]; + let mut letters = vec![]; + for key in move_focus_keys.into_iter() { + let key_str = key.to_string(); + if key_str.contains('←') + || key_str.contains('↓') + || key_str.contains('↑') + || key_str.contains('→') + { + arrows.push(key); + } else { + letters.push(key); + } + } + let arrows = style_key_with_modifier(&arrows, &help.style.colors); + let letters = style_key_with_modifier(&letters, &help.style.colors); + let move_focus = if arrows.is_empty() && letters.is_empty() { + vec![Style::new().bold().paint("UNBOUND")] + } else if arrows.is_empty() || letters.is_empty() { + arrows.into_iter().chain(letters.into_iter()).collect() + } else { + arrows + .into_iter() + .chain(vec![Style::new().paint(" or ")].into_iter()) + .chain(letters.into_iter()) + .collect() + }; + + 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 + bypasses Zellij and sends the mouse click directly to the terminal - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); + let green_color = palette_match!(help.style.colors.green); + let orange_color = palette_match!(help.style.colors.orange); strings!(&[ Style::new().paint(" Tip: "), - Style::new().fg(orange_color).bold().paint("SHIFT"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - Style::new().paint(" bypasses Zellij and sends the mouse click directly to the terminal."), + Style::new().fg(orange_color).bold().paint("Shift"), + Style::new().paint(" + <"), + Style::new().fg(green_color).bold().paint("mouse-click"), + Style::new().paint("> bypasses Zellij and sends the mouse click directly to the terminal."), ]) } -pub fn mouse_click_to_terminal_medium(palette: Palette) -> LinePart { +pub fn mouse_click_to_terminal_medium(help: &ModeInfo) -> LinePart { // Tip: SHIFT + sends the click directly to the terminal - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); + let green_color = palette_match!(help.style.colors.green); + let orange_color = palette_match!(help.style.colors.orange); strings!(&[ Style::new().paint(" Tip: "), - Style::new().fg(orange_color).bold().paint("SHIFT"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - Style::new().paint(" sends the click directly to the terminal."), + Style::new().fg(orange_color).bold().paint("Shift"), + Style::new().paint(" + <"), + Style::new().fg(green_color).bold().paint("mouse-click"), + Style::new().paint("> sends the click directly to the terminal."), ]) } -pub fn mouse_click_to_terminal_short(palette: Palette) -> LinePart { +pub fn mouse_click_to_terminal_short(help: &ModeInfo) -> LinePart { // Tip: SHIFT + => sends click to terminal. - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); + let green_color = palette_match!(help.style.colors.green); + let orange_color = palette_match!(help.style.colors.orange); strings!(&[ Style::new().paint(" Tip: "), - Style::new().fg(orange_color).bold().paint("SHIFT"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - Style::new().paint(" => sends click to terminal."), + Style::new().fg(orange_color).bold().paint("Shift"), + Style::new().paint(" + <"), + Style::new().fg(green_color).bold().paint("mouse-click"), + Style::new().paint("> => sends click to terminal."), ]) } diff --git a/default-plugins/status-bar/src/tip/data/sync_tab.rs b/default-plugins/status-bar/src/tip/data/sync_tab.rs index 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 + + - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); - - strings!(&[ + let mut bits = vec![ Style::new().paint(" Tip: "), Style::new().paint("Sync a tab and write keyboard input to all its panes with "), - Style::new().fg(orange_color).bold().paint("Ctrl"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - ]) + ]; + bits.extend(add_keybinds(help)); + strings!(&bits) } -pub fn sync_tab_medium(palette: Palette) -> LinePart { +pub fn sync_tab_medium(help: &ModeInfo) -> LinePart { // Tip: Sync input to panes in a tab with Ctrl + + - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); - - strings!(&[ + let mut bits = vec![ Style::new().paint(" Tip: "), Style::new().paint("Sync input to panes in a tab with "), - Style::new().fg(orange_color).bold().paint("Ctrl"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - ]) + ]; + bits.extend(add_keybinds(help)); + strings!(&bits) } -pub fn sync_tab_short(palette: Palette) -> LinePart { +pub fn sync_tab_short(help: &ModeInfo) -> LinePart { // Sync input in a tab with Ctrl + + - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); - - strings!(&[ - Style::new().paint(" Sync input in a tab with "), - Style::new().fg(orange_color).bold().paint("Ctrl"), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - Style::new().paint(" + "), - Style::new().fg(green_color).bold().paint(""), - ]) + let mut bits = vec![Style::new().paint(" Sync input in a tab with ")]; + bits.extend(add_keybinds(help)); + strings!(&bits) +} + +fn add_keybinds(help: &ModeInfo) -> Vec { + let to_tab = action_key( + &help.get_mode_keybinds(), + &[Action::SwitchToMode(InputMode::Tab)], + ); + let sync_tabs = action_key( + &help.get_keybinds_for_mode(InputMode::Tab), + &[ + Action::ToggleActiveSyncTab, + Action::SwitchToMode(InputMode::Normal), + ], + ); + + if sync_tabs.is_empty() { + return vec![Style::new().bold().paint("UNBOUND")]; + } + + 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, diff --git a/src/tests/e2e/cases.rs b/src/tests/e2e/cases.rs index 47e7edd4..7457ed54 100644 --- a/src/tests/e2e/cases.rs +++ b/src/tests/e2e/cases.rs @@ -983,6 +983,41 @@ pub fn accepts_basic_layout() { assert_snapshot!(last_snapshot); } +#[test] +#[ignore] +pub fn status_bar_loads_custom_keybindings() { + let fake_win_size = Size { + cols: 120, + rows: 24, + }; + let config_file_name = "changed_keys.yaml"; + let mut test_attempts = 10; + let last_snapshot = loop { + RemoteRunner::kill_running_sessions(fake_win_size); + let mut runner = RemoteRunner::new_with_config(fake_win_size, config_file_name); + runner.run_all_steps(); + let last_snapshot = runner.take_snapshot_after(Step { + name: "Wait for app to load", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(3, 1) + && remote_terminal.snapshot_contains("$ █ ││$") + && remote_terminal.snapshot_contains("$ ") { + step_is_complete = true; + } + step_is_complete + }, + }); + if runner.test_timed_out && test_attempts > 0 { + test_attempts -= 1; + continue; + } else { + break last_snapshot; + } + }; + assert_snapshot!(last_snapshot); +} + #[test] #[ignore] fn focus_pane_with_mouse() { diff --git a/src/tests/e2e/remote_runner.rs b/src/tests/e2e/remote_runner.rs index 21c91e51..03fb619e 100644 --- a/src/tests/e2e/remote_runner.rs +++ b/src/tests/e2e/remote_runner.rs @@ -20,6 +20,7 @@ use std::rc::Rc; const ZELLIJ_EXECUTABLE_LOCATION: &str = "/usr/src/zellij/x86_64-unknown-linux-musl/release/zellij"; const SET_ENV_VARIABLES: &str = "EDITOR=/usr/bin/vi"; const ZELLIJ_LAYOUT_PATH: &str = "/usr/src/zellij/fixtures/layouts"; +const ZELLIJ_CONFIG_PATH: &str = "/usr/src/zellij/fixtures/configs"; const ZELLIJ_DATA_DIR: &str = "/usr/src/zellij/e2e-data"; const ZELLIJ_FIXTURE_PATH: &str = "/usr/src/zellij/fixtures"; const CONNECTION_STRING: &str = "127.0.0.1:2222"; @@ -163,6 +164,25 @@ fn start_zellij_with_layout(channel: &mut ssh2::Channel, layout_path: &str) { std::thread::sleep(std::time::Duration::from_secs(1)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN } +fn start_zellij_with_config(channel: &mut ssh2::Channel, config_path: &str) { + stop_zellij(channel); + channel + .write_all( + format!( + "{} {} --config {} --session {} --data-dir {}\n", + SET_ENV_VARIABLES, + ZELLIJ_EXECUTABLE_LOCATION, + config_path, + SESSION_NAME, + ZELLIJ_DATA_DIR + ) + .as_bytes(), + ) + .unwrap(); + channel.flush().unwrap(); + std::thread::sleep(std::time::Duration::from_secs(1)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN +} + fn read_from_channel( channel: &Arc>, last_snapshot: &Arc>, @@ -587,6 +607,42 @@ impl RemoteRunner { reader_thread, } } + pub fn new_with_config(win_size: Size, config_file_name: &'static str) -> Self { + let remote_path = Path::new(ZELLIJ_CONFIG_PATH).join(config_file_name); + let sess = ssh_connect(); + let mut channel = sess.channel_session().unwrap(); + let mut rows = Dimension::fixed(win_size.rows); + let mut cols = Dimension::fixed(win_size.cols); + rows.set_inner(win_size.rows); + cols.set_inner(win_size.cols); + let pane_geom = PaneGeom { + x: 0, + y: 0, + rows, + cols, + }; + setup_remote_environment(&mut channel, win_size); + start_zellij_with_config(&mut channel, &remote_path.to_string_lossy()); + let channel = Arc::new(Mutex::new(channel)); + let last_snapshot = Arc::new(Mutex::new(String::new())); + let cursor_coordinates = Arc::new(Mutex::new((0, 0))); + sess.set_blocking(false); + let reader_thread = + read_from_channel(&channel, &last_snapshot, &cursor_coordinates, &pane_geom); + RemoteRunner { + steps: vec![], + channel, + currently_running_step: None, + current_step_index: 0, + retries_left: RETRIES, + retry_pause_ms: 100, + test_timed_out: false, + panic_on_no_retries_left: true, + last_snapshot, + cursor_coordinates, + reader_thread, + } + } pub fn dont_panic(mut self) -> Self { self.panic_on_no_retries_left = false; self diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__bracketed_paste.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__bracketed_paste.snap index 842eaed8..8b66d5ac 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__bracketed_paste.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__bracketed_paste.snap @@ -26,4 +26,4 @@ expression: last_snapshot │ │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ Ctrl + LOCK 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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