From f1ff272b0b65f6d328fef24531ada67ea585ce85 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Fri, 17 Feb 2023 12:05:50 +0100 Subject: feat(ui): swap layouts and stacked panes (#2167) * relayout working with hard coded layout * work * refactor(layout): PaneLayout => TiledPaneLayout * tests passing * tests passing * tests passing * stacked panes and passing tests * tests for stacked panes * refactor(panes): stacked panes * fix: focusing into stacked panes from the left/right * fix(layouts): handle stacked layouts in the middle of the screen * fix(pane-stack): focus correctly when coming to stack from above/below * fix(stacked-panes): resize stack * fix(stacked-panes): focus with mouse * fix(stacked-panes): focus next pane * fix(layout-applier): sane focus order * fix(stacked-panes): better titles for one-liners * fix(stacked-panes): handle moving pane location in stack * fix(relayout): properly calculate display area * fix(relayout): properly calculate rounding errors * fix(stacked-panes): properly handle closing a pane near a stack * fix(swap-layouts): adjust swap layout sort order * feat(swap-layouts): ui + ux * fix(swap-layouts): include base layout * refactor(layout): remove unused method * fix(swap-layouts): respect pane contents and focus * work * fix(swap-layouts): load swap layouts from external file * fix(swap-layouts): properly truncate layout children * fix(stacked-panes): allow stacked panes to become fullscreen * fix(swap-layouts): work with multiple tabs * fix(swap-layouts): embed/eject panes properly with auto-layout * fix(stacked-panes): close last pane in stack * fix(stacked-panes): move focus for all clients in stack * fix(floating-panes): set layout damaged when moving panes * fix(relayout): move out of unfitting layout when resizing whole tab * fix(ui): background color for swap layout indicator * fix(keybinds): add switch next layout in tmux * fix(ui): swap layout indication in compact layout * fix(compact): correct swap constraint * fix(tests): tmux swap config shortcut * fix(resizes): cache resizes so as not to confuse panes (eg. vim) with multiple resizes that it debounces weirdly * feat(cli): dump swap layouts * fix(ui): stacked panes without pane frames * fix(ux): move pane forward/backwards also with floating panes * refactor(lint): remove unused stuff * refactor(tab): move swap layouts to separate file * style(fmt): rustfmt * style(fmt): rustfmt * refactor(panes): various cleanups * chore(deps): upgrade termwiz to get alt left-bracket * fix(assets): merge conflicts of binary files * style(fmt): rustfmt * style(clippy): no thank you! * chore(repo): remove garbage file --- zellij-utils/assets/compact-bar.wasm | Bin 0 -> 397489 bytes zellij-utils/assets/config/default.kdl | 11 + zellij-utils/assets/layouts/compact.swap.kdl | 91 ++ zellij-utils/assets/layouts/default.swap.kdl | 94 ++ zellij-utils/assets/layouts/strider.kdl | 23 +- zellij-utils/assets/layouts/strider.swap.kdl | 102 ++ zellij-utils/assets/plugins/compact-bar.wasm | Bin 397489 -> 427886 bytes zellij-utils/assets/plugins/status-bar.wasm | Bin 485616 -> 520407 bytes zellij-utils/assets/plugins/strider.wasm | Bin 413509 -> 441654 bytes zellij-utils/assets/plugins/tab-bar.wasm | Bin 373679 -> 400807 bytes zellij-utils/assets/status-bar.wasm | Bin 0 -> 485616 bytes zellij-utils/assets/strider.wasm | Bin 0 -> 413509 bytes zellij-utils/assets/tab-bar.wasm | Bin 0 -> 373679 bytes zellij-utils/src/cli.rs | 42 +- zellij-utils/src/data.rs | 2 + zellij-utils/src/errors.rs | 3 + zellij-utils/src/input/actions.rs | 52 +- zellij-utils/src/input/layout.rs | 375 ++++- zellij-utils/src/input/options.rs | 10 + zellij-utils/src/input/unit/layout_test.rs | 669 ++++---- ...ayout_test__args_added_to_args_in_template.snap | 116 +- ...ayout_test__args_override_args_in_template.snap | 122 +- ...an_load_swap_layouts_from_a_different_file.snap | 536 +++++++ ...ildren_not_as_first_child_of_pane_template.snap | 281 ++-- ...hildren_not_as_first_child_of_tab_template.snap | 63 +- ...on_exit_added_to_close_on_exit_in_template.snap | 110 +- ...n_exit_overrides_close_on_exit_in_template.snap | 110 +- ...d_tab_and_pane_template_both_with_children.snap | 78 +- ..._layout_test__cwd_added_to_cwd_in_template.snap | 114 +- ..._layout_test__cwd_override_cwd_in_template.snap | 118 +- ...wd_prepended_to_panes_with_and_without_cwd.snap | 39 +- ...nes_with_and_without_cwd_in_pane_templates.snap | 48 +- ...anes_with_and_without_cwd_in_tab_templates.snap | 45 +- ...est__global_cwd_given_to_panes_without_cwd.snap | 102 +- ..._global_cwd_passed_from_layout_constructor.snap | 102 +- ...ructor_overrides_global_cwd_in_layout_file.snap | 102 +- ...st__global_cwd_prepended_to_panes_with_cwd.snap | 102 +- ...wd_with_tab_cwd_given_to_panes_without_cwd.snap | 39 +- ...ayout_with_command_panes_and_close_on_exit.snap | 71 +- ...out_with_command_panes_and_start_suspended.snap | 71 +- ...out_test__layout_with_default_tab_template.snap | 143 +- ...layout_with_nested_branched_pane_templates.snap | 248 +-- ...ut_test__layout_with_nested_pane_templates.snap | 183 ++- ...t__layout_test__layout_with_pane_templates.snap | 487 +++--- ...t_test__layout_with_tab_and_pane_templates.snap | 48 +- ..._test__layout_with_tabs_and_floating_panes.snap | 48 +- ...cwd_is_overriden_by_its_consumers_bare_cwd.snap | 75 +- ...cwd_overriden_by_its_consumers_command_cwd.snap | 75 +- ...n_its_consumer_command_does_not_have_a_cwd.snap | 75 +- ...hout_cwd_is_overriden_by_its_consumers_cwd.snap | 75 +- ...ithout_cwd_receives_its_consumers_bare_cwd.snap | 75 +- ...re_cwd_overriden_by_its_consumers_bare_cwd.snap | 59 +- ...ropagated_to_its_consumer_command_with_cwd.snap | 75 +- ...agated_to_its_consumer_command_without_cwd.snap | 75 +- ..._with_bare_propagated_to_its_consumer_edit.snap | 61 +- ...th_command_propagated_to_its_consumer_edit.snap | 61 +- ...t_test__tab_cwd_given_to_panes_without_cwd.snap | 39 +- ..._test__tab_cwd_prepended_to_panes_with_cwd.snap | 39 +- zellij-utils/src/kdl/kdl_layout_parser.rs | 577 +++++-- zellij-utils/src/kdl/mod.rs | 134 +- zellij-utils/src/pane_size.rs | 44 +- zellij-utils/src/setup.rs | 39 + ...est__cli_arguments_override_config_options.snap | 3 +- ...t__cli_arguments_override_layout_options-2.snap | 28 +- ...est__cli_arguments_override_layout_options.snap | 3 +- ...st__default_config_with_no_cli_arguments-2.snap | 1630 +++++++++++++++++++- ...st__default_config_with_no_cli_arguments-3.snap | 3 +- ...test__default_config_with_no_cli_arguments.snap | 198 +++ ...__layout_env_vars_override_config_env_vars.snap | 198 +++ ...__layout_keybinds_override_config_keybinds.snap | 3 +- ...__layout_options_override_config_options-2.snap | 28 +- ...st__layout_options_override_config_options.snap | 3 +- ...st__layout_plugins_override_config_plugins.snap | 198 +++ ...test__layout_themes_override_config_themes.snap | 198 +++ ...ayout_ui_config_overrides_config_ui_config.snap | 198 +++ 75 files changed, 6995 insertions(+), 2274 deletions(-) create mode 100644 zellij-utils/assets/compact-bar.wasm create mode 100644 zellij-utils/assets/layouts/compact.swap.kdl create mode 100644 zellij-utils/assets/layouts/default.swap.kdl create mode 100644 zellij-utils/assets/layouts/strider.swap.kdl create mode 100644 zellij-utils/assets/status-bar.wasm create mode 100644 zellij-utils/assets/strider.wasm create mode 100644 zellij-utils/assets/tab-bar.wasm create mode 100644 zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__can_load_swap_layouts_from_a_different_file.snap (limited to 'zellij-utils') diff --git a/zellij-utils/assets/compact-bar.wasm b/zellij-utils/assets/compact-bar.wasm new file mode 100644 index 000000000..46267f5e7 Binary files /dev/null and b/zellij-utils/assets/compact-bar.wasm differ diff --git a/zellij-utils/assets/config/default.kdl b/zellij-utils/assets/config/default.kdl index 248b4026c..1e8ddd7c3 100644 --- a/zellij-utils/assets/config/default.kdl +++ b/zellij-utils/assets/config/default.kdl @@ -40,6 +40,7 @@ keybinds { move { bind "Ctrl h" { SwitchToMode "Normal"; } bind "n" "Tab" { MovePane; } + bind "p" { MovePaneBackwards; } bind "h" "Left" { MovePane "Left"; } bind "j" "Down" { MovePane "Down"; } bind "k" "Up" { MovePane "Up"; } @@ -130,6 +131,7 @@ keybinds { bind "k" { MoveFocus "Up"; SwitchToMode "Normal"; } bind "o" { FocusNextPane; } bind "d" { Detach; } + bind "Space" { NextSwapLayout; } bind "x" { CloseFocus; SwitchToMode "Normal"; } } shared_except "locked" { @@ -142,6 +144,8 @@ keybinds { bind "Alt k" "Alt Up" { MoveFocus "Up"; } bind "Alt =" "Alt +" { Resize "Increase"; } bind "Alt -" { Resize "Decrease"; } + bind "Alt [" { PreviousSwapLayout; } + bind "Alt ]" { NextSwapLayout; } } shared_except "normal" "locked" { bind "Enter" "Esc" { SwitchToMode "Normal"; } @@ -203,6 +207,13 @@ plugins { // // pane_frames true +// Toggle between having Zellij lay out panes according to a predefined set of layouts whenever possible +// Options: +// - true (default) +// - false +// +// auto_layout true + // Define color themes for Zellij // For more examples, see: https://github.com/zellij-org/zellij/tree/main/example/themes // Once these themes are defined, one of them should to be selected in the "theme" section of this file diff --git a/zellij-utils/assets/layouts/compact.swap.kdl b/zellij-utils/assets/layouts/compact.swap.kdl new file mode 100644 index 000000000..e38fa2101 --- /dev/null +++ b/zellij-utils/assets/layouts/compact.swap.kdl @@ -0,0 +1,91 @@ +tab_template name="ui" { + children + pane size=1 borderless=true { + plugin location="zellij:compact-bar" + } +} + +swap_tiled_layout name="vertical" { + ui max_panes=4 { + pane split_direction="vertical" { + pane + pane { children; } + } + } + ui max_panes=7 { + pane split_direction="vertical" { + pane { children; } + pane { pane; pane; pane; pane; } + } + } + ui max_panes=11 { + pane split_direction="vertical" { + pane { children; } + pane { pane; pane; pane; pane; } + pane { pane; pane; pane; pane; } + } + } +} + +swap_tiled_layout name="horizontal" { + ui max_panes=3 { + pane + pane + } + ui max_panes=7 { + pane { + pane split_direction="vertical" { children; } + pane split_direction="vertical" { pane; pane; pane; pane; } + } + } + ui max_panes=11 { + pane { + pane split_direction="vertical" { children; } + pane split_direction="vertical" { pane; pane; pane; pane; } + pane split_direction="vertical" { pane; pane; pane; pane; } + } + } +} + +swap_tiled_layout name="stacked" { + ui min_panes=4 { + pane split_direction="vertical" { + pane + pane { children stacked=true; } + } + } +} + +swap_floating_layout name="staggered" { + floating_panes +} + +swap_floating_layout name="enlarged" { + floating_panes max_panes=10 { + pane { x 1; y 1; width "90%"; height "90%"; } + pane { x 2; y 2; width "90%"; height "90%"; } + pane { x 3; y 3; width "90%"; height "90%"; } + pane { x 4; y 4; width "90%"; height "90%"; } + pane { x 5; y 5; width "90%"; height "90%"; } + pane { x 6; y 6; width "90%"; height "90%"; } + pane { x 7; y 7; width "90%"; height "90%"; } + pane { x 8; y 8; width "90%"; height "90%"; } + pane { x 9; y 9; width "90%"; height "90%"; } + pane focus=true { x 10; y 10; width "90%"; height "90%"; } + } +} + +swap_floating_layout name="spread" { + floating_panes max_panes=1 { + pane {y "50%"; x "50%"; } + } + floating_panes max_panes=2 { + pane { x "1%"; y "25%"; width "45%"; } + pane { x "50%"; y "25%"; width "45%"; } + } + floating_panes max_panes=3 { + pane focus=true { y "55%"; width "45%"; height "45%"; } + pane { x "1%"; y "1%"; width "45%"; } + pane { x "50%"; y "1%"; width "45%"; } + } +} diff --git a/zellij-utils/assets/layouts/default.swap.kdl b/zellij-utils/assets/layouts/default.swap.kdl new file mode 100644 index 000000000..51e8afed0 --- /dev/null +++ b/zellij-utils/assets/layouts/default.swap.kdl @@ -0,0 +1,94 @@ +tab_template name="ui" { + pane size=1 borderless=true { + plugin location="zellij:tab-bar" + } + children + pane size=2 borderless=true { + plugin location="zellij:status-bar" + } +} + +swap_tiled_layout name="vertical" { + ui max_panes=5 { + pane split_direction="vertical" { + pane + pane { children; } + } + } + ui max_panes=8 { + pane split_direction="vertical" { + pane { children; } + pane { pane; pane; pane; pane; } + } + } + ui max_panes=12 { + pane split_direction="vertical" { + pane { children; } + pane { pane; pane; pane; pane; } + pane { pane; pane; pane; pane; } + } + } +} + +swap_tiled_layout name="horizontal" { + ui max_panes=5 { + pane + pane + } + ui max_panes=8 { + pane { + pane split_direction="vertical" { children; } + pane split_direction="vertical" { pane; pane; pane; pane; } + } + } + ui max_panes=12 { + pane { + pane split_direction="vertical" { children; } + pane split_direction="vertical" { pane; pane; pane; pane; } + pane split_direction="vertical" { pane; pane; pane; pane; } + } + } +} + +swap_tiled_layout name="stacked" { + ui min_panes=5 { + pane split_direction="vertical" { + pane + pane { children stacked=true; } + } + } +} + +swap_floating_layout name="staggered" { + floating_panes +} + +swap_floating_layout name="enlarged" { + floating_panes max_panes=10 { + pane { x 1; y 1; width "90%"; height "90%"; } + pane { x 2; y 2; width "90%"; height "90%"; } + pane { x 3; y 3; width "90%"; height "90%"; } + pane { x 4; y 4; width "90%"; height "90%"; } + pane { x 5; y 5; width "90%"; height "90%"; } + pane { x 6; y 6; width "90%"; height "90%"; } + pane { x 7; y 7; width "90%"; height "90%"; } + pane { x 8; y 8; width "90%"; height "90%"; } + pane { x 9; y 9; width "90%"; height "90%"; } + pane focus=true { x 10; y 10; width "90%"; height "90%"; } + } +} + +swap_floating_layout name="spread" { + floating_panes max_panes=1 { + pane {y "50%"; x "50%"; } + } + floating_panes max_panes=2 { + pane { x "1%"; y "25%"; width "45%"; } + pane { x "50%"; y "25%"; width "45%"; } + } + floating_panes max_panes=3 { + pane focus=true { y "55%"; width "45%"; height "45%"; } + pane { x "1%"; y "1%"; width "45%"; } + pane { x "50%"; y "1%"; width "45%"; } + } +} diff --git a/zellij-utils/assets/layouts/strider.kdl b/zellij-utils/assets/layouts/strider.kdl index dbe8d0774..57ec01a7d 100644 --- a/zellij-utils/assets/layouts/strider.kdl +++ b/zellij-utils/assets/layouts/strider.kdl @@ -1,19 +1,14 @@ layout { - default_tab_template { - pane size=1 borderless=true { - plugin location="zellij:tab-bar" - } - children - pane size=2 borderless=true { - plugin location="zellij:status-bar" - } + pane size=1 borderless=true { + plugin location="zellij:tab-bar" } - tab { - pane split_direction="Vertical" { - pane size="20%" { - plugin location="zellij:strider" - } - pane + pane split_direction="Vertical" { + pane size="20%" { + plugin location="zellij:strider" } + pane + } + pane size=2 borderless=true { + plugin location="zellij:status-bar" } } diff --git a/zellij-utils/assets/layouts/strider.swap.kdl b/zellij-utils/assets/layouts/strider.swap.kdl new file mode 100644 index 000000000..c6df15755 --- /dev/null +++ b/zellij-utils/assets/layouts/strider.swap.kdl @@ -0,0 +1,102 @@ +tab_template name="ui" { + pane size=1 borderless=true { + plugin location="zellij:tab-bar" + } + pane split_direction="Vertical" { + pane size="20%" { + plugin location="zellij:strider" + } + pane { + children + } + + } + pane size=2 borderless=true { + plugin location="zellij:status-bar" + } +} + +swap_tiled_layout name="vertical" { + ui max_panes=6 { + pane split_direction="vertical" { + pane + pane { children; } + } + } + ui max_panes=9 { + pane split_direction="vertical" { + pane { children; } + pane { pane; pane; pane; pane; } + } + } + ui max_panes=13 { + pane split_direction="vertical" { + pane { children; } + pane { pane; pane; pane; pane; } + pane { pane; pane; pane; pane; } + } + } +} + +swap_tiled_layout name="horizontal" { + ui max_panes=6 { + pane + pane + } + ui max_panes=9 { + pane { + pane split_direction="vertical" { children; } + pane split_direction="vertical" { pane; pane; pane; pane; } + } + } + ui max_panes=13 { + pane { + pane split_direction="vertical" { children; } + pane split_direction="vertical" { pane; pane; pane; pane; } + pane split_direction="vertical" { pane; pane; pane; pane; } + } + } +} + +swap_tiled_layout name="stacked" { + ui min_panes=6 { + pane split_direction="vertical" { + pane focus=true + pane { children stacked=true; } + } + } +} + +swap_floating_layout name="staggered" { + floating_panes +} + +swap_floating_layout name="enlarged" { + floating_panes max_panes=10 { + pane { x 1; y 1; width "90%"; height "90%"; } + pane { x 2; y 2; width "90%"; height "90%"; } + pane { x 3; y 3; width "90%"; height "90%"; } + pane { x 4; y 4; width "90%"; height "90%"; } + pane { x 5; y 5; width "90%"; height "90%"; } + pane { x 6; y 6; width "90%"; height "90%"; } + pane { x 7; y 7; width "90%"; height "90%"; } + pane { x 8; y 8; width "90%"; height "90%"; } + pane { x 9; y 9; width "90%"; height "90%"; } + pane focus=true { x 10; y 10; width "90%"; height "90%"; } + } +} + +swap_floating_layout name="spread" { + floating_panes max_panes=1 { + pane {y "50%"; x "50%"; } + } + floating_panes max_panes=2 { + pane { x "1%"; y "25%"; width "45%"; } + pane { x "50%"; y "25%"; width "45%"; } + } + floating_panes max_panes=3 { + pane focus=true { y "55%"; width "45%"; height "45%"; } + pane { x "1%"; y "1%"; width "45%"; } + pane { x "50%"; y "1%"; width "45%"; } + } +} diff --git a/zellij-utils/assets/plugins/compact-bar.wasm b/zellij-utils/assets/plugins/compact-bar.wasm index 46267f5e7..3a18eab82 100644 Binary files a/zellij-utils/assets/plugins/compact-bar.wasm and b/zellij-utils/assets/plugins/compact-bar.wasm differ diff --git a/zellij-utils/assets/plugins/status-bar.wasm b/zellij-utils/assets/plugins/status-bar.wasm index 5b4f6eb06..3ea5b48a2 100644 Binary files a/zellij-utils/assets/plugins/status-bar.wasm and b/zellij-utils/assets/plugins/status-bar.wasm differ diff --git a/zellij-utils/assets/plugins/strider.wasm b/zellij-utils/assets/plugins/strider.wasm index 0af68b2f2..589bab10e 100644 Binary files a/zellij-utils/assets/plugins/strider.wasm and b/zellij-utils/assets/plugins/strider.wasm differ diff --git a/zellij-utils/assets/plugins/tab-bar.wasm b/zellij-utils/assets/plugins/tab-bar.wasm index 1166de83c..1ebeacd81 100644 Binary files a/zellij-utils/assets/plugins/tab-bar.wasm and b/zellij-utils/assets/plugins/tab-bar.wasm differ diff --git a/zellij-utils/assets/status-bar.wasm b/zellij-utils/assets/status-bar.wasm new file mode 100644 index 000000000..5b4f6eb06 Binary files /dev/null and b/zellij-utils/assets/status-bar.wasm differ diff --git a/zellij-utils/assets/strider.wasm b/zellij-utils/assets/strider.wasm new file mode 100644 index 000000000..0af68b2f2 Binary files /dev/null and b/zellij-utils/assets/strider.wasm differ diff --git a/zellij-utils/assets/tab-bar.wasm b/zellij-utils/assets/tab-bar.wasm new file mode 100644 index 000000000..1166de83c Binary files /dev/null and b/zellij-utils/assets/tab-bar.wasm differ diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs index 1ef43e27f..ea0b8d728 100644 --- a/zellij-utils/src/cli.rs +++ b/zellij-utils/src/cli.rs @@ -180,9 +180,13 @@ pub enum Sessions { #[derive(Debug, Subcommand, Clone, Serialize, Deserialize)] pub enum CliAction { /// Write bytes to the terminal. - Write { bytes: Vec }, + Write { + bytes: Vec, + }, /// Write characters to the terminal. - WriteChars { chars: String }, + WriteChars { + chars: String, + }, /// [increase|decrease] the focused panes area at the [left|down|up|right] border. Resize { resize: Resize, @@ -193,13 +197,21 @@ pub enum CliAction { /// Change focus to the previous pane FocusPreviousPane, /// Move the focused pane in the specified direction. [right|left|up|down] - MoveFocus { direction: Direction }, + MoveFocus { + direction: Direction, + }, /// Move focus to the pane or tab (if on screen edge) in the specified direction /// [right|left|up|down] - MoveFocusOrTab { direction: Direction }, - /// Change the location of the focused pane in the specified direction + MoveFocusOrTab { + direction: Direction, + }, + /// Change the location of the focused pane in the specified direction or rotate forwrads /// [right|left|up|down] - MovePane { direction: Direction }, + MovePane { + direction: Option, + }, + /// Rotate the location of the previous pane backwards + MovePaneBackwards, /// Dump the focused pane to a file DumpScreen { path: PathBuf, @@ -296,7 +308,9 @@ pub enum CliAction { cwd: Option, }, /// Switch input mode of all connected clients [locked|pane|tab|resize|move|search|session] - SwitchMode { input_mode: InputMode }, + SwitchMode { + input_mode: InputMode, + }, /// Embed focused pane if floating or float focused pane if embedded TogglePaneEmbedOrFloating, /// Toggle the visibility of all fdirectionloating panes in the current Tab, open one if none exist @@ -304,7 +318,9 @@ pub enum CliAction { /// Close the focused pane. ClosePane, /// Renames the focused pane - RenamePane { name: String }, + RenamePane { + name: String, + }, /// Remove a previously set pane name UndoRenamePane, /// Go to the next tab. @@ -314,7 +330,9 @@ pub enum CliAction { /// Close the current tab. CloseTab, /// Go to tab with index [index] - GoToTab { index: u32 }, + GoToTab { + index: u32, + }, /// Go to tab with name [name] GoToTabName { name: String, @@ -323,7 +341,9 @@ pub enum CliAction { create: bool, }, /// Renames the focused pane - RenameTab { name: String }, + RenameTab { + name: String, + }, /// Remove a previously set tab name UndoRenameTab, /// Create a new tab, optionally with a specified tab layout and name @@ -340,4 +360,6 @@ pub enum CliAction { #[clap(short, long, value_parser, requires("layout"))] cwd: Option, }, + PreviousSwapLayout, + NextSwapLayout, } diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs index a0f255daa..f5c4828e3 100644 --- a/zellij-utils/src/data.rs +++ b/zellij-utils/src/data.rs @@ -686,6 +686,8 @@ pub struct TabInfo { pub is_sync_panes_active: bool, pub are_floating_panes_visible: bool, pub other_focused_clients: Vec, + pub active_swap_layout_name: Option, + pub is_swap_layout_dirty: bool, } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index a7986de8d..3493f84b7 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -249,6 +249,7 @@ pub enum ScreenContext { MoveFocusRight, MoveFocusRightOrNextTab, MovePane, + MovePaneBackwards, MovePaneDown, MovePaneUp, MovePaneRight, @@ -320,6 +321,8 @@ pub enum ScreenContext { SearchToggleWrap, AddRedPaneFrameColorOverride, ClearPaneFrameColorOverride, + PreviousSwapLayout, + NextSwapLayout, } /// Stack call representations corresponding to the different types of [`PtyInstruction`]s. diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index 7db0e4f3f..a4f4b74f6 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -1,7 +1,9 @@ //! Definition of the actions that can be bound to keys. use super::command::RunCommandAction; -use super::layout::{FloatingPanesLayout, Layout, PaneLayout}; +use super::layout::{ + FloatingPaneLayout, Layout, SwapFloatingLayout, SwapTiledLayout, TiledPaneLayout, +}; use crate::cli::CliAction; use crate::data::InputMode; use crate::data::{Direction, Resize}; @@ -115,6 +117,7 @@ pub enum Action { /// If there is no pane in the direction, move to previous/next Tab. MoveFocusOrTab(Direction), MovePane(Option), + MovePaneBackwards, /// Dumps the screen to a file DumpScreen(String, bool), /// Scroll up in focus pane. @@ -164,7 +167,13 @@ pub enum Action { PaneNameInput(Vec), UndoRenamePane, /// Create a new tab, optionally with a specified tab layout. - NewTab(Option, Vec, Option), // the String is the tab name + NewTab( + Option, + Vec, + Option>, + Option>, + Option, + ), // the String is the tab name /// Do nothing. NoOp, /// Go to the next tab. @@ -205,13 +214,15 @@ pub enum Action { /// Toggle case sensitivity of search SearchToggleOption(SearchOption), ToggleMouseMode, + PreviousSwapLayout, + NextSwapLayout, } impl Action { /// Checks that two Action are match except their mutable attributes. pub fn shallow_eq(&self, other_action: &Action) -> bool { match (self, other_action) { - (Action::NewTab(_, _, _), Action::NewTab(_, _, _)) => true, + (Action::NewTab(..), Action::NewTab(..)) => true, _ => self == other_action, } } @@ -228,7 +239,8 @@ impl Action { CliAction::FocusPreviousPane => Ok(vec![Action::FocusPreviousPane]), CliAction::MoveFocus { direction } => Ok(vec![Action::MoveFocus(direction)]), CliAction::MoveFocusOrTab { direction } => Ok(vec![Action::MoveFocusOrTab(direction)]), - CliAction::MovePane { direction } => Ok(vec![Action::MovePane(Some(direction))]), + CliAction::MovePane { direction } => Ok(vec![Action::MovePane(direction)]), + CliAction::MovePaneBackwards => Ok(vec![Action::MovePaneBackwards]), CliAction::DumpScreen { path, full } => Ok(vec![Action::DumpScreen( path.as_os_str().to_string_lossy().into(), full, @@ -342,10 +354,10 @@ impl Action { .map(|cwd| current_dir.join(cwd)) .or_else(|| Some(current_dir)); if let Some(layout_path) = layout { - let (path_to_raw_layout, raw_layout) = + let (path_to_raw_layout, raw_layout, swap_layouts) = Layout::stringified_from_path_or_default(Some(&layout_path), None) .map_err(|e| format!("Failed to load layout: {}", e))?; - let layout = Layout::from_str(&raw_layout, path_to_raw_layout, cwd).map_err(|e| { + let layout = Layout::from_str(&raw_layout, path_to_raw_layout, swap_layouts.as_ref().map(|(f, p)| (f.as_str(), p.as_str())), cwd).map_err(|e| { let stringified_error = match e { ConfigError::KdlError(kdl_error) => { let error = kdl_error.add_src(layout_path.as_path().as_os_str().to_string_lossy().to_string(), String::from(raw_layout)); @@ -381,26 +393,52 @@ impl Action { if tabs.len() > 1 { return Err(format!("Tab layout cannot itself have tabs")); } else if !tabs.is_empty() { + let swap_tiled_layouts = if layout.swap_tiled_layouts.is_empty() { + None + } else { + Some(layout.swap_tiled_layouts.clone()) + }; + let swap_floating_layouts = if layout.swap_floating_layouts.is_empty() { + None + } else { + Some(layout.swap_floating_layouts.clone()) + }; let (tab_name, layout, floating_panes_layout) = tabs.drain(..).next().unwrap(); let name = tab_name.or(name); Ok(vec![Action::NewTab( Some(layout), floating_panes_layout, + swap_tiled_layouts, + swap_floating_layouts, name, )]) } else { + let swap_tiled_layouts = if layout.swap_tiled_layouts.is_empty() { + None + } else { + Some(layout.swap_tiled_layouts.clone()) + }; + let swap_floating_layouts = if layout.swap_floating_layouts.is_empty() { + None + } else { + Some(layout.swap_floating_layouts.clone()) + }; let (layout, floating_panes_layout) = layout.new_tab(); Ok(vec![Action::NewTab( Some(layout), floating_panes_layout, + swap_tiled_layouts, + swap_floating_layouts, name, )]) } } else { - Ok(vec![Action::NewTab(None, vec![], name)]) + Ok(vec![Action::NewTab(None, vec![], None, None, name)]) } }, + CliAction::PreviousSwapLayout => Ok(vec![Action::PreviousSwapLayout]), + CliAction::NextSwapLayout => Ok(vec![Action::NextSwapLayout]), } } } diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs index eaa2e3950..a0d61b241 100644 --- a/zellij-utils/src/input/layout.rs +++ b/zellij-utils/src/input/layout.rs @@ -22,6 +22,7 @@ use std::str::FromStr; use super::plugins::{PluginTag, PluginsConfigError}; use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; use std::convert::TryFrom; use std::vec::Vec; use std::{ @@ -169,6 +170,21 @@ impl Run { } } } + pub fn is_same_category(first: &Option, second: &Option) -> bool { + match (first, second) { + (Some(Run::Plugin(..)), Some(Run::Plugin(..))) => true, + (Some(Run::Command(..)), Some(Run::Command(..))) => true, + (Some(Run::EditFile(..)), Some(Run::EditFile(..))) => true, + (Some(Run::Cwd(..)), Some(Run::Cwd(..))) => true, + _ => false, + } + } + pub fn is_terminal(run: &Option) -> bool { + match run { + Some(Run::Command(..)) | Some(Run::EditFile(..)) | Some(Run::Cwd(..)) | None => true, + _ => false, + } + } } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] @@ -211,12 +227,27 @@ impl fmt::Display for RunPluginLocation { } } +#[derive(Clone, Debug, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)] +pub enum LayoutConstraint { + MaxPanes(usize), + MinPanes(usize), + NoConstraint, +} + +pub type SwapTiledLayout = (BTreeMap, Option); // Option is the swap layout name +pub type SwapFloatingLayout = ( + BTreeMap>, + Option, +); // Option is the swap layout name + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] pub struct Layout { - pub tabs: Vec<(Option, PaneLayout, Vec)>, + pub tabs: Vec<(Option, TiledPaneLayout, Vec)>, pub focused_tab_index: Option, - pub template: Option, - pub floating_panes_template: Vec, + pub template: Option<(TiledPaneLayout, Vec)>, + pub swap_layouts: Vec<(TiledPaneLayout, Vec)>, + pub swap_tiled_layouts: Vec, + pub swap_floating_layouts: Vec, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] @@ -270,8 +301,7 @@ impl FromStr for PercentOrFixed { } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] -pub struct FloatingPanesLayout { - // TODO: change name to singular +pub struct FloatingPaneLayout { pub name: Option, pub height: Option, pub width: Option, @@ -281,7 +311,7 @@ pub struct FloatingPanesLayout { pub focus: Option, } -impl FloatingPanesLayout { +impl FloatingPaneLayout { pub fn add_cwd_to_layout(&mut self, cwd: &PathBuf) { match self.run.as_mut() { Some(run) => run.add_cwd(cwd), @@ -292,9 +322,9 @@ impl FloatingPanesLayout { } } -impl From<&PaneLayout> for FloatingPanesLayout { - fn from(pane_layout: &PaneLayout) -> Self { - FloatingPanesLayout { +impl From<&TiledPaneLayout> for FloatingPaneLayout { + fn from(pane_layout: &TiledPaneLayout) -> Self { + FloatingPaneLayout { name: pane_layout.name.clone(), run: pane_layout.run.clone(), focus: pane_layout.focus, @@ -304,21 +334,22 @@ impl From<&PaneLayout> for FloatingPanesLayout { } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] -pub struct PaneLayout { +pub struct TiledPaneLayout { pub children_split_direction: SplitDirection, pub name: Option, - pub children: Vec, + pub children: Vec, pub split_size: Option, pub run: Option, pub borderless: bool, pub focus: Option, pub external_children_index: Option, + pub children_are_stacked: bool, } -impl PaneLayout { +impl TiledPaneLayout { pub fn insert_children_layout( &mut self, - children_layout: &mut PaneLayout, + children_layout: &mut TiledPaneLayout, ) -> Result { // returns true if successfully inserted and false otherwise match self.external_children_index { @@ -338,6 +369,30 @@ impl PaneLayout { }, } } + pub fn insert_children_nodes( + &mut self, + children_nodes: &mut Vec, + ) -> Result { + // returns true if successfully inserted and false otherwise + match self.external_children_index { + Some(external_children_index) => { + children_nodes.reverse(); + for child_node in children_nodes.drain(..) { + self.children.insert(external_children_index, child_node); + } + self.external_children_index = None; + Ok(true) + }, + None => { + for pane in self.children.iter_mut() { + if pane.insert_children_nodes(children_nodes)? { + return Ok(true); + } + } + Ok(false) + }, + } + } pub fn children_block_count(&self) -> usize { let mut count = 0; if self.external_children_index.is_some() { @@ -348,11 +403,50 @@ impl PaneLayout { } count } + pub fn pane_count(&self) -> usize { + if self.children.is_empty() { + 1 // self + } else { + let mut pane_count = 0; + for child in &self.children { + pane_count += child.pane_count(); + } + pane_count + } + } pub fn position_panes_in_space( &self, space: &PaneGeom, - ) -> Result, &'static str> { - let layouts = split_space(space, self, space); + max_panes: Option, + ) -> Result, &'static str> { + let layouts = match max_panes { + Some(max_panes) => { + let mut layout_to_split = self.clone(); + let pane_count_in_layout = layout_to_split.pane_count(); + if max_panes > pane_count_in_layout { + // the + 1 here is because this was previously an "actual" pane and will now + // become just a container, so we need to account for it too + // TODO: make sure this works when the `children` node has sibling nodes, + // because we really should support that + let children_count = (max_panes - pane_count_in_layout) + 1; + let mut extra_children = vec![TiledPaneLayout::default(); children_count]; + if !layout_to_split.has_focused_node() { + if let Some(last_child) = extra_children.last_mut() { + last_child.focus = Some(true); + } + } + let _ = layout_to_split.insert_children_nodes(&mut extra_children); + } else { + layout_to_split.truncate(max_panes); + } + if !layout_to_split.has_focused_node() { + layout_to_split.focus_deepest_pane(); + } + + split_space(space, &layout_to_split, space)? + }, + None => split_space(space, self, space)?, + }; for (_pane_layout, pane_geom) in layouts.iter() { if !pane_geom.is_at_least_minimum_size() { return Err("No room on screen for this layout!"); @@ -374,8 +468,8 @@ impl PaneLayout { run_instructions } pub fn with_one_pane() -> Self { - let mut default_layout = PaneLayout::default(); - default_layout.children = vec![PaneLayout::default()]; + let mut default_layout = TiledPaneLayout::default(); + default_layout.children = vec![TiledPaneLayout::default()]; default_layout } pub fn add_cwd_to_layout(&mut self, cwd: &PathBuf) { @@ -389,6 +483,79 @@ impl PaneLayout { child.add_cwd_to_layout(cwd); } } + pub fn deepest_depth(&self) -> usize { + let mut deepest_child_depth = 0; + for child in self.children.iter() { + let child_deepest_depth = child.deepest_depth(); + if child_deepest_depth > deepest_child_depth { + deepest_child_depth = child_deepest_depth; + } + } + deepest_child_depth + 1 + } + pub fn focus_deepest_pane(&mut self) { + let mut deepest_child_index = None; + let mut deepest_path = 0; + for (i, child) in self.children.iter().enumerate() { + let child_deepest_path = child.deepest_depth(); + if child_deepest_path >= deepest_path { + deepest_path = child_deepest_path; + deepest_child_index = Some(i) + } + } + match deepest_child_index { + Some(deepest_child_index) => { + if let Some(child) = self.children.get_mut(deepest_child_index) { + child.focus_deepest_pane(); + } + }, + None => { + self.focus = Some(true); + }, + } + } + pub fn truncate(&mut self, max_panes: usize) -> usize { + // returns remaining children length + // if max_panes is 1, it means there's only enough panes for this node, + // if max_panes is 0, this is probably the root layout being called with 0 max panes + if max_panes <= 1 { + self.children.clear(); + } else if max_panes <= self.children.len() { + self.children.truncate(max_panes); + self.children.iter_mut().for_each(|l| l.children.clear()); + } else { + let mut remaining_panes = max_panes + - self + .children + .iter() + .filter(|c| c.children.is_empty()) + .count(); + for child in self.children.iter_mut() { + if remaining_panes > 1 && child.children.len() > 0 { + remaining_panes = + remaining_panes.saturating_sub(child.truncate(remaining_panes)); + } else { + child.children.clear(); + } + } + } + if self.children.len() > 0 { + self.children.len() + } else { + 1 // just me + } + } + pub fn has_focused_node(&self) -> bool { + if self.focus.map(|f| f).unwrap_or(false) { + return true; + }; + for child in &self.children { + if child.has_focused_node() { + return true; + } + } + false + } } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] @@ -429,8 +596,8 @@ impl Layout { pub fn stringified_from_path_or_default( layout_path: Option<&PathBuf>, layout_dir: Option, - ) -> Result<(String, String), ConfigError> { - // (path_to_layout as String, stringified_layout) + ) -> Result<(String, String, Option<(String, String)>), ConfigError> { + // (path_to_layout as String, stringified_layout, Option) match layout_path { Some(layout_path) => { // The way we determine where to look for the layout is similar to @@ -455,24 +622,32 @@ impl Layout { layout_dir: Option, config: Config, ) -> Result<(Layout, Config), ConfigError> { - let (path_to_raw_layout, raw_layout) = + let (path_to_raw_layout, raw_layout, raw_swap_layouts) = Layout::stringified_from_path_or_default(layout_path, layout_dir)?; - let layout = Layout::from_kdl(&raw_layout, path_to_raw_layout, None)?; + let layout = Layout::from_kdl( + &raw_layout, + path_to_raw_layout, + raw_swap_layouts + .as_ref() + .map(|(r, f)| (r.as_str(), f.as_str())), + None, + )?; let config = Config::from_kdl(&raw_layout, Some(config))?; // this merges the two config, with Ok((layout, config)) } pub fn from_str( raw: &str, path_to_raw_layout: String, + swap_layouts: Option<(&str, &str)>, // Option cwd: Option, ) -> Result { - Layout::from_kdl(raw, path_to_raw_layout, cwd) + Layout::from_kdl(raw, path_to_raw_layout, swap_layouts, cwd) } pub fn stringified_from_dir( layout: &PathBuf, layout_dir: Option<&PathBuf>, - ) -> Result<(String, String), ConfigError> { - // (path_to_layout as String, stringified_layout) + ) -> Result<(String, String, Option<(String, String)>), ConfigError> { + // (path_to_layout as String, stringified_layout, Option) match layout_dir { Some(dir) => { let layout_path = &dir.join(layout); @@ -485,18 +660,28 @@ impl Layout { None => Layout::stringified_from_default_assets(layout), } } - pub fn stringified_from_path(layout_path: &Path) -> Result<(String, String), ConfigError> { - // (path_to_layout as String, stringified_layout) + pub fn stringified_from_path( + layout_path: &Path, + ) -> Result<(String, String, Option<(String, String)>), ConfigError> { + // (path_to_layout as String, stringified_layout, Option) let mut layout_file = File::open(&layout_path) .or_else(|_| File::open(&layout_path.with_extension("kdl"))) .map_err(|e| ConfigError::IoPath(e, layout_path.into()))?; + let swap_layout_and_path = Layout::swap_layout_and_path(&layout_path); + let mut kdl_layout = String::new(); layout_file.read_to_string(&mut kdl_layout)?; - Ok((layout_path.as_os_str().to_string_lossy().into(), kdl_layout)) - } - pub fn stringified_from_default_assets(path: &Path) -> Result<(String, String), ConfigError> { - // (path_to_layout as String, stringified_layout) + Ok(( + layout_path.as_os_str().to_string_lossy().into(), + kdl_layout, + swap_layout_and_path, + )) + } + pub fn stringified_from_default_assets( + path: &Path, + ) -> Result<(String, String, Option<(String, String)>), ConfigError> { + // (path_to_layout as String, stringified_layout, Option) // TODO: ideally these should not be hard-coded // we should load layouts by name from the config // and load them from a hashmap or some such @@ -504,18 +689,31 @@ impl Layout { Some("default") => Ok(( "Default layout".into(), Self::stringified_default_from_assets()?, + Some(( + "Default swap layout".into(), + Self::stringified_default_swap_from_assets()?, + )), )), Some("strider") => Ok(( "Strider layout".into(), Self::stringified_strider_from_assets()?, + Some(( + "Strider swap layout".into(), + Self::stringified_strider_swap_from_assets()?, + )), )), Some("disable-status-bar") => Ok(( "Disable Status Bar layout".into(), Self::stringified_disable_status_from_assets()?, + None, )), Some("compact") => Ok(( "Compact layout".into(), Self::stringified_compact_from_assets()?, + Some(( + "Compact layout swap".into(), + Self::stringified_compact_swap_from_assets()?, + )), )), None | Some(_) => Err(ConfigError::IoPath( std::io::Error::new(std::io::ErrorKind::Other, "The layout was not found"), @@ -526,10 +724,15 @@ impl Layout { pub fn stringified_default_from_assets() -> Result { Ok(String::from_utf8(setup::DEFAULT_LAYOUT.to_vec())?) } - + pub fn stringified_default_swap_from_assets() -> Result { + Ok(String::from_utf8(setup::DEFAULT_SWAP_LAYOUT.to_vec())?) + } pub fn stringified_strider_from_assets() -> Result { Ok(String::from_utf8(setup::STRIDER_LAYOUT.to_vec())?) } + pub fn stringified_strider_swap_from_assets() -> Result { + Ok(String::from_utf8(setup::STRIDER_SWAP_LAYOUT.to_vec())?) + } pub fn stringified_disable_status_from_assets() -> Result { Ok(String::from_utf8(setup::NO_STATUS_LAYOUT.to_vec())?) @@ -539,12 +742,12 @@ impl Layout { Ok(String::from_utf8(setup::COMPACT_BAR_LAYOUT.to_vec())?) } - pub fn new_tab(&self) -> (PaneLayout, Vec) { - let template = match &self.template { - Some(template) => template.clone(), - None => PaneLayout::default(), - }; - (template, self.floating_panes_template.clone()) + pub fn stringified_compact_swap_from_assets() -> Result { + Ok(String::from_utf8(setup::COMPACT_BAR_SWAP_LAYOUT.to_vec())?) + } + + pub fn new_tab(&self) -> (TiledPaneLayout, Vec) { + self.template.clone().unwrap_or_default() } pub fn is_empty(&self) -> bool { @@ -555,7 +758,7 @@ impl Layout { !self.tabs.is_empty() } - pub fn tabs(&self) -> Vec<(Option, PaneLayout, Vec)> { + pub fn tabs(&self) -> Vec<(Option, TiledPaneLayout, Vec)> { // String is the tab name self.tabs.clone() } @@ -563,16 +766,60 @@ impl Layout { pub fn focused_tab_index(&self) -> Option { self.focused_tab_index } + + fn swap_layout_and_path(path: &Path) -> Option<(String, String)> { + // Option + let mut swap_layout_path = PathBuf::from(path); + swap_layout_path.set_extension("swap.kdl"); + match File::open(&swap_layout_path) { + Ok(mut stringified_swap_layout_file) => { + let mut swap_kdl_layout = String::new(); + match stringified_swap_layout_file.read_to_string(&mut swap_kdl_layout) { + Ok(..) => Some(( + swap_layout_path.as_os_str().to_string_lossy().into(), + swap_kdl_layout, + )), + Err(e) => { + log::warn!( + "Failed to read swap layout file: {}. Error: {:?}", + swap_layout_path.as_os_str().to_string_lossy(), + e + ); + None + }, + } + }, + Err(e) => { + log::warn!( + "Failed to read swap layout file: {}. Error: {:?}", + swap_layout_path.as_os_str().to_string_lossy(), + e + ); + None + }, + } + } } fn split_space( space_to_split: &PaneGeom, - layout: &PaneLayout, + layout: &TiledPaneLayout, total_space_to_split: &PaneGeom, -) -> Vec<(PaneLayout, PaneGeom)> { +) -> Result, &'static str> { let mut pane_positions = Vec::new(); - let sizes: Vec> = - layout.children.iter().map(|part| part.split_size).collect(); + let sizes: Vec> = if layout.children_are_stacked { + let mut sizes: Vec> = layout + .children + .iter() + .map(|_part| Some(SplitSize::Fixed(1))) + .collect(); + if let Some(last_size) = sizes.last_mut() { + *last_size = None; + } + sizes + } else { + layout.children.iter().map(|part| part.split_size).collect() + }; let mut split_geom = Vec::new(); let ( @@ -595,7 +842,22 @@ fn split_space( ), }; + let min_size_for_panes = sizes.iter().fold(0, |acc, size| match size { + Some(SplitSize::Percent(_)) | None => acc + 1, // TODO: minimum height/width as relevant here + Some(SplitSize::Fixed(fixed)) => acc + fixed, + }); + if min_size_for_panes > split_dimension_space.as_usize() { + return Err("Not enough room for panes"); // TODO: use error infra + } + let flex_parts = sizes.iter().filter(|s| s.is_none()).count(); + let total_fixed_size = sizes.iter().fold(0, |acc, s| { + if let Some(SplitSize::Fixed(fixed)) = s { + acc + fixed + } else { + acc + } + }); let mut total_pane_size = 0; for (&size, _part) in sizes.iter().zip(&*layout.children) { @@ -617,7 +879,11 @@ fn split_space( Dimension::percent(free_percent / flex_parts as f64) }, }; - split_dimension.adjust_inner(total_split_dimension_space.as_usize()); + split_dimension.adjust_inner( + total_split_dimension_space + .as_usize() + .saturating_sub(total_fixed_size), + ); total_pane_size += split_dimension.as_usize(); let geom = match layout.children_split_direction { @@ -626,20 +892,22 @@ fn split_space( y: space_to_split.y, cols: split_dimension, rows: inherited_dimension, + is_stacked: layout.children_are_stacked, }, SplitDirection::Horizontal => PaneGeom { x: space_to_split.x, y: current_position, cols: inherited_dimension, rows: split_dimension, + is_stacked: layout.children_are_stacked, }, }; split_geom.push(geom); current_position += split_dimension.as_usize(); } - // add extra space from rounding errors to the last pane if total_pane_size < split_dimension_space.as_usize() { + // add extra space from rounding errors to the last pane let increase_by = split_dimension_space.as_usize() - total_pane_size; if let Some(last_geom) = split_geom.last_mut() { match layout.children_split_direction { @@ -647,21 +915,32 @@ fn split_space( SplitDirection::Horizontal => last_geom.rows.increase_inner(increase_by), } } + } else if total_pane_size > split_dimension_space.as_usize() { + // remove extra space from rounding errors to the last pane + let decrease_by = total_pane_size - split_dimension_space.as_usize(); + if let Some(last_geom) = split_geom.last_mut() { + match layout.children_split_direction { + SplitDirection::Vertical => last_geom.cols.decrease_inner(decrease_by), + SplitDirection::Horizontal => last_geom.rows.decrease_inner(decrease_by), + } + } } for (i, part) in layout.children.iter().enumerate() { let part_position_and_size = split_geom.get(i).unwrap(); if !part.children.is_empty() { let mut part_positions = - split_space(part_position_and_size, part, total_space_to_split); + split_space(part_position_and_size, part, total_space_to_split)?; pane_positions.append(&mut part_positions); } else { - pane_positions.push((part.clone(), *part_position_and_size)); + let part = part.clone(); + pane_positions.push((part, *part_position_and_size)); } } if pane_positions.is_empty() { - pane_positions.push((layout.clone(), space_to_split.clone())); + let layout = layout.clone(); + pane_positions.push((layout, space_to_split.clone())); } - pane_positions + Ok(pane_positions) } impl TryFrom for RunPluginLocation { diff --git a/zellij-utils/src/input/options.rs b/zellij-utils/src/input/options.rs index 93f15b2d4..684aabc00 100644 --- a/zellij-utils/src/input/options.rs +++ b/zellij-utils/src/input/options.rs @@ -116,6 +116,11 @@ pub struct Options { #[clap(long, value_parser)] #[serde(default)] pub attach_to_session: Option, + + /// Whether to lay out panes in a predefined set of layouts whenever possible + #[clap(long, value_parser)] + #[serde(default)] + pub auto_layout: Option, } #[derive(ArgEnum, Deserialize, Serialize, Debug, Clone, Copy, PartialEq)] @@ -157,6 +162,7 @@ impl Options { pub fn merge(&self, other: Options) -> Options { let mouse_mode = other.mouse_mode.or(self.mouse_mode); let pane_frames = other.pane_frames.or(self.pane_frames); + let auto_layout = other.auto_layout.or(self.auto_layout); let mirror_session = other.mirror_session.or(self.mirror_session); let simplified_ui = other.simplified_ui.or(self.simplified_ui); let default_mode = other.default_mode.or(self.default_mode); @@ -197,6 +203,7 @@ impl Options { scrollback_editor, session_name, attach_to_session, + auto_layout, } } @@ -218,6 +225,7 @@ impl Options { let simplified_ui = merge_bool(other.simplified_ui, self.simplified_ui); let mouse_mode = merge_bool(other.mouse_mode, self.mouse_mode); let pane_frames = merge_bool(other.pane_frames, self.pane_frames); + let auto_layout = merge_bool(other.auto_layout, self.auto_layout); let mirror_session = merge_bool(other.mirror_session, self.mirror_session); let default_mode = other.default_mode.or(self.default_mode); @@ -258,6 +266,7 @@ impl Options { scrollback_editor, session_name, attach_to_session, + auto_layout, } } @@ -288,6 +297,7 @@ impl From for Options { fn from(cli_options: CliOptions) -> Self { let mut opts = cli_options.options; + // TODO: what? if cli_options.no_pane_frames { opts.pane_frames = Some(false); } diff --git a/zellij-utils/src/input/unit/layout_test.rs b/zellij-utils/src/input/unit/layout_test.rs index d588fcda6..f15fd709d 100644 --- a/zellij-utils/src/input/unit/layout_test.rs +++ b/zellij-utils/src/input/unit/layout_test.rs @@ -4,9 +4,9 @@ use insta::assert_snapshot; #[test] fn empty_layout() { let kdl_layout = "layout"; - let layout = Layout::from_kdl(kdl_layout, "layout_file_name".into(), None).unwrap(); + let layout = Layout::from_kdl(kdl_layout, "layout_file_name".into(), None, None).unwrap(); let expected_layout = Layout { - template: Some(PaneLayout::default()), + template: Some((TiledPaneLayout::default(), vec![])), ..Default::default() }; assert_eq!(layout, expected_layout); @@ -19,12 +19,15 @@ fn layout_with_one_pane() { pane } "#; - let layout = Layout::from_kdl(kdl_layout, "layout_file_name".into(), None).unwrap(); + let layout = Layout::from_kdl(kdl_layout, "layout_file_name".into(), None, None).unwrap(); let expected_layout = Layout { - template: Some(PaneLayout { - children: vec![PaneLayout::default()], - ..Default::default() - }), + template: Some(( + TiledPaneLayout { + children: vec![TiledPaneLayout::default()], + ..Default::default() + }, + vec![], + )), ..Default::default() }; assert_eq!(layout, expected_layout); @@ -39,16 +42,19 @@ fn layout_with_multiple_panes() { pane } "#; - let layout = Layout::from_kdl(kdl_layout, "layout_file_name".into(), None).unwrap(); + let layout = Layout::from_kdl(kdl_layout, "layout_file_name".into(), None, None).unwrap(); let expected_layout = Layout { - template: Some(PaneLayout { - children: vec![ - PaneLayout::default(), - PaneLayout::default(), - PaneLayout::default(), - ], - ..Default::default() - }), + template: Some(( + TiledPaneLayout { + children: vec![ + TiledPaneLayout::default(), + TiledPaneLayout::default(), + TiledPaneLayout::default(), + ], + ..Default::default() + }, + vec![], + )), ..Default::default() }; assert_eq!(layout, expected_layout); @@ -68,22 +74,25 @@ fn layout_with_nested_panes() { } } "#; - let layout = Layout::from_kdl(kdl_layout, "layout_file_name".into(), None).unwrap(); + let layout = Layout::from_kdl(kdl_layout, "layout_file_name".into(), None, None).unwrap(); let expected_layout = Layout { - template: Some(PaneLayout { - children: vec![ - PaneLayout { - children_split_direction: SplitDirection::Vertical, - children: vec![PaneLayout::default(), PaneLayout::default()], - ..Default::default() - }, - PaneLayout { - children: vec![PaneLayout::default(), PaneLayout::default()], - ..Default::default() - }, - ], - ..Default::default() - }), + template: Some(( + TiledPaneLayout { + children: vec![ + TiledPaneLayout { + children_split_direction: SplitDirection::Vertical, + children: vec![TiledPaneLayout::default(), TiledPaneLayout::default()], + ..Default::default() + }, + TiledPaneLayout { + children: vec![TiledPaneLayout::default(), TiledPaneLayout::default()], + ..Default::default() + }, + ], + ..Default::default() + }, + vec![], + )), ..Default::default() }; assert_eq!(layout, expected_layout); @@ -106,32 +115,34 @@ fn layout_with_floating_panes() { } } "#; - let layout = Layout::from_kdl(kdl_layout, "layout_file_name".into(), None).unwrap(); + let layout = Layout::from_kdl(kdl_layout, "layout_file_name".into(), None, None).unwrap(); let expected_layout = Layout { - template: Some(PaneLayout::default()), - floating_panes_template: vec![ - FloatingPanesLayout::default(), - FloatingPanesLayout { - x: Some(PercentOrFixed::Fixed(10)), - y: Some(PercentOrFixed::Percent(10)), - width: Some(PercentOrFixed::Fixed(10)), - height: Some(PercentOrFixed::Percent(10)), - ..Default::default() - }, - FloatingPanesLayout { - x: Some(PercentOrFixed::Fixed(10)), - y: Some(PercentOrFixed::Percent(10)), - ..Default::default() - }, - FloatingPanesLayout { - run: Some(Run::Command(RunCommand { - command: PathBuf::from("htop"), - hold_on_close: true, + template: Some(( + TiledPaneLayout::default(), + vec![ + FloatingPaneLayout::default(), + FloatingPaneLayout { + x: Some(PercentOrFixed::Fixed(10)), + y: Some(PercentOrFixed::Percent(10)), + width: Some(PercentOrFixed::Fixed(10)), + height: Some(PercentOrFixed::Percent(10)), ..Default::default() - })), - ..Default::default() - }, - ], + }, + FloatingPaneLayout { + x: Some(Pe