aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2026-01-13 11:27:07 +0100
committerGitHub <noreply@github.com>2026-01-13 11:27:07 +0100
commit4a27ceea0d99b036ca607e05714cef15689d7c26 (patch)
tree22c0148718a82082bdb196c5e867697f514cc541
parentdocs(CONTRIBUTING): Describe rust toolchain versioning policy (#4585) (diff)
downloadsl0ppij-4a27ceea0d99b036ca607e05714cef15689d7c26.tar.gz
sl0ppij-4a27ceea0d99b036ca607e05714cef15689d7c26.tar.xz
sl0ppij-4a27ceea0d99b036ca607e05714cef15689d7c26.zip
Layout manager (#4601)
* ad plugin override_layout command * watch available layouts and report to plugin thread * Event::AvailableLayoutInfo * save layout command * refactor save_layout * fix api and permissions * delete_layout API command * edit layout in place API * edit layout response through api * propagate layout parsing errors * generate random name plugin api * dump stringified layout API * synchronicity dump_session_layout and include LayoutMetadata in sync response * parse_layout plugin api * fix: default to default layout dir in plugin api * rename_layout plugin command API * initial implementation of multi-tab override * apply to multiple tabs * allow applying to only the active tab * add tab/pane names to layout parsing metadata * indicate is_builtin_plugin in PaneMetadata * get_layout_dir plugin API command * consolidate (and fix) is_builtin_plugin logic * fix bad ret * allow dumping the layout for just a specific tab in the session * get focused pane and tab sync api * color last substring variants * add plugin * update assets * fix tests * add e2e tests for new plugin apis * update ui with new shortcut * rustfmt * docs(changelog): add PR
-rw-r--r--CHANGELOG.md1
-rw-r--r--Cargo.lock38
-rw-r--r--Cargo.toml2
-rw-r--r--default-plugins/configuration/src/presets.rs42
-rw-r--r--default-plugins/fixture-plugin-for-tests/src/main.rs437
-rw-r--r--default-plugins/layout-manager/.cargo/config.toml2
-rw-r--r--default-plugins/layout-manager/.gitignore1
-rw-r--r--default-plugins/layout-manager/Cargo.lock3020
-rw-r--r--default-plugins/layout-manager/Cargo.toml12
l---------default-plugins/layout-manager/LICENSE.md1
-rw-r--r--default-plugins/layout-manager/README.md27
-rw-r--r--default-plugins/layout-manager/src/errors.rs155
-rw-r--r--default-plugins/layout-manager/src/main.rs295
-rw-r--r--default-plugins/layout-manager/src/screens/import_layout.rs331
-rw-r--r--default-plugins/layout-manager/src/screens/layout_list/mod.rs689
-rw-r--r--default-plugins/layout-manager/src/screens/layout_list/search.rs187
-rw-r--r--default-plugins/layout-manager/src/screens/mod.rs79
-rw-r--r--default-plugins/layout-manager/src/screens/new_layout_from_session.rs352
-rw-r--r--default-plugins/layout-manager/src/screens/rename_layout.rs156
-rw-r--r--default-plugins/layout-manager/src/text_input.rs1007
-rw-r--r--default-plugins/layout-manager/src/ui/layout_detail_view.rs345
-rw-r--r--default-plugins/layout-manager/src/ui/layout_table.rs452
-rw-r--r--default-plugins/layout-manager/src/ui/mod.rs9
-rw-r--r--default-plugins/layout-manager/src/ui/multi_line_error_message.rs26
-rw-r--r--default-plugins/layout-manager/src/ui/text_utils.rs195
-rw-r--r--default-plugins/status-bar/src/one_line_ui.rs20
-rw-r--r--example/default.kdl7
-rw-r--r--xtask/src/main.rs4
-rw-r--r--zellij-client/src/lib.rs12
-rw-r--r--zellij-client/src/web_client/session_management.rs3
-rw-r--r--zellij-server/Cargo.toml1
-rw-r--r--zellij-server/src/background_jobs.rs3
-rw-r--r--zellij-server/src/lib.rs69
-rw-r--r--zellij-server/src/plugins/mod.rs211
-rw-r--r--zellij-server/src/plugins/plugin_map.rs3
-rw-r--r--zellij-server/src/plugins/unit/plugin_tests.rs2052
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__delete_layout_not_found_plugin_command.snap7
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__delete_layout_success_plugin_command.snap5
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__dump_layout_not_found_plugin_command.snap7
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__dump_layout_success_plugin_command.snap7
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__dump_session_layout_for_tab_plugin_command.snap13
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__dump_session_layout_plugin_command.snap11
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__edit_layout_plugin_command.snap45
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__generate_random_name_permission_denied.snap5
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__generate_random_name_plugin_command.snap7
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__get_focused_pane_info_plugin_command.snap10
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__get_layout_dir_plugin_command.snap7
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__override_layout_plugin_command.snap1977
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__parse_layout_error_plugin_command.snap7
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__parse_layout_success_plugin_command.snap7
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_layout_not_found_plugin_command.snap7
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_layout_success_plugin_command.snap7
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__save_layout_already_exists_plugin_command.snap7
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__save_layout_invalid_kdl_plugin_command.snap7
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__save_layout_path_traversal_blocked_plugin_command.snap7
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__save_layout_permission_denied.snap5
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__save_layout_success_plugin_command.snap7
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__save_layout_with_overwrite_plugin_command.snap7
-rw-r--r--zellij-server/src/plugins/wasm_bridge.rs45
-rw-r--r--zellij-server/src/plugins/zellij_exports.rs713
-rw-r--r--zellij-server/src/pty.rs141
-rw-r--r--zellij-server/src/route.rs21
-rw-r--r--zellij-server/src/screen.rs362
-rw-r--r--zellij-server/src/session_layout_metadata.rs94
-rw-r--r--zellij-tile/src/shim.rs380
-rw-r--r--zellij-tile/src/ui_components/text.rs53
-rw-r--r--zellij-utils/assets/config/default.kdl7
-rwxr-xr-xzellij-utils/assets/plugins/about.wasmbin1412064 -> 1452614 bytes
-rwxr-xr-xzellij-utils/assets/plugins/compact-bar.wasmbin1571455 -> 1621317 bytes
-rwxr-xr-xzellij-utils/assets/plugins/configuration.wasmbin1473897 -> 1518813 bytes
-rwxr-xr-xzellij-utils/assets/plugins/fixture-plugin-for-tests.wasmbin1467669 -> 1518051 bytes
-rwxr-xr-xzellij-utils/assets/plugins/layout-manager.wasmbin0 -> 1808818 bytes
-rwxr-xr-xzellij-utils/assets/plugins/multiple-select.wasmbin1405967 -> 1450066 bytes
-rwxr-xr-xzellij-utils/assets/plugins/plugin-manager.wasmbin1385313 -> 1427576 bytes
-rwxr-xr-xzellij-utils/assets/plugins/sequence.wasmbin1441780 -> 1482266 bytes
-rwxr-xr-xzellij-utils/assets/plugins/session-manager.wasmbin1467557 -> 1510388 bytes
-rwxr-xr-xzellij-utils/assets/plugins/share.wasmbin1380992 -> 1422243 bytes
-rwxr-xr-xzellij-utils/assets/plugins/status-bar.wasmbin1582131 -> 1617347 bytes
-rwxr-xr-xzellij-utils/assets/plugins/strider.wasmbin1534669 -> 1586209 bytes
-rwxr-xr-xzellij-utils/assets/plugins/tab-bar.wasmbin1462479 -> 1508719 bytes
-rw-r--r--zellij-utils/assets/prost/api.action.rs28
-rw-r--r--zellij-utils/assets/prost/api.event.rs97
-rw-r--r--zellij-utils/assets/prost/api.plugin_command.rs288
-rw-r--r--zellij-utils/assets/prost_ipc/client_server_contract.rs58
-rw-r--r--zellij-utils/src/cli.rs5
-rw-r--r--zellij-utils/src/client_server_contract/common_types.proto39
-rw-r--r--zellij-utils/src/consts.rs1
-rw-r--r--zellij-utils/src/data.rs346
-rw-r--r--zellij-utils/src/errors.rs3
-rw-r--r--zellij-utils/src/input/actions.rs60
-rw-r--r--zellij-utils/src/input/config.rs97
-rw-r--r--zellij-utils/src/input/layout.rs98
-rw-r--r--zellij-utils/src/input/plugins.rs1
-rw-r--r--zellij-utils/src/ipc/protobuf_conversion.rs233
-rw-r--r--zellij-utils/src/ipc/tests/roundtrip_tests.rs20
-rw-r--r--zellij-utils/src/kdl/mod.rs51
-rw-r--r--zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap7
-rw-r--r--zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap7
-rw-r--r--zellij-utils/src/plugin_api/action.proto20
-rw-r--r--zellij-utils/src/plugin_api/action.rs181
-rw-r--r--zellij-utils/src/plugin_api/event.proto55
-rw-r--r--zellij-utils/src/plugin_api/event.rs304
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.proto137
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.rs385
-rw-r--r--zellij-utils/src/sessions.rs6
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap30
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap30
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap30
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap30
109 files changed, 16209 insertions, 669 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3aca1dc7..66a0fd6a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
* feat: allow overriding layouts at runtime (https://github.com/zellij-org/zellij/pull/4566)
* build: Update Rust toolchain to 1.92.0 (https://github.com/zellij-org/zellij/pull/4579)
* docs: Explain Rust toolchain update strategy in CONTRIBUTING (https://github.com/zellij-org/zellij/pull/4585)
+* feat: new `layout-manager` interface and plugin API commands (https://github.com/zellij-org/zellij/pull/4601)
## [0.43.1] - 2025-08-08
* fix: pane rename backspace regression (https://github.com/zellij-org/zellij/pull/4346)
diff --git a/Cargo.lock b/Cargo.lock
index 3c7b4b87..2c450cd2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2084,6 +2084,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f"
[[package]]
+name = "layout-manager"
+version = "0.1.0"
+dependencies = [
+ "fuzzy-matcher",
+ "miette",
+ "strip-ansi-escapes 0.2.1",
+ "unicode-width 0.1.10",
+ "zellij-tile",
+]
+
+[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2285,9 +2296,9 @@ checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4"
[[package]]
name = "memchr"
-version = "2.5.0"
+version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "memmem"
@@ -3691,7 +3702,7 @@ dependencies = [
"pretty-bytes",
"serde",
"serde_json",
- "strip-ansi-escapes",
+ "strip-ansi-escapes 0.1.1",
"unicode-width 0.1.10",
"zellij-tile",
]
@@ -3716,6 +3727,15 @@ dependencies = [
]
[[package]]
+name = "strip-ansi-escapes"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025"
+dependencies = [
+ "vte 0.14.1",
+]
+
+[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4469,6 +4489,15 @@ dependencies = [
]
[[package]]
+name = "vte"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
name = "vte_generate_state_changes"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5223,6 +5252,7 @@ dependencies = [
"insta",
"interprocess",
"isahc",
+ "kdl",
"lazy_static",
"libc",
"log",
@@ -5308,7 +5338,7 @@ dependencies = [
"serde_json",
"sha2",
"shellexpand",
- "strip-ansi-escapes",
+ "strip-ansi-escapes 0.1.1",
"strum",
"strum_macros",
"suggest",
diff --git a/Cargo.toml b/Cargo.toml
index 789cab50..652f5283 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -51,6 +51,7 @@ members = [
"default-plugins/share",
"default-plugins/multiple-select",
"default-plugins/sequence",
+ "default-plugins/layout-manager",
"zellij-client",
"zellij-server",
"zellij-utils",
@@ -79,6 +80,7 @@ dialoguer = { version = "0.10.4", default-features = false, features = ["passwor
humantime = { version = "2.1.0", default-features = false }
interprocess = { version = "1.2.1", default-features = false }
isahc = { version = "1.7.2", default-features = false, features = ["http2", "text-decoding"] }
+kdl = { version = "4.5.0", default-features = false, features = ["span"] }
lazy_static = { version = "1.4.0", default-features = false }
libc = { version = "0.2", default-features = false, features = ["std"] }
log = { version = "0.4.17", default-features = false }
diff --git a/default-plugins/configuration/src/presets.rs b/default-plugins/configuration/src/presets.rs
index 7e875073..03d05171 100644
--- a/default-plugins/configuration/src/presets.rs
+++ b/default-plugins/configuration/src/presets.rs
@@ -161,6 +161,13 @@ keybinds clear-defaults=true {{
}};
SwitchToMode "Locked"
}}
+ bind "l" {{
+ LaunchOrFocusPlugin "zellij:layout-manager" {{
+ floating true
+ move_to_focused_tab true
+ }};
+ SwitchToMode "Locked"
+ }}
}}
shared_except "locked" "renametab" "renamepane" {{
bind "{primary_modifier} g" {{ SwitchToMode "Locked"; }}
@@ -383,6 +390,13 @@ keybinds clear-defaults=true {{
}};
SwitchToMode "Normal"
}}
+ bind "l" {{
+ LaunchOrFocusPlugin "zellij:layout-manager" {{
+ floating true
+ move_to_focused_tab true
+ }};
+ SwitchToMode "Normal"
+ }}
}}
tmux {{
bind "[" {{ SwitchToMode "Scroll"; }}
@@ -605,6 +619,13 @@ keybinds clear-defaults=true {{
}};
SwitchToMode "Normal"
}}
+ bind "l" {{
+ LaunchOrFocusPlugin "zellij:layout-manager" {{
+ floating true
+ move_to_focused_tab true
+ }};
+ SwitchToMode "Normal"
+ }}
}}
tmux {{
bind "[" {{ SwitchToMode "Scroll"; }}
@@ -808,6 +829,13 @@ keybinds clear-defaults=true {{
}};
SwitchToMode "Normal"
}}
+ bind "l" {{
+ LaunchOrFocusPlugin "zellij:layout-manager" {{
+ floating true
+ move_to_focused_tab true
+ }};
+ SwitchToMode "Normal"
+ }}
}}
tmux {{
bind "[" {{ SwitchToMode "Scroll"; }}
@@ -1011,6 +1039,13 @@ keybinds clear-defaults=true {{
}};
SwitchToMode "Normal"
}}
+ bind "l" {{
+ LaunchOrFocusPlugin "zellij:layout-manager" {{
+ floating true
+ move_to_focused_tab true
+ }};
+ SwitchToMode "Normal"
+ }}
}}
tmux {{
bind "[" {{ SwitchToMode "Scroll"; }}
@@ -1209,6 +1244,13 @@ keybinds clear-defaults=true {{
}};
SwitchToMode "Normal"
}}
+ bind "l" {{
+ LaunchOrFocusPlugin "zellij:layout-manager" {{
+ floating true
+ move_to_focused_tab true
+ }};
+ SwitchToMode "Normal"
+ }}
}}
tmux {{
bind "[" {{ SwitchToMode "Scroll"; }}
diff --git a/default-plugins/fixture-plugin-for-tests/src/main.rs b/default-plugins/fixture-plugin-for-tests/src/main.rs
index 9f5b7e93..be8efe83 100644
--- a/default-plugins/fixture-plugin-for-tests/src/main.rs
+++ b/default-plugins/fixture-plugin-for-tests/src/main.rs
@@ -16,6 +16,7 @@ struct State {
received_payload: Option<String>,
configuration: BTreeMap<String, String>,
message_to_plugin_payload: Option<String>,
+ explicit_string_to_render: Option<String>,
}
#[derive(Default, Serialize, Deserialize)]
@@ -153,27 +154,29 @@ impl ZellijPlugin for State {
BareKey::Char('6') if key.has_no_modifiers() => close_focused_tab(),
BareKey::Char('7') if key.has_no_modifiers() => undo_rename_tab(),
BareKey::Char('8') if key.has_no_modifiers() => quit_zellij(),
- BareKey::Char('a') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('a') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
previous_swap_layout()
},
- BareKey::Char('b') if key.has_modifiers(&[KeyModifier::Ctrl]) => next_swap_layout(),
- BareKey::Char('c') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('b') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
+ next_swap_layout()
+ },
+ BareKey::Char('c') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
let tab_name = "my tab name";
go_to_tab_name(tab_name)
},
- BareKey::Char('d') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('d') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
let tab_name = "my tab name";
focus_or_create_tab(tab_name)
},
- BareKey::Char('e') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('e') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
let tab_index = 2;
go_to_tab(tab_index)
},
- BareKey::Char('f') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('f') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
let plugin_url = "file:/path/to/my/plugin.wasm";
start_or_reload_plugin(plugin_url)
},
- BareKey::Char('g') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('g') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
open_file(
FileToOpen {
path: std::path::PathBuf::from("/path/to/my/file.rs"),
@@ -182,7 +185,7 @@ impl ZellijPlugin for State {
BTreeMap::new(),
);
},
- BareKey::Char('h') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('h') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
open_file_floating(
FileToOpen {
path: std::path::PathBuf::from("/path/to/my/file.rs"),
@@ -192,7 +195,7 @@ impl ZellijPlugin for State {
BTreeMap::new(),
);
},
- BareKey::Char('i') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('i') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
open_file(
FileToOpen {
path: std::path::PathBuf::from("/path/to/my/file.rs"),
@@ -202,7 +205,7 @@ impl ZellijPlugin for State {
BTreeMap::new(),
);
},
- BareKey::Char('j') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('j') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
open_file_floating(
FileToOpen {
path: std::path::PathBuf::from("/path/to/my/file.rs"),
@@ -213,16 +216,16 @@ impl ZellijPlugin for State {
BTreeMap::new(),
);
},
- BareKey::Char('k') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('k') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
open_terminal(std::path::PathBuf::from("/path/to/my/file.rs").as_path());
},
- BareKey::Char('l') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('l') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
open_terminal_floating(
std::path::PathBuf::from("/path/to/my/file.rs").as_path(),
None,
);
},
- BareKey::Char('m') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('m') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
open_command_pane(
CommandToRun {
path: std::path::PathBuf::from("/path/to/my/file.rs"),
@@ -232,7 +235,7 @@ impl ZellijPlugin for State {
BTreeMap::new(),
);
},
- BareKey::Char('n') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('n') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
open_command_pane_floating(
CommandToRun {
path: std::path::PathBuf::from("/path/to/my/file.rs"),
@@ -243,53 +246,53 @@ impl ZellijPlugin for State {
BTreeMap::new(),
);
},
- BareKey::Char('o') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('o') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
switch_tab_to(1);
},
- BareKey::Char('p') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('p') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
hide_self();
},
- BareKey::Char('q') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('q') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
let should_float_if_hidden = false;
show_self(should_float_if_hidden);
},
- BareKey::Char('r') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('r') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
close_terminal_pane(1);
},
- BareKey::Char('s') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('s') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
close_plugin_pane(1);
},
- BareKey::Char('t') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('t') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
let should_float_if_hidden = false;
let should_be_in_place_if_hidden = false;
focus_terminal_pane(1, should_float_if_hidden, should_be_in_place_if_hidden);
},
- BareKey::Char('u') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('u') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
let should_float_if_hidden = false;
let should_be_in_place_if_hidden = false;
focus_plugin_pane(1, should_float_if_hidden, should_be_in_place_if_hidden);
},
- BareKey::Char('v') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('v') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
rename_terminal_pane(1, "new terminal_pane_name");
},
- BareKey::Char('w') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('w') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
rename_plugin_pane(1, "new plugin_pane_name");
},
- BareKey::Char('x') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('x') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
rename_tab(1, "new tab name");
},
- BareKey::Char('z') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('z') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
go_to_tab_name(&format!("{:?}", self.configuration));
},
- BareKey::Char('1') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('1') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
request_permission(&[PermissionType::ReadApplicationState]);
},
- BareKey::Char('2') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('2') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
let mut context = BTreeMap::new();
context.insert("user_key_1".to_owned(), "user_value_1".to_owned());
run_command(&["ls", "-l"], context);
},
- BareKey::Char('3') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('3') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
let mut context = BTreeMap::new();
context.insert("user_key_2".to_owned(), "user_value_2".to_owned());
let mut env_vars = BTreeMap::new();
@@ -301,7 +304,7 @@ impl ZellijPlugin for State {
context,
);
},
- BareKey::Char('4') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('4') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
let mut headers = BTreeMap::new();
let mut context = BTreeMap::new();
let body = vec![1, 2, 3];
@@ -317,31 +320,31 @@ impl ZellijPlugin for State {
context,
);
},
- BareKey::Char('5') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('5') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
switch_session(Some("my_new_session"));
},
- BareKey::Char('6') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('6') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
disconnect_other_clients()
},
- BareKey::Char('7') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('7') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
switch_session_with_layout(
Some("my_other_new_session"),
LayoutInfo::BuiltIn("compact".to_owned()),
None,
);
},
- BareKey::Char('8') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('8') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
let mut file = std::fs::File::create("/host/hi-from-plugin.txt").unwrap();
file.write_all(b"Hi there!").unwrap();
},
- BareKey::Char('9') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('9') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
switch_session_with_layout(
Some("my_other_new_session_with_cwd"),
LayoutInfo::BuiltIn("compact".to_owned()),
Some(std::path::PathBuf::from("/tmp")),
);
},
- BareKey::Char('0') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ BareKey::Char('0') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
let write_to_disk = true;
reconfigure(
"
@@ -355,13 +358,13 @@ impl ZellijPlugin for State {
write_to_disk,
);
},
- BareKey::Char('a') if key.has_modifiers(&[KeyModifier::Alt]) => {
+ BareKey::Char('a') if key.has_only_modifiers(&[KeyModifier::Alt]) => {
hide_pane_with_id(PaneId::Terminal(1));
},
- BareKey::Char('b') if key.has_modifiers(&[KeyModifier::Alt]) => {
+ BareKey::Char('b') if key.has_only_modifiers(&[KeyModifier::Alt]) => {
show_pane_with_id(PaneId::Terminal(1), true, true);
},
- BareKey::Char('c') if key.has_modifiers(&[KeyModifier::Alt]) => {
+ BareKey::Char('c') if key.has_only_modifiers(&[KeyModifier::Alt]) => {
open_command_pane_background(
CommandToRun {
path: std::path::PathBuf::from("/path/to/my/file.rs"),
@@ -371,61 +374,61 @@ impl ZellijPlugin for State {
BTreeMap::new(),
);
},
- BareKey::Char('d') if key.has_modifiers(&[KeyModifier::Alt]) => {
+ BareKey::Char('d') if key.has_only_modifiers(&[KeyModifier::Alt]) => {
rerun_command_pane(1);
},
- BareKey::Char('e') if key.has_modifiers(&[KeyModifier::Alt]) => {
+ BareKey::Char('e') if key.has_only_modifiers(&[KeyModifier::Alt]) => {
resize_pane_with_id(
ResizeStrategy::new(Resize::Increase, Some(Direction::Left)),
PaneId::Terminal(2),
);
},
- BareKey::Char('f') if key.has_modifiers(&[KeyModifier::Alt]) => {
+ BareKey::Char('f') if key.has_only_modifiers(&[KeyModifier::Alt]) => {
edit_scrollback_for_pane_with_id(PaneId::Terminal(2));
},
- BareKey::Char('g') if key.has_modifiers(&[KeyModifier::Alt]) => {
+ BareKey::Char('g') if key.has_only_modifiers(&[KeyModifier::Alt]) => {
write_to_pane_id(vec![102, 111, 111], PaneId::Terminal(2));
},
- BareKey::Char('h') if key.has_modifiers(&[KeyModifier::Alt]) => {
+ BareKey::Char('h') if key.has_only_modifiers(&[KeyModifier::Alt]) => {
write_chars_to_pane_id("foo\n", PaneId::Terminal(2));
},
- BareKey::Char('i') if key.has_modifiers(&[KeyModifier::Alt]) => {
+ BareKey::Char('i') if key.has_only_modifiers(&[KeyModifier::Alt]) => {
move_pane_with_pane_id(PaneId::Terminal(2));
},
- BareKey::Char('j') if key.has_modifiers(&[KeyModifier::Alt]) => {
+ BareKey::Char('j') if key.has_only_modifiers(&[KeyModifier::Alt]) => {
move_pane_with_pane_id_in_direction(PaneId::Terminal(2), Direction::Left);
},
- BareKey::Char('k') if key.has_modifiers(&[KeyModifier::Alt]) => {
+ BareKey::Char('k') if key.has_only_modifiers(&[KeyModifier::Alt]) => {
clear_screen_for_pane_id(PaneId::Terminal(2));
},
- BareKey::Char('l') if key.has_modifiers(&[KeyModifier::Alt]) => {
+ BareKey::Char('l') if key.has_only_modifiers(&[KeyModifier::Alt]) => {
scroll_up_in_pane_id(PaneId::Terminal(2));
},
- BareKey::Char('m') if key.has_modifiers(&[KeyModifier::Alt]) => {
+ BareKey::Char('m') if key.has_only_modifiers(&[KeyModifier::Alt]) => {
scroll_down_in_pane_id(PaneId::Terminal(2));
},
- BareKey::Char('n') if key.has_modifiers(&[KeyModifier::Alt]) => {
+ BareKey::Char('n') if key.has_only_modifiers(&[KeyModifier::Alt]) => {
scroll_to_top_in_pane_id(PaneId::Terminal(2));
},
- BareKey::Char('o') if key.has_modifiers(&[KeyModifier::Alt]) => {
+ BareKey::Char('o') if key.has_only_modifiers(&[KeyModifier::Alt]) => {
scroll_to_bottom_in_pane_id(PaneId::Terminal(2));
},
- BareKey::Char('p') if key.has_modifiers(&[KeyModifier::Alt]) => {
+ BareKey::Char('p') if key.has_only_modifiers(&[KeyModifier::Alt]) => {
page_scroll_up_in_pane_id(PaneId::Terminal(2));
},
- BareKey::Char('q') if key.has_modifiers(&[KeyModifier::Alt]) => {
+ BareKey::Char('q') if key.has_only_modifiers(&[KeyModifier::Alt]) => {
page_scroll_down_in_pane_id(PaneId::Terminal(2));
},
- BareKey::Char('r') if key.has_modifiers(&[KeyModifier::Alt]) => {
+ BareKey::Char('r') if key.has_only_modifiers(&[KeyModifier::Alt]) => {
toggle_pane_id_fullscreen(PaneId::Terminal(2));
},
- BareKey::Char('s') if key.has_modifiers(&[KeyModifier::Alt]) => {
+ BareKey::Char('s') if key.has_only_modifiers(&[KeyModifier::Alt]) => {
toggle_pane_embed_or_eject_for_pane_id(PaneId::Terminal(2));
},
- BareKey::Char('t') if key.has_modifiers(&[KeyModifier::Alt]) => {
+ BareKey::Char('t') if key.has_only_modifiers(&[KeyModifier::Alt]) => {
close_tab_with_index(2);
},
- BareKey::Char('u') if key.has_modifiers(&[KeyModifier::Alt]) => {
+ BareKey::Char('u') if key.has_only_modifiers(&[KeyModifier::Alt]) => {
let should_change_focus_to_new_tab = true;
break_panes_to_new_tab(
&[PaneId::Terminal(1), PaneId::Plugin(2)],
@@ -433,7 +436,7 @@ impl ZellijPlugin for State {
should_change_focus_to_new_tab,
);
},
- BareKey::Char('v') if key.has_modifiers(&[KeyModifier::Alt]) => {
+ BareKey::Char('v') if key.has_only_modifiers(&[KeyModifier::Alt]) => {
let should_change_focus_to_target_tab = true;
break_panes_to_tab_with_index(
&[PaneId::Terminal(1), PaneId::Plugin(2)],
@@ -441,10 +444,10 @@ impl ZellijPlugin for State {
should_change_focus_to_target_tab,
);
},
- BareKey::Char('w') if key.has_modifiers(&[KeyModifier::Alt]) => {
+ BareKey::Char('w') if key.has_only_modifiers(&[KeyModifier::Alt]) => {
reload_plugin_with_id(0);
},
- BareKey::Char('x') if key.has_modifiers(&[KeyModifier::Alt]) => {
+ BareKey::Char('x') if key.has_only_modifiers(&[KeyModifier::Alt]) => {
let config = BTreeMap::new();
let load_in_background = true;
let skip_plugin_cache = true;
@@ -455,7 +458,7 @@ impl ZellijPlugin for State {
skip_plugin_cache,
)
},
- BareKey::Char('y') if key.has_modifiers(&[KeyModifier::Alt]) => {
+ BareKey::Char('y') if key.has_only_modifiers(&[KeyModifier::Alt]) => {
let write_to_disk = true;
let keys_to_unbind = vec![
(
@@ -551,22 +554,22 @@ impl ZellijPlugin for State {
];
rebind_keys(keys_to_unbind, keys_to_rebind, write_to_disk);
},
- BareKey::Char('z') if key.has_modifiers(&[KeyModifier::Alt]) => {
+ BareKey::Char('z') if key.has_only_modifiers(&[KeyModifier::Alt]) => {
list_clients();
},
- BareKey::Char('a') if key.has_modifiers(&[KeyModifier::Super]) => {
+ BareKey::Char('a') if key.has_only_modifiers(&[KeyModifier::Super]) => {
// Test show_cursor with coordinates
show_cursor(Some((5, 10)));
},
- BareKey::Char('b') if key.has_modifiers(&[KeyModifier::Super]) => {
+ BareKey::Char('b') if key.has_only_modifiers(&[KeyModifier::Super]) => {
// Test hide_cursor
show_cursor(None);
},
- BareKey::Char('c') if key.has_modifiers(&[KeyModifier::Super]) => {
+ BareKey::Char('c') if key.has_only_modifiers(&[KeyModifier::Super]) => {
// Test copy_to_clipboard
copy_to_clipboard("test clipboard text");
},
- BareKey::Char('d') if key.has_modifiers(&[KeyModifier::Super]) => {
+ BareKey::Char('d') if key.has_only_modifiers(&[KeyModifier::Super]) => {
// Test run_action with MoveFocus
let mut context = BTreeMap::new();
context.insert("test_key".to_string(), "test_value".to_string());
@@ -577,14 +580,312 @@ impl ZellijPlugin for State {
context,
);
},
- BareKey::Char('e') if key.has_modifiers(&[KeyModifier::Super]) => {
+ BareKey::Char('e') if key.has_only_modifiers(&[KeyModifier::Super]) => {
// Test send_sigint_to_pane_id
send_sigint_to_pane_id(PaneId::Terminal(1));
},
- BareKey::Char('f') if key.has_modifiers(&[KeyModifier::Super]) => {
+ BareKey::Char('f') if key.has_only_modifiers(&[KeyModifier::Super]) => {
// Test send_sigkill_to_pane_id
send_sigkill_to_pane_id(PaneId::Terminal(1));
},
+ BareKey::Char('a')
+ if key.has_only_modifiers(&[KeyModifier::Ctrl, KeyModifier::Shift]) =>
+ {
+ let name = generate_random_name();
+ if !name.is_empty() {
+ self.explicit_string_to_render = Some(format!("Generated name"));
+ } else {
+ self.explicit_string_to_render = Some(format!("Error: got empty name"));
+ }
+ },
+ BareKey::Char('b')
+ if key.has_only_modifiers(&[KeyModifier::Ctrl, KeyModifier::Shift]) =>
+ {
+ match dump_layout("default") {
+ Ok(kdl) => {
+ self.explicit_string_to_render =
+ Some(format!("Layout dump success: {}", kdl))
+ },
+ Err(e) => {
+ self.explicit_string_to_render =
+ Some(format!("Layout dump error: {}", e))
+ },
+ }
+ },
+ BareKey::Char('c')
+ if key.has_only_modifiers(&[KeyModifier::Ctrl, KeyModifier::Shift]) =>
+ {
+ // Test dump_layout() - not found case
+ match dump_layout("nonexistent_layout_xyz") {
+ Ok(kdl) => {
+ self.explicit_string_to_render =
+ Some(format!("Layout dump success: {}", kdl))
+ },
+ Err(e) => {
+ self.explicit_string_to_render =
+ Some(format!("Layout dump error: {}", e))
+ },
+ }
+ },
+ BareKey::Char('d')
+ if key.has_only_modifiers(&[KeyModifier::Ctrl, KeyModifier::Shift]) =>
+ {
+ // Test get_layout_dir()
+ let dir = get_layout_dir();
+ if !dir.is_empty() {
+ self.explicit_string_to_render = Some(format!("Got layout folder"));
+ } else {
+ self.explicit_string_to_render =
+ Some(format!("Error: Got empty layout folder!"));
+ }
+ },
+ BareKey::Char('e')
+ if key.has_only_modifiers(&[KeyModifier::Ctrl, KeyModifier::Shift]) =>
+ {
+ // Test get_focused_pane_info()
+ match get_focused_pane_info() {
+ Ok((tab_index, pane_id)) => {
+ println!("Focused pane: tab={}, pane={:?}", tab_index, pane_id)
+ },
+ Err(e) => {
+ self.explicit_string_to_render =
+ Some(format!("Focused pane error: {}", e))
+ },
+ }
+ },
+ BareKey::Char('f')
+ if key.has_only_modifiers(&[KeyModifier::Ctrl, KeyModifier::Shift]) =>
+ {
+ // Test dump_session_layout()
+ match dump_session_layout() {
+ Ok((kdl, metadata)) => {
+ self.explicit_string_to_render = Some(format!(
+ "Session layout: {} tabs, metadata: {:?}",
+ kdl.len(),
+ metadata
+ ))
+ },
+ Err(e) => {
+ self.explicit_string_to_render =
+ Some(format!("Session layout error: {}", e))
+ },
+ }
+ },
+ BareKey::Char('g')
+ if key.has_only_modifiers(&[KeyModifier::Ctrl, KeyModifier::Shift]) =>
+ {
+ // Test dump_session_layout_for_tab()
+ match dump_session_layout_for_tab(0) {
+ Ok((kdl, metadata)) => {
+ self.explicit_string_to_render = Some(format!(
+ "Tab 0 layout: {} bytes, metadata: {:?}",
+ kdl.len(),
+ metadata
+ ))
+ },
+ Err(e) => {
+ self.explicit_string_to_render =
+ Some(format!("Tab layout error: {}", e))
+ },
+ }
+ },
+ BareKey::Char('h')
+ if key.has_only_modifiers(&[KeyModifier::Ctrl, KeyModifier::Shift]) =>
+ {
+ // Test parse_layout() - valid KDL
+ let valid_kdl = "layout { tab { pane; }; }";
+ match parse_layout(valid_kdl) {
+ Ok(metadata) => {
+ if metadata.tabs.len() == 1 {
+ self.explicit_string_to_render = Some(format!("Parse success"));
+ } else {
+ self.explicit_string_to_render =
+ Some(format!("Parse failure: got wrong metadata"));
+ }
+ },
+ Err(e) => {
+ self.explicit_string_to_render = Some(format!("Parse error: {:?}", e))
+ },
+ }
+ },
+ BareKey::Char('i')
+ if key.has_only_modifiers(&[KeyModifier::Ctrl, KeyModifier::Shift]) =>
+ {
+ // Test parse_layout() - invalid KDL
+ let invalid_kdl = "layout { this is not valid kdl }";
+ match parse_layout(invalid_kdl) {
+ Ok(metadata) => {
+ self.explicit_string_to_render =
+ Some(format!("Parse success: {:?}", metadata))
+ },
+ Err(e) => {
+ self.explicit_string_to_render = Some(format!("Parse error: {:?}", e))
+ },
+ }
+ },
+ BareKey::Char('a')
+ if key.has_only_modifiers(&[KeyModifier::Ctrl, KeyModifier::Alt]) =>
+ {
+ let test_kdl = "layout { tab { pane; }; }";
+ match save_layout("test_layout", test_kdl, false) {
+ Ok(_) => {
+ self.explicit_string_to_render = Some(format!("Save layout success"))
+ },
+ Err(e) => {
+ self.explicit_string_to_render =
+ Some(format!("Save layout error: {}", e))
+ },
+ }
+ },
+ BareKey::Char('b')
+ if key.has_only_modifiers(&[KeyModifier::Ctrl, KeyModifier::Alt]) =>
+ {
+ let test_kdl = "layout { tab { pane; }; }";
+ match save_layout("test_layout", test_kdl, false) {
+ Ok(_) => {
+ self.explicit_string_to_render = Some(format!("Save layout success"))
+ },
+ Err(e) => {
+ self.explicit_string_to_render =
+ Some(format!("Save layout error: {}", e))
+ },
+ }
+ },
+ BareKey::Char('c')
+ if key.has_only_modifiers(&[KeyModifier::Ctrl, KeyModifier::Alt]) =>
+ {
+ let test_kdl = "layout { tab { pane; pane; }; }";
+ match save_layout("test_layout2", test_kdl, true) {
+ Ok(_) => {
+ self.explicit_string_to_render =
+ Some(format!("Save layout with overwrite success"))
+ },
+ Err(e) => {
+ self.explicit_string_to_render =
+ Some(format!("Save layout with overwrite error: {}", e))
+ },
+ }
+ },
+ BareKey::Char('d')
+ if key.has_only_modifiers(&[KeyModifier::Ctrl, KeyModifier::Alt]) =>
+ {
+ // Test save_layout() - invalid KDL
+ let invalid_kdl = "not valid kdl at all";
+ match save_layout("invalid_layout", invalid_kdl, false) {
+ Ok(_) => {
+ self.explicit_string_to_render =
+ Some(format!("Save invalid layout success"))
+ },
+ Err(e) => {
+ self.explicit_string_to_render =
+ Some(format!("Save invalid layout error: {}", e))
+ },
+ }
+ },
+ BareKey::Char('e')
+ if key.has_only_modifiers(&[KeyModifier::Ctrl, KeyModifier::Alt]) =>
+ {
+ match rename_layout("test_layout", "renamed_layout") {
+ Ok(_) => {
+ self.explicit_string_to_render = Some(format!("Rename layout success"))
+ },
+ Err(e) => {
+ self.explicit_string_to_render =
+ Some(format!("Rename layout error: {}", e))
+ },
+ }
+ },
+ BareKey::Char('f')
+ if key.has_only_modifiers(&[KeyModifier::Ctrl, KeyModifier::Alt]) =>
+ {
+ // Test rename_layout() - not found case
+ match rename_layout("nonexistent_layout", "new_name") {
+ Ok(_) => {
+ self.explicit_string_to_render =
+ Some(format!("Rename nonexistent layout success"))
+ },
+ Err(e) => {
+ self.explicit_string_to_render =
+ Some(format!("Rename nonexistent layout error: {}", e))
+ },
+ }
+ },
+ BareKey::Char('g')
+ if key.has_only_modifiers(&[KeyModifier::Ctrl, KeyModifier::Alt]) =>
+ {
+ // Test delete_layout() - success case
+ match delete_layout("renamed_layout") {
+ Ok(_) => {
+ self.explicit_string_to_render = Some(format!("Delete layout success"))
+ },
+ Err(e) => {
+ self.explicit_string_to_render =
+ Some(format!("Delete layout error: {}", e))
+ },
+ }
+ },
+ BareKey::Char('h')
+ if key.has_only_modifiers(&[KeyModifier::Ctrl, KeyModifier::Alt]) =>
+ {
+ // Test delete_layout() - not found case
+ match delete_layout("nonexistent_layout") {
+ Ok(_) => {
+ self.explicit_string_to_render =
+ Some(format!("Delete nonexistent layout success"))
+ },
+ Err(e) => {
+ self.explicit_string_to_render =
+ Some(format!("Delete nonexistent layout error: {}", e))
+ },
+ }
+ },
+ BareKey::Char('i')
+ if key.has_only_modifiers(&[KeyModifier::Ctrl, KeyModifier::Alt]) =>
+ {
+ let test_kdl = "layout { tab { pane; } }";
+ match save_layout("../evil_layout", test_kdl, false) {
+ Ok(_) => {
+ self.explicit_string_to_render =
+ Some(format!("Path traversal save success"))
+ },
+ Err(e) => {
+ self.explicit_string_to_render =
+ Some(format!("Path traversal save error: {}", e))
+ },
+ }
+ },
+ BareKey::Char('j')
+ if key.has_only_modifiers(&[KeyModifier::Ctrl, KeyModifier::Alt]) =>
+ {
+ // Test edit_layout()
+ let mut context = BTreeMap::new();
+ context.insert("test_key".to_owned(), "test_value".to_owned());
+ match edit_layout("test_layout2", context) {
+ Ok(_) => {
+ self.explicit_string_to_render =
+ Some(format!("Edit layout command sent"))
+ },
+ Err(e) => {
+ self.explicit_string_to_render =
+ Some(format!("Edit layout error: {}", e))
+ },
+ }
+ },
+ BareKey::Char('k')
+ if key.has_only_modifiers(&[KeyModifier::Ctrl, KeyModifier::Alt]) =>
+ {
+ // Test override_layout()
+ let mut context = BTreeMap::new();
+ context.insert("override_test".to_owned(), "value".to_owned());
+ override_layout(
+ &LayoutInfo::BuiltIn("compact".to_owned()),
+ false, // retain_existing_terminal_panes
+ false, // retain_existing_plugin_panes
+ false, // apply_only_to_active_tab
+ context,
+ );
+ self.explicit_string_to_render = Some(format!("Override layout command sent"));
+ },
_ => {},
},
Event::CustomMessage(message, payload) => {
@@ -640,6 +941,8 @@ impl ZellijPlugin for State {
println!("Payload from worker: {:?}", payload);
} else if let Some(payload) = self.message_to_plugin_payload.take() {
println!("Payload from self: {:?}", payload);
+ } else if let Some(explicit_string_to_render) = self.explicit_string_to_render.take() {
+ println!("{}", explicit_string_to_render.trim());
} else {
println!(
"Rows: {:?}, Cols: {:?}, Received events: {:?}",
diff --git a/default-plugins/layout-manager/.cargo/config.toml b/default-plugins/layout-manager/.cargo/config.toml
new file mode 100644
index 00000000..6b509f5b
--- /dev/null
+++ b/default-plugins/layout-manager/.cargo/config.toml
@@ -0,0 +1,2 @@
+[build]
+target = "wasm32-wasip1"
diff --git a/default-plugins/layout-manager/.gitignore b/default-plugins/layout-manager/.gitignore
new file mode 100644
index 00000000..ea8c4bf7
--- /dev/null
+++ b/default-plugins/layout-manager/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/default-plugins/layout-manager/Cargo.lock b/default-plugins/layout-manager/Cargo.lock
new file mode 100644
index 00000000..73fed400
--- /dev/null
+++ b/default-plugins/layout-manager/Cargo.lock
@@ -0,0 +1,3020 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "addr2line"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "ahash"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
+dependencies = [
+ "getrandom 0.2.10",
+ "once_cell",
+ "version_check",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.71"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
+dependencies = [
+ "backtrace",
+]
+
+[[package]]
+name = "arc-swap"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6"
+
+[[package]]
+name = "arrayvec"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
+
+[[package]]
+name = "async-attributes"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5"
+dependencies = [
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "async-channel"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833"
+dependencies = [
+ "concurrent-queue",
+ "event-listener",
+ "futures-core",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb"
+dependencies = [
+ "async-lock",
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-global-executor"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776"
+dependencies = [
+ "async-channel",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "blocking",
+ "futures-lite",
+ "once_cell",
+]
+
+[[package]]
+name = "async-io"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af"
+dependencies = [
+ "async-lock",
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-lite",
+ "log",
+ "parking",
+ "polling",
+ "rustix",
+ "slab",
+ "socket2 0.4.9",
+ "waker-fn",
+]
+
+[[package]]
+name = "async-lock"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7"
+dependencies = [
+ "event-listener",
+]
+
+[[package]]
+name = "async-process"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "autocfg",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix",
+ "signal-hook",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "async-std"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d"
+dependencies = [
+ "async-attributes",
+ "async-channel",
+ "async-global-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "crossbeam-utils",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-lite",
+ "gloo-timers",
+ "kv-log-macro",
+ "log",
+ "memchr",
+ "once_cell",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
+name = "async-task"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae"
+
+[[package]]
+name = "atomic"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3"
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi 0.1.19",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+
+[[package]]
+name = "backtrace"
+version = "0.3.67"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "backtrace-ext"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50"
+dependencies = [
+ "backtrace",
+]
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bit-set"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "blocking"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65"
+dependencies = [
+ "async-channel",
+ "async-lock",
+ "async-task",
+ "atomic-waker",
+ "fastrand",
+ "futures-lite",
+ "log",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
+
+[[package]]
+name = "bytemuck"
+version = "1.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
+
+[[package]]
+name = "bytes"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
+
+[[package]]
+name = "castaway"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6"
+
+[[package]]
+name = "cc"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chrono"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
+dependencies = [
+ "libc",
+ "num-integer",
+ "num-traits",
+ "time",
+ "winapi",
+]
+
+[[package]]
+name = "clap"
+version = "3.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
+dependencies = [
+ "atty",
+ "bitflags 1.3.2",
+ "clap_derive",
+ "clap_lex",
+ "indexmap",
+ "once_cell",
+ "strsim 0.10.0",
+ "termcolor",
+ "textwrap 0.16.0",
+]
+
+[[package]]
+name = "clap_complete"
+version = "3.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f7a2e0a962c45ce25afce14220bc24f9dade0a1787f185cecf96bfba7847cd8"
+dependencies = [
+ "clap",
+]
+
+[[package]]
+name = "clap_derive"
+version = "3.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
+dependencies = [
+ "os_str_bytes",
+]
+
+[[package]]
+name = "colored"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd"
+dependencies = [
+ "atty",
+ "lazy_static",
+ "winapi",
+]
+
+[[package]]
+name = "colorsys"
+version = "0.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54261aba646433cb567ec89844be4c4825ca92a4f8afba52fc4dd88436e31bbd"
+
+[[package]]
+name = "concurrent-queue"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crossbeam"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c"
+dependencies = [
+ "cfg-if",
+ "crossbeam-channel",
+ "crossbeam-deque",
+ "crossbeam-epoch",
+ "crossbeam-queue",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
+dependencies = [
+ "cfg-if",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "crossbeam-utils",
+ "memoffset 0.9.0",
+ "scopeguard",
+]
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "csscolorparser"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf"
+dependencies = [
+ "lab",
+ "phf",
+]
+
+[[package]]
+name = "curl"
+version = "0.4.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e2161dd6eba090ff1594084e95fd67aeccf04382ffea77999ea94ed42ec67b6"
+dependencies = [
+ "curl-sys",
+ "libc",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "socket2 0.5.10",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "curl-sys"
+version = "0.4.72+curl-8.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29cbdc8314c447d11e8fd156dcdd031d9e02a7a976163e396b548c03153bc9ea"
+dependencies = [
+ "cc",
+ "libc",
+ "libnghttp2-sys",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "deltae"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ef311e2c0a388b9ba56c839ac68d33b92d18ce7104d0acc4375cb479e2b9a53"
+
+[[package]]
+name = "derivative"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "directories"
+version = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs"
+version = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "either"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "euclid"
+version = "0.22.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b52c2ef4a78da0ba68fbe1fd920627411096d2ac478f7f4c9f3a54ba6705bade"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "event-listener"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+
+[[package]]
+name = "fancy-regex"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2"
+dependencies = [
+ "bit-set",
+ "regex",
+]
+
+[[package]]
+name = "fastrand"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "filedescriptor"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d"
+dependencies = [
+ "libc",
+ "thiserror 1.0.40",
+ "winapi",
+]
+
+[[package]]
+name = "filetime"
+version = "0.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "libredox",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "finl_unicode"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6"
+
+[[package]]
+name = "fixedbitset"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
+
+[[package]]
+name = "futures-io"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
+
+[[package]]
+name = "futures-lite"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "memchr",
+ "parking",
+ "pin-project-lite",
+ "waker-fn",
+]
+
+[[package]]
+name = "fuzzy-matcher"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
+dependencies = [
+ "thread_local",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasi 0.14.2+wasi-0.2.4",
+]
+
+[[package]]
+name = "gimli"
+version = "0.27.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4"
+
+[[package]]
+name = "gloo-timers"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+dependencies = [
+ "ahash",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "http"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa 1.0.11",
+]
+
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
+name = "idna"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "include_dir"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18762faeff7122e89e0857b02f7ce6fcc0d101d5e9ad2ad7846cc01d61b7f19e"
+dependencies = [
+ "include_dir_macros",
+]
+
+[[package]]
+name = "include_dir_macros"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "inotify"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
+dependencies = [
+ "bitflags 1.3.2",
+ "inotify-sys",
+ "libc",
+]
+
+[[package]]
+name = "inotify-sys"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "interprocess"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81f2533f3be42fffe3b5e63b71aeca416c1c3bc33e4e27be018521e76b1f38fb"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "rustc_version",
+ "to_method",
+ "winapi",
+]
+
+[[package]]
+name = "io-lifetimes"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
+dependencies = [
+ "hermit-abi 0.3.1",
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "is-terminal"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
+dependencies = [
+ "hermit-abi 0.3.1",
+ "io-lifetimes",
+ "rustix",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "is_ci"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb"
+
+[[package]]
+name = "isahc"
+version = "1.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9"
+dependencies = [
+ "async-channel",
+ "castaway",
+ "crossbeam-utils",
+ "curl",
+ "curl-sys",
+ "encoding_rs",
+ "event-listener",
+ "futures-lite",
+ "http",
+ "log",
+ "mime",
+ "once_cell",
+ "polling",
+ "slab",
+ "sluice",
+ "tracing",
+ "tracing-futures",
+ "url",
+ "waker-fn",
+]
+
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
+
+[[package]]
+name = "itoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+
+[[package]]
+name = "js-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "kdl"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "062c875482ccb676fd40c804a40e3824d4464c18c364547456d1c8e8e951ae47"
+dependencies = [
+ "miette",
+ "nom",
+ "thiserror 1.0.40",
+]
+
+[[package]]
+name = "kqueue"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c8fc60ba15bf51257aa9807a48a61013db043fcf3a78cb0d916e8e396dcad98"
+dependencies = [
+ "kqueue-sys",
+ "libc",
+]
+
+[[package]]
+name = "kqueue-sys"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587"
+dependencies = [
+ "bitflags 1.3.2",
+ "libc",
+]
+
+[[package]]
+name = "kv-log-macro"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "lab"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f"
+
+[[package]]
+name = "layout-manager"
+version = "0.1.0"
+dependencies = [
+ "fuzzy-matcher",
+ "miette",
+ "strip-ansi-escapes 0.2.1",
+ "unicode-width",
+ "zellij-tile",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "lev_distance"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72d9d1bd215936bc8647ad92986bb56f3f216550b53c44ab785e3217ae33625e"
+
+[[package]]
+name = "libc"
+version = "0.2.174"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
+
+[[package]]
+name = "libnghttp2-sys"
+version = "0.1.10+1.61.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "959c25552127d2e1fa72f0e52548ec04fc386e827ba71a7bd01db46a447dc135"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "libredox"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
+dependencies = [
+ "bitflags 2.6.0",
+ "libc",
+ "redox_syscall 0.5.13",
+]
+
+[[package]]
+name = "libz-sys"
+version = "1.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
+
+[[package]]
+name = "lock_api"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
+dependencies = [
+ "value-bag",
+]
+
+[[package]]
+name = "log-mdc"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7"
+
+[[package]]
+name = "log4rs"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d36ca1786d9e79b8193a68d480a0907b612f109537115c6ff655a3a1967533fd"
+dependencies = [
+ "anyhow",
+ "arc-swap",
+ "chrono",
+ "derivative",
+ "fnv",
+ "log",
+ "log-mdc",
+ "parking_lot",
+ "thiserror 1.0.40",
+ "thread-id",
+]
+
+[[package]]
+name = "mac_address"
+version = "1.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303"
+dependencies = [
+ "nix 0.29.0",
+ "winapi",
+]
+
+[[package]]
+name = "memchr"
+version = "2.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+
+[[package]]
+name = "memmem"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15"
+
+[[package]]
+name = "memoffset"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "memoffset"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "miette"
+version = "5.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a236ff270093b0b67451bc50a509bd1bad302cb1d3c7d37d5efe931238581fa9"
+dependencies = [
+ "backtrace",
+ "backtrace-ext",
+ "is-terminal",
+ "miette-derive",
+ "once_cell",
+ "owo-colors",
+ "supports-color",
+ "supports-hyperlinks",
+ "supports-unicode",
+ "terminal_size",
+ "textwrap 0.15.2",
+ "thiserror 1.0.40",
+ "unicode-width",
+]
+
+[[package]]
+name = "miette-derive"
+version = "5.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4901771e1d44ddb37964565c654a3223ba41a594d02b8da471cc4464912b5cfa"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
+dependencies = [
+ "libc",
+ "log",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "mio"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
+dependencies = [
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "names"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bddcd3bf5144b6392de80e04c347cd7fab2508f6df16a85fc496ecd5cec39bc"
+dependencies = [
+ "rand",
+]
+
+[[package]]
+name = "nix"
+version = "0.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c"
+dependencies = [
+ "bitflags 1.3.2",
+ "cc",
+ "cfg-if",
+ "libc",
+ "memoffset 0.6.5",
+]
+
+[[package]]
+name = "nix"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
+dependencies = [
+ "bitflags 2.6.0",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+ "memoffset 0.9.0",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "notify"
+version = "6.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
+dependencies = [
+ "bitflags 2.6.0",
+ "filetime",
+ "inotify",
+ "kqueue",
+ "libc",
+ "log",
+ "mio 0.8.8",
+ "walkdir",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "num-derive"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "object"
+version = "0.30.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03b4680b86d9cfafba8fc491dc9b6df26b68cf40e9e6cd73909194759a63c385"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "ordered-float"
+version = "4.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c65ee1f9701bf938026630b455d5315f490640234259037edb259798b3bcf85e"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "os_str_bytes"
+version = "6.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac"
+
+[[package]]
+name = "owo-colors"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
+
+[[package]]
+name = "parking"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall 0.3.5",
+ "smallvec",
+ "windows-targets 0.48.0",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
+
+[[package]]
+name = "pest"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323"
+dependencies = [
+ "memchr",
+ "thiserror 2.0.12",
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5"
+dependencies = [
+ "pest",
+ "sha2",
+]
+
+[[package]]
+name = "phf"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c"
+dependencies = [
+ "phf_macros",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf"
+dependencies = [
+ "phf_shared",
+ "rand",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92aacdc5f16768709a569e913f7451034034178b05bdc8acda226659a3dccc66"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676"
+dependencies = [
+ "siphasher 0.3.10",
+]
+
+[[package]]
+name = "pin-project"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
+
+[[package]]
+name = "polling"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
+dependencies = [
+ "autocfg",
+ "bitflags 1.3.2",
+ "cfg-if",
+ "concurrent-queue",
+ "libc",
+ "log",
+ "pin-project-lite",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "prost"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd"
+dependencies = [
+ "bytes",
+ "prost-derive",
+]
+
+[[package]]
+name = "prost-derive"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4"
+dependencies = [
+ "anyhow",
+ "itertools",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.10",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6"
+dependencies = [
+ "bitflags 2.6.0",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
+dependencies = [
+ "getrandom 0.2.10",
+ "redox_syscall 0.2.16",
+ "thiserror 1.0.40",
+]
+
+[[package]]
+name = "regex"
+version = "1.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "0.37.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0"
+dependencies = [
+ "bitflags 1.3.2",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
+
+[[package]]
+name = "ryu"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "semver"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
+
+[[package]]
+name = "serde"
+version = "1.0.164"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.164"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
+dependencies = [
+ "itoa 0.4.7",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "shellexpand"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b"
+dependencies = [
+ "dirs",
+]
+
+[[package]]
+name = "signal-hook"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "siphasher"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
+
+[[package]]
+name = "siphasher"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
+
+[[package]]
+name = "slab"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "sluice"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5"
+dependencies = [
+ "async-channel",
+ "futures-core",
+ "futures-io",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
+
+[[package]]
+name = "smawk"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
+
+[[package]]
+name = "socket2"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "socket2"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "strip-ansi-escapes"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "011cbb39cf7c1f62871aea3cc46e5817b0937b49e9447370c93cacbe93a766d8"
+dependencies = [
+ "vte 0.10.1",
+]
+
+[[package]]
+name = "strip-ansi-escapes"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025"
+dependencies = [
+ "vte 0.14.1",
+]
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "strum"
+version = "0.27.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
+
+[[package]]
+name = "strum_macros"
+version = "0.27.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "suggest"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2558d0d6327d908558f161bc862d365c0d33aa796bdc81c13c0f954868576659"
+dependencies = [
+ "clap",
+ "lev_distance",
+]
+
+[[package]]
+name = "supports-color"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4950e7174bffabe99455511c39707310e7e9b440364a2fcb1cc21521be57b354"
+dependencies = [
+ "is-terminal",
+ "is_ci",
+]
+
+[[package]]
+name = "supports-hyperlinks"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f84231692eb0d4d41e4cdd0cabfdd2e6cd9e255e65f80c9aa7c98dd502b4233d"
+dependencies = [
+ "is-terminal",
+]
+
+[[package]]
+name = "supports-unicode"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b6c2cb240ab5dd21ed4906895ee23fe5a48acdbd15a3ce388e7b62a9b66baf7"
+dependencies = [
+ "is-terminal",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "fastrand",
+ "redox_syscall 0.3.5",
+ "rustix",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "terminal_size"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "terminfo"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662"
+dependencies = [
+ "fnv",
+ "nom",
+ "phf",
+ "phf_codegen",
+]
+
+[[package]]
+name = "termios"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "termwiz"
+version = "0.23.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7"
+dependencies = [
+ "anyhow",
+ "base64",
+ "bitflags 2.6.0",
+ "fancy-regex",
+ "filedescriptor",
+ "finl_unicode",
+ "fixedbitset",
+ "hex",
+ "lazy_static",
+ "libc",
+ "log",
+ "memmem",
+ "nix 0.29.0",
+ "num-derive",
+ "num-traits",
+ "ordered-float",
+ "pest",
+ "pest_derive",
+ "phf",
+ "sha2",
+ "signal-hook",
+ "siphasher 1.0.1",
+ "terminfo",
+ "termios",
+ "thiserror 1.0.40",
+ "ucd-trie",
+ "unicode-segmentation",
+ "vtparse",
+ "wezterm-bidi",
+ "wezterm-blob-leases",
+ "wezterm-color-types",
+ "wezterm-dynamic",
+ "wezterm-input-types",
+ "winapi",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d"
+dependencies = [
+ "smawk",
+ "unicode-linebreak",
+ "unicode-width",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
+
+[[package]]
+name = "thiserror"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
+dependencies = [
+ "thiserror-impl 1.0.40",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
+dependencies = [
+ "thiserror-impl 2.0.12",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "thread-id"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ee93aa2b8331c0fec9091548843f2c90019571814057da3b783f9de09349d73"
+dependencies = [
+ "libc",
+ "redox_syscall 0.2.16",
+ "winapi",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "time"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
+dependencies = [
+ "libc",
+ "wasi 0.10.0+wasi-snapshot-preview1",
+ "winapi",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "to_method"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8"
+
+[[package]]
+name = "tokio"
+version = "1.45.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio 1.0.4",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2 0.5.10",
+ "tokio-macros",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "tracing-futures"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
+dependencies = [
+ "pin-project",
+ "tracing",
+]
+
+[[package]]
+name = "typenum"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0"
+
+[[package]]
+name = "unicode-linebreak"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
+dependencies = [
+ "hashbrown",
+ "regex",
+]
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+
+[[package]]
+name = "url"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
+[[package]]
+name = "uuid"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
+dependencies = [
+ "atomic",
+ "getrandom 0.3.3",
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "value-bag"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4d330786735ea358f3bc09eea4caa098569c1c93f342d9aca0514915022fe7e"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "vte"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983"
+dependencies = [
+ "arrayvec",
+ "utf8parse",
+ "vte_generate_state_changes",
+]
+
+[[package]]
+name = "vte"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "vte_generate_state_changes"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff"
+dependencies = [
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "vtparse"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "waker-fn"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
+
+[[package]]
+name = "walkdir"
+version = "2.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.10.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasi"
+version = "0.14.2+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+dependencies = [
+ "wit-bindgen-rt",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wezterm-bidi"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec"
+dependencies = [
+ "log",
+ "wezterm-dynamic",
+]
+
+[[package]]
+name = "wezterm-blob-leases"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7"
+dependencies = [
+ "getrandom 0.3.3",
+ "mac_address",
+ "sha2",
+ "thiserror 1.0.40",
+ "uuid",
+]
+
+[[package]]
+name = "wezterm-color-types"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296"
+dependencies = [
+ "csscolorparser",
+ "deltae",
+ "lazy_static",
+ "wezterm-dynamic",
+]
+
+[[package]]
+name = "wezterm-dynamic"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac"
+dependencies = [
+ "log",
+ "ordered-float",
+ "strsim 0.11.1",
+ "thiserror 1.0.40",
+ "wezterm-dynamic-derive",
+]
+
+[[package]]
+name = "wezterm-dynamic-derive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "wezterm-input-types"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e"
+dependencies = [
+ "bitflags 1.3.2",
+ "euclid",
+ "lazy_static",
+ "serde",
+ "wezterm-dynamic",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.0",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.0",
+ "windows_aarch64_msvc 0.48.0",
+ "windows_i686_gnu 0.48.0",
+ "windows_i686_msvc 0.48.0",
+ "windows_x86_64_gnu 0.48.0",
+ "windows_x86_64_gnullvm 0.48.0",
+ "windows_x86_64_msvc 0.48.0",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
+dependencies = [
+ "bitflags 2.6.0",
+]
+
+[[package]]
+name = "zellij-tile"
+version = "0.44.0"
+dependencies = [
+ "clap",
+ "prost",
+ "serde",
+ "serde_json",
+ "strum",
+ "strum_macros",
+ "zellij-utils",
+]
+
+[[package]]
+name = "zellij-utils"
+version = "0.44.0"
+dependencies = [
+ "anyhow",
+ "async-std",
+ "backtrace",
+ "bitflags 2.6.0",
+ "clap",
+ "clap_complete",
+ "colored",
+ "colorsys",
+ "crossbeam",
+ "directories",
+ "humantime",
+ "include_dir",
+ "interprocess",
+ "isahc",
+ "kdl",
+ "lazy_static",
+ "log",
+ "log4rs",
+ "miette",
+ "names",
+ "nix 0.23.2",
+ "notify",
+ "percent-encoding",
+ "prost",
+ "serde",
+ "serde_json",
+ "sha2",
+ "shellexpand",
+ "strip-ansi-escapes 0.1.1",
+ "strum",
+ "strum_macros",
+ "suggest",
+ "tempfile",
+ "termwiz",
+ "thiserror 1.0.40",
+ "tokio",
+ "unicode-width",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
diff --git a/default-plugins/layout-manager/Cargo.toml b/default-plugins/layout-manager/Cargo.toml
new file mode 100644
index 00000000..992626ac
--- /dev/null
+++ b/default-plugins/layout-manager/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "layout-manager"
+version = "0.1.0"
+authors = ["Aram Drevekenin <aram@poor.dev>"]
+edition = "2018"
+
+[dependencies]
+zellij-tile = { path = "../../zellij-tile" }
+miette = { version = "5.7.0", features = ["fancy"] }
+strip-ansi-escapes = "0.2"
+unicode-width = "0.1"
+fuzzy-matcher = "0.3"
diff --git a/default-plugins/layout-manager/LICENSE.md b/default-plugins/layout-manager/LICENSE.md
new file mode 120000
index 00000000..f0608a63
--- /dev/null
+++ b/default-plugins/layout-manager/LICENSE.md
@@ -0,0 +1 @@
+../../LICENSE.md \ No newline at end of file
diff --git a/default-plugins/layout-manager/README.md b/default-plugins/layout-manager/README.md
new file mode 100644
index 00000000..daa5e816
--- /dev/null
+++ b/default-plugins/layout-manager/README.md
@@ -0,0 +1,27 @@
+## About
+
+This is an example [Zellij][zellij] plugin in Rust. It can be used as a template to start developing your own plugins.
+
+More about Zellij plugins: [Zellij Documentation][docs]
+
+[zellij]: https://github.com/zellij-org/zellij
+[docs]: https://zellij.dev/documentation/plugins.html
+
+## Development
+
+*Note*: you will need to have `wasm32-wasi` added to rust as a target to build the plugin. This can be done with `rustup target add wasm32-wasi`.
+
+### With the Provided Layout
+
+![img-2024-11-14-100111](https://github.com/user-attachments/assets/e3bae15c-1f94-4d4a-acea-a036f8afdf67)
+
+
+Run `zellij -l zellij.kdl` at the root of this repository. This will open a development environment that will help you develop the plugin inside Zellij.
+
+It can also be used if you prefer developing outside of the terminal - in this case you should ignore the `$EDITOR` pane and use your IDE instead.
+
+### Otherwise
+
+1. Build the project: `cargo build`
+2. Load it inside a running Zellij session: `zellij action start-or-reload-plugin file:target/wasm32-wasi/debug/rust-plugin-example.wasm`
+3. Repeat on changes (perhaps with a `watchexec` or similar command to run on fs changes).
diff --git a/default-plugins/layout-manager/src/errors.rs b/default-plugins/layout-manager/src/errors.rs
new file mode 100644
index 00000000..a86af8bf
--- /dev/null
+++ b/default-plugins/layout-manager/src/errors.rs
@@ -0,0 +1,155 @@
+use crate::screens::{KeyResponse, Screen};
+use crate::ui::{truncate_line_with_ansi, wrap_text_to_width, ErrorMessage, MultiLineErrorMessage};
+use zellij_tile::prelude::*;
+
+/// Format a layout parsing error into a detailed error string
+pub fn format_kdl_error(error: LayoutParsingError) -> String {
+ match error {
+ LayoutParsingError::KdlError {
+ mut kdl_error,
+ file_name,
+ source_code,
+ } => {
+ use miette::{GraphicalReportHandler, NamedSource, Report};
+
+ kdl_error.help_message =
+ Some("https://zellij.dev/documentation/creating-a-layout.html".to_owned());
+ let report: Report = kdl_error.into();
+ let report = report.with_source_code(NamedSource::new(file_name, source_code));
+
+ let handler = GraphicalReportHandler::new();
+ let mut output = String::new();
+ handler.render_report(&mut output, report.as_ref()).unwrap();
+ output
+ },
+ LayoutParsingError::SyntaxError => {
+ format!("Failed to deserialize KDL node. \nPossible reasons:\n{}\n{}\n{}\n{}",
+ "- Missing `;` after a node name, eg. { node; another_node; }",
+ "- Missing quotations (\") around an argument node eg. { first_node \"argument_node\"; }",
+ "- Missing an equal sign (=) between node arguments on a title line. eg. argument=\"value\"",
+ "- Found an extraneous equal sign (=) between node child arguments and their values. eg. { argument=\"value\" }")
+ },
+ }
+}
+
+#[derive(Clone)]
+pub struct ErrorScreen {
+ pub message: String,
+ pub return_to_screen: Box<Screen>,
+}
+
+impl ErrorScreen {
+ pub fn handle_key(&mut self, _key: KeyWithModifier) -> KeyResponse {
+ KeyResponse::new_screen((*self.return_to_screen).clone())
+ }
+
+ pub fn render(&self, rows: usize, cols: usize) {
+ if self.message.chars().count() > cols.saturating_sub(4) {
+ let max_width = cols.saturating_sub(4);
+ let max_rows = rows.saturating_sub(4);
+ let lines = wrap_text_to_width(&self.message, max_width);
+ let base_x = 2;
+ let base_y = rows.saturating_sub(4 + lines.len()) / 2;
+ MultiLineErrorMessage::new(lines).render(base_x, base_y, max_rows);
+ } else {
+ let desired_width = self.message.chars().count();
+ let base_y = rows.saturating_sub(5) / 2;
+ let base_x = cols.saturating_sub(desired_width) / 2;
+ ErrorMessage::new(&self.message).render(base_x, base_y);
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct ErrorDetailScreen {
+ pub layout_name: String,
+ pub detailed_error: String,
+ pub return_to_screen: Box<Screen>,
+}
+
+impl ErrorDetailScreen {
+ pub fn new(layout_name: String, detailed_error: String, return_to_screen: Box<Screen>) -> Self {
+ Self {
+ layout_name,
+ detailed_error,
+ return_to_screen,
+ }
+ }
+
+ pub fn handle_key(&mut self, _key: KeyWithModifier) -> KeyResponse {
+ KeyResponse::new_screen((*self.return_to_screen).clone())
+ }
+
+ pub fn render(&self, rows: usize, cols: usize) {
+ // Header: show layout name
+ let header = format!("Error in layout: {}", self.layout_name);
+ let header_text = Text::new(&header).error_color_all();
+ print_text_with_coordinates(header_text, 1, 0, None, None);
+
+ // Calculate available space for error content
+ let header_height = 2; // Header + gap
+ let available_rows = rows.saturating_sub(header_height);
+ let available_cols = cols.saturating_sub(2); // Padding on sides
+
+ // Render error lines with middle truncation
+ let error_lines: Vec<&str> = self.detailed_error.lines().collect();
+ let total_lines = error_lines.len();
+
+ if total_lines <= available_rows {
+ // All lines fit, show them all
+ for (i, line) in error_lines.iter().enumerate() {
+ let truncated = truncate_line_with_ansi(line, available_cols);
+ print!("\u{1b}[{};{}H{}", header_height + i + 1, 2, truncated);
+ }
+ } else {
+ // Need to truncate middle - show beginning, indicator, and end
+ let omitted_indicator_lines = 1; // Reserve 1 line for "... X lines omitted ..."
+ let lines_for_content = available_rows.saturating_sub(omitted_indicator_lines);
+
+ // Split content space: 60% for beginning, 40% for end
+ let beginning_lines = (lines_for_content as f32 * 0.6).ceil() as usize;
+ let end_lines = lines_for_content.saturating_sub(beginning_lines);
+
+ let omitted_count = total_lines.saturating_sub(beginning_lines + end_lines);
+
+ let mut current_row = 0;
+
+ // Render beginning lines
+ for line in error_lines.iter().take(beginning_lines) {
+ let truncated = truncate_line_with_ansi(line, available_cols);
+ print!(
+ "\u{1b}[{};{}H{}",
+ header_height + current_row + 1,
+ 2,
+ truncated
+ );
+ current_row += 1;
+ }
+
+ // Render omission indicator
+ let indicator = format!("... {} lines omitted ...", omitted_count);
+ let indicator_text = Text::new(&indicator).color_range(0, ..);
+ print_text_with_coordinates(
+ indicator_text,
+ (cols.saturating_sub(indicator.chars().count())) / 2,
+ header_height + current_row,
+ None,
+ None,
+ );
+ current_row += 1;
+
+ // Render end lines
+ let start_index = total_lines.saturating_sub(end_lines);
+ for line in error_lines.iter().skip(start_index) {
+ let truncated = truncate_line_with_ansi(line, available_cols);
+ print!(
+ "\u{1b}[{};{}H{}",
+ header_height + current_row + 1,
+ 2,
+ truncated
+ );
+ current_row += 1;
+ }
+ }
+ }
+}
diff --git a/default-plugins/layout-manager/src/main.rs b/default-plugins/layout-manager/src/main.rs
new file mode 100644
index 00000000..0966f4ec
--- /dev/null
+++ b/default-plugins/layout-manager/src/main.rs
@@ -0,0 +1,295 @@
+mod errors;
+mod screens;
+mod text_input;
+mod ui;
+
+use errors::format_kdl_error;
+use screens::{KeyResponse, OptimisticUpdate, Screen};
+use std::collections::BTreeMap;
+use ui::{get_last_modified_string, get_layout_display_info};
+use zellij_tile::prelude::*;
+
+#[derive(Clone)]
+pub enum DisplayLayout {
+ Valid(LayoutInfo),
+ Error {
+ name: String,
+ error: String,
+ error_message: String,
+ },
+}
+
+impl DisplayLayout {
+ fn name(&self) -> String {
+ match self {
+ DisplayLayout::Valid(info) => match info {
+ LayoutInfo::BuiltIn(name) => name.clone(),
+ LayoutInfo::File(path, _) => path.split('/').last().unwrap_or(path).to_string(),
+ LayoutInfo::Url(url) => url.split('/').last().unwrap_or(url).to_string(),
+ LayoutInfo::Stringified(_) => "raw".to_string(),
+ },
+ DisplayLayout::Error { name, .. } => name.clone(),
+ }
+ }
+ fn file_name(&self) -> Option<String> {
+ match self {
+ DisplayLayout::Valid(LayoutInfo::File(file_name, _)) => Some(file_name.clone()),
+ DisplayLayout::Error { name, .. } => Some(name.clone()),
+ _ => None,
+ }
+ }
+ fn is_error(&self) -> bool {
+ match self {
+ DisplayLayout::Error { .. } => true,
+ _ => false,
+ }
+ }
+ fn is_builtin(&self) -> bool {
+ matches!(self, DisplayLayout::Valid(LayoutInfo::BuiltIn(..)))
+ }
+
+ fn builtin_sort_priority(&self) -> usize {
+ let name = self.name().to_lowercase();
+ match name.as_str() {
+ "default" => 0,
+ "compact" => 1,
+ _ => 2,
+ }
+ }
+
+ fn get_update_time(&self) -> Option<i64> {
+ match self {
+ DisplayLayout::Valid(LayoutInfo::File(_, metadata)) => {
+ metadata.update_time.parse::<i64>().ok()
+ },
+ _ => None,
+ }
+ }
+
+ fn compare_builtin_layouts(&self, other: &DisplayLayout) -> std::cmp::Ordering {
+ match self
+ .builtin_sort_priority()
+ .cmp(&other.builtin_sort_priority())
+ {
+ std::cmp::Ordering::Equal => {
+ self.name().to_lowercase().cmp(&other.name().to_lowercase())
+ },
+ priority_order => priority_order,
+ }
+ }
+
+ fn compare_for_display(&self, other: &DisplayLayout) -> std::cmp::Ordering {
+ match (self.is_builtin(), other.is_builtin()) {
+ (false, false) => {
+ // Sort by update_time (newest first)
+ match (self.get_update_time(), other.get_update_time()) {
+ (Some(self_time), Some(other_time)) => {
+ // Reverse comparison for descending order
+ other_time.cmp(&self_time)
+ },
+ (Some(_), None) => std::cmp::Ordering::Less,
+ (None, Some(_)) => std::cmp::Ordering::Greater,
+ (None, None) => {
+ // Fallback to alphabetical
+ self.name().to_lowercase().cmp(&other.name().to_lowercase())
+ },
+ }
+ },
+ (false, true) => std::cmp::Ordering::Less,
+ (true, false) => std::cmp::Ordering::Greater,
+ (true, true) => self.compare_builtin_layouts(other),
+ }
+ }
+
+ fn name_width(&self) -> usize {
+ self.name().chars().count()
+ }
+ fn last_modified_width(&self) -> usize {
+ let display_info = get_layout_display_info(&self);
+ get_last_modified_string(display_info.1, self.is_builtin())
+ .chars()
+ .count()
+ }
+}
+
+#[derive(Default)]
+struct State {
+ display_layouts: Vec<DisplayLayout>,
+ screen: Screen,
+}
+
+impl State {
+ fn handle_key_event(&mut self, key: KeyWithModifier) -> KeyResponse {
+ match &mut self.screen {
+ Screen::LayoutList(layout_list_screen) => {
+ layout_list_screen.handle_key(key, &self.display_layouts)
+ },
+ Screen::NewLayoutFromSession(new_layout_from_session_screen) => {
+ new_layout_from_session_screen.handle_key(key)
+ },
+ Screen::ImportLayout(import_layout_screen) => import_layout_screen.handle_key(key),
+ Screen::RenameLayout(rename_layout_screen) => rename_layout_screen.handle_key(key),
+ Screen::Error(ref mut error_screen) => error_screen.handle_key(key),
+ Screen::ErrorDetail(ref mut error_detail_screen) => error_detail_screen.handle_key(key),
+ }
+ }
+
+ fn apply_key_response(&mut self, response: KeyResponse) -> bool {
+ let mut should_render = false;
+
+ if let Some(update) = response.optimistic_update {
+ self.apply_optimistic_update(update);
+ should_render = true;
+ }
+
+ if let Some(new_screen) = response.new_screen {
+ self.screen = new_screen;
+ }
+
+ should_render || response.should_render
+ }
+
+ fn apply_optimistic_update(&mut self, update: OptimisticUpdate) {
+ match update {
+ OptimisticUpdate::Delete(file_name) => {
+ self.optimistic_delete_layout(&file_name);
+ },
+ OptimisticUpdate::Rename { old_name, new_name } => {
+ self.optimistic_rename_layout(&old_name, &new_name);
+ },
+ OptimisticUpdate::Add { name, metadata } => {
+ self.optimistic_add_layout(name, metadata);
+ },
+ }
+ }
+
+ fn optimistic_add_layout(&mut self, name: String, metadata: LayoutMetadata) {
+ let placeholder = DisplayLayout::Valid(LayoutInfo::File(name, metadata));
+
+ self.display_layouts.push(placeholder);
+ self.display_layouts
+ .sort_by(|a, b| a.compare_for_display(b));
+
+ if let Screen::LayoutList(ref mut screen) = self.screen {
+ screen.update_selected_index(&self.display_layouts);
+ }
+ }
+
+ fn optimistic_delete_layout(&mut self, file_name: &str) {
+ self.display_layouts
+ .retain(|layout| layout.file_name() != Some(file_name.to_string()));
+
+ if let Screen::LayoutList(ref mut screen) = self.screen {
+ screen.update_selected_index(&self.display_layouts);
+ }
+ }
+
+ fn optimistic_rename_layout(&mut self, old_name: &str, new_name: &str) {
+ for layout in &mut self.display_layouts {
+ match layout {
+ DisplayLayout::Valid(LayoutInfo::File(file_name, _)) => {
+ if file_name == old_name {
+ let _ = std::mem::replace(file_name, new_name.to_string());
+ break;
+ }
+ },
+ DisplayLayout::Error { ref mut name, .. } => {
+ if name == old_name {
+ let _ = std::mem::replace(name, new_name.to_string());
+ break;
+ }
+ },
+ _ => {},
+ }
+ }
+
+ self.display_layouts
+ .sort_by(|a, b| a.compare_for_display(b));
+
+ if let Screen::LayoutList(ref mut screen) = self.screen {
+ screen.update_selected_index(&self.display_layouts);
+ }
+ }
+}
+
+register_plugin!(State);
+
+impl ZellijPlugin for State {
+ fn load(&mut self, _configuration: BTreeMap<String, String>) {
+ subscribe(&[
+ EventType::PastedText,
+ EventType::SessionUpdate,
+ EventType::Key,
+ EventType::AvailableLayoutInfo,
+ EventType::PermissionRequestResult,
+ ]);
+ rename_plugin_pane(get_plugin_ids().plugin_id, "Layout Manager");
+ }
+
+ fn update(&mut self, event: Event) -> bool {
+ let mut should_render = false;
+
+ match event {
+ Event::PastedText(text) => {
+ if let Screen::ImportLayout(ref mut import_screen) = self.screen {
+ import_screen.handle_pasted_text(text);
+ should_render = true;
+ }
+ },
+ Event::AvailableLayoutInfo(available_layouts, layouts_with_errors) => {
+ // Convert valid layouts to DisplayLayout::Valid
+ let mut display_layouts: Vec<DisplayLayout> = available_layouts
+ .into_iter()
+ .map(DisplayLayout::Valid)
+ .collect();
+
+ // Convert error layouts to DisplayLayout::Error
+ for layout_error in &layouts_with_errors {
+ let stringified_error = format_kdl_error(layout_error.error.clone());
+ let error_message = match &layout_error.error {
+ LayoutParsingError::KdlError { kdl_error, .. } => {
+ format!("Layout Error: {}", kdl_error.error_message)
+ },
+ LayoutParsingError::SyntaxError => "KDL parsing error".to_owned(),
+ };
+
+ display_layouts.push(DisplayLayout::Error {
+ name: layout_error.layout_name.clone(),
+ error_message,
+ error: stringified_error,
+ });
+ }
+
+ display_layouts.sort_by(|a, b| a.compare_for_display(b));
+
+ self.display_layouts = display_layouts;
+ if let Screen::LayoutList(ref mut screen) = self.screen {
+ screen.update_selected_index(&self.display_layouts);
+ }
+ should_render = true;
+ },
+ Event::Key(key) => {
+ let response = self.handle_key_event(key);
+ should_render = self.apply_key_response(response);
+ },
+ _ => {},
+ }
+
+ should_render
+ }
+
+ fn render(&mut self, rows: usize, cols: usize) {
+ match &mut self.screen {
+ Screen::LayoutList(layout_list_screen) => {
+ layout_list_screen.render(&self.display_layouts, rows, cols)
+ },
+ Screen::NewLayoutFromSession(new_layout_from_session_screen) => {
+ new_layout_from_session_screen.render(rows, cols)
+ },
+ Screen::ImportLayout(import_layout_screen) => import_layout_screen.render(rows, cols),
+ Screen::RenameLayout(rename_layout_screen) => rename_layout_screen.render(rows, cols),
+ Screen::Error(ref error_screen) => error_screen.render(rows, cols),
+ Screen::ErrorDetail(ref error_detail_screen) => error_detail_screen.render(rows, cols),
+ }
+ }
+}
diff --git a/default-plugins/layout-manager/src/screens/import_layout.rs b/default-plugins/layout-manager/src/screens/import_layout.rs
new file mode 100644
index 00000000..8bf1a3a1
--- /dev/null
+++ b/default-plugins/layout-manager/src/screens/import_layout.rs
@@ -0,0 +1,331 @@
+use super::{KeyResponse, LayoutListScreen, OptimisticUpdate, Screen};
+use crate::errors::{format_kdl_error, ErrorScreen};
+use crate::text_input::{InputAction, TextInput};
+use crate::ui::{truncate_with_ellipsis_start, LayoutDetail, Title};
+use crate::{DisplayLayout, LayoutInfo};
+use zellij_tile::prelude::*;
+
+#[derive(Clone)]
+pub struct ImportLayoutScreen {
+ pub name_input: TextInput,
+ pub pasted_text: Option<String>,
+ pub parsed_metadata: Option<LayoutMetadata>,
+ pub parse_error: Option<String>,
+ pub editing_name: bool,
+}
+
+impl ImportLayoutScreen {
+ pub fn new() -> Self {
+ Self {
+ name_input: TextInput::empty(),
+ pasted_text: None,
+ parsed_metadata: None,
+ parse_error: None,
+ editing_name: false,
+ }
+ }
+
+ pub fn handle_pasted_text(&mut self, text: String) {
+ self.pasted_text = Some(text.clone());
+
+ match parse_layout(&text) {
+ Ok(parsed_metadata) => {
+ self.parsed_metadata = Some(parsed_metadata);
+ self.parse_error = None;
+ },
+ Err(e) => {
+ self.parsed_metadata = None;
+ self.parse_error = Some(format_kdl_error(e));
+ },
+ }
+ }
+
+ pub fn handle_key(&mut self, key: KeyWithModifier) -> KeyResponse {
+ // Special Esc handling based on edit state
+ if key.bare_key == BareKey::Esc && key.has_no_modifiers() {
+ return self.handle_escape_key();
+ }
+
+ if self.editing_name {
+ self.handle_editing_mode_key(key)
+ } else {
+ self.handle_non_editing_mode_key(key)
+ }
+ }
+
+ fn handle_escape_key(&mut self) -> KeyResponse {
+ if self.editing_name {
+ self.editing_name = false;
+ KeyResponse::render()
+ } else {
+ KeyResponse::new_screen(self.cancel_import())
+ }
+ }
+
+ fn handle_editing_mode_key(&mut self, key: KeyWithModifier) -> KeyResponse {
+ let action = self.name_input.handle_key(key);
+
+ match action {
+ InputAction::Continue => KeyResponse::render(),
+ InputAction::Submit => self.exit_editing_mode(),
+ InputAction::Cancel => self.exit_editing_mode(),
+ InputAction::Complete => KeyResponse::none(),
+ InputAction::NoAction => KeyResponse::none(),
+ }
+ }
+
+ fn handle_non_editing_mode_key(&mut self, key: KeyWithModifier) -> KeyResponse {
+ match key.bare_key {
+ BareKey::Enter if key.has_no_modifiers() => self.attempt_save_layout(),
+ BareKey::Char('r') if key.has_no_modifiers() => self.enter_editing_mode(),
+ _ => KeyResponse::none(),
+ }
+ }
+
+ fn exit_editing_mode(&mut self) -> KeyResponse {
+ self.editing_name = false;
+ KeyResponse::render()
+ }
+
+ fn enter_editing_mode(&mut self) -> KeyResponse {
+ self.editing_name = true;
+ KeyResponse::render()
+ }
+
+ fn cancel_import(&self) -> Screen {
+ Screen::LayoutList(LayoutListScreen::default())
+ }
+
+ fn attempt_save_layout(&self) -> KeyResponse {
+ // Check if we have a pasted layout
+ let Some(pasted_text) = &self.pasted_text else {
+ return KeyResponse::new_screen(
+ self.create_error_screen("Please paste a layout first"),
+ );
+ };
+
+ // Check if the layout is valid
+ if self.parse_error.is_some() {
+ return KeyResponse::new_screen(
+ self.create_error_screen("Cannot save an invalid layout"),
+ );
+ }
+
+ // Get the parsed metadata for optimistic update
+ let Some(metadata) = &self.parsed_metadata else {
+ return KeyResponse::new_screen(
+ self.create_error_screen("Cannot save layout without metadata"),
+ );
+ };
+
+ let layout_name = self.determine_layout_name();
+
+ // Capture optimistic update BEFORE the API call
+ let optimistic = OptimisticUpdate::Add {
+ name: layout_name.clone(),
+ metadata: metadata.clone(),
+ };
+
+ match self.save_layout(&layout_name, pasted_text) {
+ Ok(()) => KeyResponse::new_screen(Screen::LayoutList(LayoutListScreen::default()))
+ .with_optimistic(optimistic),
+ Err(error_msg) => KeyResponse::new_screen(self.create_error_screen(&error_msg)),
+ }
+ }
+
+ fn create_error_screen(&self, message: &str) -> Screen {
+ Screen::Error(ErrorScreen {
+ message: message.to_string(),
+ return_to_screen: Box::new(Screen::ImportLayout(self.clone())),
+ })
+ }
+
+ fn determine_layout_name(&self) -> String {
+ let input_text = self.name_input.get_text();
+ if input_text.is_empty() {
+ generate_random_name()
+ } else {
+ input_text.to_string()
+ }
+ }
+
+ fn save_layout(&self, layout_name: &str, layout_content: &str) -> Result<(), String> {
+ save_layout(layout_name.to_owned(), layout_content.to_owned(), false)
+ .map_err(|err| format!("Failed to save layout '{}': {}", layout_name, err))?;
+
+ eprintln!("Successfully imported layout: {}", layout_name);
+ Ok(())
+ }
+
+ fn description_text(&self) -> &str {
+ "Paste your layout here."
+ }
+
+ fn max_description_width(&self) -> usize {
+ self.description_text().chars().count()
+ }
+
+ fn render_description(&self, x: usize, y: usize, _width: usize) {
+ let text = Text::new(self.description_text());
+ print_text_with_coordinates(text, x, y, None, None);
+ }
+
+ fn save_as_line_text(&self, max_width: Option<usize>) -> (String, usize) {
+ // Returns (text, cursor_position_in_line)
+ let input_text = self.name_input.get_text();
+ let cursor_pos = self.name_input.get_cursor_position();
+
+ let display_name = if input_text.is_empty() && !self.editing_name {
+ "RANDOM"
+ } else {
+ input_text
+ };
+
+ let text_suffix = if self.editing_name {
+ "" // No hint when editing
+ } else {
+ " (<r> Rename)" // Show rename hint when not editing
+ };
+
+ let mut text = format!("Save as: {}{}", display_name, text_suffix);
+ let mut cursor_position_in_line = 9 + cursor_pos;
+
+ if let Some(max_width) = max_width {
+ if text.chars().count() > max_width {
+ let truncated_display_name = truncate_with_ellipsis_start(
+ display_name,
+ max_width.saturating_sub(9 + text_suffix.chars().count()),
+ );
+ text = format!("Save as: {}{}", truncated_display_name, text_suffix);
+ let truncated_len = truncated_display_name.chars().count();
+ cursor_position_in_line = 9 + cursor_pos.min(truncated_len);
+ }
+ }
+
+ (text, cursor_position_in_line)
+ }
+
+ fn render_save_as_line(&self, x: usize, y: usize, width: usize) {
+ let (text, _) = self.save_as_line_text(Some(width));
+ let colored = Text::new(&text).color_substring(3, "<r>");
+ print_text_with_coordinates(colored, x, y, None, None);
+ }
+
+ fn help_text(&self) -> (&str, &[&str]) {
+ // Only Enter and Esc - NO <r> here (it's in the save-as line)
+ ("<Enter> - Save, <Esc> - Back", &["<Enter>", "<Esc>"])
+ }
+
+ fn render_help_text(&self, x: usize, y: usize, _width: usize) {
+ let (text, items_to_color) = self.help_text();
+ let mut text_obj = Text::new(text);
+ for item in items_to_color {
+ text_obj = text_obj.color_substring(3, item);
+ }
+ print_text_with_coordinates(text_obj, x, y, None, None);
+ }
+
+ fn render_title(&self, x: usize, y: usize, width: usize) {
+ let title = Text::new("Import Layout").color_all(2);
+ print_text_with_coordinates(title, x, y, Some(width), None);
+ }
+
+ pub fn render(&self, rows: usize, cols: usize) {
+ // Only render modal if we have a valid pasted layout
+ if let Some(metadata) = &self.parsed_metadata {
+ let display_layout =
+ DisplayLayout::Valid(LayoutInfo::File("imported".to_string(), metadata.clone()));
+
+ let layout_detail = LayoutDetail::new(&display_layout);
+
+ let desired_ui_width = std::cmp::max(
+ self.help_text().0.chars().count(),
+ self.save_as_line_text(None).0.chars().count(),
+ );
+
+ let actual_ui_width = std::cmp::min(desired_ui_width, cols);
+
+ // Calculate layout detail dimensions
+ let available_cols_for_details = actual_ui_width.saturating_sub(2);
+ let desired_details_height =
+ layout_detail.calculate_required_height(available_cols_for_details);
+
+ let desired_ui_height = 6 + desired_details_height;
+ let base_y = rows.saturating_sub(desired_ui_height) / 2;
+ let base_x = cols.saturating_sub(actual_ui_width) / 2;
+
+ // Update cursor BEFORE rendering
+ let save_as_line_y = base_y + 2;
+ self.update_cursor_position(base_x, save_as_line_y, actual_ui_width);
+
+ // Render components
+ self.render_title(base_x, base_y, actual_ui_width);
+ self.render_save_as_line(base_x, save_as_line_y, actual_ui_width);
+
+ let details_y = save_as_line_y + 2;
+
+ let actual_details_height = if rows < desired_ui_height {
+ rows.saturating_sub(details_y + 2)
+ } else {
+ desired_details_height
+ };
+
+ layout_detail.render(
+ base_x + 2,
+ details_y,
+ actual_details_height,
+ available_cols_for_details,
+ );
+
+ // Render help text
+ let help_y = details_y + actual_details_height + 1;
+ self.render_help_text(base_x, help_y, actual_ui_width);
+ } else if let Some(error) = &self.parse_error {
+ // Show error if parse failed
+ Title::new("Import Layout").render(0, 0);
+ self.render_parse_error(error, 2);
+
+ let help_y = rows.saturating_sub(2);
+ self.render_esc_cancel_help(0, help_y);
+ } else {
+ // Calculate desired width
+ let desired_ui_width = std::cmp::max(
+ self.help_text().0.chars().count(),
+ self.max_description_width(),
+ );
+
+ let actual_ui_width = std::cmp::min(desired_ui_width, cols);
+ let actual_ui_height = std::cmp::min(6, rows); // 6 - content rows plus padding
+
+ let base_y = rows.saturating_sub(actual_ui_height) / 2;
+ let base_x = cols.saturating_sub(actual_ui_width) / 2;
+
+ Title::new("Import Layout").render(base_x, base_y);
+ self.render_description(base_x, base_y + 2, cols);
+
+ let help_y = base_y + 4;
+ self.render_esc_cancel_help(base_x, help_y);
+ }
+ }
+ fn render_esc_cancel_help(&self, x: usize, y: usize) {
+ let help_text = "<Esc> - Cancel";
+ let help_text = Text::new(help_text).color_substring(3, "<Esc>");
+ print_text_with_coordinates(help_text, x, y, None, None);
+ }
+
+ fn render_parse_error(&self, error: &str, y: usize) {
+ // Render error message using ANSI escape sequences
+ for (i, line) in error.lines().enumerate() {
+ print!("\u{1b}[{};{}H{}", y + i + 1, 1, line);
+ }
+ }
+
+ pub fn update_cursor_position(&self, base_x: usize, cursor_y: usize, max_width: usize) {
+ if self.editing_name {
+ let (_, cursor_position_in_line) = self.save_as_line_text(Some(max_width));
+ show_cursor(Some((base_x + cursor_position_in_line, cursor_y)))
+ } else {
+ show_cursor(None)
+ }
+ }
+}
diff --git a/default-plugins/layout-manager/src/screens/layout_list/mod.rs b/default-plugins/layout-manager/src/screens/layout_list/mod.rs
new file mode 100644
index 00000000..e643657b
--- /dev/null
+++ b/default-plugins/layout-manager/src/screens/layout_list/mod.rs
@@ -0,0 +1,689 @@
+mod search;
+
+use super::{KeyResponse, OptimisticUpdate, Screen};
+use crate::text_input::InputAction;
+use crate::ui::{Controls, LayoutDetail, LayoutsTable};
+use crate::DisplayLayout;
+use search::SearchState;
+use zellij_tile::prelude::*;
+
+#[derive(Clone)]
+pub struct LayoutListScreen {
+ pub selected_layout_index: usize,
+ retain_terminal_panes: bool,
+ retain_plugin_panes: bool,
+ apply_only_to_active_tab: bool,
+ show_more_override_options: bool,
+ search_state: SearchState,
+ last_rows: usize,
+ last_cols: usize,
+}
+
+impl Default for LayoutListScreen {
+ fn default() -> Self {
+ Self {
+ selected_layout_index: 0,
+ retain_terminal_panes: false,
+ retain_plugin_panes: false,
+ apply_only_to_active_tab: false,
+ show_more_override_options: false,
+ search_state: SearchState::new(),
+ last_rows: 0,
+ last_cols: 0,
+ }
+ }
+}
+
+impl LayoutListScreen {
+ pub fn with_selected_index(selected_index: usize) -> Self {
+ Self {
+ selected_layout_index: selected_index,
+ retain_terminal_panes: true,
+ retain_plugin_panes: false,
+ apply_only_to_active_tab: false,
+ show_more_override_options: false,
+ search_state: SearchState::new(),
+ last_rows: 0,
+ last_cols: 0,
+ }
+ }
+
+ pub fn handle_key(
+ &mut self,
+ key: KeyWithModifier,
+ display_layouts: &[DisplayLayout],
+ ) -> KeyResponse {
+ if self.search_state.is_typing() {
+ self.handle_typing_filter_mode(key, display_layouts)
+ } else {
+ match key.bare_key {
+ BareKey::Char('/') if key.has_no_modifiers() => {
+ self.search_state.start_typing();
+ self.update_filter(display_layouts);
+ KeyResponse::render()
+ },
+ BareKey::Delete if key.has_no_modifiers() => {
+ if let Some(file_name) = self.get_selected_file_name(display_layouts) {
+ let _ = delete_layout(file_name.clone());
+ KeyResponse::none().with_optimistic(OptimisticUpdate::Delete(file_name))
+ } else {
+ KeyResponse::none()
+ }
+ },
+ BareKey::Char('e') if key.has_no_modifiers() => {
+ self.edit_selected_layout(display_layouts);
+ KeyResponse::none()
+ },
+ BareKey::Char('i') if key.has_no_modifiers() => {
+ let new_screen = self.start_import_layout();
+ KeyResponse::new_screen(new_screen)
+ },
+ BareKey::Char('n') if key.has_no_modifiers() => {
+ let new_screen = self.start_new_layout_creation_from_session();
+ KeyResponse::new_screen(new_screen)
+ },
+ BareKey::Char('r') if key.has_no_modifiers() => {
+ if let Some(new_screen) = self.start_rename_layout(display_layouts) {
+ KeyResponse::new_screen(new_screen)
+ } else {
+ KeyResponse::render()
+ }
+ },
+ BareKey::Up if key.has_no_modifiers() => {
+ self.navigate_up(display_layouts);
+ KeyResponse::render()
+ },
+ BareKey::Down if key.has_no_modifiers() => {
+ self.navigate_down(display_layouts);
+ KeyResponse::render()
+ },
+ BareKey::Enter if key.has_no_modifiers() => {
+ self.open_selected_layout(display_layouts);
+ KeyResponse::none()
+ },
+ BareKey::Tab if key.has_no_modifiers() => {
+ self.apply_selected_layout(display_layouts);
+ KeyResponse::none()
+ },
+ BareKey::Char('t') if key.has_no_modifiers() => {
+ self.toggle_retain_options();
+ KeyResponse::render()
+ },
+ BareKey::Char('a') if key.has_no_modifiers() => {
+ self.toggle_target_option();
+ KeyResponse::render()
+ },
+ BareKey::Char('m') if key.has_no_modifiers() => {
+ if let Some(new_screen) = self.show_error_detail(display_layouts) {
+ KeyResponse::new_screen(new_screen)
+ } else {
+ KeyResponse::render()
+ }
+ },
+ BareKey::Char('?') if key.has_no_modifiers() => {
+ self.show_more_override_options = !self.show_more_override_options;
+ KeyResponse::render()
+ },
+ BareKey::Esc if key.has_no_modifiers() => {
+ self.clear_filter();
+ KeyResponse::render()
+ },
+ _ => KeyResponse::none(),
+ }
+ }
+ }
+
+ fn handle_typing_filter_mode(
+ &mut self,
+ key: KeyWithModifier,
+ display_layouts: &[DisplayLayout],
+ ) -> KeyResponse {
+ // Special handling for Enter and Esc
+ match key.bare_key {
+ BareKey::Esc if key.has_no_modifiers() => {
+ self.clear_filter();
+ return KeyResponse::render();
+ },
+ BareKey::Enter if key.has_no_modifiers() => {
+ if self.search_state.get_filter_input().is_empty()
+ || self.search_state.get_search_results().is_empty()
+ {
+ self.clear_filter();
+ } else {
+ self.search_state.stop_typing();
+ show_cursor(None);
+ }
+ return KeyResponse::render();
+ },
+ _ => {},
+ }
+
+ // Pass all keys to TextInput
+ let action = self.search_state.get_filter_input_mut().handle_key(key);
+
+ match action {
+ InputAction::Continue => {
+ self.update_filter(display_layouts);
+ KeyResponse::render()
+ },
+ InputAction::Cancel => {
+ // Ctrl-C or Esc - clear filter
+ self.clear_filter();
+ KeyResponse::render()
+ },
+ InputAction::Submit => {
+ // Enter handled above
+ KeyResponse::none()
+ },
+ InputAction::Complete => KeyResponse::none(),
+ InputAction::NoAction => KeyResponse::none(),
+ }
+ }
+
+ fn edit_selected_layout(&self, display_layouts: &[DisplayLayout]) {
+ if let Some(file_name) = self.get_selected_file_name(display_layouts) {
+ let _ = edit_layout(file_name, Default::default());
+ }
+ }
+
+ fn get_selected_file_name(&self, display_layouts: &[DisplayLayout]) -> Option<String> {
+ display_layouts
+ .get(self.selected_layout_index)
+ .and_then(|layout| layout.file_name())
+ }
+
+ fn start_new_layout_creation_from_session(&self) -> Screen {
+ match dump_session_layout() {
+ Ok((session_layout, Some(session_layout_metadata))) => {
+ Screen::NewLayoutFromSession(super::NewLayoutFromCurrentSessionScreen {
+ name_input: crate::text_input::TextInput::empty(),
+ session_layout,
+ current_layout_metadata: session_layout_metadata,
+ editing_name: false,
+ save_current_tab_only: false,
+ })
+ },
+ Ok((_, None)) => Screen::Error(super::ErrorScreen {
+ message: "Failed to retrieve session layout metadata".to_string(),
+ return_to_screen: Box::new(Screen::LayoutList(self.clone())),
+ }),
+ Err(error_msg) => Screen::Error(super::ErrorScreen {
+ message: format!("Failed to dump session layout: {}", error_msg),
+ return_to_screen: Box::new(Screen::LayoutList(self.clone())),
+ }),
+ }
+ }
+
+ fn start_import_layout(&self) -> Screen {
+ Screen::ImportLayout(super::ImportLayoutScreen::new())
+ }
+
+ fn start_rename_layout(&self, display_layouts: &[DisplayLayout]) -> Option<Screen> {
+ if let Some(layout_file_name) = self.get_selected_file_name(display_layouts) {
+ Some(Screen::RenameLayout(super::RenameLayoutScreen::new(
+ layout_file_name,
+ self.selected_layout_index,
+ )))
+ } else {
+ None
+ }
+ }
+
+ fn navigate_up(&mut self, display_layouts: &[DisplayLayout]) {
+ if self.is_searching() {
+ // Navigate in search results
+ if self.search_state.get_selected_search_index() == 0 {
+ self.search_state.set_selected_search_index(
+ self.search_state
+ .get_search_results()
+ .len()
+ .saturating_sub(1),
+ );
+ } else {
+ let new_index = self
+ .search_state
+ .get_selected_search_index()
+ .saturating_sub(1);
+ self.search_state.set_selected_search_index(new_index);
+ }
+ // Keep selected_layout_index synchronized with the actual position
+ self.selected_layout_index = self
+ .search_state
+ .get_search_results()
+ .get(self.search_state.get_selected_search_index())
+ .map(|r| r.original_index)
+ .unwrap_or(0);
+ } else {
+ // Normal navigation
+ if self.selected_layout_index == 0 {
+ self.selected_layout_index = display_layouts.len().saturating_sub(1);
+ } else {
+ self.selected_layout_index -= 1;
+ }
+ }
+ self.apply_only_to_active_tab = self.should_default_to_current_tab(display_layouts);
+ }
+
+ fn navigate_down(&mut self, display_layouts: &[DisplayLayout]) {
+ if self.is_searching() {
+ // Navigate in search results
+ if self.search_state.get_selected_search_index() + 1
+ >= self.search_state.get_search_results().len()
+ {
+ self.search_state.set_selected_search_index(0);
+ } else {
+ let new_index = self.search_state.get_selected_search_index() + 1;
+ self.search_state.set_selected_search_index(new_index);
+ }
+ // Keep selected_layout_index synchronized with the actual position
+ self.selected_layout_index = self
+ .search_state
+ .get_search_results()
+ .get(self.search_state.get_selected_search_index())
+ .map(|r| r.original_index)
+ .unwrap_or(0);
+ } else {
+ // Normal navigation
+ if self.selected_layout_index + 1 >= display_layouts.len() {
+ self.selected_layout_index = 0;
+ } else {
+ self.selected_layout_index += 1;
+ }
+ }
+ self.apply_only_to_active_tab = self.should_default_to_current_tab(display_layouts);
+ }
+
+ fn apply_selected_layout(&self, display_layouts: &[DisplayLayout]) {
+ if let Some(DisplayLayout::Valid(chosen_layout)) =
+ display_layouts.get(self.selected_layout_index)
+ {
+ override_layout(
+ chosen_layout,
+ self.retain_terminal_panes,
+ self.retain_plugin_panes,
+ self.apply_only_to_active_tab,
+ Default::default(),
+ );
+ }
+ }
+
+ fn toggle_retain_options(&mut self) {
+ // Cycle through: Terminals -> Plugins -> Both -> None -> (back to Terminals)
+ match (self.retain_terminal_panes, self.retain_plugin_panes) {
+ (true, false) => {
+ // Terminals -> Plugins
+ self.retain_terminal_panes = false;
+ self.retain_plugin_panes = true;
+ },
+ (false, true) => {
+ // Plugins -> Both
+ self.retain_terminal_panes = true;
+ self.retain_plugin_panes = true;
+ },
+ (true, true) => {
+ // Both -> None
+ self.retain_terminal_panes = false;
+ self.retain_plugin_panes = false;
+ },
+ (false, false) => {
+ // None -> Terminals
+ self.retain_terminal_panes = true;
+ self.retain_plugin_panes = false;
+ },
+ }
+ }
+
+ fn toggle_target_option(&mut self) {
+ // Toggle between: All Tabs (false) <-> Current (true)
+ self.apply_only_to_active_tab = !self.apply_only_to_active_tab;
+ }
+
+ fn show_error_detail(&self, display_layouts: &[DisplayLayout]) -> Option<Screen> {
+ if let Some(DisplayLayout::Error {
+ name,
+ error,
+ error_message: _,
+ }) = display_layouts.get(self.selected_layout_index)
+ {
+ Some(Screen::ErrorDetail(super::ErrorDetailScreen::new(
+ name.clone(),
+ error.clone(),
+ Box::new(Screen::LayoutList(self.clone())),
+ )))
+ } else {
+ None
+ }
+ }
+
+ fn should_default_to_current_tab(&self, display_layouts: &[DisplayLayout]) -> bool {
+ match display_layouts.get(self.selected_layout_index) {
+ Some(DisplayLayout::Valid(LayoutInfo::BuiltIn(_))) => true,
+ Some(DisplayLayout::Valid(LayoutInfo::File(_, metadata))) => metadata.tabs.len() == 1,
+ _ => false,
+ }
+ }
+
+ fn open_selected_layout(&self, display_layouts: &[DisplayLayout]) {
+ if let Some(DisplayLayout::Valid(chosen_layout)) =
+ display_layouts.get(self.selected_layout_index)
+ {
+ new_tabs_with_layout_info(chosen_layout);
+ }
+ }
+
+ pub fn render(&mut self, display_layouts: &[DisplayLayout], rows: usize, cols: usize) {
+ show_cursor(None);
+ // Store terminal dimensions for cursor positioning
+ self.last_rows = rows;
+ self.last_cols = cols;
+
+ self.update_filter(display_layouts);
+
+ // Calculate base coordinates using the full display_layouts (NOT filtered results)
+ let (total_width, total_height) =
+ self.calculate_content_dimensions(rows, cols, display_layouts);
+ let (base_x, base_y) =
+ self.calculate_base_coordinates(rows, cols, total_width, total_height);
+
+ let layouts_to_render = self.effective_layouts(display_layouts);
+ let (content_height, controls_y) = self.calculate_layout(rows, &display_layouts);
+
+ let controls = Controls::new(
+ self.retain_terminal_panes,
+ self.retain_plugin_panes,
+ self.apply_only_to_active_tab,
+ self.show_more_override_options,
+ self.search_state.is_typing(),
+ self.search_state.is_active(),
+ );
+ let controls_width = controls.calculate_width(cols);
+
+ let (table_width, detail_x) =
+ self.calculate_horizontal_split(cols, display_layouts, controls_width);
+
+ let table_y = if self.search_state.is_active() || self.search_state.is_typing() {
+ 1 + base_y
+ } else {
+ base_y
+ };
+
+ if self.search_state.is_active() || self.search_state.is_typing() {
+ self.search_state.render_filter_line(base_x, base_y);
+ }
+
+ let selected_index = self.effective_selected_index();
+
+ let (visible_layouts, selected_in_window, hidden_above, hidden_below) =
+ calculate_visible_window(
+ &layouts_to_render,
+ selected_index,
+ content_height.saturating_sub(1), // account for title row
+ );
+
+ let matched_indices = self
+ .search_state
+ .get_matched_indices_for_visible(hidden_above, visible_layouts.len());
+
+ let has_visible_layouts = !visible_layouts.is_empty();
+ LayoutsTable::new(
+ visible_layouts,
+ selected_in_window,
+ hidden_above,
+ hidden_below,
+ )
+ .with_matched_indices(matched_indices)
+ .render(
+ base_x,
+ table_y,
+ content_height.saturating_sub(1),
+ table_width,
+ );
+
+ if has_visible_layouts {
+ if let Some(selected_layout) = display_layouts.get(self.selected_layout_index) {
+ LayoutDetail::new(selected_layout).render(
+ detail_x + base_x,
+ table_y + 1,
+ content_height.saturating_sub(1),
+ table_width,
+ );
+ } else {
+ // Render "No layout selected" message (moved from LayoutDetail)
+ let msg = Text::new("No layout selected").color_all(2);
+ print_text_with_coordinates(msg, detail_x + base_x, table_y, None, None);
+ }
+ }
+
+ controls.render(base_x, controls_y + base_y, cols);
+ }
+
+ fn calculate_layout(&self, rows: usize, display_layouts: &[DisplayLayout]) -> (usize, usize) {
+ let rows_in_table = display_layouts.len() + 1; // 1 for the title row
+ let controls_height = self.get_controls_height();
+ let filter_row_height = if self.is_searching() { 1 } else { 0 };
+ let padding = 1;
+ let mut content_height = std::cmp::max(rows_in_table, 5);
+ if content_height + controls_height + padding + filter_row_height >= rows {
+ content_height = rows
+ .saturating_sub(controls_height)
+ .saturating_sub(padding)
+ .saturating_sub(filter_row_height)
+ }
+ let controls_y = content_height + padding + filter_row_height;
+
+ (content_height, controls_y)
+ }
+
+ fn get_controls_height(&self) -> usize {
+ if self.show_more_override_options {
+ 5
+ } else {
+ 3
+ }
+ }
+
+ fn calculate_horizontal_split(
+ &self,
+ cols: usize,
+ display_layouts: &[DisplayLayout],
+ controls_width: usize,
+ ) -> (usize, usize) {
+ let padding = 2;
+ let widest_layout_name = display_layouts
+ .iter()
+ .map(|layout| layout.name_width())
+ .max()
+ .unwrap_or(0);
+ let widest_last_modified = display_layouts
+ .iter()
+ .map(|layout| layout.last_modified_width())
+ .max()
+ .unwrap_or(0);
+
+ let content_based_table_width = std::cmp::min(
+ (cols / 2).saturating_sub(padding),
+ widest_layout_name + widest_last_modified + padding,
+ );
+
+ let controls_based_table_width = controls_width.saturating_sub(padding) / 2;
+
+ let table_width = content_based_table_width.max(controls_based_table_width);
+ let detail_x = table_width.saturating_add(padding);
+
+ (table_width, detail_x)
+ }
+
+ fn calculate_content_dimensions(
+ &self,
+ rows: usize,
+ cols: usize,
+ display_layouts: &[DisplayLayout],
+ ) -> (usize, usize) {
+ let filter_row_height = if self.is_searching() { 1 } else { 0 };
+ let (content_height, _) = self.calculate_layout(rows, display_layouts);
+ let padding = 1;
+ let controls_height = self.get_controls_height();
+ let total_height = filter_row_height + content_height + padding + controls_height;
+
+ let controls = Controls::new(
+ self.retain_terminal_panes,
+ self.retain_plugin_panes,
+ self.apply_only_to_active_tab,
+ self.show_more_override_options,
+ self.search_state.is_typing(),
+ self.search_state.is_active(),
+ );
+ let controls_width = controls.calculate_width(cols);
+
+ let (table_width, detail_x) =
+ self.calculate_horizontal_split(cols, display_layouts, controls_width);
+
+ let layout_width = detail_x.saturating_add(table_width);
+ let total_width = layout_width.max(controls_width);
+
+ (total_width, total_height)
+ }
+
+ fn calculate_base_coordinates(
+ &self,
+ rows: usize,
+ cols: usize,
+ total_width: usize,
+ total_height: usize,
+ ) -> (usize, usize) {
+ let base_x = if total_width < cols {
+ (cols.saturating_sub(total_width)) / 2
+ } else {
+ 0
+ };
+ let base_y = if total_height < rows {
+ (rows.saturating_sub(total_height)) / 2
+ } else {
+ 0
+ };
+ (base_x, base_y)
+ }
+
+ pub fn update_selected_index(&mut self, display_layouts: &[DisplayLayout]) {
+ if self.selected_layout_index >= display_layouts.len() {
+ self.selected_layout_index = display_layouts.len().saturating_sub(1);
+ }
+
+ // Refresh filter results if filtering is active
+ if self.search_state.is_active() && !self.search_state.get_filter_input().is_empty() {
+ self.update_filter(display_layouts);
+ }
+
+ self.apply_only_to_active_tab = self.should_default_to_current_tab(display_layouts);
+ }
+
+ fn is_searching(&self) -> bool {
+ (self.search_state.is_active() && !self.search_state.get_search_results().is_empty())
+ || self.search_state.is_typing()
+ }
+
+ fn effective_selected_index(&self) -> usize {
+ if self.is_searching() {
+ self.search_state.get_selected_search_index()
+ } else {
+ self.selected_layout_index
+ }
+ }
+
+ fn effective_layouts(&self, display_layouts: &[DisplayLayout]) -> Vec<DisplayLayout> {
+ if self.is_searching() {
+ self.search_state
+ .get_search_results()
+ .iter()
+ .map(|r| r.layout.clone())
+ .collect()
+ } else {
+ display_layouts.to_vec()
+ }
+ }
+
+ fn update_filter(&mut self, display_layouts: &[DisplayLayout]) {
+ let (total_width, total_height) =
+ self.calculate_content_dimensions(self.last_rows, self.last_cols, display_layouts);
+ let (base_x, base_y) = self.calculate_base_coordinates(
+ self.last_rows,
+ self.last_cols,
+ total_width,
+ total_height,
+ );
+
+ self.search_state
+ .update_filter(display_layouts, base_x, base_y);
+
+ // Update selected_layout_index to match the selected search result
+ if let Some(original_index) = self.search_state.get_current_selected_original_index() {
+ self.selected_layout_index = original_index;
+ }
+ }
+
+ fn clear_filter(&mut self) {
+ self.search_state.clear_filter();
+ self.selected_layout_index = 0;
+ }
+}
+
+fn calculate_visible_window(
+ layouts: &[DisplayLayout],
+ selected_index: usize,
+ max_visible: usize,
+) -> (Vec<DisplayLayout>, usize, usize, usize) {
+ // returns: (index_in_rendered, hidden_items_above, hidden_items_below)
+ if layouts.is_empty() || max_visible == 0 {
+ return (Vec::new(), 0, 0, 0);
+ }
+
+ if layouts.len() <= max_visible {
+ return (layouts.to_vec(), selected_index, 0, 0);
+ }
+
+ let Some(selected_layout) = layouts.get(selected_index) else {
+ return (Vec::new(), 0, 0, 0);
+ };
+
+ let mut visible = Vec::with_capacity(max_visible);
+ visible.push(selected_layout.clone());
+
+ let mut before = selected_index;
+ let mut after = selected_index;
+ let mut take_before = true;
+
+ while visible.len() < max_visible {
+ if take_before && before > 0 {
+ before = before.saturating_sub(1);
+ if let Some(layout) = layouts.get(before) {
+ visible.insert(0, layout.clone());
+ }
+ } else if !take_before && after + 1 < layouts.len() {
+ after += 1;
+ if let Some(layout) = layouts.get(after) {
+ visible.push(layout.clone());
+ }
+ } else if before > 0 {
+ before = before.saturating_sub(1);
+ if let Some(layout) = layouts.get(before) {
+ visible.insert(0, layout.clone());
+ }
+ } else if after + 1 < layouts.len() {
+ after += 1;
+ if let Some(layout) = layouts.get(after) {
+ visible.push(layout.clone());
+ }
+ } else {
+ break;
+ }
+ take_before = !take_before;
+ }
+
+ let adjusted_index = selected_index.saturating_sub(before);
+ (
+ visible,
+ adjusted_index,
+ before,
+ layouts.len().saturating_sub(after + 1),
+ )
+}
diff --git a/default-plugins/layout-manager/src/screens/layout_list/search.rs b/default-plugins/layout-manager/src/screens/layout_list/search.rs
new file mode 100644
index 00000000..1414d6a3
--- /dev/null
+++ b/default-plugins/layout-manager/src/screens/layout_list/search.rs
@@ -0,0 +1,187 @@
+use crate::text_input::TextInput;
+use crate::DisplayLayout;
+use fuzzy_matcher::skim::SkimMatcherV2;
+use fuzzy_matcher::FuzzyMatcher;
+use zellij_tile::prelude::*;
+
+#[derive(Clone)]
+pub struct SearchResult {
+ pub layout: DisplayLayout,
+ pub original_index: usize,
+ pub score: i64,
+ pub matched_indices: Vec<usize>,
+}
+
+#[derive(Clone)]
+pub struct SearchState {
+ filter_active: bool,
+ filter_input: TextInput,
+ typing_filter: bool,
+ search_results: Vec<SearchResult>,
+ selected_search_index: usize,
+}
+
+impl Default for SearchState {
+ fn default() -> Self {
+ Self {
+ filter_active: false,
+ filter_input: TextInput::empty(),
+ typing_filter: false,
+ search_results: Vec::new(),
+ selected_search_index: 0,
+ }
+ }
+}
+
+impl SearchState {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn is_active(&self) -> bool {
+ self.filter_active
+ }
+
+ pub fn is_typing(&self) -> bool {
+ self.typing_filter
+ }
+
+ pub fn start_typing(&mut self) {
+ self.typing_filter = true;
+ }
+
+ pub fn stop_typing(&mut self) {
+ self.typing_filter = false;
+ }
+
+ pub fn get_filter_input(&self) -> &TextInput {
+ &self.filter_input
+ }
+
+ pub fn get_filter_input_mut(&mut self) -> &mut TextInput {
+ &mut self.filter_input
+ }
+
+ pub fn get_search_results(&self) -> &[SearchResult] {
+ &self.search_results
+ }
+
+ pub fn get_selected_search_index(&self) -> usize {
+ self.selected_search_index
+ }
+
+ pub fn set_selected_search_index(&mut self, index: usize) {
+ self.selected_search_index = index;
+ }
+
+ pub fn update_filter(
+ &mut self,
+ display_layouts: &[DisplayLayout],
+ base_x: usize,
+ base_y: usize,
+ ) {
+ let filter_prompt = "Filter:";
+ let filter_text = self.filter_input.get_text();
+
+ // Clear results if filter is empty
+ if filter_text.is_empty() && !self.typing_filter {
+ self.search_results.clear();
+ self.selected_search_index = 0;
+ return;
+ }
+
+ // Perform fuzzy matching
+ let matcher = SkimMatcherV2::default();
+
+ let mut results: Vec<SearchResult> = display_layouts
+ .iter()
+ .enumerate()
+ .filter_map(|(index, layout)| {
+ let name = layout.name();
+ matcher
+ .fuzzy_indices(&name, filter_text)
+ .map(|(score, indices)| SearchResult {
+ layout: layout.clone(),
+ original_index: index,
+ score,
+ matched_indices: indices,
+ })
+ })
+ .collect();
+
+ // Sort by score descending (best matches first)
+ results.sort_by(|a, b| b.score.cmp(&a.score));
+ self.search_results = results;
+
+ if self.search_results.is_empty() {
+ self.filter_active = false;
+ } else {
+ self.filter_active = true;
+ }
+
+ // Keep selection in bounds
+ if self.selected_search_index >= self.search_results.len() {
+ self.selected_search_index = self.search_results.len().saturating_sub(1);
+ }
+
+ // Show cursor when typing
+ if self.typing_filter {
+ let cursor_pos = self.filter_input.get_cursor_position();
+ show_cursor(Some((
+ filter_prompt.chars().count() + 1 + cursor_pos + base_x,
+ base_y,
+ )));
+ } else {
+ show_cursor(None);
+ }
+ }
+
+ pub fn clear_filter(&mut self) {
+ self.filter_active = false;
+ self.typing_filter = false;
+ self.filter_input.clear();
+ self.search_results.clear();
+ self.selected_search_index = 0;
+ show_cursor(None);
+ }
+
+ pub fn get_current_selected_original_index(&self) -> Option<usize> {
+ self.search_results
+ .get(self.selected_search_index)
+ .map(|result| result.original_index)
+ }
+
+ pub fn render_filter_line(&self, base_x: usize, base_y: usize) {
+ let filter_text_str = self.filter_input.get_text();
+ let filter_text = if self.typing_filter {
+ let mut filter_line =
+ Text::new(format!("Filter: {}", filter_text_str)).color_substring(2, "Filter:");
+ if !filter_text_str.is_empty() {
+ filter_line = filter_line.color_last_substring(3, filter_text_str)
+ }
+ filter_line
+ } else {
+ Text::new(format!("Filter: {} (<Esc> - clear)", filter_text_str))
+ .color_substring(3, "<Esc>")
+ .color_substring(2, "Filter:")
+ };
+ print_text_with_coordinates(filter_text, base_x, base_y, None, None);
+ }
+
+ pub fn get_matched_indices_for_visible(
+ &self,
+ hidden_above: usize,
+ visible_count: usize,
+ ) -> Vec<Option<Vec<usize>>> {
+ if !self.filter_active || self.search_results.is_empty() {
+ return vec![None; visible_count];
+ }
+
+ self.search_results
+ .iter()
+ .skip(hidden_above)
+ .take(visible_count)
+ .map(|result| Some(result.matched_indices.clone()))
+ .collect()
+ }
+}
diff --git a/default-plugins/layout-manager/src/screens/mod.rs b/default-plugins/layout-manager/src/screens/mod.rs
new file mode 100644
index 00000000..d9d617be
--- /dev/null
+++ b/default-plugins/layout-manager/src/screens/mod.rs
@@ -0,0 +1,79 @@
+mod import_layout;
+mod layout_list;
+mod new_layout_from_session;
+mod rename_layout;
+
+use zellij_tile::prelude::LayoutMetadata;
+
+pub use import_layout::ImportLayoutScreen;
+pub use layout_list::LayoutListScreen;
+pub use new_layout_from_session::NewLayoutFromCurrentSessionScreen;
+pub use rename_layout::RenameLayoutScreen;
+
+// Re-export error types from errors module for convenience
+pub use crate::errors::{ErrorDetailScreen, ErrorScreen};
+
+#[derive(Clone)]
+pub enum Screen {
+ LayoutList(LayoutListScreen),
+ NewLayoutFromSession(NewLayoutFromCurrentSessionScreen),
+ ImportLayout(ImportLayoutScreen),
+ RenameLayout(RenameLayoutScreen),
+ Error(ErrorScreen),
+ ErrorDetail(ErrorDetailScreen),
+}
+
+impl Default for Screen {
+ fn default() -> Self {
+ Screen::LayoutList(LayoutListScreen::default())
+ }
+}
+
+/// Optimistic state updates to apply before Zellij confirms
+#[derive(Clone, Debug)]
+pub enum OptimisticUpdate {
+ Delete(String), // file_name
+ Rename {
+ old_name: String,
+ new_name: String,
+ },
+ Add {
+ name: String,
+ metadata: LayoutMetadata,
+ },
+}
+
+/// Response from screen key handlers
+#[derive(Default)]
+pub struct KeyResponse {
+ pub should_render: bool,
+ pub new_screen: Option<Screen>,
+ pub optimistic_update: Option<OptimisticUpdate>,
+}
+
+impl KeyResponse {
+ pub fn render() -> Self {
+ KeyResponse {
+ should_render: true,
+ new_screen: None,
+ optimistic_update: None,
+ }
+ }
+
+ pub fn new_screen(screen: Screen) -> Self {
+ KeyResponse {
+ should_render: true,
+ new_screen: Some(screen),
+ optimistic_update: None,
+ }
+ }
+
+ pub fn with_optimistic(mut self, update: OptimisticUpdate) -> Self {
+ self.optimistic_update = Some(update);
+ self
+ }
+
+ pub fn none() -> Self {
+ KeyResponse::default()
+ }
+}
diff --git a/default-plugins/layout-manager/src/screens/new_layout_from_session.rs b/default-plugins/layout-manager/src/screens/new_layout_from_session.rs
new file mode 100644
index 00000000..fbf6d63b
--- /dev/null
+++ b/default-plugins/layout-manager/src/screens/new_layout_from_session.rs
@@ -0,0 +1,352 @@
+use super::{ErrorScreen, KeyResponse, LayoutListScreen, OptimisticUpdate, Screen};
+use crate::text_input::{InputAction, TextInput};
+use crate::ui::{truncate_with_ellipsis_start, LayoutDetail};
+use crate::DisplayLayout;
+use zellij_tile::prelude::*;
+
+#[derive(Clone)]
+pub struct NewLayoutFromCurrentSessionScreen {
+ pub name_input: TextInput,
+ pub session_layout: String,
+ pub current_layout_metadata: LayoutMetadata,
+ pub editing_name: bool,
+ pub save_current_tab_only: bool,
+}
+
+impl NewLayoutFromCurrentSessionScreen {
+ pub fn handle_key(&mut self, key: KeyWithModifier) -> KeyResponse {
+ // Special Esc handling based on edit state
+ if key.bare_key == BareKey::Esc && key.has_no_modifiers() {
+ return self.handle_escape_key();
+ }
+
+ if self.editing_name {
+ self.handle_editing_mode_key(key)
+ } else {
+ self.handle_non_editing_mode_key(key)
+ }
+ }
+
+ fn handle_escape_key(&mut self) -> KeyResponse {
+ if self.editing_name {
+ self.editing_name = false;
+ KeyResponse::render()
+ } else {
+ KeyResponse::new_screen(self.cancel_save())
+ }
+ }
+
+ fn handle_editing_mode_key(&mut self, key: KeyWithModifier) -> KeyResponse {
+ let action = self.name_input.handle_key(key);
+
+ match action {
+ InputAction::Continue => KeyResponse::render(),
+ InputAction::Submit => self.exit_editing_mode(),
+ InputAction::Cancel => self.exit_editing_mode(),
+ InputAction::Complete => KeyResponse::none(),
+ InputAction::NoAction => KeyResponse::none(),
+ }
+ }
+
+ fn handle_non_editing_mode_key(&mut self, key: KeyWithModifier) -> KeyResponse {
+ match key.bare_key {
+ BareKey::Enter if key.has_no_modifiers() => self.attempt_save_current_session(),
+ BareKey::Tab if key.has_no_modifiers() => self.handle_tab_toggle(),
+ BareKey::Char('u') if key.has_no_modifiers() => self.handle_update_layout(),
+ BareKey::Char('r') if key.has_no_modifiers() => self.enter_editing_mode(),
+ _ => KeyResponse::none(),
+ }
+ }
+
+ fn exit_editing_mode(&mut self) -> KeyResponse {
+ self.editing_name = false;
+ KeyResponse::render()
+ }
+
+ fn enter_editing_mode(&mut self) -> KeyResponse {
+ self.editing_name = true;
+ KeyResponse::render()
+ }
+
+ fn handle_tab_toggle(&mut self) -> KeyResponse {
+ self.toggle_tab_target();
+ if let Some(new_screen) = self.update_layout_from_session() {
+ KeyResponse::new_screen(new_screen)
+ } else {
+ KeyResponse::render()
+ }
+ }
+
+ fn handle_update_layout(&mut self) -> KeyResponse {
+ if let Some(new_screen) = self.update_layout_from_session() {
+ KeyResponse::new_screen(new_screen)
+ } else {
+ KeyResponse::render()
+ }
+ }
+
+ fn update_layout_from_session(&mut self) -> Option<Screen> {
+ let Ok(focused_pane_info) = get_focused_pane_info() else {
+ eprintln!("Cannot retrieve focused tab info");
+ return None;
+ };
+ let focused_tab_index = focused_pane_info.0;
+ if self.save_current_tab_only {
+ match dump_session_layout_for_tab(focused_tab_index) {
+ Ok((session_layout, Some(session_layout_metadata))) => {
+ self.session_layout = session_layout;
+ self.current_layout_metadata = session_layout_metadata;
+ None
+ },
+ Ok((_, None)) => Some(Screen::Error(super::ErrorScreen {
+ message: "Failed to retrieve session layout metadata".to_string(),
+ return_to_screen: Box::new(Screen::LayoutList(Default::default())),
+ })),
+ Err(error_msg) => Some(Screen::Error(super::ErrorScreen {
+ message: format!("Failed to dump session layout: {}", error_msg),
+ return_to_screen: Box::new(Default::default()),
+ })),
+ }
+ } else {
+ match dump_session_layout() {
+ Ok((session_layout, Some(session_layout_metadata))) => {
+ self.session_layout = session_layout;
+ self.current_layout_metadata = session_layout_metadata;
+ None
+ },
+ Ok((_, None)) => Some(Screen::Error(super::ErrorScreen {
+ message: "Failed to retrieve session layout metadata".to_string(),
+ return_to_screen: Box::new(Screen::LayoutList(Default::default())),
+ })),
+ Err(error_msg) => Some(Screen::Error(super::ErrorScreen {
+ message: format!("Failed to dump session layout: {}", error_msg),
+ return_to_screen: Box::new(Default::default()),
+ })),
+ }
+ }
+ }
+
+ fn cancel_save(&self) -> Screen {
+ Screen::LayoutList(LayoutListScreen::default())
+ }
+
+ fn toggle_tab_target(&mut self) {
+ self.save_current_tab_only = !self.save_current_tab_only;
+ }
+
+ fn attempt_save_current_session(&self) -> KeyResponse {
+ let layout_name = self.determine_layout_name();
+
+ // Capture optimistic update BEFORE the API call
+ let optimistic = OptimisticUpdate::Add {
+ name: layout_name.clone(),
+ metadata: self.current_layout_metadata.clone(),
+ };
+
+ match self.save_session_layout(&layout_name) {
+ Ok(()) => KeyResponse::new_screen(Screen::LayoutList(LayoutListScreen::default()))
+ .with_optimistic(optimistic),
+ Err(error_msg) => KeyResponse::new_screen(self.create_error_screen(&error_msg)),
+ }
+ }
+
+ fn create_error_screen(&self, message: &str) -> Screen {
+ Screen::Error(ErrorScreen {
+ message: message.to_string(),
+ return_to_screen: Box::new(Screen::NewLayoutFromSession(self.clone())),
+ })
+ }
+
+ fn determine_layout_name(&self) -> String {
+ let input_text = self.name_input.get_text();
+ if input_text.is_empty() {
+ generate_random_name()
+ } else {
+ input_text.to_string()
+ }
+ }
+
+ fn save_session_layout(&self, layout_name: &str) -> Result<(), String> {
+ save_layout(layout_name.to_owned(), self.session_layout.clone(), false)
+ .map_err(|err| format!("Failed to save layout '{}': {}", layout_name, err))?;
+
+ eprintln!(
+ "Successfully saved current session as layout: {}",
+ layout_name
+ );
+ Ok(())
+ }
+
+ pub fn render(&self, rows: usize, cols: usize) {
+ let display_layout = DisplayLayout::Valid(LayoutInfo::File(
+ "current".to_string(),
+ self.current_layout_metadata.clone(),
+ ));
+
+ let layout_detail = LayoutDetail::new(&display_layout);
+
+ let desired_ui_width = std::cmp::max(
+ self.help_text_full().0.chars().count(),
+ self.max_description_width(),
+ );
+
+ let actual_ui_width = std::cmp::min(desired_ui_width, cols);
+
+ let available_cols_for_details = actual_ui_width.saturating_sub(2); // Account for indentation
+ let desired_details_height =
+ layout_detail.calculate_required_height(available_cols_for_details);
+
+ let desired_ui_height = 12 + desired_details_height;
+ let base_y = rows.saturating_sub(desired_ui_height) / 2;
+ let base_x = cols.saturating_sub(actual_ui_width) / 2;
+
+ let save_as_line_y = base_y + 5;
+ self.update_cursor_position(base_x, save_as_line_y, actual_ui_width); // NOTE: must be before any render
+ // happens
+
+ self.render_title(base_x, base_y, actual_ui_width);
+ self.render_description(base_x, base_y + 2, actual_ui_width);
+ self.render_save_as_line(base_x, save_as_line_y, actual_ui_width);
+ self.render_tab_toggle(base_x, base_y + 7, actual_ui_width);
+
+ let details_y = base_y + 9;
+
+ let actual_details_height = if rows < desired_ui_height {
+ rows.saturating_sub(details_y + 2)
+ } else {
+ desired_details_height
+ };
+
+ layout_detail.render(
+ base_x + 2,
+ details_y,
+ actual_details_height,
+ available_cols_for_details,
+ );
+
+ let help_y = details_y + actual_details_height + 1;
+ self.render_help_text(base_x, help_y, actual_ui_width);
+ }
+
+ fn render_title(&self, x: usize, y: usize, width: usize) {
+ let title = Text::new("Save Layout of Current Session").color_all(2);
+ print_text_with_coordinates(title, x, y, Some(width), None);
+ }
+
+ fn description_text_full(&self) -> (&str, &str) {
+ (
+ "This layout was created from the current session.",
+ "Save it to recreate the session later or share it with others.",
+ )
+ }
+ fn description_text_short(&self) -> (&str, &str) {
+ (
+ "Layout from current session.",
+ "Save it to recreate or share the later.",
+ )
+ }
+ fn render_description(&self, x: usize, y: usize, width: usize) {
+ let (line1, line2) = if width >= self.max_description_width() {
+ self.description_text_full()
+ } else {
+ self.description_text_short()
+ };
+ print_text_with_coordinates(Text::new(line1), x, y, None, None);
+ print_text_with_coordinates(Text::new(line2), x, y + 1, None, None);
+ }
+ fn max_description_width(&self) -> usize {
+ let (line1, line2) = self.description_text_full();
+ std::cmp::max(line1.chars().count(), line2.chars().count())
+ }
+
+ fn save_as_line_text(&self, width: usize) -> (String, usize) {
+ // (text, cursor_position_in_line)
+ let input_text = self.name_input.get_text();
+ let cursor_pos = self.name_input.get_cursor_position();
+
+ let display_name = if input_text.is_empty() && !self.editing_name {
+ "RANDOM"
+ } else {
+ input_text
+ };
+ let mut text = format!("Save as: {} (<r> Rename)", display_name);
+ let mut cursor_position_in_line = 9 + cursor_pos;
+ if text.chars().count() > width {
+ let truncated_display_name = truncate_with_ellipsis_start(
+ display_name,
+ width.saturating_sub(22), // size of text without the display name
+ );
+ text = format!("Save as: {} (<r> Rename)", truncated_display_name);
+ let truncated_len = truncated_display_name.chars().count();
+ cursor_position_in_line = 9 + cursor_pos.min(truncated_len);
+ }
+ (text, cursor_position_in_line)
+ }
+ fn render_save_as_line(&self, x: usize, y: usize, width: usize) {
+ let (text, _) = self.save_as_line_text(width);
+ let colored = Text::new(&text).color_substring(3, "<r>");
+
+ print_text_with_coordinates(colored, x, y, None, None);
+ }
+
+ fn render_tab_toggle(&self, x: usize, y: usize, width: usize) {
+ let text = if self.save_current_tab_only {
+ "<Tab> All Tabs | [Current Tab Only]"
+ } else {
+ "<Tab> [All Tabs] | Current Tab Only"
+ };
+
+ let short_text = if self.save_current_tab_only {
+ "<Tab> All | [Current Tab]"
+ } else {
+ "<Tab> [All] | Current Tab"
+ };
+ let text = if text.chars().count() > width {
+ short_text
+ } else {
+ text
+ };
+ let colored = Text::new(text)
+ .color_substring(3, "<Tab>")
+ .color_substring(0, "[All Tabs]")
+ .color_substring(0, "[All]")
+ .color_substring(0, "[Current Tab Only]")
+ .color_substring(0, "[Current Tab]");
+
+ print_text_with_coordinates(colored, x, y, None, None);
+ }
+
+ fn help_text_full(&self) -> (&str, &[&str]) {
+ (
+ "<Enter> - Save, <u> - Update Layout, <Esc> - Back",
+ &["<Enter>", "<u>", "<Esc>"],
+ )
+ }
+ fn help_text_short(&self) -> (&str, &[&str]) {
+ (
+ "<Enter> - Save, <u> - Update, <Esc> - Back",
+ &["<Enter>", "<u>", "<Esc>"],
+ )
+ }
+ fn render_help_text(&self, x: usize, y: usize, width: usize) {
+ let (text, items_to_color) = if width < self.help_text_full().0.chars().count() {
+ self.help_text_short()
+ } else {
+ self.help_text_full()
+ };
+ let mut text = Text::new(text);
+ for item in items_to_color {
+ text = text.color_substring(3, item)
+ }
+ print_text_with_coordinates(text, x, y, None, None);
+ }
+
+ pub fn update_cursor_position(&self, base_x: usize, cursor_y: usize, max_width: usize) {
+ if self.editing_name {
+ let (_, cursor_position_in_line) = self.save_as_line_text(max_width);
+ show_cursor(Some((base_x + cursor_position_in_line, cursor_y)))
+ } else {
+ show_cursor(None)
+ }
+ }
+}
diff --git a/default-plugins/layout-manager/src/screens/rename_layout.rs b/default-plugins/layout-manager/src/screens/rename_layout.rs
new file mode 100644
index 00000000..5ca48bb5
--- /dev/null
+++ b/default-plugins/layout-manager/src/screens/rename_layout.rs
@@ -0,0 +1,156 @@
+use super::{ErrorScreen, KeyResponse, LayoutListScreen, OptimisticUpdate, Screen};
+use crate::text_input::{InputAction, TextInput};
+use crate::ui::truncate_with_ellipsis_start;
+use zellij_tile::prelude::*;
+
+#[derive(Clone)]
+pub struct RenameLayoutScreen {
+ pub name_input: TextInput,
+ pub old_file_name: String,
+ pub selected_layout_index: usize,
+}
+
+impl RenameLayoutScreen {
+ pub fn new(old_file_name: String, selected_layout_index: usize) -> Self {
+ Self {
+ name_input: TextInput::new(old_file_name.clone()),
+ old_file_name,
+ selected_layout_index,
+ }
+ }
+
+ pub fn handle_key(&mut self, key: KeyWithModifier) -> KeyResponse {
+ let action = self.name_input.handle_key(key);
+
+ match action {
+ InputAction::Continue => KeyResponse::render(),
+ InputAction::Submit => self.attempt_rename(),
+ InputAction::Cancel => KeyResponse::new_screen(self.cancel_rename()),
+ InputAction::Complete => KeyResponse::none(),
+ InputAction::NoAction => KeyResponse::none(),
+ }
+ }
+
+ fn cancel_rename(&self) -> Screen {
+ Screen::LayoutList(LayoutListScreen::with_selected_index(
+ self.selected_layout_index,
+ ))
+ }
+
+ fn attempt_rename(&self) -> KeyResponse {
+ let name_input_text = self.name_input.get_text();
+
+ if name_input_text.is_empty() {
+ show_cursor(None);
+ return KeyResponse::new_screen(
+ self.create_error_screen("Layout name cannot be empty"),
+ );
+ }
+
+ let optimistic = OptimisticUpdate::Rename {
+ old_name: self.old_file_name.clone(),
+ new_name: name_input_text.to_string(),
+ };
+
+ match rename_layout(&self.old_file_name, name_input_text) {
+ Ok(_) => KeyResponse::new_screen(Screen::LayoutList(
+ LayoutListScreen::with_selected_index(self.selected_layout_index),
+ ))
+ .with_optimistic(optimistic),
+ Err(error_msg) => KeyResponse::new_screen(self.create_error_screen(&error_msg)),
+ }
+ }
+
+ fn create_error_screen(&self, message: &str) -> Screen {
+ show_cursor(None);
+ Screen::Error(ErrorScreen {
+ message: message.to_string(),
+ return_to_screen: Box::new(Screen::RenameLayout(self.clone())),
+ })
+ }
+
+ fn rename_line_text(&self, max_width: Option<usize>) -> (String, usize) {
+ // Returns (text, cursor_position_in_line)
+ let prompt = "Rename Layout: ";
+ let prompt_len = prompt.chars().count();
+
+ let input_text = self.name_input.get_text();
+ let cursor_pos = self.name_input.get_cursor_position();
+
+ let mut text = format!("{}{}", prompt, input_text);
+ let mut cursor_position_in_line = prompt_len + cursor_pos;
+
+ if let Some(max_width) = max_width {
+ if text.chars().count() > max_width {
+ let truncated_name =
+ truncate_with_ellipsis_start(input_text, max_width.saturating_sub(prompt_len));
+ text = format!("{}{}", prompt, truncated_name);
+ let truncated_len = truncated_name.chars().count();
+ cursor_position_in_line = prompt_len + cursor_pos.min(truncated_len);
+ }
+ }
+
+ (text, cursor_position_in_line)
+ }
+
+ fn render_rename_line(&self, x: usize, y: usize, width: usize) {
+ let (text, _) = self.rename_line_text(Some(width));
+ let colored = Text::new(&text);
+ print_text_with_coordinates(colored, x, y, None, None);
+ }
+
+ fn help_text(&self) -> (&str, &[&str]) {
+ ("<Enter> - Save, <Esc> - Cancel", &["<Enter>", "<Esc>"])
+ }
+
+ fn render_help_text(&self, x: usize, y: usize, _width: usize) {
+ let (text, items_to_color) = self.help_text();
+ let mut text_obj = Text::new(text);
+ for item in items_to_color {
+ text_obj = text_obj.color_substring(3, item);
+ }
+ print_text_with_coordinates(text_obj, x, y, None, None);
+ }
+
+ fn render_title(&self, x: usize, y: usize, width: usize) {
+ let title = Text::new("Rename Layout").color_all(2);
+ print_text_with_coordinates(title, x, y, Some(width), None);
+ }
+
+ fn update_cursor_position(&self, base_x: usize, cursor_y: usize, max_width: usize) {
+ // Always show cursor since rename is always in editing mode
+ let (_, cursor_position_in_line) = self.rename_line_text(Some(max_width));
+ show_cursor(Some((base_x + cursor_position_in_line, cursor_y)))
+ }
+
+ pub fn render(&self, rows: usize, cols: usize) {
+ // Calculate desired width based on rename line and help text
+ let desired_ui_width = std::cmp::max(
+ self.help_text().0.chars().count(),
+ self.rename_line_text(None).0.chars().count(),
+ );
+
+ // Leave at least 4 columns margin (2 on each side) to prevent text from reaching screen edge
+ let max_allowed_width = cols.saturating_sub(4);
+ let actual_ui_width = std::cmp::min(desired_ui_width, max_allowed_width);
+
+ // Calculate total height: title(1) + spacing(1) + rename_line(1) + spacing(1) + help(1) = 5
+ let desired_ui_height = 5;
+ let actual_ui_height = std::cmp::min(desired_ui_height, rows);
+
+ let base_y = rows.saturating_sub(actual_ui_height) / 2;
+ let base_x = cols.saturating_sub(actual_ui_width) / 2;
+
+ // Update cursor BEFORE rendering (critical!)
+ let rename_line_y = base_y + 2;
+ self.update_cursor_position(base_x, rename_line_y, actual_ui_width);
+
+ // Render components
+ self.render_title(base_x, base_y, actual_ui_width);
+ self.render_rename_line(base_x, rename_line_y, actual_ui_width);
+
+ // Help text at bottom
+ let help_y = base_y + 4;
+ self.render_help_text(base_x, help_y, actual_ui_width);
+ }
+}
diff --git a/default-plugins/layout-manager/src/text_input.rs b/default-plugins/layout-manager/src/text_input.rs
new file mode 100644
index 00000000..4e3a0b48
--- /dev/null
+++ b/default-plugins/layout-manager/src/text_input.rs
@@ -0,0 +1,1007 @@
+// This is a duplicate of the same file in the sequence (and possibly other) plugins
+// Right now there's no good place to put them for shared logic except the zellij-tile-utils crate
+// (which I'd rather eliminate), hence the duplication.
+use zellij_tile::prelude::*;
+
+const MAX_UNDO_STACK_SIZE: usize = 100;
+
+/// Action returned by TextInput after handling a key event
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(test, derive(Eq))]
+pub enum InputAction {
+ /// Continue editing
+ Continue,
+ /// User pressed Enter to submit
+ Submit,
+ /// User pressed Esc to cancel
+ Cancel,
+ /// User pressed Tab to request completion
+ Complete,
+ /// Key was not handled by the input
+ NoAction,
+}
+
+/// A reusable text input component with cursor support and standard editing keybindings
+#[derive(Debug, Clone)]
+pub struct TextInput {
+ buffer: String,
+ cursor_position: usize, // Character position (0-based), NOT byte position
+ undo_stack: Vec<(String, usize)>, // (buffer, cursor) snapshots
+ redo_stack: Vec<(String, usize)>,
+ last_edit_was_insert: bool, // For coalescing consecutive inserts
+}
+
+impl TextInput {
+ /// Create a new TextInput with the given initial text
+ /// Cursor is positioned at the end of the text
+ pub fn new(initial_text: String) -> Self {
+ let cursor_position = initial_text.chars().count();
+ Self {
+ buffer: initial_text,
+ cursor_position,
+ undo_stack: Vec::new(),
+ redo_stack: Vec::new(),
+ last_edit_was_insert: false,
+ }
+ }
+
+ /// Create an empty TextInput
+ pub fn empty() -> Self {
+ Self::new(String::new())
+ }
+
+ /// Get the current text
+ pub fn get_text(&self) -> &str {
+ &self.buffer
+ }
+
+ /// Get the cursor position (in characters, not bytes)
+ pub fn get_cursor_position(&self) -> usize {
+ self.cursor_position
+ }
+
+ /// Check if the input is empty
+ pub fn is_empty(&self) -> bool {
+ self.buffer.is_empty()
+ }
+
+ /// Get a shorthand for cursor_position
+ #[allow(unused)]
+ pub fn cursor_position(&self) -> usize {
+ self.cursor_position
+ }
+
+ /// Get mutable access to the underlying buffer for direct manipulation
+ #[allow(unused)]
+ pub fn get_text_mut(&mut self) -> &mut String {
+ &mut self.buffer
+ }
+
+ /// Set the text and move cursor to the end
+ #[allow(unused)]
+ pub fn set_text(&mut self, text: String) {
+ self.break_coalescing();
+ self.save_undo_state();
+ self.cursor_position = text.chars().count();
+ self.buffer = text;
+ }
+
+ /// Set cursor position (clamped to text length)
+ #[allow(unused)]
+ pub fn set_cursor_position(&mut self, pos: usize) {
+ let text_len = self.buffer.chars().count();
+ self.cursor_position = pos.min(text_len);
+ }
+
+ /// Clear all text and reset cursor
+ pub fn clear(&mut self) {
+ self.break_coalescing();
+ self.save_undo_state();
+ self.buffer.clear();
+ self.cursor_position = 0;
+ }
+
+ /// Insert a character at the current cursor position
+ pub fn insert_char(&mut self, c: char) {
+ self.save_undo_state_unless_coalescing();
+ // Convert cursor position (char index) to byte index
+ let byte_index = self.char_index_to_byte_index(self.cursor_position);
+ self.buffer.insert(byte_index, c);
+ self.cursor_position += 1;
+ }
+
+ /// Delete the character before the cursor (backspace)
+ pub fn backspace(&mut self) {
+ if self.cursor_position > 0 {
+ self.break_coalescing();
+ self.save_undo_state();
+ self.cursor_position -= 1;
+ let byte_index = self.char_index_to_byte_index(self.cursor_position);
+ self.buffer.remove(byte_index);
+ }
+ }
+
+ /// Delete the character at the cursor position (delete key)
+ pub fn delete(&mut self) {
+ let len = self.buffer.chars().count();
+ if self.cursor_position < len {
+ self.break_coalescing();
+ self.save_undo_state();
+ let byte_index = self.char_index_to_byte_index(self.cursor_position);
+ self.buffer.remove(byte_index);
+ }
+ }
+
+ /// Delete the word before the cursor (Ctrl/Alt + Backspace)
+ pub fn delete_word_backward(&mut self) {
+ if self.cursor_position == 0 {
+ return;
+ }
+
+ self.break_coalescing();
+ self.save_undo_state();
+
+ let old_position = self.cursor_position;
+ self.move_word_left();
+ let new_position = self.cursor_position;
+
+ // Delete from new position to old position
+ let start_byte = self.char_index_to_byte_index(new_position);
+ let end_byte = self.char_index_to_byte_index(old_position);
+ self.buffer.drain(start_byte..end_byte);
+ }
+
+ /// Delete the word after the cursor (Ctrl/Alt + Delete)
+ pub fn delete_word_forward(&mut self) {
+ let chars: Vec<char> = self.buffer.chars().collect();
+ let len = chars.len();
+
+ if self.cursor_position >= len {
+ return;
+ }
+
+ self.break_coalescing();
+ self.save_undo_state();
+
+ let start_position = self.cursor_position;
+ let mut end_position = start_position;
+
+ // Skip the current word
+ while end_position < len && !chars[end_position].is_whitespace() {
+ end_position += 1;
+ }
+
+ // Skip any whitespace after the word
+ while end_position < len && chars[end_position].is_whitespace() {
+ end_position += 1;
+ }
+
+ // Delete from start to end position
+ let start_byte = self.char_index_to_byte_index(start_position);
+ let end_byte = self.char_index_to_byte_index(end_position);
+ self.buffer.drain(start_byte..end_byte);
+ }
+
+ /// Move cursor one position to the left
+ pub fn move_left(&mut self) {
+ if self.cursor_position > 0 {
+ self.break_coalescing();
+ self.cursor_position -= 1;
+ }
+ }
+
+ /// Move cursor one position to the right
+ pub fn move_right(&mut self) {
+ let len = self.buffer.chars().count();
+ if self.cursor_position < len {
+ self.break_coalescing();
+ self.cursor_position += 1;
+ }
+ }
+
+ /// Move cursor to the start of the text (Ctrl-A / Home)
+ pub fn move_to_start(&mut self) {
+ self.break_coalescing();
+ self.cursor_position = 0;
+ }
+
+ /// Move cursor to the end of the text (Ctrl-E / End)
+ pub fn move_to_end(&mut self) {
+ self.break_coalescing();
+ self.cursor_position = self.buffer.chars().count();
+ }
+
+ /// Move cursor to the start of the previous word (Ctrl/Alt + Left)
+ pub fn move_word_left(&mut self) {
+ if self.cursor_position == 0 {
+ return;
+ }
+
+ self.break_coalescing();
+
+ let chars: Vec<char> = self.buffer.chars().collect();
+ let mut pos = self.cursor_position;
+
+ // Skip any whitespace immediately to the left
+ while pos > 0 && chars[pos - 1].is_whitespace() {
+ pos -= 1;
+ }
+
+ // Skip the word characters
+ while pos > 0 && !chars[pos - 1].is_whitespace() {
+ pos -= 1;
+ }
+
+ self.cursor_position = pos;
+ }
+
+ /// Move cursor to the start of the next word (Ctrl/Alt + Right)
+ pub fn move_word_right(&mut self) {
+ let chars: Vec<char> = self.buffer.chars().collect();
+ let len = chars.len();
+
+ if self.cursor_position >= len {
+ return;
+ }
+
+ self.break_coalescing();
+
+ let mut pos = self.cursor_position;
+
+ // Skip the current word
+ while pos < len && !chars[pos].is_whitespace() {
+ pos += 1;
+ }
+
+ // Skip any whitespace
+ while pos < len && chars[pos].is_whitespace() {
+ pos += 1;
+ }
+
+ self.cursor_position = pos;
+ }
+
+ /// Handle a key event and return the appropriate action
+ /// This is the main entry point for key handling
+ pub fn handle_key(&mut self, key: KeyWithModifier) -> InputAction {
+ // Check for Ctrl modifiers
+ if key.has_modifiers(&[KeyModifier::Ctrl]) {
+ match key.bare_key {
+ BareKey::Char('a') => {
+ self.move_to_start();
+ return InputAction::Continue;
+ },
+ BareKey::Char('e') => {
+ self.move_to_end();
+ return InputAction::Continue;
+ },
+ BareKey::Char('c') => {
+ // Ctrl-C clears the prompt
+ return InputAction::Cancel;
+ },
+ BareKey::Char('z') => {
+ // Ctrl-Z: Undo
+ self.undo();
+ return InputAction::Continue;
+ },
+ BareKey::Char('y') => {
+ // Ctrl-Y: Redo
+ self.redo();
+ return InputAction::Continue;
+ },
+ BareKey::Left => {
+ self.move_word_left();
+ return InputAction::Continue;
+ },
+ BareKey::Right => {
+ self.move_word_right();
+ return InputAction::Continue;
+ },
+ BareKey::Backspace => {
+ self.delete_word_backward();
+ return InputAction::Continue;
+ },
+ BareKey::Delete => {
+ self.delete_word_forward();
+ return InputAction::Continue;
+ },
+ _ => {},
+ }
+ }
+
+ // Check for Ctrl+Shift modifiers (alternative redo: Ctrl+Shift+Z)
+ if key.has_modifiers(&[KeyModifier::Ctrl, KeyModifier::Shift]) {
+ match key.bare_key {
+ BareKey::Char('Z') => {
+ // Ctrl-Shift-Z: Redo (alternative)
+ self.redo();
+ return InputAction::Continue;
+ },
+ _ => {},
+ }
+ }
+
+ // Check for Alt modifiers
+ if key.has_modifiers(&[KeyModifier::Alt]) {
+ match key.bare_key {
+ BareKey::Left => {
+ self.move_word_left();
+ return InputAction::Continue;
+ },
+ BareKey::Right => {
+ self.move_word_right();
+ return InputAction::Continue;
+ },
+ BareKey::Backspace => {
+ self.delete_word_backward();
+ return InputAction::Continue;
+ },
+ BareKey::Delete => {
+ self.delete_word_forward();
+ return InputAction::Continue;
+ },
+ _ => {},
+ }
+ }
+
+ // Handle bare keys (no modifiers)
+ match key.bare_key {
+ BareKey::Enter => InputAction::Submit,
+ BareKey::Esc => InputAction::Cancel,
+ BareKey::Tab => InputAction::Complete,
+ BareKey::Backspace => {
+ self.backspace();
+ InputAction::Continue
+ },
+ BareKey::Delete => {
+ self.delete();
+ InputAction::Continue
+ },
+ BareKey::Left => {
+ self.move_left();
+ InputAction::Continue
+ },
+ BareKey::Right => {
+ self.move_right();
+ InputAction::Continue
+ },
+ BareKey::Home => {
+ self.move_to_start();
+ InputAction::Continue
+ },
+ BareKey::End => {
+ self.move_to_end();
+ InputAction::Continue
+ },
+ BareKey::Char(c) => {
+ self.insert_char(c);
+ InputAction::Continue
+ },
+ _ => InputAction::NoAction,
+ }
+ }
+
+ /// Helper: Convert character index to byte index
+ fn char_index_to_byte_index(&self, char_index: usize) -> usize {
+ self.buffer
+ .char_indices()
+ .nth(char_index)
+ .map(|(byte_idx, _)| byte_idx)
+ .unwrap_or(self.buffer.len())
+ }
+
+ /// Save current state to undo stack before making changes
+ fn save_undo_state(&mut self) {
+ if self.undo_stack.len() >= MAX_UNDO_STACK_SIZE {
+ self.undo_stack.remove(0);
+ }
+ self.undo_stack
+ .push((self.buffer.clone(), self.cursor_position));
+ self.redo_stack.clear();
+ }
+
+ /// Save state only if not coalescing with previous insert
+ fn save_undo_state_unless_coalescing(&mut self) {
+ if !self.last_edit_was_insert {
+ self.save_undo_state();
+ }
+ self.last_edit_was_insert = true;
+ }
+
+ /// Mark that a non-insert edit occurred (breaks coalescing)
+ fn break_coalescing(&mut self) {
+ self.last_edit_was_insert = false;
+ }
+
+ /// Undo last change
+ pub fn undo(&mut self) -> bool {
+ if let Some((buffer, cursor)) = self.undo_stack.pop() {
+ self.redo_stack
+ .push((self.buffer.clone(), self.cursor_position));
+ self.buffer = buffer;
+ self.cursor_position = cursor;
+ self.break_coalescing();
+ true
+ } else {
+ false
+ }
+ }
+
+ /// Redo last undone change
+ pub fn redo(&mut self) -> bool {
+ if let Some((buffer, cursor)) = self.redo_stack.pop() {
+ self.undo_stack
+ .push((self.buffer.clone(), self.cursor_position));
+ self.buffer = buffer;
+ self.cursor_position = cursor;
+ self.break_coalescing();
+ true
+ } else {
+ false
+ }
+ }
+
+ /// Check if undo is available
+ #[allow(unused)]
+ pub fn can_undo(&self) -> bool {
+ !self.undo_stack.is_empty()
+ }
+
+ /// Check if redo is available
+ #[allow(unused)]
+ pub fn can_redo(&self) -> bool {
+ !self.redo_stack.is_empty()
+ }
+
+ #[allow(unused)]
+ pub fn drain_text(&mut self) -> String {
+ self.cursor_position = 0;
+ self.buffer.drain(..).collect()
+ }
+}
+
+// run with:
+// cargo test --lib --target x86_64-unknown-linux-gnu
+//
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_new_and_empty() {
+ let input = TextInput::new("hello".to_string());
+ assert_eq!(input.get_text(), "hello");
+ assert_eq!(input.get_cursor_position(), 5);
+
+ let empty = TextInput::empty();
+ assert_eq!(empty.get_text(), "");
+ assert_eq!(empty.get_cursor_position(), 0);
+ }
+
+ #[test]
+ fn test_insert_char() {
+ let mut input = TextInput::new("helo".to_string());
+ input.cursor_position = 3; // Position after "hel"
+ input.insert_char('l');
+ assert_eq!(input.get_text(), "hello");
+ assert_eq!(input.get_cursor_position(), 4);
+ }
+
+ #[test]
+ fn test_backspace() {
+ let mut input = TextInput::new("hello".to_string());
+ input.backspace();
+ assert_eq!(input.get_text(), "hell");
+ assert_eq!(input.get_cursor_position(), 4);
+
+ // Backspace at start does nothing
+ input.cursor_position = 0;
+ input.backspace();
+ assert_eq!(input.get_text(), "hell");
+ assert_eq!(input.get_cursor_position(), 0);
+ }
+
+ #[test]
+ fn test_delete() {
+ let mut input = TextInput::new("hello".to_string());
+ input.cursor_position = 0;
+ input.delete();
+ assert_eq!(input.get_text(), "ello");
+ assert_eq!(input.get_cursor_position(), 0);
+
+ // Delete at end does nothing
+ input.move_to_end();
+ input.delete();
+ assert_eq!(input.get_text(), "ello");
+ }
+
+ #[test]
+ fn test_cursor_movement() {
+ let mut input = TextInput::new("hello".to_string());
+ assert_eq!(input.get_cursor_position(), 5);
+
+ input.move_left();
+ assert_eq!(input.get_cursor_position(), 4);
+
+ input.move_right();
+ assert_eq!(input.get_cursor_position(), 5);
+
+ input.move_to_start();
+ assert_eq!(input.get_cursor_position(), 0);
+
+ input.move_to_end();
+ assert_eq!(input.get_cursor_position(), 5);
+ }
+
+ #[test]
+ fn test_unicode_support() {
+ let mut input = TextInput::new("hello 🦀 world".to_string());
+ assert_eq!(input.get_cursor_position(), 13); // 13 characters
+
+ input.cursor_position = 6; // After "hello "
+ input.insert_char('🐱');
+ assert_eq!(input.get_text(), "hello 🐱🦀 world");
+ }
+
+ #[test]
+ fn test_word_jump_right() {
+ let mut input = TextInput::new("hello world foo bar".to_string());
+ input.cursor_position = 0;
+
+ // Jump from start to "world"
+ input.move_word_right();
+ assert_eq!(input.get_cursor_position(), 6); // After "hello "
+
+ // Jump to "foo"
+ input.move_word_right();
+ assert_eq!(input.get_cursor_position(), 12); // After "world "
+
+ // Jump to "bar"
+ input.move_word_right();
+ assert_eq!(input.get_cursor_position(), 16); // After "foo "
+
+ // Jump to end
+ input.move_word_right();
+ assert_eq!(input.get_cursor_position(), 19); // At end
+ }
+
+ #[test]
+ fn test_word_jump_left() {
+ let mut input = TextInput::new("hello world foo bar".to_string());
+ input.move_to_end();
+ assert_eq!(input.get_cursor_position(), 19);
+
+ // Jump back to "bar"
+ input.move_word_left();
+ assert_eq!(input.get_cursor_position(), 16); // Start of "bar"
+
+ // Jump back to "foo"
+ input.move_word_left();
+ assert_eq!(input.get_cursor_position(), 12); // Start of "foo"
+
+ // Jump back to "world"
+ input.move_word_left();
+ assert_eq!(input.get_cursor_position(), 6); // Start of "world"
+
+ // Jump back to "hello"
+ input.move_word_left();
+ assert_eq!(input.get_cursor_position(), 0); // Start of "hello"
+ }
+
+ #[test]
+ fn test_word_jump_with_multiple_spaces() {
+ let mut input = TextInput::new("hello world".to_string());
+ input.cursor_position = 0;
+
+ // Jump over multiple spaces
+ input.move_word_right();
+ assert_eq!(input.get_cursor_position(), 8); // After "hello ", at start of "world"
+
+ // Jump back should skip spaces
+ input.move_word_left();
+ assert_eq!(input.get_cursor_position(), 0); // Back to start of "hello"
+ }
+
+ #[test]
+ fn test_word_jump_boundaries() {
+ let mut input = TextInput::new("test".to_string());
+
+ // At start - word left does nothing
+ input.cursor_position = 0;
+ input.move_word_left();
+ assert_eq!(input.get_cursor_position(), 0);
+
+ // At end - word right does nothing
+ input.move_to_end();
+ let end_pos = input.get_cursor_position();
+ input.move_word_right();
+ assert_eq!(input.get_cursor_position(), end_pos);
+ }
+
+ #[test]
+ fn test_up_down_arrows() {
+ let mut input = TextInput::new("hello world".to_string());
+
+ // Start in the middle
+ input.cursor_position = 5;
+ assert_eq!(input.get_cursor_position(), 5);
+
+ // Up arrow should go to start
+ input.move_to_start();
+ assert_eq!(input.get_cursor_position(), 0);
+
+ // Move back to middle
+ input.cursor_position = 5;
+
+ // Down arrow should go to end
+ input.move_to_end();
+ assert_eq!(input.get_cursor_position(), 11);
+ }
+
+ #[test]
+ fn test_delete_word_backward() {
+ let mut input = TextInput::new("hello world foo".to_string());
+
+ // Delete "foo" from end
+ input.move_to_end();
+ input.delete_word_backward();
+ assert_eq!(input.get_text(), "hello world ");
+ assert_eq!(input.get_cursor_position(), 12);
+
+ // Delete "world "
+ input.delete_word_backward();
+ assert_eq!(input.get_text(), "hello ");
+ assert_eq!(input.get_cursor_position(), 6);
+
+ // Delete "hello "
+ input.delete_word_backward();
+ assert_eq!(input.get_text(), "");
+ assert_eq!(input.get_cursor_position(), 0);
+
+ // Delete on empty buffer does nothing
+ input.delete_word_backward();
+ assert_eq!(input.get_text(), "");
+ assert_eq!(input.get_cursor_position(), 0);
+ }
+
+ #[test]
+ fn test_delete_word_backward_middle() {
+ let mut input = TextInput::new("hello world foo".to_string());
+
+ // Position in middle of "world"
+ input.cursor_position = 8; // After "hello wo"
+ input.delete_word_backward();
+ assert_eq!(input.get_text(), "hello rld foo");
+ assert_eq!(input.get_cursor_position(), 6); // After "hello "
+ }
+
+ #[test]
+ fn test_delete_word_forward() {
+ let mut input = TextInput::new("hello world foo".to_string());
+
+ // Delete "hello " from start
+ input.cursor_position = 0;
+ input.delete_word_forward();
+ assert_eq!(input.get_text(), "world foo");
+ assert_eq!(input.get_cursor_position(), 0);
+
+ // Delete "world "
+ input.delete_word_forward();
+ assert_eq!(input.get_text(), "foo");
+ assert_eq!(input.get_cursor_position(), 0);
+
+ // Delete "foo"
+ input.delete_word_forward();
+ assert_eq!(input.get_text(), "");
+ assert_eq!(input.get_cursor_position(), 0);
+
+ // Delete on empty buffer does nothing
+ input.delete_word_forward();
+ assert_eq!(input.get_text(), "");
+ assert_eq!(input.get_cursor_position(), 0);
+ }
+
+ #[test]
+ fn test_delete_word_forward_middle() {
+ let mut input = TextInput::new("hello world foo".to_string());
+
+ // Position in middle of "world"
+ input.cursor_position = 8; // After "hello wo"
+ input.delete_word_forward();
+ assert_eq!(input.get_text(), "hello wofoo");
+ assert_eq!(input.get_cursor_position(), 8); // Same position, text deleted forward
+ }
+
+ #[test]
+ fn test_delete_word_with_multiple_spaces() {
+ let mut input = TextInput::new("hello world".to_string());
+
+ // Delete forward includes trailing spaces
+ input.cursor_position = 0;
+ input.delete_word_forward();
+ assert_eq!(input.get_text(), "world");
+ assert_eq!(input.get_cursor_position(), 0);
+ }
+
+ #[test]
+ fn test_undo_redo_basic() {
+ let mut input = TextInput::empty();
+
+ // Type "hello"
+ input.insert_char('h');
+ input.insert_char('e');
+ input.insert_char('l');
+ input.insert_char('l');
+ input.insert_char('o');
+ assert_eq!(input.get_text(), "hello");
+
+ // Undo should remove all characters (coalesced into one undo entry)
+ assert!(input.can_undo());
+ assert!(input.undo());
+ assert_eq!(input.get_text(), "");
+ assert_eq!(input.get_cursor_position(), 0);
+
+ // Redo should restore "hello"
+ assert!(input.can_redo());
+ assert!(input.redo());
+ assert_eq!(input.get_text(), "hello");
+ assert_eq!(input.get_cursor_position(), 5);
+ }
+
+ #[test]
+ fn test_undo_coalescing_breaks_on_cursor_move() {
+ let mut input = TextInput::empty();
+
+ // Type "he"
+ input.insert_char('h');
+ input.insert_char('e');
+
+ // Move cursor to start (breaks coalescing)
+ input.move_to_start();
+
+ // Type "llo"
+ input.insert_char('l');
+ input.insert_char('l');
+ input.insert_char('o');
+
+ assert_eq!(input.get_text(), "llohe");
+
+ // First undo removes "llo" (second coalesced group)
+ input.undo();
+ assert_eq!(input.get_text(), "he");
+
+ // Second undo removes "he" (first coalesced group)
+ input.undo();
+ assert_eq!(input.get_text(), "");
+ }
+
+ #[test]
+ fn test_undo_backspace() {
+ let mut input = TextInput::new("hello".to_string());
+
+ // Backspace once
+ input.backspace();
+ assert_eq!(input.get_text(), "hell");
+
+ // Undo should restore "hello"
+ input.undo();
+ assert_eq!(input.get_text(), "hello");
+ assert_eq!(input.get_cursor_position(), 5);
+ }
+
+ #[test]
+ fn test_undo_delete() {
+ let mut input = TextInput::new("hello".to_string());
+ input.cursor_position = 0;
+
+ // Delete first character
+ input.delete();
+ assert_eq!(input.get_text(), "ello");
+
+ // Undo should restore "hello"
+ input.undo();
+ assert_eq!(input.get_text(), "hello");
+ assert_eq!(input.get_cursor_position(), 0);
+ }
+
+ #[test]
+ fn test_undo_word_delete() {
+ let mut input = TextInput::new("hello world".to_string());
+
+ // Delete "world" backward
+ input.delete_word_backward();
+ assert_eq!(input.get_text(), "hello ");
+
+ // Undo should restore "hello world"
+ input.undo();
+ assert_eq!(input.get_text(), "hello world");
+ assert_eq!(input.get_cursor_position(), 11);
+ }
+
+ #[test]
+ fn test_undo_clear() {
+ let mut input = TextInput::new("hello world".to_string());
+
+ // Clear the buffer
+ input.clear();
+ assert_eq!(input.get_text(), "");
+
+ // Undo should restore the text
+ input.undo();
+ assert_eq!(input.get_text(), "hello world");
+ }
+
+ #[test]
+ fn test_undo_set_text() {
+ let mut input = TextInput::new("hello".to_string());
+
+ // Replace with new text
+ input.set_text("goodbye".to_string());
+ assert_eq!(input.get_text(), "goodbye");
+
+ // Undo should restore "hello"
+ input.undo();
+ assert_eq!(input.get_text(), "hello");
+ }
+
+ #[test]
+ fn test_redo_clears_on_new_edit() {
+ let mut input = TextInput::empty();
+
+ // Type "hello"
+ input.insert_char('h');
+ input.insert_char('e');
+ input.insert_char('l');
+ input.insert_char('l');
+ input.insert_char('o');
+
+ // Undo
+ input.undo();
+ assert_eq!(input.get_text(), "");
+ assert!(input.can_redo());
+
+ // Make a new edit (should clear redo stack)
+ input.insert_char('x');
+ assert!(!input.can_redo());
+ }
+
+ #[test]
+ fn test_multiple_undo_redo() {
+ let mut input = TextInput::empty();
+
+ // First edit: type "hello"
+ for c in "hello".chars() {
+ input.insert_char(c);
+ }
+
+ // Break coalescing
+ input.move_left();
+
+ // Second edit: type "world"
+ for c in "world".chars() {
+ input.insert_char(c);
+ }
+
+ assert_eq!(input.get_text(), "hellworldo");
+
+ // Undo "world"
+ input.undo();
+ assert_eq!(input.get_text(), "hello");
+
+ // Undo "hello"
+ input.undo();
+ assert_eq!(input.get_text(), "");
+
+ // Redo "hello"
+ input.redo();
+ assert_eq!(input.get_text(), "hello");
+
+ // Redo "world"
+ input.redo();
+ assert_eq!(input.get_text(), "hellworldo");
+ }
+
+ #[test]
+ fn test_undo_stack_limit() {
+ let mut input = TextInput::empty();
+
+ // Perform 102 separate edits (breaking coalescing each time)
+ for _i in 0..102 {
+ input.backspace(); // Break coalescing
+ input.insert_char('x');
+ }
+
+ // Should have at most 100 undo entries
+ let mut undo_count = 0;
+ while input.undo() {
+ undo_count += 1;
+ }
+
+ // We should have 100 undo entries (the stack limit)
+ // Plus the final state change from the last coalescing break
+ assert!(
+ undo_count <= 100,
+ "Undo count should be at most 100, got {}",
+ undo_count
+ );
+ }
+
+ #[test]
+ fn test_undo_redo_empty_stack() {
+ let mut input = TextInput::empty();
+
+ // Undo on empty stack should return false
+ assert!(!input.can_undo());
+ assert!(!input.undo());
+
+ // Redo on empty stack should return false
+ assert!(!input.can_redo());
+ assert!(!input.redo());
+ }
+
+ #[test]
+ fn test_undo_restores_cursor_position() {
+ let mut input = TextInput::new("hello world".to_string());
+
+ // Move cursor to position 5 (before "world")
+ input.cursor_position = 5;
+
+ // Insert a space
+ input.insert_char(' ');
+ assert_eq!(input.get_text(), "hello world");
+ assert_eq!(input.get_cursor_position(), 6);
+
+ // Undo should restore both text and cursor position
+ input.undo();
+ assert_eq!(input.get_text(), "hello world");
+ assert_eq!(input.get_cursor_position(), 5);
+ }
+
+ #[test]
+ fn test_coalescing_consecutive_inserts() {
+ let mut input = TextInput::empty();
+
+ // Type several characters
+ input.insert_char('a');
+ input.insert_char('b');
+ input.insert_char('c');
+
+ assert_eq!(input.get_text(), "abc");
+
+ // Single undo should remove all three (they were coalesced)
+ input.undo();
+ assert_eq!(input.get_text(), "");
+
+ // No more undo available
+ assert!(!input.can_undo());
+ }
+
+ #[test]
+ fn test_backspace_breaks_coalescing() {
+ let mut input = TextInput::empty();
+
+ // Type "ab"
+ input.insert_char('a');
+ input.insert_char('b');
+
+ // Backspace
+ input.backspace();
+ assert_eq!(input.get_text(), "a");
+
+ // Type "c"
+ input.insert_char('c');
+ assert_eq!(input.get_text(), "ac");
+
+ // Undo should remove just "c"
+ input.undo();
+ assert_eq!(input.get_text(), "a");
+
+ // Undo should remove backspace operation
+ input.undo();
+ assert_eq!(input.get_text(), "ab");
+
+ // Undo should remove "ab"
+ input.undo();
+ assert_eq!(input.get_text(), "");
+ }
+}
diff --git a/default-plugins/layout-manager/src/ui/layout_detail_view.rs b/default-plugins/layout-manager/src/ui/layout_detail_view.rs
new file mode 100644
index 00000000..2095d38f
--- /dev/null
+++ b/default-plugins/layout-manager/src/ui/layout_detail_view.rs
@@ -0,0 +1,345 @@
+use super::text_utils::{get_layout_display_info, truncate_with_ellipsis, wrap_text_to_width};
+use crate::DisplayLayout;
+use zellij_tile::prelude::*;
+
+pub struct LayoutDetail<'a> {
+ layout: &'a DisplayLayout,
+}
+
+impl<'a> LayoutDetail<'a> {
+ pub fn new(layout: &'a DisplayLayout) -> Self {
+ Self { layout }
+ }
+
+ pub fn render(&self, x: usize, y: usize, max_rows: usize, max_cols: usize) {
+ match self.layout {
+ DisplayLayout::Valid(_) => {
+ self.render_valid_layout(self.layout, x, y, max_rows, max_cols)
+ },
+ DisplayLayout::Error {
+ name: _,
+ error: _,
+ error_message,
+ } => self.render_error(error_message, x, y, max_rows, max_cols),
+ }
+ }
+
+ pub fn calculate_required_height(&self, max_cols: usize) -> usize {
+ match self.layout {
+ DisplayLayout::Valid(_) => {
+ let (_, metadata_opt) = get_layout_display_info(self.layout);
+ let Some(metadata) = metadata_opt else {
+ return 1; // No metadata available
+ };
+ self.calculate_content_lines(metadata, max_cols)
+ },
+ DisplayLayout::Error { .. } => {
+ 3 // Error message + hint line
+ },
+ }
+ }
+
+ fn calculate_content_lines(&self, metadata: &LayoutMetadata, max_cols: usize) -> usize {
+ if !self.should_show_tabs_section(metadata) {
+ // Panes only
+ let panes_lines = self.prepare_panes_content(metadata, max_cols);
+ panes_lines.len()
+ } else {
+ // Tabs and panes side-by-side
+ let (left_width, right_width, _) = self.calculate_column_layout(max_cols);
+ let tabs_lines = self.prepare_tabs_content(metadata, left_width);
+ let panes_lines = self.prepare_panes_content(metadata, right_width);
+ tabs_lines.len().max(panes_lines.len())
+ }
+ }
+
+ fn render_valid_layout(
+ &self,
+ layout: &DisplayLayout,
+ x: usize,
+ y: usize,
+ max_rows: usize,
+ max_cols: usize,
+ ) {
+ let (name, metadata_opt) = get_layout_display_info(layout);
+ let Some(metadata) = metadata_opt else {
+ if layout.is_builtin() {
+ self.render_built_in_indication(&name, x, y, max_rows, max_cols);
+ } else {
+ self.render_no_metadata(x, y);
+ }
+ return;
+ };
+
+ self.render_tabs_and_panes(metadata, x, y, max_rows, max_cols);
+ }
+
+ fn render_no_metadata(&self, x: usize, y: usize) {
+ let msg = Text::new("No metadata available").color_all(1);
+ print_text_with_coordinates(msg, x, y + 1, None, None);
+ }
+ fn render_built_in_indication(
+ &self,
+ name: &str,
+ x: usize,
+ y: usize,
+ max_rows: usize,
+ max_cols: usize,
+ ) {
+ if max_rows == 0 {
+ return;
+ }
+
+ let full_text = format!("{} is a built-in layout. Create your own layouts to automate or share workspace setup.", name);
+ let wrapped_lines = wrap_text_to_width(&full_text, max_cols);
+
+ let mut current_y = y;
+ for line in wrapped_lines.iter().take(max_rows) {
+ let text = if line.contains(name) {
+ Text::new(line).color_substring(1, name)
+ } else {
+ Text::new(line)
+ };
+ print_text_with_coordinates(text, x, current_y, None, None);
+ current_y += 1;
+ }
+ }
+
+ fn render_error(
+ &self,
+ error_message: &str,
+ x: usize,
+ y: usize,
+ max_rows: usize,
+ max_cols: usize,
+ ) {
+ if max_rows == 0 {
+ return;
+ }
+
+ let wrapped_lines = wrap_text_to_width(error_message, max_cols);
+ let available_rows = max_rows.saturating_sub(3);
+
+ let mut current_y = y + 1; // + 1 (and saturating_sub(3) above) to be aligned witht he
+ // table on the left
+ for line in wrapped_lines.iter().take(available_rows) {
+ let text = Text::new(line).error_color_all();
+ print_text_with_coordinates(text, x, current_y, None, None);
+ current_y += 1;
+ }
+
+ let hint = Text::new("<m> - Show detailed error").color_substring(3, "<m>");
+ print_text_with_coordinates(hint, x, current_y + 1, None, None); // 1 for gap
+ }
+
+ fn render_tabs_and_panes(
+ &self,
+ metadata: &LayoutMetadata,
+ x: usize,
+ y: usize,
+ max_rows: usize,
+ max_cols: usize,
+ ) {
+ // let max_rows = max_rows.saturating_sub(1);
+
+ if !self.should_show_tabs_section(metadata) {
+ self.render_panes_only(metadata, x, y, max_rows, max_cols);
+ return;
+ }
+
+ let (left_width, right_width, padding) = self.calculate_column_layout(max_cols);
+ let right_x = x + left_width + padding;
+
+ let tabs_lines = self.prepare_tabs_content(metadata, left_width);
+ let panes_lines = self.prepare_panes_content(metadata, right_width);
+
+ let (tabs_truncated, panes_truncated) =
+ self.render_side_by_side_columns(&panes_lines, &tabs_lines, x, right_x, y, max_rows);
+
+ if tabs_truncated {
+ self.render_truncation_ellipsis(x, y + max_rows);
+ }
+ if panes_truncated {
+ self.render_truncation_ellipsis(right_x, y + max_rows);
+ }
+ }
+
+ fn render_side_by_side_columns(
+ &self,
+ left_lines: &[String],
+ right_lines: &[String],
+ left_x: usize,
+ right_x: usize,
+ start_y: usize,
+ max_rows: usize,
+ ) -> (bool, bool) {
+ let max_lines = left_lines.len().max(right_lines.len());
+ let mut left_truncated = false;
+ let mut right_truncated = false;
+
+ for line_idx in 0..=max_lines {
+ if line_idx >= max_rows {
+ if line_idx < left_lines.len() {
+ left_truncated = true;
+ }
+ if line_idx < right_lines.len() {
+ right_truncated = true;
+ }
+ break;
+ }
+
+ let current_y = start_y + line_idx;
+
+ if let Some(line) = left_lines.get(line_idx) {
+ self.render_colored_line(line, left_x, current_y, line_idx == 0, 2);
+ }
+
+ if let Some(line) = right_lines.get(line_idx) {
+ self.render_colored_line(line, right_x, current_y, line_idx == 0, 2);
+ }
+ }
+
+ (left_truncated, right_truncated)
+ }
+
+ fn should_show_tabs_section(&self, metadata: &LayoutMetadata) -> bool {
+ metadata.tabs.len() > 1
+ }
+
+ fn prepare_tabs_content(&self, metadata: &LayoutMetadata, max_width: usize) -> Vec<String> {
+ let mut lines = Vec::new();
+
+ // Add title
+ let title = "Tabs:";
+ lines.push(truncate_with_ellipsis(title, max_width));
+
+ // Add tabs
+ for (i, tab) in metadata.tabs.iter().enumerate() {
+ let tab_label = if let Some(name) = &tab.name {
+ name.clone()
+ } else {
+ format!("Tab {}", i + 1)
+ };
+
+ // Account for " - " prefix (4 characters)
+ let available_width = max_width.saturating_sub(4);
+ let truncated_label = truncate_with_ellipsis(&tab_label, available_width);
+ lines.push(format!(" - {}", truncated_label));
+ }
+
+ lines
+ }
+
+ fn prepare_panes_content(&self, metadata: &LayoutMetadata, max_width: usize) -> Vec<String> {
+ let mut lines = Vec::new();
+
+ let (named_panes, terminal_count) = self.collect_pane_info(metadata);
+
+ // Early return if nothing to show
+ if !self.has_panes_to_show(&named_panes, terminal_count) {
+ return lines;
+ }
+
+ // Add title
+ let title = "Panes:";
+ lines.push(truncate_with_ellipsis(title, max_width));
+
+ // Add named panes
+ for pane_name in &named_panes {
+ let available_width = max_width.saturating_sub(4);
+ let truncated_name = truncate_with_ellipsis(pane_name, available_width);
+ lines.push(format!(" - {}", truncated_name));
+ }
+
+ // Add terminal count if any
+ if terminal_count > 0 {
+ let text = if named_panes.is_empty() {
+ format!(" {} Terminals", terminal_count)
+ } else {
+ format!(" +{} Terminals", terminal_count)
+ };
+ let truncated_text = truncate_with_ellipsis(&text, max_width);
+ lines.push(truncated_text);
+ }
+
+ lines
+ }
+
+ fn calculate_column_layout(&self, max_cols: usize) -> (usize, usize, usize) {
+ let left_width = (max_cols - 1) / 2;
+ let padding = 1;
+ let right_width = max_cols - left_width - padding;
+ (left_width, right_width, padding)
+ }
+
+ fn render_colored_line(
+ &self,
+ line: &str,
+ x: usize,
+ y: usize,
+ is_title: bool,
+ title_color: usize,
+ ) {
+ let text = if is_title {
+ Text::new(line).color_all(title_color)
+ } else {
+ Text::new(line)
+ };
+ print_text_with_coordinates(text, x, y, None, None);
+ }
+
+ fn render_truncation_ellipsis(&self, x: usize, y: usize) {
+ let ellipsis = Text::new(" [...]").color_all(1);
+ print_text_with_coordinates(ellipsis, x, y, None, None);
+ }
+
+ fn render_panes_only(
+ &self,
+ metadata: &LayoutMetadata,
+ x: usize,
+ y: usize,
+ max_rows: usize,
+ max_cols: usize,
+ ) {
+ let panes_lines = self.prepare_panes_content(metadata, max_cols);
+
+ let mut was_truncated = false;
+ for (line_idx, pane_line) in panes_lines.iter().enumerate() {
+ if line_idx >= max_rows {
+ was_truncated = true;
+ break;
+ }
+
+ let current_y = y + line_idx;
+ self.render_colored_line(pane_line, x, current_y, line_idx == 0, 2);
+ }
+
+ if was_truncated {
+ self.render_truncation_ellipsis(x, y + max_rows);
+ }
+ }
+
+ fn has_panes_to_show(&self, named_panes: &[String], terminal_count: usize) -> bool {
+ !named_panes.is_empty() || terminal_count > 0
+ }
+
+ fn collect_pane_info(&self, metadata: &LayoutMetadata) -> (Vec<String>, usize) {
+ let mut named_panes = Vec::new();
+ let mut terminal_count = 0;
+
+ for tab in &metadata.tabs {
+ for pane in &tab.panes {
+ // Skip builtin plugin panes
+ if pane.is_builtin_plugin {
+ continue;
+ }
+ match &pane.name {
+ Some(name) => named_panes.push(name.clone()),
+ None => terminal_count += 1,
+ }
+ }
+ }
+
+ (named_panes, terminal_count)
+ }
+}
diff --git a/default-plugins/layout-manager/src/ui/layout_table.rs b/default-plugins/layout-manager/src/ui/layout_table.rs
new file mode 100644
index 00000000..2f22f06e
--- /dev/null
+++ b/default-plugins/layout-manager/src/ui/layout_table.rs
@@ -0,0 +1,452 @@
+use super::text_utils::{
+ get_last_modified_string, get_layout_display_info, truncate_with_ellipsis,
+};
+use crate::DisplayLayout;
+use zellij_tile::prelude::*;
+
+fn create_table_row(
+ name: &str,
+ metadata: Option<&LayoutMetadata>,
+ is_error: bool,
+ is_builtin: bool,
+ max_name_width: Option<usize>,
+ matched_indices: Option<&Vec<usize>>,
+) -> Vec<Text> {
+ let display_name = if let Some(max_width) = max_name_width {
+ truncate_with_ellipsis(name, max_width)
+ } else {
+ name.to_string()
+ };
+
+ let name_text = if let Some(indices) = matched_indices {
+ // Create text with highlighted matched characters
+ let mut text = Text::new(&display_name);
+
+ // Apply base color to entire text
+ text = if is_error {
+ text.error_color_all()
+ } else {
+ text.color_all(1)
+ };
+
+ text = text.color_indices(3, indices.iter().cloned().collect());
+
+ text
+ } else {
+ // No matches, use original coloring
+ if is_error {
+ Text::new(&display_name).error_color_all()
+ } else {
+ Text::new(&display_name).color_all(1)
+ }
+ };
+
+ let last_modified_string = get_last_modified_string(metadata, is_builtin);
+ let last_modified = if is_builtin {
+ Text::new(&last_modified_string).color_all(0)
+ } else {
+ Text::new(&last_modified_string)
+ };
+
+ vec![name_text, last_modified]
+}
+
+fn apply_selection(mut row: Vec<Text>) -> Vec<Text> {
+ for cell in &mut row {
+ *cell = std::mem::take(cell).selected();
+ }
+ row
+}
+
+pub struct LayoutsTable {
+ display_layouts: Vec<DisplayLayout>,
+ selected_layout_index: usize,
+ hidden_above: usize,
+ hidden_below: usize,
+ matched_indices: Vec<Option<Vec<usize>>>,
+}
+
+impl LayoutsTable {
+ pub fn new(
+ display_layouts: Vec<DisplayLayout>,
+ selected_layout_index: usize,
+ hidden_above: usize,
+ hidden_below: usize,
+ ) -> Self {
+ Self {
+ display_layouts,
+ selected_layout_index,
+ hidden_above,
+ hidden_below,
+ matched_indices: Vec::new(),
+ }
+ }
+
+ pub fn with_matched_indices(mut self, matched_indices: Vec<Option<Vec<usize>>>) -> Self {
+ self.matched_indices = matched_indices;
+ self
+ }
+
+ pub fn render(&self, x: usize, y: usize, _max_rows: usize, max_cols: usize) {
+ let mut table = match self.overflow_indicator(self.hidden_above) {
+ Some(overflow_indicator) => {
+ Table::new().add_styled_row(vec![Text::new(" "), overflow_indicator])
+ },
+ None => Table::new().add_row(vec![" ", " "]),
+ };
+
+ // Calculate the actual width needed for the "last modified" column
+ let max_last_modified_width = self
+ .display_layouts
+ .iter()
+ .map(|layout| {
+ let (_, metadata_opt) = get_layout_display_info(layout);
+ get_last_modified_string(metadata_opt, layout.is_builtin()).len()
+ })
+ .max()
+ .unwrap_or(1);
+
+ let table_padding = 2;
+ let max_name_width = if max_cols > max_last_modified_width + table_padding {
+ Some(max_cols.saturating_sub(max_last_modified_width + table_padding))
+ } else {
+ Some(max_cols.saturating_div(2))
+ };
+
+ for (i, layout) in self.display_layouts.iter().enumerate() {
+ let (name, metadata_opt) = get_layout_display_info(layout);
+ let matched_indices = self.matched_indices.get(i).and_then(|opt| opt.as_ref());
+ let mut row = create_table_row(
+ &name,
+ metadata_opt,
+ layout.is_error(),
+ layout.is_builtin(),
+ max_name_width,
+ matched_indices,
+ );
+
+ if i == self.selected_layout_index {
+ row = apply_selection(row);
+ }
+
+ table = table.add_styled_row(row);
+ }
+
+ if let Some(overflow_indicator) = self.overflow_indicator(self.hidden_below) {
+ table = table.add_styled_row(vec![Text::new(" "), overflow_indicator]);
+ }
+
+ print_table_with_coordinates(table, x, y, Some(max_cols), None);
+ }
+ fn overflow_indicator(&self, count: usize) -> Option<Text> {
+ if count > 0 {
+ Some(Text::new(format!("[+{}]", count)).color_all(2))
+ } else {
+ None
+ }
+ }
+}
+
+fn color_control_text(text: &str, keys: &[&str]) -> Text {
+ let mut colored_text = Text::new(text);
+ for key in keys {
+ colored_text = colored_text.color_substring(3, key)
+ }
+ colored_text
+}
+
+pub struct Controls {
+ retain_terminal_panes: bool,
+ retain_plugin_panes: bool,
+ apply_only_to_active_tab: bool,
+ show_more_options: bool,
+ typing_filter: bool,
+ filter_active: bool,
+}
+
+impl Controls {
+ pub fn new(
+ retain_terminal_panes: bool,
+ retain_plugin_panes: bool,
+ apply_only_to_active_tab: bool,
+ show_more_options: bool,
+ typing_filter: bool,
+ filter_active: bool,
+ ) -> Self {
+ Self {
+ retain_terminal_panes,
+ retain_plugin_panes,
+ apply_only_to_active_tab,
+ show_more_options,
+ typing_filter,
+ filter_active,
+ }
+ }
+
+ fn get_typing_filter_controls(&self, max_cols: usize) -> (&str, &[&str]) {
+ let long_text = "- <Enter> - accept filter";
+ let short_text = "<Enter> - accept filter";
+ let minimum_text = "<Enter> ...";
+ let text = if max_cols >= long_text.chars().count() {
+ long_text
+ } else if max_cols >= short_text.chars().count() {
+ short_text
+ } else {
+ minimum_text
+ };
+ (text, &["<Enter>"])
+ }
+
+ fn get_filter_active_controls(&self, max_cols: usize) -> (&str, &[&str]) {
+ let long_text = "- <Enter> Open, <↓↑> Nav, <e> Edit, <r> Rename, <Del> - Delete";
+ let short_text = "<Enter>/<↓↑>/<e>/<r>/<Del> Open/Nav/Edit/Rename/Del";
+ let minimum_text = "<Enter>/<↓↑>/<e>/<r>/<Del> ...";
+ let text = if max_cols >= long_text.chars().count() {
+ long_text
+ } else if max_cols >= short_text.chars().count() {
+ short_text
+ } else {
+ minimum_text
+ };
+ (text, &["<Enter>", "<↓↑>", "<e>", "<r>", "<Del>"])
+ }
+
+ fn get_default_controls(&self, max_cols: usize) -> (&str, &[&str]) {
+ let long_text = "- <Enter> Open, <↓↑> Nav, </> Filter, <e> Edit, <r> Rename, <Del> - Del";
+ let short_text = "<Enter>/<↓↑>/</>/<e>/<r>/<Del> Open/Nav/Filter/Edit/Rename/Del";
+ let minimum_text = "<Enter>/<↓↑>/</>/<e>/<r>/<Del> ...";
+ let text = if max_cols >= long_text.chars().count() {
+ long_text
+ } else if max_cols >= short_text.chars().count() {
+ short_text
+ } else {
+ minimum_text
+ };
+ (text, &["<Enter>", "<↓↑>", "</>", "<e>", "<r>", "<Del>"])
+ }
+
+ fn get_basic_controls_text_and_keys(&self, max_cols: usize) -> (&str, &[&str]) {
+ if self.filter_active {
+ self.get_filter_active_controls(max_cols)
+ } else {
+ self.get_default_controls(max_cols)
+ }
+ }
+
+ fn get_override_text_and_keys(&self, max_cols: usize) -> (String, &[&str]) {
+ let toggle_word = if self.show_more_options {
+ "less"
+ } else {
+ "more"
+ };
+ let long_text = format!(
+ "- <Tab> Override Session Layout, <?> {} options",
+ toggle_word
+ );
+ let short_text = format!("<Tab> Override, <?> {} options", toggle_word);
+ let minimum_text = format!("<Tab>/<?> ...");
+ let text = if max_cols >= long_text.chars().count() {
+ long_text
+ } else if max_cols >= short_text.chars().count() {
+ short_text
+ } else {
+ minimum_text
+ };
+ (text, &["<Tab>", "<?>"])
+ }
+
+ fn get_new_layout_text_and_keys(&self, max_cols: usize) -> (&str, &[&str]) {
+ let long_text = "- New Layout: <n> from current session, <i> import";
+ let short_text = "New: <n> current, <i> import";
+ let minimum_text = "New: <n>/<N>/<i> ...";
+ let text = if max_cols >= long_text.chars().count() {
+ long_text
+ } else if max_cols >= short_text.chars().count() {
+ short_text
+ } else {
+ minimum_text
+ };
+ (text, &["<n>", "<i>"])
+ }
+
+ pub fn calculate_width(&self, max_cols: usize) -> usize {
+ let mut width = 0;
+
+ // Line 1: Basic controls
+ let (basic_text, _) = self.get_default_controls(max_cols);
+ width = width.max(basic_text.chars().count());
+
+ // Line 2: Override and toggle
+ let (override_text, _) = self.get_override_text_and_keys(max_cols);
+ width = width.max(override_text.chars().count());
+
+ // Line 3 (or 5 if show_more_options): New layout controls
+ let (new_layout_text, _) = self.get_new_layout_text_and_keys(max_cols);
+ width = width.max(new_layout_text.chars().count());
+
+ // Lines 4-5: Advanced options (only if show_more_options)
+ if self.show_more_options {
+ let (retain_text, _) = self.get_retain_text_and_highlight(max_cols);
+ width = width.max(retain_text.chars().count());
+
+ let (target_text, _) = self.get_target_text_and_highlight(max_cols);
+ width = width.max(target_text.chars().count());
+ }
+
+ width
+ }
+
+ pub fn render(&self, x: usize, y: usize, max_cols: usize) {
+ if self.typing_filter {
+ self.render_typing_filter_controls(x, y, max_cols);
+ } else {
+ self.render_basic_controls(x, y, max_cols);
+ self.render_override_and_toggle(x, y + 1, max_cols);
+
+ let new_layout_y = if self.show_more_options { y + 4 } else { y + 2 };
+ self.render_new_layout_controls(x, new_layout_y, max_cols);
+
+ if self.show_more_options {
+ self.render_advanced_options(x, y + 2, max_cols);
+ }
+ }
+ }
+
+ fn render_basic_controls(&self, x: usize, y: usize, max_width: usize) {
+ let (text, keys) = self.get_basic_controls_text_and_keys(max_width);
+ let controls_line = color_control_text(text, keys);
+ print_text_with_coordinates(controls_line, x, y, None, None);
+ }
+
+ fn render_override_and_toggle(&self, x: usize, y: usize, max_cols: usize) {
+ let (text, keys) = self.get_override_text_and_keys(max_cols);
+ let override_line = color_control_text(&text, keys);
+ print_text_with_coordinates(override_line, x, y, None, None);
+ }
+
+ fn render_new_layout_controls(&self, x: usize, y: usize, max_cols: usize) {
+ let (text, keys) = self.get_new_layout_text_and_keys(max_cols);
+ let new_layout_line = color_control_text(text, keys).color_substring(2, "New Layout:");
+ print_text_with_coordinates(new_layout_line, x, y, None, None);
+ }
+
+ fn render_typing_filter_controls(&self, x: usize, y: usize, max_cols: usize) {
+ let (text, keys) = self.get_typing_filter_controls(max_cols);
+ let controls_line = color_control_text(text, keys);
+ print_text_with_coordinates(controls_line, x, y, None, None);
+ }
+
+ fn render_advanced_options(&self, x: usize, y: usize, max_cols: usize) {
+ self.render_retain_option(x, y, max_cols);
+ self.render_target_option(x, y + 1, max_cols);
+ }
+
+ fn render_retain_option(&self, x: usize, y: usize, max_cols: usize) {
+ let (retain_text, retain_substring) = self.get_retain_text_and_highlight(max_cols);
+ let retain_line =
+ color_control_text(retain_text, &["<t>"]).color_substring(0, retain_substring);
+ print_text_with_coordinates(retain_line, x, y, None, None);
+ }
+
+ fn get_retain_text_and_highlight(&self, max_cols: usize) -> (&str, &str) {
+ let long_text = " <t> Retain: Terminals | Plugins | Both | None ";
+ let short_text = " <t> Retain: Term | Pl | Both | None ";
+ if max_cols >= long_text.chars().count() {
+ match (self.retain_terminal_panes, self.retain_plugin_panes) {
+ (true, false) => (
+ " <t> Retain: [Terminals] | Plugins | Both | None",
+ "[Terminals]",
+ ),
+ (false, true) => (
+ " <t> Retain: Terminals | [Plugins] | Both | None",
+ "[Plugins]",
+ ),
+ (true, true) => (
+ " <t> Retain: Terminals | Plugins | [Both] | None",
+ "[Both]",
+ ),
+ (false, false) => (
+ " <t> Retain: Terminals | Plugins | Both | [None]",
+ "[None]",
+ ),
+ }
+ } else if max_cols >= short_text.chars().count() {
+ match (self.retain_terminal_panes, self.retain_plugin_panes) {
+ (true, false) => (" <t> Retain: [Term]| Pl | Both | None", "[Term]"),
+ (false, true) => (" <t> Retain: Term |[Pl]| Both | None", "[Pl]"),
+ (true, true) => (" <t> Retain: Term | Pl |[Both]| None", "[Both]"),
+ (false, false) => (" <t> Retain: Term | Pl | Both |[None]", "[None]"),
+ }
+ } else {
+ match (self.retain_terminal_panes, self.retain_plugin_panes) {
+ (true, false) => (" <t> R: [T] | P | B | N ...", "[T]"),
+ (false, true) => (" <t> R: T |[P]| B | N ...", "[P]"),
+ (true, true) => (" <t> R: T | P |[B]| N ...", "[B]"),
+ (false, false) => (" <t> R: T | P | B |[N]...", "[N]"),
+ }
+ }
+ }
+
+ fn render_target_option(&self, x: usize, y: usize, max_cols: usize) {
+ let (target_text, target_substring) = self.get_target_text_and_highlight(max_cols);
+ let target_line =
+ color_control_text(target_text, &["<a>"]).color_substring(2, target_substring);
+ print_text_with_coordinates(target_line, x, y, None, None);
+ }
+
+ fn get_target_text_and_highlight(&self, max_cols: usize) -> (&str, &str) {
+ if self.apply_only_to_active_tab {
+ let long_text = " <a> Target: All Tabs | [Current]";
+ let short_text = " <a> Target: All |[Current]";
+ if max_cols >= long_text.chars().count() {
+ (long_text, "[Current]")
+ } else {
+ (short_text, "[Current]")
+ }
+ } else {
+ let long_text = " <a> Target: [All Tabs] | Current ";
+ let short_text = " <a> Target: [All]| Current ";
+ if max_cols >= long_text.chars().count() {
+ (long_text, "[All Tabs]")
+ } else {
+ (short_text, "[All]")
+ }
+ }
+ }
+}
+
+pub struct Title<'a> {
+ text: &'a str,
+}
+
+impl<'a> Title<'a> {
+ pub fn new(text: &'a str) -> Self {
+ Self { text }
+ }
+
+ pub fn render(&self, x: usize, y: usize) {
+ let title = Text::new(self.text).color_all(2);
+ print_text_with_coordinates(title, x, y, None, None);
+ }
+}
+
+pub struct ErrorMessage<'a> {
+ message: &'a str,
+}
+
+impl<'a> ErrorMessage<'a> {
+ pub fn new(message: &'a str) -> Self {
+ Self { message }
+ }
+
+ pub fn render(&self, x: usize, y: usize) {
+ let title = Text::new("Error").error_color_all();
+ print_text_with_coordinates(title, x, y, None, None);
+
+ let message = Text::new(self.message).error_color_all();
+ print_text_with_coordinates(message, x, y + 2, None, None);
+
+ let help = Text::new("Press any key to continue");
+ print_text_with_coordinates(help, x, y + 4, None, None);
+ }
+}
diff --git a/default-plugins/layout-manager/src/ui/mod.rs b/default-plugins/layout-manager/src/ui/mod.rs
new file mode 100644
index 00000000..f5a98e00
--- /dev/null
+++ b/default-plugins/layout-manager/src/ui/mod.rs
@@ -0,0 +1,9 @@
+pub mod layout_detail_view;
+pub mod layout_table;
+pub mod multi_line_error_message;
+pub mod text_utils;
+
+pub use layout_detail_view::LayoutDetail;
+pub use layout_table::{Controls, ErrorMessage, LayoutsTable, Title};
+pub use multi_line_error_message::MultiLineErrorMessage;
+pub use text_utils::*;
diff --git a/default-plugins/layout-manager/src/ui/multi_line_error_message.rs b/default-plugins/layout-manager/src/ui/multi_line_error_message.rs
new file mode 100644
index 00000000..223d7de3
--- /dev/null
+++ b/default-plugins/layout-manager/src/ui/multi_line_error_message.rs
@@ -0,0 +1,26 @@
+use zellij_tile::prelude::*;
+
+pub struct MultiLineErrorMessage {
+ message: Vec<String>,
+}
+
+impl MultiLineErrorMessage {
+ pub fn new(message: Vec<String>) -> Self {
+ Self { message }
+ }
+
+ pub fn render(&self, x: usize, y: usize, max_rows: usize) {
+ let title = Text::new("Error").error_color_all();
+ print_text_with_coordinates(title, x, y, None, None);
+
+ let mut current_y = y + 2;
+ for line in self.message.iter().take(max_rows) {
+ let text = Text::new(line).error_color_all();
+ print_text_with_coordinates(text, x, current_y, None, None);
+ current_y += 1;
+ }
+
+ let help = Text::new("Press any key to continue");
+ print_text_with_coordinates(help, x, current_y + 2, None, None);
+ }
+}
diff --git a/default-plugins/layout-manager/src/ui/text_utils.rs b/default-plugins/layout-manager/src/ui/text_utils.rs
new file mode 100644
index 00000000..3a4623d9
--- /dev/null
+++ b/default-plugins/layout-manager/src/ui/text_utils.rs
@@ -0,0 +1,195 @@
+use crate::DisplayLayout;
+use unicode_width::UnicodeWidthStr;
+use zellij_tile::prelude::*;
+
+pub fn truncate_with_ellipsis(text: &str, max_width: usize) -> String {
+ if max_width == 0 {
+ return String::new();
+ }
+ if text.chars().count() <= max_width {
+ return text.to_string();
+ }
+ if max_width <= 3 {
+ return text.chars().take(max_width).collect();
+ }
+ // Reserve 3 characters for "..."
+ let truncate_at = max_width.saturating_sub(3);
+ format!("{}...", text.chars().take(truncate_at).collect::<String>())
+}
+
+pub fn truncate_with_ellipsis_start(text: &str, max_width: usize) -> String {
+ if max_width == 0 {
+ return String::new();
+ }
+ if text.chars().count() <= max_width {
+ return text.to_string();
+ }
+ if max_width <= 3 {
+ return text
+ .chars()
+ .rev()
+ .take(max_width)
+ .collect::<Vec<_>>()
+ .into_iter()
+ .rev()
+ .collect();
+ }
+ let truncate_at = max_width.saturating_sub(3);
+ let suffix: String = text
+ .chars()
+ .rev()
+ .take(truncate_at)
+ .collect::<Vec<_>>()
+ .into_iter()
+ .rev()
+ .collect();
+ format!("...{}", suffix)
+}
+
+struct AnsiString<'a> {
+ content: &'a str,
+}
+
+impl<'a> AnsiString<'a> {
+ fn new(content: &'a str) -> Self {
+ Self { content }
+ }
+
+ fn stripped_content(&self) -> String {
+ strip_ansi_escapes::strip_str(self.content)
+ }
+
+ fn display_width(&self) -> usize {
+ let stripped = self.stripped_content();
+ UnicodeWidthStr::width(stripped.as_str())
+ }
+
+ fn fits_within(&self, max_width: usize) -> bool {
+ self.display_width() <= max_width
+ }
+
+ fn truncate_to_width(&self, max_width: usize) -> String {
+ if max_width == 0 {
+ return String::new();
+ }
+
+ if self.fits_within(max_width) {
+ return self.content.to_string();
+ }
+
+ let mut truncated = String::new();
+ for ch in self.content.chars() {
+ let test_string = format!("{}{}", truncated, ch);
+ let test_ansi = AnsiString::new(&test_string);
+
+ if test_ansi.display_width() > max_width {
+ break;
+ }
+
+ truncated.push(ch);
+ }
+
+ truncated
+ }
+
+ fn with_reset_code(content: String) -> String {
+ format!("{}\u{1b}[0m", content)
+ }
+}
+
+pub fn truncate_line_with_ansi(line: &str, max_width: usize) -> String {
+ let ansi_line = AnsiString::new(line);
+ let truncated = ansi_line.truncate_to_width(max_width);
+ AnsiString::with_reset_code(truncated)
+}
+
+pub fn wrap_text_to_width(text: &str, max_width: usize) -> Vec<String> {
+ if max_width == 0 {
+ return Vec::new();
+ }
+ if max_width < 3 {
+ return text.chars().map(|c| c.to_string()).collect();
+ }
+
+ let words: Vec<&str> = text.split_whitespace().collect();
+ let mut lines = Vec::new();
+ let mut current_line = String::new();
+
+ for word in words.iter() {
+ let word_to_add = if word.len() > max_width {
+ truncate_with_ellipsis(word, max_width)
+ } else {
+ word.to_string()
+ };
+
+ if current_line.is_empty() {
+ current_line = word_to_add;
+ } else {
+ let test_line = format!("{} {}", current_line, word_to_add);
+ if test_line.len() <= max_width {
+ current_line = test_line;
+ } else {
+ lines.push(current_line);
+ current_line = word_to_add;
+ }
+ }
+ }
+
+ if !current_line.is_empty() {
+ lines.push(current_line);
+ }
+
+ lines
+}
+
+pub fn get_layout_display_info(layout: &DisplayLayout) -> (String, Option<&LayoutMetadata>) {
+ match layout {
+ DisplayLayout::Valid(info) => match info {
+ LayoutInfo::BuiltIn(name) => (name.clone(), None),
+ LayoutInfo::File(path, metadata) => {
+ let name = path.split('/').last().unwrap_or(path).to_string();
+ (name, Some(metadata))
+ },
+ LayoutInfo::Url(url) => {
+ let name = url.split('/').last().unwrap_or(url).to_string();
+ (name, None)
+ },
+ LayoutInfo::Stringified(_) => ("raw".to_string(), None),
+ },
+ DisplayLayout::Error { name, .. } => (name.clone(), None),
+ }
+}
+
+fn format_elapsed(unix_epoch_str: &str) -> String {
+ let Ok(timestamp) = unix_epoch_str.parse::<i64>() else {
+ return " ".to_string();
+ };
+
+ use std::time::{SystemTime, UNIX_EPOCH};
+ let now = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap()
+ .as_secs() as i64;
+
+ let diff = now - timestamp;
+
+ if diff < 60 {
+ "now".to_string()
+ } else if diff < 3600 {
+ format!("{}m ago", diff / 60)
+ } else if diff < 86400 {
+ format!("{}h ago", diff / 3600)
+ } else {
+ format!("{}d ago", diff / 86400)
+ }
+}
+
+pub fn get_last_modified_string(metadata: Option<&LayoutMetadata>, is_builtin: bool) -> String {
+ if let Some(metadata) = metadata {
+ format_elapsed(&metadata.update_time)
+ } else if is_builtin {
+ "Built-in".to_string()
+ } else {
+ " ".to_string()
+ }
+}
diff --git a/default-plugins/status-bar/src/one_line_ui.rs b/default-plugins/status-bar/src/one_line_ui.rs
index a73c7b80..928d0d46 100644
--- a/default-plugins/status-bar/src/one_line_ui.rs
+++ b/default-plugins/status-bar/src/one_line_ui.rs
@@ -1491,6 +1491,7 @@ fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec<KeyWithModifier
(s("Session Manager"), s("Manager"), session_manager_key(&km)),
(s("Share"), s("Share"), share_key(&km)),
(s("Configure"), s("Config"), configuration_key(&km)),
+ (s("Layout Manager"), s("Layouts"), layout_manager_key(&km)),
(s("Plugin Manager"), s("Plugins"), plugin_manager_key(&km)),
(s("About"), s("About"), about_key(&km)),
(s("Select pane"), s("Select"), to_basemode_key),
@@ -1631,6 +1632,25 @@ fn plugin_manager_key(keymap: &[(KeyWithModifier, Vec<Action>)]) -> Vec<KeyWithM
}
}
+fn layout_manager_key(keymap: &[(KeyWithModifier, Vec<Action>)]) -> Vec<KeyWithModifier> {
+ let mut matching = keymap.iter().find_map(|(key, acvec)| {
+ let has_match = acvec
+ .iter()
+ .find(|a| a.launches_plugin("zellij:layout-manager"))
+ .is_some();
+ if has_match {
+ Some(key.clone())
+ } else {
+ None
+ }
+ });
+ if let Some(matching) = matching.take() {
+ vec![matching]
+ } else {
+ vec![]
+ }
+}
+
fn about_key(keymap: &[(KeyWithModifier, Vec<Action>)]) -> Vec<KeyWithModifier> {
let mut matching = keymap.iter().find_map(|(key, acvec)| {
let has_match = acvec
diff --git a/example/default.kdl b/example/default.kdl
index e11be799..f82030be 100644
--- a/example/default.kdl
+++ b/example/default.kdl
@@ -150,6 +150,13 @@ keybinds {
};
SwitchToMode "Normal"
}
+ bind "l" {
+ LaunchOrFocusPlugin "zellij:layout-manager" {
+ floating true
+ move_to_focused_tab true
+ };
+ SwitchToMode "Normal"
+ }
}
tmux {
bind "[" { SwitchToMode "Scroll"; }
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index 54c2e068..96a50e3f 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -83,6 +83,10 @@ fn workspace_members() -> &'static Vec<WorkspaceMember> {
build: true,
},
WorkspaceMember {
+ crate_name: "default-plugins/layout-manager",
+ build: true,
+ },
+ WorkspaceMember {
crate_name: "zellij-utils",
build: false,
},
diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs
index ce11e874..5fcf68be 100644
--- a/zellij-client/src/lib.rs
+++ b/zellij-client/src/lib.rs
@@ -85,7 +85,7 @@ use zellij_utils::cli::CliArgs;
use zellij_utils::{
channels::{self, ChannelWithContext, SenderWithContext},
consts::{set_permissions, ZELLIJ_SOCK_DIR},
- data::{ClientId, ConnectToSession, KeyWithModifier, LayoutInfo},
+ data::{ClientId, ConnectToSession, KeyWithModifier, LayoutInfo, LayoutMetadata},
envs,
errors::{ClientContext, ContextType, ErrorInstruction},
input::{cli_assets::CliAssets, config::Config, options::Options},
@@ -696,7 +696,10 @@ pub fn start_client(
config_dir: cli_args.config_dir.clone(),
should_ignore_config: cli_args.is_setup_clean(),
configuration_options: Some(config_options.clone()),
- layout: Some(LayoutInfo::File(path_to_layout.display().to_string())),
+ layout: Some(LayoutInfo::File(
+ path_to_layout.display().to_string(),
+ LayoutMetadata::default(),
+ )),
terminal_window_size: full_screen_ws,
data_dir: cli_args.data_dir.clone(),
is_debug: cli_args.debug,
@@ -1128,7 +1131,10 @@ pub fn start_server_detached(
config_dir: cli_args.config_dir.clone(),
should_ignore_config: cli_args.is_setup_clean(),
configuration_options: Some(config_options.clone()),
- layout: Some(LayoutInfo::File(path_to_layout.display().to_string())),
+ layout: Some(LayoutInfo::File(
+ path_to_layout.display().to_string(),
+ LayoutMetadata::default(),
+ )),
terminal_window_size: Size { cols: 50, rows: 50 }, // static number until a
// client connects
data_dir: cli_args.data_dir.clone(),
diff --git a/zellij-client/src/web_client/session_management.rs b/zellij-client/src/web_client/session_management.rs
index d3518b53..9530ba94 100644
--- a/zellij-client/src/web_client/session_management.rs
+++ b/zellij-client/src/web_client/session_management.rs
@@ -4,7 +4,7 @@ use crate::spawn_server;
use std::{fs, path::PathBuf};
use zellij_utils::{
consts::session_layout_cache_file_name,
- data::{ConnectToSession, LayoutInfo, WebSharing},
+ data::{ConnectToSession, LayoutInfo, LayoutMetadata, WebSharing},
envs,
input::{cli_assets::CliAssets, config::Config, options::Options},
ipc::{ClientAttributes, ClientToServerMsg},
@@ -70,6 +70,7 @@ pub fn create_first_message(
session_layout_cache_file_name(&session_name)
.display()
.to_string(),
+ LayoutMetadata::default(),
))
} else {
None
diff --git a/zellij-server/Cargo.toml b/zellij-server/Cargo.toml
index 263e66b1..e513feb6 100644
--- a/zellij-server/Cargo.toml
+++ b/zellij-server/Cargo.toml
@@ -26,6 +26,7 @@ daemonize = { workspace = true }
highway = { version = "0.6.4", default-features = false, features = ["std"] }
interprocess = { workspace = true }
isahc = { workspace = true }
+kdl = { workspace = true }
lazy_static = { workspace = true }
libc = { workspace = true }
log = { workspace = true }
diff --git a/zellij-server/src/background_jobs.rs b/zellij-server/src/background_jobs.rs
index 62907048..d91a80a4 100644
--- a/zellij-server/src/background_jobs.rs
+++ b/zellij-server/src/background_jobs.rs
@@ -204,6 +204,7 @@ pub(crate) fn background_jobs_main(
let current_session_name =
current_session_name.lock().unwrap().to_string();
let current_session_info = current_session_info.lock().unwrap().clone();
+ let available_layouts = current_session_info.available_layouts.clone();
let current_session_layout =
current_session_layout.lock().unwrap().clone();
if !disable_session_metadata {
@@ -221,6 +222,8 @@ pub(crate) fn background_jobs_main(
let current_session_plugin_list =
current_session_plugin_list.lock().unwrap().clone();
session_info.populate_plugin_list(current_session_plugin_list);
+ // these are not serialized, so must be explicitly added
+ session_info.available_layouts = available_layouts.clone();
}
}
let resurrectable_sessions =
diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs
index 810ff27f..2ccb9d24 100644
--- a/zellij-server/src/lib.rs
+++ b/zellij-server/src/lib.rs
@@ -51,15 +51,15 @@ use zellij_utils::{
DEFAULT_SCROLL_BUFFER_SIZE, SCROLL_BUFFER_SIZE, ZELLIJ_SEEN_RELEASE_NOTES_CACHE_FILE,
},
data::{
- ConnectToSession, Event, InputMode, KeyWithModifier, LayoutInfo, PluginCapabilities, Style,
- WebSharing,
+ ConnectToSession, Event, InputMode, KeyWithModifier, LayoutInfo, LayoutWithError,
+ PluginCapabilities, Style, WebSharing,
},
errors::{prelude::*, ContextType, ErrorInstruction, FatalError, ServerContext},
home::{default_layout_dir, get_default_data_dir},
input::{
actions::Action,
command::{RunCommand, TerminalAction},
- config::{watch_config_file_changes, Config},
+ config::{watch_config_file_changes, watch_layout_dir_changes, Config},
get_mode_info,
keybinds::Keybinds,
layout::{FloatingPaneLayout, Layout, PluginAlias, Run, RunPluginOrAlias},
@@ -401,6 +401,7 @@ impl SessionMetaData {
keybinds: Some(new_config.keybinds),
default_mode: new_config.options.default_mode,
default_shell: self.default_shell.clone(),
+ layout_dir: new_config.options.layout_dir,
was_written_to_disk: config_was_written_to_disk,
})
.unwrap();
@@ -1781,6 +1782,9 @@ fn init_session(
.unwrap();
let zellij_cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
+
+ let (available_layouts, available_layout_errors) = get_available_layouts(&config_options);
+
let plugin_thread = thread::Builder::new()
.name("wasm".to_string())
.spawn({
@@ -1800,7 +1804,10 @@ fn init_session(
let client_attributes = client_attributes.clone();
let default_shell = default_shell.clone();
let capabilities = capabilities.clone();
- let layout_dir = config_options.layout_dir.clone();
+ let layout_dir = config_options
+ .layout_dir
+ .clone()
+ .or_else(|| default_layout_dir());
let background_plugins = config.background_plugins.clone();
move || {
plugin_thread_main(
@@ -1809,6 +1816,8 @@ fn init_session(
data_dir,
layout,
layout_dir,
+ available_layouts,
+ available_layout_errors,
path_to_default_shell,
zellij_cwd,
capabilities,
@@ -1873,7 +1882,19 @@ fn init_session(
})
.unwrap();
if let Some(config_file_path) = cli_assets.config_file_path.clone() {
+ let layout_dir = config_options
+ .layout_dir
+ .clone()
+ .or_else(|| default_layout_dir());
+ let default_layout_name = config_options
+ .default_layout
+ .map(|l| format!("{}", l.display()));
report_changes_in_config_file(config_file_path, to_server.clone());
+
+ // Watch layout directory for changes
+ if let Some(layout_dir_path) = layout_dir {
+ report_changes_in_layout_dir(layout_dir_path, default_layout_name, to_plugin.clone());
+ }
}
SessionMetaData {
@@ -1997,6 +2018,33 @@ fn report_changes_in_config_file(
});
}
+fn report_changes_in_layout_dir(
+ layout_dir: PathBuf,
+ default_layout_name: Option<String>,
+ to_plugin: SenderWithContext<PluginInstruction>,
+) {
+ std::thread::spawn(move || {
+ let rt = tokio::runtime::Runtime::new().unwrap();
+ rt.block_on(async move {
+ let to_plugin = to_plugin.clone();
+ watch_layout_dir_changes(
+ layout_dir,
+ default_layout_name,
+ move |new_layouts, layout_errors| {
+ let to_plugin = to_plugin.clone();
+ async move {
+ let _ = to_plugin.send(PluginInstruction::LayoutListUpdate(
+ new_layouts,
+ layout_errors,
+ ));
+ }
+ },
+ )
+ .await;
+ });
+ });
+}
+
fn update_new_saved_config(
new_config: Option<Config>,
write_config_to_disk: bool,
@@ -2086,3 +2134,16 @@ pub fn get_engine() -> Engine {
log::info!("Loading plugins using Wasmi interpreter");
Engine::default()
}
+
+// TODO: move elsewhere
+fn get_available_layouts(config_options: &Options) -> (Vec<LayoutInfo>, Vec<LayoutWithError>) {
+ let layout_dir = config_options
+ .layout_dir
+ .clone()
+ .or_else(|| default_layout_dir());
+ let default_layout_name = config_options
+ .default_layout
+ .as_ref()
+ .map(|l| format!("{}", l.display()));
+ Layout::list_available_layouts(layout_dir, &default_layout_name)
+}
diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs
index a8f60b30..c6c931c8 100644
--- a/zellij-server/src/plugins/mod.rs
+++ b/zellij-server/src/plugins/mod.rs
@@ -23,6 +23,7 @@ use crate::screen::ScreenInstruction;
use crate::session_layout_metadata::SessionLayoutMetadata;
use crate::{pty::PtyInstruction, thread_bus::Bus, ClientId, ServerInstruction};
use zellij_utils::data::PaneRenderReport;
+use zellij_utils::input::layout::TabLayoutInfo;
pub use wasm_bridge::PluginRenderAsset;
use wasm_bridge::WasmBridge;
@@ -31,18 +32,15 @@ use async_std::{channel, future::timeout, task};
use zellij_utils::{
data::{
ClientInfo, CommandOrPlugin, Event, EventType, FloatingPaneCoordinates, InputMode,
- MessageToPlugin, PermissionStatus, PermissionType, PipeMessage, PipeSource,
- PluginCapabilities, WebServerStatus,
+ LayoutInfo, LayoutWithError, MessageToPlugin, PermissionStatus, PermissionType,
+ PipeMessage, PipeSource, PluginCapabilities, WebServerStatus,
},
errors::{prelude::*, ContextType, PluginContext},
input::{
actions::Action,
command::TerminalAction,
keybinds::Keybinds,
- layout::{
- FloatingPaneLayout, Layout, Run, RunPlugin, RunPluginOrAlias, SwapFloatingLayout,
- SwapTiledLayout, TiledPaneLayout,
- },
+ layout::{FloatingPaneLayout, Layout, Run, RunPlugin, RunPluginOrAlias, TiledPaneLayout},
plugins::PluginAliases,
},
ipc::ClientAttributes,
@@ -53,6 +51,12 @@ use zellij_utils::{
pub type PluginId = u32;
#[derive(Clone, Debug)]
+pub struct DumpSessionLayoutResponse {
+ pub layout_result: Result<String, String>,
+ pub metadata: Option<zellij_utils::data::LayoutMetadata>,
+}
+
+#[derive(Clone, Debug)]
pub enum PluginInstruction {
Load(
Option<bool>, // should float
@@ -100,13 +104,9 @@ pub enum PluginInstruction {
OverrideLayout(
Option<PathBuf>, // cwd
Option<TerminalAction>, // default_shell
- TiledPaneLayout,
- Vec<FloatingPaneLayout>,
- Option<Vec<SwapTiledLayout>>,
- Option<Vec<SwapFloatingLayout>>,
- bool, // retain_existing_terminal_panes
- bool, // retain_existing_plugin_panes
- usize, // tab_index
+ Vec<TabLayoutInfo>, // layouts for each tab
+ bool, // retain_existing_terminal_panes
+ bool, // retain_existing_plugin_panes
ClientId,
Option<NotificationEnd>,
),
@@ -140,7 +140,11 @@ pub enum PluginInstruction {
),
DumpLayout(SessionLayoutMetadata, ClientId, Option<NotificationEnd>),
ListClientsMetadata(SessionLayoutMetadata, ClientId, Option<NotificationEnd>),
- DumpLayoutToPlugin(SessionLayoutMetadata, PluginId),
+ DumpLayoutToPlugin {
+ session_layout_metadata: SessionLayoutMetadata,
+ plugin_id: PluginId,
+ response_channel: crossbeam::channel::Sender<DumpSessionLayoutResponse>,
+ },
LogLayoutToHd(SessionLayoutMetadata),
CliPipe {
pipe_id: String,
@@ -183,6 +187,7 @@ pub enum PluginInstruction {
keybinds: Option<Keybinds>,
default_mode: Option<InputMode>,
default_shell: Option<TerminalAction>,
+ layout_dir: Option<PathBuf>,
was_written_to_disk: bool,
},
FailedToWriteConfigToDisk {
@@ -200,6 +205,8 @@ pub enum PluginInstruction {
terminal_id: Option<u32>,
cli_client_id: Option<ClientId>,
},
+ LayoutListUpdate(Vec<LayoutInfo>, Vec<LayoutWithError>),
+ RequestStateUpdateForPlugin(PluginId),
Exit,
}
@@ -241,7 +248,7 @@ impl From<&PluginInstruction> for PluginContext {
PluginInstruction::UnblockCliPipes { .. } => PluginContext::UnblockCliPipes,
PluginInstruction::WatchFilesystem => PluginContext::WatchFilesystem,
PluginInstruction::KeybindPipe { .. } => PluginContext::KeybindPipe,
- PluginInstruction::DumpLayoutToPlugin(..) => PluginContext::DumpLayoutToPlugin,
+ PluginInstruction::DumpLayoutToPlugin { .. } => PluginContext::DumpLayoutToPlugin,
PluginInstruction::Reconfigure { .. } => PluginContext::Reconfigure,
PluginInstruction::FailedToWriteConfigToDisk { .. } => {
PluginContext::FailedToWriteConfigToDisk
@@ -252,6 +259,10 @@ impl From<&PluginInstruction> for PluginContext {
PluginInstruction::FailedToStartWebServer(..) => PluginContext::FailedToStartWebServer,
PluginInstruction::PaneRenderReport(..) => PluginContext::PaneRenderReport,
PluginInstruction::UserInput { .. } => PluginContext::UserInput,
+ PluginInstruction::LayoutListUpdate(..) => PluginContext::LayoutListUpdate,
+ PluginInstruction::RequestStateUpdateForPlugin(..) => {
+ PluginContext::RequestStateUpdateForPlugin
+ },
}
}
}
@@ -262,6 +273,8 @@ pub(crate) fn plugin_thread_main(
data_dir: PathBuf,
mut layout: Box<Layout>,
layout_dir: Option<PathBuf>,
+ available_layouts: Vec<LayoutInfo>,
+ available_layout_errors: Vec<LayoutWithError>,
path_to_default_shell: PathBuf,
zellij_cwd: PathBuf,
capabilities: PluginCapabilities,
@@ -297,6 +310,8 @@ pub(crate) fn plugin_thread_main(
default_shell,
layout.clone(),
layout_dir,
+ available_layouts,
+ available_layout_errors,
default_mode,
default_keybinds,
);
@@ -592,17 +607,13 @@ pub(crate) fn plugin_thread_main(
PluginInstruction::OverrideLayout(
cwd,
default_shell,
- mut tiled_layout,
- mut floating_layouts,
- swap_tiled_layouts,
- swap_floating_layouts,
+ tab_layouts,
retain_existing_terminal_panes,
retain_existing_plugin_panes,
- tab_index,
client_id,
completion_tx,
) => {
- // Prefer connected clients to avoid opening plugins in background for CLI clients
+ // 1. Prefer connected clients over CLI clients
let client_id = if wasm_bridge.client_is_connected(&client_id) {
client_id
} else if let Some(first_client_id) = wasm_bridge.get_first_client_id() {
@@ -611,70 +622,78 @@ pub(crate) fn plugin_thread_main(
client_id
};
- let mut plugin_ids: HashMap<RunPluginOrAlias, Vec<PluginId>> = HashMap::new();
+ // 2. Process each tab layout
+ let mut tab_layouts_with_plugin_ids = Vec::new();
- // Populate plugin aliases in layouts
- tiled_layout.populate_plugin_aliases_in_layout(&plugin_aliases);
- floating_layouts.iter_mut().for_each(|f| {
- f.run
- .as_mut()
- .map(|f| f.populate_run_plugin_if_needed(&plugin_aliases));
- });
+ for mut tab_layout_info in tab_layouts {
+ // Populate plugin aliases in layouts
+ tab_layout_info
+ .tiled_layout
+ .populate_plugin_aliases_in_layout(&plugin_aliases);
+ tab_layout_info.floating_layouts.iter_mut().for_each(|f| {
+ f.run
+ .as_mut()
+ .map(|r| r.populate_run_plugin_if_needed(&plugin_aliases));
+ });
- // Extract run instructions from tiled layout
- let extracted_run_instructions = tiled_layout.extract_run_instructions();
+ // Extract run instructions from tiled layout
+ let extracted_run_instructions =
+ tab_layout_info.tiled_layout.extract_run_instructions();
- // Extract run instructions from floating layouts (excluding already_running)
- let extracted_floating_plugins: Vec<Option<Run>> = floating_layouts
- .iter()
- .filter(|f| !f.already_running)
- .map(|f| f.run.clone())
- .collect();
+ // Extract run instructions from floating layouts (excluding already_running)
+ let extracted_floating_plugins: Vec<Option<Run>> = tab_layout_info
+ .floating_layouts
+ .iter()
+ .filter(|f| !f.already_running)
+ .map(|f| f.run.clone())
+ .collect();
- // Combine all run instructions
- let mut all_run_instructions = extracted_run_instructions;
- all_run_instructions.extend(extracted_floating_plugins);
+ // Combine all run instructions
+ let mut all_run_instructions = extracted_run_instructions;
+ all_run_instructions.extend(extracted_floating_plugins);
- // Load plugins for all Run::Plugin instructions
- let size = Size::default();
- for run_instruction in all_run_instructions {
- if let Some(Run::Plugin(run_plugin_or_alias)) = run_instruction {
- let run_plugin = run_plugin_or_alias.get_run_plugin();
- let cwd = run_plugin_or_alias.get_initial_cwd();
- let skip_cache = false;
- match wasm_bridge.load_plugin(
- &run_plugin,
- Some(tab_index),
- size,
- cwd,
- skip_cache,
- Some(client_id),
- ) {
- Ok((plugin_id, _client_id)) => {
- plugin_ids
- .entry(run_plugin_or_alias.clone())
- .or_default()
- .push(plugin_id);
- },
- Err(e) => {
- log::error!("Failed to load plugin: {}", e);
- },
+ // Load plugins for all Run::Plugin instructions
+ let mut plugin_ids: HashMap<RunPluginOrAlias, Vec<PluginId>> = HashMap::new();
+ let size = Size::default();
+
+ for run_instruction in all_run_instructions {
+ if let Some(Run::Plugin(run_plugin_or_alias)) = run_instruction {
+ let run_plugin = run_plugin_or_alias.get_run_plugin();
+ let cwd = run_plugin_or_alias.get_initial_cwd();
+ let skip_cache = false;
+
+ match wasm_bridge.load_plugin(
+ &run_plugin,
+ Some(tab_layout_info.tab_index),
+ size,
+ cwd,
+ skip_cache,
+ Some(client_id),
+ ) {
+ Ok((plugin_id, _client_id)) => {
+ plugin_ids
+ .entry(run_plugin_or_alias.clone())
+ .or_default()
+ .push(plugin_id);
+ },
+ Err(e) => {
+ log::error!("Failed to load plugin: {}", e);
+ },
+ }
}
}
+
+ // Pair this tab's layout with its plugin IDs
+ tab_layouts_with_plugin_ids.push((tab_layout_info, plugin_ids));
}
- // Send to pty thread
+ // 3. Send to pty thread with all tab layouts and their plugin IDs
drop(bus.senders.send_to_pty(PtyInstruction::OverrideLayout(
cwd,
default_shell,
- tiled_layout,
- floating_layouts,
- swap_tiled_layouts,
- swap_floating_layouts,
+ tab_layouts_with_plugin_ids,
retain_existing_terminal_panes,
retain_existing_plugin_panes,
- tab_index,
- plugin_ids,
client_id,
completion_tx,
)));
@@ -756,6 +775,7 @@ pub(crate) fn plugin_thread_main(
&mut session_layout_metadata,
&wasm_bridge,
&plugin_aliases,
+ None,
);
drop(bus.senders.send_to_pty(PtyInstruction::DumpLayout(
session_layout_metadata,
@@ -772,6 +792,7 @@ pub(crate) fn plugin_thread_main(
&mut session_layout_metadata,
&wasm_bridge,
&plugin_aliases,
+ None,
);
drop(bus.senders.send_to_pty(PtyInstruction::ListClientsMetadata(
session_layout_metadata,
@@ -779,16 +800,33 @@ pub(crate) fn plugin_thread_main(
completion_tx,
)));
},
- PluginInstruction::DumpLayoutToPlugin(mut session_layout_metadata, plugin_id) => {
+ PluginInstruction::DumpLayoutToPlugin {
+ mut session_layout_metadata,
+ plugin_id,
+ response_channel,
+ } => {
populate_session_layout_metadata(
&mut session_layout_metadata,
&wasm_bridge,
&plugin_aliases,
+ Some(plugin_id),
);
+
+ let layout_metadata = session_layout_metadata.to_layout_metadata();
+
match session_serialization::serialize_session_layout(
session_layout_metadata.into(),
) {
Ok((layout, _pane_contents)) => {
+ // send synchronous response
+ let response = DumpSessionLayoutResponse {
+ layout_result: Ok(layout.clone()),
+ metadata: Some(layout_metadata),
+ };
+ let _ = response_channel.send(response);
+
+ // send CustomMessage to plugin (backwards compatibility, should get rid of
+ // this on API version upgrade)
let updates = vec![(
Some(plugin_id),
None,
@@ -797,13 +835,17 @@ pub(crate) fn plugin_thread_main(
wasm_bridge.update_plugins(updates, shutdown_send.clone())?;
},
Err(e) => {
+ let error_msg = format!("{}", e);
+ let response = DumpSessionLayoutResponse {
+ layout_result: Err(error_msg.clone()),
+ metadata: None,
+ };
+ let _ = response_channel.send(response);
+
let updates = vec![(
Some(plugin_id),
None,
- Event::CustomMessage(
- "session_layout_error".to_owned(),
- format!("{}", e),
- ),
+ Event::CustomMessage("session_layout_error".to_owned(), error_msg),
)];
wasm_bridge.update_plugins(updates, shutdown_send.clone())?;
},
@@ -818,6 +860,7 @@ pub(crate) fn plugin_thread_main(
&mut session_layout_metadata,
&wasm_bridge,
&plugin_aliases,
+ Some(plugin_id),
);
let mut clients_metadata = session_layout_metadata.all_clients_metadata();
let mut client_list_for_plugin = vec![];
@@ -843,6 +886,7 @@ pub(crate) fn plugin_thread_main(
&mut session_layout_metadata,
&wasm_bridge,
&plugin_aliases,
+ None,
);
drop(
bus.senders
@@ -1076,10 +1120,11 @@ pub(crate) fn plugin_thread_main(
keybinds,
default_mode,
default_shell,
+ layout_dir,
was_written_to_disk,
} => {
wasm_bridge
- .reconfigure(client_id, keybinds, default_mode, default_shell)
+ .reconfigure(client_id, keybinds, default_mode, default_shell, layout_dir)
.non_fatal();
// TODO: notify plugins that this happened so that they can eg. rebind temporary keys that
// were lost
@@ -1152,6 +1197,12 @@ pub(crate) fn plugin_thread_main(
)];
wasm_bridge.update_plugins(updates, shutdown_send.clone())?;
},
+ PluginInstruction::LayoutListUpdate(layouts, errors) => {
+ wasm_bridge.update_available_layouts(layouts, errors);
+ },
+ PluginInstruction::RequestStateUpdateForPlugin(plugin_id) => {
+ wasm_bridge.state_update_for_plugin(plugin_id);
+ },
PluginInstruction::Exit => {
break;
},
@@ -1188,7 +1239,13 @@ fn populate_session_layout_metadata(
session_layout_metadata: &mut SessionLayoutMetadata,
wasm_bridge: &WasmBridge,
plugin_aliases: &PluginAliases,
+ exclude_plugin_id: Option<u32>,
) {
+ // Remove the requesting plugin from the layout to prevent deadlock
+ if let Some(plugin_id) = exclude_plugin_id {
+ session_layout_metadata.remove_plugin_from_layout(plugin_id);
+ }
+
let plugin_ids = session_layout_metadata.all_plugin_ids();
let mut plugin_ids_to_cmds: HashMap<u32, RunPlugin> = HashMap::new();
for plugin_id in plugin_ids {
diff --git a/zellij-server/src/plugins/plugin_map.rs b/zellij-server/src/plugins/plugin_map.rs
index 7520114f..6f57deff 100644
--- a/zellij-server/src/plugins/plugin_map.rs
+++ b/zellij-server/src/plugins/plugin_map.rs
@@ -406,6 +406,9 @@ impl RunningPlugin {
pub fn update_default_shell(&mut self, default_shell: Option<TerminalAction>) {
self.store.data_mut().default_shell = default_shell;
}
+ pub fn update_layout_dir(&mut self, layout_dir: Option<PathBuf>) {
+ self.store.data_mut().layout_dir = layout_dir;
+ }
pub fn intercepting_key_presses(&self) -> bool {
self.store.data().intercepting_key_presses
}
diff --git a/zellij-server/src/plugins/unit/plugin_tests.rs b/zellij-server/src/plugins/unit/plugin_tests.rs
index c0368fbc..3dfa288f 100644
--- a/zellij-server/src/plugins/unit/plugin_tests.rs
+++ b/zellij-server/src/plugins/unit/plugin_tests.rs
@@ -338,6 +338,7 @@ fn create_plugin_thread(
config.set_max_recursion_depth(1000);
let engine = Engine::new(&config);
let data_dir = PathBuf::from(tempdir().unwrap().path());
+ let layout_dir = PathBuf::from(tempdir().unwrap().path());
let default_shell = PathBuf::from(".");
let plugin_capabilities = PluginCapabilities::default();
let client_attributes = ClientAttributes::default();
@@ -362,7 +363,9 @@ fn create_plugin_thread(
engine,
data_dir,
Box::new(Layout::default()),
- None,
+ Some(layout_dir),
+ vec![],
+ vec![],
default_shell,
zellij_cwd,
plugin_capabilities,
@@ -450,6 +453,8 @@ fn create_plugin_thread_with_server_receiver(
data_dir,
Box::new(Layout::default()),
None,
+ vec![],
+ vec![],
default_shell,
zellij_cwd,
plugin_capabilities,
@@ -485,6 +490,7 @@ fn create_plugin_thread_with_server_receiver(
fn create_plugin_thread_with_pty_receiver(
zellij_cwd: Option<PathBuf>,
+ layout_dir: Option<PathBuf>,
) -> (
SenderWithContext<PluginInstruction>,
Receiver<(PtyInstruction, ErrorContext)>,
@@ -528,6 +534,7 @@ fn create_plugin_thread_with_pty_receiver(
config.set_max_recursion_depth(1000);
let engine = Engine::new(&config);
let data_dir = PathBuf::from(tempdir().unwrap().path());
+ let layout_dir = layout_dir.unwrap_or_else(|| PathBuf::from(tempdir().unwrap().path()));
let default_shell = PathBuf::from(".");
let plugin_capabilities = PluginCapabilities::default();
let client_attributes = ClientAttributes::default();
@@ -542,7 +549,9 @@ fn create_plugin_thread_with_pty_receiver(
engine,
data_dir,
Box::new(Layout::default()),
- None,
+ Some(layout_dir),
+ vec![],
+ vec![],
default_shell,
zellij_cwd,
plugin_capabilities,
@@ -631,6 +640,8 @@ fn create_plugin_thread_with_background_jobs_receiver(
data_dir,
Box::new(Layout::default()),
None,
+ vec![],
+ vec![],
default_shell,
zellij_cwd,
plugin_capabilities,
@@ -4220,7 +4231,7 @@ pub fn open_file_floating_plugin_command() {
let plugin_host_folder = PathBuf::from(temp_folder.path());
let cache_path = plugin_host_folder.join("permissions_test.kdl");
let (plugin_thread_sender, pty_receiver, screen_receiver, teardown) =
- create_plugin_thread_with_pty_receiver(Some(plugin_host_folder));
+ create_plugin_thread_with_pty_receiver(Some(plugin_host_folder), None);
let plugin_should_float = Some(false);
let plugin_title = Some("test_plugin".to_owned());
let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
@@ -4306,7 +4317,7 @@ pub fn open_file_plugin_command() {
let plugin_host_folder = PathBuf::from(temp_folder.path());
let cache_path = plugin_host_folder.join("permissions_test.kdl");
let (plugin_thread_sender, pty_receiver, screen_receiver, teardown) =
- create_plugin_thread_with_pty_receiver(Some(plugin_host_folder));
+ create_plugin_thread_with_pty_receiver(Some(plugin_host_folder), None);
let plugin_should_float = Some(false);
let plugin_title = Some("test_plugin".to_owned());
let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
@@ -4392,7 +4403,7 @@ pub fn open_file_with_line_plugin_command() {
let plugin_host_folder = PathBuf::from(temp_folder.path());
let cache_path = plugin_host_folder.join("permissions_test.kdl");
let (plugin_thread_sender, pty_receiver, screen_receiver, teardown) =
- create_plugin_thread_with_pty_receiver(Some(plugin_host_folder));
+ create_plugin_thread_with_pty_receiver(Some(plugin_host_folder), None);
let plugin_should_float = Some(false);
let plugin_title = Some("test_plugin".to_owned());
let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
@@ -4479,7 +4490,7 @@ pub fn open_file_with_line_floating_plugin_command() {
let plugin_host_folder = PathBuf::from(temp_folder.path());
let cache_path = plugin_host_folder.join("permissions_test.kdl");
let (plugin_thread_sender, pty_receiver, screen_receiver, teardown) =
- create_plugin_thread_with_pty_receiver(Some(plugin_host_folder));
+ create_plugin_thread_with_pty_receiver(Some(plugin_host_folder), None);
let plugin_should_float = Some(false);
let plugin_title = Some("test_plugin".to_owned());
let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
@@ -4565,7 +4576,7 @@ pub fn open_terminal_plugin_command() {
let plugin_host_folder = PathBuf::from(temp_folder.path());
let cache_path = plugin_host_folder.join("permissions_test.kdl");
let (plugin_thread_sender, pty_receiver, screen_receiver, teardown) =
- create_plugin_thread_with_pty_receiver(Some(plugin_host_folder));
+ create_plugin_thread_with_pty_receiver(Some(plugin_host_folder), None);
let plugin_should_float = Some(false);
let plugin_title = Some("test_plugin".to_owned());
let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
@@ -4647,7 +4658,7 @@ pub fn open_terminal_floating_plugin_command() {
let plugin_host_folder = PathBuf::from(temp_folder.path());
let cache_path = plugin_host_folder.join("permissions_test.kdl");
let (plugin_thread_sender, pty_receiver, screen_receiver, teardown) =
- create_plugin_thread_with_pty_receiver(Some(plugin_host_folder));
+ create_plugin_thread_with_pty_receiver(Some(plugin_host_folder), None);
let plugin_should_float = Some(false);
let plugin_title = Some("test_plugin".to_owned());
let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
@@ -4729,7 +4740,7 @@ pub fn open_command_pane_plugin_command() {
let plugin_host_folder = PathBuf::from(temp_folder.path());
let cache_path = plugin_host_folder.join("permissions_test.kdl");
let (plugin_thread_sender, pty_receiver, screen_receiver, teardown) =
- create_plugin_thread_with_pty_receiver(Some(plugin_host_folder));
+ create_plugin_thread_with_pty_receiver(Some(plugin_host_folder), None);
let plugin_should_float = Some(false);
let plugin_title = Some("test_plugin".to_owned());
let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
@@ -4811,7 +4822,7 @@ pub fn open_command_pane_floating_plugin_command() {
let plugin_host_folder = PathBuf::from(temp_folder.path());
let cache_path = plugin_host_folder.join("permissions_test.kdl");
let (plugin_thread_sender, pty_receiver, screen_receiver, teardown) =
- create_plugin_thread_with_pty_receiver(Some(plugin_host_folder));
+ create_plugin_thread_with_pty_receiver(Some(plugin_host_folder), None);
let plugin_should_float = Some(false);
let plugin_title = Some("test_plugin".to_owned());
let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
@@ -7250,7 +7261,7 @@ pub fn open_command_pane_background_plugin_command() {
let plugin_host_folder = PathBuf::from(temp_folder.path());
let cache_path = plugin_host_folder.join("permissions_test.kdl");
let (plugin_thread_sender, pty_receiver, screen_receiver, teardown) =
- create_plugin_thread_with_pty_receiver(Some(plugin_host_folder));
+ create_plugin_thread_with_pty_receiver(Some(plugin_host_folder), None);
let plugin_should_float = Some(false);
let plugin_title = Some("test_plugin".to_owned());
let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
@@ -9462,7 +9473,7 @@ pub fn send_sigint_to_pane_id_plugin_command() {
let plugin_host_folder = PathBuf::from(temp_folder.path());
let cache_path = plugin_host_folder.join("permissions_test.kdl");
let (plugin_thread_sender, pty_receiver, screen_receiver, teardown) =
- create_plugin_thread_with_pty_receiver(Some(plugin_host_folder));
+ create_plugin_thread_with_pty_receiver(Some(plugin_host_folder), None);
let received_pty_instructions = Arc::new(Mutex::new(vec![]));
let client_id = 1;
@@ -9551,7 +9562,7 @@ pub fn send_sigkill_to_pane_id_plugin_command() {
let plugin_host_folder = PathBuf::from(temp_folder.path());
let cache_path = plugin_host_folder.join("permissions_test.kdl");
let (plugin_thread_sender, pty_receiver, screen_receiver, teardown) =
- create_plugin_thread_with_pty_receiver(Some(plugin_host_folder));
+ create_plugin_thread_with_pty_receiver(Some(plugin_host_folder), None);
let received_pty_instructions = Arc::new(Mutex::new(vec![]));
let client_id = 1;
@@ -9804,3 +9815,2018 @@ pub fn run_action_without_permission() {
// Should be None because permission was denied
assert_snapshot!(format!("{:#?}", move_focus_instruction));
}
+
+#[test]
+#[ignore]
+pub fn generate_random_name_plugin_command() {
+ let temp_folder = tempdir().unwrap();
+ let plugin_host_folder = PathBuf::from(temp_folder.path());
+ let cache_path = plugin_host_folder.join("permissions_test.kdl");
+ let (plugin_thread_sender, screen_receiver, teardown) =
+ create_plugin_thread(Some(plugin_host_folder.clone()));
+
+ let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+ let screen_thread = grant_permissions_and_log_actions_in_thread!(
+ received_screen_instructions,
+ ScreenInstruction::PluginBytes,
+ screen_receiver,
+ 2,
+ &PermissionType::ReadApplicationState,
+ cache_path,
+ plugin_thread_sender,
+ 1
+ );
+
+ let plugin_should_float = Some(false);
+ let plugin_title = Some("test_plugin".to_owned());
+ let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+ configuration: Default::default(),
+ ..Default::default()
+ });
+ let tab_index = 1;
+ let client_id = 1;
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+
+ let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+ let _ = plugin_thread_sender.send(PluginInstruction::Load(
+ plugin_should_float,
+ false,
+ plugin_title,
+ run_plugin,
+ Some(tab_index),
+ None,
+ client_id,
+ size,
+ None,
+ None,
+ false,
+ None,
+ None,
+ None,
+ ));
+
+ std::thread::sleep(std::time::Duration::from_millis(500));
+
+ // Send Ctrl+Shift+A to trigger generate_random_name()
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(
+ KeyWithModifier::new(BareKey::Char('a'))
+ .with_ctrl_modifier()
+ .with_shift_modifier(),
+ ),
+ )]));
+
+ screen_thread.join().unwrap();
+ teardown();
+
+ let plugin_bytes_event = received_screen_instructions
+ .lock()
+ .unwrap()
+ .iter()
+ .find_map(|i| {
+ if let ScreenInstruction::PluginBytes(plugin_render_assets) = i {
+ for plugin_render_asset in plugin_render_assets {
+ let plugin_bytes = plugin_render_asset.bytes.clone();
+ let plugin_bytes = String::from_utf8_lossy(plugin_bytes.as_slice()).to_string();
+ if plugin_bytes.contains("Generated name") {
+ return Some(plugin_bytes);
+ }
+ }
+ }
+ None
+ });
+ assert_snapshot!(format!("{:#?}", plugin_bytes_event));
+}
+
+#[test]
+#[ignore]
+pub fn dump_layout_success_plugin_command() {
+ let temp_folder = tempdir().unwrap();
+ let plugin_host_folder = PathBuf::from(temp_folder.path());
+ let cache_path = plugin_host_folder.join("permissions_test.kdl");
+ let (plugin_thread_sender, screen_receiver, teardown) =
+ create_plugin_thread(Some(plugin_host_folder.clone()));
+
+ let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+ let screen_thread = grant_permissions_and_log_actions_in_thread!(
+ received_screen_instructions,
+ ScreenInstruction::PluginBytes,
+ screen_receiver,
+ 2,
+ &PermissionType::ReadApplicationState,
+ cache_path,
+ plugin_thread_sender,
+ 1
+ );
+
+ let plugin_should_float = Some(false);
+ let plugin_title = Some("test_plugin".to_owned());
+ let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+ configuration: Default::default(),
+ ..Default::default()
+ });
+ let tab_index = 1;
+ let client_id = 1;
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+
+ let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+ let _ = plugin_thread_sender.send(PluginInstruction::Load(
+ plugin_should_float,
+ false,
+ plugin_title,
+ run_plugin,
+ Some(tab_index),
+ None,
+ client_id,
+ size,
+ None,
+ None,
+ false,
+ None,
+ None,
+ None,
+ ));
+
+ std::thread::sleep(std::time::Duration::from_millis(500));
+
+ // Send Ctrl+Shift+B to trigger dump_layout("default")
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(
+ KeyWithModifier::new(BareKey::Char('b'))
+ .with_ctrl_modifier()
+ .with_shift_modifier(),
+ ),
+ )]));
+
+ screen_thread.join().unwrap();
+ teardown();
+
+ let plugin_bytes_event = received_screen_instructions
+ .lock()
+ .unwrap()
+ .iter()
+ .find_map(|i| {
+ if let ScreenInstruction::PluginBytes(plugin_render_assets) = i {
+ for plugin_render_asset in plugin_render_assets {
+ let plugin_bytes = plugin_render_asset.bytes.clone();
+ let plugin_bytes = String::from_utf8_lossy(plugin_bytes.as_slice()).to_string();
+ if plugin_bytes.contains("Layout dump") {
+ return Some(plugin_bytes);
+ }
+ }
+ }
+ None
+ });
+ assert_snapshot!(format!("{:#?}", plugin_bytes_event));
+}
+
+#[test]
+#[ignore]
+pub fn dump_layout_not_found_plugin_command() {
+ let temp_folder = tempdir().unwrap();
+ let plugin_host_folder = PathBuf::from(temp_folder.path());
+ let cache_path = plugin_host_folder.join("permissions_test.kdl");
+ let (plugin_thread_sender, screen_receiver, teardown) =
+ create_plugin_thread(Some(plugin_host_folder.clone()));
+
+ let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+ let screen_thread = grant_permissions_and_log_actions_in_thread!(
+ received_screen_instructions,
+ ScreenInstruction::PluginBytes,
+ screen_receiver,
+ 2,
+ &PermissionType::ReadApplicationState,
+ cache_path,
+ plugin_thread_sender,
+ 1
+ );
+
+ let plugin_should_float = Some(false);
+ let plugin_title = Some("test_plugin".to_owned());
+ let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+ configuration: Default::default(),
+ ..Default::default()
+ });
+ let tab_index = 1;
+ let client_id = 1;
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+
+ let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+ let _ = plugin_thread_sender.send(PluginInstruction::Load(
+ plugin_should_float,
+ false,
+ plugin_title,
+ run_plugin,
+ Some(tab_index),
+ None,
+ client_id,
+ size,
+ None,
+ None,
+ false,
+ None,
+ None,
+ None,
+ ));
+
+ std::thread::sleep(std::time::Duration::from_millis(500));
+
+ // Send Ctrl+Shift+C to trigger dump_layout("nonexistent_layout_xyz")
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(
+ KeyWithModifier::new(BareKey::Char('c'))
+ .with_ctrl_modifier()
+ .with_shift_modifier(),
+ ),
+ )]));
+
+ screen_thread.join().unwrap();
+ teardown();
+
+ let plugin_bytes_event = received_screen_instructions
+ .lock()
+ .unwrap()
+ .iter()
+ .find_map(|i| {
+ if let ScreenInstruction::PluginBytes(plugin_render_assets) = i {
+ for plugin_render_asset in plugin_render_assets {
+ let plugin_bytes = plugin_render_asset.bytes.clone();
+ let plugin_bytes = String::from_utf8_lossy(plugin_bytes.as_slice()).to_string();
+ if plugin_bytes.contains("Layout dump error") {
+ return Some(plugin_bytes);
+ }
+ }
+ }
+ None
+ });
+ assert_snapshot!(format!("{:#?}", plugin_bytes_event));
+}
+
+#[test]
+#[ignore]
+pub fn get_layout_dir_plugin_command() {
+ let temp_folder = tempdir().unwrap();
+ let plugin_host_folder = PathBuf::from(temp_folder.path());
+ let cache_path = plugin_host_folder.join("permissions_test.kdl");
+ let (plugin_thread_sender, screen_receiver, teardown) =
+ create_plugin_thread(Some(plugin_host_folder.clone()));
+
+ let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+ let screen_thread = grant_permissions_and_log_actions_in_thread!(
+ received_screen_instructions,
+ ScreenInstruction::PluginBytes,
+ screen_receiver,
+ 2,
+ &PermissionType::ReadApplicationState,
+ cache_path,
+ plugin_thread_sender,
+ 1
+ );
+
+ let plugin_should_float = Some(false);
+ let plugin_title = Some("test_plugin".to_owned());
+ let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+ configuration: Default::default(),
+ ..Default::default()
+ });
+ let tab_index = 1;
+ let client_id = 1;
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+
+ let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+ let _ = plugin_thread_sender.send(PluginInstruction::Load(
+ plugin_should_float,
+ false,
+ plugin_title,
+ run_plugin,
+ Some(tab_index),
+ None,
+ client_id,
+ size,
+ None,
+ None,
+ false,
+ None,
+ None,
+ None,
+ ));
+
+ std::thread::sleep(std::time::Duration::from_millis(500));
+
+ // Send Ctrl+Shift+D to trigger get_layout_dir()
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(
+ KeyWithModifier::new(BareKey::Char('d'))
+ .with_ctrl_modifier()
+ .with_shift_modifier(),
+ ),
+ )]));
+
+ screen_thread.join().unwrap();
+ teardown();
+
+ let plugin_bytes_event = received_screen_instructions
+ .lock()
+ .unwrap()
+ .iter()
+ .find_map(|i| {
+ if let ScreenInstruction::PluginBytes(plugin_render_assets) = i {
+ for plugin_render_asset in plugin_render_assets {
+ let plugin_bytes = plugin_render_asset.bytes.clone();
+ let plugin_bytes = String::from_utf8_lossy(plugin_bytes.as_slice()).to_string();
+ if plugin_bytes.contains("Got layout folder") {
+ return Some(plugin_bytes);
+ }
+ }
+ }
+ None
+ });
+ assert_snapshot!(format!("{:#?}", plugin_bytes_event));
+}
+
+#[test]
+#[ignore]
+pub fn get_focused_pane_info_plugin_command() {
+ let temp_folder = tempdir().unwrap();
+ let plugin_host_folder = PathBuf::from(temp_folder.path());
+ let cache_path = plugin_host_folder.join("permissions_test.kdl");
+ let (plugin_thread_sender, screen_receiver, teardown) =
+ create_plugin_thread(Some(plugin_host_folder.clone()));
+
+ let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+ let screen_thread = grant_permissions_and_log_actions_in_thread_struct_variant!(
+ received_screen_instructions,
+ ScreenInstruction::GetFocusedPaneInfo,
+ screen_receiver,
+ 1,
+ &PermissionType::ReadApplicationState,
+ cache_path,
+ plugin_thread_sender,
+ 1
+ );
+
+ let plugin_should_float = Some(false);
+ let plugin_title = Some("test_plugin".to_owned());
+ let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+ configuration: Default::default(),
+ ..Default::default()
+ });
+ let tab_index = 1;
+ let client_id = 1;
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+
+ let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+ let _ = plugin_thread_sender.send(PluginInstruction::Load(
+ plugin_should_float,
+ false,
+ plugin_title,
+ run_plugin,
+ Some(tab_index),
+ None,
+ client_id,
+ size,
+ None,
+ None,
+ false,
+ None,
+ None,
+ None,
+ ));
+
+ std::thread::sleep(std::time::Duration::from_millis(500));
+
+ // Send Ctrl+Shift+E to trigger get_focused_pane_info()
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(
+ KeyWithModifier::new(BareKey::Char('e'))
+ .with_ctrl_modifier()
+ .with_shift_modifier(),
+ ),
+ )]));
+
+ screen_thread.join().unwrap();
+ teardown();
+
+ let focused_pane_info_query = received_screen_instructions
+ .lock()
+ .unwrap()
+ .iter()
+ .find_map(|i| {
+ if let ScreenInstruction::GetFocusedPaneInfo { .. } = i {
+ return Some(i.clone());
+ } else {
+ None
+ }
+ });
+ assert_snapshot!(format!("{:#?}", focused_pane_info_query));
+}
+
+#[test]
+#[ignore]
+pub fn dump_session_layout_plugin_command() {
+ let temp_folder = tempdir().unwrap();
+ let plugin_host_folder = PathBuf::from(temp_folder.path());
+ let cache_path = plugin_host_folder.join("permissions_test.kdl");
+ let (plugin_thread_sender, screen_receiver, teardown) =
+ create_plugin_thread(Some(plugin_host_folder.clone()));
+
+ let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+ let screen_thread = grant_permissions_and_log_actions_in_thread!(
+ received_screen_instructions,
+ ScreenInstruction::PluginBytes,
+ screen_receiver,
+ 2,
+ &PermissionType::ReadApplicationState,
+ cache_path,
+ plugin_thread_sender,
+ 1
+ );
+
+ let plugin_should_float = Some(false);
+ let plugin_title = Some("test_plugin".to_owned());
+ let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+ configuration: Default::default(),
+ ..Default::default()
+ });
+ let tab_index = 1;
+ let client_id = 1;
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+
+ let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+ let _ = plugin_thread_sender.send(PluginInstruction::Load(
+ plugin_should_float,
+ false,
+ plugin_title,
+ run_plugin,
+ Some(tab_index),
+ None,
+ client_id,
+ size,
+ None,
+ None,
+ false,
+ None,
+ None,
+ None,
+ ));
+
+ std::thread::sleep(std::time::Duration::from_millis(500));
+
+ // Send Ctrl+Shift+F to trigger dump_session_layout()
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(
+ KeyWithModifier::new(BareKey::Char('f'))
+ .with_ctrl_modifier()
+ .with_shift_modifier(),
+ ),
+ )]));
+
+ screen_thread.join().unwrap();
+ teardown();
+
+ let dump_layout_instruction = received_screen_instructions
+ .lock()
+ .unwrap()
+ .iter()
+ .find_map(|i| {
+ if let ScreenInstruction::DumpLayoutToPlugin { .. } = i {
+ return Some(i.clone());
+ }
+ None
+ });
+ assert_snapshot!(format!("{:#?}", dump_layout_instruction));
+}
+
+#[test]
+#[ignore]
+pub fn dump_session_layout_for_tab_plugin_command() {
+ let temp_folder = tempdir().unwrap();
+ let plugin_host_folder = PathBuf::from(temp_folder.path());
+ let cache_path = plugin_host_folder.join("permissions_test.kdl");
+ let (plugin_thread_sender, screen_receiver, teardown) =
+ create_plugin_thread(Some(plugin_host_folder.clone()));
+
+ let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+ let screen_thread = grant_permissions_and_log_actions_in_thread!(
+ received_screen_instructions,
+ ScreenInstruction::PluginBytes,
+ screen_receiver,
+ 2,
+ &PermissionType::ReadApplicationState,
+ cache_path,
+ plugin_thread_sender,
+ 1
+ );
+
+ let plugin_should_float = Some(false);
+ let plugin_title = Some("test_plugin".to_owned());
+ let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+ configuration: Default::default(),
+ ..Default::default()
+ });
+ let tab_index = 1;
+ let client_id = 1;
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+
+ let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+ let _ = plugin_thread_sender.send(PluginInstruction::Load(
+ plugin_should_float,
+ false,
+ plugin_title,
+ run_plugin,
+ Some(tab_index),
+ None,
+ client_id,
+ size,
+ None,
+ None,
+ false,
+ None,
+ None,
+ None,
+ ));
+
+ std::thread::sleep(std::time::Duration::from_millis(500));
+
+ // Send Ctrl+Shift+G to trigger dump_session_layout_for_tab(0)
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(
+ KeyWithModifier::new(BareKey::Char('g'))
+ .with_ctrl_modifier()
+ .with_shift_modifier(),
+ ),
+ )]));
+
+ screen_thread.join().unwrap();
+ teardown();
+
+ let dump_layout_instruction = received_screen_instructions
+ .lock()
+ .unwrap()
+ .iter()
+ .find_map(|i| {
+ if let ScreenInstruction::DumpLayoutToPlugin { .. } = i {
+ return Some(i.clone());
+ }
+ None
+ });
+ assert_snapshot!(format!("{:#?}", dump_layout_instruction));
+}
+
+#[test]
+#[ignore]
+pub fn parse_layout_success_plugin_command() {
+ let temp_folder = tempdir().unwrap();
+ let plugin_host_folder = PathBuf::from(temp_folder.path());
+ let cache_path = plugin_host_folder.join("permissions_test.kdl");
+ let (plugin_thread_sender, screen_receiver, teardown) =
+ create_plugin_thread(Some(plugin_host_folder.clone()));
+
+ let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+ let screen_thread = grant_permissions_and_log_actions_in_thread!(
+ received_screen_instructions,
+ ScreenInstruction::PluginBytes,
+ screen_receiver,
+ 2,
+ &PermissionType::ReadApplicationState,
+ cache_path,
+ plugin_thread_sender,
+ 1
+ );
+
+ let plugin_should_float = Some(false);
+ let plugin_title = Some("test_plugin".to_owned());
+ let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+ configuration: Default::default(),
+ ..Default::default()
+ });
+ let tab_index = 1;
+ let client_id = 1;
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+
+ let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+ let _ = plugin_thread_sender.send(PluginInstruction::Load(
+ plugin_should_float,
+ false,
+ plugin_title,
+ run_plugin,
+ Some(tab_index),
+ None,
+ client_id,
+ size,
+ None,
+ None,
+ false,
+ None,
+ None,
+ None,
+ ));
+
+ std::thread::sleep(std::time::Duration::from_millis(500));
+
+ // Send Ctrl+Shift+H to trigger parse_layout() with valid KDL
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(
+ KeyWithModifier::new(BareKey::Char('h'))
+ .with_ctrl_modifier()
+ .with_shift_modifier(),
+ ),
+ )]));
+
+ screen_thread.join().unwrap();
+ teardown();
+
+ let plugin_bytes_event = received_screen_instructions
+ .lock()
+ .unwrap()
+ .iter()
+ .find_map(|i| {
+ if let ScreenInstruction::PluginBytes(plugin_render_assets) = i {
+ for plugin_render_asset in plugin_render_assets {
+ let plugin_bytes = plugin_render_asset.bytes.clone();
+ let plugin_bytes = String::from_utf8_lossy(plugin_bytes.as_slice()).to_string();
+ if plugin_bytes.contains("Parse success") {
+ return Some(plugin_bytes);
+ }
+ }
+ }
+ None
+ });
+ assert_snapshot!(format!("{:#?}", plugin_bytes_event));
+}
+
+#[test]
+#[ignore]
+pub fn parse_layout_error_plugin_command() {
+ let temp_folder = tempdir().unwrap();
+ let plugin_host_folder = PathBuf::from(temp_folder.path());
+ let cache_path = plugin_host_folder.join("permissions_test.kdl");
+ let (plugin_thread_sender, screen_receiver, teardown) =
+ create_plugin_thread(Some(plugin_host_folder.clone()));
+
+ let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+ let screen_thread = grant_permissions_and_log_actions_in_thread!(
+ received_screen_instructions,
+ ScreenInstruction::PluginBytes,
+ screen_receiver,
+ 2,
+ &PermissionType::ReadApplicationState,
+ cache_path,
+ plugin_thread_sender,
+ 1
+ );
+
+ let plugin_should_float = Some(false);
+ let plugin_title = Some("test_plugin".to_owned());
+ let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+ configuration: Default::default(),
+ ..Default::default()
+ });
+ let tab_index = 1;
+ let client_id = 1;
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+
+ let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+ let _ = plugin_thread_sender.send(PluginInstruction::Load(
+ plugin_should_float,
+ false,
+ plugin_title,
+ run_plugin,
+ Some(tab_index),
+ None,
+ client_id,
+ size,
+ None,
+ None,
+ false,
+ None,
+ None,
+ None,
+ ));
+
+ std::thread::sleep(std::time::Duration::from_millis(500));
+
+ // Send Ctrl+Shift+I to trigger parse_layout() with invalid KDL
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(
+ KeyWithModifier::new(BareKey::Char('i'))
+ .with_ctrl_modifier()
+ .with_shift_modifier(),
+ ),
+ )]));
+
+ screen_thread.join().unwrap();
+ teardown();
+
+ let plugin_bytes_event = received_screen_instructions
+ .lock()
+ .unwrap()
+ .iter()
+ .find_map(|i| {
+ if let ScreenInstruction::PluginBytes(plugin_render_assets) = i {
+ for plugin_render_asset in plugin_render_assets {
+ let plugin_bytes = plugin_render_asset.bytes.clone();
+ let plugin_bytes = String::from_utf8_lossy(plugin_bytes.as_slice()).to_string();
+ if plugin_bytes.contains("Parse error:") {
+ return Some(plugin_bytes);
+ }
+ }
+ }
+ None
+ });
+ assert_snapshot!(format!("{:#?}", plugin_bytes_event));
+}
+
+#[test]
+#[ignore]
+pub fn save_layout_success_plugin_command() {
+ let temp_folder = tempdir().unwrap();
+ let plugin_host_folder = PathBuf::from(temp_folder.path());
+ let cache_path = plugin_host_folder.join("permissions_test.kdl");
+ let (plugin_thread_sender, screen_receiver, teardown) =
+ create_plugin_thread(Some(plugin_host_folder.clone()));
+
+ let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+ let screen_thread = grant_permissions_and_log_actions_in_thread!(
+ received_screen_instructions,
+ ScreenInstruction::PluginBytes,
+ screen_receiver,
+ 2,
+ &PermissionType::ChangeApplicationState,
+ cache_path,
+ plugin_thread_sender,
+ 1
+ );
+
+ let plugin_should_float = Some(false);
+ let plugin_title = Some("test_plugin".to_owned());
+ let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+ configuration: Default::default(),
+ ..Default::default()
+ });
+ let tab_index = 1;
+ let client_id = 1;
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+
+ let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+ let _ = plugin_thread_sender.send(PluginInstruction::Load(
+ plugin_should_float,
+ false,
+ plugin_title,
+ run_plugin,
+ Some(tab_index),
+ None,
+ client_id,
+ size,
+ None,
+ None,
+ false,
+ None,
+ None,
+ None,
+ ));
+
+ std::thread::sleep(std::time::Duration::from_millis(500));
+
+ // Send Ctrl+Alt+A to trigger save_layout()
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(
+ KeyWithModifier::new(BareKey::Char('a'))
+ .with_ctrl_modifier()
+ .with_alt_modifier(),
+ ),
+ )]));
+
+ screen_thread.join().unwrap();
+ teardown();
+
+ let plugin_bytes_event = received_screen_instructions
+ .lock()
+ .unwrap()
+ .iter()
+ .find_map(|i| {
+ if let ScreenInstruction::PluginBytes(plugin_render_assets) = i {
+ for plugin_render_asset in plugin_render_assets {
+ let plugin_bytes = plugin_render_asset.bytes.clone();
+ let plugin_bytes = String::from_utf8_lossy(plugin_bytes.as_slice()).to_string();
+ if plugin_bytes.contains("Save layout") {
+ return Some(plugin_bytes);
+ }
+ }
+ }
+ None
+ });
+ assert_snapshot!(format!("{:#?}", plugin_bytes_event));
+}
+
+#[test]
+#[ignore]
+pub fn save_layout_already_exists_plugin_command() {
+ let temp_folder = tempdir().unwrap();
+ let plugin_host_folder = PathBuf::from(temp_folder.path());
+ let cache_path = plugin_host_folder.join("permissions_test.kdl");
+ let (plugin_thread_sender, screen_receiver, teardown) =
+ create_plugin_thread(Some(plugin_host_folder.clone()));
+
+ let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+ let screen_thread = grant_permissions_and_log_actions_in_thread!(
+ received_screen_instructions,
+ ScreenInstruction::PluginBytes,
+ screen_receiver,
+ 3,
+ &PermissionType::ChangeApplicationState,
+ cache_path,
+ plugin_thread_sender,
+ 1
+ );
+
+ let plugin_should_float = Some(false);
+ let plugin_title = Some("test_plugin".to_owned());
+ let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+ configuration: Default::default(),
+ ..Default::default()
+ });
+ let tab_index = 1;
+ let client_id = 1;
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+
+ let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+ let _ = plugin_thread_sender.send(PluginInstruction::Load(
+ plugin_should_float,
+ false,
+ plugin_title,
+ run_plugin,
+ Some(tab_index),
+ None,
+ client_id,
+ size,
+ None,
+ None,
+ false,
+ None,
+ None,
+ None,
+ ));
+
+ std::thread::sleep(std::time::Duration::from_millis(500));
+
+ // Send Ctrl+Alt+A to save the layout first time
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(
+ KeyWithModifier::new(BareKey::Char('a'))
+ .with_ctrl_modifier()
+ .with_alt_modifier(),
+ ),
+ )]));
+
+ std::thread::sleep(std::time::Duration::from_millis(200));
+
+ // Send Ctrl+Alt+B to try saving without overwrite
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(
+ KeyWithModifier::new(BareKey::Char('b'))
+ .with_ctrl_modifier()
+ .with_alt_modifier(),
+ ),
+ )]));
+
+ screen_thread.join().unwrap();
+ teardown();
+
+ let plugin_bytes_event = received_screen_instructions
+ .lock()
+ .unwrap()
+ .iter()
+ .rev()
+ .find_map(|i| {
+ if let ScreenInstruction::PluginBytes(plugin_render_assets) = i {
+ for plugin_render_asset in plugin_render_assets {
+ let plugin_bytes = plugin_render_asset.bytes.clone();
+ let plugin_bytes = String::from_utf8_lossy(plugin_bytes.as_slice()).to_string();
+ if plugin_bytes.contains("error") {
+ return Some(plugin_bytes);
+ }
+ }
+ }
+ None
+ });
+ assert_snapshot!(format!("{:#?}", plugin_bytes_event));
+}
+
+#[test]
+#[ignore]
+pub fn save_layout_with_overwrite_plugin_command() {
+ let temp_folder = tempdir().unwrap();
+ let plugin_host_folder = PathBuf::from(temp_folder.path());
+ let cache_path = plugin_host_folder.join("permissions_test.kdl");
+ let (plugin_thread_sender, screen_receiver, teardown) =
+ create_plugin_thread(Some(plugin_host_folder.clone()));
+
+ let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+ let screen_thread = grant_permissions_and_log_actions_in_thread!(
+ received_screen_instructions,
+ ScreenInstruction::PluginBytes,
+ screen_receiver,
+ 2,
+ &PermissionType::ChangeApplicationState,
+ cache_path,
+ plugin_thread_sender,
+ 1
+ );
+
+ let plugin_should_float = Some(false);
+ let plugin_title = Some("test_plugin".to_owned());
+ let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+ configuration: Default::default(),
+ ..Default::default()
+ });
+ let tab_index = 1;
+ let client_id = 1;
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+
+ let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+ let _ = plugin_thread_sender.send(PluginInstruction::Load(
+ plugin_should_float,
+ false,
+ plugin_title,
+ run_plugin,
+ Some(tab_index),
+ None,
+ client_id,
+ size,
+ None,
+ None,
+ false,
+ None,
+ None,
+ None,
+ ));
+
+ std::thread::sleep(std::time::Duration::from_millis(500));
+
+ // Send Ctrl+Alt+C to save with overwrite=true
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(
+ KeyWithModifier::new(BareKey::Char('c'))
+ .with_ctrl_modifier()
+ .with_alt_modifier(),
+ ),
+ )]));
+
+ screen_thread.join().unwrap();
+ teardown();
+
+ let plugin_bytes_event = received_screen_instructions
+ .lock()
+ .unwrap()
+ .iter()
+ .find_map(|i| {
+ if let ScreenInstruction::PluginBytes(plugin_render_assets) = i {
+ for plugin_render_asset in plugin_render_assets {
+ let plugin_bytes = plugin_render_asset.bytes.clone();
+ let plugin_bytes = String::from_utf8_lossy(plugin_bytes.as_slice()).to_string();
+ if plugin_bytes.contains("Save layout") {
+ return Some(plugin_bytes);
+ }
+ }
+ }
+ None
+ });
+ assert_snapshot!(format!("{:#?}", plugin_bytes_event));
+}
+
+#[test]
+#[ignore]
+pub fn save_layout_invalid_kdl_plugin_command() {
+ let temp_folder = tempdir().unwrap();
+ let plugin_host_folder = PathBuf::from(temp_folder.path());
+ let cache_path = plugin_host_folder.join("permissions_test.kdl");
+ let (plugin_thread_sender, screen_receiver, teardown) =
+ create_plugin_thread(Some(plugin_host_folder.clone()));
+
+ let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+ let screen_thread = grant_permissions_and_log_actions_in_thread!(
+ received_screen_instructions,
+ ScreenInstruction::PluginBytes,
+ screen_receiver,
+ 2,
+ &PermissionType::ChangeApplicationState,
+ cache_path,
+ plugin_thread_sender,
+ 1
+ );
+
+ let plugin_should_float = Some(false);
+ let plugin_title = Some("test_plugin".to_owned());
+ let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+ configuration: Default::default(),
+ ..Default::default()
+ });
+ let tab_index = 1;
+ let client_id = 1;
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+
+ let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+ let _ = plugin_thread_sender.send(PluginInstruction::Load(
+ plugin_should_float,
+ false,
+ plugin_title,
+ run_plugin,
+ Some(tab_index),
+ None,
+ client_id,
+ size,
+ None,
+ None,
+ false,
+ None,
+ None,
+ None,
+ ));
+
+ std::thread::sleep(std::time::Duration::from_millis(500));
+
+ // Send Ctrl+Alt+D to save invalid KDL
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(
+ KeyWithModifier::new(BareKey::Char('d'))
+ .with_ctrl_modifier()
+ .with_alt_modifier(),
+ ),
+ )]));
+
+ screen_thread.join().unwrap();
+ teardown();
+
+ let plugin_bytes_event = received_screen_instructions
+ .lock()
+ .unwrap()
+ .iter()
+ .find_map(|i| {
+ if let ScreenInstruction::PluginBytes(plugin_render_assets) = i {
+ for plugin_render_asset in plugin_render_assets {
+ let plugin_bytes = plugin_render_asset.bytes.clone();
+ let plugin_bytes = String::from_utf8_lossy(plugin_bytes.as_slice()).to_string();
+ if plugin_bytes.contains("invalid layout error") {
+ return Some(plugin_bytes);
+ }
+ }
+ }
+ None
+ });
+ assert_snapshot!(format!("{:#?}", plugin_bytes_event));
+}
+
+#[test]
+#[ignore]
+pub fn rename_layout_success_plugin_command() {
+ let temp_folder = tempdir().unwrap();
+ let plugin_host_folder = PathBuf::from(temp_folder.path());
+ let cache_path = plugin_host_folder.join("permissions_test.kdl");
+ let (plugin_thread_sender, screen_receiver, teardown) =
+ create_plugin_thread(Some(plugin_host_folder.clone()));
+
+ let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+ let screen_thread = grant_permissions_and_log_actions_in_thread!(
+ received_screen_instructions,
+ ScreenInstruction::PluginBytes,
+ screen_receiver,
+ 3,
+ &PermissionType::ChangeApplicationState,
+ cache_path,
+ plugin_thread_sender,
+ 1
+ );
+
+ let plugin_should_float = Some(false);
+ let plugin_title = Some("test_plugin".to_owned());
+ let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+ configuration: Default::default(),
+ ..Default::default()
+ });
+ let tab_index = 1;
+ let client_id = 1;
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+
+ let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+ let _ = plugin_thread_sender.send(PluginInstruction::Load(
+ plugin_should_float,
+ false,
+ plugin_title,
+ run_plugin,
+ Some(tab_index),
+ None,
+ client_id,
+ size,
+ None,
+ None,
+ false,
+ None,
+ None,
+ None,
+ ));
+
+ std::thread::sleep(std::time::Duration::from_millis(500));
+
+ // Send Ctrl+Alt+A to save layout first
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(
+ KeyWithModifier::new(BareKey::Char('a'))
+ .with_ctrl_modifier()
+ .with_alt_modifier(),
+ ),
+ )]));
+
+ std::thread::sleep(std::time::Duration::from_millis(200));
+
+ // Send Ctrl+Alt+E to rename layout
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(
+ KeyWithModifier::new(BareKey::Char('e'))
+ .with_ctrl_modifier()
+ .with_alt_modifier(),
+ ),
+ )]));
+
+ screen_thread.join().unwrap();
+ teardown();
+
+ let plugin_bytes_event = received_screen_instructions
+ .lock()
+ .unwrap()
+ .iter()
+ .rev()
+ .find_map(|i| {
+ if let ScreenInstruction::PluginBytes(plugin_render_assets) = i {
+ for plugin_render_asset in plugin_render_assets {
+ let plugin_bytes = plugin_render_asset.bytes.clone();
+ let plugin_bytes = String::from_utf8_lossy(plugin_bytes.as_slice()).to_string();
+ if plugin_bytes.contains("Rename layout") {
+ return Some(plugin_bytes);
+ }
+ }
+ }
+ None
+ });
+ assert_snapshot!(format!("{:#?}", plugin_bytes_event));
+}
+
+#[test]
+#[ignore]
+pub fn rename_layout_not_found_plugin_command() {
+ let temp_folder = tempdir().unwrap();
+ let plugin_host_folder = PathBuf::from(temp_folder.path());
+ let cache_path = plugin_host_folder.join("permissions_test.kdl");
+ let (plugin_thread_sender, screen_receiver, teardown) =
+ create_plugin_thread(Some(plugin_host_folder.clone()));
+
+ let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+ let screen_thread = grant_permissions_and_log_actions_in_thread!(
+ received_screen_instructions,
+ ScreenInstruction::PluginBytes,
+ screen_receiver,
+ 2,
+ &PermissionType::ChangeApplicationState,
+ cache_path,
+ plugin_thread_sender,
+ 1
+ );
+
+ let plugin_should_float = Some(false);
+ let plugin_title = Some("test_plugin".to_owned());
+ let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+ configuration: Default::default(),
+ ..Default::default()
+ });
+ let tab_index = 1;
+ let client_id = 1;
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+
+ let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+ let _ = plugin_thread_sender.send(PluginInstruction::Load(
+ plugin_should_float,
+ false,
+ plugin_title,
+ run_plugin,
+ Some(tab_index),
+ None,
+ client_id,
+ size,
+ None,
+ None,
+ false,
+ None,
+ None,
+ None,
+ ));
+
+ std::thread::sleep(std::time::Duration::from_millis(500));
+
+ // Send Ctrl+Alt+F to rename nonexistent layout
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(
+ KeyWithModifier::new(BareKey::Char('f'))
+ .with_ctrl_modifier()
+ .with_alt_modifier(),
+ ),
+ )]));
+
+ screen_thread.join().unwrap();
+ teardown();
+
+ let plugin_bytes_event = received_screen_instructions
+ .lock()
+ .unwrap()
+ .iter()
+ .find_map(|i| {
+ if let ScreenInstruction::PluginBytes(plugin_render_assets) = i {
+ for plugin_render_asset in plugin_render_assets {
+ let plugin_bytes = plugin_render_asset.bytes.clone();
+ let plugin_bytes = String::from_utf8_lossy(plugin_bytes.as_slice()).to_string();
+ if plugin_bytes.contains("Rename") {
+ return Some(plugin_bytes);
+ }
+ }
+ }
+ None
+ });
+ assert_snapshot!(format!("{:#?}", plugin_bytes_event));
+}
+
+#[test]
+#[ignore]
+pub fn delete_layout_success_plugin_command() {
+ let temp_folder = tempdir().unwrap();
+ let plugin_host_folder = PathBuf::from(temp_folder.path());
+ let cache_path = plugin_host_folder.join("permissions_test.kdl");
+ let (plugin_thread_sender, screen_receiver, teardown) =
+ create_plugin_thread(Some(plugin_host_folder.clone()));
+
+ let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+ let screen_thread = grant_permissions_and_log_actions_in_thread!(
+ received_screen_instructions,
+ ScreenInstruction::PluginBytes,
+ screen_receiver,
+ 3,
+ &PermissionType::ChangeApplicationState,
+ cache_path,
+ plugin_thread_sender,
+ 1
+ );
+
+ let plugin_should_float = Some(false);
+ let plugin_title = Some("test_plugin".to_owned());
+ let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+ configuration: Default::default(),
+ ..Default::default()
+ });
+ let tab_index = 1;
+ let client_id = 1;
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+
+ let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+ let _ = plugin_thread_sender.send(PluginInstruction::Load(
+ plugin_should_float,
+ false,
+ plugin_title,
+ run_plugin,
+ Some(tab_index),
+ None,
+ client_id,
+ size,
+ None,
+ None,
+ false,
+ None,
+ None,
+ None,
+ ));
+
+ std::thread::sleep(std::time::Duration::from_millis(500));
+
+ // Send Ctrl+Alt+A to save layout first
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(
+ KeyWithModifier::new(BareKey::Char('a'))
+ .with_ctrl_modifier()
+ .with_alt_modifier(),
+ ),
+ )]));
+
+ std::thread::sleep(std::time::Duration::from_millis(200));
+
+ // Send Ctrl+Alt+E to rename layout
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(
+ KeyWithModifier::new(BareKey::Char('e'))
+ .with_ctrl_modifier()
+ .with_alt_modifier(),
+ ),
+ )]));
+
+ std::thread::sleep(std::time::Duration::from_millis(200));
+
+ // Send Ctrl+Alt+G to delete renamed layout
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(
+ KeyWithModifier::new(BareKey::Char('g'))
+ .with_ctrl_modifier()
+ .with_alt_modifier(),
+ ),
+ )]));
+
+ screen_thread.join().unwrap();
+ teardown();
+
+ let plugin_bytes_event = received_screen_instructions
+ .lock()
+ .unwrap()
+ .iter()
+ .rev()
+ .find_map(|i| {
+ if let ScreenInstruction::PluginBytes(plugin_render_assets) = i {
+ for plugin_render_asset in plugin_render_assets {
+ let plugin_bytes = plugin_render_asset.bytes.clone();
+ let plugin_bytes = String::from_utf8_lossy(plugin_bytes.as_slice()).to_string();
+ if plugin_bytes.contains("Delete layout") {
+ return Some(plugin_bytes);
+ }
+ }
+ }
+ None
+ });
+ assert_snapshot!(format!("{:#?}", plugin_bytes_event));
+}
+
+#[test]
+#[ignore]
+pub fn delete_layout_not_found_plugin_command() {
+ let temp_folder = tempdir().unwrap();
+ let plugin_host_folder = PathBuf::from(temp_folder.path());
+ let cache_path = plugin_host_folder.join("permissions_test.kdl");
+ let (plugin_thread_sender, screen_receiver, teardown) =
+ create_plugin_thread(Some(plugin_host_folder.clone()));
+
+ let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+ let screen_thread = grant_permissions_and_log_actions_in_thread!(
+ received_screen_instructions,
+ ScreenInstruction::PluginBytes,
+ screen_receiver,
+ 2,
+ &PermissionType::ChangeApplicationState,
+ cache_path,
+ plugin_thread_sender,
+ 1
+ );
+
+ let plugin_should_float = Some(false);
+ let plugin_title = Some("test_plugin".to_owned());
+ let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+ configuration: Default::default(),
+ ..Default::default()
+ });
+ let tab_index = 1;
+ let client_id = 1;
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+
+ let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+ let _ = plugin_thread_sender.send(PluginInstruction::Load(
+ plugin_should_float,
+ false,
+ plugin_title,
+ run_plugin,
+ Some(tab_index),
+ None,
+ client_id,
+ size,
+ None,
+ None,
+ false,
+ None,
+ None,
+ None,
+ ));
+
+ std::thread::sleep(std::time::Duration::from_millis(500));
+
+ // Send Ctrl+Alt+H to delete nonexistent layout
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(
+ KeyWithModifier::new(BareKey::Char('h'))
+ .with_ctrl_modifier()
+ .with_alt_modifier(),
+ ),
+ )]));
+
+ screen_thread.join().unwrap();
+ teardown();
+
+ let plugin_bytes_event = received_screen_instructions
+ .lock()
+ .unwrap()
+ .iter()
+ .find_map(|i| {
+ if let ScreenInstruction::PluginBytes(plugin_render_assets) = i {
+ for plugin_render_asset in plugin_render_assets {
+ let plugin_bytes = plugin_render_asset.bytes.clone();
+ let plugin_bytes = String::from_utf8_lossy(plugin_bytes.as_slice()).to_string();
+ if plugin_bytes.contains("Delete") {
+ return Some(plugin_bytes);
+ }
+ }
+ }
+ None
+ });
+ assert_snapshot!(format!("{:#?}", plugin_bytes_event));
+}
+
+#[test]
+#[ignore]
+pub fn save_layout_path_traversal_blocked_plugin_command() {
+ let temp_folder = tempdir().unwrap();
+ let plugin_host_folder = PathBuf::from(temp_folder.path());
+ let cache_path = plugin_host_folder.join("permissions_test.kdl");
+ let (plugin_thread_sender, screen_receiver, teardown) =
+ create_plugin_thread(Some(plugin_host_folder.clone()));
+
+ let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+ let screen_thread = grant_permissions_and_log_actions_in_thread!(
+ received_screen_instructions,
+ ScreenInstruction::PluginBytes,
+ screen_receiver,
+ 2,
+ &PermissionType::ChangeApplicationState,
+ cache_path,
+ plugin_thread_sender,
+ 1
+ );
+
+ let plugin_should_float = Some(false);
+ let plugin_title = Some("test_plugin".to_owned());
+ let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+ configuration: Default::default(),
+ ..Default::default()
+ });
+ let tab_index = 1;
+ let client_id = 1;
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+
+ let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+ let _ = plugin_thread_sender.send(PluginInstruction::Load(
+ plugin_should_float,
+ false,
+ plugin_title,
+ run_plugin,
+ Some(tab_index),
+ None,
+ client_id,
+ size,
+ None,
+ None,
+ false,
+ None,
+ None,
+ None,
+ ));
+
+ std::thread::sleep(std::time::Duration::from_millis(500));
+
+ // Send Ctrl+Alt+I to test path traversal blocking
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(
+ KeyWithModifier::new(BareKey::Char('i'))
+ .with_ctrl_modifier()
+ .with_alt_modifier(),
+ ),
+ )]));
+
+ screen_thread.join().unwrap();
+ teardown();
+
+ let plugin_bytes_event = received_screen_instructions
+ .lock()
+ .unwrap()
+ .iter()
+ .find_map(|i| {
+ if let ScreenInstruction::PluginBytes(plugin_render_assets) = i {
+ for plugin_render_asset in plugin_render_assets {
+ let plugin_bytes = plugin_render_asset.bytes.clone();
+ let plugin_bytes = String::from_utf8_lossy(plugin_bytes.as_slice()).to_string();
+ if plugin_bytes.contains("Path traversal") {
+ return Some(plugin_bytes);
+ }
+ }
+ }
+ None
+ });
+ assert_snapshot!(format!("{:#?}", plugin_bytes_event));
+}
+
+#[test]
+#[ignore]
+pub fn edit_layout_plugin_command() {
+ let temp_host_folder = tempdir().unwrap();
+ let plugin_host_folder = PathBuf::from(temp_host_folder.path());
+ let temp_layout_folder = tempdir().unwrap();
+ let layout_folder_path = PathBuf::from(temp_layout_folder.path());
+ let cache_path = plugin_host_folder.join("permissions_test.kdl");
+ let (plugin_thread_sender, pty_receiver, screen_receiver, teardown) =
+ create_plugin_thread_with_pty_receiver(Some(plugin_host_folder), Some(layout_folder_path));
+
+ let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+ let received_pty_instructions = Arc::new(Mutex::new(vec![]));
+ let pty_thread = log_actions_in_thread!(
+ received_pty_instructions,
+ PtyInstruction::SpawnInPlaceTerminal,
+ pty_receiver,
+ 1
+ );
+ let _screen_thread = grant_permissions_and_log_actions_in_thread!(
+ received_screen_instructions,
+ ScreenInstruction::PluginBytes,
+ screen_receiver,
+ 3,
+ &PermissionType::ChangeApplicationState,
+ cache_path,
+ plugin_thread_sender,
+ 1
+ );
+
+ let plugin_should_float = Some(false);
+ let plugin_title = Some("test_plugin".to_owned());
+ let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+ configuration: Default::default(),
+ ..Default::default()
+ });
+ let tab_index = 1;
+ let client_id = 1;
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+
+ let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+ let _ = plugin_thread_sender.send(PluginInstruction::Load(
+ plugin_should_float,
+ false,
+ plugin_title,
+ run_plugin,
+ Some(tab_index),
+ None,
+ client_id,
+ size,
+ None,
+ None,
+ false,
+ None,
+ None,
+ None,
+ ));
+
+ std::thread::sleep(std::time::Duration::from_millis(500));
+
+ // Send Ctrl+Alt+J to trigger edit_layout()
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(
+ KeyWithModifier::new(BareKey::Char('j'))
+ .with_ctrl_modifier()
+ .with_alt_modifier(),
+ ),
+ )]));
+
+ pty_thread.join().unwrap();
+ teardown();
+
+ // this is the editor pane for the layout being spawned
+ let spawn_terminal_instruction =
+ received_pty_instructions
+ .lock()
+ .unwrap()
+ .iter()
+ .find_map(|i| {
+ if let PtyInstruction::SpawnInPlaceTerminal(..) = i {
+ Some(i.clone())
+ } else {
+ None
+ }
+ });
+
+ assert_snapshot!(format!("{:#?}", spawn_terminal_instruction).replace(
+ &format!("{}", temp_layout_folder.path().display().to_string()),
+ "LAYOUT_FOLDER_PATH"
+ ))
+}
+
+#[test]
+#[ignore]
+pub fn override_layout_plugin_command() {
+ let temp_folder = tempdir().unwrap();
+ let plugin_host_folder = PathBuf::from(temp_folder.path());
+ let cache_path = plugin_host_folder.join("permissions_test.kdl");
+ let (plugin_thread_sender, screen_receiver, teardown) =
+ create_plugin_thread(Some(plugin_host_folder.clone()));
+
+ let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+ let screen_thread = grant_permissions_and_log_actions_in_thread!(
+ received_screen_instructions,
+ ScreenInstruction::OverrideLayout,
+ screen_receiver,
+ 1,
+ &PermissionType::ChangeApplicationState,
+ cache_path,
+ plugin_thread_sender,
+ 1
+ );
+
+ let plugin_should_float = Some(false);
+ let plugin_title = Some("test_plugin".to_owned());
+ let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+ configuration: Default::default(),
+ ..Default::default()
+ });
+ let tab_index = 1;
+ let client_id = 1;
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+
+ let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+ let _ = plugin_thread_sender.send(PluginInstruction::Load(
+ plugin_should_float,
+ false,
+ plugin_title,
+ run_plugin,
+ Some(tab_index),
+ None,
+ client_id,
+ size,
+ None,
+ None,
+ false,
+ None,
+ None,
+ None,
+ ));
+
+ std::thread::sleep(std::time::Duration::from_millis(500));
+
+ // Send Ctrl+Alt+K to trigger override_layout()
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(
+ KeyWithModifier::new(BareKey::Char('k'))
+ .with_ctrl_modifier()
+ .with_alt_modifier(),
+ ),
+ )]));
+
+ screen_thread.join().unwrap();
+ teardown();
+
+ let override_layout_instruction = received_screen_instructions
+ .lock()
+ .unwrap()
+ .iter()
+ .find_map(|i| {
+ if let ScreenInstruction::OverrideLayout(..) = i {
+ Some(i.clone())
+ } else {
+ None
+ }
+ });
+ assert_snapshot!(format!("{:#?}", override_layout_instruction));
+}
+
+#[test]
+#[ignore]
+pub fn generate_random_name_permission_denied() {
+ let temp_folder = tempdir().unwrap();
+ let plugin_host_folder = PathBuf::from(temp_folder.path());
+ let cache_path = plugin_host_folder.join("permissions_test.kdl");
+ let (plugin_thread_sender, screen_receiver, teardown) =
+ create_plugin_thread(Some(plugin_host_folder.clone()));
+
+ let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+ let client_id = 1;
+
+ let screen_thread = deny_permissions_and_log_actions_in_thread!(
+ received_screen_instructions,
+ ScreenInstruction::PluginBytes,
+ screen_receiver,
+ 2,
+ &PermissionType::ReadApplicationState,
+ cache_path,
+ plugin_thread_sender,
+ client_id
+ );
+
+ let plugin_should_float = Some(false);
+ let plugin_title = Some("test_plugin".to_owned());
+ let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+ configuration: Default::default(),
+ ..Default::default()
+ });
+ let tab_index = 1;
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+
+ let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+ let _ = plugin_thread_sender.send(PluginInstruction::Load(
+ plugin_should_float,
+ false,
+ plugin_title,
+ run_plugin,
+ Some(tab_index),
+ None,
+ client_id,
+ size,
+ None,
+ None,
+ false,
+ None,
+ None,
+ None,
+ ));
+
+ std::thread::sleep(std::time::Duration::from_millis(500));
+
+ // Send Ctrl+Shift+A to trigger generate_random_name()
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(
+ KeyWithModifier::new(BareKey::Char('a'))
+ .with_ctrl_modifier()
+ .with_shift_modifier(),
+ ),
+ )]));
+
+ // Give it time to potentially (incorrectly) send the output
+ std::thread::sleep(std::time::Duration::from_millis(1000));
+
+ let _ = plugin_thread_sender.send(PluginInstruction::Exit);
+ screen_thread.join().unwrap();
+ teardown();
+
+ let plugin_bytes_with_name = received_screen_instructions
+ .lock()
+ .unwrap()
+ .iter()
+ .find_map(|i| {
+ if let ScreenInstruction::PluginBytes(plugin_render_assets) = i {
+ for plugin_render_asset in plugin_render_assets {
+ let plugin_bytes = plugin_render_asset.bytes.clone();
+ let plugin_bytes = String::from_utf8_lossy(plugin_bytes.as_slice()).to_string();
+ if plugin_bytes.contains("Generated name:") {
+ return Some(plugin_bytes);
+ }
+ }
+ }
+ None
+ });
+
+ // Should be None because permission was denied
+ assert_snapshot!(format!("{:#?}", plugin_bytes_with_name));
+}
+
+#[test]
+#[ignore]
+pub fn save_layout_permission_denied() {
+ let temp_folder = tempdir().unwrap();
+ let plugin_host_folder = PathBuf::from(temp_folder.path());
+ let cache_path = plugin_host_folder.join("permissions_test.kdl");
+ let (plugin_thread_sender, screen_receiver, teardown) =
+ create_plugin_thread(Some(plugin_host_folder.clone()));
+
+ let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+ let client_id = 1;
+
+ let screen_thread = deny_permissions_and_log_actions_in_thread!(
+ received_screen_instructions,
+ ScreenInstruction::PluginBytes,
+ screen_receiver,
+ 2,
+ &PermissionType::ChangeApplicationState,
+ cache_path,
+ plugin_thread_sender,
+ client_id
+ );
+
+ let plugin_should_float = Some(false);
+ let plugin_title = Some("test_plugin".to_owned());
+ let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+ configuration: Default::default(),
+ ..Default::default()
+ });
+ let tab_index = 1;
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+
+ let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+ let _ = plugin_thread_sender.send(PluginInstruction::Load(
+ plugin_should_float,
+ false,
+ plugin_title,
+ run_plugin,
+ Some(tab_index),
+ None,
+ client_id,
+ size,
+ None,
+ None,
+ false,
+ None,
+ None,
+ None,
+ ));
+
+ std::thread::sleep(std::time::Duration::from_millis(500));
+
+ // Send Ctrl+Alt+A to trigger save_layout()
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(
+ KeyWithModifier::new(BareKey::Char('a'))
+ .with_ctrl_modifier()
+ .with_alt_modifier(),
+ ),
+ )]));
+
+ // Give it time to potentially (incorrectly) send the output
+ std::thread::sleep(std::time::Duration::from_millis(1000));
+
+ let _ = plugin_thread_sender.send(PluginInstruction::Exit);
+ screen_thread.join().unwrap();
+ teardown();
+
+ let plugin_bytes_with_save = received_screen_instructions
+ .lock()
+ .unwrap()
+ .iter()
+ .find_map(|i| {
+ if let ScreenInstruction::PluginBytes(plugin_render_assets) = i {
+ for plugin_render_asset in plugin_render_assets {
+ let plugin_bytes = plugin_render_asset.bytes.clone();
+ let plugin_bytes = String::from_utf8_lossy(plugin_bytes.as_slice()).to_string();
+ if plugin_bytes.contains("Save layout") {
+ return Some(plugin_bytes);
+ }
+ }
+ }
+ None
+ });
+
+ // Should be None because permission was denied
+ assert_snapshot!(format!("{:#?}", plugin_bytes_with_save));
+}
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__delete_layout_not_found_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__delete_layout_not_found_plugin_command.snap
new file mode 100644
index 00000000..470461a5
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__delete_layout_not_found_plugin_command.snap
@@ -0,0 +1,7 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+expression: "format!(\"{:#?}\", plugin_bytes_event)"
+---
+Some(
+ "Delete nonexistent layout error: Layout 'nonexistent_layout' not found\n\r",
+)
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__delete_layout_success_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__delete_layout_success_plugin_command.snap
new file mode 100644
index 00000000..1af0a4de
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__delete_layout_success_plugin_command.snap
@@ -0,0 +1,5 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+expression: "format!(\"{:#?}\", plugin_bytes_event)"
+---
+None
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__dump_layout_not_found_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__dump_layout_not_found_plugin_command.snap
new file mode 100644
index 00000000..aac26703
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__dump_layout_not_found_plugin_command.snap
@@ -0,0 +1,7 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+expression: "format!(\"{:#?}\", plugin_bytes_event)"
+---
+Some(
+ "Layout dump error: IoError: The layout was not found, File: nonexistent_layout_xyz\n\r",
+)
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__dump_layout_success_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__dump_layout_success_plugin_command.snap
new file mode 100644
index 00000000..df2ce697
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__dump_layout_success_plugin_command.snap
@@ -0,0 +1,7 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+expression: "format!(\"{:#?}\", plugin_bytes_event)"
+---
+Some(
+ "Layout dump success: layout {\n\r pane size=1 borderless=true {\n\r plugin location=\"tab-bar\"\n\r }\n\r pane\n\r pane size=1 borderless=true {\n\r plugin location=\"status-bar\"\n\r }\n\r}\n\r",
+)
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__dump_session_layout_for_tab_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__dump_session_layout_for_tab_plugin_command.snap
new file mode 100644
index 00000000..2299bca7
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__dump_session_layout_for_tab_plugin_command.snap
@@ -0,0 +1,13 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+expression: "format!(\"{:#?}\", dump_layout_instruction)"
+---
+Some(
+ DumpLayoutToPlugin {
+ plugin_id: 0,
+ tab_index: Some(
+ 0,
+ ),
+ response_channel: Sender { .. },
+ },
+)
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__dump_session_layout_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__dump_session_layout_plugin_command.snap
new file mode 100644
index 00000000..95b6b784
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__dump_session_layout_plugin_command.snap
@@ -0,0 +1,11 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+expression: "format!(\"{:#?}\", dump_layout_instruction)"
+---
+Some(
+ DumpLayoutToPlugin {
+ plugin_id: 0,
+ tab_index: None,
+ response_channel: Sender { .. },
+ },
+)
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__edit_layout_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__edit_layout_plugin_command.snap
new file mode 100644
index 00000000..611f2015
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__edit_layout_plugin_command.snap
@@ -0,0 +1,45 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+expression: "format!(\"{:#?}\",\nspawn_terminal_instruction).replace(&format!(\"{}\",\ntemp_layout_folder.path().display().to_string()), \"LAYOUT_FOLDER_PATH\")"
+---
+Some(
+ SpawnInPlaceTerminal(
+ Some(
+ OpenFile(
+ OpenFilePayload {
+ path: "LAYOUT_FOLDER_PATH/test_layout2.kdl",
+ line_number: None,
+ cwd: Some(
+ "LAYOUT_FOLDER_PATH",
+ ),
+ originating_plugin: Some(
+ OriginatingPlugin {
+ plugin_id: 0,
+ client_id: 1,
+ context: {
+ "test_key": "test_value",
+ },
+ },
+ ),
+ },
+ ),
+ ),
+ Some(
+ "Editing: LAYOUT_FOLDER_PATH/test_layout2.kdl",
+ ),
+ false,
+ PaneId(
+ Plugin(
+ 0,
+ ),
+ ),
+ Some(
+ NotificationEnd {
+ channel: None,
+ exit_status: None,
+ unblock_condition: None,
+ affected_pane_id: None,
+ },
+ ),
+ ),
+)
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__generate_random_name_permission_denied.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__generate_random_name_permission_denied.snap
new file mode 100644
index 00000000..1383e484
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__generate_random_name_permission_denied.snap
@@ -0,0 +1,5 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+expression: "format!(\"{:#?}\", plugin_bytes_with_name)"
+---
+None
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__generate_random_name_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__generate_random_name_plugin_command.snap
new file mode 100644
index 00000000..78ff0528
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__generate_random_name_plugin_command.snap
@@ -0,0 +1,7 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+expression: "format!(\"{:#?}\", plugin_bytes_event)"
+---
+Some(
+ "Generated name\n\r",
+)
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__get_focused_pane_info_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__get_focused_pane_info_plugin_command.snap
new file mode 100644
index 00000000..bd6d78eb
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__get_focused_pane_info_plugin_command.snap
@@ -0,0 +1,10 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+expression: "format!(\"{:#?}\", focused_pane_info_query)"
+---
+Some(
+ GetFocusedPaneInfo {
+ client_id: 1,
+ response_channel: Sender { .. },
+ },
+)
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__get_layout_dir_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__get_layout_dir_plugin_command.snap
new file mode 100644
index 00000000..8d421c3f
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__get_layout_dir_plugin_command.snap
@@ -0,0 +1,7 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+expression: "format!(\"{:#?}\", plugin_bytes_event)"
+---
+Some(
+ "Got layout folder\n\r",
+)
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__override_layout_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__override_layout_plugin_command.snap
new file mode 100644
index 00000000..24de4279
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__override_layout_plugin_command.snap
@@ -0,0 +1,1977 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+expression: "format!(\"{:#?}\", override_layout_instruction)"
+---
+Some(
+ OverrideLayout(
+ None,
+ None,
+ [
+ TabLayoutInfo {
+ tab_index: 0,
+ tab_name: None,
+ tiled_layout: TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: Some(
+ Fixed(
+ 1,
+ ),
+ ),
+ run: Some(
+ Plugin(
+ Alias(
+ PluginAlias {
+ name: "compact-bar",
+ configuration: Some(
+ PluginUserConfiguration(
+ {},
+ ),
+ ),
+ initial_cwd: None,
+ run_plugin: None,
+ },
+ ),
+ ),
+ ),
+ borderless: true,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ floating_layouts: [],
+ swap_tiled_layouts: Some(
+ [
+ (
+ {
+ MaxPanes(
+ 4,
+ ): TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Vertical,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: Some(
+ 0,
+ ),
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: Some(
+ Fixed(
+ 1,
+ ),
+ ),
+ run: Some(
+ Plugin(
+ Alias(
+ PluginAlias {
+ name: "compact-bar",
+ configuration: Some(
+ PluginUserConfiguration(
+ {},
+ ),
+ ),
+ initial_cwd: None,
+ run_plugin: None,
+ },
+ ),
+ ),
+ ),
+ borderless: true,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ MaxPanes(
+ 7,
+ ): TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Vertical,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: Some(
+ 0,
+ ),
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: Some(
+ Fixed(
+ 1,
+ ),
+ ),
+ run: Some(
+ Plugin(
+ Alias(
+ PluginAlias {
+ name: "compact-bar",
+ configuration: Some(
+ PluginUserConfiguration(
+ {},
+ ),
+ ),
+ initial_cwd: None,
+ run_plugin: None,
+ },
+ ),
+ ),
+ ),
+ borderless: true,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ MaxPanes(
+ 11,
+ ): TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Vertical,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: Some(
+ 0,
+ ),
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: Some(
+ Fixed(
+ 1,
+ ),
+ ),
+ run: Some(
+ Plugin(
+ Alias(
+ PluginAlias {
+ name: "compact-bar",
+ configuration: Some(
+ PluginUserConfiguration(
+ {},
+ ),
+ ),
+ initial_cwd: None,
+ run_plugin: None,
+ },
+ ),
+ ),
+ ),
+ borderless: true,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ },
+ Some(
+ "vertical",
+ ),
+ ),
+ (
+ {
+ MaxPanes(
+ 3,
+ ): TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: Some(
+ Fixed(
+ 1,
+ ),
+ ),
+ run: Some(
+ Plugin(
+ Alias(
+ PluginAlias {
+ name: "compact-bar",
+ configuration: Some(
+ PluginUserConfiguration(
+ {},
+ ),
+ ),
+ initial_cwd: None,
+ run_plugin: None,
+ },
+ ),
+ ),
+ ),
+ borderless: true,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ MaxPanes(
+ 7,
+ ): TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Vertical,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: Some(
+ 0,
+ ),
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Vertical,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: Some(
+ Fixed(
+ 1,
+ ),
+ ),
+ run: Some(
+ Plugin(
+ Alias(
+ PluginAlias {
+ name: "compact-bar",
+ configuration: Some(
+ PluginUserConfiguration(
+ {},
+ ),
+ ),
+ initial_cwd: None,
+ run_plugin: None,
+ },
+ ),
+ ),
+ ),
+ borderless: true,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ MaxPanes(
+ 11,
+ ): TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Vertical,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: Some(
+ 0,
+ ),
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Vertical,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Vertical,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: Some(
+ Fixed(
+ 1,
+ ),
+ ),
+ run: Some(
+ Plugin(
+ Alias(
+ PluginAlias {
+ name: "compact-bar",
+ configuration: Some(
+ PluginUserConfiguration(
+ {},
+ ),
+ ),
+ initial_cwd: None,
+ run_plugin: None,
+ },
+ ),
+ ),
+ ),
+ borderless: true,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ },
+ Some(
+ "horizontal",
+ ),
+ ),
+ (
+ {
+ MinPanes(
+ 4,
+ ): TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Vertical,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: Some(
+ 0,
+ ),
+ children_are_stacked: true,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: Some(
+ Fixed(
+ 1,
+ ),
+ ),
+ run: Some(
+ Plugin(
+ Alias(
+ PluginAlias {
+ name: "compact-bar",
+ configuration: Some(
+ PluginUserConfiguration(
+ {},
+ ),
+ ),
+ initial_cwd: None,
+ run_plugin: None,
+ },
+ ),
+ ),
+ ),
+ borderless: true,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ },
+ },
+ Some(
+ "stacked",
+ ),
+ ),
+ ],
+ ),
+ swap_floating_layouts: Some(
+ [
+ (
+ {
+ NoConstraint: [],
+ },
+ Some(
+ "staggered",
+ ),
+ ),
+ (
+ {
+ MaxPanes(
+ 10,
+ ): [
+ FloatingPaneLayout {
+ name: None,
+ height: Some(
+ Percent(
+ 90,
+ ),
+ ),
+ width: Some(
+ Percent(
+ 90,
+ ),
+ ),
+ x: Some(
+ Percent(
+ 5,
+ ),
+ ),
+ y: Some(
+ Fixed(
+ 1,
+ ),
+ ),
+ pinned: None,
+ run: None,
+ focus: None,
+ already_running: false,
+ pane_initial_contents: None,
+ logical_position: None,
+ },
+ FloatingPaneLayout {
+ name: None,
+ height: Some(
+ Percent(
+ 90,
+ ),
+ ),
+ width: Some(
+ Percent(
+ 90,
+ ),
+ ),
+ x: Some(
+ Percent(
+ 5,
+ ),
+ ),
+ y: Some(
+ Fixed(
+ 2,
+ ),
+ ),
+ pinned: None,
+ run: None,
+ focus: None,
+ already_running: false,
+ pane_initial_contents: None,
+ logical_position: None,
+ },
+ FloatingPaneLayout {
+ name: None,
+ height: Some(
+ Percent(
+ 90,
+ ),
+ ),
+ width: Some(
+ Percent(
+ 90,
+ ),
+ ),
+ x: Some(
+ Percent(
+ 5,
+ ),
+ ),
+ y: Some(
+ Fixed(
+ 3,
+ ),
+ ),
+ pinned: None,
+ run: None,
+ focus: None,
+ already_running: false,
+ pane_initial_contents: None,
+ logical_position: None,
+ },
+ FloatingPaneLayout {
+ name: None,
+ height: Some(
+ Percent(
+ 90,
+ ),
+ ),
+ width: Some(
+ Percent(
+ 90,
+ ),
+ ),
+ x: Some(
+ Percent(
+ 5,
+ ),
+ ),
+ y: Some(
+ Fixed(
+ 4,
+ ),
+ ),
+ pinned: None,
+ run: None,
+ focus: None,
+ already_running: false,
+ pane_initial_contents: None,
+ logical_position: None,
+ },
+ FloatingPaneLayout {
+ name: None,
+ height: Some(
+ Percent(
+ 90,
+ ),
+ ),
+ width: Some(
+ Percent(
+ 90,
+ ),
+ ),
+ x: Some(
+ Percent(
+ 5,
+ ),
+ ),
+ y: Some(
+ Fixed(
+ 5,
+ ),
+ ),
+ pinned: None,
+ run: None,
+ focus: None,
+ already_running: false,
+ pane_initial_contents: None,
+ logical_position: None,
+ },
+ FloatingPaneLayout {
+ name: None,
+ height: Some(
+ Percent(
+ 90,
+ ),
+ ),
+ width: Some(
+ Percent(
+ 90,
+ ),
+ ),
+ x: Some(
+ Percent(
+ 5,
+ ),
+ ),
+ y: Some(
+ Fixed(
+ 6,
+ ),
+ ),
+ pinned: None,
+ run: None,
+ focus: None,
+ already_running: false,
+ pane_initial_contents: None,
+ logical_position: None,
+ },
+ FloatingPaneLayout {
+ name: None,
+ height: Some(
+ Percent(
+ 90,
+ ),
+ ),
+ width: Some(
+ Percent(
+ 90,
+ ),
+ ),
+ x: Some(
+ Percent(
+ 5,
+ ),
+ ),
+ y: Some(
+ Fixed(
+ 7,
+ ),
+ ),
+ pinned: None,
+ run: None,
+ focus: None,
+ already_running: false,
+ pane_initial_contents: None,
+ logical_position: None,
+ },
+ FloatingPaneLayout {
+ name: None,
+ height: Some(
+ Percent(
+ 90,
+ ),
+ ),
+ width: Some(
+ Percent(
+ 90,
+ ),
+ ),
+ x: Some(
+ Percent(
+ 5,
+ ),
+ ),
+ y: Some(
+ Fixed(
+ 8,
+ ),
+ ),
+ pinned: None,
+ run: None,
+ focus: None,
+ already_running: false,
+ pane_initial_contents: None,
+ logical_position: None,
+ },
+ FloatingPaneLayout {
+ name: None,
+ height: Some(
+ Percent(
+ 90,
+ ),
+ ),
+ width: Some(
+ Percent(
+ 90,
+ ),
+ ),
+ x: Some(
+ Percent(
+ 5,
+ ),
+ ),
+ y: Some(
+ Fixed(
+ 9,
+ ),
+ ),
+ pinned: None,
+ run: None,
+ focus: None,
+ already_running: false,
+ pane_initial_contents: None,
+ logical_position: None,
+ },
+ FloatingPaneLayout {
+ name: None,
+ height: Some(
+ Percent(
+ 90,
+ ),
+ ),
+ width: Some(
+ Percent(
+ 90,
+ ),
+ ),
+ x: Some(
+ Fixed(
+ 10,
+ ),
+ ),
+ y: Some(
+ Fixed(
+ 10,
+ ),
+ ),
+ pinned: None,
+ run: None,
+ focus: None,
+ already_running: false,
+ pane_initial_contents: None,
+ logical_position: None,
+ },
+ ],
+ },
+ Some(
+ "enlarged",
+ ),
+ ),
+ (
+ {
+ MaxPanes(
+ 1,
+ ): [
+ FloatingPaneLayout {
+ name: None,
+ height: None,
+ width: None,
+ x: Some(
+ Percent(
+ 50,
+ ),
+ ),
+ y: Some(
+ Percent(
+ 50,
+ ),
+ ),
+ pinned: None,
+ run: None,
+ focus: None,
+ already_running: false,
+ pane_initial_contents: None,
+ logical_position: None,
+ },
+ ],
+ MaxPanes(
+ 2,
+ ): [
+ FloatingPaneLayout {
+ name: None,
+ height: None,
+ width: Some(
+ Percent(
+ 45,
+ ),
+ ),
+ x: Some(
+ Percent(
+ 1,
+ ),
+ ),
+ y: Some(
+ Percent(
+ 25,
+ ),
+ ),
+ pinned: None,
+ run: None,
+ focus: None,
+ already_running: false,
+ pane_initial_contents: None,
+ logical_position: None,
+ },
+ FloatingPaneLayout {
+ name: None,
+ height: None,
+ width: Some(
+ Percent(
+ 45,
+ ),
+ ),
+ x: Some(
+ Percent(
+ 50,
+ ),
+ ),
+ y: Some(
+ Percent(
+ 25,
+ ),
+ ),
+ pinned: None,
+ run: None,
+ focus: None,
+ already_running: false,
+ pane_initial_contents: None,
+ logical_position: None,
+ },
+ ],
+ MaxPanes(
+ 3,
+ ): [
+ FloatingPaneLayout {
+ name: None,
+ height: Some(
+ Percent(
+ 45,
+ ),
+ ),
+ width: Some(
+ Percent(
+ 45,
+ ),
+ ),
+ x: None,
+ y: Some(
+ Percent(
+ 55,
+ ),
+ ),
+ pinned: None,
+ run: None,
+ focus: None,
+ already_running: false,
+ pane_initial_contents: None,
+ logical_position: None,
+ },
+ FloatingPaneLayout {
+ name: None,
+ height: None,
+ width: Some(
+ Percent(
+ 45,
+ ),
+ ),
+ x: Some(
+ Percent(
+ 1,
+ ),
+ ),
+ y: Some(
+ Percent(
+ 1,
+ ),
+ ),
+ pinned: None,
+ run: None,
+ focus: None,
+ already_running: false,
+ pane_initial_contents: None,
+ logical_position: None,
+ },
+ FloatingPaneLayout {
+ name: None,
+ height: None,
+ width: Some(
+ Percent(
+ 45,
+ ),
+ ),
+ x: Some(
+ Percent(
+ 50,
+ ),
+ ),
+ y: Some(
+ Percent(
+ 1,
+ ),
+ ),
+ pinned: None,
+ run: None,
+ focus: None,
+ already_running: false,
+ pane_initial_contents: None,
+ logical_position: None,
+ },
+ ],
+ MaxPanes(
+ 4,
+ ): [
+ FloatingPaneLayout {
+ name: None,
+ height: Some(
+ Percent(
+ 45,
+ ),
+ ),
+ width: Some(
+ Percent(
+ 45,
+ ),
+ ),
+ x: Some(
+ Percent(
+ 1,
+ ),
+ ),
+ y: Some(
+ Percent(
+ 55,
+ ),
+ ),
+ pinned: None,
+ run: None,
+ focus: None,
+ already_running: false,
+ pane_initial_contents: None,
+ logical_position: None,
+ },
+ FloatingPaneLayout {
+ name: None,
+ height: Some(
+ Percent(
+ 45,
+ ),
+ ),
+ width: Some(
+ Percent(
+ 45,
+ ),
+ ),
+ x: Some(
+ Percent(
+ 50,
+ ),
+ ),
+ y: Some(
+ Percent(
+ 55,
+ ),
+ ),
+ pinned: None,
+ run: None,
+ focus: None,
+ already_running: false,
+ pane_initial_contents: None,
+ logical_position: None,
+ },
+ FloatingPaneLayout {
+ name: None,
+ height: Some(
+ Percent(
+ 45,
+ ),
+ ),
+ width: Some(
+ Percent(
+ 45,
+ ),
+ ),
+ x: Some(
+ Percent(
+ 1,
+ ),
+ ),
+ y: Some(
+ Percent(
+ 1,
+ ),
+ ),
+ pinned: None,
+ run: None,
+ focus: None,
+ already_running: false,
+ pane_initial_contents: None,
+ logical_position: None,
+ },
+ FloatingPaneLayout {
+ name: None,
+ height: Some(
+ Percent(
+ 45,
+ ),
+ ),
+ width: Some(
+ Percent(
+ 45,
+ ),
+ ),
+ x: Some(
+ Percent(
+ 50,
+ ),
+ ),
+ y: Some(
+ Percent(
+ 1,
+ ),
+ ),
+ pinned: None,
+ run: None,
+ focus: None,
+ already_running: false,
+ pane_initial_contents: None,
+ logical_position: None,
+ },
+ ],
+ },
+ Some(
+ "spread",
+ ),
+ ),
+ ],
+ ),
+ },
+ ],
+ false,
+ false,
+ false,
+ 1,
+ Some(
+ NotificationEnd {
+ channel: None,
+ exit_status: None,
+ unblock_condition: None,
+ affected_pane_id: None,
+ },
+ ),
+ ),
+)
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__parse_layout_error_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__parse_layout_error_plugin_command.snap
new file mode 100644
index 00000000..8e665541
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__parse_layout_error_plugin_command.snap
@@ -0,0 +1,7 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+expression: "format!(\"{:#?}\", plugin_bytes_event)"
+---
+Some(
+ "Parse error: KdlError { kdl_error: KdlError { error_message: \"Failed to deserialize KDL node. \\nPossible reasons:\\n- Missing `;` after a node name, eg. { node; another_node; }\\n- Missing quotations (\\\") around an argument node eg. { first_node \\\"argument_node\\\"; }\\n- Missing an equal sign (=) between node arguments on a title line. eg. argument=\\\"value\\\"\\n- Found an extraneous equal sign (=) between node child arguments and their values. eg. { argument=\\\"value\\\" }\", src: None, offset: Some(9), len: Some(5), help_message: None }, file_name: \"parse_layout_api\", source_code: \"layout { this is not valid kdl }\" }\n\r",
+)
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__parse_layout_success_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__parse_layout_success_plugin_command.snap
new file mode 100644
index 00000000..73583531
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__parse_layout_success_plugin_command.snap
@@ -0,0 +1,7 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+expression: "format!(\"{:#?}\", plugin_bytes_event)"
+---
+Some(
+ "Parse success\n\r",
+)
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_layout_not_found_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_layout_not_found_plugin_command.snap
new file mode 100644
index 00000000..59ff10a3
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_layout_not_found_plugin_command.snap
@@ -0,0 +1,7 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+expression: "format!(\"{:#?}\", plugin_bytes_event)"
+---
+Some(
+ "Rename nonexistent layout error: Layout 'nonexistent_layout' not found\n\r",
+)
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_layout_success_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_layout_success_plugin_command.snap
new file mode 100644
index 00000000..8a4810a0
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_layout_success_plugin_command.snap
@@ -0,0 +1,7 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+expression: "format!(\"{:#?}\", plugin_bytes_event)"
+---
+Some(
+ "Rename layout success\n\r",
+)
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__save_layout_already_exists_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__save_layout_already_exists_plugin_command.snap
new file mode 100644
index 00000000..a4487819
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__save_layout_already_exists_plugin_command.snap
@@ -0,0 +1,7 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+expression: "format!(\"{:#?}\", plugin_bytes_event)"
+---
+Some(
+ "Save layout error: Layout file 'test_layout' already exists. Use overwrite flag to replace it.\n\r",
+)
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__save_layout_invalid_kdl_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__save_layout_invalid_kdl_plugin_command.snap
new file mode 100644
index 00000000..0e02eefd
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__save_layout_invalid_kdl_plugin_command.snap
@@ -0,0 +1,7 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+expression: "format!(\"{:#?}\", plugin_bytes_event)"
+---
+Some(
+ "Save invalid layout error: Invalid layout KDL: KdlError(KdlError { error_message: \"Failed to deserialize KDL node. \\nPossible reasons:\\n- Missing `;` after a node name, eg. { node; another_node; }\\n- Missing quotations (\\\") around an argument node eg. { first_node \\\"argument_node\\\"; }\\n- Missing an equal sign (=) between node arguments on a title line. eg. argument=\\\"value\\\"\\n- Found an extraneous equal sign (=) between node child arguments and their values. eg. { argument=\\\"value\\\" }\", src: Some(NamedSource { name: \"invalid_layout.kdl\", source: \"<redacted>\"), offset: Some(0), len: Some(4), help_message: None })\n\r",
+)
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__save_layout_path_traversal_blocked_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__save_layout_path_traversal_blocked_plugin_command.snap
new file mode 100644
index 00000000..8c950759
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__save_layout_path_traversal_blocked_plugin_command.snap
@@ -0,0 +1,7 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+expression: "format!(\"{:#?}\", plugin_bytes_event)"
+---
+Some(
+ "Path traversal save error: Layout name cannot contain path separators\n\r",
+)
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__save_layout_permission_denied.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__save_layout_permission_denied.snap
new file mode 100644
index 00000000..e8605f1e
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__save_layout_permission_denied.snap
@@ -0,0 +1,5 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+expression: "format!(\"{:#?}\", plugin_bytes_with_save)"
+---
+None
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__save_layout_success_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__save_layout_success_plugin_command.snap
new file mode 100644
index 00000000..ba23455c
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__save_layout_success_plugin_command.snap
@@ -0,0 +1,7 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+expression: "format!(\"{:#?}\", plugin_bytes_event)"
+---
+Some(
+ "Save layout success\n\r",
+)
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__save_layout_with_overwrite_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__save_layout_with_overwrite_plugin_command.snap
new file mode 100644
index 00000000..11f09285
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__save_layout_with_overwrite_plugin_command.snap
@@ -0,0 +1,7 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+expression: "format!(\"{:#?}\", plugin_bytes_event)"
+---
+Some(
+ "Save layout with overwrite success\n\r",
+)
diff --git a/zellij-server/src/plugins/wasm_bridge.rs b/zellij-server/src/plugins/wasm_bridge.rs
index 2453a0a1..bdbe25f3 100644
--- a/zellij-server/src/plugins/wasm_bridge.rs
+++ b/zellij-server/src/plugins/wasm_bridge.rs
@@ -23,8 +23,8 @@ use url::Url;
use wasmi::{Engine, Module};
use zellij_utils::consts::{ZELLIJ_CACHE_DIR, ZELLIJ_SESSION_CACHE_DIR, ZELLIJ_TMP_DIR};
use zellij_utils::data::{
- FloatingPaneCoordinates, InputMode, PaneContents, PaneRenderReport, PermissionStatus,
- PermissionType, PipeMessage, PipeSource,
+ FloatingPaneCoordinates, InputMode, LayoutInfo, LayoutWithError, PaneContents,
+ PaneRenderReport, PermissionStatus, PermissionType, PipeMessage, PipeSource,
};
use zellij_utils::downloader::Downloader;
use zellij_utils::input::keybinds::Keybinds;
@@ -179,6 +179,8 @@ pub struct WasmBridge {
HashMap<RunPluginLocation, HashMap<PluginUserConfiguration, Vec<(PluginId, ClientId)>>>,
pending_pipes: PendingPipes,
layout_dir: Option<PathBuf>,
+ available_layouts: Vec<LayoutInfo>,
+ available_layout_errors: Vec<LayoutWithError>,
default_mode: InputMode,
default_keybinds: Keybinds,
keybinds: HashMap<ClientId, Keybinds>,
@@ -199,6 +201,8 @@ impl WasmBridge {
default_shell: Option<TerminalAction>,
default_layout: Box<Layout>,
layout_dir: Option<PathBuf>,
+ available_layouts: Vec<LayoutInfo>,
+ available_layout_errors: Vec<LayoutWithError>,
default_mode: InputMode,
default_keybinds: Keybinds,
) -> Self {
@@ -240,6 +244,8 @@ impl WasmBridge {
cached_plugin_map: HashMap::new(),
pending_pipes: Default::default(),
layout_dir,
+ available_layouts,
+ available_layout_errors,
default_mode,
default_keybinds,
keybinds: HashMap::new(),
@@ -1324,6 +1330,7 @@ impl WasmBridge {
keybinds: Option<Keybinds>,
default_mode: Option<InputMode>,
default_shell: Option<TerminalAction>,
+ layout_dir: Option<PathBuf>,
) -> Result<()> {
let plugins_to_reconfigure: Vec<(PluginId, Arc<Mutex<RunningPlugin>>)> = self
.plugin_map
@@ -1347,11 +1354,13 @@ impl WasmBridge {
self.keybinds.insert(client_id, keybinds.clone());
}
self.default_shell = default_shell.clone();
+ self.layout_dir = layout_dir.clone();
for (plugin_id, running_plugin) in plugins_to_reconfigure {
self.plugin_executor.execute_for_plugin(plugin_id, {
let running_plugin = running_plugin.clone();
let keybinds = keybinds.clone();
let default_shell = default_shell.clone();
+ let layout_dir = layout_dir.clone();
move |_senders,
_plugin_map,
_connected_clients,
@@ -1366,6 +1375,7 @@ impl WasmBridge {
running_plugin.update_default_mode(default_mode);
}
running_plugin.update_default_shell(default_shell);
+ running_plugin.update_layout_dir(layout_dir);
}
});
}
@@ -1842,6 +1852,35 @@ impl WasmBridge {
.next()
.copied()
}
+ pub fn update_available_layouts(
+ &mut self,
+ layouts: Vec<LayoutInfo>,
+ errors: Vec<LayoutWithError>,
+ ) {
+ // Diff with existing layouts
+ if self.available_layouts != layouts || self.available_layout_errors != errors {
+ // Update the stored layouts
+ self.available_layouts = layouts.clone();
+ self.available_layout_errors = errors.clone();
+
+ // Notify all plugins of the change
+ let _ = self.senders.send_to_plugin(PluginInstruction::Update(vec![(
+ None, // Broadcast to all plugins
+ None, // Broadcast to all clients
+ Event::AvailableLayoutInfo(layouts, errors),
+ )]));
+ }
+ }
+ pub fn state_update_for_plugin(&self, plugin_id: PluginId) {
+ let _ = self.senders.send_to_plugin(PluginInstruction::Update(vec![(
+ Some(plugin_id),
+ None,
+ Event::AvailableLayoutInfo(
+ self.available_layouts.clone(),
+ self.available_layout_errors.clone(),
+ ),
+ )]));
+ }
}
fn handle_plugin_successful_loading(
@@ -1852,6 +1891,7 @@ fn handle_plugin_successful_loading(
let _ = senders.send_to_background_jobs(BackgroundJob::StopPluginLoadingAnimation(plugin_id));
let _ = senders.send_to_screen(ScreenInstruction::RequestStateUpdateForPlugins);
let _ = senders.send_to_background_jobs(BackgroundJob::ReportPluginList(plugin_list));
+ let _ = senders.send_to_plugin(PluginInstruction::RequestStateUpdateForPlugin(plugin_id));
}
fn handle_plugin_loading_failure(
@@ -1902,6 +1942,7 @@ fn check_event_permission(
| Event::FailedToWriteConfigToDisk(..)
| Event::CommandPaneReRun(..)
| Event::CwdChanged(..)
+ | Event::AvailableLayoutInfo(..)
| Event::InputReceived => PermissionType::ReadApplicationState,
Event::WebServerStatus(..) => PermissionType::StartWebServer,
Event::PaneRenderReport(..) => PermissionType::ReadPaneContents,
diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs
index 035c278b..82e4dabf 100644
--- a/zellij-server/src/plugins/zellij_exports.rs
+++ b/zellij-server/src/plugins/zellij_exports.rs
@@ -20,12 +20,16 @@ use std::{
};
use wasmi::{Caller, Linker};
use zellij_utils::data::{
- CommandType, ConnectToSession, Event, FloatingPaneCoordinates, GetPanePidResponse, HttpVerb,
- KeyWithModifier, LayoutInfo, MessageToPlugin, NewPanePlacement, OriginatingPlugin,
- PaneScrollbackResponse, PermissionStatus, PermissionType, PluginPermission,
+ CommandType, ConnectToSession, DeleteLayoutResponse, EditLayoutResponse, Event,
+ FloatingPaneCoordinates, GetFocusedPaneInfoResponse, GetPanePidResponse, HttpVerb,
+ KeyWithModifier, LayoutInfo, LayoutMetadata, LayoutParsingError, MessageToPlugin,
+ NewPanePlacement, OriginatingPlugin, PaneScrollbackResponse, PermissionStatus, PermissionType,
+ PluginPermission, RenameLayoutResponse, SaveLayoutResponse, TabMetadata,
};
+use zellij_utils::home::default_layout_dir;
use zellij_utils::input::permission::PermissionCache;
use zellij_utils::ipc::{ClientToServerMsg, IpcSenderWithContext};
+use zellij_utils::sessions::generate_random_name as generate_random_name_impl;
#[cfg(feature = "web_server_capability")]
use zellij_utils::web_authentication_tokens::{
create_token, list_tokens, rename_token, revoke_all_tokens, revoke_token,
@@ -34,6 +38,7 @@ use zellij_utils::web_authentication_tokens::{
use zellij_utils::web_server_commands::shutdown_all_webserver_instances;
use crate::{panes::PaneId, screen::ScreenInstruction};
+use kdl::KdlDocument;
use prost::Message;
use zellij_utils::{
@@ -46,11 +51,22 @@ use zellij_utils::{
input::{
actions::Action,
command::{OpenFilePayload, RunCommand, RunCommandAction, TerminalAction},
- layout::{Layout, RunPluginOrAlias},
+ config::ConfigError,
+ layout::{Layout, RunPluginOrAlias, TabLayoutInfo},
},
plugin_api::{
- event::ProtobufPaneScrollbackResponse,
- plugin_command::{ProtobufGetPanePidResponse, ProtobufPluginCommand},
+ event::{
+ layout_parsing_error::ErrorType as ProtobufLayoutParsingErrorType,
+ ProtobufLayoutParsingError, ProtobufPaneScrollbackResponse, ProtobufSyntaxError,
+ },
+ plugin_command::{
+ dump_layout_response, dump_session_layout_response, parse_layout_response,
+ ProtobufDeleteLayoutResponse, ProtobufDumpLayoutResponse,
+ ProtobufDumpSessionLayoutResponse, ProtobufEditLayoutResponse,
+ ProtobufGenerateRandomNameResponse, ProtobufGetFocusedPaneInfoResponse,
+ ProtobufGetLayoutDirResponse, ProtobufGetPanePidResponse, ProtobufParseLayoutResponse,
+ ProtobufPluginCommand, ProtobufRenameLayoutResponse, ProtobufSaveLayoutResponse,
+ },
plugin_ids::{ProtobufPluginIds, ProtobufZellijVersion},
},
};
@@ -107,6 +123,11 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) {
PluginCommand::ShowCursor(cursor_position) => show_cursor(env, cursor_position),
PluginCommand::GetPluginIds => get_plugin_ids(env),
PluginCommand::GetZellijVersion => get_zellij_version(env),
+ PluginCommand::GenerateRandomName => generate_random_name(env),
+ PluginCommand::DumpLayout(layout_name) => dump_layout(env, layout_name),
+ PluginCommand::ParseLayout(layout_string) => parse_layout(env, layout_string),
+ PluginCommand::GetLayoutDir => get_layout_dir(env),
+ PluginCommand::GetFocusedPaneInfo => get_focused_pane_info(env),
PluginCommand::OpenFile(file_to_open, context) => {
open_file(env, file_to_open, context)
},
@@ -184,6 +205,34 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) {
PluginCommand::NewTabsWithLayoutInfo(layout_info) => {
new_tabs_with_layout_info(env, layout_info)?
},
+ PluginCommand::OverrideLayout(
+ layout_info,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ apply_only_to_active_tab,
+ context,
+ ) => override_layout(
+ env,
+ layout_info,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ apply_only_to_active_tab,
+ context,
+ )?,
+ PluginCommand::SaveLayout {
+ layout_name,
+ layout_kdl,
+ overwrite,
+ } => save_layout(env, layout_name, layout_kdl, overwrite),
+ PluginCommand::DeleteLayout { layout_name } => delete_layout(env, layout_name),
+ PluginCommand::RenameLayout {
+ old_layout_name,
+ new_layout_name,
+ } => rename_layout(env, old_layout_name, new_layout_name),
+ PluginCommand::EditLayout {
+ layout_name,
+ context,
+ } => edit_layout(env, layout_name, context),
PluginCommand::NewTab { name, cwd } => new_tab(env, name, cwd),
PluginCommand::GoToNextTab => go_to_next_tab(env),
PluginCommand::GoToPreviousTab => go_to_previous_tab(env),
@@ -325,7 +374,9 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) {
scan_host_folder(env, folder_to_scan)
},
PluginCommand::WatchFilesystem => watch_filesystem(env),
- PluginCommand::DumpSessionLayout => dump_session_layout(env),
+ PluginCommand::DumpSessionLayout { tab_index } => {
+ dump_session_layout(env, tab_index)
+ },
PluginCommand::CloseSelf => close_self(env),
PluginCommand::Reconfigure(new_config, write_config_to_disk) => {
reconfigure(env, new_config, write_config_to_disk)?
@@ -715,6 +766,218 @@ fn get_zellij_version(env: &PluginEnv) {
.non_fatal();
}
+fn generate_random_name(env: &PluginEnv) {
+ let name = generate_random_name_impl();
+ let response = ProtobufGenerateRandomNameResponse { name };
+ wasi_write_object(env, &response.encode_to_vec())
+ .with_context(|| {
+ format!(
+ "failed to send generated random name to plugin {}",
+ env.name()
+ )
+ })
+ .non_fatal();
+}
+
+fn dump_layout(env: &PluginEnv, layout_name: String) {
+ let layout_path = PathBuf::from(&layout_name);
+
+ let layout_dir = env.layout_dir.clone().or_else(default_layout_dir);
+ let response = match Layout::stringified_from_dir(&layout_path, layout_dir.as_ref()) {
+ Ok((_, layout_content, _)) => ProtobufDumpLayoutResponse {
+ result: Some(dump_layout_response::Result::LayoutContent(layout_content)),
+ },
+ Err(e) => ProtobufDumpLayoutResponse {
+ result: Some(dump_layout_response::Result::Error(e.to_string())),
+ },
+ };
+
+ wasi_write_object(env, &response.encode_to_vec())
+ .with_context(|| format!("failed to send layout dump to plugin {}", env.name()))
+ .non_fatal();
+}
+
+fn get_layout_dir(env: &PluginEnv) {
+ // Get layout dir from env or fall back to default
+ let layout_dir = env.layout_dir.clone().or_else(default_layout_dir);
+
+ // Convert PathBuf to String
+ let layout_dir_string = layout_dir
+ .and_then(|path| path.to_str().map(|s| s.to_string()))
+ .unwrap_or_else(|| String::from(""));
+
+ let response = ProtobufGetLayoutDirResponse {
+ layout_dir: layout_dir_string,
+ };
+
+ wasi_write_object(env, &response.encode_to_vec())
+ .with_context(|| format!("failed to send layout dir to plugin {}", env.name()))
+ .non_fatal();
+}
+
+fn get_focused_pane_info(env: &PluginEnv) {
+ use crossbeam::channel::RecvTimeoutError;
+ use std::time::Duration;
+
+ let err_context = || {
+ format!(
+ "failed to get focused pane info for client {:?} from plugin {}",
+ env.client_id,
+ env.name()
+ )
+ };
+
+ // Create oneshot channel for response
+ let (response_sender, response_receiver) = crossbeam::channel::bounded(1);
+
+ // Send request to screen thread
+ env.senders
+ .send_to_screen(ScreenInstruction::GetFocusedPaneInfo {
+ client_id: env.client_id,
+ response_channel: response_sender,
+ })
+ .with_context(err_context)
+ .non_fatal();
+
+ // Block waiting for response with 100ms timeout
+ let response = match response_receiver.recv_timeout(Duration::from_millis(100)) {
+ Ok(response) => response,
+ Err(RecvTimeoutError::Timeout) => {
+ log::error!(
+ "GetFocusedPaneInfo timed out for plugin {} for client {:?}",
+ env.plugin_id,
+ env.client_id
+ );
+ GetFocusedPaneInfoResponse::Err("Timeout retrieving focused pane info".to_string())
+ },
+ Err(RecvTimeoutError::Disconnected) => {
+ log::error!(
+ "GetFocusedPaneInfo channel disconnected for plugin {}",
+ env.plugin_id
+ );
+ GetFocusedPaneInfoResponse::Err(
+ "Channel disconnected while retrieving focused pane info".to_string(),
+ )
+ },
+ };
+
+ // Convert to protobuf and write response back to plugin
+ let protobuf_response = ProtobufGetFocusedPaneInfoResponse::from(response);
+ wasi_write_object(env, &protobuf_response.encode_to_vec())
+ .with_context(err_context)
+ .non_fatal();
+}
+
+fn parse_layout(env: &PluginEnv, layout_string: String) {
+ use std::time::{SystemTime, UNIX_EPOCH};
+
+ // Parse the KDL layout - same parameters as list_available_layouts in layout.rs:1260
+ let parse_result = Layout::from_kdl(
+ &layout_string,
+ Some("parse_layout_api".to_string()), // file_name for error reporting
+ None, // no swap layouts
+ None, // no cwd
+ );
+
+ let response = match parse_result {
+ Ok(layout) => {
+ // Extract tabs from layout
+ // This logic matches LayoutMetadata::from in data.rs:1762-1772
+ let layout_tabs = layout.tabs();
+ let tabs = if layout_tabs.is_empty() {
+ // Use default template if no explicit tabs defined
+ let (tiled_pane_layout, floating_pane_layout) = layout.new_tab();
+ vec![TabMetadata::from(&(
+ None,
+ tiled_pane_layout,
+ floating_pane_layout,
+ ))]
+ } else {
+ layout
+ .tabs()
+ .into_iter()
+ .map(|tab| TabMetadata::from(&tab))
+ .collect()
+ };
+
+ // Get current time as Unix epoch timestamp
+ let current_time = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap_or_default()
+ .as_secs()
+ .to_string();
+
+ let layout_metadata = LayoutMetadata {
+ tabs,
+ creation_time: current_time.clone(),
+ update_time: current_time,
+ };
+
+ // Convert LayoutMetadata to protobuf
+ match layout_metadata.try_into() {
+ Ok(protobuf_metadata) => ProtobufParseLayoutResponse {
+ result: Some(parse_layout_response::Result::Metadata(protobuf_metadata)),
+ },
+ Err(e) => {
+ log::error!("Failed to convert metadata to protobuf: {}", e);
+ // Fallback to SyntaxError if conversion fails
+ let error = LayoutParsingError::SyntaxError;
+ let protobuf_error =
+ error
+ .try_into()
+ .unwrap_or_else(|_| ProtobufLayoutParsingError {
+ error_type: Some(ProtobufLayoutParsingErrorType::SyntaxError(
+ ProtobufSyntaxError {},
+ )),
+ });
+ ProtobufParseLayoutResponse {
+ result: Some(parse_layout_response::Result::Error(protobuf_error)),
+ }
+ },
+ }
+ },
+ Err(config_error) => {
+ // Build LayoutParsingError following list_available_layouts pattern
+ // See zellij-utils/src/input/layout.rs:1238-1245
+ let file_name = "parse_layout_api".to_string();
+ let source_code = layout_string;
+
+ let error = match config_error {
+ ConfigError::KdlError(kdl_err) => LayoutParsingError::KdlError {
+ kdl_error: kdl_err,
+ file_name,
+ source_code,
+ },
+ _ => LayoutParsingError::SyntaxError,
+ };
+
+ // Convert LayoutParsingError to protobuf
+ // TryFrom is implemented in zellij-utils/src/plugin_api/event.rs:1293-1314
+ let protobuf_error = error.try_into().unwrap_or_else(|e| {
+ log::error!("Failed to convert error to protobuf: {}", e);
+ ProtobufLayoutParsingError {
+ error_type: Some(ProtobufLayoutParsingErrorType::SyntaxError(
+ ProtobufSyntaxError {},
+ )),
+ }
+ });
+
+ ProtobufParseLayoutResponse {
+ result: Some(parse_layout_response::Result::Error(protobuf_error)),
+ }
+ },
+ };
+
+ wasi_write_object(env, &response.encode_to_vec())
+ .with_context(|| {
+ format!(
+ "failed to send parse layout response to plugin {}",
+ env.name()
+ )
+ })
+ .non_fatal();
+}
+
fn open_file(env: &PluginEnv, file_to_open: FileToOpen, context: BTreeMap<String, String>) {
let error_msg = || format!("failed to open file in plugin {}", env.name());
let floating = false;
@@ -2195,12 +2458,79 @@ fn watch_filesystem(env: &PluginEnv) {
.map(|sender| sender.send(PluginInstruction::WatchFilesystem));
}
-fn dump_session_layout(env: &PluginEnv) {
- let _ = env
+// note this removes the requesting plugin
+fn dump_session_layout(env: &PluginEnv, tab_index: Option<usize>) {
+ use crate::plugins::DumpSessionLayoutResponse;
+
+ // Create oneshot channel for response
+ let (response_sender, response_receiver) = crossbeam::channel::bounded(1);
+
+ // Send request to screen thread
+ let send_result = env
.senders
- .to_screen
- .as_ref()
- .map(|sender| sender.send(ScreenInstruction::DumpLayoutToPlugin(env.plugin_id)));
+ .send_to_screen(ScreenInstruction::DumpLayoutToPlugin {
+ plugin_id: env.plugin_id,
+ tab_index,
+ response_channel: response_sender,
+ });
+
+ if let Err(e) = send_result {
+ let error_response = ProtobufDumpSessionLayoutResponse {
+ result: Some(dump_session_layout_response::Result::Error(format!(
+ "Failed to send dump layout request: {}",
+ e
+ ))),
+ metadata: None,
+ };
+ let _ = wasi_write_object(env, &error_response.encode_to_vec());
+ return;
+ }
+
+ // Wait for response with 1 second timeout
+ let response: DumpSessionLayoutResponse =
+ match response_receiver.recv_timeout(Duration::from_secs(1)) {
+ Ok(resp) => resp,
+ Err(e) => {
+ let error_response = ProtobufDumpSessionLayoutResponse {
+ result: Some(dump_session_layout_response::Result::Error(format!(
+ "Timeout waiting for session layout: {}",
+ e
+ ))),
+ metadata: None,
+ };
+ let _ = wasi_write_object(env, &error_response.encode_to_vec());
+ return;
+ },
+ };
+
+ // Convert LayoutMetadata to protobuf
+ let protobuf_metadata = response
+ .metadata
+ .and_then(|metadata| match metadata.try_into() {
+ Ok(pb) => Some(pb),
+ Err(e) => {
+ log::error!("Failed to convert LayoutMetadata to protobuf: {}", e);
+ None
+ },
+ });
+
+ // Build protobuf response
+ let protobuf_response = match response.layout_result {
+ Ok(layout_content) => ProtobufDumpSessionLayoutResponse {
+ result: Some(dump_session_layout_response::Result::LayoutContent(
+ layout_content,
+ )),
+ metadata: protobuf_metadata,
+ },
+ Err(error) => ProtobufDumpSessionLayoutResponse {
+ result: Some(dump_session_layout_response::Result::Error(error)),
+ metadata: None,
+ },
+ };
+
+ // Write response back to plugin via WASI pipe
+ let _ = wasi_write_object(env, &protobuf_response.encode_to_vec())
+ .with_context(|| format!("failed to send session layout to plugin {}", env.name()));
}
fn list_clients(env: &PluginEnv) {
@@ -2493,6 +2823,298 @@ fn get_pane_pid(env: &PluginEnv, pane_id: PaneId) {
.non_fatal();
}
+fn save_layout(env: &PluginEnv, layout_name: String, layout_kdl: String, overwrite: bool) {
+ let err_context = || format!("Failed to save layout '{}'", layout_name);
+
+ let response = try_save_layout(env, &layout_name, &layout_kdl, overwrite)
+ .map(|_| SaveLayoutResponse::Ok(()))
+ .unwrap_or_else(SaveLayoutResponse::Err);
+
+ // Serialize and send response back to plugin
+ let protobuf_response = ProtobufSaveLayoutResponse::from(response);
+ let serialized = protobuf_response.encode_to_vec();
+ wasi_write_object(env, &serialized)
+ .with_context(err_context)
+ .non_fatal();
+}
+
+fn try_save_layout(
+ env: &PluginEnv,
+ layout_name: &str,
+ layout_kdl: &str,
+ overwrite: bool,
+) -> Result<(), String> {
+ // Step 1: Sanitize layout name for path traversal and invalid characters
+ let safe_name = sanitize_layout_name(layout_name)?;
+
+ // Step 2: Validate the layout by parsing it
+ Layout::from_kdl(
+ layout_kdl,
+ Some(format!("{}.kdl", safe_name)),
+ None, // No swap layouts
+ None, // No cwd override
+ )
+ .map_err(|config_error| format!("Invalid layout KDL: {:?}", config_error))?;
+
+ // Step 3: Get layout_dir from PluginEnv
+ let layout_dir = env
+ .layout_dir
+ .clone()
+ .or_else(default_layout_dir)
+ .ok_or_else(|| "Layout directory not found".to_string())?;
+
+ // Step 4: Create file path
+ let file_path = layout_dir.join(format!("{}.kdl", safe_name));
+
+ // Step 5: Check if file exists when overwrite=false
+ if file_path.exists() && !overwrite {
+ return Err(format!(
+ "Layout file '{}' already exists. Use overwrite flag to replace it.",
+ safe_name
+ ));
+ }
+
+ // Step 6: Ensure layout directory exists
+ std::fs::create_dir_all(layout_dir)
+ .map_err(|e| format!("Failed to create layout directory: {}", e))?;
+
+ // Step 7: Write to disk
+ let mut parsed_layout: KdlDocument = layout_kdl.parse().unwrap(); // unwrap
+ // should
+ // be
+ // safe,
+ // but
+ // let's
+ // do
+ // it
+ // nicer
+ parsed_layout.fmt();
+ std::fs::write(&file_path, &parsed_layout.to_string())
+ .map_err(|io_error| format!("Failed to write layout file: {}", io_error))?;
+
+ Ok(())
+}
+
+fn delete_layout(env: &PluginEnv, layout_name: String) {
+ let err_context = || format!("Failed to delete layout '{}'", layout_name);
+
+ let response = try_delete_layout(env, &layout_name)
+ .map(|_| DeleteLayoutResponse::Ok(()))
+ .unwrap_or_else(|e| DeleteLayoutResponse::Err(e));
+
+ let protobuf_response: ProtobufDeleteLayoutResponse = response.into();
+ let serialized = protobuf_response.encode_to_vec();
+ wasi_write_object(env, &serialized)
+ .with_context(err_context)
+ .non_fatal();
+}
+
+fn rename_layout(env: &PluginEnv, old_layout_name: String, new_layout_name: String) {
+ let err_context = || {
+ format!(
+ "Failed to rename layout '{}' to '{}'",
+ old_layout_name, new_layout_name
+ )
+ };
+
+ let response = try_rename_layout(env, &old_layout_name, &new_layout_name)
+ .map(|_| RenameLayoutResponse::Ok(()))
+ .unwrap_or_else(|e| RenameLayoutResponse::Err(e));
+
+ let protobuf_response: ProtobufRenameLayoutResponse = response.into();
+ let serialized = protobuf_response.encode_to_vec();
+ wasi_write_object(env, &serialized)
+ .with_context(err_context)
+ .non_fatal();
+}
+
+fn try_delete_layout(env: &PluginEnv, layout_name: &str) -> Result<(), String> {
+ // Sanitize the layout name to prevent directory traversal
+ let safe_name =
+ sanitize_layout_name(layout_name).map_err(|e| format!("Invalid layout name: {}", e))?;
+
+ // Get the layout directory from PluginEnv
+ let layout_dir = env
+ .layout_dir
+ .clone()
+ .or_else(default_layout_dir)
+ .ok_or_else(|| "Layout directory not found".to_string())?;
+
+ // Construct the full file path
+ let file_path = layout_dir.join(format!("{}.kdl", safe_name));
+
+ // Check if the file exists
+ if !file_path.exists() {
+ return Err(format!("Layout '{}' not found", safe_name));
+ }
+
+ // Delete the file
+ std::fs::remove_file(&file_path).map_err(|e| format!("Failed to delete layout file: {}", e))?;
+
+ Ok(())
+}
+
+fn try_rename_layout(
+ env: &PluginEnv,
+ old_layout_name: &str,
+ new_layout_name: &str,
+) -> Result<(), String> {
+ // Step 1: Sanitize both layout names
+ let safe_old_name = sanitize_layout_name(old_layout_name)
+ .map_err(|e| format!("Invalid old layout name: {}", e))?;
+
+ let safe_new_name = sanitize_layout_name(new_layout_name)
+ .map_err(|e| format!("Invalid new layout name: {}", e))?;
+
+ // Step 2: Get layout directory from PluginEnv
+ let layout_dir = env
+ .layout_dir
+ .clone()
+ .or_else(default_layout_dir)
+ .ok_or_else(|| "Layout directory not found".to_string())?;
+
+ // Step 3: Construct file paths
+ let old_file_path = layout_dir.join(format!("{}.kdl", safe_old_name));
+ let new_file_path = layout_dir.join(format!("{}.kdl", safe_new_name));
+
+ // Step 4: Check if source file exists
+ if !old_file_path.exists() {
+ return Err(format!("Layout '{}' not found", safe_old_name));
+ }
+
+ // Step 5: Check if target file already exists (fail if it does - no overwrite)
+ if new_file_path.exists() {
+ return Err(format!(
+ "Layout '{}' already exists. Cannot rename '{}' to existing layout name.",
+ safe_new_name, safe_old_name
+ ));
+ }
+
+ // Step 6: Rename the file
+ std::fs::rename(&old_file_path, &new_file_path)
+ .map_err(|e| format!("Failed to rename layout file: {}", e))?;
+
+ Ok(())
+}
+
+fn edit_layout(env: &PluginEnv, layout_name: String, context: BTreeMap<String, String>) {
+ let err_context = || format!("Failed to edit layout '{}'", layout_name);
+
+ let response = try_edit_layout(env, &layout_name, context)
+ .map(|_| EditLayoutResponse::Ok(()))
+ .unwrap_or_else(|e| EditLayoutResponse::Err(e));
+
+ let protobuf_response: ProtobufEditLayoutResponse = response.into();
+ let serialized = protobuf_response.encode_to_vec();
+ wasi_write_object(env, &serialized)
+ .with_context(err_context)
+ .non_fatal();
+}
+
+fn try_edit_layout(
+ env: &PluginEnv,
+ layout_name: &str,
+ context: BTreeMap<String, String>,
+) -> Result<(), String> {
+ // Sanitize the layout name to prevent directory traversal
+ let safe_name =
+ sanitize_layout_name(layout_name).map_err(|e| format!("Invalid layout name: {}", e))?;
+
+ // Get the layout directory from PluginEnv
+ let layout_dir = env
+ .layout_dir
+ .clone()
+ .or_else(default_layout_dir)
+ .ok_or_else(|| "Layout directory not found".to_string())?;
+
+ // Construct the full file path
+ let file_path = layout_dir.join(format!("{}.kdl", safe_name));
+
+ // Create FileToOpen for the layout file
+ let file_to_open = FileToOpen {
+ path: file_path,
+ line_number: None,
+ cwd: Some(layout_dir.clone()),
+ };
+
+ // Create an Action::EditFile
+ let action = Action::EditFile {
+ payload: OpenFilePayload::new(
+ file_to_open.path,
+ file_to_open.line_number,
+ file_to_open.cwd,
+ )
+ .with_originating_plugin(OriginatingPlugin::new(
+ env.plugin_id,
+ env.client_id,
+ context,
+ )),
+ direction: None,
+ floating: false,
+ in_place: true,
+ start_suppressed: false,
+ coordinates: None,
+ near_current_pane: true,
+ };
+
+ // Route the action - this is fallible
+ route_action(
+ action,
+ env.client_id,
+ None,
+ Some(PaneId::Plugin(env.plugin_id)),
+ env.senders.clone(),
+ env.capabilities.clone(),
+ env.client_attributes.clone(),
+ env.default_shell.clone(),
+ env.default_layout.clone(),
+ None,
+ env.keybinds.clone(),
+ env.default_mode.clone(),
+ None,
+ )
+ .map(|_| ())
+ .map_err(|e| format!("Failed to route edit action: {:?}", e))
+}
+
+/// Sanitize layout name to prevent path traversal and invalid filenames
+/// Returns the sanitized name or an error message
+fn sanitize_layout_name(name: &str) -> Result<String, String> {
+ // Check for empty name
+ if name.is_empty() {
+ return Err("Layout name cannot be empty".to_string());
+ }
+
+ // Check for path separators (prevent directory traversal)
+ if name.contains('/') || name.contains('\\') {
+ return Err("Layout name cannot contain path separators".to_string());
+ }
+
+ // Check for parent directory references
+ if name.contains("..") {
+ return Err("Layout name cannot contain '..'".to_string());
+ }
+
+ // Check length (most filesystems have 255 char limit, leave room for .kdl)
+ if name.len() > 250 {
+ return Err("Layout name too long (max 250 characters)".to_string());
+ }
+
+ // Only allow alphanumeric, hyphens, underscores, and spaces
+ let valid_chars = name
+ .chars()
+ .all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == ' ');
+
+ if !valid_chars {
+ return Err(
+ "Layout name can only contain letters, numbers, hyphens, underscores, and spaces"
+ .to_string(),
+ );
+ }
+
+ Ok(name.to_string())
+}
+
fn move_pane_with_pane_id(env: &PluginEnv, pane_id: PaneId) {
let _ = env
.senders
@@ -2935,6 +3557,57 @@ fn replace_pane_with_existing_pane(
));
}
+fn override_layout(
+ env: &mut PluginEnv,
+ layout_info: LayoutInfo,
+ retain_existing_terminal_panes: bool,
+ retain_existing_plugin_panes: bool,
+ apply_only_to_active_tab: bool,
+ context: BTreeMap<String, String>,
+) -> Result<()> {
+ let layout = Layout::from_layout_info(&env.layout_dir, layout_info)
+ .map_err(|e| anyhow!("Failed to parse layout: {:?}", e))?;
+
+ // Convert all tabs to Vec<TabLayoutInfo>
+ let tabs: Vec<TabLayoutInfo> = layout
+ .tabs
+ .iter()
+ .enumerate()
+ .map(|(index, (tab_name, tiled, floating))| TabLayoutInfo {
+ tab_index: index,
+ tab_name: tab_name.clone(),
+ tiled_layout: tiled.clone(),
+ floating_layouts: floating.clone(),
+ swap_tiled_layouts: Some(layout.swap_tiled_layouts.clone()),
+ swap_floating_layouts: Some(layout.swap_floating_layouts.clone()),
+ })
+ .collect();
+
+ // If no tabs, create default tab
+ let tabs = if tabs.is_empty() {
+ let (tiled, floating) = layout.new_tab();
+ vec![TabLayoutInfo {
+ tab_index: 0,
+ tab_name: None,
+ tiled_layout: tiled,
+ floating_layouts: floating,
+ swap_tiled_layouts: Some(layout.swap_tiled_layouts),
+ swap_floating_layouts: Some(layout.swap_floating_layouts),
+ }]
+ } else {
+ tabs
+ };
+
+ let action = Action::OverrideLayout {
+ tabs,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ apply_only_to_active_tab,
+ };
+ run_action(env, action, context);
+ Ok(())
+}
+
// Custom panic handler for plugins.
//
// This is called when a panic occurs in a plugin. Since most panics will likely originate in the
@@ -3105,14 +3778,24 @@ fn check_command_permission(
| PluginCommand::ReplacePaneWithExistingPane(..)
| PluginCommand::KillSessions(..)
| PluginCommand::SendSigintToPaneId(..)
- | PluginCommand::SendSigkillToPaneId(..) => PermissionType::ChangeApplicationState,
+ | PluginCommand::SendSigkillToPaneId(..)
+ | PluginCommand::OverrideLayout(..)
+ | PluginCommand::SaveLayout { .. }
+ | PluginCommand::DeleteLayout { .. }
+ | PluginCommand::RenameLayout { .. }
+ | PluginCommand::EditLayout { .. } => PermissionType::ChangeApplicationState,
PluginCommand::UnblockCliPipeInput(..)
| PluginCommand::BlockCliPipeInput(..)
| PluginCommand::CliPipeOutput(..) => PermissionType::ReadCliPipes,
PluginCommand::MessageToPlugin(..) => PermissionType::MessageAndLaunchOtherPlugins,
PluginCommand::ListClients
- | PluginCommand::DumpSessionLayout
- | PluginCommand::GetPanePid { .. } => PermissionType::ReadApplicationState,
+ | PluginCommand::DumpSessionLayout { .. }
+ | PluginCommand::GetPanePid { .. }
+ | PluginCommand::GenerateRandomName
+ | PluginCommand::GetLayoutDir
+ | PluginCommand::GetFocusedPaneInfo
+ | PluginCommand::DumpLayout(..)
+ | PluginCommand::ParseLayout(..) => PermissionType::ReadApplicationState,
PluginCommand::RebindKeys { .. } | PluginCommand::Reconfigure(..) => {
PermissionType::Reconfigure
},
diff --git a/zellij-server/src/pty.rs b/zellij-server/src/pty.rs
index 544830af..82a24b55 100644
--- a/zellij-server/src/pty.rs
+++ b/zellij-server/src/pty.rs
@@ -3,8 +3,8 @@ use crate::route::NotificationEnd;
use crate::terminal_bytes::TerminalBytes;
use crate::{
panes::PaneId,
- plugins::{PluginId, PluginInstruction},
- screen::ScreenInstruction,
+ plugins::{DumpSessionLayoutResponse, PluginId, PluginInstruction},
+ screen::{ScreenInstruction, TabOverrideResult},
session_layout_metadata::SessionLayoutMetadata,
thread_bus::{Bus, ThreadSenders},
ClientId, ServerInstruction,
@@ -27,7 +27,7 @@ use zellij_utils::{
command::{OpenFilePayload, RunCommand, TerminalAction},
layout::{
FloatingPaneLayout, Layout, Run, RunPluginOrAlias, SwapFloatingLayout, SwapTiledLayout,
- TiledPaneLayout,
+ TabLayoutInfo, TiledPaneLayout,
},
},
pane_size::Size,
@@ -79,16 +79,11 @@ pub enum PtyInstruction {
Option<NotificationEnd>, // completion signal
), // the String is the tab name
OverrideLayout(
- Option<PathBuf>, // CWD
- Option<TerminalAction>, // Default Shell
- TiledPaneLayout,
- Vec<FloatingPaneLayout>,
- Option<Vec<SwapTiledLayout>>,
- Option<Vec<SwapFloatingLayout>>,
- bool, // retain_existing_terminal_panes
- bool, // retain_existing_plugin_panes
- usize, // tab_index
- HashMap<RunPluginOrAlias, Vec<u32>>, // plugin_ids
+ Option<PathBuf>, // CWD
+ Option<TerminalAction>, // Default Shell
+ Vec<(TabLayoutInfo, HashMap<RunPluginOrAlias, Vec<u32>>)>, // (layout, plugin_ids) per tab
+ bool, // retain_existing_terminal_panes
+ bool, // retain_existing_plugin_panes
ClientId,
Option<NotificationEnd>,
),
@@ -109,7 +104,11 @@ pub enum PtyInstruction {
Option<NotificationEnd>, // completion signal
), // String is an optional pane name
DumpLayout(SessionLayoutMetadata, ClientId, Option<NotificationEnd>),
- DumpLayoutToPlugin(SessionLayoutMetadata, PluginId),
+ DumpLayoutToPlugin {
+ session_layout_metadata: SessionLayoutMetadata,
+ plugin_id: PluginId,
+ response_channel: crossbeam::channel::Sender<DumpSessionLayoutResponse>,
+ },
LogLayoutToHd(SessionLayoutMetadata),
FillPluginCwd(
Option<bool>, // should float
@@ -159,7 +158,7 @@ impl From<&PtyInstruction> for PtyContext {
PtyInstruction::DropToShellInPane { .. } => PtyContext::DropToShellInPane,
PtyInstruction::SpawnInPlaceTerminal(..) => PtyContext::SpawnInPlaceTerminal,
PtyInstruction::DumpLayout(..) => PtyContext::DumpLayout,
- PtyInstruction::DumpLayoutToPlugin(..) => PtyContext::DumpLayoutToPlugin,
+ PtyInstruction::DumpLayoutToPlugin { .. } => PtyContext::DumpLayoutToPlugin,
PtyInstruction::LogLayoutToHd(..) => PtyContext::LogLayoutToHd,
PtyInstruction::FillPluginCwd(..) => PtyContext::FillPluginCwd,
PtyInstruction::ListClientsMetadata(..) => PtyContext::ListClientsMetadata,
@@ -496,34 +495,55 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) -> Result<()> {
PtyInstruction::OverrideLayout(
cwd,
default_shell,
- tiled_layout,
- floating_panes_layout,
- swap_tiled_layouts,
- swap_floating_layouts,
+ tab_layouts_with_plugin_ids,
retain_existing_terminal_panes,
retain_existing_plugin_panes,
- tab_index,
- plugin_ids,
client_id,
completion_tx,
) => {
let err_context = || "failed to override layout";
- pty.spawn_terminals_for_layout_override(
- cwd,
- tiled_layout,
- floating_panes_layout,
- default_shell,
- plugin_ids,
- tab_index,
- client_id,
- swap_tiled_layouts,
- swap_floating_layouts,
- retain_existing_terminal_panes,
- retain_existing_plugin_panes,
- completion_tx,
- )
- .with_context(err_context)?;
+ let mut all_tab_results = Vec::new();
+
+ // Process each tab
+ for (tab_layout_info, plugin_ids) in tab_layouts_with_plugin_ids {
+ match pty.spawn_terminals_for_layout_override(
+ cwd.clone(),
+ tab_layout_info.tiled_layout,
+ tab_layout_info.floating_layouts,
+ default_shell.clone(),
+ plugin_ids,
+ tab_layout_info.tab_index,
+ tab_layout_info.tab_name,
+ client_id,
+ tab_layout_info.swap_tiled_layouts,
+ tab_layout_info.swap_floating_layouts,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ ) {
+ Ok(tab_result) => all_tab_results.push(tab_result),
+ Err(e) => {
+ log::error!(
+ "Failed to spawn terminals for tab {}: {:?}",
+ tab_layout_info.tab_index,
+ e
+ );
+ // Continue with other tabs (best-effort approach)
+ },
+ }
+ }
+
+ // Send all results back to screen in one message
+ pty.bus
+ .senders
+ .send_to_screen(ScreenInstruction::OverrideLayoutComplete(
+ all_tab_results,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ client_id,
+ completion_tx,
+ ))
+ .with_context(err_context)?;
},
PtyInstruction::ClosePane(id, _completion_tx) => {
pty.close_pane(id)
@@ -682,15 +702,20 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) -> Result<()> {
.with_context(err_context)
.non_fatal();
},
- PtyInstruction::DumpLayoutToPlugin(mut session_layout_metadata, plugin_id) => {
+ PtyInstruction::DumpLayoutToPlugin {
+ mut session_layout_metadata,
+ plugin_id,
+ response_channel,
+ } => {
let err_context = || format!("Failed to dump layout");
pty.populate_session_layout_metadata(&mut session_layout_metadata);
pty.bus
.senders
- .send_to_plugin(PluginInstruction::DumpLayoutToPlugin(
+ .send_to_plugin(PluginInstruction::DumpLayoutToPlugin {
session_layout_metadata,
plugin_id,
- ))
+ response_channel,
+ })
.with_context(err_context)
.non_fatal();
},
@@ -1258,13 +1283,13 @@ impl Pty {
default_shell: Option<TerminalAction>,
plugin_ids: HashMap<RunPluginOrAlias, Vec<u32>>,
tab_index: usize,
+ tab_name: Option<String>,
client_id: ClientId,
swap_tiled_layouts: Option<Vec<SwapTiledLayout>>,
swap_floating_layouts: Option<Vec<SwapFloatingLayout>>,
- retain_existing_terminal_panes: bool,
- retain_existing_plugin_panes: bool,
- completion_tx: Option<NotificationEnd>,
- ) -> Result<()> {
+ _retain_existing_terminal_panes: bool,
+ _retain_existing_plugin_panes: bool,
+ ) -> Result<TabOverrideResult> {
let err_context = || format!("failed to spawn terminals for layout for");
let mut default_shell =
@@ -1351,23 +1376,17 @@ impl Pty {
})
.collect();
- self.bus
- .senders
- .send_to_screen(ScreenInstruction::OverrideLayoutComplete(
- layout,
- floating_panes_layout,
- swap_tiled_layouts,
- swap_floating_layouts,
- new_tab_pane_ids,
- new_tab_floating_pane_ids,
- plugin_ids,
- retain_existing_terminal_panes,
- retain_existing_plugin_panes,
- tab_index,
- client_id,
- completion_tx,
- ))
- .with_context(err_context)?;
+ let tab_result = TabOverrideResult {
+ tab_index,
+ tab_name,
+ tiled_layout: layout,
+ floating_layouts: floating_panes_layout,
+ swap_tiled_layouts,
+ swap_floating_layouts,
+ new_terminal_pids: new_tab_pane_ids,
+ new_floating_pane_pids: new_tab_floating_pane_ids,
+ plugin_ids,
+ };
let mut terminals_to_start = vec![];
@@ -1429,7 +1448,7 @@ impl Pty {
for (terminal_id, originating_plugin) in originating_plugins_to_inform {
self.inform_originating_plugin_of_open(terminal_id, originating_plugin);
}
- Ok(())
+ Ok(tab_result)
}
fn inform_originating_plugin_of_open(
&mut self,
diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs
index 57b499e1..f1c47dfa 100644
--- a/zellij-server/src/route.rs
+++ b/zellij-server/src/route.rs
@@ -28,7 +28,7 @@ use zellij_utils::{
command::TerminalAction,
get_mode_info,
keybinds::Keybinds,
- layout::{Layout, TiledPaneLayout},
+ layout::Layout,
},
ipc::{
ClientAttributes, ClientToServerMsg, ExitReason, IpcReceiverWithContext, ServerToClientMsg,
@@ -1115,33 +1115,22 @@ pub(crate) fn route_action(
.with_context(err_context)?;
},
Action::OverrideLayout {
- tiled_layout,
- floating_layouts,
- swap_tiled_layouts,
- swap_floating_layouts,
- tab_name,
+ tabs,
retain_existing_terminal_panes,
retain_existing_plugin_panes,
+ apply_only_to_active_tab,
} => {
- // Extract required layout fields, use defaults if None
let cwd = None;
let shell = default_shell.clone();
- let tiled = tiled_layout.unwrap_or_else(|| TiledPaneLayout::default());
- let floating = floating_layouts;
- let swap_tiled = swap_tiled_layouts;
- let swap_floating = swap_floating_layouts;
senders
.send_to_screen(ScreenInstruction::OverrideLayout(
cwd,
shell,
- tab_name,
- tiled,
- floating,
- swap_tiled,
- swap_floating,
+ tabs,
retain_existing_terminal_panes,
retain_existing_plugin_panes,
+ apply_only_to_active_tab,
client_id,
Some(NotificationEnd::new(completion_tx)),
))
diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs
index a93ad27e..cc7bff40 100644
--- a/zellij-server/src/screen.rs
+++ b/zellij-server/src/screen.rs
@@ -12,9 +12,9 @@ use crate::route::NotificationEnd;
use log::{debug, warn};
use zellij_utils::data::{
- CommandOrPlugin, Direction, FloatingPaneCoordinates, KeyWithModifier, NewPanePlacement,
- PaneContents, PaneManifest, PaneScrollbackResponse, PluginPermission, Resize, ResizeStrategy,
- SessionInfo, Styling, WebSharing,
+ CommandOrPlugin, Direction, FloatingPaneCoordinates, GetFocusedPaneInfoResponse,
+ KeyWithModifier, NewPanePlacement, PaneContents, PaneManifest, PaneScrollbackResponse,
+ PluginPermission, Resize, ResizeStrategy, SessionInfo, Styling, WebSharing,
};
use zellij_utils::errors::prelude::*;
use zellij_utils::input::command::RunCommand;
@@ -30,7 +30,7 @@ use zellij_utils::{
input::command::TerminalAction,
input::layout::{
FloatingPaneLayout, Layout, Run, RunPluginOrAlias, SplitSize, SwapFloatingLayout,
- SwapTiledLayout, TiledPaneLayout,
+ SwapTiledLayout, TabLayoutInfo, TiledPaneLayout,
},
position::Position,
};
@@ -47,7 +47,7 @@ use crate::{
output::Output,
panes::sixel::SixelImageStore,
panes::PaneId,
- plugins::{PluginId, PluginInstruction, PluginRenderAsset},
+ plugins::{DumpSessionLayoutResponse, PluginId, PluginInstruction, PluginRenderAsset},
pty::{get_default_shell, ClientTabIndexOrPaneId, PtyInstruction, VteBytes},
tab::{SuppressedPanes, Tab},
thread_bus::Bus,
@@ -142,6 +142,20 @@ macro_rules! active_tab_and_connected_client_id {
type InitialTitle = String;
type HoldForCommand = Option<RunCommand>;
+/// Result of overriding a single tab's layout
+#[derive(Debug, Clone)]
+pub struct TabOverrideResult {
+ pub tab_index: usize,
+ pub tab_name: Option<String>,
+ pub tiled_layout: TiledPaneLayout,
+ pub floating_layouts: Vec<FloatingPaneLayout>,
+ pub swap_tiled_layouts: Option<Vec<SwapTiledLayout>>,
+ pub swap_floating_layouts: Option<Vec<SwapFloatingLayout>>,
+ pub new_terminal_pids: Vec<(u32, HoldForCommand)>,
+ pub new_floating_pane_pids: Vec<(u32, HoldForCommand)>,
+ pub plugin_ids: HashMap<RunPluginOrAlias, Vec<u32>>,
+}
+
/// Instructions that can be sent to the [`Screen`].
#[derive(Debug, Clone)]
pub enum ScreenInstruction {
@@ -192,7 +206,15 @@ pub enum ScreenInstruction {
DumpScreen(String, ClientId, bool, Option<NotificationEnd>),
DumpLayout(Option<PathBuf>, ClientId, Option<NotificationEnd>), // PathBuf is the default configured
// shell
- DumpLayoutToPlugin(PluginId),
+ DumpLayoutToPlugin {
+ plugin_id: PluginId,
+ tab_index: Option<usize>,
+ response_channel: crossbeam::channel::Sender<DumpSessionLayoutResponse>,
+ },
+ GetFocusedPaneInfo {
+ client_id: ClientId,
+ response_channel: crossbeam::channel::Sender<GetFocusedPaneInfoResponse>,
+ },
EditScrollback(ClientId, Option<NotificationEnd>),
GetPaneScrollback {
pane_id: PaneId,
@@ -296,29 +318,19 @@ pub enum ScreenInstruction {
PreviousSwapLayout(ClientId, Option<NotificationEnd>),
NextSwapLayout(ClientId, Option<NotificationEnd>),
OverrideLayout(
- Option<PathBuf>, // cwd
- Option<TerminalAction>, // default_shell
- Option<String>, // new name for tab
- TiledPaneLayout,
- Vec<FloatingPaneLayout>,
- Option<Vec<SwapTiledLayout>>,
- Option<Vec<SwapFloatingLayout>>,
- bool, // retain_existing_terminal_panes
- bool, // retain_existing_plugin_panes
+ Option<PathBuf>, // cwd (applies to all tabs)
+ Option<TerminalAction>, // default_shell (applies to all tabs)
+ Vec<TabLayoutInfo>, // layouts for each tab to override
+ bool, // retain_existing_terminal_panes
+ bool, // retain_existing_plugin_panes
+ bool, // apply_only_to_focused_tab
ClientId,
Option<NotificationEnd>,
),
OverrideLayoutComplete(
- TiledPaneLayout,
- Vec<FloatingPaneLayout>,
- Option<Vec<SwapTiledLayout>>,
- Option<Vec<SwapFloatingLayout>>,
- Vec<(u32, HoldForCommand)>, // new terminal pids
- Vec<(u32, HoldForCommand)>, // new floating pane pids
- HashMap<RunPluginOrAlias, Vec<u32>>,
- bool, // retain_existing_terminal_panes
- bool, // retain_existing_plugin_panes
- usize, // tab_index
+ Vec<TabOverrideResult>, // results for each tab
+ bool, // retain_existing_terminal_panes
+ bool, // retain_existing_plugin_panes
ClientId,
Option<NotificationEnd>,
),
@@ -564,7 +576,8 @@ impl From<&ScreenInstruction> for ScreenContext {
ScreenInstruction::ClearScreen(..) => ScreenContext::ClearScreen,
ScreenInstruction::DumpScreen(..) => ScreenContext::DumpScreen,
ScreenInstruction::DumpLayout(..) => ScreenContext::DumpLayout,
- ScreenInstruction::DumpLayoutToPlugin(..) => ScreenContext::DumpLayoutToPlugin,
+ ScreenInstruction::DumpLayoutToPlugin { .. } => ScreenContext::DumpLayoutToPlugin,
+ ScreenInstruction::GetFocusedPaneInfo { .. } => ScreenContext::GetFocusedPaneInfo,
ScreenInstruction::EditScrollback(..) => ScreenContext::EditScrollback,
ScreenInstruction::GetPaneScrollback { .. } => ScreenContext::GetPaneScrollback,
ScreenInstruction::ScrollUp(..) => ScreenContext::ScrollUp,
@@ -2087,7 +2100,7 @@ impl Screen {
// because this is mostly about HD access - it does however throw off the timing in the
// tests and causes them to flake, which is why we skip it here
#[cfg(not(test))]
- let available_layouts =
+ let (available_layouts, _layout_errors) =
Layout::list_available_layouts(self.layout_dir.clone(), &self.default_layout_name);
#[cfg(test)]
let available_layouts = vec![];
@@ -2135,7 +2148,8 @@ impl Screen {
}
fn dump_layout_to_hd(&mut self) -> Result<()> {
let err_context = || format!("Failed to log and report session state");
- let session_layout_metadata = self.get_layout_metadata(Some(self.default_shell.clone()));
+ let session_layout_metadata =
+ self.get_layout_metadata(Some(self.default_shell.clone()), None);
self.bus
.senders
.send_to_plugin(PluginInstruction::LogLayoutToHd(session_layout_metadata))
@@ -3296,7 +3310,11 @@ impl Screen {
}
Ok(())
}
- fn get_layout_metadata(&self, default_shell: Option<PathBuf>) -> SessionLayoutMetadata {
+ fn get_layout_metadata(
+ &self,
+ default_shell: Option<PathBuf>,
+ tab_index: Option<usize>,
+ ) -> SessionLayoutMetadata {
let mut session_layout_metadata = SessionLayoutMetadata::new(self.default_layout.clone());
if let Some(default_shell) = default_shell {
session_layout_metadata.update_default_shell(default_shell);
@@ -3305,7 +3323,14 @@ impl Screen {
let active_tab_index =
first_client_id.and_then(|client_id| self.active_tab_indices.get(&client_id));
- for (tab_index, tab) in self.tabs.iter() {
+ // Filter tabs based on optional tab_index parameter
+ let tabs_to_process: Vec<_> = self
+ .tabs
+ .iter()
+ .filter(|(idx, _)| tab_index.map_or(true, |target| **idx == target))
+ .collect();
+
+ for (tab_index, tab) in tabs_to_process {
let tab_is_focused = active_tab_index == Some(&tab_index);
let hide_floating_panes = !tab.are_floating_panes_visible();
let mut suppressed_panes = HashMap::new();
@@ -4188,7 +4213,7 @@ pub(crate) fn screen_thread_main(
},
ScreenInstruction::DumpLayout(default_shell, client_id, completion_tx) => {
let err_context = || format!("Failed to dump layout");
- let session_layout_metadata = screen.get_layout_metadata(default_shell);
+ let session_layout_metadata = screen.get_layout_metadata(default_shell, None);
screen
.bus
.senders
@@ -4201,7 +4226,7 @@ pub(crate) fn screen_thread_main(
},
ScreenInstruction::ListClientsMetadata(default_shell, client_id, completion_tx) => {
let err_context = || format!("Failed to dump layout");
- let session_layout_metadata = screen.get_layout_metadata(default_shell);
+ let session_layout_metadata = screen.get_layout_metadata(default_shell, None);
screen
.bus
.senders
@@ -4212,24 +4237,51 @@ pub(crate) fn screen_thread_main(
))
.with_context(err_context)?;
},
- ScreenInstruction::DumpLayoutToPlugin(plugin_id) => {
+ ScreenInstruction::DumpLayoutToPlugin {
+ plugin_id,
+ tab_index,
+ response_channel,
+ } => {
let err_context = || format!("Failed to dump layout");
let session_layout_metadata =
- screen.get_layout_metadata(Some(screen.default_shell.clone()));
+ screen.get_layout_metadata(Some(screen.default_shell.clone()), tab_index);
screen
.bus
.senders
- .send_to_pty(PtyInstruction::DumpLayoutToPlugin(
+ .send_to_pty(PtyInstruction::DumpLayoutToPlugin {
session_layout_metadata,
plugin_id,
- ))
+ response_channel,
+ })
.with_context(err_context)
.non_fatal();
},
+ ScreenInstruction::GetFocusedPaneInfo {
+ client_id,
+ response_channel,
+ } => {
+ let response = match screen.active_tab_indices.get(&client_id) {
+ Some(&focused_tab_index) => match screen.get_active_pane_id(&client_id) {
+ Some(focused_pane_id) => GetFocusedPaneInfoResponse::Ok {
+ tab_index: focused_tab_index,
+ pane_id: focused_pane_id.into(),
+ },
+ None => GetFocusedPaneInfoResponse::Err(format!(
+ "No active pane found for client {:?}",
+ client_id
+ )),
+ },
+ None => GetFocusedPaneInfoResponse::Err(format!(
+ "Client {:?} not found in active_tab_indices",
+ client_id
+ )),
+ };
+ let _ = response_channel.send(response);
+ },
ScreenInstruction::ListClientsToPlugin(plugin_id, client_id) => {
let err_context = || format!("Failed to dump layout");
let session_layout_metadata =
- screen.get_layout_metadata(Some(screen.default_shell.clone()));
+ screen.get_layout_metadata(Some(screen.default_shell.clone()), None);
screen
.bus
.senders
@@ -5193,90 +5245,212 @@ pub(crate) fn screen_thread_main(
ScreenInstruction::OverrideLayout(
cwd,
default_shell,
- mut new_tab_name,
- mut tiled_layout,
- mut floating_layouts,
- swap_tiled_layouts,
- swap_floating_layouts,
+ mut tab_layouts,
retain_existing_terminal_panes,
retain_existing_plugin_panes,
+ apply_only_to_focused_tab,
client_id,
- _completion_tx,
+ completion_tx,
) => {
- let active_tab = match screen.get_active_tab_mut(client_id) {
- Ok(tab) => tab,
- Err(_) => {
- log::error!(
- "OverrideLayout: No active tab for client_id {:?}",
- client_id
+ // 1. Determine which tabs to close (exist but not in layout)
+ let existing_tab_indices: HashSet<usize> = screen.tabs.keys().copied().collect();
+ let layout_tab_indices: HashSet<usize> =
+ tab_layouts.iter().map(|tl| tl.tab_index).collect();
+ let tabs_to_close: Vec<usize> = existing_tab_indices
+ .difference(&layout_tab_indices)
+ .copied()
+ .collect();
+
+ // 2. Process each tab layout
+ let mut processed_tab_layouts = Vec::new();
+
+ if apply_only_to_focused_tab {
+ match screen.get_active_tab_mut(client_id) {
+ Ok(active_tab) => {
+ if tab_layouts.is_empty() {
+ log::error!("No tab layouts found, cannot override.");
+ continue;
+ }
+ let mut tab_layout_info = tab_layouts.remove(0);
+ tab_layout_info.tab_index = active_tab.index;
+ // Set the tab name if provided
+ if let Some(name) = tab_layout_info.tab_name.take() {
+ active_tab.name = name;
+ }
+
+ // Find already-running panes for this tab
+ let (tiled_to_ignore, floating_indices) = find_already_running_panes(
+ &tab_layout_info.tiled_layout,
+ &tab_layout_info.floating_layouts,
+ active_tab,
+ );
+
+ // Mark run instructions to ignore (prevents re-spawning)
+ for run_instruction in tiled_to_ignore {
+ tab_layout_info
+ .tiled_layout
+ .ignore_run_instruction(run_instruction);
+ }
+
+ for idx in floating_indices {
+ tab_layout_info
+ .floating_layouts
+ .get_mut(idx)
+ .map(|f| f.already_running = true);
+ }
+
+ processed_tab_layouts.push(tab_layout_info);
+ },
+ Err(e) => {
+ log::error!("Failed to override layout of active tab: {}", e);
+ },
+ }
+ } else {
+ for mut tab_layout_info in tab_layouts {
+ // Get the tab by index
+ let tab = match screen.tabs.get_mut(&tab_layout_info.tab_index) {
+ Some(t) => t,
+ None => {
+ // no corresponding tab exists, we'll create it
+ processed_tab_layouts.push(tab_layout_info);
+ continue;
+ },
+ };
+
+ // Set the tab name if provided
+ if let Some(name) = tab_layout_info.tab_name.take() {
+ tab.name = name;
+ }
+
+ // Find already-running panes for this tab
+ let (tiled_to_ignore, floating_indices) = find_already_running_panes(
+ &tab_layout_info.tiled_layout,
+ &tab_layout_info.floating_layouts,
+ tab,
);
- continue;
- },
- };
- if let Some(name) = new_tab_name.take() {
- active_tab.name = name;
- }
- let (tiled_to_ignore, floating_indices) =
- find_already_running_panes(&tiled_layout, &floating_layouts, active_tab);
+ // Mark run instructions to ignore (prevents re-spawning)
+ for run_instruction in tiled_to_ignore {
+ tab_layout_info
+ .tiled_layout
+ .ignore_run_instruction(run_instruction);
+ }
- for run_instruction in tiled_to_ignore {
- tiled_layout.ignore_run_instruction(run_instruction);
- }
+ for idx in floating_indices {
+ tab_layout_info
+ .floating_layouts
+ .get_mut(idx)
+ .map(|f| f.already_running = true);
+ }
- for idx in floating_indices {
- floating_layouts
- .get_mut(idx)
- .map(|f| f.already_running = true);
+ processed_tab_layouts.push(tab_layout_info);
+ }
}
- let tab_index = active_tab.index;
+ // 3. Send to plugin thread with all tab layouts
screen
.bus
.senders
.send_to_plugin(PluginInstruction::OverrideLayout(
cwd,
default_shell,
- tiled_layout,
- floating_layouts,
- swap_tiled_layouts,
- swap_floating_layouts,
+ processed_tab_layouts,
retain_existing_terminal_panes,
retain_existing_plugin_panes,
- tab_index,
client_id,
- _completion_tx,
+ completion_tx,
))?;
+
+ // 4. Close tabs that aren't in the layout
+ if !apply_only_to_focused_tab {
+ for tab_index in tabs_to_close {
+ screen.close_tab_at_index(tab_index)?;
+ }
+ }
},
ScreenInstruction::OverrideLayoutComplete(
- tiled_layout,
- floating_layouts,
- new_swap_tiled_layouts,
- new_swap_floating_layouts,
- new_terminal_pids,
- new_floating_pane_pids,
- plugin_ids,
+ tab_results,
retain_existing_terminal_panes,
retain_existing_plugin_panes,
- tab_index,
client_id,
completion_tx,
) => {
- screen.tabs.get_mut(&tab_index).map(|t| {
- t.override_layout(
- tiled_layout,
- floating_layouts,
- new_swap_tiled_layouts,
- new_swap_floating_layouts,
- new_terminal_pids,
- new_floating_pane_pids,
- plugin_ids,
- retain_existing_terminal_panes,
- retain_existing_plugin_panes,
- client_id,
- None,
- )
- });
+ // Process each tab result
+ for tab_result in tab_results {
+ if let Some(tab) = screen.tabs.get_mut(&tab_result.tab_index) {
+ if let Err(e) = tab.override_layout(
+ tab_result.tiled_layout,
+ tab_result.floating_layouts,
+ tab_result.swap_tiled_layouts.clone(),
+ tab_result.swap_floating_layouts.clone(),
+ tab_result.new_terminal_pids,
+ tab_result.new_floating_pane_pids,
+ tab_result.plugin_ids,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ client_id,
+ None,
+ ) {
+ log::error!(
+ "Failed to override layout for tab {}: {:?}",
+ tab_result.tab_index,
+ e
+ );
+ }
+ } else {
+ // Tab doesn't exist - create it
+ let swap_layouts = (
+ tab_result.swap_tiled_layouts.clone().unwrap_or_default(),
+ tab_result.swap_floating_layouts.clone().unwrap_or_default(),
+ );
+ if let Err(e) = screen.new_tab(
+ tab_result.tab_index,
+ swap_layouts,
+ tab_result.tab_name.clone(),
+ None,
+ ) {
+ log::error!(
+ "Failed to create new tab {} during override completion: {:?}",
+ tab_result.tab_index,
+ e
+ );
+ continue;
+ }
+
+ // Now override the newly created tab's layout
+ if let Some(tab) = screen.tabs.get_mut(&tab_result.tab_index) {
+ if let Err(e) = tab.override_layout(
+ tab_result.tiled_layout,
+ tab_result.floating_layouts,
+ tab_result.swap_tiled_layouts,
+ tab_result.swap_floating_layouts,
+ tab_result.new_terminal_pids,
+ tab_result.new_floating_pane_pids,
+ tab_result.plugin_ids,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ client_id,
+ None,
+ ) {
+ log::error!(
+ "Failed to override layout for new tab {}: {:?}",
+ tab_result.tab_index,
+ e
+ );
+ }
+ }
+ }
+ }
+
+ for event in pending_events_waiting_for_client.drain(..) {
+ screen.bus.senders.send_to_screen(event).non_fatal();
+ }
+
+ for event in pending_events_waiting_for_tab.drain(..) {
+ screen.bus.senders.send_to_screen(event).non_fatal();
+ }
+
+ // Single render and log after all tabs are updated (performance optimization)
screen.log_and_report_session_state()?;
let _ = screen.render(None);
drop(completion_tx); // action ends here, notify the action initiator
diff --git a/zellij-server/src/session_layout_metadata.rs b/zellij-server/src/session_layout_metadata.rs
index 238d476a..96e5246d 100644
--- a/zellij-server/src/session_layout_metadata.rs
+++ b/zellij-server/src/session_layout_metadata.rs
@@ -2,9 +2,11 @@ use crate::panes::PaneId;
use crate::ClientId;
use std::collections::{BTreeMap, HashMap};
use std::path::PathBuf;
+use std::time::{SystemTime, UNIX_EPOCH};
use zellij_utils::common_path::common_path_all;
use zellij_utils::pane_size::PaneGeom;
use zellij_utils::{
+ data::{LayoutMetadata, PaneMetadata, TabMetadata},
input::command::RunCommand,
input::layout::{Layout, Run, RunPlugin, RunPluginOrAlias},
input::plugins::PluginAliases,
@@ -241,6 +243,27 @@ impl SessionLayoutMetadata {
}
plugin_ids
}
+ pub fn remove_plugin_from_layout(&mut self, plugin_id_to_remove: u32) {
+ for tab in &mut self.tabs {
+ // Filter tiled panes
+ tab.tiled_panes.retain(|pane| {
+ if let PaneId::Plugin(id) = pane.id {
+ id != plugin_id_to_remove
+ } else {
+ true
+ }
+ });
+
+ // Filter floating panes
+ tab.floating_panes.retain(|pane| {
+ if let PaneId::Plugin(id) = pane.id {
+ id != plugin_id_to_remove
+ } else {
+ true
+ }
+ });
+ }
+ }
pub fn update_terminal_commands(
&mut self,
mut terminal_ids_to_commands: HashMap<u32, Vec<String>>,
@@ -330,6 +353,22 @@ impl SessionLayoutMetadata {
self.default_layout
.populate_plugin_aliases_in_layout(&plugin_aliases);
}
+ pub fn to_layout_metadata(&self) -> LayoutMetadata {
+ // Get current timestamp for both creation and update time
+ let current_time = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .map(|d| d.as_secs().to_string())
+ .unwrap_or_default();
+
+ // Convert all tabs
+ let tabs = self.tabs.iter().map(|tab| tab.to_tab_metadata()).collect();
+
+ LayoutMetadata {
+ tabs,
+ creation_time: current_time.clone(),
+ update_time: current_time,
+ }
+ }
}
impl Into<GlobalLayoutManifest> for SessionLayoutMetadata {
@@ -358,6 +397,27 @@ impl Into<TabLayoutManifest> for TabLayoutMetadata {
}
}
+impl TabLayoutMetadata {
+ fn to_tab_metadata(&self) -> TabMetadata {
+ let mut panes = Vec::new();
+
+ // Extract pane metadata from tiled panes
+ for pane in &self.tiled_panes {
+ panes.push(pane.to_pane_metadata());
+ }
+
+ // Extract pane metadata from floating panes
+ for pane in &self.floating_panes {
+ panes.push(pane.to_pane_metadata());
+ }
+
+ TabMetadata {
+ panes,
+ name: self.name.clone(),
+ }
+ }
+}
+
impl Into<PaneLayoutManifest> for PaneLayoutMetadata {
fn into(self) -> PaneLayoutManifest {
PaneLayoutManifest {
@@ -417,6 +477,40 @@ impl PaneLayoutMetadata {
focused_clients,
}
}
+ fn to_pane_metadata(&self) -> PaneMetadata {
+ use zellij_utils::input::layout::RunPluginLocation;
+
+ // Try to extract a meaningful name from the pane
+ // Priority: explicit title > command name > file name > plugin location
+ let name = self.title.clone().or_else(|| {
+ self.run.as_ref().and_then(|run| match run {
+ Run::Command(cmd) => Some(cmd.command.display().to_string()),
+ Run::EditFile(path, _, _) => {
+ path.file_name().map(|n| n.to_string_lossy().to_string())
+ },
+ Run::Plugin(plugin) => Some(plugin.location_string()),
+ Run::Cwd(_) => None,
+ })
+ });
+
+ let is_plugin = matches!(self.id, PaneId::Plugin(_));
+
+ // Detect if this is a builtin plugin
+ let is_builtin_plugin = self
+ .run
+ .as_ref()
+ .map(|run| match run {
+ Run::Plugin(plugin) => plugin.is_builtin_plugin(),
+ _ => false,
+ })
+ .unwrap_or(false);
+
+ PaneMetadata {
+ name,
+ is_plugin,
+ is_builtin_plugin,
+ }
+ }
}
pub struct ClientMetadata {
diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs
index 7e1dce3d..25180259 100644
--- a/zellij-tile/src/shim.rs
+++ b/zellij-tile/src/shim.rs
@@ -10,7 +10,12 @@ use zellij_utils::input::actions::Action;
pub use zellij_utils::plugin_api;
use zellij_utils::plugin_api::event::ProtobufPaneScrollbackResponse;
use zellij_utils::plugin_api::plugin_command::{
- CreateTokenResponse, ListTokensResponse, ProtobufGetPanePidResponse, ProtobufPluginCommand,
+ dump_layout_response, dump_session_layout_response, get_focused_pane_info_response,
+ parse_layout_response, CreateTokenResponse, ListTokensResponse, ProtobufDeleteLayoutResponse,
+ ProtobufDumpLayoutResponse, ProtobufDumpSessionLayoutResponse, ProtobufEditLayoutResponse,
+ ProtobufGenerateRandomNameResponse, ProtobufGetFocusedPaneInfoResponse,
+ ProtobufGetLayoutDirResponse, ProtobufGetPanePidResponse, ProtobufParseLayoutResponse,
+ ProtobufPluginCommand, ProtobufRenameLayoutResponse, ProtobufSaveLayoutResponse,
RenameWebTokenResponse, RevokeAllWebTokensResponse, RevokeTokenResponse,
};
use zellij_utils::plugin_api::plugin_ids::{ProtobufPluginIds, ProtobufZellijVersion};
@@ -89,6 +94,98 @@ pub fn get_zellij_version() -> String {
protobuf_zellij_version.version
}
+/// Generates a random human-readable name using Zellij's curated word lists.
+/// Returns a name in the format AdjectiveNoun (e.g., "BraveRustacean", "ZippyWeasel").
+///
+/// This uses the same word lists as session name generation, providing
+/// approximately 4,096 unique combinations.
+pub fn generate_random_name() -> String {
+ let plugin_command = PluginCommand::GenerateRandomName;
+ let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
+ object_to_stdout(&protobuf_plugin_command.encode_to_vec());
+ unsafe { host_run_plugin_command() };
+ let response =
+ ProtobufGenerateRandomNameResponse::decode(bytes_from_stdin().unwrap().as_slice()).unwrap();
+ response.name
+}
+
+/// Dumps a layout by name and returns its KDL content as a String
+///
+/// Supports both built-in layouts (eg. "default", "compact", "welcome")
+/// and custom layouts from the plugin's layout directory.
+pub fn dump_layout(layout_name: &str) -> Result<String, String> {
+ // Create the plugin command with the layout name
+ let plugin_command = PluginCommand::DumpLayout(layout_name.to_string());
+
+ // Convert to protobuf and encode
+ let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
+ object_to_stdout(&protobuf_plugin_command.encode_to_vec());
+
+ // Call the host function (blocks until response)
+ unsafe { host_run_plugin_command() };
+
+ // Read and decode the response
+ let response_bytes =
+ bytes_from_stdin().map_err(|e| format!("Failed to read response from stdin: {:?}", e))?;
+
+ let protobuf_response = ProtobufDumpLayoutResponse::decode(response_bytes.as_slice())
+ .map_err(|e| format!("Failed to decode protobuf response: {}", e))?;
+
+ // Extract result from the oneof field
+ match protobuf_response.result {
+ Some(dump_layout_response::Result::LayoutContent(content)) => Ok(content),
+ Some(dump_layout_response::Result::Error(error)) => Err(error),
+ None => Err("Server returned empty response".to_string()),
+ }
+}
+
+/// Returns the path to the layout directory.
+///
+/// This is the directory where Zellij looks for layout files. It can be:
+/// - The directory specified via CLI `--layout-dir` flag
+/// - The directory specified in the config file
+/// - The directory specified via ZELLIJ_LAYOUT_DIR env var
+/// - The default: `~/.config/zellij/layouts`
+///
+/// # Returns
+/// A String containing the absolute path to the layout directory.
+/// Returns an empty string if the layout directory cannot be determined (rare edge case).
+pub fn get_layout_dir() -> String {
+ let plugin_command = PluginCommand::GetLayoutDir;
+ let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
+ object_to_stdout(&protobuf_plugin_command.encode_to_vec());
+ unsafe { host_run_plugin_command() };
+ let response =
+ ProtobufGetLayoutDirResponse::decode(bytes_from_stdin().unwrap().as_slice()).unwrap();
+ response.layout_dir
+}
+
+/// Returns the focused pane ID and tab index for the client associated with this plugin.
+pub fn get_focused_pane_info() -> Result<(usize, PaneId), String> {
+ let plugin_command = PluginCommand::GetFocusedPaneInfo;
+ let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
+ object_to_stdout(&protobuf_plugin_command.encode_to_vec());
+ unsafe { host_run_plugin_command() };
+
+ let protobuf_response =
+ ProtobufGetFocusedPaneInfoResponse::decode(bytes_from_stdin().unwrap().as_slice()).unwrap();
+
+ match protobuf_response.result {
+ Some(get_focused_pane_info_response::Result::FocusedPaneInfo(info)) => {
+ let tab_index = info.focused_tab_index as usize;
+ match info.focused_pane_id {
+ Some(pb_pane_id) => match pb_pane_id.try_into() {
+ Ok(pane_id) => Ok((tab_index, pane_id)),
+ Err(_) => Err("Invalid pane_id in response".to_string()),
+ },
+ None => Err("Missing pane_id in response".to_string()),
+ }
+ },
+ Some(get_focused_pane_info_response::Result::Error(err)) => Err(err),
+ None => Err("Empty response from host".to_string()),
+ }
+}
+
// Host Functions
/// Open a file in the user's default `$EDITOR` in a new pane
@@ -439,8 +536,8 @@ pub fn new_tabs_with_layout(layout: &str) {
}
/// Provide a LayoutInfo to be applied to the current session in a new tab. If the layout has multiple tabs, they will all be opened.
-pub fn new_tabs_with_layout_info(layout_info: LayoutInfo) {
- let plugin_command = PluginCommand::NewTabsWithLayoutInfo(layout_info);
+pub fn new_tabs_with_layout_info<L: AsRef<LayoutInfo>>(layout_info: L) {
+ let plugin_command = PluginCommand::NewTabsWithLayoutInfo(layout_info.as_ref().clone());
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
unsafe { host_run_plugin_command() };
@@ -1009,12 +1106,70 @@ pub fn watch_filesystem() {
unsafe { host_run_plugin_command() };
}
-/// Get the serialized session layout in KDL format as a CustomMessage Event
-pub fn dump_session_layout() {
- let plugin_command = PluginCommand::DumpSessionLayout;
+/// Get the serialized session layout in KDL format synchronously
+/// note: this removes the requesting plugin from the dumped layout
+pub fn dump_session_layout() -> Result<(String, Option<LayoutMetadata>), String> {
+ dump_session_layout_impl(None)
+}
+
+/// Get the serialized layout for a specific tab in KDL format synchronously
+/// note: this removes the requesting plugin from the dumped layout
+pub fn dump_session_layout_for_tab(
+ tab_index: usize,
+) -> Result<(String, Option<LayoutMetadata>), String> {
+ dump_session_layout_impl(Some(tab_index))
+}
+
+fn dump_session_layout_impl(
+ tab_index: Option<usize>,
+) -> Result<(String, Option<LayoutMetadata>), String> {
+ let plugin_command = PluginCommand::DumpSessionLayout { tab_index };
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
+
unsafe { host_run_plugin_command() };
+
+ let response_bytes =
+ bytes_from_stdin().map_err(|e| format!("Failed to read response from stdin: {:?}", e))?;
+ let protobuf_response = ProtobufDumpSessionLayoutResponse::decode(response_bytes.as_slice())
+ .map_err(|e| format!("Failed to decode protobuf response: {}", e))?;
+
+ // Extract metadata if present
+ let metadata = protobuf_response
+ .metadata
+ .and_then(|pb_metadata| pb_metadata.try_into().ok());
+
+ match protobuf_response.result {
+ Some(dump_session_layout_response::Result::LayoutContent(content)) => {
+ Ok((content, metadata))
+ },
+ Some(dump_session_layout_response::Result::Error(error)) => Err(error),
+ None => Err("Server returned empty response".to_string()),
+ }
+}
+
+/// Parses a KDL layout string and returns LayoutMetadata
+pub fn parse_layout(layout_string: &str) -> Result<LayoutMetadata, LayoutParsingError> {
+ let plugin_command = PluginCommand::ParseLayout(layout_string.to_string());
+ let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
+ object_to_stdout(&protobuf_plugin_command.encode_to_vec());
+
+ unsafe { host_run_plugin_command() };
+
+ let response_bytes = bytes_from_stdin().map_err(|_| LayoutParsingError::SyntaxError)?;
+
+ let protobuf_response = ProtobufParseLayoutResponse::decode(response_bytes.as_slice())
+ .map_err(|_| LayoutParsingError::SyntaxError)?;
+
+ match protobuf_response.result {
+ Some(parse_layout_response::Result::Metadata(metadata)) => metadata
+ .try_into()
+ .map_err(|_| LayoutParsingError::SyntaxError),
+ Some(parse_layout_response::Result::Error(error)) => Err(error
+ .try_into()
+ .map_err(|_| LayoutParsingError::SyntaxError)?),
+ None => Err(LayoutParsingError::SyntaxError),
+ }
}
/// Get a list of clients, their focused pane and running command or focused plugin back as an
@@ -1191,6 +1346,200 @@ pub fn get_pane_pid(pane_id: PaneId) -> Result<i32, String> {
}
}
+/// Save a layout to the user's layout directory
+///
+/// # Arguments
+/// * `layout_name` - Name of the layout file (without .kdl extension)
+/// * `layout_kdl` - KDL string representing the layout
+/// * `overwrite` - Whether to overwrite if the file already exists
+///
+/// # Returns
+/// * `Ok(())` - Layout was successfully validated and saved
+/// * `Err(String)` - Error message (parse error, I/O error, file exists, etc.)
+pub fn save_layout<S: AsRef<str>>(
+ layout_name: S,
+ layout_kdl: S,
+ overwrite: bool,
+) -> Result<(), String> {
+ let plugin_command = PluginCommand::SaveLayout {
+ layout_name: layout_name.as_ref().to_owned(),
+ layout_kdl: layout_kdl.as_ref().to_owned(),
+ overwrite,
+ };
+ let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
+ object_to_stdout(&protobuf_plugin_command.encode_to_vec());
+ unsafe { host_run_plugin_command() };
+
+ // Read response from stdin
+ let response_bytes =
+ bytes_from_stdin().map_err(|e| format!("Failed to read response from stdin: {:?}", e))?;
+
+ // Decode protobuf response
+ let protobuf_response = ProtobufSaveLayoutResponse::decode(response_bytes.as_slice())
+ .map_err(|e| format!("Failed to decode protobuf response: {}", e))?;
+
+ // Convert to Rust type
+ let response = SaveLayoutResponse::try_from(protobuf_response)
+ .map_err(|e| format!("Failed to convert protobuf response: {}", e))?;
+
+ // Convert Result enum to actual Result type
+ match response {
+ SaveLayoutResponse::Ok(_) => Ok(()),
+ SaveLayoutResponse::Err(error_msg) => Err(error_msg),
+ }
+}
+
+/// Delete a layout from the user's layout directory.
+///
+/// # Arguments
+///
+/// * `layout_name` - The name of the layout file to delete (without the `.kdl` extension).
+/// Layout names are sanitized server-side to prevent directory traversal.
+///
+/// # Returns
+///
+/// * `Ok(())` - The layout was successfully deleted
+/// * `Err(String)` - An error occurred with a descriptive message (e.g., file not found, permission denied)
+///
+/// # Permissions
+///
+/// Requires the `ChangeApplicationState` permission.
+///
+/// # Examples
+///
+/// ```no_run
+/// use zellij_tile::prelude::*;
+///
+/// // Delete a layout named "my-layout"
+/// match delete_layout("my-layout") {
+/// Ok(_) => eprintln!("Layout deleted successfully"),
+/// Err(e) => eprintln!("Failed to delete layout: {}", e),
+/// }
+/// ```
+pub fn delete_layout<S: AsRef<str>>(layout_name: S) -> Result<(), String> {
+ let plugin_command = PluginCommand::DeleteLayout {
+ layout_name: layout_name.as_ref().to_owned(),
+ };
+ let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
+ object_to_stdout(&protobuf_plugin_command.encode_to_vec());
+ unsafe { host_run_plugin_command() };
+
+ // Read response from stdin
+ let response_bytes =
+ bytes_from_stdin().map_err(|e| format!("Failed to read response from stdin: {:?}", e))?;
+
+ // Decode protobuf response
+ let protobuf_response = ProtobufDeleteLayoutResponse::decode(response_bytes.as_slice())
+ .map_err(|e| format!("Failed to decode protobuf response: {}", e))?;
+
+ // Convert to Rust type
+ let response = DeleteLayoutResponse::try_from(protobuf_response)
+ .map_err(|e| format!("Failed to convert protobuf response: {}", e))?;
+
+ // Convert Result enum to actual Result type
+ match response {
+ DeleteLayoutResponse::Ok(_) => Ok(()),
+ DeleteLayoutResponse::Err(error_msg) => Err(error_msg),
+ }
+}
+
+/// Rename a layout file in the user's layout directory
+///
+/// # Arguments
+/// * `old_layout_name` - Current name of the layout (without .kdl extension)
+/// * `new_layout_name` - New name for the layout (without .kdl extension)
+///
+/// # Returns
+/// * `Ok(())` - Layout was successfully renamed
+/// * `Err(String)` - Error message describing what went wrong
+///
+/// # Error Cases
+/// * Old layout name is invalid (empty, contains path separators, etc.)
+/// * New layout name is invalid
+/// * Source layout file doesn't exist
+/// * Target layout file already exists (no overwrite)
+/// * Layout directory not found
+/// * File system error during rename operation
+///
+/// # Permissions
+///
+/// Requires the `ChangeApplicationState` permission.
+///
+/// # Examples
+///
+/// ```no_run
+/// use zellij_tile::prelude::*;
+///
+/// // Rename a layout from "old-name" to "new-name"
+/// match rename_layout("old-name", "new-name") {
+/// Ok(_) => eprintln!("Layout renamed successfully"),
+/// Err(e) => eprintln!("Failed to rename layout: {}", e),
+/// }
+/// ```
+pub fn rename_layout(
+ old_layout_name: impl Into<String>,
+ new_layout_name: impl Into<String>,
+) -> Result<(), String> {
+ let plugin_command = PluginCommand::RenameLayout {
+ old_layout_name: old_layout_name.into(),
+ new_layout_name: new_layout_name.into(),
+ };
+ let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
+ object_to_stdout(&protobuf_plugin_command.encode_to_vec());
+ unsafe { host_run_plugin_command() };
+
+ // Read response from stdin
+ let response_bytes =
+ bytes_from_stdin().map_err(|e| format!("Failed to read response from stdin: {:?}", e))?;
+
+ // Decode protobuf response
+ let protobuf_response = ProtobufRenameLayoutResponse::decode(response_bytes.as_slice())
+ .map_err(|e| format!("Failed to decode protobuf response: {}", e))?;
+
+ // Convert to native response type
+ let response: RenameLayoutResponse = protobuf_response
+ .try_into()
+ .map_err(|e| format!("Failed to convert protobuf response: {}", e))?;
+
+ match response {
+ RenameLayoutResponse::Ok(_) => Ok(()),
+ RenameLayoutResponse::Err(error_msg) => Err(error_msg),
+ }
+}
+
+/// Opens a layout file in the user's default `$EDITOR`
+pub fn edit_layout<S: AsRef<str>>(
+ layout_name: S,
+ context: BTreeMap<String, String>,
+) -> Result<(), String> {
+ let layout_name = layout_name.as_ref().to_owned();
+ let plugin_command = PluginCommand::EditLayout {
+ layout_name,
+ context,
+ };
+ let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
+ object_to_stdout(&protobuf_plugin_command.encode_to_vec());
+ unsafe { host_run_plugin_command() };
+
+ // Read response from stdin
+ let response_bytes =
+ bytes_from_stdin().map_err(|e| format!("Failed to read response from stdin: {:?}", e))?;
+
+ // Decode protobuf response
+ let protobuf_response = ProtobufEditLayoutResponse::decode(response_bytes.as_slice())
+ .map_err(|e| format!("Failed to decode protobuf response: {}", e))?;
+
+ // Convert to Rust type
+ let response = EditLayoutResponse::try_from(protobuf_response)
+ .map_err(|e| format!("Failed to convert protobuf response: {}", e))?;
+
+ // Convert Result enum to actual Result type
+ match response {
+ EditLayoutResponse::Ok(_) => Ok(()),
+ EditLayoutResponse::Err(error_msg) => Err(error_msg),
+ }
+}
+
/// Switch the position of the pane with this id with a different pane
pub fn move_pane_with_pane_id(pane_id: PaneId) {
let plugin_command = PluginCommand::MovePaneWithPaneId(pane_id);
@@ -1641,6 +1990,25 @@ pub fn get_focused_pane(tab_position: usize, pane_manifest: &PaneManifest) -> Op
None
}
+pub fn override_layout<L: AsRef<LayoutInfo>>(
+ layout_info: L,
+ retain_existing_terminal_panes: bool,
+ retain_existing_plugin_panes: bool,
+ apply_only_to_active_tab: bool,
+ context: BTreeMap<String, String>,
+) {
+ let plugin_command = PluginCommand::OverrideLayout(
+ layout_info.as_ref().clone(),
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ apply_only_to_active_tab,
+ context,
+ );
+ let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
+ object_to_stdout(&protobuf_plugin_command.encode_to_vec());
+ unsafe { host_run_plugin_command() };
+}
+
// Internal Functions
#[doc(hidden)]
diff --git a/zellij-tile/src/ui_components/text.rs b/zellij-tile/src/ui_components/text.rs
index 492e2352..fe813fb3 100644
--- a/zellij-tile/src/ui_components/text.rs
+++ b/zellij-tile/src/ui_components/text.rs
@@ -175,6 +175,24 @@ impl Text {
self
}
+
+ pub fn error_color_last_substring<S: AsRef<str>>(self, substr: S) -> Self {
+ const ERROR_COLOR_LEVEL: usize = 6;
+ let substr = substr.as_ref();
+ let mut start = 0;
+ let mut last_pos = None;
+
+ while let Some(pos) = self.text[start..].find(substr) {
+ last_pos = Some(start + pos);
+ start = start + pos + substr.len();
+ }
+
+ if let Some(abs_pos) = last_pos {
+ return self.color_range(ERROR_COLOR_LEVEL, abs_pos..abs_pos + substr.len());
+ }
+ self
+ }
+
pub fn error_color_all(self) -> Self {
const ERROR_COLOR_LEVEL: usize = 6;
self.color_range(ERROR_COLOR_LEVEL, ..)
@@ -239,6 +257,24 @@ impl Text {
self
}
+
+ pub fn success_color_last_substring<S: AsRef<str>>(self, substr: S) -> Self {
+ const SUCCESS_COLOR_LEVEL: usize = 7;
+ let substr = substr.as_ref();
+ let mut start = 0;
+ let mut last_pos = None;
+
+ while let Some(pos) = self.text[start..].find(substr) {
+ last_pos = Some(start + pos);
+ start = start + pos + substr.len();
+ }
+
+ if let Some(abs_pos) = last_pos {
+ return self.color_range(SUCCESS_COLOR_LEVEL, abs_pos..abs_pos + substr.len());
+ }
+ self
+ }
+
pub fn success_color_all(self) -> Self {
const SUCCESS_COLOR_LEVEL: usize = 7;
self.color_range(SUCCESS_COLOR_LEVEL, ..)
@@ -307,6 +343,23 @@ impl Text {
self
}
+
+ pub fn color_last_substring<S: AsRef<str>>(self, index_level: usize, substr: S) -> Self {
+ let substr = substr.as_ref();
+ let mut start = 0;
+ let mut last_pos = None;
+
+ while let Some(pos) = self.text[start..].find(substr) {
+ last_pos = Some(start + pos);
+ start = start + pos + substr.len();
+ }
+
+ if let Some(abs_pos) = last_pos {
+ return self.color_range(index_level, abs_pos..abs_pos + substr.len());
+ }
+ self
+ }
+
pub fn content(&self) -> &str {
&self.text
}
diff --git a/zellij-utils/assets/config/default.kdl b/zellij-utils/assets/config/default.kdl
index e06dbe5c..db2e4468 100644
--- a/zellij-utils/assets/config/default.kdl
+++ b/zellij-utils/assets/config/default.kdl
@@ -156,6 +156,13 @@ keybinds {
};
SwitchToMode "Normal"
}
+ bind "l" {
+ LaunchOrFocusPlugin "zellij:layout-manager" {
+ floating true
+ move_to_focused_tab true
+ };
+ SwitchToMode "Normal"
+ }
}
tmux {
bind "[" { SwitchToMode "Scroll"; }
diff --git a/zellij-utils/assets/plugins/about.wasm b/zellij-utils/assets/plugins/about.wasm
index 812659bc..aab775a5 100755
--- a/zellij-utils/assets/plugins/about.wasm
+++ b/zellij-utils/assets/plugins/about.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/compact-bar.wasm b/zellij-utils/assets/plugins/compact-bar.wasm
index 75f94c4b..2719eaa4 100755
--- a/zellij-utils/assets/plugins/compact-bar.wasm
+++ b/zellij-utils/assets/plugins/compact-bar.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/configuration.wasm b/zellij-utils/assets/plugins/configuration.wasm
index ffa97232..d5111f1a 100755
--- a/zellij-utils/assets/plugins/configuration.wasm
+++ b/zellij-utils/assets/plugins/configuration.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm b/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm
index cd74ed1d..1db435ba 100755
--- a/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm
+++ b/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/layout-manager.wasm b/zellij-utils/assets/plugins/layout-manager.wasm
new file mode 100755
index 00000000..e147d179
--- /dev/null
+++ b/zellij-utils/assets/plugins/layout-manager.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/multiple-select.wasm b/zellij-utils/assets/plugins/multiple-select.wasm
index 4412b8df..43676c22 100755
--- a/zellij-utils/assets/plugins/multiple-select.wasm
+++ b/zellij-utils/assets/plugins/multiple-select.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/plugin-manager.wasm b/zellij-utils/assets/plugins/plugin-manager.wasm
index cb8e2239..ef666220 100755
--- a/zellij-utils/assets/plugins/plugin-manager.wasm
+++ b/zellij-utils/assets/plugins/plugin-manager.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/sequence.wasm b/zellij-utils/assets/plugins/sequence.wasm
index d6286cb0..b2e470a1 100755
--- a/zellij-utils/assets/plugins/sequence.wasm
+++ b/zellij-utils/assets/plugins/sequence.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/session-manager.wasm b/zellij-utils/assets/plugins/session-manager.wasm
index ff97eb14..6f556f01 100755
--- a/zellij-utils/assets/plugins/session-manager.wasm
+++ b/zellij-utils/assets/plugins/session-manager.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/share.wasm b/zellij-utils/assets/plugins/share.wasm
index ed580f3f..99606d71 100755
--- a/zellij-utils/assets/plugins/share.wasm
+++ b/zellij-utils/assets/plugins/share.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/status-bar.wasm b/zellij-utils/assets/plugins/status-bar.wasm
index 98e8e647..f5db526c 100755
--- a/zellij-utils/assets/plugins/status-bar.wasm
+++ b/zellij-utils/assets/plugins/status-bar.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/strider.wasm b/zellij-utils/assets/plugins/strider.wasm
index c33e5e2f..735f74a9 100755
--- a/zellij-utils/assets/plugins/strider.wasm
+++ b/zellij-utils/assets/plugins/strider.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/tab-bar.wasm b/zellij-utils/assets/plugins/tab-bar.wasm
index 609fa718..fc2d6f1c 100755
--- a/zellij-utils/assets/plugins/tab-bar.wasm
+++ b/zellij-utils/assets/plugins/tab-bar.wasm
Binary files differ
diff --git a/zellij-utils/assets/prost/api.action.rs b/zellij-utils/assets/prost/api.action.rs
index ccf96860..52ab0ab0 100644
--- a/zellij-utils/assets/prost/api.action.rs
+++ b/zellij-utils/assets/prost/api.action.rs
@@ -257,21 +257,31 @@ pub struct NewTabPayload {
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
-pub struct OverrideLayoutPayload {
- #[prost(message, optional, tag="1")]
+pub struct TabLayoutInfo {
+ #[prost(uint32, tag="1")]
+ pub tab_index: u32,
+ #[prost(string, optional, tag="2")]
+ pub tab_name: ::core::option::Option<::prost::alloc::string::String>,
+ #[prost(message, optional, tag="3")]
pub tiled_layout: ::core::option::Option<TiledPaneLayout>,
- #[prost(message, repeated, tag="2")]
+ #[prost(message, repeated, tag="4")]
pub floating_layouts: ::prost::alloc::vec::Vec<FloatingPaneLayout>,
- #[prost(message, repeated, tag="3")]
+ #[prost(message, repeated, tag="5")]
pub swap_tiled_layouts: ::prost::alloc::vec::Vec<SwapTiledLayout>,
- #[prost(message, repeated, tag="4")]
+ #[prost(message, repeated, tag="6")]
pub swap_floating_layouts: ::prost::alloc::vec::Vec<SwapFloatingLayout>,
- #[prost(string, optional, tag="5")]
- pub tab_name: ::core::option::Option<::prost::alloc::string::String>,
- #[prost(bool, tag="10")]
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct OverrideLayoutPayload {
+ #[prost(message, repeated, tag="1")]
+ pub tabs: ::prost::alloc::vec::Vec<TabLayoutInfo>,
+ #[prost(bool, tag="2")]
pub retain_existing_terminal_panes: bool,
- #[prost(bool, tag="11")]
+ #[prost(bool, tag="3")]
pub retain_existing_plugin_panes: bool,
+ #[prost(bool, tag="4")]
+ pub apply_only_to_active_tab: bool,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
diff --git a/zellij-utils/assets/prost/api.event.rs b/zellij-utils/assets/prost/api.event.rs
index 5e1d7d1c..03a3597c 100644
--- a/zellij-utils/assets/prost/api.event.rs
+++ b/zellij-utils/assets/prost/api.event.rs
@@ -9,7 +9,7 @@ pub struct EventNameList {
pub struct Event {
#[prost(enumeration="EventType", tag="1")]
pub name: i32,
- #[prost(oneof="event::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33")]
+ #[prost(oneof="event::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34")]
pub payload: ::core::option::Option<event::Payload>,
}
/// Nested message and enum types in `Event`.
@@ -81,6 +81,8 @@ pub mod event {
ActionCompletePayload(super::ActionCompletePayload),
#[prost(message, tag="33")]
CwdChangedPayload(super::CwdChangedPayload),
+ #[prost(message, tag="34")]
+ AvailableLayoutInfoPayload(super::AvailableLayoutInfoPayload),
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
@@ -218,6 +220,14 @@ pub struct SessionUpdatePayload {
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct AvailableLayoutInfoPayload {
+ #[prost(message, repeated, tag="1")]
+ pub available_layouts: ::prost::alloc::vec::Vec<LayoutInfo>,
+ #[prost(message, repeated, tag="2")]
+ pub layouts_with_errors: ::prost::alloc::vec::Vec<LayoutWithError>,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
pub struct RunCommandResultPayload {
#[prost(int32, optional, tag="1")]
pub exit_code: ::core::option::Option<i32>,
@@ -391,6 +401,88 @@ pub struct LayoutInfo {
pub name: ::prost::alloc::string::String,
#[prost(string, tag="2")]
pub source: ::prost::alloc::string::String,
+ #[prost(message, optional, tag="5")]
+ pub layout_metadata: ::core::option::Option<LayoutMetadata>,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct LayoutWithError {
+ #[prost(string, tag="1")]
+ pub layout_name: ::prost::alloc::string::String,
+ #[prost(message, optional, tag="2")]
+ pub error: ::core::option::Option<LayoutParsingError>,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct LayoutParsingError {
+ #[prost(oneof="layout_parsing_error::ErrorType", tags="1, 2")]
+ pub error_type: ::core::option::Option<layout_parsing_error::ErrorType>,
+}
+/// Nested message and enum types in `LayoutParsingError`.
+pub mod layout_parsing_error {
+ #[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Oneof)]
+ pub enum ErrorType {
+ #[prost(message, tag="1")]
+ KdlError(super::KdlErrorVariant),
+ #[prost(message, tag="2")]
+ SyntaxError(super::SyntaxError),
+ }
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct KdlErrorVariant {
+ #[prost(message, optional, tag="1")]
+ pub kdl_error: ::core::option::Option<KdlError>,
+ #[prost(string, tag="2")]
+ pub file_name: ::prost::alloc::string::String,
+ #[prost(string, tag="3")]
+ pub source_code: ::prost::alloc::string::String,
+}
+/// Empty message, just a marker
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct SyntaxError {
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct KdlError {
+ #[prost(string, tag="1")]
+ pub error_message: ::prost::alloc::string::String,
+ #[prost(uint64, optional, tag="2")]
+ pub offset: ::core::option::Option<u64>,
+ #[prost(uint64, optional, tag="3")]
+ pub len: ::core::option::Option<u64>,
+ #[prost(string, optional, tag="4")]
+ pub help_message: ::core::option::Option<::prost::alloc::string::String>,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct LayoutMetadata {
+ #[prost(message, repeated, tag="1")]
+ pub tabs: ::prost::alloc::vec::Vec<TabMetadata>,
+ #[prost(string, tag="2")]
+ pub creation_time: ::prost::alloc::string::String,
+ #[prost(string, tag="3")]
+ pub update_time: ::prost::alloc::string::String,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct TabMetadata {
+ #[prost(message, repeated, tag="1")]
+ pub pane_metadata: ::prost::alloc::vec::Vec<PaneMetadata>,
+ #[prost(string, optional, tag="2")]
+ pub name: ::core::option::Option<::prost::alloc::string::String>,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct PaneMetadata {
+ #[prost(string, optional, tag="1")]
+ pub name: ::core::option::Option<::prost::alloc::string::String>,
+ #[prost(bool, tag="2")]
+ pub is_plugin: bool,
+ #[prost(bool, tag="3")]
+ pub is_builtin_plugin: bool,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
@@ -674,6 +766,7 @@ pub enum EventType {
UserAction = 37,
ActionComplete = 38,
CwdChanged = 39,
+ AvailableLayoutInfo = 40,
}
impl EventType {
/// String value of the enum field names used in the ProtoBuf definition.
@@ -721,6 +814,7 @@ impl EventType {
EventType::UserAction => "UserAction",
EventType::ActionComplete => "ActionComplete",
EventType::CwdChanged => "CwdChanged",
+ EventType::AvailableLayoutInfo => "AvailableLayoutInfo",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
@@ -765,6 +859,7 @@ impl EventType {
"UserAction" => Some(Self::UserAction),
"ActionComplete" => Some(Self::ActionComplete),
"CwdChanged" => Some(Self::CwdChanged),
+ "AvailableLayoutInfo" => Some(Self::AvailableLayoutInfo),
_ => None,
}
}
diff --git a/zellij-utils/assets/prost/api.plugin_command.rs b/zellij-utils/assets/prost/api.plugin_command.rs
index 58abbc2b..7caeb247 100644
--- a/zellij-utils/assets/prost/api.plugin_command.rs
+++ b/zellij-utils/assets/prost/api.plugin_command.rs
@@ -3,7 +3,7 @@
pub struct PluginCommand {
#[prost(enumeration="CommandName", tag="1")]
pub name: i32,
- #[prost(oneof="plugin_command::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 119, 120, 121, 122, 123")]
+ #[prost(oneof="plugin_command::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134")]
pub payload: ::core::option::Option<plugin_command::Payload>,
}
/// Nested message and enum types in `PluginCommand`.
@@ -229,6 +229,28 @@ pub mod plugin_command {
SendSigkillToPaneIdPayload(super::PaneId),
#[prost(message, tag="123")]
GetPanePidPayload(super::GetPanePidPayload),
+ #[prost(message, tag="124")]
+ OverrideLayoutPayload(super::OverrideLayoutPayload),
+ #[prost(message, tag="125")]
+ SaveLayoutPayload(super::SaveLayoutPayload),
+ #[prost(message, tag="126")]
+ DeleteLayoutPayload(super::DeleteLayoutPayload),
+ #[prost(message, tag="127")]
+ EditLayoutPayload(super::EditLayoutPayload),
+ #[prost(message, tag="128")]
+ GenerateRandomNamePayload(super::GenerateRandomNamePayload),
+ #[prost(message, tag="129")]
+ DumpLayoutPayload(super::DumpLayoutPayload),
+ #[prost(message, tag="130")]
+ ParseLayoutPayload(super::ParseLayoutPayload),
+ #[prost(message, tag="131")]
+ RenameLayoutPayload(super::RenameLayoutPayload),
+ #[prost(message, tag="132")]
+ GetLayoutDirPayload(super::GetLayoutDirPayload),
+ #[prost(message, tag="133")]
+ DumpSessionLayoutPayload(super::DumpSessionLayoutPayload),
+ #[prost(message, tag="134")]
+ GetFocusedPaneInfoPayload(super::GetFocusedPaneInfoPayload),
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
@@ -241,6 +263,12 @@ pub struct NewTabPayload {
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct DumpSessionLayoutPayload {
+ #[prost(uint32, optional, tag="1")]
+ pub tab_index: ::core::option::Option<u32>,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
pub struct RunActionPayload {
#[prost(message, optional, tag="1")]
pub action: ::core::option::Option<super::action::Action>,
@@ -970,6 +998,234 @@ pub mod get_pane_pid_response {
Error(::prost::alloc::string::String),
}
}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct SaveLayoutResponse {
+ #[prost(oneof="save_layout_response::Result", tags="1, 2")]
+ pub result: ::core::option::Option<save_layout_response::Result>,
+}
+/// Nested message and enum types in `SaveLayoutResponse`.
+pub mod save_layout_response {
+ #[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Oneof)]
+ pub enum Result {
+ #[prost(bool, tag="1")]
+ Success(bool),
+ #[prost(string, tag="2")]
+ Error(::prost::alloc::string::String),
+ }
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct DeleteLayoutResponse {
+ #[prost(oneof="delete_layout_response::Result", tags="1, 2")]
+ pub result: ::core::option::Option<delete_layout_response::Result>,
+}
+/// Nested message and enum types in `DeleteLayoutResponse`.
+pub mod delete_layout_response {
+ #[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Oneof)]
+ pub enum Result {
+ #[prost(bool, tag="1")]
+ Success(bool),
+ #[prost(string, tag="2")]
+ Error(::prost::alloc::string::String),
+ }
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct EditLayoutResponse {
+ #[prost(oneof="edit_layout_response::Result", tags="1, 2")]
+ pub result: ::core::option::Option<edit_layout_response::Result>,
+}
+/// Nested message and enum types in `EditLayoutResponse`.
+pub mod edit_layout_response {
+ #[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Oneof)]
+ pub enum Result {
+ #[prost(bool, tag="1")]
+ Success(bool),
+ #[prost(string, tag="2")]
+ Error(::prost::alloc::string::String),
+ }
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct GenerateRandomNameResponse {
+ #[prost(string, tag="1")]
+ pub name: ::prost::alloc::string::String,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct OverrideLayoutPayload {
+ #[prost(message, optional, tag="1")]
+ pub layout_info: ::core::option::Option<super::event::LayoutInfo>,
+ #[prost(message, repeated, tag="2")]
+ pub context: ::prost::alloc::vec::Vec<ContextItem>,
+ #[prost(bool, tag="3")]
+ pub retain_existing_terminal_panes: bool,
+ #[prost(bool, tag="4")]
+ pub retain_existing_plugin_panes: bool,
+ #[prost(bool, tag="5")]
+ pub apply_only_to_active_tab: bool,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct SaveLayoutPayload {
+ #[prost(string, tag="1")]
+ pub layout_name: ::prost::alloc::string::String,
+ #[prost(string, tag="2")]
+ pub layout_kdl: ::prost::alloc::string::String,
+ #[prost(bool, tag="3")]
+ pub overwrite: bool,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct DeleteLayoutPayload {
+ #[prost(string, tag="1")]
+ pub layout_name: ::prost::alloc::string::String,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct EditLayoutPayload {
+ #[prost(string, tag="1")]
+ pub layout_name: ::prost::alloc::string::String,
+ #[prost(message, repeated, tag="2")]
+ pub context: ::prost::alloc::vec::Vec<ContextItem>,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct GenerateRandomNamePayload {
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct DumpLayoutPayload {
+ #[prost(string, tag="1")]
+ pub layout_name: ::prost::alloc::string::String,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct DumpLayoutResponse {
+ #[prost(oneof="dump_layout_response::Result", tags="1, 2")]
+ pub result: ::core::option::Option<dump_layout_response::Result>,
+}
+/// Nested message and enum types in `DumpLayoutResponse`.
+pub mod dump_layout_response {
+ #[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Oneof)]
+ pub enum Result {
+ #[prost(string, tag="1")]
+ LayoutContent(::prost::alloc::string::String),
+ #[prost(string, tag="2")]
+ Error(::prost::alloc::string::String),
+ }
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct DumpSessionLayoutResponse {
+ #[prost(message, optional, tag="3")]
+ pub metadata: ::core::option::Option<super::event::LayoutMetadata>,
+ #[prost(oneof="dump_session_layout_response::Result", tags="1, 2")]
+ pub result: ::core::option::Option<dump_session_layout_response::Result>,
+}
+/// Nested message and enum types in `DumpSessionLayoutResponse`.
+pub mod dump_session_layout_response {
+ #[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Oneof)]
+ pub enum Result {
+ #[prost(string, tag="1")]
+ LayoutContent(::prost::alloc::string::String),
+ #[prost(string, tag="2")]
+ Error(::prost::alloc::string::String),
+ }
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct ParseLayoutPayload {
+ #[prost(string, tag="1")]
+ pub layout_string: ::prost::alloc::string::String,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct ParseLayoutResponse {
+ #[prost(oneof="parse_layout_response::Result", tags="1, 2")]
+ pub result: ::core::option::Option<parse_layout_response::Result>,
+}
+/// Nested message and enum types in `ParseLayoutResponse`.
+pub mod parse_layout_response {
+ #[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Oneof)]
+ pub enum Result {
+ #[prost(message, tag="1")]
+ Metadata(super::super::event::LayoutMetadata),
+ #[prost(message, tag="2")]
+ Error(super::super::event::LayoutParsingError),
+ }
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct RenameLayoutPayload {
+ #[prost(string, tag="1")]
+ pub old_layout_name: ::prost::alloc::string::String,
+ #[prost(string, tag="2")]
+ pub new_layout_name: ::prost::alloc::string::String,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct RenameLayoutResponse {
+ #[prost(oneof="rename_layout_response::Result", tags="1, 2")]
+ pub result: ::core::option::Option<rename_layout_response::Result>,
+}
+/// Nested message and enum types in `RenameLayoutResponse`.
+pub mod rename_layout_response {
+ #[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Oneof)]
+ pub enum Result {
+ #[prost(bool, tag="1")]
+ Success(bool),
+ #[prost(string, tag="2")]
+ Error(::prost::alloc::string::String),
+ }
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct GetLayoutDirPayload {
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct GetLayoutDirResponse {
+ #[prost(string, tag="1")]
+ pub layout_dir: ::prost::alloc::string::String,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct GetFocusedPaneInfoPayload {
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct GetFocusedPaneInfoResponse {
+ #[prost(oneof="get_focused_pane_info_response::Result", tags="1, 2")]
+ pub result: ::core::option::Option<get_focused_pane_info_response::Result>,
+}
+/// Nested message and enum types in `GetFocusedPaneInfoResponse`.
+pub mod get_focused_pane_info_response {
+ #[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Oneof)]
+ pub enum Result {
+ #[prost(message, tag="1")]
+ FocusedPaneInfo(super::FocusedPaneInfo),
+ #[prost(string, tag="2")]
+ Error(::prost::alloc::string::String),
+ }
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct FocusedPaneInfo {
+ #[prost(uint32, tag="1")]
+ pub focused_tab_index: u32,
+ #[prost(message, optional, tag="2")]
+ pub focused_pane_id: ::core::option::Option<PaneId>,
+}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
pub enum CommandName {
@@ -1126,6 +1382,16 @@ pub enum CommandName {
SendSigintToPaneId = 171,
SendSigkillToPaneId = 172,
GetPanePid = 173,
+ OverrideLayout = 174,
+ SaveLayout = 175,
+ DeleteLayout = 176,
+ EditLayout = 177,
+ GenerateRandomName = 178,
+ DumpLayout = 179,
+ ParseLayout = 180,
+ RenameLayout = 181,
+ GetLayoutDir = 182,
+ GetFocusedPaneInfo = 183,
}
impl CommandName {
/// String value of the enum field names used in the ProtoBuf definition.
@@ -1287,6 +1553,16 @@ impl CommandName {
CommandName::SendSigintToPaneId => "SendSigintToPaneId",
CommandName::SendSigkillToPaneId => "SendSigkillToPaneId",
CommandName::GetPanePid => "GetPanePid",
+ CommandName::OverrideLayout => "OverrideLayout",
+ CommandName::SaveLayout => "SaveLayout",
+ CommandName::DeleteLayout => "DeleteLayout",
+ CommandName::EditLayout => "EditLayout",
+ CommandName::GenerateRandomName => "GenerateRandomName",
+ CommandName::DumpLayout => "DumpLayout",
+ CommandName::ParseLayout => "ParseLayout",
+ CommandName::RenameLayout => "RenameLayout",
+ CommandName::GetLayoutDir => "GetLayoutDir",
+ CommandName::GetFocusedPaneInfo => "GetFocusedPaneInfo",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
@@ -1445,6 +1721,16 @@ impl CommandName {
"SendSigintToPaneId" => Some(Self::SendSigintToPaneId),
"SendSigkillToPaneId" => Some(Self::SendSigkillToPaneId),
"GetPanePid" => Some(Self::GetPanePid),
+ "OverrideLayout" => Some(Self::OverrideLayout),
+ "SaveLayout" => Some(Self::SaveLayout),
+ "DeleteLayout" => Some(Self::DeleteLayout),
+ "EditLayout" => Some(Self::EditLayout),
+ "GenerateRandomName" => Some(Self::GenerateRandomName),
+ "DumpLayout" => Some(Self::DumpLayout),
+ "ParseLayout" => Some(Self::ParseLayout),
+ "RenameLayout" => Some(Self::RenameLayout),
+ "GetLayoutDir" => Some(Self::GetLayoutDir),
+ "GetFocusedPaneInfo" => Some(Self::GetFocusedPaneInfo),
_ => None,
}
}
diff --git a/zellij-utils/assets/prost_ipc/client_server_contract.rs b/zellij-utils/assets/prost_ipc/client_server_contract.rs
index b0a9be5b..8d3680c2 100644
--- a/zellij-utils/assets/prost_ipc/client_server_contract.rs
+++ b/zellij-utils/assets/prost_ipc/client_server_contract.rs
@@ -466,21 +466,31 @@ pub struct NextSwapLayoutAction {
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
-pub struct OverrideLayoutAction {
- #[prost(message, optional, tag="1")]
+pub struct TabLayoutInfo {
+ #[prost(uint32, tag="1")]
+ pub tab_index: u32,
+ #[prost(string, optional, tag="2")]
+ pub tab_name: ::core::option::Option<::prost::alloc::string::String>,
+ #[prost(message, optional, tag="3")]
pub tiled_layout: ::core::option::Option<TiledPaneLayout>,
- #[prost(message, repeated, tag="2")]
+ #[prost(message, repeated, tag="4")]
pub floating_layouts: ::prost::alloc::vec::Vec<FloatingPaneLayout>,
- #[prost(message, repeated, tag="3")]
+ #[prost(message, repeated, tag="5")]
pub swap_tiled_layouts: ::prost::alloc::vec::Vec<SwapTiledLayout>,
- #[prost(message, repeated, tag="4")]
+ #[prost(message, repeated, tag="6")]
pub swap_floating_layouts: ::prost::alloc::vec::Vec<SwapFloatingLayout>,
- #[prost(string, optional, tag="5")]
- pub tab_name: ::core::option::Option<::prost::alloc::string::String>,
- #[prost(bool, tag="6")]
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct OverrideLayoutAction {
+ #[prost(message, repeated, tag="1")]
+ pub tabs: ::prost::alloc::vec::Vec<TabLayoutInfo>,
+ #[prost(bool, tag="2")]
pub retain_existing_terminal_panes: bool,
- #[prost(bool, tag="7")]
+ #[prost(bool, tag="3")]
pub retain_existing_plugin_panes: bool,
+ #[prost(bool, tag="4")]
+ pub apply_only_to_active_tab: bool,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
@@ -1014,6 +1024,8 @@ pub struct CliAssets {
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct LayoutInfo {
+ #[prost(message, optional, tag="5")]
+ pub layout_metadata: ::core::option::Option<LayoutMetadata>,
#[prost(oneof="layout_info::LayoutType", tags="1, 2, 3, 4")]
pub layout_type: ::core::option::Option<layout_info::LayoutType>,
}
@@ -1034,6 +1046,34 @@ pub mod layout_info {
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct LayoutMetadata {
+ #[prost(message, repeated, tag="1")]
+ pub tabs: ::prost::alloc::vec::Vec<TabMetadata>,
+ #[prost(string, tag="2")]
+ pub creation_time: ::prost::alloc::string::String,
+ #[prost(string, tag="3")]
+ pub update_time: ::prost::alloc::string::String,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct TabMetadata {
+ #[prost(message, repeated, tag="1")]
+ pub pane_metadata: ::prost::alloc::vec::Vec<PaneMetadata>,
+ #[prost(string, optional, tag="2")]
+ pub name: ::core::option::Option<::prost::alloc::string::String>,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct PaneMetadata {
+ #[prost(string, optional, tag="1")]
+ pub name: ::core::option::Option<::prost::alloc::string::String>,
+ #[prost(bool, tag="2")]
+ pub is_plugin: bool,
+ #[prost(bool, tag="3")]
+ pub is_builtin_plugin: bool,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ConnectToSession {
#[prost(string, optional, tag="1")]
pub name: ::core::option::Option<::prost::alloc::string::String>,
diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs
index 1d196df9..d5070f2f 100644
--- a/zellij-utils/src/cli.rs
+++ b/zellij-utils/src/cli.rs
@@ -950,6 +950,11 @@ pub enum CliAction {
/// Retain existing plugin panes that do not fit with the layout default: false)
#[clap(long, value_parser, takes_value(false), default_value("false"))]
retain_existing_plugin_panes: bool,
+
+ /// Only apply the layout to the active tab (uses just the first layout tab if it has
+ /// multiple)
+ #[clap(long, value_parser, takes_value(false), default_value("false"))]
+ apply_only_to_active_tab: bool,
},
/// Query all tab names
QueryTabNames,
diff --git a/zellij-utils/src/client_server_contract/common_types.proto b/zellij-utils/src/client_server_contract/common_types.proto
index bbaeadad..3435718f 100644
--- a/zellij-utils/src/client_server_contract/common_types.proto
+++ b/zellij-utils/src/client_server_contract/common_types.proto
@@ -272,14 +272,20 @@ message DenyAction {}
message ToggleMouseModeAction {}
message PreviousSwapLayoutAction {}
message NextSwapLayoutAction {}
+message TabLayoutInfo {
+ uint32 tab_index = 1;
+ optional string tab_name = 2;
+ TiledPaneLayout tiled_layout = 3;
+ repeated FloatingPaneLayout floating_layouts = 4;
+ repeated SwapTiledLayout swap_tiled_layouts = 5;
+ repeated SwapFloatingLayout swap_floating_layouts = 6;
+}
+
message OverrideLayoutAction {
- optional TiledPaneLayout tiled_layout = 1;
- repeated FloatingPaneLayout floating_layouts = 2;
- repeated SwapTiledLayout swap_tiled_layouts = 3;
- repeated SwapFloatingLayout swap_floating_layouts = 4;
- optional string tab_name = 5;
- bool retain_existing_terminal_panes = 6;
- bool retain_existing_plugin_panes = 7;
+ repeated TabLayoutInfo tabs = 1;
+ bool retain_existing_terminal_panes = 2;
+ bool retain_existing_plugin_panes = 3;
+ bool apply_only_to_active_tab = 4;
}
message QueryTabNamesAction {}
message BreakPaneAction {}
@@ -635,6 +641,25 @@ message LayoutInfo {
string url = 3;
string stringified = 4;
}
+ optional LayoutMetadata layout_metadata = 5;
+}
+
+message LayoutMetadata {
+ repeated TabMetadata tabs = 1;
+ string creation_time = 2;
+ string update_time = 3;
+
+}
+
+message TabMetadata {
+ repeated PaneMetadata pane_metadata = 1;
+ optional string name = 2;
+}
+
+message PaneMetadata {
+ optional string name = 1;
+ bool is_plugin = 2;
+ bool is_builtin_plugin = 3;
}
message ConnectToSession {
diff --git a/zellij-utils/src/consts.rs b/zellij-utils/src/consts.rs
index e5b93e1a..3faab0c9 100644
--- a/zellij-utils/src/consts.rs
+++ b/zellij-utils/src/consts.rs
@@ -139,6 +139,7 @@ mod not_wasm {
add_plugin!(assets, "share.wasm");
add_plugin!(assets, "multiple-select.wasm");
add_plugin!(assets, "sequence.wasm");
+ add_plugin!(assets, "layout-manager.wasm");
assets
};
}
diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs
index 4a29375a..177d8c7b 100644
--- a/zellij-utils/src/data.rs
+++ b/zellij-utils/src/data.rs
@@ -1,8 +1,10 @@
use crate::home::default_layout_dir;
use crate::input::actions::{Action, RunCommandAction};
-use crate::input::config::ConversionError;
+use crate::input::config::{ConversionError, KdlError};
use crate::input::keybinds::Keybinds;
-use crate::input::layout::{RunPlugin, RunPluginOrAlias, SplitSize};
+use crate::input::layout::{
+ Layout, Run, RunPlugin, RunPluginLocation, RunPluginOrAlias, SplitSize,
+};
use crate::pane_size::PaneGeom;
use crate::position::Position;
use crate::shared::{colors as default_colors, eightbit_to_rgb};
@@ -11,6 +13,7 @@ use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::fmt;
use std::fs::Metadata;
+use std::hash::{Hash, Hasher};
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use std::str::{self, FromStr};
@@ -612,6 +615,17 @@ impl KeyWithModifier {
}
true
}
+ pub fn has_only_modifiers(&self, modifiers: &[KeyModifier]) -> bool {
+ for modifier in modifiers {
+ if !self.key_modifiers.contains(modifier) {
+ return false;
+ }
+ }
+ if self.key_modifiers.len() != modifiers.len() {
+ return false;
+ }
+ true
+ }
}
#[derive(Eq, Clone, Copy, Debug, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)]
@@ -996,6 +1010,7 @@ pub enum Event {
PaneRenderReport(HashMap<PaneId, PaneContents>),
ActionComplete(Action, Option<PaneId>, BTreeMap<String, String>), // Action, pane_id, context
CwdChanged(PaneId, PathBuf, Vec<ClientId>), // pane_id, cwd, focused_client_ids
+ AvailableLayoutInfo(Vec<LayoutInfo>, Vec<LayoutWithError>),
}
#[derive(Debug, Clone, PartialEq, Eq, EnumDiscriminants, Display, Serialize, Deserialize)]
@@ -1717,16 +1732,266 @@ impl From<RunPlugin> for PluginInfo {
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub enum LayoutInfo {
BuiltIn(String),
- File(String),
+ File(String, LayoutMetadata),
Url(String),
Stringified(String),
}
+#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
+pub struct LayoutWithError {
+ pub layout_name: String,
+ pub error: LayoutParsingError,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
+pub enum LayoutParsingError {
+ KdlError {
+ kdl_error: KdlError,
+ file_name: String,
+ source_code: String,
+ },
+ SyntaxError,
+}
+
+impl AsRef<LayoutInfo> for LayoutInfo {
+ fn as_ref(&self) -> &LayoutInfo {
+ self
+ }
+}
+
+#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
+pub struct LayoutMetadata {
+ pub tabs: Vec<TabMetadata>,
+ pub creation_time: String,
+ pub update_time: String,
+}
+
+impl From<&PathBuf> for LayoutMetadata {
+ fn from(path: &PathBuf) -> LayoutMetadata {
+ match Layout::stringified_from_path(path) {
+ Ok((path_str, stringified_layout, _swap_layouts)) => {
+ match Layout::from_kdl(&stringified_layout, Some(path_str), None, None) {
+ Ok(layout) => {
+ let layout_tabs = layout.tabs();
+ let tabs = if layout_tabs.is_empty() {
+ let (tiled_pane_layout, floating_pane_layout) = layout.new_tab();
+ vec![TabMetadata::from(&(
+ None,
+ tiled_pane_layout,
+ floating_pane_layout,
+ ))]
+ } else {
+ layout
+ .tabs()
+ .into_iter()
+ .map(|tab| TabMetadata::from(&tab))
+ .collect()
+ };
+
+ // Get file metadata for creation and modification times as Unix epochs
+ let (creation_time, update_time) =
+ LayoutMetadata::creation_and_update_times(&path);
+
+ LayoutMetadata {
+ tabs,
+ creation_time,
+ update_time,
+ }
+ },
+ Err(e) => {
+ log::error!("Failed to parse layout: {}", e);
+ LayoutMetadata::default()
+ },
+ }
+ },
+ Err(e) => {
+ log::error!("Failed to read layout file: {}", e);
+ LayoutMetadata::default()
+ },
+ }
+ }
+}
+
+impl LayoutMetadata {
+ fn creation_and_update_times(path: &PathBuf) -> (String, String) {
+ // (creation_time, update_time) returns stringified unix epoch
+ match std::fs::metadata(path) {
+ Ok(metadata) => {
+ let creation_time = metadata
+ .created()
+ .ok()
+ .and_then(|t| {
+ t.duration_since(std::time::UNIX_EPOCH)
+ .ok()
+ .map(|d| d.as_secs().to_string())
+ })
+ .unwrap_or_default();
+
+ let update_time = metadata
+ .modified()
+ .ok()
+ .and_then(|t| {
+ t.duration_since(std::time::UNIX_EPOCH)
+ .ok()
+ .map(|d| d.as_secs().to_string())
+ })
+ .unwrap_or_default();
+
+ (creation_time, update_time)
+ },
+ Err(_) => (String::new(), String::new()),
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
+pub struct TabMetadata {
+ pub panes: Vec<PaneMetadata>,
+ pub name: Option<String>,
+}
+
+impl
+ From<&(
+ Option<String>,
+ crate::input::layout::TiledPaneLayout,
+ Vec<crate::input::layout::FloatingPaneLayout>,
+ )> for TabMetadata
+{
+ fn from(
+ tab: &(
+ Option<String>,
+ crate::input::layout::TiledPaneLayout,
+ Vec<crate::input::layout::FloatingPaneLayout>,
+ ),
+ ) -> Self {
+ let (tab_name, tiled_pane_layout, floating_panes) = tab;
+
+ // Collect panes from tiled layout (only leaf nodes are real panes)
+ let mut panes = Vec::new();
+ collect_leaf_panes(&tiled_pane_layout, &mut panes);
+
+ // Collect panes from floating panes
+ for floating_pane in floating_panes {
+ panes.push(PaneMetadata::from(floating_pane));
+ }
+
+ TabMetadata {
+ panes,
+ name: tab_name.clone(),
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
+pub struct PaneMetadata {
+ pub name: Option<String>,
+ pub is_plugin: bool,
+ pub is_builtin_plugin: bool,
+}
+
+impl From<&crate::input::layout::TiledPaneLayout> for PaneMetadata {
+ fn from(pane: &crate::input::layout::TiledPaneLayout) -> Self {
+ let mut is_plugin = false;
+ let mut is_builtin_plugin = false;
+
+ // Try to get the name from the pane's name field first
+ let name = if let Some(ref name) = pane.name {
+ Some(name.clone())
+ } else if let Some(ref run) = pane.run {
+ // If no explicit name, glean it from the run configuration
+ match run {
+ Run::Command(cmd) => {
+ // Use the command name
+ Some(cmd.command.to_string_lossy().to_string())
+ },
+ Run::EditFile(path, _line, _cwd) => {
+ // Use the file name
+ path.file_name().map(|n| n.to_string_lossy().to_string())
+ },
+ Run::Plugin(plugin) => {
+ is_plugin = true;
+ is_builtin_plugin = plugin.is_builtin_plugin();
+ Some(plugin.location_string())
+ },
+ Run::Cwd(_) => None,
+ }
+ } else {
+ None
+ };
+
+ PaneMetadata {
+ name,
+ is_plugin,
+ is_builtin_plugin,
+ }
+ }
+}
+
+impl From<&crate::input::layout::FloatingPaneLayout> for PaneMetadata {
+ fn from(pane: &crate::input::layout::FloatingPaneLayout) -> Self {
+ let mut is_plugin = false;
+ let mut is_builtin_plugin = false;
+
+ // Try to get the name from the pane's name field first
+ let name = if let Some(ref name) = pane.name {
+ Some(name.clone())
+ } else if let Some(ref run) = pane.run {
+ // If no explicit name, glean it from the run configuration
+ match run {
+ Run::Command(cmd) => {
+ // Use the command name
+ Some(cmd.command.to_string_lossy().to_string())
+ },
+ Run::EditFile(path, _line, _cwd) => {
+ // Use the file name
+ path.file_name().map(|n| n.to_string_lossy().to_string())
+ },
+ Run::Plugin(plugin) => {
+ is_plugin = true;
+ is_builtin_plugin = match plugin {
+ crate::input::layout::RunPluginOrAlias::RunPlugin(run_plugin) => {
+ matches!(run_plugin.location, RunPluginLocation::Zellij(_))
+ },
+ crate::input::layout::RunPluginOrAlias::Alias(_) => false,
+ };
+ // Use the plugin location string
+ Some(plugin.location_string())
+ },
+ Run::Cwd(_) => None,
+ }
+ } else {
+ None
+ };
+
+ PaneMetadata {
+ name,
+ is_plugin,
+ is_builtin_plugin,
+ }
+ }
+}
+
+// Helper function to recursively collect leaf panes from TiledPaneLayout
+fn collect_leaf_panes(
+ pane: &crate::input::layout::TiledPaneLayout,
+ result: &mut Vec<PaneMetadata>,
+) {
+ if pane.children.is_empty() {
+ // This is a leaf node (actual pane)
+ result.push(PaneMetadata::from(pane));
+ } else {
+ // This is a container, recurse into children
+ for child in &pane.children {
+ collect_leaf_panes(child, result);
+ }
+ }
+}
+
impl LayoutInfo {
pub fn name(&self) -> &str {
match self {
LayoutInfo::BuiltIn(name) => &name,
- LayoutInfo::File(name) => &name,
+ LayoutInfo::File(name, _) => &name,
LayoutInfo::Url(url) => &url,
LayoutInfo::Stringified(layout) => &layout,
}
@@ -1734,7 +1999,7 @@ impl LayoutInfo {
pub fn is_builtin(&self) -> bool {
match self {
LayoutInfo::BuiltIn(_name) => true,
- LayoutInfo::File(_name) => false,
+ LayoutInfo::File(_name, _) => false,
LayoutInfo::Url(_url) => false,
LayoutInfo::Stringified(_stringified) => false,
}
@@ -1753,8 +2018,11 @@ impl LayoutInfo {
else {
return None;
};
+ let file_path = layout_dir.join(layout_path);
Some(LayoutInfo::File(
- layout_dir.join(layout_path).display().to_string(),
+ // layout_dir.join(layout_path).display().to_string(),
+ file_path.display().to_string(),
+ LayoutMetadata::from(&file_path),
))
} else if layout_path.starts_with("http://") || layout_path.starts_with("https://")
{
@@ -1768,8 +2036,6 @@ impl LayoutInfo {
}
}
-use std::hash::{Hash, Hasher};
-
#[allow(clippy::derive_hash_xor_eq)]
impl Hash for SessionInfo {
fn hash<H: Hasher>(&self, state: &mut H) {
@@ -2113,6 +2379,36 @@ pub enum GetPanePidResponse {
Err(String),
}
+#[derive(Debug, Clone, PartialEq)]
+pub enum GetFocusedPaneInfoResponse {
+ Ok { tab_index: usize, pane_id: PaneId },
+ Err(String),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum SaveLayoutResponse {
+ Ok(()),
+ Err(String),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum DeleteLayoutResponse {
+ Ok(()),
+ Err(String),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum RenameLayoutResponse {
+ Ok(()),
+ Err(String),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum EditLayoutResponse {
+ Ok(()),
+ Err(String),
+}
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct SelectedText {
pub start: Position,
@@ -2391,7 +2687,7 @@ pub struct ConnectToSession {
impl ConnectToSession {
pub fn apply_layout_dir(&mut self, layout_dir: &PathBuf) {
- if let Some(LayoutInfo::File(file_path)) = self.layout.as_mut() {
+ if let Some(LayoutInfo::File(file_path, _layout_metadata)) = self.layout.as_mut() {
*file_path = Path::join(layout_dir, &file_path)
.to_string_lossy()
.to_string();
@@ -2829,7 +3125,9 @@ pub enum PluginCommand {
KillSessions(Vec<String>), // one or more session names
ScanHostFolder(PathBuf), // TODO: rename to ScanHostFolder
WatchFilesystem,
- DumpSessionLayout,
+ DumpSessionLayout {
+ tab_index: Option<usize>,
+ },
CloseSelf,
NewTabsWithLayoutInfo(LayoutInfo),
Reconfigure(String, bool), // String -> stringified configuration, bool -> save configuration
@@ -2920,4 +3218,32 @@ pub enum PluginCommand {
// suppress_replaced_pane)
RunAction(Action, BTreeMap<String, String>),
CopyToClipboard(String), // text to copy
+ OverrideLayout(
+ LayoutInfo,
+ bool, // retain_existing_terminal_panes
+ bool, // retain_existing_plugin_panes
+ bool, // apply_only_to_active_tab,
+ BTreeMap<String, String>, // context
+ ),
+ SaveLayout {
+ layout_name: String,
+ layout_kdl: String,
+ overwrite: bool,
+ },
+ DeleteLayout {
+ layout_name: String,
+ },
+ RenameLayout {
+ old_layout_name: String,
+ new_layout_name: String,
+ },
+ EditLayout {
+ layout_name: String,
+ context: Context,
+ },
+ GenerateRandomName,
+ DumpLayout(String),
+ ParseLayout(String), // String contains raw KDL layout
+ GetLayoutDir,
+ GetFocusedPaneInfo,
}
diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs
index 78330d76..06f69e06 100644
--- a/zellij-utils/src/errors.rs
+++ b/zellij-utils/src/errors.rs
@@ -353,6 +353,7 @@ pub enum ScreenContext {
SerializeLayoutForResurrection,
RenameSession,
DumpLayoutToPlugin,
+ GetFocusedPaneInfo,
ListClientsMetadata,
Reconfigure,
RerunCommandPane,
@@ -469,6 +470,8 @@ pub enum PluginContext {
FailedToStartWebServer,
PaneRenderReport,
UserInput,
+ LayoutListUpdate,
+ RequestStateUpdateForPlugin,
}
/// Stack call representations corresponding to the different types of [`ClientInstruction`]s.
diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs
index 42ec2037..caeadf4d 100644
--- a/zellij-utils/src/input/actions.rs
+++ b/zellij-utils/src/input/actions.rs
@@ -3,7 +3,7 @@
pub use super::command::{OpenFilePayload, RunCommandAction};
use super::layout::{
FloatingPaneLayout, Layout, PluginAlias, RunPlugin, RunPluginLocation, RunPluginOrAlias,
- SwapFloatingLayout, SwapTiledLayout, TiledPaneLayout,
+ SwapFloatingLayout, SwapTiledLayout, TabLayoutInfo, TiledPaneLayout,
};
use crate::cli::CliAction;
use crate::data::{
@@ -351,13 +351,10 @@ pub enum Action {
NextSwapLayout,
/// Override the layout of the active tab
OverrideLayout {
- tiled_layout: Option<TiledPaneLayout>,
- floating_layouts: Vec<FloatingPaneLayout>,
- swap_tiled_layouts: Option<Vec<SwapTiledLayout>>,
- swap_floating_layouts: Option<Vec<SwapFloatingLayout>>,
- tab_name: Option<String>,
+ tabs: Vec<TabLayoutInfo>,
retain_existing_terminal_panes: bool,
retain_existing_plugin_panes: bool,
+ apply_only_to_active_tab: bool,
},
/// Query all tab names
QueryTabNames,
@@ -989,6 +986,7 @@ impl Action {
layout_dir,
retain_existing_terminal_panes,
retain_existing_plugin_panes,
+ apply_only_to_active_tab,
} => {
// Determine layout_dir: CLI arg > config > default
let layout_dir = layout_dir
@@ -1041,31 +1039,41 @@ impl Action {
stringified_error
})?;
- // Extract layout from first tab or use default
- let swap_tiled_layouts = Some(layout.swap_tiled_layouts.clone());
- let swap_floating_layouts = Some(layout.swap_floating_layouts.clone());
-
- if layout.tabs.len() > 1 {
- return Err(format!("This layout has {} tabs, overriding only supports layouts with 0 or 1 tabs.", layout.tabs.len()));
- }
+ // Convert all tabs to Vec<TabLayoutInfo>
+ let tabs: Vec<TabLayoutInfo> = layout
+ .tabs
+ .iter()
+ .enumerate()
+ .map(|(index, (tab_name, tiled, floating))| TabLayoutInfo {
+ tab_index: index,
+ tab_name: tab_name.clone(),
+ tiled_layout: tiled.clone(),
+ floating_layouts: floating.clone(),
+ swap_tiled_layouts: Some(layout.swap_tiled_layouts.clone()),
+ swap_floating_layouts: Some(layout.swap_floating_layouts.clone()),
+ })
+ .collect();
- let (name, tiled_layout, floating_panes_layout) =
- if let Some(first_tab) = layout.tabs.get(0).take() {
- first_tab.clone()
- } else {
- let name = None;
- let (tiled_panes, floating_panes) = layout.new_tab();
- (name, tiled_panes, floating_panes)
- };
+ // If no tabs, create default tab
+ let tabs = if tabs.is_empty() {
+ let (tiled, floating) = layout.new_tab();
+ vec![TabLayoutInfo {
+ tab_index: 0,
+ tab_name: None,
+ tiled_layout: tiled,
+ floating_layouts: floating,
+ swap_tiled_layouts: Some(layout.swap_tiled_layouts),
+ swap_floating_layouts: Some(layout.swap_floating_layouts),
+ }]
+ } else {
+ tabs
+ };
Ok(vec![Action::OverrideLayout {
- tiled_layout: Some(tiled_layout),
- floating_layouts: floating_panes_layout,
- swap_tiled_layouts,
- swap_floating_layouts,
- tab_name: name,
+ tabs,
retain_existing_terminal_panes,
retain_existing_plugin_panes,
+ apply_only_to_active_tab,
}])
},
CliAction::QueryTabNames => Ok(vec![Action::QueryTabNames]),
diff --git a/zellij-utils/src/input/config.rs b/zellij-utils/src/input/config.rs
index 1f407621..9882b8e6 100644
--- a/zellij-utils/src/input/config.rs
+++ b/zellij-utils/src/input/config.rs
@@ -1,4 +1,8 @@
use crate::data::Styling;
+
+#[cfg(not(target_family = "wasm"))]
+use crate::data::{LayoutInfo, LayoutWithError};
+
use miette::{Diagnostic, LabeledSpan, NamedSource, SourceCode};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
@@ -36,15 +40,40 @@ pub struct Config {
pub web_client: WebClientConfig,
}
-#[derive(Error, Debug)]
+#[derive(Error, Debug, Serialize, Deserialize)]
pub struct KdlError {
pub error_message: String,
+ #[serde(skip)]
pub src: Option<NamedSource>,
pub offset: Option<usize>,
pub len: Option<usize>,
pub help_message: Option<String>,
}
+impl Clone for KdlError {
+ fn clone(&self) -> Self {
+ KdlError {
+ error_message: self.error_message.clone(),
+ src: None, // NamedSource doesn't implement Clone, so we skip it
+ offset: self.offset,
+ len: self.len,
+ help_message: self.help_message.clone(),
+ }
+ }
+}
+
+impl PartialEq for KdlError {
+ fn eq(&self, other: &Self) -> bool {
+ // Compare everything except src (which doesn't implement PartialEq)
+ self.error_message == other.error_message
+ && self.offset == other.offset
+ && self.len == other.len
+ && self.help_message == other.help_message
+ }
+}
+
+impl Eq for KdlError {}
+
impl KdlError {
pub fn add_src(mut self, src_name: String, src_input: String) -> Self {
self.src = Some(NamedSource::new(src_name, src_input));
@@ -482,6 +511,72 @@ where
}
}
+#[cfg(not(target_family = "wasm"))]
+pub async fn watch_layout_dir_changes<F, Fut>(
+ layout_dir: PathBuf,
+ default_layout_name: Option<String>,
+ on_layout_change: F,
+) where
+ F: Fn(Vec<LayoutInfo>, Vec<LayoutWithError>) -> Fut + Send + 'static,
+ Fut: std::future::Future<Output = ()> + Send,
+{
+ use crate::input::layout::Layout;
+ use notify::{self, Config as WatcherConfig, Event, PollWatcher, RecursiveMode, Watcher};
+ use std::time::Duration;
+ use tokio::sync::mpsc;
+
+ loop {
+ if layout_dir.exists() {
+ let (tx, mut rx) = mpsc::unbounded_channel();
+
+ let mut watcher = match PollWatcher::new(
+ move |res: Result<Event, notify::Error>| {
+ let _ = tx.send(res);
+ },
+ WatcherConfig::default().with_poll_interval(Duration::from_secs(1)),
+ ) {
+ Ok(watcher) => watcher,
+ Err(_) => break,
+ };
+
+ if watcher
+ .watch(&layout_dir, RecursiveMode::Recursive)
+ .is_err()
+ {
+ break;
+ }
+
+ while let Some(event_result) = rx.recv().await {
+ match event_result {
+ Ok(event) => {
+ if event.kind.is_remove()
+ || event.kind.is_create()
+ || event.kind.is_modify()
+ {
+ tokio::time::sleep(Duration::from_millis(100)).await;
+
+ if !layout_dir.exists() {
+ break;
+ }
+
+ let (layouts, layout_errors) = Layout::list_available_layouts(
+ Some(layout_dir.clone()),
+ &default_layout_name,
+ );
+ on_layout_change(layouts, layout_errors).await;
+ }
+ },
+ Err(_) => break,
+ }
+ }
+ }
+
+ while !layout_dir.exists() {
+ tokio::time::sleep(Duration::from_secs(3)).await;
+ }
+ }
+}
+
#[cfg(test)]
mod config_test {
use super::*;
diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs
index d925f7c5..8ba81159 100644
--- a/zellij-utils/src/input/layout.rs
+++ b/zellij-utils/src/input/layout.rs
@@ -11,7 +11,7 @@
#[cfg(not(target_family = "wasm"))]
use crate::downloader::Downloader;
use crate::{
- data::{Direction, LayoutInfo},
+ data::{Direction, LayoutInfo, LayoutMetadata, LayoutParsingError, LayoutWithError},
home::{default_layout_dir, find_default_config_dir},
input::{
command::RunCommand,
@@ -229,6 +229,11 @@ impl RunPluginOrAlias {
},
}
}
+ pub fn is_builtin_plugin(&self) -> bool {
+ self.get_run_plugin()
+ .map(|r| matches!(r.location, RunPluginLocation::Zellij(_)))
+ .unwrap_or(false)
+ }
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
@@ -681,6 +686,17 @@ pub struct Layout {
pub swap_floating_layouts: Vec<SwapFloatingLayout>,
}
+/// Layout configuration for a single tab in multi-tab override
+#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
+pub struct TabLayoutInfo {
+ pub tab_index: usize,
+ pub tab_name: Option<String>,
+ pub tiled_layout: TiledPaneLayout,
+ pub floating_layouts: Vec<FloatingPaneLayout>,
+ pub swap_tiled_layouts: Option<Vec<SwapTiledLayout>>,
+ pub swap_floating_layouts: Option<Vec<SwapFloatingLayout>>,
+}
+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub enum PercentOrFixed {
Percent(usize), // 1 to 100
@@ -1189,39 +1205,70 @@ impl Layout {
pub fn list_available_layouts(
layout_dir: Option<PathBuf>,
default_layout_name: &Option<String>,
- ) -> Vec<LayoutInfo> {
- let mut available_layouts = layout_dir
+ ) -> (Vec<LayoutInfo>, Vec<LayoutWithError>) {
+ let (mut available_layouts, layouts_with_errors) = layout_dir
.clone()
.or_else(|| default_layout_dir())
- .and_then(|layout_dir| match std::fs::read_dir(layout_dir) {
- Ok(layout_files) => Some(layout_files),
+ .and_then(|layout_dir| match std::fs::read_dir(&layout_dir) {
+ Ok(layout_files) => Some((layout_files, layout_dir)),
Err(_) => None,
})
- .map(|layout_files| {
+ .map(|(layout_files, layout_dir)| {
let mut available_layouts = vec![];
+ let mut layouts_with_errors = vec![];
for file in layout_files {
if let Ok(file) = file {
if file.path().extension().map(|e| e.to_ascii_lowercase())
== Some(std::ffi::OsString::from("kdl"))
{
- if Layout::from_path_or_default_without_config(
+ let layout_name = file
+ .path()
+ .file_stem()
+ .and_then(|f| f.to_str())
+ .map(|f| f.to_string())
+ .unwrap_or_default();
+
+ match Layout::from_path_or_default_without_config(
Some(&file.path()),
- layout_dir.clone(),
- )
- .is_ok()
- {
- if let Some(file_name) = file.path().file_stem() {
+ Some(layout_dir.clone()),
+ ) {
+ Ok(_layout) => {
+ let file_path = layout_dir.join(file.path()); // TODO: do we
+ // need
+ // file_stem()
+ // here too?
available_layouts.push(LayoutInfo::File(
- file_name.to_string_lossy().to_string(),
+ layout_name,
+ LayoutMetadata::from(&file_path),
))
- }
+ },
+ Err(config_error) => {
+ let file_path = file.path();
+ let file_name =
+ file_path.to_str().unwrap_or("unknown").to_string();
+ let source_code =
+ std::fs::read_to_string(&file_path).unwrap_or_default();
+
+ let error = match config_error {
+ ConfigError::KdlError(kdl_err) => {
+ LayoutParsingError::KdlError {
+ kdl_error: kdl_err,
+ file_name,
+ source_code,
+ }
+ },
+ _ => LayoutParsingError::SyntaxError,
+ };
+ layouts_with_errors
+ .push(LayoutWithError { layout_name, error });
+ },
}
}
}
}
- available_layouts
+ (available_layouts, layouts_with_errors)
})
- .unwrap_or_else(Default::default);
+ .unwrap_or_else(|| (Default::default(), Default::default()));
let default_layout_name = default_layout_name
.as_ref()
.map(|d| d.as_str())
@@ -1242,7 +1289,7 @@ impl Layout {
a_name.cmp(&b_name)
}
});
- available_layouts
+ (available_layouts, layouts_with_errors)
}
pub fn from_layout_info(
layout_dir: &Option<PathBuf>,
@@ -1250,7 +1297,7 @@ impl Layout {
) -> Result<Layout, ConfigError> {
let mut should_start_layout_commands_suspended = false;
let (path_to_raw_layout, raw_layout, raw_swap_layouts) = match layout_info {
- LayoutInfo::File(layout_name_without_extension) => {
+ LayoutInfo::File(layout_name_without_extension, _layout_metadata) => {
let layout_dir = layout_dir.clone().or_else(|| default_layout_dir());
let (path_to_layout, stringified_layout, swap_layouts) =
Self::stringified_from_dir(
@@ -1293,7 +1340,7 @@ impl Layout {
) -> Result<(Layout, Config), ConfigError> {
let mut should_start_layout_commands_suspended = false;
let (path_to_raw_layout, raw_layout, raw_swap_layouts) = match layout_info {
- LayoutInfo::File(layout_name_without_extension) => {
+ LayoutInfo::File(layout_name_without_extension, _layout_metadata) => {
let layout_dir = layout_dir.clone().or_else(|| default_layout_dir());
let (path_to_layout, stringified_layout, swap_layouts) =
Self::stringified_from_dir(
@@ -1371,6 +1418,19 @@ impl Layout {
let raw_layout = String::new();
Ok(raw_layout)
}
+ pub fn from_path_without_config(layout_path: &PathBuf) -> Result<Layout, ConfigError> {
+ // (path_to_layout as String, stringified_layout, Option<path_to_swap_layout as String, stringified_swap_layout>)
+ let (path_to_layout, raw_layout, raw_swap_layouts) =
+ Layout::stringified_from_path(layout_path)?;
+ Layout::from_kdl(
+ &raw_layout,
+ Some(path_to_layout),
+ raw_swap_layouts
+ .as_ref()
+ .map(|(r, f)| (r.as_str(), f.as_str())),
+ None,
+ )
+ }
pub fn from_path_or_default(
layout_path: Option<&PathBuf>,
layout_dir: Option<PathBuf>,
diff --git a/zellij-utils/src/input/plugins.rs b/zellij-utils/src/input/plugins.rs
index 892c7a66..fb63a786 100644
--- a/zellij-utils/src/input/plugins.rs
+++ b/zellij-utils/src/input/plugins.rs
@@ -68,6 +68,7 @@ impl PluginConfig {
|| tag == "share"
|| tag == "multiple-select"
|| tag == "sequence"
+ || tag == "layout-manager"
{
Some(PluginConfig {
path: PathBuf::from(&tag),
diff --git a/zellij-utils/src/ipc/protobuf_conversion.rs b/zellij-utils/src/ipc/protobuf_conversion.rs
index e803f51e..4ade64b6 100644
--- a/zellij-utils/src/ipc/protobuf_conversion.rs
+++ b/zellij-utils/src/ipc/protobuf_conversion.rs
@@ -5,9 +5,10 @@ use crate::{
ClientToServerMsg as ProtoClientToServerMsg, ColorRegistersMsg, ConfigFileUpdatedMsg,
ConnStatusMsg, ConnectedMsg, DetachSessionMsg, ExitMsg, ExitReason as ProtoExitReason,
FailedToStartWebServerMsg, FirstClientConnectedMsg, ForegroundColorMsg,
- InputMode as ProtoInputMode, KeyMsg, KillSessionMsg, LogErrorMsg, LogMsg,
- QueryTerminalSizeMsg, RenamedSessionMsg, RenderMsg,
- ServerToClientMsg as ProtoServerToClientMsg, StartWebServerMsg, SwitchSessionMsg,
+ InputMode as ProtoInputMode, KeyMsg, KillSessionMsg, LayoutMetadata as ProtoLayoutMetadata,
+ LogErrorMsg, LogMsg, PaneMetadata as ProtoPaneMetadata, QueryTerminalSizeMsg,
+ RenamedSessionMsg, RenderMsg, ServerToClientMsg as ProtoServerToClientMsg,
+ StartWebServerMsg, SwitchSessionMsg, TabMetadata as ProtoTabMetadata,
TerminalPixelDimensionsMsg, TerminalResizeMsg, UnblockCliPipeInputMsg,
UnblockInputThreadMsg, WebServerStartedMsg,
},
@@ -1098,25 +1099,15 @@ impl From<crate::input::actions::Action>
ActionType::NextSwapLayout(NextSwapLayoutAction {})
},
crate::input::actions::Action::OverrideLayout {
- tiled_layout,
- floating_layouts,
- swap_tiled_layouts,
- swap_floating_layouts,
- tab_name,
+ tabs,
retain_existing_terminal_panes,
retain_existing_plugin_panes,
+ apply_only_to_active_tab,
} => ActionType::OverrideLayout(OverrideLayoutAction {
- tiled_layout: tiled_layout.map(|l| l.into()),
- floating_layouts: floating_layouts.into_iter().map(|l| l.into()).collect(),
- swap_tiled_layouts: swap_tiled_layouts
- .map(|layouts| layouts.into_iter().map(|l| l.into()).collect())
- .unwrap_or_default(),
- swap_floating_layouts: swap_floating_layouts
- .map(|layouts| layouts.into_iter().map(|l| l.into()).collect())
- .unwrap_or_default(),
- tab_name,
+ tabs: tabs.into_iter().map(|t| t.into()).collect(),
retain_existing_terminal_panes,
retain_existing_plugin_panes,
+ apply_only_to_active_tab,
}),
crate::input::actions::Action::QueryTabNames => {
ActionType::QueryTabNames(QueryTabNamesAction {})
@@ -1695,45 +1686,16 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Action>
ActionType::NextSwapLayout(_) => Ok(crate::input::actions::Action::NextSwapLayout),
ActionType::OverrideLayout(override_layout_action) => {
Ok(crate::input::actions::Action::OverrideLayout {
- tiled_layout: override_layout_action
- .tiled_layout
- .map(|l| l.try_into())
- .transpose()?,
- floating_layouts: override_layout_action
- .floating_layouts
+ tabs: override_layout_action
+ .tabs
.into_iter()
- .map(|l| l.try_into())
+ .map(|t| t.try_into())
.collect::<Result<Vec<_>>>()?,
- swap_tiled_layouts: if override_layout_action.swap_tiled_layouts.is_empty() {
- None
- } else {
- Some(
- override_layout_action
- .swap_tiled_layouts
- .into_iter()
- .map(|l| l.try_into())
- .collect::<Result<Vec<_>>>()?,
- )
- },
- swap_floating_layouts: if override_layout_action
- .swap_floating_layouts
- .is_empty()
- {
- None
- } else {
- Some(
- override_layout_action
- .swap_floating_layouts
- .into_iter()
- .map(|l| l.try_into())
- .collect::<Result<Vec<_>>>()?,
- )
- },
- tab_name: override_layout_action.tab_name,
retain_existing_terminal_panes: override_layout_action
.retain_existing_terminal_panes,
retain_existing_plugin_panes: override_layout_action
.retain_existing_plugin_panes,
+ apply_only_to_active_tab: override_layout_action.apply_only_to_active_tab,
})
},
ActionType::QueryTabNames(_) => Ok(crate::input::actions::Action::QueryTabNames),
@@ -2031,14 +1993,19 @@ impl From<crate::data::LayoutInfo>
{
fn from(layout: crate::data::LayoutInfo) -> Self {
use crate::client_server_contract::client_server_contract::layout_info::LayoutType;
- let layout_type = match layout {
- crate::data::LayoutInfo::BuiltIn(name) => LayoutType::BuiltinName(name),
- crate::data::LayoutInfo::File(path) => LayoutType::FilePath(path),
- crate::data::LayoutInfo::Url(url) => LayoutType::Url(url),
- crate::data::LayoutInfo::Stringified(content) => LayoutType::Stringified(content),
+ let (layout_type, layout_metadata) = match layout {
+ crate::data::LayoutInfo::BuiltIn(name) => (LayoutType::BuiltinName(name), None),
+ crate::data::LayoutInfo::File(path, metadata) => {
+ (LayoutType::FilePath(path), Some(metadata.into()))
+ },
+ crate::data::LayoutInfo::Url(url) => (LayoutType::Url(url), None),
+ crate::data::LayoutInfo::Stringified(content) => {
+ (LayoutType::Stringified(content), None)
+ },
};
Self {
layout_type: Some(layout_type),
+ layout_metadata,
}
}
}
@@ -2053,7 +2020,14 @@ impl TryFrom<crate::client_server_contract::client_server_contract::LayoutInfo>
use crate::client_server_contract::client_server_contract::layout_info::LayoutType;
match layout.layout_type {
Some(LayoutType::BuiltinName(name)) => Ok(crate::data::LayoutInfo::BuiltIn(name)),
- Some(LayoutType::FilePath(path)) => Ok(crate::data::LayoutInfo::File(path)),
+ Some(LayoutType::FilePath(path)) => {
+ let layout_metadata = layout
+ .layout_metadata
+ .map(|m| m.try_into())
+ .transpose()?
+ .unwrap_or_default();
+ Ok(crate::data::LayoutInfo::File(path, layout_metadata))
+ },
Some(LayoutType::Url(url)) => Ok(crate::data::LayoutInfo::Url(url)),
Some(LayoutType::Stringified(content)) => {
Ok(crate::data::LayoutInfo::Stringified(content))
@@ -2063,6 +2037,77 @@ impl TryFrom<crate::client_server_contract::client_server_contract::LayoutInfo>
}
}
+impl From<crate::data::LayoutMetadata> for ProtoLayoutMetadata {
+ fn from(metadata: crate::data::LayoutMetadata) -> Self {
+ ProtoLayoutMetadata {
+ tabs: metadata.tabs.into_iter().map(|t| t.into()).collect(),
+ creation_time: metadata.creation_time,
+ update_time: metadata.update_time,
+ }
+ }
+}
+
+impl TryFrom<ProtoLayoutMetadata> for crate::data::LayoutMetadata {
+ type Error = anyhow::Error;
+ fn try_from(proto_metadata: ProtoLayoutMetadata) -> Result<Self> {
+ let tabs = proto_metadata
+ .tabs
+ .into_iter()
+ .map(|t| t.try_into())
+ .collect::<Result<Vec<_>>>()?;
+ Ok(crate::data::LayoutMetadata {
+ tabs,
+ creation_time: proto_metadata.creation_time,
+ update_time: proto_metadata.update_time,
+ })
+ }
+}
+
+impl From<crate::data::TabMetadata> for ProtoTabMetadata {
+ fn from(metadata: crate::data::TabMetadata) -> Self {
+ ProtoTabMetadata {
+ pane_metadata: metadata.panes.into_iter().map(|p| p.into()).collect(),
+ name: metadata.name,
+ }
+ }
+}
+
+impl TryFrom<ProtoTabMetadata> for crate::data::TabMetadata {
+ type Error = anyhow::Error;
+ fn try_from(proto_metadata: ProtoTabMetadata) -> Result<Self> {
+ let panes = proto_metadata
+ .pane_metadata
+ .into_iter()
+ .map(|p| p.try_into())
+ .collect::<Result<Vec<_>>>()?;
+ Ok(crate::data::TabMetadata {
+ panes,
+ name: proto_metadata.name,
+ })
+ }
+}
+
+impl From<crate::data::PaneMetadata> for ProtoPaneMetadata {
+ fn from(metadata: crate::data::PaneMetadata) -> Self {
+ ProtoPaneMetadata {
+ name: metadata.name,
+ is_plugin: metadata.is_plugin,
+ is_builtin_plugin: metadata.is_builtin_plugin,
+ }
+ }
+}
+
+impl TryFrom<ProtoPaneMetadata> for crate::data::PaneMetadata {
+ type Error = anyhow::Error;
+ fn try_from(proto_metadata: ProtoPaneMetadata) -> Result<Self> {
+ Ok(crate::data::PaneMetadata {
+ name: proto_metadata.name,
+ is_plugin: proto_metadata.is_plugin,
+ is_builtin_plugin: proto_metadata.is_builtin_plugin,
+ })
+ }
+}
+
impl From<ExitReason> for ProtoExitReason {
fn from(reason: ExitReason) -> Self {
match reason {
@@ -2686,6 +2731,82 @@ impl From<crate::input::layout::Run>
}
}
+// TabLayoutInfo conversion
+impl From<crate::input::layout::TabLayoutInfo>
+ for crate::client_server_contract::client_server_contract::TabLayoutInfo
+{
+ fn from(tab_info: crate::input::layout::TabLayoutInfo) -> Self {
+ Self {
+ tab_index: tab_info.tab_index as u32,
+ tab_name: tab_info.tab_name,
+ tiled_layout: Some(tab_info.tiled_layout.into()),
+ floating_layouts: tab_info
+ .floating_layouts
+ .into_iter()
+ .map(|l| l.into())
+ .collect(),
+ swap_tiled_layouts: tab_info
+ .swap_tiled_layouts
+ .unwrap_or_default()
+ .into_iter()
+ .map(|l| l.into())
+ .collect(),
+ swap_floating_layouts: tab_info
+ .swap_floating_layouts
+ .unwrap_or_default()
+ .into_iter()
+ .map(|l| l.into())
+ .collect(),
+ }
+ }
+}
+
+impl TryFrom<crate::client_server_contract::client_server_contract::TabLayoutInfo>
+ for crate::input::layout::TabLayoutInfo
+{
+ type Error = anyhow::Error;
+
+ fn try_from(
+ protobuf_tab: crate::client_server_contract::client_server_contract::TabLayoutInfo,
+ ) -> Result<Self> {
+ Ok(crate::input::layout::TabLayoutInfo {
+ tab_index: protobuf_tab.tab_index as usize,
+ tab_name: protobuf_tab.tab_name.filter(|s| !s.is_empty()),
+ tiled_layout: protobuf_tab
+ .tiled_layout
+ .ok_or_else(|| anyhow!("missing tiled_layout"))?
+ .try_into()?,
+ floating_layouts: protobuf_tab
+ .floating_layouts
+ .into_iter()
+ .map(|l| l.try_into())
+ .collect::<Result<Vec<_>>>()?,
+ swap_tiled_layouts: if protobuf_tab.swap_tiled_layouts.is_empty() {
+ None
+ } else {
+ Some(
+ protobuf_tab
+ .swap_tiled_layouts
+ .into_iter()
+ .map(|l| l.try_into())
+ .collect::<Result<Vec<_>>>()?,
+ )
+ },
+ swap_floating_layouts: if protobuf_tab.swap_floating_layouts.is_empty() {
+ None
+ } else {
+ Some(
+ protobuf_tab
+ .swap_floating_layouts
+ .into_iter()
+ .map(|l| l.try_into())
+ .collect::<Result<Vec<_>>>()?,
+ )
+ },
+ })
+ }
+}
+
// TiledPaneLayout conversion
impl From<crate::input::layout::TiledPaneLayout>
for crate::client_server_contract::client_server_contract::TiledPaneLayout
diff --git a/zellij-utils/src/ipc/tests/roundtrip_tests.rs b/zellij-utils/src/ipc/tests/roundtrip_tests.rs
index 216c6e14..01275198 100644
--- a/zellij-utils/src/ipc/tests/roundtrip_tests.rs
+++ b/zellij-utils/src/ipc/tests/roundtrip_tests.rs
@@ -1,8 +1,8 @@
use super::test_framework::*;
use crate::data::{
BareKey, CommandOrPlugin, ConnectToSession, Direction, FloatingPaneCoordinates, InputMode,
- KeyModifier, KeyWithModifier, LayoutInfo, OriginatingPlugin, PaneId, PluginTag, Resize,
- WebSharing,
+ KeyModifier, KeyWithModifier, LayoutInfo, LayoutMetadata, OriginatingPlugin, PaneId, PluginTag,
+ Resize, WebSharing,
};
use crate::input::actions::{Action, SearchDirection, SearchOption};
use crate::input::cli_assets::CliAssets;
@@ -2054,13 +2054,10 @@ fn test_client_messages() {
});
test_client_roundtrip!(ClientToServerMsg::Action {
action: Action::OverrideLayout {
- tiled_layout: None,
- floating_layouts: vec![],
- swap_tiled_layouts: None,
- swap_floating_layouts: None,
- tab_name: None,
+ tabs: vec![],
retain_existing_terminal_panes: false,
retain_existing_plugin_panes: false,
+ apply_only_to_active_tab: false,
},
terminal_id: Some(1),
client_id: Some(100),
@@ -3050,7 +3047,14 @@ fn test_server_messages() {
name: Some("new_session_name".to_owned()),
tab_position: Some(5),
pane_id: Some((5, true)),
- layout: Some(LayoutInfo::File("/path/to/my/file.kdl".to_owned())),
+ layout: Some(LayoutInfo::File(
+ "/path/to/my/file.kdl".to_owned(),
+ LayoutMetadata {
+ tabs: vec![],
+ creation_time: "0".to_owned(),
+ update_time: "0".to_owned()
+ }
+ )),
cwd: Some(PathBuf::from("/path/to/cwd")),
}
});
diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs
index 7603af3f..95d67ffd 100644
--- a/zellij-utils/src/kdl/mod.rs
+++ b/zellij-utils/src/kdl/mod.rs
@@ -1,15 +1,16 @@
mod kdl_layout_parser;
use crate::data::{
BareKey, Direction, FloatingPaneCoordinates, InputMode, KeyWithModifier, LayoutInfo,
- MultiplayerColors, Palette, PaletteColor, PaneId, PaneInfo, PaneManifest, PermissionType,
- Resize, SessionInfo, StyleDeclaration, Styling, TabInfo, WebSharing, DEFAULT_STYLES,
+ LayoutMetadata, MultiplayerColors, Palette, PaletteColor, PaneId, PaneInfo, PaneManifest,
+ PermissionType, Resize, SessionInfo, StyleDeclaration, Styling, TabInfo, WebSharing,
+ DEFAULT_STYLES,
};
use crate::envs::EnvironmentVariables;
use crate::home::{find_default_config_dir, get_layout_dir};
use crate::input::config::{Config, ConfigError, KdlError};
use crate::input::keybinds::Keybinds;
use crate::input::layout::{
- Layout, PluginUserConfiguration, RunPlugin, RunPluginOrAlias, SplitSize,
+ Layout, PluginUserConfiguration, RunPlugin, RunPluginOrAlias, SplitSize, TabLayoutInfo,
};
use crate::input::options::{Clipboard, OnForceClose, Options};
use crate::input::permission::{GrantedPermission, PermissionCache};
@@ -1727,13 +1728,10 @@ impl TryFrom<(&KdlNode, &Options)> for Action {
let command_metadata = action_children.iter().next();
if command_metadata.is_none() {
return Ok(Action::OverrideLayout {
- tiled_layout: None,
- floating_layouts: vec![],
- swap_tiled_layouts: None,
- swap_floating_layouts: None,
- tab_name: None,
+ tabs: vec![],
retain_existing_terminal_panes: false,
retain_existing_plugin_panes: false,
+ apply_only_to_active_tab: false,
});
}
@@ -1760,6 +1758,9 @@ impl TryFrom<(&KdlNode, &Options)> for Action {
kdl_child_bool_value_for_entry(c_m, "retain_existing_plugin_panes")
})
.unwrap_or(false);
+ let apply_only_to_active_tab = command_metadata
+ .and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "apply_only_to_active_tab"))
+ .unwrap_or(false);
let layout_dir = config_options
.layout_dir
@@ -1804,26 +1805,38 @@ impl TryFrom<(&KdlNode, &Options)> for Action {
let (tab_name, layout, floating_panes_layout) = tabs.drain(..).next().unwrap();
let name = tab_name.or(name);
- Ok(Action::OverrideLayout {
- tiled_layout: Some(layout),
+ let tab_layout_info = TabLayoutInfo {
+ tab_index: 0,
+ tab_name: name,
+ tiled_layout: layout,
floating_layouts: floating_panes_layout,
swap_tiled_layouts,
swap_floating_layouts,
- tab_name: name,
+ };
+
+ Ok(Action::OverrideLayout {
+ tabs: vec![tab_layout_info],
retain_existing_terminal_panes,
retain_existing_plugin_panes,
+ apply_only_to_active_tab,
})
} else {
let (layout, floating_panes_layout) = layout.new_tab();
- Ok(Action::OverrideLayout {
- tiled_layout: Some(layout),
+ let tab_layout_info = TabLayoutInfo {
+ tab_index: 0,
+ tab_name: name,
+ tiled_layout: layout,
floating_layouts: floating_panes_layout,
swap_tiled_layouts,
swap_floating_layouts,
- tab_name: name,
+ };
+
+ Ok(Action::OverrideLayout {
+ tabs: vec![tab_layout_info],
retain_existing_terminal_panes,
retain_existing_plugin_panes,
+ apply_only_to_active_tab,
})
}
},
@@ -5154,7 +5167,9 @@ impl SessionInfo {
match layout_source {
Some(layout_source) => match layout_source {
"built-in" => Some(LayoutInfo::BuiltIn(layout_name)),
- "file" => Some(LayoutInfo::File(layout_name)),
+ "file" => {
+ Some(LayoutInfo::File(layout_name, LayoutMetadata::default()))
+ },
_ => None,
},
None => None,
@@ -5288,7 +5303,7 @@ impl SessionInfo {
let mut available_layouts_children = KdlDocument::new();
for layout_info in &self.available_layouts {
let (layout_name, layout_source) = match layout_info {
- LayoutInfo::File(name) => (name.clone(), "file"),
+ LayoutInfo::File(name, _layout_metadata) => (name.clone(), "file"),
LayoutInfo::BuiltIn(name) => (name.clone(), "built-in"),
LayoutInfo::Url(url) => (url.clone(), "url"),
LayoutInfo::Stringified(_stringified) => ("stringified-layout".to_owned(), "N/A"),
@@ -5906,9 +5921,9 @@ fn serialize_and_deserialize_session_info_with_data() {
connected_clients: 2,
is_current_session: false,
available_layouts: vec![
- LayoutInfo::File("layout1".to_owned()),
+ LayoutInfo::File("layout1".to_owned(), LayoutMetadata::default()),
LayoutInfo::BuiltIn("layout2".to_owned()),
- LayoutInfo::File("layout3".to_owned()),
+ LayoutInfo::File("layout3".to_owned(), LayoutMetadata::default()),
],
plugins: Default::default(),
web_client_count: 2,
diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap
index 6353ee0e..9fa94e0b 100644
--- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap
+++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap
@@ -114,6 +114,13 @@ keybinds clear-defaults=true {
}
SwitchToMode "normal"
}
+ bind "l" {
+ LaunchOrFocusPlugin "zellij:layout-manager" {
+ floating true
+ move_to_focused_tab true
+ }
+ SwitchToMode "normal"
+ }
bind "Ctrl o" { SwitchToMode "normal"; }
bind "p" {
LaunchOrFocusPlugin "plugin-manager" {
diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap
index e562e030..c9d93654 100644
--- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap
+++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap
@@ -114,6 +114,13 @@ keybinds clear-defaults=true {
}
SwitchToMode "normal"
}
+ bind "l" {
+ LaunchOrFocusPlugin "zellij:layout-manager" {
+ floating true
+ move_to_focused_tab true
+ }
+ SwitchToMode "normal"
+ }
bind "Ctrl o" { SwitchToMode "normal"; }
bind "p" {
LaunchOrFocusPlugin "plugin-manager" {
diff --git a/zellij-utils/src/plugin_api/action.proto b/zellij-utils/src/plugin_api/action.proto
index de8ef44f..6b899b48 100644
--- a/zellij-utils/src/plugin_api/action.proto
+++ b/zellij-utils/src/plugin_api/action.proto
@@ -162,14 +162,20 @@ message NewTabPayload {
optional UnblockCondition first_pane_unblock_condition = 9;
}
+message TabLayoutInfo {
+ uint32 tab_index = 1;
+ optional string tab_name = 2;
+ TiledPaneLayout tiled_layout = 3;
+ repeated FloatingPaneLayout floating_layouts = 4;
+ repeated SwapTiledLayout swap_tiled_layouts = 5;
+ repeated SwapFloatingLayout swap_floating_layouts = 6;
+}
+
message OverrideLayoutPayload {
- optional TiledPaneLayout tiled_layout = 1;
- repeated FloatingPaneLayout floating_layouts = 2;
- repeated SwapTiledLayout swap_tiled_layouts = 3;
- repeated SwapFloatingLayout swap_floating_layouts = 4;
- optional string tab_name = 5;
- bool retain_existing_terminal_panes = 10;
- bool retain_existing_plugin_panes = 11;
+ repeated TabLayoutInfo tabs = 1;
+ bool retain_existing_terminal_panes = 2;
+ bool retain_existing_plugin_panes = 3;
+ bool apply_only_to_active_tab = 4;
}
message Action {
diff --git a/zellij-utils/src/plugin_api/action.rs b/zellij-utils/src/plugin_api/action.rs
index 860b090b..aa4c8b09 100644
--- a/zellij-utils/src/plugin_api/action.rs
+++ b/zellij-utils/src/plugin_api/action.rs
@@ -62,6 +62,7 @@ pub use super::generated_api::api::{
SwapFloatingLayout as ProtobufSwapFloatingLayout,
SwapTiledLayout as ProtobufSwapTiledLayout,
SwitchToModePayload,
+ TabLayoutInfo as ProtobufTabLayoutInfo,
TiledPaneLayout as ProtobufTiledPaneLayout,
TiledPlacement as ProtobufTiledPlacement,
UnblockCondition as ProtobufUnblockCondition,
@@ -83,7 +84,7 @@ use crate::input::layout::SplitSize;
use crate::input::layout::{
FloatingPaneLayout, LayoutConstraint, PercentOrFixed, PluginAlias, PluginUserConfiguration,
Run, RunPlugin, RunPluginLocation, RunPluginOrAlias, SplitDirection, SwapFloatingLayout,
- SwapTiledLayout, TiledPaneLayout,
+ SwapTiledLayout, TabLayoutInfo, TiledPaneLayout,
};
use crate::input::mouse::{MouseEvent, MouseEventType};
use crate::position::Position;
@@ -723,48 +724,15 @@ impl TryFrom<ProtobufAction> for Action {
},
Some(ProtobufActionName::OverrideLayout) => match protobuf_action.optional_payload {
Some(OptionalPayload::OverrideLayoutPayload(payload)) => {
- let tiled_layout = payload.tiled_layout.map(|l| l.try_into()).transpose()?;
-
- let floating_layouts = payload
- .floating_layouts
- .into_iter()
- .map(|l| l.try_into())
- .collect::<Result<Vec<_>, _>>()?;
-
- let swap_tiled_layouts = if payload.swap_tiled_layouts.is_empty() {
- None
- } else {
- Some(
- payload
- .swap_tiled_layouts
- .into_iter()
- .map(|l| l.try_into())
- .collect::<Result<Vec<_>, _>>()?,
- )
- };
-
- let swap_floating_layouts = if payload.swap_floating_layouts.is_empty() {
- None
- } else {
- Some(
- payload
- .swap_floating_layouts
- .into_iter()
- .map(|l| l.try_into())
- .collect::<Result<Vec<_>, _>>()?,
- )
- };
-
- let tab_name = payload.tab_name.filter(|s| !s.is_empty());
-
Ok(Action::OverrideLayout {
- tiled_layout,
- floating_layouts,
- swap_tiled_layouts,
- swap_floating_layouts,
- tab_name,
+ tabs: payload
+ .tabs
+ .into_iter()
+ .map(|t| t.try_into())
+ .collect::<Result<Vec<_>, _>>()?,
retain_existing_terminal_panes: payload.retain_existing_terminal_panes,
retain_existing_plugin_panes: payload.retain_existing_plugin_panes,
+ apply_only_to_active_tab: payload.apply_only_to_active_tab,
})
},
Some(_) => Err("Mismatched payload for OverrideLayout"),
@@ -1556,56 +1524,24 @@ impl TryFrom<Action> for ProtobufAction {
optional_payload: None,
}),
Action::OverrideLayout {
- tiled_layout,
- floating_layouts,
- swap_tiled_layouts,
- swap_floating_layouts,
- tab_name,
+ tabs,
retain_existing_terminal_panes,
retain_existing_plugin_panes,
- } => {
- let protobuf_tiled_layout = tiled_layout.map(|l| l.try_into()).transpose()?;
-
- let protobuf_floating_layouts = floating_layouts
- .into_iter()
- .map(|l| l.try_into())
- .collect::<Result<Vec<_>, _>>()?;
-
- let protobuf_swap_tiled_layouts = swap_tiled_layouts
- .map(|layouts| {
- layouts
- .into_iter()
- .map(|l| l.try_into())
- .collect::<Result<Vec<_>, _>>()
- })
- .transpose()?
- .unwrap_or_default();
-
- let protobuf_swap_floating_layouts = swap_floating_layouts
- .map(|layouts| {
- layouts
+ apply_only_to_active_tab,
+ } => Ok(ProtobufAction {
+ name: ProtobufActionName::OverrideLayout as i32,
+ optional_payload: Some(OptionalPayload::OverrideLayoutPayload(
+ OverrideLayoutPayload {
+ tabs: tabs
.into_iter()
- .map(|l| l.try_into())
- .collect::<Result<Vec<_>, _>>()
- })
- .transpose()?
- .unwrap_or_default();
-
- Ok(ProtobufAction {
- name: ProtobufActionName::OverrideLayout as i32,
- optional_payload: Some(OptionalPayload::OverrideLayoutPayload(
- OverrideLayoutPayload {
- tiled_layout: protobuf_tiled_layout,
- floating_layouts: protobuf_floating_layouts,
- swap_tiled_layouts: protobuf_swap_tiled_layouts,
- swap_floating_layouts: protobuf_swap_floating_layouts,
- tab_name: tab_name.clone(),
- retain_existing_terminal_panes,
- retain_existing_plugin_panes,
- },
- )),
- })
- },
+ .map(|t| t.try_into())
+ .collect::<Result<Vec<_>, _>>()?,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ apply_only_to_active_tab,
+ },
+ )),
+ }),
Action::QueryTabNames => Ok(ProtobufAction {
name: ProtobufActionName::QueryTabNames as i32,
optional_payload: None,
@@ -2739,6 +2675,77 @@ impl TryFrom<CommandOrPlugin> for ProtobufCommandOrPlugin {
}
}
+impl TryFrom<ProtobufTabLayoutInfo> for TabLayoutInfo {
+ type Error = &'static str;
+
+ fn try_from(protobuf_tab: ProtobufTabLayoutInfo) -> Result<Self, Self::Error> {
+ Ok(TabLayoutInfo {
+ tab_index: protobuf_tab.tab_index as usize,
+ tab_name: protobuf_tab.tab_name.filter(|s| !s.is_empty()),
+ tiled_layout: protobuf_tab
+ .tiled_layout
+ .ok_or("missing tiled_layout")?
+ .try_into()?,
+ floating_layouts: protobuf_tab
+ .floating_layouts
+ .into_iter()
+ .map(|l| l.try_into())
+ .collect::<Result<Vec<_>, _>>()?,
+ swap_tiled_layouts: if protobuf_tab.swap_tiled_layouts.is_empty() {
+ None
+ } else {
+ Some(
+ protobuf_tab
+ .swap_tiled_layouts
+ .into_iter()
+ .map(|l| l.try_into())
+ .collect::<Result<Vec<_>, _>>()?,
+ )
+ },
+ swap_floating_layouts: if protobuf_tab.swap_floating_layouts.is_empty() {
+ None
+ } else {
+ Some(
+ protobuf_tab
+ .swap_floating_layouts
+ .into_iter()
+ .map(|l| l.try_into())
+ .collect::<Result<Vec<_>, _>>()?,
+ )
+ },
+ })
+ }
+}
+
+impl TryFrom<TabLayoutInfo> for ProtobufTabLayoutInfo {
+ type Error = &'static str;
+
+ fn try_from(tab_info: TabLayoutInfo) -> Result<Self, Self::Error> {
+ Ok(ProtobufTabLayoutInfo {
+ tab_index: tab_info.tab_index as u32,
+ tab_name: tab_info.tab_name,
+ tiled_layout: Some(tab_info.tiled_layout.try_into()?),
+ floating_layouts: tab_info
+ .floating_layouts
+ .into_iter()
+ .map(|l| l.try_into())
+ .collect::<Result<Vec<_>, _>>()?,
+ swap_tiled_layouts: tab_info
+ .swap_tiled_layouts
+ .unwrap_or_default()
+ .into_iter()
+ .map(|l| l.try_into())
+ .collect::<Result<Vec<_>, _>>()?,
+ swap_floating_layouts: tab_info
+ .swap_floating_layouts
+ .unwrap_or_default()
+ .into_iter()
+ .map(|l| l.try_into())
+ .collect::<Result<Vec<_>, _>>()?,
+ })
+ }
+}
+
impl TryFrom<ProtobufTiledPaneLayout> for TiledPaneLayout {
type Error = &'static str;
fn try_from(protobuf: ProtobufTiledPaneLayout) -> Result<Self, Self::Error> {
diff --git a/zellij-utils/src/plugin_api/event.proto b/zellij-utils/src/plugin_api/event.proto
index 3bdeea19..10125387 100644
--- a/zellij-utils/src/plugin_api/event.proto
+++ b/zellij-utils/src/plugin_api/event.proto
@@ -62,6 +62,7 @@ enum EventType {
UserAction = 37;
ActionComplete = 38;
CwdChanged = 39;
+ AvailableLayoutInfo = 40;
}
message EventNameList {
@@ -103,6 +104,7 @@ message Event {
UserActionPayload user_action_payload = 31;
ActionCompletePayload action_complete_payload = 32;
CwdChangedPayload cwd_changed_payload = 33;
+ AvailableLayoutInfoPayload available_layout_info_payload = 34;
}
}
@@ -202,6 +204,11 @@ message SessionUpdatePayload {
repeated ResurrectableSession resurrectable_sessions = 2;
}
+message AvailableLayoutInfoPayload {
+ repeated LayoutInfo available_layouts = 1;
+ repeated LayoutWithError layouts_with_errors = 2;
+}
+
message RunCommandResultPayload {
optional int32 exit_code = 1;
bytes stdout = 2;
@@ -318,6 +325,54 @@ message PluginInfo {
message LayoutInfo {
string name = 1;
string source = 2;
+ optional LayoutMetadata layout_metadata = 5;
+}
+
+message LayoutWithError {
+ string layout_name = 1;
+ LayoutParsingError error = 2;
+}
+
+message LayoutParsingError {
+ oneof error_type {
+ KdlErrorVariant kdl_error = 1;
+ SyntaxError syntax_error = 2;
+ }
+}
+
+message KdlErrorVariant {
+ KdlError kdl_error = 1;
+ string file_name = 2;
+ string source_code = 3;
+}
+
+message SyntaxError {
+ // Empty message, just a marker
+}
+
+message KdlError {
+ string error_message = 1;
+ optional uint64 offset = 2;
+ optional uint64 len = 3;
+ optional string help_message = 4;
+}
+
+message LayoutMetadata {
+ repeated TabMetadata tabs = 1;
+ string creation_time = 2;
+ string update_time = 3;
+
+}
+
+message TabMetadata {
+ repeated PaneMetadata pane_metadata = 1;
+ optional string name = 2;
+}
+
+message PaneMetadata {
+ optional string name = 1;
+ bool is_plugin = 2;
+ bool is_builtin_plugin = 3;
}
message ResurrectableSession {
diff --git a/zellij-utils/src/plugin_api/event.rs b/zellij-utils/src/plugin_api/event.rs
index 4e430b0b..80d77e56 100644
--- a/zellij-utils/src/plugin_api/event.rs
+++ b/zellij-utils/src/plugin_api/event.rs
@@ -1,23 +1,29 @@
pub use super::generated_api::api::{
action::{Action as ProtobufAction, Position as ProtobufPosition},
event::{
- event::Payload as ProtobufEventPayload, pane_scrollback_response,
- ActionCompletePayload as ProtobufActionCompletePayload, ClientInfo as ProtobufClientInfo,
- ClientPaneHistory as ProtobufClientPaneHistory,
+ event::Payload as ProtobufEventPayload,
+ layout_parsing_error::ErrorType as ProtobufLayoutParsingErrorType,
+ pane_scrollback_response, ActionCompletePayload as ProtobufActionCompletePayload,
+ AvailableLayoutInfoPayload as ProtobufAvailableLayoutInfoPayload,
+ ClientInfo as ProtobufClientInfo, ClientPaneHistory as ProtobufClientPaneHistory,
ClientTabHistory as ProtobufClientTabHistory, ContextItem as ProtobufContextItem,
CopyDestination as ProtobufCopyDestination, CwdChangedPayload as ProtobufCwdChangedPayload,
Event as ProtobufEvent, EventNameList as ProtobufEventNameList,
EventType as ProtobufEventType, FileMetadata as ProtobufFileMetadata,
- InputModeKeybinds as ProtobufInputModeKeybinds, KeyBind as ProtobufKeyBind,
- LayoutInfo as ProtobufLayoutInfo, ModeUpdatePayload as ProtobufModeUpdatePayload,
+ InputModeKeybinds as ProtobufInputModeKeybinds, KdlError as ProtobufKdlError,
+ KdlErrorVariant as ProtobufKdlErrorVariant, KeyBind as ProtobufKeyBind,
+ LayoutInfo as ProtobufLayoutInfo, LayoutMetadata as ProtobufLayoutMetadata,
+ LayoutParsingError as ProtobufLayoutParsingError,
+ LayoutWithError as ProtobufLayoutWithError, ModeUpdatePayload as ProtobufModeUpdatePayload,
PaneContents as ProtobufPaneContents, PaneContentsEntry as ProtobufPaneContentsEntry,
PaneId as ProtobufPaneId, PaneInfo as ProtobufPaneInfo,
- PaneManifest as ProtobufPaneManifest,
+ PaneManifest as ProtobufPaneManifest, PaneMetadata as ProtobufPaneMetadata,
PaneRenderReportPayload as ProtobufPaneRenderReportPayload,
PaneScrollbackResponse as ProtobufPaneScrollbackResponse, PaneType as ProtobufPaneType,
PluginInfo as ProtobufPluginInfo, ResurrectableSession as ProtobufResurrectableSession,
SelectedText as ProtobufSelectedText, SessionManifest as ProtobufSessionManifest,
- TabInfo as ProtobufTabInfo, UserActionPayload as ProtobufUserActionPayload,
+ SyntaxError as ProtobufSyntaxError, TabInfo as ProtobufTabInfo,
+ TabMetadata as ProtobufTabMetadata, UserActionPayload as ProtobufUserActionPayload,
WebServerStatusPayload as ProtobufWebServerStatusPayload, WebSharing as ProtobufWebSharing,
*,
},
@@ -28,9 +34,10 @@ pub use super::generated_api::api::{
#[allow(hidden_glob_reexports)]
use crate::data::{
ClientId, ClientInfo, CopyDestination, Event, EventType, FileMetadata, InputMode,
- KeyWithModifier, LayoutInfo, ModeInfo, Mouse, PaneContents, PaneId, PaneInfo, PaneManifest,
- PaneScrollbackResponse, PermissionStatus, PluginCapabilities, PluginInfo, SelectedText,
- SessionInfo, Style, TabInfo, WebServerStatus, WebSharing,
+ KeyWithModifier, LayoutInfo, LayoutMetadata, ModeInfo, Mouse, PaneContents, PaneId, PaneInfo,
+ PaneManifest, PaneMetadata, PaneScrollbackResponse, PermissionStatus, PluginCapabilities,
+ PluginInfo, SelectedText, SessionInfo, Style, TabInfo, TabMetadata, WebServerStatus,
+ WebSharing,
};
use crate::errors::prelude::*;
@@ -458,6 +465,29 @@ impl TryFrom<ProtobufEvent> for Event {
},
_ => Err("Malformed payload for the CwdChanged Event"),
},
+ Some(ProtobufEventType::AvailableLayoutInfo) => match protobuf_event.payload {
+ Some(ProtobufEventPayload::AvailableLayoutInfoPayload(
+ available_layout_info_payload,
+ )) => {
+ let mut available_layouts: Vec<LayoutInfo> = vec![];
+ let mut layouts_with_errors: Vec<crate::data::LayoutWithError> = vec![];
+
+ for protobuf_layout_info in available_layout_info_payload.available_layouts {
+ available_layouts.push(LayoutInfo::try_from(protobuf_layout_info)?);
+ }
+
+ for protobuf_error in available_layout_info_payload.layouts_with_errors {
+ layouts_with_errors
+ .push(crate::data::LayoutWithError::try_from(protobuf_error)?);
+ }
+
+ Ok(Event::AvailableLayoutInfo(
+ available_layouts,
+ layouts_with_errors,
+ ))
+ },
+ _ => Err("Malformed payload for the AvailableLayoutInfo Event"),
+ },
None => Err("Unknown Protobuf Event"),
}
}
@@ -910,6 +940,30 @@ impl TryFrom<Event> for ProtobufEvent {
payload: Some(event::Payload::CwdChangedPayload(cwd_changed_payload)),
})
},
+ Event::AvailableLayoutInfo(available_layouts, layouts_with_errors) => {
+ let mut protobuf_available_layouts = vec![];
+ let mut protobuf_layouts_with_errors = vec![];
+
+ for layout_info in available_layouts {
+ protobuf_available_layouts.push(layout_info.try_into()?);
+ }
+
+ for layout_error in layouts_with_errors {
+ protobuf_layouts_with_errors.push(layout_error.try_into()?);
+ }
+
+ let available_layout_info_payload = ProtobufAvailableLayoutInfoPayload {
+ available_layouts: protobuf_available_layouts,
+ layouts_with_errors: protobuf_layouts_with_errors,
+ };
+
+ Ok(ProtobufEvent {
+ name: ProtobufEventType::AvailableLayoutInfo as i32,
+ payload: Some(event::Payload::AvailableLayoutInfoPayload(
+ available_layout_info_payload,
+ )),
+ })
+ },
}
}
}
@@ -1078,21 +1132,25 @@ impl TryFrom<LayoutInfo> for ProtobufLayoutInfo {
type Error = &'static str;
fn try_from(layout_info: LayoutInfo) -> Result<Self, &'static str> {
match layout_info {
- LayoutInfo::File(name) => Ok(ProtobufLayoutInfo {
+ LayoutInfo::File(name, layout_metadata) => Ok(ProtobufLayoutInfo {
source: "file".to_owned(),
name,
+ layout_metadata: Some(layout_metadata.try_into()?),
}),
LayoutInfo::BuiltIn(name) => Ok(ProtobufLayoutInfo {
source: "built-in".to_owned(),
name,
+ layout_metadata: None,
}),
LayoutInfo::Url(name) => Ok(ProtobufLayoutInfo {
source: "url".to_owned(),
name,
+ layout_metadata: None,
}),
LayoutInfo::Stringified(stringified_layout) => Ok(ProtobufLayoutInfo {
source: "stringified".to_owned(),
name: stringified_layout.clone(),
+ layout_metadata: None,
}),
}
}
@@ -1102,7 +1160,14 @@ impl TryFrom<ProtobufLayoutInfo> for LayoutInfo {
type Error = &'static str;
fn try_from(protobuf_layout_info: ProtobufLayoutInfo) -> Result<Self, &'static str> {
match protobuf_layout_info.source.as_str() {
- "file" => Ok(LayoutInfo::File(protobuf_layout_info.name)),
+ "file" => {
+ let layout_metadata = protobuf_layout_info
+ .layout_metadata
+ .map(|m| m.try_into())
+ .transpose()?
+ .unwrap_or_default();
+ Ok(LayoutInfo::File(protobuf_layout_info.name, layout_metadata))
+ },
"built-in" => Ok(LayoutInfo::BuiltIn(protobuf_layout_info.name)),
"url" => Ok(LayoutInfo::Url(protobuf_layout_info.name)),
"stringified" => Ok(LayoutInfo::Stringified(protobuf_layout_info.name)),
@@ -1111,6 +1176,183 @@ impl TryFrom<ProtobufLayoutInfo> for LayoutInfo {
}
}
+impl TryFrom<ProtobufLayoutMetadata> for LayoutMetadata {
+ type Error = &'static str;
+ fn try_from(protobuf_metadata: ProtobufLayoutMetadata) -> Result<Self, &'static str> {
+ let tabs = protobuf_metadata
+ .tabs
+ .into_iter()
+ .map(|t| t.try_into())
+ .collect::<Result<Vec<_>, _>>()?;
+ Ok(LayoutMetadata {
+ tabs,
+ creation_time: protobuf_metadata.creation_time,
+ update_time: protobuf_metadata.update_time,
+ })
+ }
+}
+
+impl TryFrom<LayoutMetadata> for ProtobufLayoutMetadata {
+ type Error = &'static str;
+ fn try_from(metadata: LayoutMetadata) -> Result<Self, &'static str> {
+ let tabs = metadata
+ .tabs
+ .into_iter()
+ .map(|t| t.try_into())
+ .collect::<Result<Vec<_>, _>>()?;
+ Ok(ProtobufLayoutMetadata {
+ tabs,
+ creation_time: metadata.creation_time,
+ update_time: metadata.update_time,
+ })
+ }
+}
+
+impl TryFrom<ProtobufTabMetadata> for TabMetadata {
+ type Error = &'static str;
+ fn try_from(protobuf_metadata: ProtobufTabMetadata) -> Result<Self, &'static str> {
+ let panes = protobuf_metadata
+ .pane_metadata
+ .into_iter()
+ .map(|p| p.try_into())
+ .collect::<Result<Vec<_>, _>>()?;
+ Ok(TabMetadata {
+ panes,
+ name: protobuf_metadata.name,
+ })
+ }
+}
+
+impl TryFrom<TabMetadata> for ProtobufTabMetadata {
+ type Error = &'static str;
+ fn try_from(metadata: TabMetadata) -> Result<Self, &'static str> {
+ let pane_metadata = metadata
+ .panes
+ .into_iter()
+ .map(|p| p.try_into())
+ .collect::<Result<Vec<_>, _>>()?;
+ Ok(ProtobufTabMetadata {
+ pane_metadata,
+ name: metadata.name,
+ })
+ }
+}
+
+impl TryFrom<ProtobufPaneMetadata> for PaneMetadata {
+ type Error = &'static str;
+ fn try_from(protobuf_metadata: ProtobufPaneMetadata) -> Result<Self, &'static str> {
+ Ok(PaneMetadata {
+ name: protobuf_metadata.name,
+ is_plugin: protobuf_metadata.is_plugin,
+ is_builtin_plugin: protobuf_metadata.is_builtin_plugin,
+ })
+ }
+}
+
+impl TryFrom<PaneMetadata> for ProtobufPaneMetadata {
+ type Error = &'static str;
+ fn try_from(metadata: PaneMetadata) -> Result<Self, &'static str> {
+ Ok(ProtobufPaneMetadata {
+ name: metadata.name,
+ is_plugin: metadata.is_plugin,
+ is_builtin_plugin: metadata.is_builtin_plugin,
+ })
+ }
+}
+
+// LayoutWithError conversions
+impl TryFrom<ProtobufLayoutWithError> for crate::data::LayoutWithError {
+ type Error = &'static str;
+ fn try_from(protobuf: ProtobufLayoutWithError) -> Result<Self, Self::Error> {
+ Ok(crate::data::LayoutWithError {
+ layout_name: protobuf.layout_name,
+ error: protobuf.error.ok_or("Missing error field")?.try_into()?,
+ })
+ }
+}
+
+impl TryFrom<crate::data::LayoutWithError> for ProtobufLayoutWithError {
+ type Error = &'static str;
+ fn try_from(layout_error: crate::data::LayoutWithError) -> Result<Self, Self::Error> {
+ Ok(ProtobufLayoutWithError {
+ layout_name: layout_error.layout_name,
+ error: Some(layout_error.error.try_into()?),
+ })
+ }
+}
+
+// LayoutParsingError conversions
+impl TryFrom<ProtobufLayoutParsingError> for crate::data::LayoutParsingError {
+ type Error = &'static str;
+ fn try_from(protobuf: ProtobufLayoutParsingError) -> Result<Self, Self::Error> {
+ match protobuf.error_type.ok_or("Missing error_type")? {
+ ProtobufLayoutParsingErrorType::KdlError(kdl_variant) => {
+ Ok(crate::data::LayoutParsingError::KdlError {
+ kdl_error: kdl_variant
+ .kdl_error
+ .ok_or("Missing kdl_error")?
+ .try_into()?,
+ file_name: kdl_variant.file_name,
+ source_code: kdl_variant.source_code,
+ })
+ },
+ ProtobufLayoutParsingErrorType::SyntaxError(_) => {
+ Ok(crate::data::LayoutParsingError::SyntaxError)
+ },
+ }
+ }
+}
+
+impl TryFrom<crate::data::LayoutParsingError> for ProtobufLayoutParsingError {
+ type Error = &'static str;
+ fn try_from(error: crate::data::LayoutParsingError) -> Result<Self, Self::Error> {
+ let error_type = match error {
+ crate::data::LayoutParsingError::KdlError {
+ kdl_error,
+ file_name,
+ source_code,
+ } => ProtobufLayoutParsingErrorType::KdlError(ProtobufKdlErrorVariant {
+ kdl_error: Some(kdl_error.try_into()?),
+ file_name,
+ source_code,
+ }),
+ crate::data::LayoutParsingError::SyntaxError => {
+ ProtobufLayoutParsingErrorType::SyntaxError(ProtobufSyntaxError {})
+ },
+ };
+ Ok(ProtobufLayoutParsingError {
+ error_type: Some(error_type),
+ })
+ }
+}
+
+// KdlError conversions
+impl TryFrom<ProtobufKdlError> for crate::input::config::KdlError {
+ type Error = &'static str;
+ fn try_from(protobuf: ProtobufKdlError) -> Result<Self, Self::Error> {
+ Ok(crate::input::config::KdlError {
+ error_message: protobuf.error_message,
+ src: None, // We don't serialize NamedSource
+ offset: protobuf.offset.map(|o| o as usize),
+ len: protobuf.len.map(|l| l as usize),
+ help_message: protobuf.help_message,
+ })
+ }
+}
+
+impl TryFrom<crate::input::config::KdlError> for ProtobufKdlError {
+ type Error = &'static str;
+ fn try_from(kdl: crate::input::config::KdlError) -> Result<Self, Self::Error> {
+ Ok(ProtobufKdlError {
+ error_message: kdl.error_message,
+ // src is not serialized
+ offset: kdl.offset.map(|o| o as u64),
+ len: kdl.len.map(|l| l as u64),
+ help_message: kdl.help_message,
+ })
+ }
+}
+
impl TryFrom<CopyDestination> for ProtobufCopyDestination {
type Error = &'static str;
fn try_from(copy_destination: CopyDestination) -> Result<Self, &'static str> {
@@ -1620,6 +1862,7 @@ impl TryFrom<ProtobufEventType> for EventType {
ProtobufEventType::UserAction => EventType::UserAction,
ProtobufEventType::ActionComplete => EventType::ActionComplete,
ProtobufEventType::CwdChanged => EventType::CwdChanged,
+ ProtobufEventType::AvailableLayoutInfo => EventType::AvailableLayoutInfo,
})
}
}
@@ -1667,6 +1910,7 @@ impl TryFrom<EventType> for ProtobufEventType {
EventType::UserAction => ProtobufEventType::UserAction,
EventType::ActionComplete => ProtobufEventType::ActionComplete,
EventType::CwdChanged => ProtobufEventType::CwdChanged,
+ EventType::AvailableLayoutInfo => ProtobufEventType::AvailableLayoutInfo,
})
}
}
@@ -2268,9 +2512,23 @@ fn serialize_session_update_event_with_non_default_values() {
connected_clients: 2,
is_current_session: true,
available_layouts: vec![
- LayoutInfo::File("layout 1".to_owned()),
+ LayoutInfo::File(
+ "layout 1".to_owned(),
+ LayoutMetadata {
+ tabs: vec![],
+ creation_time: "0".to_owned(),
+ update_time: "0".to_owned(),
+ },
+ ),
LayoutInfo::BuiltIn("layout2".to_owned()),
- LayoutInfo::File("layout3".to_owned()),
+ LayoutInfo::File(
+ "layout3".to_owned(),
+ LayoutMetadata {
+ tabs: vec![],
+ creation_time: "0".to_owned(),
+ update_time: "0".to_owned(),
+ },
+ ),
],
plugins,
web_clients_allowed: false,
@@ -2287,9 +2545,23 @@ fn serialize_session_update_event_with_non_default_values() {
connected_clients: 0,
is_current_session: false,
available_layouts: vec![
- LayoutInfo::File("layout 1".to_owned()),
+ LayoutInfo::File(
+ "layout 1".to_owned(),
+ LayoutMetadata {
+ tabs: vec![],
+ creation_time: "0".to_owned(),
+ update_time: "0".to_owned(),
+ },
+ ),
LayoutInfo::BuiltIn("layout2".to_owned()),
- LayoutInfo::File("layout3".to_owned()),
+ LayoutInfo::File(
+ "layout3".to_owned(),
+ LayoutMetadata {
+ tabs: vec![],
+ creation_time: "0".to_owned(),
+ update_time: "0".to_owned(),
+ },
+ ),
],
plugins: Default::default(),
web_clients_allowed: false,
diff --git a/zellij-utils/src/plugin_api/plugin_command.proto b/zellij-utils/src/plugin_api/plugin_command.proto
index fb456ccd..0a0ad2b0 100644
--- a/zellij-utils/src/plugin_api/plugin_command.proto
+++ b/zellij-utils/src/plugin_api/plugin_command.proto
@@ -166,6 +166,16 @@ enum CommandName {
SendSigintToPaneId = 171;
SendSigkillToPaneId = 172;
GetPanePid = 173;
+ OverrideLayout = 174;
+ SaveLayout = 175;
+ DeleteLayout = 176;
+ EditLayout = 177;
+ GenerateRandomName = 178;
+ DumpLayout = 179;
+ ParseLayout = 180;
+ RenameLayout = 181;
+ GetLayoutDir = 182;
+ GetFocusedPaneInfo = 183;
}
message PluginCommand {
@@ -280,6 +290,17 @@ message PluginCommand {
PaneId send_sigint_to_pane_id_payload = 121;
PaneId send_sigkill_to_pane_id_payload = 122;
GetPanePidPayload get_pane_pid_payload = 123;
+ OverrideLayoutPayload override_layout_payload = 124;
+ SaveLayoutPayload save_layout_payload = 125;
+ DeleteLayoutPayload delete_layout_payload = 126;
+ EditLayoutPayload edit_layout_payload = 127;
+ GenerateRandomNamePayload generate_random_name_payload = 128;
+ DumpLayoutPayload dump_layout_payload = 129;
+ ParseLayoutPayload parse_layout_payload = 130;
+ RenameLayoutPayload rename_layout_payload = 131;
+ GetLayoutDirPayload get_layout_dir_payload = 132;
+ DumpSessionLayoutPayload dump_session_layout_payload = 133;
+ GetFocusedPaneInfoPayload get_focused_pane_info_payload = 134;
}
}
@@ -288,6 +309,10 @@ message NewTabPayload {
optional string cwd = 2;
}
+message DumpSessionLayoutPayload {
+ optional uint32 tab_index = 1;
+}
+
message RunActionPayload {
action.Action action = 1;
repeated ContextItem context = 2;
@@ -755,3 +780,115 @@ message GetPanePidResponse {
string error = 2;
}
}
+
+message SaveLayoutResponse {
+ oneof result {
+ bool success = 1;
+ string error = 2;
+ }
+}
+
+message DeleteLayoutResponse {
+ oneof result {
+ bool success = 1;
+ string error = 2;
+ }
+}
+
+message EditLayoutResponse {
+ oneof result {
+ bool success = 1;
+ string error = 2;
+ }
+}
+
+message GenerateRandomNameResponse {
+ string name = 1;
+}
+
+message OverrideLayoutPayload {
+ event.LayoutInfo layout_info = 1;
+ repeated ContextItem context = 2;
+ bool retain_existing_terminal_panes = 3;
+ bool retain_existing_plugin_panes = 4;
+ bool apply_only_to_active_tab = 5;
+}
+
+message SaveLayoutPayload {
+ string layout_name = 1;
+ string layout_kdl = 2;
+ bool overwrite = 3;
+}
+
+message DeleteLayoutPayload {
+ string layout_name = 1;
+}
+
+message EditLayoutPayload {
+ string layout_name = 1;
+ repeated ContextItem context = 2;
+}
+
+message GenerateRandomNamePayload {}
+
+message DumpLayoutPayload {
+ string layout_name = 1;
+}
+
+message DumpLayoutResponse {
+ oneof result {
+ string layout_content = 1;
+ string error = 2;
+ }
+}
+
+message DumpSessionLayoutResponse {
+ oneof result {
+ string layout_content = 1;
+ string error = 2;
+ }
+ optional api.event.LayoutMetadata metadata = 3;
+}
+
+message ParseLayoutPayload {
+ string layout_string = 1;
+}
+
+message ParseLayoutResponse {
+ oneof result {
+ api.event.LayoutMetadata metadata = 1;
+ api.event.LayoutParsingError error = 2;
+ }
+}
+
+message RenameLayoutPayload {
+ string old_layout_name = 1;
+ string new_layout_name = 2;
+}
+
+message RenameLayoutResponse {
+ oneof result {
+ bool success = 1;
+ string error = 2;
+ }
+}
+
+message GetLayoutDirPayload {}
+
+message GetLayoutDirResponse {
+ string layout_dir = 1;
+}
+
+message GetFocusedPaneInfoPayload {}
+
+message GetFocusedPaneInfoResponse {
+ oneof result {
+ FocusedPaneInfo focused_pane_info = 1;
+ string error = 2;
+ }
+}
+
+message FocusedPaneInfo {
+ uint32 focused_tab_index = 1;
+ PaneId focused_pane_id = 2;
+}
diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs
index 542e1ea2..8c4bf451 100644
--- a/zellij-utils/src/plugin_api/plugin_command.rs
+++ b/zellij-utils/src/plugin_api/plugin_command.rs
@@ -3,35 +3,48 @@ pub use super::generated_api::api::{
event::{EventNameList as ProtobufEventNameList, Header},
input_mode::InputMode as ProtobufInputMode,
plugin_command::{
- get_pane_pid_response, plugin_command::Payload, BreakPanesToNewTabPayload,
- BreakPanesToTabWithIndexPayload, ChangeFloatingPanesCoordinatesPayload,
- ChangeHostFolderPayload, ClearScreenForPaneIdPayload, CliPipeOutputPayload,
- CloseMultiplePanesPayload, CloseTabWithIndexPayload, CommandName, ContextItem,
- CopyToClipboardPayload, CreateTokenResponse as ProtobufCreateTokenResponse,
- CreateTokenResponse, CursorPosition, EditScrollbackForPaneWithIdPayload,
- EmbedMultiplePanesPayload, EnvVariable, ExecCmdPayload,
+ delete_layout_response, dump_layout_response, dump_session_layout_response,
+ edit_layout_response, get_focused_pane_info_response, get_pane_pid_response,
+ parse_layout_response, plugin_command::Payload, rename_layout_response,
+ save_layout_response, BreakPanesToNewTabPayload, BreakPanesToTabWithIndexPayload,
+ ChangeFloatingPanesCoordinatesPayload, ChangeHostFolderPayload,
+ ClearScreenForPaneIdPayload, CliPipeOutputPayload, CloseMultiplePanesPayload,
+ CloseTabWithIndexPayload, CommandName, ContextItem, CopyToClipboardPayload,
+ CreateTokenResponse as ProtobufCreateTokenResponse, CreateTokenResponse, CursorPosition,
+ DeleteLayoutPayload, DeleteLayoutResponse as ProtobufDeleteLayoutResponse,
+ DumpLayoutPayload, DumpLayoutResponse as ProtobufDumpLayoutResponse,
+ DumpSessionLayoutPayload, DumpSessionLayoutResponse as ProtobufDumpSessionLayoutResponse,
+ EditLayoutPayload, EditLayoutResponse as ProtobufEditLayoutResponse,
+ EditScrollbackForPaneWithIdPayload, EmbedMultiplePanesPayload, EnvVariable, ExecCmdPayload,
FixedOrPercent as ProtobufFixedOrPercent,
FixedOrPercentValue as ProtobufFixedOrPercentValue, FloatMultiplePanesPayload,
- FloatingPaneCoordinates as ProtobufFloatingPaneCoordinates, GenerateWebLoginTokenPayload,
- GetPanePidPayload, GetPanePidResponse as ProtobufGetPanePidResponse,
- GetPaneScrollbackPayload, GroupAndUngroupPanesPayload, HidePaneWithIdPayload,
- HighlightAndUnhighlightPanesPayload, HttpVerb as ProtobufHttpVerb, IdAndNewName,
- KeyToRebind, KeyToUnbind, KillSessionsPayload, ListTokensResponse, LoadNewPluginPayload,
- MessageToPluginPayload, MovePaneWithPaneIdInDirectionPayload, MovePaneWithPaneIdPayload,
- MovePayload, NewPluginArgs as ProtobufNewPluginArgs, NewTabPayload,
- NewTabsWithLayoutInfoPayload, OpenCommandPaneFloatingNearPluginPayload,
- OpenCommandPaneInPlaceOfPluginPayload, OpenCommandPaneNearPluginPayload,
- OpenCommandPanePayload, OpenFileFloatingNearPluginPayload, OpenFileInPlaceOfPluginPayload,
+ FloatingPaneCoordinates as ProtobufFloatingPaneCoordinates, FocusedPaneInfo,
+ GenerateRandomNamePayload,
+ GenerateRandomNameResponse as ProtobufGenerateRandomNameResponse,
+ GenerateWebLoginTokenPayload, GetFocusedPaneInfoPayload,
+ GetFocusedPaneInfoResponse as ProtobufGetFocusedPaneInfoResponse, GetLayoutDirPayload,
+ GetLayoutDirResponse as ProtobufGetLayoutDirResponse, GetPanePidPayload,
+ GetPanePidResponse as ProtobufGetPanePidResponse, GetPaneScrollbackPayload,
+ GroupAndUngroupPanesPayload, HidePaneWithIdPayload, HighlightAndUnhighlightPanesPayload,
+ HttpVerb as ProtobufHttpVerb, IdAndNewName, KeyToRebind, KeyToUnbind, KillSessionsPayload,
+ ListTokensResponse, LoadNewPluginPayload, MessageToPluginPayload,
+ MovePaneWithPaneIdInDirectionPayload, MovePaneWithPaneIdPayload, MovePayload,
+ NewPluginArgs as ProtobufNewPluginArgs, NewTabPayload, NewTabsWithLayoutInfoPayload,
+ OpenCommandPaneFloatingNearPluginPayload, OpenCommandPaneInPlaceOfPluginPayload,
+ OpenCommandPaneNearPluginPayload, OpenCommandPanePayload,
+ OpenFileFloatingNearPluginPayload, OpenFileInPlaceOfPluginPayload,
OpenFileNearPluginPayload, OpenFilePayload, OpenTerminalFloatingNearPluginPayload,
- OpenTerminalInPlaceOfPluginPayload, OpenTerminalNearPluginPayload,
+ OpenTerminalInPlaceOfPluginPayload, OpenTerminalNearPluginPayload, OverrideLayoutPayload,
PageScrollDownInPaneIdPayload, PageScrollUpInPaneIdPayload, PaneId as ProtobufPaneId,
- PaneIdAndFloatingPaneCoordinates, PaneType as ProtobufPaneType,
- PluginCommand as ProtobufPluginCommand, PluginMessagePayload, RebindKeysPayload,
- ReconfigurePayload, ReloadPluginPayload, RenameWebLoginTokenPayload,
- RenameWebTokenResponse, ReplacePaneWithExistingPanePayload, RequestPluginPermissionPayload,
- RerunCommandPanePayload, ResizePaneIdWithDirectionPayload, ResizePayload,
- RevokeAllWebTokensResponse, RevokeTokenResponse, RevokeWebLoginTokenPayload,
- RunActionPayload, RunCommandPayload, ScrollDownInPaneIdPayload,
+ PaneIdAndFloatingPaneCoordinates, PaneType as ProtobufPaneType, ParseLayoutPayload,
+ ParseLayoutResponse as ProtobufParseLayoutResponse, PluginCommand as ProtobufPluginCommand,
+ PluginMessagePayload, RebindKeysPayload, ReconfigurePayload, ReloadPluginPayload,
+ RenameLayoutPayload, RenameLayoutResponse as ProtobufRenameLayoutResponse,
+ RenameWebLoginTokenPayload, RenameWebTokenResponse, ReplacePaneWithExistingPanePayload,
+ RequestPluginPermissionPayload, RerunCommandPanePayload, ResizePaneIdWithDirectionPayload,
+ ResizePayload, RevokeAllWebTokensResponse, RevokeTokenResponse, RevokeWebLoginTokenPayload,
+ RunActionPayload, RunCommandPayload, SaveLayoutPayload,
+ SaveLayoutResponse as ProtobufSaveLayoutResponse, ScrollDownInPaneIdPayload,
ScrollToBottomInPaneIdPayload, ScrollToTopInPaneIdPayload, ScrollUpInPaneIdPayload,
SetFloatingPanePinnedPayload, SetSelfMouseSelectionSupportPayload, SetTimeoutPayload,
ShowCursorPayload, ShowPaneWithIdPayload, StackPanesPayload, SubscribePayload,
@@ -44,8 +57,10 @@ pub use super::generated_api::api::{
};
use crate::data::{
- ConnectToSession, FloatingPaneCoordinates, GetPanePidResponse, HttpVerb, InputMode,
- KeyWithModifier, MessageToPlugin, NewPluginArgs, PaneId, PermissionType, PluginCommand,
+ ConnectToSession, DeleteLayoutResponse, EditLayoutResponse, FloatingPaneCoordinates,
+ GetFocusedPaneInfoResponse, GetPanePidResponse, HttpVerb, InputMode, KeyWithModifier,
+ MessageToPlugin, NewPluginArgs, PaneId, PermissionType, PluginCommand, RenameLayoutResponse,
+ SaveLayoutResponse,
};
use crate::input::actions::Action;
use crate::input::layout::SplitSize;
@@ -227,6 +242,135 @@ impl From<GetPanePidResponse> for ProtobufGetPanePidResponse {
}
}
+impl From<GetFocusedPaneInfoResponse> for ProtobufGetFocusedPaneInfoResponse {
+ fn from(response: GetFocusedPaneInfoResponse) -> Self {
+ match response {
+ GetFocusedPaneInfoResponse::Ok { tab_index, pane_id } => {
+ let protobuf_pane_id = ProtobufPaneId {
+ pane_type: match pane_id {
+ PaneId::Terminal(_) => ProtobufPaneType::Terminal as i32,
+ PaneId::Plugin(_) => ProtobufPaneType::Plugin as i32,
+ },
+ id: match pane_id {
+ PaneId::Terminal(id) | PaneId::Plugin(id) => id,
+ },
+ };
+ ProtobufGetFocusedPaneInfoResponse {
+ result: Some(get_focused_pane_info_response::Result::FocusedPaneInfo(
+ FocusedPaneInfo {
+ focused_tab_index: tab_index as u32,
+ focused_pane_id: Some(protobuf_pane_id),
+ },
+ )),
+ }
+ },
+ GetFocusedPaneInfoResponse::Err(err) => ProtobufGetFocusedPaneInfoResponse {
+ result: Some(get_focused_pane_info_response::Result::Error(err)),
+ },
+ }
+ }
+}
+
+impl TryFrom<ProtobufSaveLayoutResponse> for SaveLayoutResponse {
+ type Error = &'static str;
+ fn try_from(protobuf_response: ProtobufSaveLayoutResponse) -> Result<Self, &'static str> {
+ match protobuf_response.result {
+ Some(save_layout_response::Result::Success(_)) => Ok(SaveLayoutResponse::Ok(())),
+ Some(save_layout_response::Result::Error(error)) => Ok(SaveLayoutResponse::Err(error)),
+ None => Err("Empty SaveLayoutResponse"),
+ }
+ }
+}
+
+impl From<SaveLayoutResponse> for ProtobufSaveLayoutResponse {
+ fn from(response: SaveLayoutResponse) -> Self {
+ match response {
+ SaveLayoutResponse::Ok(_) => ProtobufSaveLayoutResponse {
+ result: Some(save_layout_response::Result::Success(true)),
+ },
+ SaveLayoutResponse::Err(error) => ProtobufSaveLayoutResponse {
+ result: Some(save_layout_response::Result::Error(error)),
+ },
+ }
+ }
+}
+
+impl TryFrom<ProtobufDeleteLayoutResponse> for DeleteLayoutResponse {
+ type Error = &'static str;
+ fn try_from(protobuf_response: ProtobufDeleteLayoutResponse) -> Result<Self, &'static str> {
+ match protobuf_response.result {
+ Some(delete_layout_response::Result::Success(_)) => Ok(DeleteLayoutResponse::Ok(())),
+ Some(delete_layout_response::Result::Error(error)) => {
+ Ok(DeleteLayoutResponse::Err(error))
+ },
+ None => Err("Empty DeleteLayoutResponse"),
+ }
+ }
+}
+
+impl From<DeleteLayoutResponse> for ProtobufDeleteLayoutResponse {
+ fn from(response: DeleteLayoutResponse) -> Self {
+ match response {
+ DeleteLayoutResponse::Ok(_) => ProtobufDeleteLayoutResponse {
+ result: Some(delete_layout_response::Result::Success(true)),
+ },
+ DeleteLayoutResponse::Err(error) => ProtobufDeleteLayoutResponse {
+ result: Some(delete_layout_response::Result::Error(error)),
+ },
+ }
+ }
+}
+
+impl TryFrom<ProtobufRenameLayoutResponse> for RenameLayoutResponse {
+ type Error = &'static str;
+ fn try_from(protobuf_response: ProtobufRenameLayoutResponse) -> Result<Self, &'static str> {
+ match protobuf_response.result {
+ Some(rename_layout_response::Result::Success(_)) => Ok(RenameLayoutResponse::Ok(())),
+ Some(rename_layout_response::Result::Error(error)) => {
+ Ok(RenameLayoutResponse::Err(error))
+ },
+ None => Err("Empty RenameLayoutResponse"),
+ }
+ }
+}
+
+impl From<RenameLayoutResponse> for ProtobufRenameLayoutResponse {
+ fn from(response: RenameLayoutResponse) -> Self {
+ match response {
+ RenameLayoutResponse::Ok(_) => ProtobufRenameLayoutResponse {
+ result: Some(rename_layout_response::Result::Success(true)),
+ },
+ RenameLayoutResponse::Err(error) => ProtobufRenameLayoutResponse {
+ result: Some(rename_layout_response::Result::Error(error)),
+ },
+ }
+ }
+}
+
+impl TryFrom<ProtobufEditLayoutResponse> for EditLayoutResponse {
+ type Error = &'static str;
+ fn try_from(protobuf_response: ProtobufEditLayoutResponse) -> Result<Self, &'static str> {
+ match protobuf_response.result {
+ Some(edit_layout_response::Result::Success(_)) => Ok(EditLayoutResponse::Ok(())),
+ Some(edit_layout_response::Result::Error(error)) => Ok(EditLayoutResponse::Err(error)),
+ None => Err("Empty EditLayoutResponse"),
+ }
+ }
+}
+
+impl From<EditLayoutResponse> for ProtobufEditLayoutResponse {
+ fn from(response: EditLayoutResponse) -> Self {
+ match response {
+ EditLayoutResponse::Ok(_) => ProtobufEditLayoutResponse {
+ result: Some(edit_layout_response::Result::Success(true)),
+ },
+ EditLayoutResponse::Err(error) => ProtobufEditLayoutResponse {
+ result: Some(edit_layout_response::Result::Error(error)),
+ },
+ }
+ }
+}
+
impl TryFrom<(InputMode, KeyWithModifier, Vec<Action>)> for KeyToRebind {
type Error = &'static str;
fn try_from(
@@ -1047,8 +1191,13 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
None => Ok(PluginCommand::WatchFilesystem),
},
Some(CommandName::DumpSessionLayout) => match protobuf_plugin_command.payload {
- Some(_) => Err("DumpSessionLayout should have no payload, found a payload"),
- None => Ok(PluginCommand::DumpSessionLayout),
+ Some(Payload::DumpSessionLayoutPayload(payload)) => {
+ Ok(PluginCommand::DumpSessionLayout {
+ tab_index: payload.tab_index.map(|i| i as usize),
+ })
+ },
+ None => Ok(PluginCommand::DumpSessionLayout { tab_index: None }),
+ _ => Err("Mismatched payload for DumpSessionLayout"),
},
Some(CommandName::CloseSelf) => match protobuf_plugin_command.payload {
Some(_) => Err("CloseSelf should have no payload, found a payload"),
@@ -1215,6 +1364,70 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
},
_ => Err("Mismatched payload for GetPanePid"),
},
+ Some(CommandName::OverrideLayout) => match protobuf_plugin_command.payload {
+ Some(Payload::OverrideLayoutPayload(override_layout_payload)) => {
+ let layout_info = override_layout_payload
+ .layout_info
+ .ok_or("OverrideLayout missing layout_info")?
+ .try_into()
+ .map_err(|_| "Failed to parse LayoutInfo")?;
+ let context = override_layout_payload
+ .context
+ .into_iter()
+ .map(|c| (c.name, c.value))
+ .collect();
+ Ok(PluginCommand::OverrideLayout(
+ layout_info,
+ override_layout_payload.retain_existing_terminal_panes,
+ override_layout_payload.retain_existing_plugin_panes,
+ override_layout_payload.apply_only_to_active_tab,
+ context,
+ ))
+ },
+ _ => Err("Mismatched payload for OverrideLayout"),
+ },
+ Some(CommandName::SaveLayout) => match protobuf_plugin_command.payload {
+ Some(Payload::SaveLayoutPayload(save_layout_payload)) => {
+ Ok(PluginCommand::SaveLayout {
+ layout_name: save_layout_payload.layout_name,
+ layout_kdl: save_layout_payload.layout_kdl,
+ overwrite: save_layout_payload.overwrite,
+ })
+ },
+ _ => Err("Mismatched payload for SaveLayout"),
+ },
+ Some(CommandName::DeleteLayout) => match protobuf_plugin_command.payload {
+ Some(Payload::DeleteLayoutPayload(delete_layout_payload)) => {
+ Ok(PluginCommand::DeleteLayout {
+ layout_name: delete_layout_payload.layout_name,
+ })
+ },
+ _ => Err("Mismatched payload for DeleteLayout"),
+ },
+ Some(CommandName::RenameLayout) => match protobuf_plugin_command.payload {
+ Some(Payload::RenameLayoutPayload(rename_layout_payload)) => {
+ Ok(PluginCommand::RenameLayout {
+ old_layout_name: rename_layout_payload.old_layout_name,
+ new_layout_name: rename_layout_payload.new_layout_name,
+ })
+ },
+ _ => Err("Mismatched payload for RenameLayout"),
+ },
+ Some(CommandName::EditLayout) => match protobuf_plugin_command.payload {
+ Some(Payload::EditLayoutPayload(edit_layout_payload)) => {
+ let layout_name = edit_layout_payload.layout_name;
+ let context: BTreeMap<String, String> = edit_layout_payload
+ .context
+ .into_iter()
+ .map(|e| (e.name, e.value))
+ .collect();
+ Ok(PluginCommand::EditLayout {
+ layout_name,
+ context,
+ })
+ },
+ _ => Err("Mismatched payload for EditLayout"),
+ },
Some(CommandName::MovePaneWithPaneId) => match protobuf_plugin_command.payload {
Some(Payload::MovePaneWithPaneIdPayload(move_pane_with_pane_id_payload)) => {
match move_pane_with_pane_id_payload.pane_id {
@@ -1845,6 +2058,21 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
},
_ => Err("Mismatched payload for CopyToClipboard"),
},
+ Some(CommandName::GenerateRandomName) => Ok(PluginCommand::GenerateRandomName),
+ Some(CommandName::DumpLayout) => match protobuf_plugin_command.payload {
+ Some(Payload::DumpLayoutPayload(payload)) => {
+ Ok(PluginCommand::DumpLayout(payload.layout_name))
+ },
+ _ => Err("Mismatched payload for DumpLayout"),
+ },
+ Some(CommandName::ParseLayout) => match protobuf_plugin_command.payload {
+ Some(Payload::ParseLayoutPayload(payload)) => {
+ Ok(PluginCommand::ParseLayout(payload.layout_string))
+ },
+ _ => Err("Mismatched payload for ParseLayout"),
+ },
+ Some(CommandName::GetLayoutDir) => Ok(PluginCommand::GetLayoutDir),
+ Some(CommandName::GetFocusedPaneInfo) => Ok(PluginCommand::GetFocusedPaneInfo),
None => Err("Unrecognized plugin command"),
}
}
@@ -2417,9 +2645,13 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
name: CommandName::WatchFilesystem as i32,
payload: None,
}),
- PluginCommand::DumpSessionLayout => Ok(ProtobufPluginCommand {
+ PluginCommand::DumpSessionLayout { tab_index } => Ok(ProtobufPluginCommand {
name: CommandName::DumpSessionLayout as i32,
- payload: None,
+ payload: tab_index.map(|idx| {
+ Payload::DumpSessionLayoutPayload(DumpSessionLayoutPayload {
+ tab_index: Some(idx as u32),
+ })
+ }),
}),
PluginCommand::CloseSelf => Ok(ProtobufPluginCommand {
name: CommandName::CloseSelf as i32,
@@ -2545,6 +2777,69 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
pane_id: Some(pane_id.try_into()?),
})),
}),
+ PluginCommand::OverrideLayout(
+ layout_info,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ apply_only_to_active_tab,
+ context,
+ ) => Ok(ProtobufPluginCommand {
+ name: CommandName::OverrideLayout as i32,
+ payload: Some(Payload::OverrideLayoutPayload(OverrideLayoutPayload {
+ layout_info: layout_info.try_into().ok(),
+ context: context
+ .into_iter()
+ .map(|(name, value)| ContextItem { name, value })
+ .collect(),
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ apply_only_to_active_tab,
+ })),
+ }),
+ PluginCommand::SaveLayout {
+ layout_name,
+ layout_kdl,
+ overwrite,
+ } => Ok(ProtobufPluginCommand {
+ name: CommandName::SaveLayout as i32,
+ payload: Some(Payload::SaveLayoutPayload(SaveLayoutPayload {
+ layout_name,
+ layout_kdl,
+ overwrite,
+ })),
+ }),
+ PluginCommand::DeleteLayout { layout_name } => Ok(ProtobufPluginCommand {
+ name: CommandName::DeleteLayout as i32,
+ payload: Some(Payload::DeleteLayoutPayload(DeleteLayoutPayload {
+ layout_name,
+ })),
+ }),
+ PluginCommand::RenameLayout {
+ old_layout_name,
+ new_layout_name,
+ } => Ok(ProtobufPluginCommand {
+ name: CommandName::RenameLayout as i32,
+ payload: Some(Payload::RenameLayoutPayload(RenameLayoutPayload {
+ old_layout_name,
+ new_layout_name,
+ })),
+ }),
+ PluginCommand::EditLayout {
+ layout_name,
+ context,
+ } => {
+ let context: Vec<_> = context
+ .into_iter()
+ .map(|(name, value)| ContextItem { name, value })
+ .collect();
+ Ok(ProtobufPluginCommand {
+ name: CommandName::EditLayout as i32,
+ payload: Some(Payload::EditLayoutPayload(EditLayoutPayload {
+ layout_name,
+ context,
+ })),
+ })
+ },
PluginCommand::MovePaneWithPaneId(pane_id) => Ok(ProtobufPluginCommand {
name: CommandName::MovePaneWithPaneId as i32,
payload: Some(Payload::MovePaneWithPaneIdPayload(
@@ -3073,6 +3368,34 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
text,
})),
}),
+ PluginCommand::GenerateRandomName => Ok(ProtobufPluginCommand {
+ name: CommandName::GenerateRandomName as i32,
+ payload: Some(Payload::GenerateRandomNamePayload(
+ GenerateRandomNamePayload {},
+ )),
+ }),
+ PluginCommand::DumpLayout(layout_name) => Ok(ProtobufPluginCommand {
+ name: CommandName::DumpLayout as i32,
+ payload: Some(Payload::DumpLayoutPayload(DumpLayoutPayload {
+ layout_name,
+ })),
+ }),
+ PluginCommand::ParseLayout(layout_string) => Ok(ProtobufPluginCommand {
+ name: CommandName::ParseLayout as i32,
+ payload: Some(Payload::ParseLayoutPayload(ParseLayoutPayload {
+ layout_string,
+ })),
+ }),
+ PluginCommand::GetLayoutDir => Ok(ProtobufPluginCommand {
+ name: CommandName::GetLayoutDir as i32,
+ payload: Some(Payload::GetLayoutDirPayload(GetLayoutDirPayload {})),
+ }),
+ PluginCommand::GetFocusedPaneInfo => Ok(ProtobufPluginCommand {
+ name: CommandName::GetFocusedPaneInfo as i32,
+ payload: Some(Payload::GetFocusedPaneInfoPayload(
+ GetFocusedPaneInfoPayload {},
+ )),
+ }),
}
}
}
diff --git a/zellij-utils/src/sessions.rs b/zellij-utils/src/sessions.rs
index b72be064..57d79779 100644
--- a/zellij-utils/src/sessions.rs
+++ b/zellij-utils/src/sessions.rs
@@ -493,6 +493,12 @@ pub fn get_name_generator() -> impl Iterator<Item = String> {
names::Generator::new(&ADJECTIVES, &NOUNS, names::Name::Plain)
}
+/// Generates a random human-readable name using curated adjectives and nouns.
+/// Returns a single name in the format: AdjectiveNoun (e.g., "BraveRustacean")
+pub fn generate_random_name() -> String {
+ get_name_generator().next().unwrap()
+}
+
const ADJECTIVES: &[&'static str] = &[
"adamant",
"adept",
diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap
index 3e60c317..9635303b 100644
--- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap
+++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap
@@ -4326,6 +4326,36 @@ Config {
bare_key: Char(
'l',
),
+ key_modifiers: {},
+ }: [
+ LaunchOrFocusPlugin {
+ plugin: RunPlugin(
+ RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: Zellij(
+ PluginTag(
+ "layout-manager",
+ ),
+ ),
+ configuration: PluginUserConfiguration(
+ {},
+ ),
+ initial_cwd: None,
+ },
+ ),
+ should_float: true,
+ move_to_focused_tab: true,
+ should_open_in_place: false,
+ skip_cache: false,
+ },
+ SwitchToMode {
+ input_mode: Normal,
+ },
+ ],
+ KeyWithModifier {
+ bare_key: Char(
+ 'l',
+ ),
key_modifiers: {
Alt,
},
diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap
index e0fa22f9..879be0ef 100644
--- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap
+++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap
@@ -4326,6 +4326,36 @@ Config {
bare_key: Char(
'l',
),
+ key_modifiers: {},
+ }: [
+ LaunchOrFocusPlugin {
+ plugin: RunPlugin(
+ RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: Zellij(
+ PluginTag(
+ "layout-manager",
+ ),
+ ),
+ configuration: PluginUserConfiguration(
+ {},
+ ),
+ initial_cwd: None,
+ },
+ ),
+ should_float: true,
+ move_to_focused_tab: true,
+ should_open_in_place: false,
+ skip_cache: false,
+ },
+ SwitchToMode {
+ input_mode: Normal,
+ },
+ ],
+ KeyWithModifier {
+ bare_key: Char(
+ 'l',
+ ),
key_modifiers: {
Alt,
},
diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap
index e9935ef3..bc5119f7 100644
--- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap
+++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap
@@ -4326,6 +4326,36 @@ Config {
bare_key: Char(
'l',
),
+ key_modifiers: {},
+ }: [
+ LaunchOrFocusPlugin {
+ plugin: RunPlugin(
+ RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: Zellij(
+ PluginTag(
+ "layout-manager",
+ ),
+ ),
+ configuration: PluginUserConfiguration(
+ {},
+ ),
+ initial_cwd: None,
+ },
+ ),
+ should_float: true,
+ move_to_focused_tab: true,
+ should_open_in_place: false,
+ skip_cache: false,
+ },
+ SwitchToMode {
+ input_mode: Normal,
+ },
+ ],
+ KeyWithModifier {
+ bare_key: Char(
+ 'l',
+ ),
key_modifiers: {
Alt,
},
diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap
index 368b5d88..477e152b 100644
--- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap
+++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap
@@ -4326,6 +4326,36 @@ Config {
bare_key: Char(
'l',
),
+ key_modifiers: {},
+ }: [
+ LaunchOrFocusPlugin {
+ plugin: RunPlugin(
+ RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: Zellij(
+ PluginTag(
+ "layout-manager",
+ ),
+ ),
+ configuration: PluginUserConfiguration(
+ {},
+ ),
+ initial_cwd: None,
+ },
+ ),
+ should_float: true,
+ move_to_focused_tab: true,
+ should_open_in_place: false,
+ skip_cache: false,
+ },
+ SwitchToMode {
+ input_mode: Normal,
+ },
+ ],
+ KeyWithModifier {
+ bare_key: Char(
+ 'l',
+ ),
key_modifiers: {
Alt,
},