aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2025-12-23 13:41:56 +0100
committerGitHub <noreply@github.com>2025-12-23 13:41:56 +0100
commit3fe48a972c55537502128779116d38d8f8aedb7e (patch)
tree350386442db2a46e1d8ba3d9d4dd0c7d53b34c13
parentRead only web tokens (#4551) (diff)
downloadsl0ppij-3fe48a972c55537502128779116d38d8f8aedb7e.tar.gz
sl0ppij-3fe48a972c55537502128779116d38d8f8aedb7e.tar.xz
sl0ppij-3fe48a972c55537502128779116d38d8f8aedb7e.zip
Feature: allow overriding a layout for a currently running tab (change layout at runtime) (#4566)
* add tests for LayoutApplier * initial untested implementation of override functions * add override-layout cli action with stub in screen * filter run instructions to ignore * implement override-layout roundtrip through plugin and pty threads * initial full-cycle working override implementation * fix some bugs (eg. actually start new terminals) * fix some bugs with focus after override * fix floating pane matching bug in layout applier * formatting * use first tab, error on more than 1 tab * fix some floating_panes ordering and shell issues * get new tab name to work * respect swap layouts * allow specifying tab properties on main layout node if we just have one tab * add tests for new override methods * start work on retaining panes, hard-coded things, kind of working but broken in many places * fixed most tiled pane override bugs * reset viewport before reapplying * revert constraint system changes * retain-existing-terminal-panes flag * add retain terminal panes tests * add retain plugin panes flag * add keybind * add retain plugin panes tests * add override-layout e2e test * refactor: layout_applier * style: rustfmt * some cleanups * fix cli flags * no crashy * formatting and artifacts * moar cleanups * fix: respect expanded in stack when overriding * docs(changelog): add PR
-rw-r--r--CHANGELOG.md1
-rw-r--r--src/tests/e2e/cases.rs49
-rw-r--r--src/tests/e2e/remote_runner.rs13
-rw-r--r--src/tests/e2e/snapshots/zellij__tests__e2e__cases__override_layout_from_default_to_compact.snap28
-rw-r--r--zellij-client/src/web_client/server_listener.rs2
-rw-r--r--zellij-client/src/web_client/session_management.rs51
-rw-r--r--zellij-client/src/web_client/types.rs3
-rw-r--r--zellij-server/src/panes/floating_panes/mod.rs12
-rw-r--r--zellij-server/src/panes/tiled_panes/mod.rs104
-rw-r--r--zellij-server/src/plugins/mod.rs127
-rw-r--r--zellij-server/src/pty.rs234
-rw-r--r--zellij-server/src/route.rs35
-rw-r--r--zellij-server/src/screen.rs161
-rw-r--r--zellij-server/src/tab/layout_applier.rs318
-rw-r--r--zellij-server/src/tab/mod.rs114
-rw-r--r--zellij-server/src/tab/swap_layouts.rs11
-rw-r--r--zellij-server/src/tab/unit/layout_applier_tests.rs7100
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_complex_nested_layout.snap48
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_empty_layout.snap9
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_horizontal_split_with_sizes.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_borderless_panes.snap32
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_commands.snap32
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_excess_terminal_ids.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_floating_pane_with_command.snap26
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_floating_panes.snap34
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_focus.snap32
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_missing_terminal_ids.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_mixed_plugin_and_terminal_panes.snap40
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_mixed_tiled_and_floating_panes.snap51
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_multiple_stacks.snap53
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_named_panes.snap32
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_plugin_panes.snap32
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_stacked_panes.snap43
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_nested_layout.snap32
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_simple_two_pane_layout.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_three_pane_layout.snap32
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_vertical_split_with_sizes.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_all_closed_no_matches.snap35
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_exact_match_preservation.snap26
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_focus_handling.snap26
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_new_panes_created.snap44
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_position_and_size_update.snap26
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_retain_terminal_panes_no_matches.snap53
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_retain_terminal_panes_partial_match.snap35
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_return_value_has_panes.snap26
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_return_value_no_panes.snap16
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_full_tiled_and_floating_together.snap34
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_layout_basic_with_both_tiled_and_floating.snap34
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_layout_hide_floating_panes_true.snap34
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_layout_show_floating_panes.snap26
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_mixed_retain_terminal_panes_both_tiled_and_floating.snap59
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_retain_plugin_but_close_terminal_panes.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_retain_terminal_but_close_plugin_panes.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_all_panes_closed_no_matches.snap32
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_empty_layout_closes_all.snap9
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_exact_match_preservation_commands.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_exact_match_preservation_plugins.snap16
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_focus_on_new_pane.snap32
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_focus_when_focused_pane_closed.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_mixed_some_matches_some_new.snap32
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_new_panes_for_unmatched_positions.snap40
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_retain_terminal_panes_no_matches.snap56
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_retain_terminal_panes_partial_match.snap32
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_viewport_adjustment_with_borderless.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__reapply_floating_pane_layout.snap26
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__reapply_layout_exact_match.snap32
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__reapply_layout_logical_position_match.snap32
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__reapply_layout_with_fewer_positions.snap38
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__reapply_layout_with_more_positions.snap24
-rw-r--r--zellij-server/src/thread_bus.rs5
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_name_and_layout.snap12
-rwxr-xr-xzellij-utils/assets/plugins/about.wasmbin1390199 -> 1412064 bytes
-rwxr-xr-xzellij-utils/assets/plugins/compact-bar.wasmbin1548034 -> 1571455 bytes
-rwxr-xr-xzellij-utils/assets/plugins/configuration.wasmbin1450980 -> 1473897 bytes
-rwxr-xr-xzellij-utils/assets/plugins/fixture-plugin-for-tests.wasmbin1445407 -> 1467669 bytes
-rwxr-xr-xzellij-utils/assets/plugins/multiple-select.wasmbin1382984 -> 1405967 bytes
-rwxr-xr-xzellij-utils/assets/plugins/plugin-manager.wasmbin1363473 -> 1385313 bytes
-rwxr-xr-xzellij-utils/assets/plugins/sequence.wasmbin1419868 -> 1441780 bytes
-rwxr-xr-xzellij-utils/assets/plugins/session-manager.wasmbin1445706 -> 1467557 bytes
-rwxr-xr-xzellij-utils/assets/plugins/share.wasmbin1353298 -> 1380992 bytes
-rwxr-xr-xzellij-utils/assets/plugins/status-bar.wasmbin1558406 -> 1582131 bytes
-rwxr-xr-xzellij-utils/assets/plugins/strider.wasmbin1512515 -> 1534669 bytes
-rwxr-xr-xzellij-utils/assets/plugins/tab-bar.wasmbin1438990 -> 1462479 bytes
-rw-r--r--zellij-utils/assets/prost/api.action.rs25
-rw-r--r--zellij-utils/assets/prost_ipc/client_server_contract.rs22
-rw-r--r--zellij-utils/src/cli.rs18
-rw-r--r--zellij-utils/src/client_server_contract/common_types.proto10
-rw-r--r--zellij-utils/src/errors.rs7
-rw-r--r--zellij-utils/src/input/actions.rs95
-rw-r--r--zellij-utils/src/input/unit/layout_test.rs119
-rw-r--r--zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__env_var_expansion.snap246
-rw-r--r--zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_given_to_panes_without_cwd.snap84
-rw-r--r--zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_passed_from_layout_constructor.snap6
-rw-r--r--zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_passed_from_layout_constructor_overrides_global_cwd_in_layout_file.snap84
-rw-r--r--zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_prepended_to_panes_with_cwd.snap84
-rw-r--r--zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_is_overriden_by_its_consumers_bare_cwd.snap64
-rw-r--r--zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_overriden_by_its_consumers_command_cwd.snap64
-rw-r--r--zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_remains_when_its_consumer_command_does_not_have_a_cwd.snap64
-rw-r--r--zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_without_cwd_is_overriden_by_its_consumers_cwd.snap64
-rw-r--r--zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_without_cwd_receives_its_consumers_bare_cwd.snap64
-rw-r--r--zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_cwd_overriden_by_its_consumers_bare_cwd.snap55
-rw-r--r--zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_command_with_cwd.snap64
-rw-r--r--zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_command_without_cwd.snap64
-rw-r--r--zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_edit.snap59
-rw-r--r--zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_command_propagated_to_its_consumer_edit.snap59
-rw-r--r--zellij-utils/src/ipc.rs20
-rw-r--r--zellij-utils/src/ipc/protobuf_conversion.rs90
-rw-r--r--zellij-utils/src/ipc/tests/roundtrip_tests.rs14
-rw-r--r--zellij-utils/src/kdl/kdl_layout_parser.rs118
-rw-r--r--zellij-utils/src/kdl/mod.rs104
-rw-r--r--zellij-utils/src/plugin_api/action.proto12
-rw-r--r--zellij-utils/src/plugin_api/action.rs101
112 files changed, 11726 insertions, 177 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index acc3317f..17577560 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
* feat: improve cli interface (add detach, switch-session and more) (https://github.com/zellij-org/zellij/pull/4492)
* feat: command sequences, conditionally blocking CLI commands, new plugin APIs (https://github.com/zellij-org/zellij/pull/4546)
* feat: read-only web authentication-tokens (https://github.com/zellij-org/zellij/pull/4551)
+* feat: allow overriding layouts at runtime (https://github.com/zellij-org/zellij/pull/4566)
## [0.43.1] - 2025-08-08
* fix: pane rename backspace regression (https://github.com/zellij-org/zellij/pull/4346)
diff --git a/src/tests/e2e/cases.rs b/src/tests/e2e/cases.rs
index eab81039..f7d9ea0d 100644
--- a/src/tests/e2e/cases.rs
+++ b/src/tests/e2e/cases.rs
@@ -2646,6 +2646,7 @@ pub fn pin_floating_panes() {
let last_snapshot = account_for_races_in_snapshot(last_snapshot);
assert_snapshot!(last_snapshot);
}
+
#[test]
#[ignore]
pub fn watcher_client_functionality() {
@@ -2849,3 +2850,51 @@ pub fn watcher_client_functionality() {
assert_snapshot!(main_client_snapshot);
assert_snapshot!(watcher_snapshot);
}
+
+#[test]
+#[ignore]
+pub fn override_layout_from_default_to_compact() {
+ let fake_win_size = Size {
+ cols: 120,
+ rows: 24,
+ };
+ let mut test_attempts = 10;
+ let last_snapshot = loop {
+ RemoteRunner::kill_running_sessions(fake_win_size);
+
+ let mut runner = RemoteRunner::new(fake_win_size)
+ .add_step(Step {
+ name: "Wait for default layout to load",
+ instruction: |remote_terminal: RemoteTerminal| -> bool {
+ remote_terminal.status_bar_appears()
+ },
+ })
+ .add_step(Step {
+ name: "Override to compact layout",
+ instruction: |mut remote_terminal: RemoteTerminal| -> bool {
+ remote_terminal.run_zellij_action("override-layout compact");
+ std::thread::sleep(std::time::Duration::from_millis(500));
+ remote_terminal.send_key(&ENTER);
+ true
+ },
+ });
+
+ runner.run_all_steps();
+ let last_snapshot = runner.take_snapshot_after(Step {
+ name: "Wait for compact layout to appear",
+ instruction: |remote_terminal: RemoteTerminal| -> bool {
+ remote_terminal.snapshot_contains("NORMAL")
+ },
+ });
+
+ if runner.test_timed_out && test_attempts > 0 {
+ test_attempts -= 1;
+ continue;
+ } else {
+ break last_snapshot;
+ }
+ };
+
+ let last_snapshot = account_for_races_in_snapshot(last_snapshot);
+ assert_snapshot!(last_snapshot);
+}
diff --git a/src/tests/e2e/remote_runner.rs b/src/tests/e2e/remote_runner.rs
index 377869f9..d4663ac5 100644
--- a/src/tests/e2e/remote_runner.rs
+++ b/src/tests/e2e/remote_runner.rs
@@ -419,6 +419,19 @@ impl RemoteTerminal {
channel.flush().unwrap();
std::thread::sleep(std::time::Duration::from_secs(1)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
}
+ pub fn run_zellij_action(&mut self, action_and_arguments: &str) {
+ let mut channel = self.channel.lock().unwrap();
+ channel
+ .write_all(
+ format!(
+ "{} action {}",
+ ZELLIJ_EXECUTABLE_LOCATION, action_and_arguments
+ )
+ .as_bytes(),
+ )
+ .unwrap();
+ channel.flush().unwrap();
+ }
pub fn send_command_through_the_cli(&mut self, command: &str) {
let mut channel = self.channel.lock().unwrap();
channel
diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__override_layout_from_default_to_compact.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__override_layout_from_default_to_compact.snap
new file mode 100644
index 00000000..48724fb5
--- /dev/null
+++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__override_layout_from_default_to_compact.snap
@@ -0,0 +1,28 @@
+---
+source: src/tests/e2e/cases.rs
+expression: last_snapshot
+---
+┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
+│$ /usr/src/zellij/x86_64-unknown-linux-musl/release/zellij action override-layout compact │
+│$ │
+│$ █ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │
+└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
+ Zellij (e2e-test) NORMAL  Tab #1 
diff --git a/zellij-client/src/web_client/server_listener.rs b/zellij-client/src/web_client/server_listener.rs
index bd4979d1..daad1233 100644
--- a/zellij-client/src/web_client/server_listener.rs
+++ b/zellij-client/src/web_client/server_listener.rs
@@ -15,7 +15,7 @@ use zellij_utils::{
data::Style,
input::{config::Config, options::Options},
ipc::{ClientToServerMsg, ExitReason, ServerToClientMsg},
- sessions::{generate_unique_session_name, session_exists},
+ sessions::generate_unique_session_name,
setup::Setup,
};
diff --git a/zellij-client/src/web_client/session_management.rs b/zellij-client/src/web_client/session_management.rs
index e42009ff..d3518b53 100644
--- a/zellij-client/src/web_client/session_management.rs
+++ b/zellij-client/src/web_client/session_management.rs
@@ -141,54 +141,3 @@ pub fn create_ipc_pipe(session_name: &str) -> PathBuf {
};
zellij_ipc_pipe
}
-
-fn ipc_pipe_and_first_message_for_existing_session(
- session_name: &str,
- client_attributes: ClientAttributes,
- config_file_path: Option<PathBuf>,
- mut config_opts: Options,
- is_read_only: bool,
-) -> (ClientToServerMsg, PathBuf) {
- let zellij_ipc_pipe: PathBuf = {
- let mut sock_dir = zellij_utils::consts::ZELLIJ_SOCK_DIR.clone();
- fs::create_dir_all(&sock_dir).unwrap();
- zellij_utils::shared::set_permissions(&sock_dir, 0o700).unwrap();
- sock_dir.push(session_name);
- sock_dir
- };
-
- config_opts.web_server = Some(true);
- config_opts.web_sharing = Some(WebSharing::On);
-
- let is_web_client = true;
- let first_message = if is_read_only {
- // read only clients attach as watchers
- ClientToServerMsg::AttachWatcherClient {
- terminal_size: client_attributes.size,
- is_web_client,
- }
- } else {
- let cli_assets = CliAssets {
- config_file_path,
- config_dir: None,
- should_ignore_config: false,
- configuration_options: Some(config_opts),
- layout: None,
- terminal_window_size: client_attributes.size,
- data_dir: None,
- is_debug: false,
- max_panes: None,
- force_run_layout_commands: false,
- cwd: None,
- };
-
- ClientToServerMsg::AttachClient {
- cli_assets,
- tab_position_to_focus: None,
- pane_to_focus: None,
- is_web_client,
- }
- };
-
- (first_message, zellij_ipc_pipe)
-}
diff --git a/zellij-client/src/web_client/types.rs b/zellij-client/src/web_client/types.rs
index 2994653a..7079973c 100644
--- a/zellij-client/src/web_client/types.rs
+++ b/zellij-client/src/web_client/types.rs
@@ -9,9 +9,8 @@ use crate::os_input_output::ClientOsApi;
use crate::web_client::session_management::spawn_new_session;
use std::path::PathBuf;
use zellij_utils::{
- data::LayoutInfo,
input::{config::Config, options::Options},
- ipc::{ClientAttributes, ClientToServerMsg},
+ ipc::ClientToServerMsg,
};
pub trait ClientOsApiFactory: Send + Sync + std::fmt::Debug {
diff --git a/zellij-server/src/panes/floating_panes/mod.rs b/zellij-server/src/panes/floating_panes/mod.rs
index d347dcd0..a52c78e7 100644
--- a/zellij-server/src/panes/floating_panes/mod.rs
+++ b/zellij-server/src/panes/floating_panes/mod.rs
@@ -264,6 +264,18 @@ impl FloatingPanes {
);
floating_pane_grid.find_room_for_new_pane()
}
+ pub fn move_client_focus_to_existing_panes(&mut self) {
+ let existing_pane_ids: Vec<PaneId> = self.panes.keys().copied().collect();
+ let nonexisting_panes_that_are_focused = self
+ .active_panes
+ .values()
+ .filter(|pane_id| !existing_pane_ids.contains(pane_id))
+ .copied()
+ .collect::<Vec<_>>();
+ for pane_id in nonexisting_panes_that_are_focused {
+ self.move_clients_out_of_pane(pane_id);
+ }
+ }
pub fn position_floating_pane_layout(
&mut self,
floating_pane_layout: &FloatingPaneLayout,
diff --git a/zellij-server/src/panes/tiled_panes/mod.rs b/zellij-server/src/panes/tiled_panes/mod.rs
index 4df68db4..a658f801 100644
--- a/zellij-server/src/panes/tiled_panes/mod.rs
+++ b/zellij-server/src/panes/tiled_panes/mod.rs
@@ -178,6 +178,13 @@ impl TiledPanes {
let should_relayout = true;
self.add_pane(pane_id, pane, should_relayout, client_id);
}
+ pub fn set_pane_logical_position(&mut self, pane_id: PaneId, logical_position: usize) {
+ if let Some(pane) = self.panes.get_mut(&pane_id) {
+ let mut position_and_size = pane.position_and_size();
+ position_and_size.logical_position = Some(logical_position);
+ pane.set_geom(position_and_size);
+ }
+ }
pub fn insert_pane_without_relayout(
&mut self,
pane_id: PaneId,
@@ -1220,8 +1227,12 @@ impl TiledPanes {
let display_area = self.display_area.borrow();
new_screen_size.rows != display_area.rows || new_screen_size.cols != display_area.cols
}
+ pub fn force_resize(&mut self) {
+ let display_area = self.display_area.borrow().clone();
+ self.resize(display_area);
+ }
+
pub fn resize(&mut self, new_screen_size: Size) {
- // this is blocked out to appease the borrow checker
{
if self.display_area_changed(new_screen_size) {
self.clear_tombstones();
@@ -1235,41 +1246,61 @@ impl TiledPanes {
*display_area,
*viewport,
);
- match pane_grid.layout(SplitDirection::Horizontal, cols) {
- Ok(_) => {
- let column_difference = cols as isize - display_area.cols as isize;
- // FIXME: Should the viewport be an Offset?
- viewport.cols = (viewport.cols as isize + column_difference) as usize;
- display_area.cols = cols;
- },
- Err(e) => match e.downcast_ref::<ZellijError>() {
- Some(ZellijError::PaneSizeUnchanged) => {}, // ignore unchanged layout
- _ => {
- // display area still changed, even if we had an error
+
+ let resize_horizontally = |pane_grid: &mut TiledPaneGrid,
+ display_area: &mut Size,
+ viewport: &mut Viewport,
+ cols: usize|
+ -> bool {
+ match pane_grid.layout(SplitDirection::Horizontal, cols) {
+ Ok(_) => {
+ let column_difference = cols as isize - display_area.cols as isize;
+ viewport.cols = (viewport.cols as isize + column_difference) as usize;
display_area.cols = cols;
- Err::<(), _>(anyError::msg(e))
- .context("failed to resize tab horizontally")
- .non_fatal();
+ true
},
- },
+ Err(e) => match e.downcast_ref::<ZellijError>() {
+ Some(ZellijError::PaneSizeUnchanged) => true,
+ _ => false,
+ },
+ }
};
- match pane_grid.layout(SplitDirection::Vertical, rows) {
- Ok(_) => {
- let row_difference = rows as isize - display_area.rows as isize;
- viewport.rows = (viewport.rows as isize + row_difference) as usize;
- display_area.rows = rows;
- },
- Err(e) => match e.downcast_ref::<ZellijError>() {
- Some(ZellijError::PaneSizeUnchanged) => {}, // ignore unchanged layout
- _ => {
- // display area still changed, even if we had an error
+
+ let resize_vertically = |pane_grid: &mut TiledPaneGrid,
+ display_area: &mut Size,
+ viewport: &mut Viewport,
+ rows: usize|
+ -> bool {
+ match pane_grid.layout(SplitDirection::Vertical, rows) {
+ Ok(_) => {
+ let row_difference = rows as isize - display_area.rows as isize;
+ viewport.rows = (viewport.rows as isize + row_difference) as usize;
display_area.rows = rows;
- Err::<(), _>(anyError::msg(e))
- .context("failed to resize tab vertically")
- .non_fatal();
+ true
},
- },
+ Err(e) => match e.downcast_ref::<ZellijError>() {
+ Some(ZellijError::PaneSizeUnchanged) => true,
+ _ => false,
+ },
+ }
};
+
+ let successfully_resized_horizontally =
+ resize_horizontally(&mut pane_grid, &mut display_area, &mut viewport, cols);
+ if successfully_resized_horizontally {
+ resize_vertically(&mut pane_grid, &mut display_area, &mut viewport, rows);
+ } else {
+ log::warn!("Failed to resize horizontally, attempting to first resize vertically");
+ let successfully_resized_vertically =
+ resize_vertically(&mut pane_grid, &mut display_area, &mut viewport, rows);
+ if successfully_resized_vertically {
+ resize_horizontally(&mut pane_grid, &mut display_area, &mut viewport, cols);
+ } else {
+ log::error!("Failed to resize vertically, will not attempt again.");
+ }
+ }
+ display_area.rows = rows;
+ display_area.cols = cols;
}
self.set_pane_frames(self.draw_pane_frames);
}
@@ -2555,6 +2586,7 @@ impl TiledPanes {
self.active_panes.focus_all_panes(&mut self.panes);
}
pub fn drain(&mut self) -> BTreeMap<PaneId, Box<dyn Pane>> {
+ self.unset_fullscreen();
match self.panes.iter().next().map(|(pid, _p)| *pid) {
Some(first_pid) => self.panes.split_off(&first_pid),
None => BTreeMap::new(),
@@ -2566,6 +2598,18 @@ impl TiledPanes {
pub fn set_active_panes(&mut self, active_panes: ActivePanes) {
self.active_panes = active_panes;
}
+ pub fn move_client_focus_to_existing_panes(&mut self) {
+ let existing_pane_ids: Vec<PaneId> = self.panes.keys().copied().collect();
+ let nonexisting_panes_that_are_focused = self
+ .active_panes
+ .values()
+ .filter(|pane_id| !existing_pane_ids.contains(pane_id))
+ .copied()
+ .collect::<Vec<_>>();
+ for pane_id in nonexisting_panes_that_are_focused {
+ self.move_clients_out_of_pane(pane_id);
+ }
+ }
fn move_clients_between_panes(&mut self, from_pane_id: PaneId, to_pane_id: PaneId) {
let clients_in_pane: Vec<ClientId> = self
.active_panes
diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs
index 24a1e811..a8f60b30 100644
--- a/zellij-server/src/plugins/mod.rs
+++ b/zellij-server/src/plugins/mod.rs
@@ -39,7 +39,10 @@ use zellij_utils::{
actions::Action,
command::TerminalAction,
keybinds::Keybinds,
- layout::{FloatingPaneLayout, Layout, Run, RunPlugin, RunPluginOrAlias, TiledPaneLayout},
+ layout::{
+ FloatingPaneLayout, Layout, Run, RunPlugin, RunPluginOrAlias, SwapFloatingLayout,
+ SwapTiledLayout, TiledPaneLayout,
+ },
plugins::PluginAliases,
},
ipc::ClientAttributes,
@@ -94,6 +97,19 @@ pub enum PluginInstruction {
(ClientId, bool), // bool -> is_web_client
Option<NotificationEnd>, // completion signal
),
+ 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
+ ClientId,
+ Option<NotificationEnd>,
+ ),
ApplyCachedEvents {
plugin_ids: Vec<PluginId>,
done_receiving_permissions: bool,
@@ -201,6 +217,7 @@ impl From<&PluginInstruction> for PluginContext {
PluginInstruction::AddClient(_) => PluginContext::AddClient,
PluginInstruction::RemoveClient(_) => PluginContext::RemoveClient,
PluginInstruction::NewTab(..) => PluginContext::NewTab,
+ PluginInstruction::OverrideLayout(..) => PluginContext::OverrideLayout,
PluginInstruction::ApplyCachedEvents { .. } => PluginContext::ApplyCachedEvents,
PluginInstruction::ApplyCachedWorkerMessages(..) => {
PluginContext::ApplyCachedWorkerMessages
@@ -538,18 +555,24 @@ pub(crate) fn plugin_thread_main(
.get_initial_cwd()
.or_else(|| cwd.clone());
let skip_cache = false;
- let (plugin_id, _client_id) = wasm_bridge.load_plugin(
+ match wasm_bridge.load_plugin(
&run_plugin,
Some(tab_index),
size,
cwd,
skip_cache,
Some(client_id),
- )?;
- plugin_ids
- .entry(run_plugin_or_alias.clone())
- .or_default()
- .push(plugin_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);
+ },
+ }
}
}
drop(bus.senders.send_to_pty(PtyInstruction::NewTab(
@@ -566,6 +589,96 @@ pub(crate) fn plugin_thread_main(
completion_tx,
)));
},
+ PluginInstruction::OverrideLayout(
+ cwd,
+ default_shell,
+ mut tiled_layout,
+ mut floating_layouts,
+ swap_tiled_layouts,
+ swap_floating_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
+ 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() {
+ first_client_id
+ } else {
+ client_id
+ };
+
+ let mut plugin_ids: HashMap<RunPluginOrAlias, Vec<PluginId>> = HashMap::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));
+ });
+
+ // Extract run instructions from tiled layout
+ let extracted_run_instructions = 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();
+
+ // 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);
+ },
+ }
+ }
+ }
+
+ // Send to pty thread
+ drop(bus.senders.send_to_pty(PtyInstruction::OverrideLayout(
+ cwd,
+ default_shell,
+ tiled_layout,
+ floating_layouts,
+ swap_tiled_layouts,
+ swap_floating_layouts,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ tab_index,
+ plugin_ids,
+ client_id,
+ completion_tx,
+ )));
+ },
PluginInstruction::ApplyCachedEvents {
plugin_ids,
done_receiving_permissions,
diff --git a/zellij-server/src/pty.rs b/zellij-server/src/pty.rs
index fee6054e..544830af 100644
--- a/zellij-server/src/pty.rs
+++ b/zellij-server/src/pty.rs
@@ -25,7 +25,10 @@ use zellij_utils::{
errors::{ContextType, PtyContext},
input::{
command::{OpenFilePayload, RunCommand, TerminalAction},
- layout::{FloatingPaneLayout, Layout, Run, RunPluginOrAlias, TiledPaneLayout},
+ layout::{
+ FloatingPaneLayout, Layout, Run, RunPluginOrAlias, SwapFloatingLayout, SwapTiledLayout,
+ TiledPaneLayout,
+ },
},
pane_size::Size,
session_serialization,
@@ -75,6 +78,20 @@ pub enum PtyInstruction {
(ClientId, bool), // bool -> is_web_client
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
+ ClientId,
+ Option<NotificationEnd>,
+ ),
ClosePane(PaneId, Option<NotificationEnd>),
CloseTab(Vec<PaneId>),
ReRunCommandInPane(PaneId, RunCommand, Option<NotificationEnd>),
@@ -137,6 +154,7 @@ impl From<&PtyInstruction> for PtyContext {
PtyInstruction::ClosePane(..) => PtyContext::ClosePane,
PtyInstruction::CloseTab(_) => PtyContext::CloseTab,
PtyInstruction::NewTab(..) => PtyContext::NewTab,
+ PtyInstruction::OverrideLayout(..) => PtyContext::OverrideLayout,
PtyInstruction::ReRunCommandInPane(..) => PtyContext::ReRunCommandInPane,
PtyInstruction::DropToShellInPane { .. } => PtyContext::DropToShellInPane,
PtyInstruction::SpawnInPlaceTerminal(..) => PtyContext::SpawnInPlaceTerminal,
@@ -475,6 +493,38 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) -> Result<()> {
)
.with_context(err_context)?;
},
+ PtyInstruction::OverrideLayout(
+ cwd,
+ default_shell,
+ tiled_layout,
+ floating_panes_layout,
+ swap_tiled_layouts,
+ swap_floating_layouts,
+ 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)?;
+ },
PtyInstruction::ClosePane(id, _completion_tx) => {
pty.close_pane(id)
.and_then(|_| {
@@ -1200,6 +1250,187 @@ impl Pty {
}
Ok(())
}
+ pub fn spawn_terminals_for_layout_override(
+ &mut self,
+ cwd: Option<PathBuf>,
+ layout: TiledPaneLayout,
+ floating_panes_layout: Vec<FloatingPaneLayout>,
+ default_shell: Option<TerminalAction>,
+ plugin_ids: HashMap<RunPluginOrAlias, Vec<u32>>,
+ tab_index: usize,
+ 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<()> {
+ let err_context = || format!("failed to spawn terminals for layout for");
+
+ let mut default_shell =
+ default_shell.unwrap_or_else(|| self.get_default_terminal(cwd, None));
+ self.fill_cwd(&mut default_shell, client_id);
+
+ let extracted_run_instructions = layout.extract_run_instructions();
+ let extracted_floating_run_instructions = floating_panes_layout
+ .iter()
+ .filter(|f| !f.already_running)
+ .map(|f| f.run.clone());
+ let mut new_pane_pids: Vec<(u32, bool, Option<RunCommand>, Result<RawFd>)> = vec![]; // (terminal_id,
+ // starts_held,
+ // run_command,
+ // file_descriptor)
+
+ let mut new_floating_panes_pids: Vec<(u32, bool, Option<RunCommand>, Result<RawFd>)> =
+ vec![]; // same
+ // as
+ // new_pane_pids
+
+ let mut originating_plugins_to_inform = vec![];
+
+ for run_instruction in extracted_run_instructions {
+ let originating_plugin = run_instruction.as_ref().and_then(|r| {
+ if let Run::Command(run_command) = r {
+ run_command.originating_plugin.clone()
+ } else {
+ None
+ }
+ });
+ let mut terminal_id = None;
+ if let Some(new_pane_data) =
+ self.apply_run_instruction(run_instruction, default_shell.clone())?
+ {
+ terminal_id = Some(new_pane_data.0);
+ new_pane_pids.push(new_pane_data);
+ }
+ if let (Some(originating_plugin), Some(terminal_id)) = (originating_plugin, terminal_id)
+ {
+ originating_plugins_to_inform.push((terminal_id, originating_plugin));
+ }
+ }
+ for run_instruction in extracted_floating_run_instructions {
+ let originating_plugin = run_instruction.as_ref().and_then(|r| {
+ if let Run::Command(run_command) = r {
+ run_command.originating_plugin.clone()
+ } else {
+ None
+ }
+ });
+ let mut terminal_id = None;
+ if let Some(new_pane_data) =
+ self.apply_run_instruction(run_instruction, default_shell.clone())?
+ {
+ terminal_id = Some(new_pane_data.0);
+ new_floating_panes_pids.push(new_pane_data);
+ }
+ if let (Some(originating_plugin), Some(terminal_id)) = (originating_plugin, terminal_id)
+ {
+ originating_plugins_to_inform.push((terminal_id, originating_plugin));
+ }
+ }
+
+ // Option<RunCommand> should only be Some if the pane starts held
+ let new_tab_pane_ids: Vec<(u32, Option<RunCommand>)> = new_pane_pids
+ .iter()
+ .map(|(terminal_id, starts_held, run_command, _)| {
+ if *starts_held {
+ (*terminal_id, run_command.clone())
+ } else {
+ (*terminal_id, None)
+ }
+ })
+ .collect();
+ let new_tab_floating_pane_ids: Vec<(u32, Option<RunCommand>)> = new_floating_panes_pids
+ .iter()
+ .map(|(terminal_id, starts_held, run_command, _)| {
+ if *starts_held {
+ (*terminal_id, run_command.clone())
+ } else {
+ (*terminal_id, None)
+ }
+ })
+ .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 mut terminals_to_start = vec![];
+
+ terminals_to_start.append(&mut new_pane_pids);
+ terminals_to_start.append(&mut new_floating_panes_pids);
+ for (terminal_id, starts_held, run_command, pid_primary) in terminals_to_start {
+ if starts_held {
+ // we do not run a command or start listening for bytes on held panes
+ continue;
+ }
+ match pid_primary {
+ Ok(pid_primary) => {
+ let terminal_bytes = task::spawn({
+ let senders = self.bus.senders.clone();
+ let os_input = self
+ .bus
+ .os_input
+ .as_ref()
+ .with_context(err_context)?
+ .clone();
+ let debug_to_file = self.debug_to_file;
+ async move {
+ TerminalBytes::new(
+ pid_primary,
+ senders,
+ os_input,
+ debug_to_file,
+ terminal_id,
+ )
+ .listen()
+ .await
+ .context("failed to spawn terminals for layout")
+ .fatal();
+ }
+ });
+ self.task_handles.insert(terminal_id, terminal_bytes);
+ },
+ _ => match run_command {
+ Some(run_command) => {
+ if run_command.hold_on_close {
+ send_command_not_found_to_screen(
+ self.bus.senders.clone(),
+ terminal_id,
+ run_command.clone(),
+ )
+ .with_context(err_context)?;
+ } else {
+ self.close_pane(PaneId::Terminal(terminal_id))
+ .with_context(err_context)?;
+ }
+ },
+ None => {
+ self.close_pane(PaneId::Terminal(terminal_id))
+ .with_context(err_context)?;
+ },
+ },
+ }
+ }
+ for (terminal_id, originating_plugin) in originating_plugins_to_inform {
+ self.inform_originating_plugin_of_open(terminal_id, originating_plugin);
+ }
+ Ok(())
+ }
fn inform_originating_plugin_of_open(
&mut self,
terminal_id: u32,
@@ -1552,7 +1783,6 @@ impl Pty {
&mut self,
session_layout_metadata: &mut SessionLayoutMetadata,
) {
- log::info!("populate_session_layout_metadata");
let terminal_ids = session_layout_metadata.all_terminal_ids();
let mut terminal_ids_to_commands: HashMap<u32, Vec<String>> = HashMap::new();
let mut terminal_ids_to_cwds: HashMap<u32, PathBuf> = HashMap::new();
diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs
index 57f64d1a..57b499e1 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,
+ layout::{Layout, TiledPaneLayout},
},
ipc::{
ClientAttributes, ClientToServerMsg, ExitReason, IpcReceiverWithContext, ServerToClientMsg,
@@ -1114,6 +1114,39 @@ pub(crate) fn route_action(
))
.with_context(err_context)?;
},
+ Action::OverrideLayout {
+ tiled_layout,
+ floating_layouts,
+ swap_tiled_layouts,
+ swap_floating_layouts,
+ tab_name,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ } => {
+ // 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,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ client_id,
+ Some(NotificationEnd::new(completion_tx)),
+ ))
+ .with_context(err_context)?;
+ },
Action::QueryTabNames => {
senders
.send_to_screen(ScreenInstruction::QueryTabNames(
diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs
index c8998831..a93ad27e 100644
--- a/zellij-server/src/screen.rs
+++ b/zellij-server/src/screen.rs
@@ -295,6 +295,33 @@ pub enum ScreenInstruction {
ClearPaneFrameColorOverride(Vec<PaneId>),
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
+ 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
+ ClientId,
+ Option<NotificationEnd>,
+ ),
QueryTabNames(ClientId, Option<NotificationEnd>),
NewTiledPluginPane(
RunPluginOrAlias,
@@ -610,6 +637,8 @@ impl From<&ScreenInstruction> for ScreenContext {
},
ScreenInstruction::PreviousSwapLayout(..) => ScreenContext::PreviousSwapLayout,
ScreenInstruction::NextSwapLayout(..) => ScreenContext::NextSwapLayout,
+ ScreenInstruction::OverrideLayout(..) => ScreenContext::OverrideLayout,
+ ScreenInstruction::OverrideLayoutComplete(..) => ScreenContext::OverrideLayoutComplete,
ScreenInstruction::QueryTabNames(..) => ScreenContext::QueryTabNames,
ScreenInstruction::NewTiledPluginPane(..) => ScreenContext::NewTiledPluginPane,
ScreenInstruction::NewFloatingPluginPane(..) => ScreenContext::NewFloatingPluginPane,
@@ -3548,6 +3577,47 @@ fn get_default_editor() -> Option<PathBuf> {
None
}
+fn find_already_running_panes(
+ tiled_layout: &TiledPaneLayout,
+ floating_layouts: &[FloatingPaneLayout],
+ active_tab: &Tab,
+) -> (Vec<Option<Run>>, Vec<usize>) {
+ let mut layout_tiled_instructions = tiled_layout.extract_run_instructions();
+ let running_tiled_instructions: Vec<Option<Run>> = active_tab
+ .get_tiled_panes()
+ .map(|(_, pane)| pane.invoked_with().clone())
+ .collect();
+
+ let mut tiled_to_ignore = Vec::new();
+ for running_instr in running_tiled_instructions {
+ if let Some(pos) = layout_tiled_instructions
+ .iter()
+ .position(|layout_instr| layout_instr == &running_instr)
+ {
+ layout_tiled_instructions.remove(pos);
+ tiled_to_ignore.push(running_instr);
+ }
+ }
+
+ let mut running_floating_instructions: Vec<Option<Run>> = active_tab
+ .get_floating_panes()
+ .map(|(_, pane)| pane.invoked_with().clone())
+ .collect();
+
+ let mut floating_indices = Vec::new();
+ for (idx, floating_layout) in floating_layouts.iter().enumerate() {
+ if let Some(pos) = running_floating_instructions
+ .iter()
+ .position(|instr| instr == &floating_layout.run)
+ {
+ running_floating_instructions.remove(pos);
+ floating_indices.push(idx);
+ }
+ }
+
+ (tiled_to_ignore, floating_indices)
+}
+
// The box is here in order to make the
// NewClient enum smaller
#[allow(clippy::boxed_local)]
@@ -5120,6 +5190,97 @@ pub(crate) fn screen_thread_main(
screen.render(None)?;
screen.log_and_report_session_state()?;
},
+ ScreenInstruction::OverrideLayout(
+ cwd,
+ default_shell,
+ mut new_tab_name,
+ mut tiled_layout,
+ mut floating_layouts,
+ swap_tiled_layouts,
+ swap_floating_layouts,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ client_id,
+ _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
+ );
+ 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);
+
+ for run_instruction in tiled_to_ignore {
+ tiled_layout.ignore_run_instruction(run_instruction);
+ }
+
+ for idx in floating_indices {
+ floating_layouts
+ .get_mut(idx)
+ .map(|f| f.already_running = true);
+ }
+
+ let tab_index = active_tab.index;
+ screen
+ .bus
+ .senders
+ .send_to_plugin(PluginInstruction::OverrideLayout(
+ cwd,
+ default_shell,
+ tiled_layout,
+ floating_layouts,
+ swap_tiled_layouts,
+ swap_floating_layouts,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ tab_index,
+ client_id,
+ _completion_tx,
+ ))?;
+ },
+ ScreenInstruction::OverrideLayoutComplete(
+ 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,
+ 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,
+ )
+ });
+ screen.log_and_report_session_state()?;
+ let _ = screen.render(None);
+ drop(completion_tx); // action ends here, notify the action initiator
+ },
ScreenInstruction::QueryTabNames(client_id, completion_tx) => {
let tab_names = screen
.get_tabs_mut()
diff --git a/zellij-server/src/tab/layout_applier.rs b/zellij-server/src/tab/layout_applier.rs
index 559dc5db..a0f30da8 100644
--- a/zellij-server/src/tab/layout_applier.rs
+++ b/zellij-server/src/tab/layout_applier.rs
@@ -122,6 +122,38 @@ impl<'a> LayoutApplier<'a> {
let should_show_floating_panes = layout_has_floating_panes && !hide_floating_panes;
return Ok(should_show_floating_panes);
}
+ pub fn override_layout(
+ &mut self,
+ tiled_panes_layout: TiledPaneLayout,
+ floating_panes_layout: Vec<FloatingPaneLayout>,
+ new_terminal_ids: Vec<(u32, HoldForCommand)>,
+ new_floating_terminal_ids: Vec<(u32, HoldForCommand)>,
+ mut new_plugin_ids: HashMap<RunPluginOrAlias, Vec<u32>>,
+ retain_existing_terminal_panes: bool,
+ retain_existing_plugin_panes: bool,
+ client_id: ClientId,
+ ) -> Result<bool> {
+ // true => should_show_floating_panes
+ let hide_floating_panes = tiled_panes_layout.hide_floating_panes;
+ self.override_tiled_panes_layout_for_existing_panes(
+ &tiled_panes_layout,
+ new_terminal_ids,
+ &mut new_plugin_ids,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ client_id,
+ )?;
+
+ let layout_has_floating_panes = self.override_floating_panes_layout_for_existing_panes(
+ &floating_panes_layout,
+ new_floating_terminal_ids,
+ &mut new_plugin_ids,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ )?;
+ let should_show_floating_panes = layout_has_floating_panes && !hide_floating_panes;
+ return Ok(should_show_floating_panes);
+ }
pub fn apply_tiled_panes_layout_to_existing_panes(
&mut self,
layout: &TiledPaneLayout,
@@ -181,16 +213,144 @@ impl<'a> LayoutApplier<'a> {
// add the rest of the panes where tiled_panes finds room for them (eg. if the layout had
// less panes than we've got in our state)
let remaining_pane_ids: Vec<PaneId> = existing_tab_state.pane_ids();
- pane_applier.handle_remaining_tiled_pane_ids(remaining_pane_ids, existing_tab_state);
+ pane_applier.handle_remaining_tiled_pane_ids(
+ remaining_pane_ids,
+ existing_tab_state,
+ None,
+ None,
+ );
pane_applier.finalize_tiled_state();
LayoutApplier::offset_viewport(
self.viewport.clone(),
+ self.display_area.clone(),
self.tiled_panes,
self.draw_pane_frames,
);
Ok(())
}
+ pub fn override_tiled_panes_layout_for_existing_panes(
+ &mut self,
+ tiled_panes_layout: &TiledPaneLayout,
+ mut new_terminal_ids: Vec<(u32, HoldForCommand)>,
+ mut new_plugin_ids: &mut HashMap<RunPluginOrAlias, Vec<u32>>,
+ retain_existing_terminal_panes: bool,
+ retain_existing_plugin_panes: bool,
+ client_id: ClientId,
+ ) -> Result<()> {
+ let positions_in_layout = self.flatten_layout(tiled_panes_layout, false)?;
+
+ let mut existing_tab_state = ExistingTabState::new(self.tiled_panes.drain());
+
+ let mut pane_applier = PaneApplier::new(
+ &mut self.tiled_panes,
+ &mut self.floating_panes,
+ &self.senders,
+ &self.character_cell_size,
+ );
+ let mut positions_left_without_exact_matches = vec![];
+
+ // look for exact matches (eg. panes that expect a specific command or plugin to run in them)
+ let mut last_logical_position = None;
+ for (layout, position_and_size) in positions_in_layout {
+ match existing_tab_state.find_and_extract_exact_pane_with_same_run(&layout.run) {
+ Some(pane) => {
+ last_logical_position = position_and_size.logical_position;
+ pane_applier.apply_position_and_size_to_tiled_pane(
+ pane,
+ position_and_size,
+ layout,
+ );
+ },
+ None => {
+ last_logical_position = position_and_size.logical_position;
+ positions_left_without_exact_matches.push((layout, position_and_size));
+ },
+ }
+ }
+
+ let remaining_pane_ids: Vec<PaneId> = existing_tab_state.pane_ids();
+ self.close_non_retained_panes(
+ &mut existing_tab_state,
+ &remaining_pane_ids,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ );
+
+ let (focus_pane_id, pane_ids_expanded_in_stack) = self.position_new_panes(
+ &mut new_terminal_ids,
+ &mut new_plugin_ids,
+ &mut positions_left_without_exact_matches,
+ )?;
+
+ // we do this because we have to add the remaining tiled pane ids ONLY AFTER positioning
+ // the new panes, otherwise the layout might get borked
+ let mut pane_applier = PaneApplier::new(
+ &mut self.tiled_panes,
+ &mut self.floating_panes,
+ &self.senders,
+ &self.character_cell_size,
+ );
+ if retain_existing_terminal_panes || retain_existing_plugin_panes {
+ pane_applier.handle_remaining_tiled_pane_ids(
+ remaining_pane_ids,
+ existing_tab_state,
+ last_logical_position,
+ Some(client_id),
+ );
+ }
+
+ pane_applier.finalize_tiled_state();
+
+ if let Some(pane_id) = focus_pane_id {
+ *self.focus_pane_id = Some(pane_id);
+ self.tiled_panes.focus_pane(pane_id, client_id);
+ }
+
+ // in case focused panes were closed by the override
+ self.tiled_panes.move_client_focus_to_existing_panes();
+
+ for pane_id in &pane_ids_expanded_in_stack {
+ self.tiled_panes.expand_pane_in_stack(*pane_id);
+ }
+
+ LayoutApplier::offset_viewport(
+ self.viewport.clone(),
+ self.display_area.clone(),
+ self.tiled_panes,
+ self.draw_pane_frames,
+ );
+ Ok(())
+ }
+ fn close_non_retained_panes(
+ &self,
+ existing_tab_state: &mut ExistingTabState,
+ remaining_pane_ids: &[PaneId],
+ retain_existing_terminal_panes: bool,
+ retain_existing_plugin_panes: bool,
+ ) {
+ for pane_id in remaining_pane_ids {
+ if retain_existing_terminal_panes && matches!(pane_id, PaneId::Terminal(_)) {
+ continue;
+ }
+ if retain_existing_plugin_panes && matches!(pane_id, PaneId::Plugin(_)) {
+ continue;
+ }
+ existing_tab_state.remove_pane(pane_id);
+ match pane_id {
+ PaneId::Terminal(_) => {
+ let _ = self
+ .senders
+ .send_to_pty(PtyInstruction::ClosePane(*pane_id, None));
+ },
+ PaneId::Plugin(plugin_id) => {
+ let _ = self
+ .senders
+ .send_to_plugin(PluginInstruction::Unload(*plugin_id));
+ },
+ }
+ }
+ }
fn flatten_layout(
&self,
layout: &TiledPaneLayout,
@@ -243,7 +403,7 @@ impl<'a> LayoutApplier<'a> {
&layout.run_instructions_to_ignore,
&mut positions_in_layout,
);
- let focus_pane_id = self.position_new_panes(
+ let (focus_pane_id, pane_ids_expanded_in_stack) = self.position_new_panes(
&mut new_terminal_ids,
&mut new_plugin_ids,
&mut positions_in_layout,
@@ -252,6 +412,11 @@ impl<'a> LayoutApplier<'a> {
run_instructions_without_a_location,
&mut new_terminal_ids,
);
+
+ for pane_id in &pane_ids_expanded_in_stack {
+ self.tiled_panes.expand_pane_in_stack(*pane_id);
+ }
+
self.adjust_viewport().with_context(err_context)?;
self.set_focused_tiled_pane(focus_pane_id, client_id);
Ok(())
@@ -288,10 +453,12 @@ impl<'a> LayoutApplier<'a> {
new_terminal_ids: &mut Vec<(u32, HoldForCommand)>,
new_plugin_ids: &mut HashMap<RunPluginOrAlias, Vec<u32>>,
positions_in_layout: &mut Vec<(TiledPaneLayout, PaneGeom)>,
- ) -> Result<Option<PaneId>> {
+ ) -> Result<(Option<PaneId>, Vec<PaneId>)> {
+ // returns: optional pane id to focus, pane ids
+ // expanded in stack
// here we open new panes for each run instruction in the layout with the details
// we got from the plugin thread and pty thread
- // let positions_and_size = positions_in_layout.iter();
+ let mut pane_ids_expanded_in_stack = vec![];
let mut focus_pane_id: Option<PaneId> = None;
let mut set_focus_pane_id = |layout: &TiledPaneLayout, pane_id: PaneId| {
if layout.focus.unwrap_or(false) && focus_pane_id.is_none() {
@@ -302,15 +469,21 @@ impl<'a> LayoutApplier<'a> {
if let Some(Run::Plugin(run)) = layout.run.clone() {
let pid =
self.new_tiled_plugin_pane(run, new_plugin_ids, &position_and_size, &layout)?;
+ if layout.is_expanded_in_stack {
+ pane_ids_expanded_in_stack.push(PaneId::Terminal(pid));
+ }
set_focus_pane_id(&layout, PaneId::Plugin(pid));
} else if !new_terminal_ids.is_empty() {
// there are still panes left to fill, use the pids we received in this method
let (pid, hold_for_command) = new_terminal_ids.remove(0);
self.new_terminal_pane(pid, &hold_for_command, &position_and_size, &layout)?;
+ if layout.is_expanded_in_stack {
+ pane_ids_expanded_in_stack.push(PaneId::Terminal(pid));
+ }
set_focus_pane_id(&layout, PaneId::Terminal(pid));
}
}
- Ok(focus_pane_id)
+ Ok((focus_pane_id, pane_ids_expanded_in_stack))
}
fn handle_run_instructions_without_a_location(
&mut self,
@@ -715,6 +888,112 @@ impl<'a> LayoutApplier<'a> {
Ok(false)
}
}
+ pub fn override_floating_panes_layout_for_existing_panes(
+ &mut self,
+ floating_panes_layout: &Vec<FloatingPaneLayout>,
+ new_terminal_ids: Vec<(u32, HoldForCommand)>,
+ mut new_plugin_ids: &mut HashMap<RunPluginOrAlias, Vec<u32>>,
+ retain_existing_terminal_panes: bool,
+ retain_existing_plugin_panes: bool,
+ ) -> Result<bool> {
+ let layout_has_floating_panes = !floating_panes_layout.is_empty();
+ let mut positions_in_layout = floating_panes_layout.clone();
+ let mut logical_position = 0;
+ for floating_pane_layout in positions_in_layout.iter_mut() {
+ floating_pane_layout.logical_position = Some(logical_position);
+ logical_position += 1;
+ }
+ let mut existing_tab_state = ExistingTabState::new(self.floating_panes.drain());
+ let mut positions_left = vec![];
+
+ let mut pane_applier = PaneApplier::new(
+ &mut self.tiled_panes,
+ &mut self.floating_panes,
+ &self.senders,
+ &self.character_cell_size,
+ );
+
+ // find already running exact matches
+ for floating_pane_layout in positions_in_layout {
+ match existing_tab_state
+ .find_and_extract_exact_pane_with_same_run(&floating_pane_layout.run)
+ {
+ Some(pane) => {
+ pane_applier
+ .apply_floating_panes_layout_to_floating_pane(pane, floating_pane_layout)?;
+ },
+ None => {
+ positions_left.push(floating_pane_layout);
+ },
+ }
+ }
+
+ let remaining_pane_ids: Vec<PaneId> = existing_tab_state.pane_ids();
+ self.close_non_retained_panes(
+ &mut existing_tab_state,
+ &remaining_pane_ids,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ );
+
+ // open new panes
+ let mut focused_floating_pane = None;
+ let mut new_floating_terminal_ids = new_terminal_ids.iter();
+ for floating_pane_layout in positions_left {
+ let position_and_size = self
+ .floating_panes
+ .position_floating_pane_layout(&floating_pane_layout)?;
+ let pid_to_focus = if let Some(Run::Plugin(run)) = floating_pane_layout.run.clone() {
+ self.new_floating_plugin_pane(
+ run,
+ &mut new_plugin_ids,
+ position_and_size,
+ &floating_pane_layout,
+ )?
+ } else if let Some((pid, hold_for_command)) = new_floating_terminal_ids.next() {
+ self.new_floating_terminal_pane(
+ pid,
+ hold_for_command,
+ position_and_size,
+ &floating_pane_layout,
+ )?
+ } else {
+ None
+ };
+ if let Some(pid_to_focus) = pid_to_focus {
+ focused_floating_pane = Some(pid_to_focus);
+ }
+ }
+
+ // we do this because we have to add the remaining tiled pane ids ONLY AFTER positioning
+ // the new panes, otherwise the layout might get borked
+ let mut pane_applier = PaneApplier::new(
+ &mut self.tiled_panes,
+ &mut self.floating_panes,
+ &self.senders,
+ &self.character_cell_size,
+ );
+ pane_applier.handle_remaining_floating_pane_ids(existing_tab_state, logical_position);
+
+ if let Some(focused_floating_pane) = focused_floating_pane {
+ self.floating_panes
+ .focus_pane_for_all_clients(focused_floating_pane);
+ } else {
+ // we do this in case there is no explicitly focused pane in the new layout, so as to
+ // make sure any panes that are already focused have a higher z_index than the ones
+ // added above
+ self.floating_panes.reapply_pane_focus();
+ }
+
+ // in case focused panes were closed by the override
+ self.floating_panes.move_client_focus_to_existing_panes();
+
+ if layout_has_floating_panes {
+ Ok(true)
+ } else {
+ Ok(false)
+ }
+ }
fn resize_whole_tab(&mut self, new_screen_size: Size) -> Result<()> {
let err_context = || {
format!(
@@ -732,9 +1011,15 @@ impl<'a> LayoutApplier<'a> {
}
pub fn offset_viewport(
viewport: Rc<RefCell<Viewport>>,
+ display_area: Rc<RefCell<Size>>,
tiled_panes: &mut TiledPanes,
draw_pane_frames: bool,
) {
+ {
+ // reset viewport before reapplying offset
+ let mut viewport = viewport.borrow_mut();
+ *viewport = (*display_area.borrow()).into();
+ }
let boundary_geoms = tiled_panes.non_selectable_pane_geoms_inside_viewport();
{
// curly braces here is so that we free viewport immediately when we're done
@@ -784,6 +1069,7 @@ impl<'a> LayoutApplier<'a> {
self.resize_whole_tab(display_area).context(err_context)?;
LayoutApplier::offset_viewport(
self.viewport.clone(),
+ self.display_area.clone(),
self.tiled_panes,
self.draw_pane_frames,
);
@@ -843,6 +1129,20 @@ impl ExistingTabState {
}
None
}
+ pub fn find_and_extract_exact_pane_with_same_run(
+ &mut self,
+ run: &Option<Run>,
+ ) -> Option<Box<dyn Pane>> {
+ let candidates = self.pane_candidates();
+ let pane_id = candidates.iter().find_map(|(_pid, p)| {
+ if p.invoked_with() == run {
+ Some(p.pid())
+ } else {
+ None
+ }
+ });
+ pane_id.and_then(|p| self.existing_panes.remove(&p))
+ }
pub fn find_and_extract_pane_with_same_logical_position(
&mut self,
logical_position: Option<usize>,
@@ -1015,10 +1315,16 @@ impl<'a> PaneApplier<'a> {
&mut self,
remaining_pane_ids: Vec<PaneId>,
mut existing_tab_state: ExistingTabState,
+ mut last_logical_position: Option<usize>,
+ client_id: Option<ClientId>,
) {
for pane_id in remaining_pane_ids {
if let Some(pane) = existing_tab_state.remove_pane(&pane_id) {
- self.tiled_panes.insert_pane(pane.pid(), pane, None);
+ self.tiled_panes.insert_pane(pane.pid(), pane, client_id);
+ if let Some(l) = last_logical_position.take() {
+ self.tiled_panes.set_pane_logical_position(pane_id, l);
+ last_logical_position = Some(l + 1);
+ }
}
}
}
diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs
index 7e441a0e..696a0d2f 100644
--- a/zellij-server/src/tab/mod.rs
+++ b/zellij-server/src/tab/mod.rs
@@ -859,6 +859,102 @@ impl Tab {
// we should still be able to properly recover from this with a useful error
// message though
log::error!("Failed to apply layout: {}", e);
+ self.tiled_panes.reapply_pane_frames();
+ self.is_pending = false;
+ self.apply_buffered_instructions().non_fatal();
+ },
+ }
+ Ok(())
+ }
+ pub fn override_layout(
+ &mut self,
+ layout: TiledPaneLayout,
+ floating_panes_layout: Vec<FloatingPaneLayout>,
+ mut new_swap_tiled_layouts: Option<Vec<SwapTiledLayout>>,
+ mut new_swap_floating_layouts: Option<Vec<SwapFloatingLayout>>,
+ new_terminal_ids: Vec<(u32, HoldForCommand)>,
+ new_floating_terminal_ids: Vec<(u32, HoldForCommand)>,
+ new_plugin_ids: HashMap<RunPluginOrAlias, Vec<u32>>,
+ retain_existing_terminal_panes: bool,
+ retain_existing_plugin_panes: bool,
+ client_id: ClientId,
+ blocking_terminal: Option<(u32, NotificationEnd)>,
+ ) -> Result<()> {
+ let new_swap_tiled_layouts = new_swap_tiled_layouts.take().unwrap_or_else(|| vec![]);
+ let new_swap_floating_layouts = new_swap_floating_layouts.take().unwrap_or_else(|| vec![]);
+ self.swap_layouts
+ .set_swap_tiled_layouts(new_swap_tiled_layouts);
+ self.swap_layouts
+ .set_swap_floating_layouts(new_swap_floating_layouts);
+ match LayoutApplier::new(
+ &self.viewport,
+ &self.senders,
+ &self.sixel_image_store,
+ &self.link_handler,
+ &self.terminal_emulator_colors,
+ &self.terminal_emulator_color_codes,
+ &self.character_cell_size,
+ &self.connected_clients_in_app,
+ &self.style,
+ &self.display_area,
+ &mut self.tiled_panes,
+ &mut self.floating_panes,
+ self.draw_pane_frames,
+ &mut self.focus_pane_id,
+ &self.os_api,
+ self.debug,
+ self.arrow_fonts,
+ self.styled_underlines,
+ self.explicitly_disable_kitty_keyboard_protocol,
+ blocking_terminal,
+ )
+ .override_layout(
+ layout,
+ floating_panes_layout,
+ new_terminal_ids,
+ new_floating_terminal_ids,
+ new_plugin_ids,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ client_id,
+ ) {
+ Ok(should_show_floating_panes) => {
+ if should_show_floating_panes && !self.floating_panes.panes_are_visible() {
+ self.toggle_floating_panes(Some(client_id), None, None)
+ .non_fatal();
+ } else if !should_show_floating_panes && self.floating_panes.panes_are_visible() {
+ self.toggle_floating_panes(Some(client_id), None, None)
+ .non_fatal();
+ }
+
+ // this is essentially another pass of the layout applier
+ // we do this because the layout applier does not know about swap layouts, and in
+ // this case we might have had to re-add existing panes that were not in the
+ // overridden layout (eg. if we had more panes than were in the layout). In such a
+ // case, we would like to make sure these extra panes fit the current swap layout
+ self.swap_layouts.set_is_tiled_damaged();
+ self.swap_layouts.set_is_floating_damaged();
+ let _ = self.relayout_tiled_panes(false);
+ let _ = self.relayout_floating_panes(false);
+
+ self.tiled_panes.reapply_pane_frames();
+ self.is_pending = false;
+
+ LayoutApplier::offset_viewport(
+ self.viewport.clone(),
+ self.display_area.clone(),
+ &mut self.tiled_panes,
+ self.draw_pane_frames,
+ );
+
+ self.apply_buffered_instructions().non_fatal();
+ },
+ Err(e) => {
+ // TODO: this should only happen due to an erroneous layout created by user
+ // configuration that was somehow not caught in our KDL layout parser
+ // we should still be able to properly recover from this with a useful error
+ // message though
+ log::error!("Failed to apply layout: {}", e);
},
}
Ok(())
@@ -1269,6 +1365,18 @@ impl Tab {
self.set_force_render();
Ok(())
}
+ fn normalize_invoked_with_for_default_shell(&self, invoked_with: Option<Run>) -> Option<Run> {
+ let default_shell_run_command = Run::Command(RunCommand {
+ command: self.default_shell.clone(),
+ use_terminal_title: true,
+ ..Default::default()
+ });
+ if invoked_with == Some(default_shell_run_command) {
+ None
+ } else {
+ invoked_with
+ }
+ }
pub fn new_pane(
&mut self,
pid: PaneId,
@@ -1280,6 +1388,7 @@ impl Tab {
client_id: Option<ClientId>,
blocking_notification: Option<NotificationEnd>,
) -> Result<()> {
+ let invoked_with = self.normalize_invoked_with_for_default_shell(invoked_with);
match new_pane_placement {
NewPanePlacement::NoPreference => self.new_no_preference_pane(
pid,
@@ -3409,6 +3518,7 @@ impl Tab {
// outside the viewport (a ui-pane, eg. the status-bar and tab-bar) and need to adjust for it
LayoutApplier::offset_viewport(
self.viewport.clone(),
+ self.display_area.clone(),
&mut self.tiled_panes,
self.draw_pane_frames,
);
@@ -5745,3 +5855,7 @@ mod tab_tests;
#[cfg(test)]
#[path = "./unit/tab_integration_tests.rs"]
mod tab_integration_tests;
+
+#[cfg(test)]
+#[path = "./unit/layout_applier_tests.rs"]
+mod layout_applier_tests;
diff --git a/zellij-server/src/tab/swap_layouts.rs b/zellij-server/src/tab/swap_layouts.rs
index 8ee9709d..af4e5590 100644
--- a/zellij-server/src/tab/swap_layouts.rs
+++ b/zellij-server/src/tab/swap_layouts.rs
@@ -55,6 +55,17 @@ impl SwapLayouts {
self.current_tiled_layout_position = 0;
self.current_floating_layout_position = 0;
}
+ pub fn set_swap_tiled_layouts(&mut self, new_swap_tiled_layouts: Vec<SwapTiledLayout>) {
+ self.swap_tiled_layouts = new_swap_tiled_layouts;
+ self.current_tiled_layout_position = 0;
+ }
+ pub fn set_swap_floating_layouts(
+ &mut self,
+ new_swap_floating_layouts: Vec<SwapFloatingLayout>,
+ ) {
+ self.swap_floating_layouts = new_swap_floating_layouts;
+ self.current_floating_layout_position = 0;
+ }
pub fn set_is_floating_damaged(&mut self) {
self.is_floating_damaged = true;
}
diff --git a/zellij-server/src/tab/unit/layout_applier_tests.rs b/zellij-server/src/tab/unit/layout_applier_tests.rs
new file mode 100644
index 00000000..4ccf6f22
--- /dev/null
+++ b/zellij-server/src/tab/unit/layout_applier_tests.rs
@@ -0,0 +1,7100 @@
+use crate::panes::sixel::SixelImageStore;
+use crate::panes::{FloatingPanes, TiledPanes};
+use crate::panes::{LinkHandler, PaneId};
+use crate::plugins::PluginInstruction;
+use crate::pty::PtyInstruction;
+use crate::tab::layout_applier::LayoutApplier;
+use crate::{
+ os_input_output::{AsyncReader, Pid, ServerOsApi},
+ thread_bus::ThreadSenders,
+ ClientId,
+};
+use insta::assert_snapshot;
+use std::cell::RefCell;
+use std::collections::{HashMap, HashSet};
+use std::fmt::Write;
+use std::os::unix::io::RawFd;
+use std::path::PathBuf;
+use std::rc::Rc;
+
+use interprocess::local_socket::LocalSocketStream;
+use zellij_utils::{
+ channels::{self, ChannelWithContext, Receiver, SenderWithContext},
+ data::{ModeInfo, Palette, Style},
+ errors::prelude::*,
+ input::command::{RunCommand, TerminalAction},
+ input::layout::RunPluginOrAlias,
+ input::layout::{FloatingPaneLayout, Layout, Run, TiledPaneLayout},
+ ipc::{ClientToServerMsg, IpcReceiverWithContext, ServerToClientMsg},
+ pane_size::{Size, SizeInPixels, Viewport},
+};
+
+#[derive(Clone)]
+struct FakeInputOutput {}
+
+impl ServerOsApi for FakeInputOutput {
+ fn set_terminal_size_using_terminal_id(
+ &self,
+ _id: u32,
+ _cols: u16,
+ _rows: u16,
+ _width_in_pixels: Option<u16>,
+ _height_in_pixels: Option<u16>,
+ ) -> Result<()> {
+ Ok(())
+ }
+
+ fn spawn_terminal(
+ &self,
+ _file_to_open: TerminalAction,
+ _quit_cb: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>,
+ _default_editor: Option<PathBuf>,
+ ) -> Result<(u32, RawFd, RawFd)> {
+ unimplemented!()
+ }
+
+ fn read_from_tty_stdout(&self, _fd: RawFd, _buf: &mut [u8]) -> Result<usize> {
+ unimplemented!()
+ }
+
+ fn async_file_reader(&self, _fd: RawFd) -> Box<dyn AsyncReader> {
+ unimplemented!()
+ }
+
+ fn write_to_tty_stdin(&self, _id: u32, _buf: &[u8]) -> Result<usize> {
+ unimplemented!()
+ }
+
+ fn tcdrain(&self, _id: u32) -> Result<()> {
+ unimplemented!()
+ }
+
+ fn kill(&self, _pid: Pid) -> Result<()> {
+ unimplemented!()
+ }
+
+ fn force_kill(&self, _pid: Pid) -> Result<()> {
+ unimplemented!()
+ }
+
+ fn box_clone(&self) -> Box<dyn ServerOsApi> {
+ Box::new((*self).clone())
+ }
+
+ fn send_to_client(&self, _client_id: ClientId, _msg: ServerToClientMsg) -> Result<()> {
+ unimplemented!()
+ }
+
+ fn new_client(
+ &mut self,
+ _client_id: ClientId,
+ _stream: LocalSocketStream,
+ ) -> Result<IpcReceiverWithContext<ClientToServerMsg>> {
+ unimplemented!()
+ }
+
+ fn remove_client(&mut self, _client_id: ClientId) -> Result<()> {
+ unimplemented!()
+ }
+
+ fn load_palette(&self) -> Palette {
+ unimplemented!()
+ }
+
+ fn get_cwd(&self, _pid: Pid) -> Option<PathBuf> {
+ unimplemented!()
+ }
+
+ fn write_to_file(&mut self, _buf: String, _name: Option<String>) -> Result<()> {
+ unimplemented!()
+ }
+
+ fn re_run_command_in_terminal(
+ &self,
+ _terminal_id: u32,
+ _run_command: RunCommand,
+ _quit_cb: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>,
+ ) -> Result<(RawFd, RawFd)> {
+ unimplemented!()
+ }
+
+ fn clear_terminal_id(&self, _terminal_id: u32) -> Result<()> {
+ unimplemented!()
+ }
+
+ fn send_sigint(&self, _pid: Pid) -> Result<()> {
+ unimplemented!()
+ }
+}
+
+/// Parse KDL layout string and extract tiled and floating layouts
+fn parse_kdl_layout(kdl_str: &str) -> (TiledPaneLayout, Vec<FloatingPaneLayout>) {
+ let layout = Layout::from_kdl(kdl_str, Some("test_layout".into()), None, None)
+ .expect("Failed to parse KDL layout");
+ layout.new_tab()
+}
+
+/// Creates all the fixtures needed for LayoutApplier tests
+#[allow(clippy::type_complexity)]
+fn create_layout_applier_fixtures(
+ size: Size,
+) -> (
+ Rc<RefCell<Viewport>>,
+ ThreadSenders,
+ Rc<RefCell<SixelImageStore>>,
+ Rc<RefCell<LinkHandler>>,
+ Rc<RefCell<Palette>>,
+ Rc<RefCell<HashMap<usize, String>>>,
+ Rc<RefCell<Option<SizeInPixels>>>,
+ Rc<RefCell<HashMap<ClientId, bool>>>,
+ Style,
+ Rc<RefCell<Size>>,
+ TiledPanes,
+ FloatingPanes,
+ bool,
+ Option<PaneId>,
+ Box<dyn ServerOsApi>,
+ bool,
+ bool,
+ bool,
+ bool,
+) {
+ let viewport = Rc::new(RefCell::new(Viewport {
+ x: 0,
+ y: 0,
+ rows: size.rows,
+ cols: size.cols,
+ }));
+
+ let (mock_plugin_sender, _mock_plugin_receiver) = channels::unbounded();
+ let mut senders = ThreadSenders::default().silently_fail_on_send();
+ senders.replace_to_plugin(SenderWithContext::new(mock_plugin_sender));
+ let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default()));
+ let link_handler = Rc::new(RefCell::new(LinkHandler::new()));
+ let terminal_emulator_colors = Rc::new(RefCell::new(Palette::default()));
+ let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new()));
+ let character_cell_size = Rc::new(RefCell::new(None));
+
+ let client_id = 1;
+ let mut connected_clients_map = HashMap::new();
+ connected_clients_map.insert(client_id, false);
+ let connected_clients = Rc::new(RefCell::new(connected_clients_map));
+
+ let style = Style::default();
+ let display_area = Rc::new(RefCell::new(size));
+
+ let os_api = Box::new(FakeInputOutput {});
+
+ // Create TiledPanes
+ let connected_clients_set = Rc::new(RefCell::new(HashSet::from([client_id])));
+ let mode_info = Rc::new(RefCell::new(HashMap::new()));
+ let stacked_resize = Rc::new(RefCell::new(false));
+ let session_is_mirrored = true;
+ let draw_pane_frames = true;
+ let default_mode_info = ModeInfo::default();
+
+ let tiled_panes = TiledPanes::new(
+ display_area.clone(),
+ viewport.clone(),
+ connected_clients_set.clone(),
+ connected_clients.clone(),
+ mode_info.clone(),
+ character_cell_size.clone(),
+ stacked_resize,
+ session_is_mirrored,
+ draw_pane_frames,
+ default_mode_info.clone(),
+ style.clone(),
+ os_api.box_clone(),
+ senders.clone(),
+ );
+
+ // Create FloatingPanes
+ let floating_panes = FloatingPanes::new(
+ display_area.clone(),
+ viewport.clone(),
+ connected_clients_set,
+ connected_clients.clone(),
+ mode_info,
+ character_cell_size.clone(),
+ session_is_mirrored,
+ default_mode_info,
+ style.clone(),
+ os_api.box_clone(),
+ senders.clone(),
+ );
+
+ let focus_pane_id = None;
+ let debug = false;
+ let arrow_fonts = true;
+ let styled_underlines = true;
+ let explicitly_disable_kitty_keyboard_protocol = false;
+
+ (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ tiled_panes,
+ floating_panes,
+ draw_pane_frames,
+ focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ )
+}
+
+/// Creates fixtures with receivers for verifying messages sent to pty and plugin threads
+#[allow(clippy::type_complexity)]
+fn create_layout_applier_fixtures_with_receivers(
+ size: Size,
+) -> (
+ Rc<RefCell<Viewport>>,
+ ThreadSenders,
+ Rc<RefCell<SixelImageStore>>,
+ Rc<RefCell<LinkHandler>>,
+ Rc<RefCell<Palette>>,
+ Rc<RefCell<HashMap<usize, String>>>,
+ Rc<RefCell<Option<SizeInPixels>>>,
+ Rc<RefCell<HashMap<ClientId, bool>>>,
+ Style,
+ Rc<RefCell<Size>>,
+ TiledPanes,
+ FloatingPanes,
+ bool,
+ Option<PaneId>,
+ Box<dyn ServerOsApi>,
+ bool,
+ bool,
+ bool,
+ bool,
+ Receiver<(PtyInstruction, zellij_utils::errors::ErrorContext)>,
+ Receiver<(PluginInstruction, zellij_utils::errors::ErrorContext)>,
+) {
+ let viewport = Rc::new(RefCell::new(Viewport {
+ x: 0,
+ y: 0,
+ rows: size.rows,
+ cols: size.cols,
+ }));
+
+ let (mock_pty_sender, mock_pty_receiver): ChannelWithContext<PtyInstruction> =
+ channels::unbounded();
+ let (mock_plugin_sender, mock_plugin_receiver): ChannelWithContext<PluginInstruction> =
+ channels::unbounded();
+
+ let mut senders = ThreadSenders::default().silently_fail_on_send();
+ senders.replace_to_pty(SenderWithContext::new(mock_pty_sender));
+ senders.replace_to_plugin(SenderWithContext::new(mock_plugin_sender));
+
+ let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default()));
+ let link_handler = Rc::new(RefCell::new(LinkHandler::new()));
+ let terminal_emulator_colors = Rc::new(RefCell::new(Palette::default()));
+ let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new()));
+ let character_cell_size = Rc::new(RefCell::new(None));
+
+ let client_id = 1;
+ let mut connected_clients_map = HashMap::new();
+ connected_clients_map.insert(client_id, false);
+ let connected_clients = Rc::new(RefCell::new(connected_clients_map));
+
+ let style = Style::default();
+ let display_area = Rc::new(RefCell::new(size));
+
+ let os_api = Box::new(FakeInputOutput {});
+
+ // Create TiledPanes
+ let connected_clients_set = Rc::new(RefCell::new(HashSet::from([client_id])));
+ let mode_info = Rc::new(RefCell::new(HashMap::new()));
+ let stacked_resize = Rc::new(RefCell::new(false));
+ let session_is_mirrored = true;
+ let draw_pane_frames = true;
+ let default_mode_info = ModeInfo::default();
+
+ let tiled_panes = TiledPanes::new(
+ display_area.clone(),
+ viewport.clone(),
+ connected_clients_set.clone(),
+ connected_clients.clone(),
+ mode_info.clone(),
+ character_cell_size.clone(),
+ stacked_resize,
+ session_is_mirrored,
+ draw_pane_frames,
+ default_mode_info.clone(),
+ style.clone(),
+ os_api.box_clone(),
+ senders.clone(),
+ );
+
+ // Create FloatingPanes
+ let floating_panes = FloatingPanes::new(
+ display_area.clone(),
+ viewport.clone(),
+ connected_clients_set,
+ connected_clients.clone(),
+ mode_info,
+ character_cell_size.clone(),
+ session_is_mirrored,
+ default_mode_info,
+ style.clone(),
+ os_api.box_clone(),
+ senders.clone(),
+ );
+
+ let focus_pane_id = None;
+ let debug = false;
+ let arrow_fonts = true;
+ let styled_underlines = true;
+ let explicitly_disable_kitty_keyboard_protocol = false;
+
+ (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ tiled_panes,
+ floating_panes,
+ draw_pane_frames,
+ focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ mock_pty_receiver,
+ mock_plugin_receiver,
+ )
+}
+
+/// Takes a snapshot of the current pane state for assertion
+fn take_pane_state_snapshot(
+ tiled_panes: &TiledPanes,
+ floating_panes: &FloatingPanes,
+ focus_pane_id: &Option<PaneId>,
+ viewport: &Rc<RefCell<Viewport>>,
+ display_area: &Rc<RefCell<Size>>,
+) -> String {
+ let mut output = String::new();
+
+ // Viewport info
+ let viewport_state = viewport.borrow();
+ writeln!(
+ &mut output,
+ "VIEWPORT: x={}, y={}, cols={}, rows={}",
+ viewport_state.x, viewport_state.y, viewport_state.cols, viewport_state.rows
+ )
+ .unwrap();
+
+ let display_state = display_area.borrow();
+ writeln!(
+ &mut output,
+ "DISPLAY: cols={}, rows={}",
+ display_state.cols, display_state.rows
+ )
+ .unwrap();
+
+ // Focus state
+ writeln!(&mut output, "FOCUS: {:?}", focus_pane_id).unwrap();
+
+ writeln!(&mut output).unwrap();
+
+ // Tiled panes
+ writeln!(&mut output, "TILED PANES ({})", tiled_panes.panes.len()).unwrap();
+ let mut tiled_list: Vec<_> = tiled_panes.get_panes().collect();
+ tiled_list.sort_by_key(|(id, _)| **id);
+
+ for (pane_id, pane) in tiled_list {
+ let geom = pane.position_and_size();
+ let run = pane.invoked_with();
+ let selectable = pane.selectable();
+
+ writeln!(&mut output, " {:?}:", pane_id).unwrap();
+ writeln!(
+ &mut output,
+ " geom: x={}, y={}, cols={}, rows={}",
+ geom.x,
+ geom.y,
+ geom.cols.as_usize(),
+ geom.rows.as_usize()
+ )
+ .unwrap();
+
+ if let Some(logical_pos) = geom.logical_position {
+ writeln!(&mut output, " logical_position: {}", logical_pos).unwrap();
+ }
+
+ if let Some(stack_id) = geom.stacked {
+ writeln!(&mut output, " stacked: {}", stack_id).unwrap();
+ }
+
+ writeln!(&mut output, " run: {}", format_run_instruction(run)).unwrap();
+
+ writeln!(&mut output, " selectable: {}", selectable).unwrap();
+ writeln!(&mut output, " title: {}", pane.current_title()).unwrap();
+ writeln!(&mut output, " borderless: {}", pane.borderless()).unwrap();
+
+ writeln!(&mut output).unwrap();
+ }
+
+ // Floating panes
+ if floating_panes.pane_ids().count() > 0 {
+ writeln!(
+ &mut output,
+ "FLOATING PANES ({})",
+ floating_panes.pane_ids().count()
+ )
+ .unwrap();
+ let mut floating_list: Vec<_> = floating_panes.get_panes().collect();
+ floating_list.sort_by_key(|(id, _)| **id);
+
+ for (pane_id, pane) in floating_list {
+ let geom = pane.position_and_size();
+ let run = pane.invoked_with();
+
+ writeln!(&mut output, " {:?}:", pane_id).unwrap();
+ writeln!(
+ &mut output,
+ " geom: x={}, y={}, cols={}, rows={}",
+ geom.x,
+ geom.y,
+ geom.cols.as_usize(),
+ geom.rows.as_usize()
+ )
+ .unwrap();
+
+ if let Some(logical_pos) = geom.logical_position {
+ writeln!(&mut output, " logical_position: {}", logical_pos).unwrap();
+ }
+
+ writeln!(&mut output, " run: {}", format_run_instruction(run)).unwrap();
+ writeln!(&mut output, " pinned: {}", geom.is_pinned).unwrap();
+ writeln!(&mut output, " selectable: {}", pane.selectable()).unwrap();
+ writeln!(&mut output, " title: {}", pane.current_title()).unwrap();
+ writeln!(&mut output, " borderless: {}", pane.borderless()).unwrap();
+ writeln!(&mut output).unwrap();
+ }
+ }
+
+ output
+}
+
+/// Format a Run instruction as a human-readable string
+fn format_run_instruction(run: &Option<Run>) -> String {
+ match run {
+ None => "None".to_string(),
+ Some(Run::Command(cmd)) => {
+ let mut s = format!("Command({})", cmd.command.display());
+ if !cmd.args.is_empty() {
+ s.push_str(&format!(" args={:?}", cmd.args));
+ }
+ if let Some(cwd) = &cmd.cwd {
+ s.push_str(&format!(" cwd={:?}", cwd));
+ }
+ s
+ },
+ Some(Run::Plugin(plugin)) => {
+ format!("Plugin({})", plugin.location_string())
+ },
+ Some(Run::Cwd(path)) => format!("Cwd({:?})", path),
+ Some(Run::EditFile(path, line, cwd)) => {
+ format!("EditFile({:?}, line={:?}, cwd={:?})", path, line, cwd)
+ },
+ }
+}
+
+/// Collect all close pane messages from the pty receiver
+fn collect_close_pane_messages(
+ pty_receiver: &Receiver<(PtyInstruction, zellij_utils::errors::ErrorContext)>,
+) -> Vec<PaneId> {
+ let mut closed_panes = Vec::new();
+ while let Ok((instruction, _)) = pty_receiver.try_recv() {
+ if let PtyInstruction::ClosePane(pane_id, _) = instruction {
+ closed_panes.push(pane_id);
+ }
+ }
+ closed_panes
+}
+
+/// Collect all unload plugin messages from the plugin receiver
+fn collect_unload_plugin_messages(
+ plugin_receiver: &Receiver<(PluginInstruction, zellij_utils::errors::ErrorContext)>,
+) -> Vec<u32> {
+ let mut unloaded_plugins = Vec::new();
+ while let Ok((instruction, _)) = plugin_receiver.try_recv() {
+ if let PluginInstruction::Unload(plugin_id) = instruction {
+ unloaded_plugins.push(plugin_id);
+ }
+ }
+ unloaded_plugins
+}
+
+#[test]
+fn test_apply_empty_layout() {
+ let kdl_layout = r#"
+ layout {
+ }
+ "#;
+
+ let (tiled_layout, floating_layout) = parse_kdl_layout(kdl_layout);
+ let terminal_ids = vec![];
+
+ let size = Size {
+ cols: 100,
+ rows: 50,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None, // blocking_terminal
+ );
+
+ let result = applier.apply_layout(
+ tiled_layout,
+ floating_layout,
+ terminal_ids,
+ vec![], // new_floating_terminal_ids
+ HashMap::new(), // new_plugin_ids
+ 1, // client_id
+ );
+
+ assert!(result.is_ok());
+
+ let snapshot = take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ );
+
+ assert_snapshot!(snapshot);
+}
+
+#[test]
+fn test_apply_simple_two_pane_layout() {
+ let kdl_layout = r#"
+ layout {
+ pane
+ pane
+ }
+ "#;
+
+ let (tiled_layout, floating_layout) = parse_kdl_layout(kdl_layout);
+ let terminal_ids = vec![(1, None), (2, None)];
+
+ let size = Size {
+ cols: 100,
+ rows: 50,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ tiled_layout,
+ floating_layout,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ let snapshot = take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ );
+
+ assert_snapshot!(snapshot);
+}
+
+#[test]
+fn test_apply_three_pane_layout() {
+ let kdl_layout = r#"
+ layout {
+ pane
+ pane
+ pane
+ }
+ "#;
+
+ let (tiled_layout, floating_layout) = parse_kdl_layout(kdl_layout);
+ let terminal_ids = vec![(1, None), (2, None), (3, None)];
+
+ let size = Size {
+ cols: 100,
+ rows: 50,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ tiled_layout,
+ floating_layout,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_apply_horizontal_split_with_sizes() {
+ let kdl_layout = r#"
+ layout {
+ pane split_direction="Horizontal" {
+ pane size="30%"
+ pane size="70%"
+ }
+ }
+ "#;
+
+ let (tiled_layout, floating_layout) = parse_kdl_layout(kdl_layout);
+ let terminal_ids = vec![(1, None), (2, None)];
+
+ let size = Size {
+ cols: 100,
+ rows: 50,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ tiled_layout,
+ floating_layout,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_apply_vertical_split_with_sizes() {
+ let kdl_layout = r#"
+ layout {
+ pane split_direction="Vertical" {
+ pane size="60%"
+ pane size="40%"
+ }
+ }
+ "#;
+
+ let (tiled_layout, floating_layout) = parse_kdl_layout(kdl_layout);
+ let terminal_ids = vec![(1, None), (2, None)];
+
+ let size = Size {
+ cols: 100,
+ rows: 50,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ tiled_layout,
+ floating_layout,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_apply_nested_layout() {
+ let kdl_layout = r#"
+ layout {
+ pane split_direction="Vertical" {
+ pane size="60%"
+ pane size="40%" split_direction="Horizontal" {
+ pane
+ pane
+ }
+ }
+ }
+ "#;
+
+ let (tiled_layout, floating_layout) = parse_kdl_layout(kdl_layout);
+ let terminal_ids = vec![(1, None), (2, None), (3, None)];
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ tiled_layout,
+ floating_layout,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_apply_layout_with_focus() {
+ let kdl_layout = r#"
+ layout {
+ pane
+ pane focus=true
+ pane
+ }
+ "#;
+
+ let (tiled_layout, floating_layout) = parse_kdl_layout(kdl_layout);
+ let terminal_ids = vec![(1, None), (2, None), (3, None)];
+
+ let size = Size {
+ cols: 100,
+ rows: 50,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ tiled_layout,
+ floating_layout,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Snapshot should show FOCUS: Some(Terminal(2))
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_apply_layout_with_commands() {
+ let kdl_layout = r#"
+ layout {
+ pane command="htop"
+ pane command="tail" {
+ args "-f" "/var/log/syslog"
+ }
+ pane
+ }
+ "#;
+
+ let (tiled_layout, floating_layout) = parse_kdl_layout(kdl_layout);
+ let terminal_ids = vec![(1, None), (2, None), (3, None)];
+
+ let size = Size {
+ cols: 100,
+ rows: 50,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ tiled_layout,
+ floating_layout,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_apply_layout_with_named_panes() {
+ let kdl_layout = r#"
+ layout {
+ pane name="editor"
+ pane name="terminal"
+ pane name="logs"
+ }
+ "#;
+
+ let (tiled_layout, floating_layout) = parse_kdl_layout(kdl_layout);
+ let terminal_ids = vec![(1, None), (2, None), (3, None)];
+
+ let size = Size {
+ cols: 100,
+ rows: 50,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ tiled_layout,
+ floating_layout,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_apply_layout_with_borderless_panes() {
+ let kdl_layout = r#"
+ layout {
+ pane borderless=true
+ pane
+ pane borderless=true
+ }
+ "#;
+
+ let (tiled_layout, floating_layout) = parse_kdl_layout(kdl_layout);
+ let terminal_ids = vec![(1, None), (2, None), (3, None)];
+
+ let size = Size {
+ cols: 100,
+ rows: 50,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ tiled_layout,
+ floating_layout,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Snapshot should show viewport adjusted for borderless panes
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_apply_layout_with_floating_panes() {
+ let kdl_layout = r#"
+ layout {
+ pane
+ pane
+ floating_panes {
+ pane {
+ x 10
+ y 10
+ width 40
+ height 20
+ }
+ }
+ }
+ "#;
+
+ let (tiled_layout, floating_layout) = parse_kdl_layout(kdl_layout);
+ let terminal_ids = vec![(1, None), (2, None)];
+ let floating_terminal_ids = vec![(3, None)];
+
+ let size = Size {
+ cols: 100,
+ rows: 50,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ let should_show_floating = applier
+ .apply_layout(
+ tiled_layout,
+ floating_layout,
+ terminal_ids,
+ floating_terminal_ids,
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ assert_eq!(should_show_floating, true);
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_apply_layout_with_floating_pane_with_command() {
+ let kdl_layout = r#"
+ layout {
+ pane
+ floating_panes {
+ pane {
+ x 10
+ y 10
+ width 50
+ height 25
+ command "htop"
+ }
+ }
+ }
+ "#;
+
+ let (tiled_layout, floating_layout) = parse_kdl_layout(kdl_layout);
+ let terminal_ids = vec![(1, None)];
+ let floating_terminal_ids = vec![(2, None)];
+
+ let size = Size {
+ cols: 100,
+ rows: 50,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ tiled_layout,
+ floating_layout,
+ terminal_ids,
+ floating_terminal_ids,
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_apply_layout_with_mixed_tiled_and_floating_panes() {
+ let kdl_layout = r#"
+ layout {
+ pane split_direction="Vertical" {
+ pane size="60%" command="vim"
+ pane size="40%" split_direction="Horizontal" {
+ pane name="terminal" focus=true
+ pane command="tail" {
+ args "-f" "/var/log/syslog"
+ }
+ }
+ }
+ floating_panes {
+ pane {
+ x 5
+ y 5
+ width 40
+ height 15
+ command "htop"
+ name "monitor"
+ }
+ pane {
+ x "50%"
+ y 10
+ width 45
+ height 20
+ name "notes"
+ }
+ }
+ }
+ "#;
+
+ let (tiled_layout, floating_layout) = parse_kdl_layout(kdl_layout);
+ let terminal_ids = vec![(1, None), (2, None), (3, None)];
+ let floating_terminal_ids = vec![(4, None), (5, None)];
+
+ let size = Size {
+ cols: 150,
+ rows: 50,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ let should_show_floating = applier
+ .apply_layout(
+ tiled_layout,
+ floating_layout,
+ terminal_ids,
+ floating_terminal_ids,
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ assert_eq!(should_show_floating, true);
+
+ // Snapshot should show:
+ // - 3 tiled panes with correct geometries, commands, and names
+ // - 2 floating panes with correct positions and properties
+ // - Focus on terminal pane (Terminal(2))
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_reapply_layout_exact_match() {
+ // First apply initial layout
+ let initial_kdl = r#"
+ layout {
+ pane command="htop"
+ pane command="vim"
+ pane
+ }
+ "#;
+
+ let (tiled_layout, floating_layout) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None), (2, None), (3, None)];
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ tiled_layout,
+ floating_layout,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Now reapply with commands in different positions
+ let new_kdl = r#"
+ layout {
+ pane
+ pane command="htop"
+ pane command="vim"
+ }
+ "#;
+
+ let (new_layout, _) = parse_kdl_layout(new_kdl);
+
+ applier
+ .apply_tiled_panes_layout_to_existing_panes(&new_layout)
+ .unwrap();
+
+ let snapshot = take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ );
+
+ // Snapshot will show panes matched by command and repositioned
+ assert_snapshot!(snapshot);
+}
+
+#[test]
+fn test_reapply_layout_logical_position_match() {
+ // Apply initial layout - 3 panes in horizontal split
+ let initial_kdl = r#"
+ layout {
+ pane
+ pane
+ pane
+ }
+ "#;
+
+ let (tiled_layout, floating_layout) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None), (2, None), (3, None)];
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ tiled_layout,
+ floating_layout,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Reapply DIFFERENT layout - still 3 panes but with different split
+ // This tests logical position matching (position 0, 1, 2) without exact command match
+ let new_kdl = r#"
+ layout {
+ pane split_direction="Vertical" {
+ pane size="50%"
+ pane size="50%"
+ }
+ pane
+ }
+ "#;
+
+ let (new_layout, _) = parse_kdl_layout(new_kdl);
+
+ applier
+ .apply_tiled_panes_layout_to_existing_panes(&new_layout)
+ .unwrap();
+
+ // Panes should be repositioned according to new layout structure
+ // while being matched by their logical positions (0, 1, 2)
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_reapply_layout_with_more_positions() {
+ // Apply initial layout with 2 panes
+ let initial_kdl = r#"
+ layout {
+ pane
+ pane
+ }
+ "#;
+
+ let (tiled_layout, floating_layout) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None), (2, None)];
+
+ let size = Size {
+ cols: 100,
+ rows: 50,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ tiled_layout,
+ floating_layout,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Reapply with 4 positions (but we only have 2 panes)
+ let new_kdl = r#"
+ layout {
+ pane
+ pane
+ pane
+ pane
+ }
+ "#;
+
+ let (new_layout, _) = parse_kdl_layout(new_kdl);
+
+ applier
+ .apply_tiled_panes_layout_to_existing_panes(&new_layout)
+ .unwrap();
+
+ // Should show 2 panes filling first 2 positions, remaining positions empty
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_reapply_floating_pane_layout() {
+ // Apply initial layout
+ let initial_kdl = r#"
+ layout {
+ pane
+ floating_panes {
+ pane {
+ x 10
+ y 10
+ width 40
+ height 20
+ }
+ }
+ }
+ "#;
+
+ let (tiled_layout, floating_layout) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None)];
+ let floating_terminal_ids = vec![(2, None)];
+
+ let size = Size {
+ cols: 100,
+ rows: 50,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ tiled_layout,
+ floating_layout,
+ terminal_ids,
+ floating_terminal_ids,
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Reapply with different position
+ let new_kdl = r#"
+ layout {
+ pane
+ floating_panes {
+ pane {
+ x 20
+ y 20
+ width 50
+ height 25
+ }
+ }
+ }
+ "#;
+
+ let (_, new_floating_layout) = parse_kdl_layout(new_kdl);
+
+ applier
+ .apply_floating_panes_layout_to_existing_panes(&new_floating_layout)
+ .unwrap();
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_apply_complex_nested_layout() {
+ let kdl_layout = r#"
+ layout {
+ pane split_direction="Vertical" {
+ pane size="25%"
+ pane size="50%" split_direction="Horizontal" {
+ pane size="60%"
+ pane size="40%" split_direction="Vertical" {
+ pane
+ pane
+ }
+ }
+ pane size="25%"
+ }
+ }
+ "#;
+
+ let (tiled_layout, floating_layout) = parse_kdl_layout(kdl_layout);
+ let terminal_ids = vec![(1, None), (2, None), (3, None), (4, None), (5, None)];
+
+ let size = Size {
+ cols: 200,
+ rows: 60,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ tiled_layout,
+ floating_layout,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_apply_layout_with_stacked_panes() {
+ let kdl_layout = r#"
+ layout {
+ pane split_direction="Vertical" {
+ pane size="70%" stacked=true {
+ pane name="editor-1"
+ pane name="editor-2"
+ pane name="editor-3"
+ }
+ pane size="30%"
+ }
+ }
+ "#;
+
+ let (tiled_layout, floating_layout) = parse_kdl_layout(kdl_layout);
+ let terminal_ids = vec![(1, None), (2, None), (3, None), (4, None)];
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ tiled_layout,
+ floating_layout,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Snapshot should show:
+ // - 4 panes total (3 in stack + 1 regular)
+ // - Stack panes should have stacked field set with same stack_id
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_apply_layout_with_multiple_stacks() {
+ let kdl_layout = r#"
+ layout {
+ pane split_direction="Vertical" {
+ pane size="50%" stacked=true {
+ pane name="left-1"
+ pane name="left-2"
+ }
+ pane size="50%" stacked=true {
+ pane name="right-1"
+ pane name="right-2"
+ pane name="right-3"
+ }
+ }
+ }
+ "#;
+
+ let (tiled_layout, floating_layout) = parse_kdl_layout(kdl_layout);
+ let terminal_ids = vec![(1, None), (2, None), (3, None), (4, None), (5, None)];
+
+ let size = Size {
+ cols: 150,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ tiled_layout,
+ floating_layout,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Snapshot should show:
+ // - 5 panes in 2 different stacks (different stack_ids)
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_apply_layout_with_plugin_panes() {
+ let kdl_layout = r#"
+ layout {
+ pane
+ pane {
+ plugin location="zellij:tab-bar"
+ }
+ pane {
+ plugin location="zellij:status-bar"
+ }
+ }
+ "#;
+
+ let (tiled_layout, floating_layout) = parse_kdl_layout(kdl_layout);
+ let terminal_ids = vec![(1, None)];
+
+ // Create plugin IDs - need to match the RunPluginOrAlias from the layout
+
+ let mut new_plugin_ids = HashMap::new();
+
+ // Create plugin aliases that match the layout
+ let tab_bar_plugin = RunPluginOrAlias::from_url("zellij:tab-bar", &None, None, None).unwrap();
+ let status_bar_plugin =
+ RunPluginOrAlias::from_url("zellij:status-bar", &None, None, None).unwrap();
+
+ new_plugin_ids.insert(tab_bar_plugin, vec![100]);
+ new_plugin_ids.insert(status_bar_plugin, vec![101]);
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ tiled_layout,
+ floating_layout,
+ terminal_ids,
+ vec![],
+ new_plugin_ids,
+ 1,
+ )
+ .unwrap();
+
+ // Snapshot should show:
+ // - 1 terminal pane
+ // - 2 plugin panes with correct plugin locations
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_apply_layout_with_mixed_plugin_and_terminal_panes() {
+ let kdl_layout = r#"
+ layout {
+ pane split_direction="Vertical" {
+ pane size="20%" {
+ plugin location="file:///path/to/filebrowser.wasm"
+ }
+ pane size="60%" split_direction="Horizontal" {
+ pane command="vim"
+ pane command="cargo" {
+ args "watch" "-x" "test"
+ }
+ }
+ pane size="20%" {
+ plugin location="zellij:compact-bar"
+ }
+ }
+ }
+ "#;
+
+ let (tiled_layout, floating_layout) = parse_kdl_layout(kdl_layout);
+ let terminal_ids = vec![(1, None), (2, None)];
+
+ let mut new_plugin_ids = HashMap::new();
+
+ let filebrowser_plugin =
+ RunPluginOrAlias::from_url("file:///path/to/filebrowser.wasm", &None, None, None).unwrap();
+ let compact_bar_plugin =
+ RunPluginOrAlias::from_url("zellij:compact-bar", &None, None, None).unwrap();
+
+ new_plugin_ids.insert(filebrowser_plugin, vec![102]);
+ new_plugin_ids.insert(compact_bar_plugin, vec![103]);
+
+ let size = Size {
+ cols: 200,
+ rows: 50,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ tiled_layout,
+ floating_layout,
+ terminal_ids,
+ vec![],
+ new_plugin_ids,
+ 1,
+ )
+ .unwrap();
+
+ // Snapshot should show:
+ // - 2 terminal panes with commands
+ // - 2 plugin panes with different locations
+ // - Correct size distribution (20%, 60%, 20%)
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_apply_layout_with_missing_plugin_ids() {
+ let kdl_layout = r#"
+ layout {
+ pane
+ pane {
+ plugin location="zellij:tab-bar"
+ }
+ }
+ "#;
+
+ let (tiled_layout, floating_layout) = parse_kdl_layout(kdl_layout);
+ let terminal_ids = vec![(1, None)];
+ // Don't provide plugin IDs - empty HashMap
+ let new_plugin_ids = HashMap::new();
+
+ let size = Size {
+ cols: 100,
+ rows: 50,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ let result = applier.apply_layout(
+ tiled_layout,
+ floating_layout,
+ terminal_ids,
+ vec![],
+ new_plugin_ids,
+ 1,
+ );
+
+ // This should return an error - missing plugin ID
+ assert!(result.is_err());
+}
+
+#[test]
+fn test_apply_layout_with_excess_terminal_ids() {
+ let kdl_layout = r#"
+ layout {
+ pane
+ pane
+ }
+ "#;
+
+ let (tiled_layout, floating_layout) = parse_kdl_layout(kdl_layout);
+ // Provide more terminal IDs than needed
+ let terminal_ids = vec![(1, None), (2, None), (3, None), (4, None)];
+
+ let size = Size {
+ cols: 100,
+ rows: 50,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ let result = applier.apply_layout(
+ tiled_layout,
+ floating_layout,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ );
+
+ assert!(result.is_ok());
+
+ // Snapshot should show only 2 panes created
+ // Excess IDs should be closed by the applier
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_override_layout_basic_with_both_tiled_and_floating() {
+ // Setup: Apply initial layout with 2 tiled panes + 1 floating pane
+ let initial_kdl = r#"
+ layout {
+ pane command="htop"
+ pane command="vim"
+ floating_panes {
+ pane {
+ x 10
+ y 10
+ width 40
+ height 20
+ command "tail"
+ args "-f" "/var/log/syslog"
+ }
+ }
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None), (2, None)];
+ let floating_terminal_ids = vec![(3, None)];
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ floating_terminal_ids,
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Now override with different layout (2 tiled + 1 floating)
+ let override_kdl = r#"
+ layout {
+ pane command="top"
+ pane command="htop"
+ floating_panes {
+ pane {
+ x 20
+ y 20
+ width 50
+ height 25
+ command "watch"
+ args "df" "-h"
+ }
+ }
+ }
+ "#;
+
+ let (override_tiled, override_floating) = parse_kdl_layout(override_kdl);
+ let new_terminal_ids = vec![(4, None)];
+ let new_floating_terminal_ids = vec![(5, None)];
+
+ let retain_existing_terminal_panes = false;
+ let retain_existing_plugin_panes = false;
+ let should_show_floating = applier
+ .override_layout(
+ override_tiled,
+ override_floating,
+ new_terminal_ids,
+ new_floating_terminal_ids,
+ HashMap::new(),
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ 1,
+ )
+ .unwrap();
+
+ // Should show floating panes
+ assert_eq!(should_show_floating, true);
+
+ // Verify close messages were sent for vim (Terminal(2)) and tail (Terminal(3))
+ let closed_panes = collect_close_pane_messages(&pty_receiver);
+ assert_eq!(closed_panes.len(), 2);
+ assert!(closed_panes.contains(&PaneId::Terminal(2))); // vim
+ assert!(closed_panes.contains(&PaneId::Terminal(3))); // tail
+
+ // No plugins should be unloaded
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert!(unloaded_plugins.is_empty());
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_override_layout_hide_floating_panes_true() {
+ // Setup: Initial layout with floating panes
+ let initial_kdl = r#"
+ layout {
+ pane
+ floating_panes {
+ pane {
+ x 10
+ y 10
+ width 40
+ height 20
+ }
+ }
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None)];
+ let floating_terminal_ids = vec![(2, None)];
+
+ let size = Size {
+ cols: 100,
+ rows: 50,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ floating_terminal_ids,
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Override with layout that has hide_floating_panes true
+ let override_kdl = r#"
+ layout {
+ hide_floating_panes true
+ pane
+ pane
+ floating_panes {
+ pane {
+ x 15
+ y 15
+ width 45
+ height 22
+ }
+ }
+ }
+ "#;
+
+ let (override_tiled, override_floating) = parse_kdl_layout(override_kdl);
+ let new_terminal_ids = vec![(3, None)];
+ let new_floating_terminal_ids = vec![(4, None)];
+
+ let retain_existing_terminal_panes = false;
+ let retain_existing_plugin_panes = false;
+ let should_show_floating = applier
+ .override_layout(
+ override_tiled,
+ override_floating,
+ new_terminal_ids,
+ new_floating_terminal_ids,
+ HashMap::new(),
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ 1,
+ )
+ .unwrap();
+
+ // Should NOT show floating panes because of hide_floating_panes
+ assert_eq!(should_show_floating, false);
+
+ let closed_panes = collect_close_pane_messages(&pty_receiver);
+ assert_eq!(closed_panes.len(), 0);
+
+ // No plugins should be unloaded
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert!(unloaded_plugins.is_empty());
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_override_layout_show_floating_panes() {
+ // Setup: Initial layout
+ let initial_kdl = r#"
+ layout {
+ pane
+ pane
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None), (2, None)];
+
+ let size = Size {
+ cols: 100,
+ rows: 50,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Override with layout containing floating panes
+ let override_kdl = r#"
+ layout {
+ pane
+ floating_panes {
+ pane {
+ x 20
+ y 10
+ width 50
+ height 30
+ }
+ }
+ }
+ "#;
+
+ let (override_tiled, override_floating) = parse_kdl_layout(override_kdl);
+ let new_floating_terminal_ids = vec![(3, None)];
+
+ let retain_existing_terminal_panes = false;
+ let retain_existing_plugin_panes = false;
+ let should_show_floating = applier
+ .override_layout(
+ override_tiled,
+ override_floating,
+ vec![],
+ new_floating_terminal_ids,
+ HashMap::new(),
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ 1,
+ )
+ .unwrap();
+
+ // Should show floating panes
+ assert_eq!(should_show_floating, true);
+
+ // Verify close message was sent for one tiled pane (Terminal(2))
+ let closed_panes = collect_close_pane_messages(&pty_receiver);
+ assert_eq!(closed_panes.len(), 1);
+ assert!(closed_panes.contains(&PaneId::Terminal(2)));
+
+ // No plugins should be unloaded
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert!(unloaded_plugins.is_empty());
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+// ============================================================================
+// Suite 2: override_tiled_panes_layout_for_existing_panes Tests
+// ============================================================================
+
+#[test]
+fn test_override_tiled_exact_match_preservation_commands() {
+ // Setup: Apply initial layout with 3 panes running different commands
+ let initial_kdl = r#"
+ layout {
+ pane command="htop"
+ pane command="vim"
+ pane command="tail" {
+ args "-f" "/var/log/syslog"
+ }
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None), (2, None), (3, None)];
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Override: New layout with only htop and vim in different positions
+ let override_kdl = r#"
+ layout {
+ pane command="vim"
+ pane command="htop"
+ }
+ "#;
+
+ let (override_tiled, _) = parse_kdl_layout(override_kdl);
+
+ applier
+ .override_tiled_panes_layout_for_existing_panes(
+ &override_tiled,
+ vec![],
+ &mut HashMap::new(),
+ false,
+ false,
+ 1,
+ )
+ .unwrap();
+
+ // htop and vim panes should be preserved (same PaneIds: Terminal(1) and Terminal(2))
+ // tail pane should be closed
+ // Panes should be repositioned to new layout positions
+
+ // Verify close message was sent for tail pane (Terminal(3))
+ let closed_panes = collect_close_pane_messages(&pty_receiver);
+ assert_eq!(closed_panes.len(), 1);
+ assert!(closed_panes.contains(&PaneId::Terminal(3))); // tail
+
+ // No plugins should be unloaded
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert!(unloaded_plugins.is_empty());
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_override_tiled_exact_match_preservation_plugins() {
+ // Setup: Initial layout with 2 terminal panes + 1 plugin pane
+ let initial_kdl = r#"
+ layout {
+ pane
+ pane
+ pane {
+ plugin location="zellij:tab-bar"
+ }
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None), (2, None)];
+
+ let mut initial_plugin_ids = HashMap::new();
+ let tab_bar_plugin = RunPluginOrAlias::from_url("zellij:tab-bar", &None, None, None).unwrap();
+ initial_plugin_ids.insert(tab_bar_plugin.clone(), vec![100]);
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ vec![],
+ initial_plugin_ids,
+ 1,
+ )
+ .unwrap();
+
+ // Override: New layout with only the plugin pane
+ let override_kdl = r#"
+ layout {
+ pane {
+ plugin location="zellij:tab-bar"
+ }
+ }
+ "#;
+
+ let (override_tiled, _) = parse_kdl_layout(override_kdl);
+
+ applier
+ .override_tiled_panes_layout_for_existing_panes(
+ &override_tiled,
+ vec![],
+ &mut HashMap::new(),
+ false,
+ false,
+ 1,
+ )
+ .unwrap();
+
+ // Plugin pane should be preserved
+ // Terminal panes should be closed
+ // Total pane count is 1
+
+ // Verify close messages were sent for both terminal panes (Terminal(1) and Terminal(2))
+ let closed_panes = collect_close_pane_messages(&pty_receiver);
+ assert_eq!(closed_panes.len(), 2);
+ assert!(closed_panes.contains(&PaneId::Terminal(1)));
+ assert!(closed_panes.contains(&PaneId::Terminal(2)));
+
+ // No plugins should be unloaded (plugin is preserved)
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert!(unloaded_plugins.is_empty());
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_override_tiled_all_panes_closed_no_matches() {
+ // Setup: 3 panes running htop, vim, tail
+ let initial_kdl = r#"
+ layout {
+ pane command="htop"
+ pane command="vim"
+ pane command="tail" {
+ args "-f" "/var/log/syslog"
+ }
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None), (2, None), (3, None)];
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Override: New layout with 3 completely different commands
+ let override_kdl = r#"
+ layout {
+ pane command="cargo" {
+ args "watch"
+ }
+ pane command="npm" {
+ args "start"
+ }
+ pane command="python" {
+ args "-m" "http.server"
+ }
+ }
+ "#;
+
+ let (override_tiled, _) = parse_kdl_layout(override_kdl);
+ let new_terminal_ids = vec![(4, None), (5, None), (6, None)];
+
+ applier
+ .override_tiled_panes_layout_for_existing_panes(
+ &override_tiled,
+ new_terminal_ids,
+ &mut HashMap::new(),
+ false,
+ false,
+ 1,
+ )
+ .unwrap();
+
+ // All original pane IDs gone (1, 2, 3 should not be present)
+ // 3 new panes with new IDs (4, 5, 6)
+ // Total pane count is 3
+
+ // Verify close messages were sent for all original panes (Terminal(1), Terminal(2), Terminal(3))
+ let closed_panes = collect_close_pane_messages(&pty_receiver);
+ assert_eq!(closed_panes.len(), 3);
+ assert!(closed_panes.contains(&PaneId::Terminal(1))); // htop
+ assert!(closed_panes.contains(&PaneId::Terminal(2))); // vim
+ assert!(closed_panes.contains(&PaneId::Terminal(3))); // tail
+
+ // No plugins should be unloaded
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert!(unloaded_plugins.is_empty());
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_override_tiled_mixed_some_matches_some_new() {
+ // Setup: 2 panes - one running htop, one generic shell (no command)
+ let initial_kdl = r#"
+ layout {
+ pane command="htop"
+ pane
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None), (2, None)];
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Override: Layout with htop, vim, and generic shell
+ let override_kdl = r#"
+ layout {
+ pane command="htop"
+ pane command="vim"
+ pane
+ }
+ "#;
+
+ let (override_tiled, _) = parse_kdl_layout(override_kdl);
+ let new_terminal_ids = vec![(3, None), (4, None)];
+
+ applier
+ .override_tiled_panes_layout_for_existing_panes(
+ &override_tiled,
+ new_terminal_ids,
+ &mut HashMap::new(),
+ false,
+ false,
+ 1,
+ )
+ .unwrap();
+
+ // htop preserved with same ID (Terminal(1))
+ // Original shell pane closed (generic shells are NOT exact matches)
+ // 2 new panes created (vim and new shell)
+ // Total pane count is 3
+
+ // Verify close message was not sent for original shell pane (Terminal(2))
+ let closed_panes = collect_close_pane_messages(&pty_receiver);
+ assert_eq!(closed_panes.len(), 0);
+
+ // No plugins should be unloaded
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert!(unloaded_plugins.is_empty());
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_override_tiled_new_panes_for_unmatched_positions() {
+ // Setup: 1 pane running htop
+ let initial_kdl = r#"
+ layout {
+ pane command="htop"
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None)];
+
+ let size = Size {
+ cols: 150,
+ rows: 50,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Override: Layout with 4 positions
+ let override_kdl = r#"
+ layout {
+ pane command="htop"
+ pane command="vim"
+ pane
+ pane
+ }
+ "#;
+
+ let (override_tiled, _) = parse_kdl_layout(override_kdl);
+ let new_terminal_ids = vec![(2, None), (3, None), (4, None)];
+
+ applier
+ .override_tiled_panes_layout_for_existing_panes(
+ &override_tiled,
+ new_terminal_ids,
+ &mut HashMap::new(),
+ false,
+ false,
+ 1,
+ )
+ .unwrap();
+
+ // 1 original htop pane preserved (Terminal(1))
+ // 3 new panes created (Terminal(2), Terminal(3), Terminal(4))
+ // Total 4 panes
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_override_tiled_focus_on_new_pane() {
+ // Setup: 2 panes, first one focused
+ let initial_kdl = r#"
+ layout {
+ pane focus=true
+ pane
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None), (2, None)];
+
+ let size = Size {
+ cols: 100,
+ rows: 50,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Override: Layout with 3 panes where second pane has focus=true
+ let override_kdl = r#"
+ layout {
+ pane
+ pane focus=true
+ pane
+ }
+ "#;
+
+ let (override_tiled, _) = parse_kdl_layout(override_kdl);
+ let new_terminal_ids = vec![(3, None)];
+
+ applier
+ .override_tiled_panes_layout_for_existing_panes(
+ &override_tiled,
+ new_terminal_ids,
+ &mut HashMap::new(),
+ false,
+ false,
+ 1,
+ )
+ .unwrap();
+
+ // focus_pane_id should point to the newly created middle pane (Terminal(3))
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_override_tiled_focus_when_focused_pane_closed() {
+ // Setup: 3 panes, middle one focused running vim
+ let initial_kdl = r#"
+ layout {
+ pane command="htop"
+ pane command="vim" focus=true
+ pane command="tail" {
+ args "-f" "/var/log/syslog"
+ }
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None), (2, None), (3, None)];
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Override: Layout with 2 panes running htop and cargo (not vim)
+ let override_kdl = r#"
+ layout {
+ pane command="htop"
+ pane command="cargo" {
+ args "check"
+ }
+ }
+ "#;
+
+ let (override_tiled, _) = parse_kdl_layout(override_kdl);
+ let new_terminal_ids = vec![(4, None)];
+
+ applier
+ .override_tiled_panes_layout_for_existing_panes(
+ &override_tiled,
+ new_terminal_ids,
+ &mut HashMap::new(),
+ false,
+ false,
+ 1,
+ )
+ .unwrap();
+
+ // Focused pane (vim) no longer exists
+ // Focus should be moved to one of the remaining panes
+
+ // Verify close messages were sent for vim (Terminal(2)) and tail (Terminal(3))
+ let closed_panes = collect_close_pane_messages(&pty_receiver);
+ assert_eq!(closed_panes.len(), 2);
+ assert!(closed_panes.contains(&PaneId::Terminal(2))); // vim (focused)
+ assert!(closed_panes.contains(&PaneId::Terminal(3))); // tail
+
+ // No plugins should be unloaded
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert!(unloaded_plugins.is_empty());
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_override_tiled_empty_layout_closes_all() {
+ // Setup: 3 panes running various commands
+ let initial_kdl = r#"
+ layout {
+ pane command="htop"
+ pane command="vim"
+ pane command="tail" {
+ args "-f" "/var/log/syslog"
+ }
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None), (2, None), (3, None)];
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Override: Empty layout
+ let override_kdl = r#"
+ layout {
+ }
+ "#;
+
+ let (override_tiled, _) = parse_kdl_layout(override_kdl);
+
+ applier
+ .override_tiled_panes_layout_for_existing_panes(
+ &override_tiled,
+ vec![],
+ &mut HashMap::new(),
+ false,
+ false,
+ 1,
+ )
+ .unwrap();
+
+ // No panes in snapshot
+ // Pane count is 0
+
+ // Verify close messages were sent for all panes (Terminal(1), Terminal(2), Terminal(3))
+ let closed_panes = collect_close_pane_messages(&pty_receiver);
+ assert_eq!(closed_panes.len(), 3);
+ assert!(closed_panes.contains(&PaneId::Terminal(1))); // htop
+ assert!(closed_panes.contains(&PaneId::Terminal(2))); // vim
+ assert!(closed_panes.contains(&PaneId::Terminal(3))); // tail
+
+ // No plugins should be unloaded
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert!(unloaded_plugins.is_empty());
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+// ============================================================================
+// Suite 3: override_floating_panes_layout_for_existing_panes Tests
+// ============================================================================
+
+#[test]
+fn test_override_floating_exact_match_preservation() {
+ // Setup: 2 floating panes running htop and vim
+ let initial_kdl = r#"
+ layout {
+ pane
+ floating_panes {
+ pane {
+ x 10
+ y 10
+ width 40
+ height 20
+ command "htop"
+ }
+ pane {
+ x 20
+ y 20
+ width 50
+ height 25
+ command "vim"
+ }
+ }
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None)];
+ let floating_terminal_ids = vec![(2, None), (3, None)];
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ floating_terminal_ids,
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Override: Layout with htop at different x/y position
+ let override_kdl = r#"
+ layout {
+ pane
+ floating_panes {
+ pane {
+ x 50
+ y 30
+ width 45
+ height 22
+ command "htop"
+ }
+ }
+ }
+ "#;
+
+ let (_, override_floating) = parse_kdl_layout(override_kdl);
+
+ applier
+ .override_floating_panes_layout_for_existing_panes(
+ &override_floating,
+ vec![],
+ &mut HashMap::new(),
+ false,
+ false,
+ )
+ .unwrap();
+
+ // htop pane preserved (Terminal(2)), repositioned
+ // vim pane closed (Terminal(3))
+ // Total floating pane count is 1
+
+ // Verify close message was sent for vim pane (Terminal(3))
+ let closed_panes = collect_close_pane_messages(&pty_receiver);
+ assert_eq!(closed_panes.len(), 1);
+ assert!(closed_panes.contains(&PaneId::Terminal(3))); // vim
+
+ // No plugins should be unloaded
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert!(unloaded_plugins.is_empty());
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_override_floating_all_closed_no_matches() {
+ // Setup: 2 floating panes with specific commands
+ let initial_kdl = r#"
+ layout {
+ pane
+ floating_panes {
+ pane {
+ x 10
+ y 10
+ width 40
+ height 20
+ command "htop"
+ }
+ pane {
+ x 20
+ y 20
+ width 50
+ height 25
+ command "vim"
+ }
+ }
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None)];
+ let floating_terminal_ids = vec![(2, None), (3, None)];
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ floating_terminal_ids,
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Override: Layout with different commands
+ let override_kdl = r#"
+ layout {
+ pane
+ floating_panes {
+ pane {
+ x 15
+ y 15
+ width 45
+ height 22
+ command "top"
+ }
+ pane {
+ x 25
+ y 25
+ width 55
+ height 27
+ command "emacs"
+ }
+ }
+ }
+ "#;
+
+ let (_, override_floating) = parse_kdl_layout(override_kdl);
+ let new_floating_terminal_ids = vec![(4, None), (5, None)];
+
+ applier
+ .override_floating_panes_layout_for_existing_panes(
+ &override_floating,
+ new_floating_terminal_ids,
+ &mut HashMap::new(),
+ false,
+ false,
+ )
+ .unwrap();
+
+ // Both original panes closed (IDs 2, 3 gone)
+ // New panes created with new IDs (4, 5)
+ // Pane count matches new layout (2 floating panes)
+
+ // Verify close messages were sent for both floating panes (Terminal(2), Terminal(3))
+ let closed_panes = collect_close_pane_messages(&pty_receiver);
+ assert_eq!(closed_panes.len(), 2);
+ assert!(closed_panes.contains(&PaneId::Terminal(2))); // htop
+ assert!(closed_panes.contains(&PaneId::Terminal(3))); // vim
+
+ // No plugins should be unloaded
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert!(unloaded_plugins.is_empty());
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_override_floating_new_panes_created() {
+ // Setup: 1 floating pane running htop
+ let initial_kdl = r#"
+ layout {
+ pane
+ floating_panes {
+ pane {
+ x 10
+ y 10
+ width 40
+ height 20
+ command "htop"
+ }
+ }
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None)];
+ let floating_terminal_ids = vec![(2, None)];
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ floating_terminal_ids,
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Override: Layout with 3 floating panes: htop, vim, and generic shell
+ let override_kdl = r#"
+ layout {
+ pane
+ floating_panes {
+ pane {
+ x 10
+ y 10
+ width 40
+ height 20
+ command "htop"
+ }
+ pane {
+ x 20
+ y 20
+ width 50
+ height 25
+ command "vim"
+ }
+ pane {
+ x 30
+ y 30
+ width 45
+ height 22
+ }
+ }
+ }
+ "#;
+
+ let (_, override_floating) = parse_kdl_layout(override_kdl);
+ let new_floating_terminal_ids = vec![(3, None), (4, None)];
+
+ applier
+ .override_floating_panes_layout_for_existing_panes(
+ &override_floating,
+ new_floating_terminal_ids,
+ &mut HashMap::new(),
+ false,
+ false,
+ )
+ .unwrap();
+
+ // Original htop preserved (Terminal(2))
+ // 2 new floating panes created (Terminal(3), Terminal(4))
+ // Total 3 floating panes
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_override_floating_focus_handling() {
+ // Setup: 2 floating panes, one focused
+ let initial_kdl = r#"
+ layout {
+ pane
+ floating_panes {
+ pane {
+ x 10
+ y 10
+ width 40
+ height 20
+ focus true
+ }
+ pane {
+ x 20
+ y 20
+ width 50
+ height 25
+ }
+ }
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None)];
+ let floating_terminal_ids = vec![(2, None), (3, None)];
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ floating_terminal_ids,
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Override: Layout with 1 new pane that has focus=true
+ let override_kdl = r#"
+ layout {
+ pane
+ floating_panes {
+ pane {
+ x 30
+ y 30
+ width 60
+ height 30
+ focus true
+ }
+ }
+ }
+ "#;
+
+ let (_, override_floating) = parse_kdl_layout(override_kdl);
+ let new_floating_terminal_ids = vec![(4, None)];
+
+ applier
+ .override_floating_panes_layout_for_existing_panes(
+ &override_floating,
+ new_floating_terminal_ids,
+ &mut HashMap::new(),
+ false,
+ false,
+ )
+ .unwrap();
+
+ // Focus should be set on newly created pane (Terminal(4))
+
+ // Verify close messages were sent for Terminal(3)
+ let closed_panes = collect_close_pane_messages(&pty_receiver);
+ assert_eq!(closed_panes.len(), 1);
+ assert!(closed_panes.contains(&PaneId::Terminal(3)));
+
+ // No plugins should be unloaded
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert!(unloaded_plugins.is_empty());
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_override_floating_position_and_size_update() {
+ // Setup: 1 floating pane running htop at specific position
+ let initial_kdl = r#"
+ layout {
+ pane
+ floating_panes {
+ pane {
+ x 10
+ y 10
+ width 40
+ height 20
+ command "htop"
+ }
+ }
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None)];
+ let floating_terminal_ids = vec![(2, None)];
+
+ let size = Size {
+ cols: 120,
+ rows: 50,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ floating_terminal_ids,
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Override: Layout with htop at different position and size
+ let override_kdl = r#"
+ layout {
+ pane
+ floating_panes {
+ pane {
+ x 50
+ y 30
+ width 60
+ height 30
+ command "htop"
+ }
+ }
+ }
+ "#;
+
+ let (_, override_floating) = parse_kdl_layout(override_kdl);
+
+ applier
+ .override_floating_panes_layout_for_existing_panes(
+ &override_floating,
+ vec![],
+ &mut HashMap::new(),
+ false,
+ false,
+ )
+ .unwrap();
+
+ // Same pane ID preserved (Terminal(2))
+ // Geometry updated: x=50, y=30, cols=60, rows=30
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_override_floating_return_value_has_panes() {
+ // Setup: Empty floating panes
+ let initial_kdl = r#"
+ layout {
+ pane
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None)];
+
+ let size = Size {
+ cols: 100,
+ rows: 50,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ ) = create_layout_applier_fixtures(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Override: Layout with 1 floating pane
+ let override_kdl = r#"
+ layout {
+ pane
+ floating_panes {
+ pane {
+ x 20
+ y 10
+ width 50
+ height 30
+ }
+ }
+ }
+ "#;
+
+ let (_, override_floating) = parse_kdl_layout(override_kdl);
+ let new_floating_terminal_ids = vec![(2, None)];
+
+ let has_floating_panes = applier
+ .override_floating_panes_layout_for_existing_panes(
+ &override_floating,
+ new_floating_terminal_ids,
+ &mut HashMap::new(),
+ false,
+ false,
+ )
+ .unwrap();
+
+ // Function should return true because layout has floating panes
+ assert_eq!(has_floating_panes, true);
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_override_floating_return_value_no_panes() {
+ // Setup: 1 floating pane
+ let initial_kdl = r#"
+ layout {
+ pane
+ floating_panes {
+ pane {
+ x 10
+ y 10
+ width 40
+ height 20
+ }
+ }
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None)];
+ let floating_terminal_ids = vec![(2, None)];
+
+ let size = Size {
+ cols: 100,
+ rows: 50,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ floating_terminal_ids,
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Override: Empty floating layout (no floating_panes block)
+ let override_kdl = r#"
+ layout {
+ pane
+ }
+ "#;
+
+ let (_, override_floating) = parse_kdl_layout(override_kdl);
+
+ let has_floating_panes = applier
+ .override_floating_panes_layout_for_existing_panes(
+ &override_floating,
+ vec![],
+ &mut HashMap::new(),
+ false,
+ false,
+ )
+ .unwrap();
+
+ // Function should return false because layout has no floating panes
+ assert_eq!(has_floating_panes, false);
+
+ // Verify close message was sent for floating pane (Terminal(2))
+ let closed_panes = collect_close_pane_messages(&pty_receiver);
+ assert_eq!(closed_panes.len(), 1);
+ assert!(closed_panes.contains(&PaneId::Terminal(2)));
+
+ // No plugins should be unloaded
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert!(unloaded_plugins.is_empty());
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+// ============================================================================
+// Suite 4: Integration Tests
+// ============================================================================
+
+#[test]
+fn test_override_full_tiled_and_floating_together() {
+ // Setup: Initial layout with 3 tiled panes + 2 floating panes
+ let initial_kdl = r#"
+ layout {
+ pane command="htop"
+ pane command="vim"
+ pane
+ floating_panes {
+ pane {
+ x 10
+ y 10
+ width 40
+ height 20
+ command "cargo"
+ args "watch"
+ }
+ pane {
+ x 20
+ y 20
+ width 50
+ height 25
+ command "tail"
+ args "-f" "/var/log/syslog"
+ }
+ }
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None), (2, None), (3, None)];
+ let floating_terminal_ids = vec![(4, None), (5, None)];
+
+ let size = Size {
+ cols: 150,
+ rows: 50,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ floating_terminal_ids,
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Override: Layout with 2 tiled (htop, npm start) + 1 floating (cargo watch)
+ let override_kdl = r#"
+ layout {
+ pane command="htop"
+ pane command="npm" {
+ args "start"
+ }
+ floating_panes {
+ pane {
+ x 30
+ y 30
+ width 60
+ height 30
+ command "cargo"
+ args "watch"
+ }
+ }
+ }
+ "#;
+
+ let (override_tiled, override_floating) = parse_kdl_layout(override_kdl);
+ let new_terminal_ids = vec![(6, None)];
+
+ applier
+ .override_tiled_panes_layout_for_existing_panes(
+ &override_tiled,
+ new_terminal_ids,
+ &mut HashMap::new(),
+ false,
+ false,
+ 1,
+ )
+ .unwrap();
+
+ applier
+ .override_floating_panes_layout_for_existing_panes(
+ &override_floating,
+ vec![],
+ &mut HashMap::new(),
+ false,
+ false,
+ )
+ .unwrap();
+
+ // Tiled: htop preserved (Terminal(1)), vim and shell closed, npm start created (Terminal(6))
+ // Floating: cargo watch preserved (Terminal(4)), tail closed
+ // Total: 2 tiled + 1 floating
+
+ // Verify close messages were sent for vim (Terminal(2)), shell (Terminal(3)), and tail (Terminal(5))
+ let closed_panes = collect_close_pane_messages(&pty_receiver);
+ assert_eq!(closed_panes.len(), 3);
+ assert!(closed_panes.contains(&PaneId::Terminal(2))); // vim
+ assert!(closed_panes.contains(&PaneId::Terminal(3))); // shell
+ assert!(closed_panes.contains(&PaneId::Terminal(5))); // tail
+
+ // No plugins should be unloaded
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert!(unloaded_plugins.is_empty());
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_override_viewport_adjustment_with_borderless() {
+ // Setup: Initial layout with borderless panes
+ let initial_kdl = r#"
+ layout {
+ pane borderless=true
+ pane
+ pane borderless=true
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None), (2, None), (3, None)];
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Override: Layout with different borderless configuration
+ let override_kdl = r#"
+ layout {
+ pane
+ pane borderless=true
+ }
+ "#;
+
+ let (override_tiled, _) = parse_kdl_layout(override_kdl);
+
+ applier
+ .override_tiled_panes_layout_for_existing_panes(
+ &override_tiled,
+ vec![],
+ &mut HashMap::new(),
+ false,
+ false,
+ 1,
+ )
+ .unwrap();
+
+ // Viewport dimensions should be correctly adjusted for borderless panes
+
+ // Verify close message was sent for at least the extra pane (Terminal(3))
+ // Generic panes without commands don't match exactly, so all 3 may be closed
+ let closed_panes = collect_close_pane_messages(&pty_receiver);
+ assert!(closed_panes.len() >= 1);
+ assert!(closed_panes.contains(&PaneId::Terminal(3)));
+
+ // No plugins should be unloaded
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert!(unloaded_plugins.is_empty());
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_override_tiled_retain_terminal_panes_partial_match() {
+ // Test that when retain_existing_terminal_panes is true, terminal panes that don't match
+ // the new layout are retained instead of being closed.
+ // Setup: Apply initial layout with 3 panes running different commands
+ let initial_kdl = r#"
+ layout {
+ pane command="htop"
+ pane command="vim"
+ pane command="tail" {
+ args "-f" "/var/log/syslog"
+ }
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None), (2, None), (3, None)];
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Override: New layout with only vim and htop (tail is not in the new layout)
+ let override_kdl = r#"
+ layout {
+ pane command="vim"
+ pane command="htop"
+ }
+ "#;
+
+ let (override_tiled, _) = parse_kdl_layout(override_kdl);
+
+ // Override with retain_existing_terminal_panes = true
+ applier
+ .override_tiled_panes_layout_for_existing_panes(
+ &override_tiled,
+ vec![],
+ &mut HashMap::new(),
+ true, // retain_existing_terminal_panes
+ false,
+ 1,
+ )
+ .unwrap();
+
+ // With retain_existing_terminal_panes = true:
+ // - NO terminal panes should be closed (not even tail)
+ // - All 3 original terminals (Terminal(1), Terminal(2), Terminal(3)) should still exist
+ // - vim and htop panes should match the new layout positions
+ // - tail pane should be retained and added after the matched panes
+
+ // Verify NO close messages were sent
+ let closed_panes = collect_close_pane_messages(&pty_receiver);
+ assert_eq!(
+ closed_panes.len(),
+ 0,
+ "No terminal panes should be closed when retain_existing_terminal_panes is true"
+ );
+
+ // No plugins should be unloaded
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert!(unloaded_plugins.is_empty());
+
+ // All 3 original terminals should still exist
+ assert_eq!(
+ tiled_panes.visible_panes_count(),
+ 3,
+ "All 3 terminal panes should be retained"
+ );
+
+ // we're not asserting a snapshot here because adding panes uses unstable sorting and so the
+ // test would be flaky
+}
+
+#[test]
+fn test_override_tiled_retain_terminal_panes_no_matches() {
+ // Test that when retain_existing_terminal_panes is true and NO panes match,
+ // all original terminals are retained AND new terminals are created.
+ // Setup: Apply initial layout with 3 panes
+ let initial_kdl = r#"
+ layout {
+ pane command="htop"
+ pane command="vim"
+ pane command="tail" {
+ args "-f" "/var/log/syslog"
+ }
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None), (2, None), (3, None)];
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ vec![],
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Override: New layout with completely different commands (no matches)
+ let override_kdl = r#"
+ layout {
+ pane command="cargo"
+ pane command="npm"
+ pane command="python"
+ }
+ "#;
+
+ let (override_tiled, _) = parse_kdl_layout(override_kdl);
+ let new_terminal_ids = vec![(4, None), (5, None), (6, None)];
+
+ // Override with retain_existing_terminal_panes = true
+ applier
+ .override_tiled_panes_layout_for_existing_panes(
+ &override_tiled,
+ new_terminal_ids,
+ &mut HashMap::new(),
+ true, // retain_existing_terminal_panes
+ false,
+ 1,
+ )
+ .unwrap();
+
+ // With retain_existing_terminal_panes = true and no matches:
+ // - NO terminal panes should be closed
+ // - All 3 original terminals (Terminal(1), Terminal(2), Terminal(3)) should still exist
+ // - 3 NEW terminals (Terminal(4), Terminal(5), Terminal(6)) should be created
+ // - Total: 6 terminal panes
+
+ // Verify NO close messages were sent
+ let closed_panes = collect_close_pane_messages(&pty_receiver);
+ assert_eq!(
+ closed_panes.len(),
+ 0,
+ "No terminal panes should be closed when retain_existing_terminal_panes is true"
+ );
+
+ // No plugins should be unloaded
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert!(unloaded_plugins.is_empty());
+
+ // Should have 6 total panes (3 original + 3 new)
+ assert_eq!(
+ tiled_panes.visible_panes_count(),
+ 6,
+ "Should have 6 terminal panes (3 original + 3 new)"
+ );
+
+ // we're not asserting a snapshot here because adding panes uses unstable sorting and so the
+ // test would be flaky
+}
+
+#[test]
+fn test_override_floating_retain_terminal_panes_partial_match() {
+ // Test that when retain_existing_terminal_panes is true, floating terminal panes
+ // that don't match the new layout are retained instead of being closed.
+ // Setup: 1 tiled pane + 2 floating panes running htop and vim
+ let initial_kdl = r#"
+ layout {
+ pane
+ floating_panes {
+ pane {
+ x 10
+ y 10
+ width 40
+ height 20
+ command "htop"
+ }
+ pane {
+ x 20
+ y 20
+ width 50
+ height 25
+ command "vim"
+ }
+ }
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None)];
+ let floating_terminal_ids = vec![(2, None), (3, None)];
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ floating_terminal_ids,
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Override: Layout with only htop (vim is not in the new layout)
+ let override_kdl = r#"
+ layout {
+ pane
+ floating_panes {
+ pane {
+ x 50
+ y 30
+ width 45
+ height 22
+ command "htop"
+ }
+ }
+ }
+ "#;
+
+ let (_, override_floating) = parse_kdl_layout(override_kdl);
+
+ // Override with retain_existing_terminal_panes = true
+ applier
+ .override_floating_panes_layout_for_existing_panes(
+ &override_floating,
+ vec![],
+ &mut HashMap::new(),
+ true, // retain_existing_terminal_panes
+ false,
+ )
+ .unwrap();
+
+ // With retain_existing_terminal_panes = true:
+ // - NO terminal panes should be closed (not even vim)
+ // - Both original floating terminals (Terminal(2), Terminal(3)) should still exist
+ // - htop pane should match the new layout position
+ // - vim pane should be retained as a floating pane
+
+ // Verify NO close messages were sent
+ let closed_panes = collect_close_pane_messages(&pty_receiver);
+ assert_eq!(
+ closed_panes.len(),
+ 0,
+ "No terminal panes should be closed when retain_existing_terminal_panes is true"
+ );
+
+ // No plugins should be unloaded
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert!(unloaded_plugins.is_empty());
+
+ // Both floating panes should still exist
+ assert_eq!(
+ floating_panes.visible_panes_count(),
+ 2,
+ "Both floating terminal panes should be retained"
+ );
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_override_floating_retain_terminal_panes_no_matches() {
+ // Test that when retain_existing_terminal_panes is true and NO floating panes match,
+ // all original floating terminals are retained AND new floating terminals are created.
+ // Setup: 1 tiled pane + 2 floating panes running htop and vim
+ let initial_kdl = r#"
+ layout {
+ pane
+ floating_panes {
+ pane {
+ x 10
+ y 10
+ width 40
+ height 20
+ command "htop"
+ }
+ pane {
+ x 20
+ y 20
+ width 50
+ height 25
+ command "vim"
+ }
+ }
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None)];
+ let floating_terminal_ids = vec![(2, None), (3, None)];
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ floating_terminal_ids,
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Override: Layout with 2 different floating panes (top and emacs)
+ let override_kdl = r#"
+ layout {
+ pane
+ floating_panes {
+ pane {
+ x 30
+ y 30
+ width 40
+ height 20
+ command "top"
+ }
+ pane {
+ x 40
+ y 40
+ width 50
+ height 25
+ command "emacs"
+ }
+ }
+ }
+ "#;
+
+ let (_, override_floating) = parse_kdl_layout(override_kdl);
+ let new_floating_terminal_ids = vec![(4, None), (5, None)];
+
+ // Override with retain_existing_terminal_panes = true
+ applier
+ .override_floating_panes_layout_for_existing_panes(
+ &override_floating,
+ new_floating_terminal_ids,
+ &mut HashMap::new(),
+ true, // retain_existing_terminal_panes
+ false,
+ )
+ .unwrap();
+
+ // With retain_existing_terminal_panes = true and no matches:
+ // - NO terminal panes should be closed
+ // - Both original floating terminals (Terminal(2), Terminal(3)) should still exist
+ // - 2 NEW floating terminals (Terminal(4), Terminal(5)) should be created
+ // - Total: 4 floating panes
+
+ // Verify NO close messages were sent
+ let closed_panes = collect_close_pane_messages(&pty_receiver);
+ assert_eq!(
+ closed_panes.len(),
+ 0,
+ "No terminal panes should be closed when retain_existing_terminal_panes is true"
+ );
+
+ // No plugins should be unloaded
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert!(unloaded_plugins.is_empty());
+
+ // Should have 4 floating panes (2 original + 2 new)
+ assert_eq!(
+ floating_panes.visible_panes_count(),
+ 4,
+ "Should have 4 floating panes (2 original + 2 new)"
+ );
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_override_mixed_retain_terminal_panes_both_tiled_and_floating() {
+ // Test that when retain_existing_terminal_panes is true, both tiled and floating
+ // terminal panes that don't match the new layout are retained.
+ // Setup: Apply initial layout with 3 tiled panes + 1 floating pane
+ let initial_kdl = r#"
+ layout {
+ pane command="htop"
+ pane command="vim"
+ pane command="tail" {
+ args "-f" "/var/log/syslog"
+ }
+ floating_panes {
+ pane {
+ x 10
+ y 10
+ width 40
+ height 20
+ command "watch"
+ args "df" "-h"
+ }
+ }
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None), (2, None), (3, None)];
+ let floating_terminal_ids = vec![(4, None)];
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ floating_terminal_ids,
+ HashMap::new(),
+ 1,
+ )
+ .unwrap();
+
+ // Override: 2 tiled (top, htop) + 1 floating (different watch command)
+ let override_kdl = r#"
+ layout {
+ pane command="top"
+ pane command="htop"
+ floating_panes {
+ pane {
+ x 20
+ y 20
+ width 50
+ height 25
+ command "watch"
+ args "free" "-h"
+ }
+ }
+ }
+ "#;
+
+ let (override_tiled, override_floating) = parse_kdl_layout(override_kdl);
+ let new_terminal_ids = vec![(5, None)];
+ let new_floating_terminal_ids = vec![(6, None)];
+
+ let retain_existing_terminal_panes = true;
+ let retain_existing_plugin_panes = false;
+ applier
+ .override_layout(
+ override_tiled,
+ override_floating,
+ new_terminal_ids,
+ new_floating_terminal_ids,
+ HashMap::new(),
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ 1,
+ )
+ .unwrap();
+
+ // With retain_existing_terminal_panes = true:
+ // - NO terminal panes should be closed (neither tiled nor floating)
+ // - Original tiled terminals: Terminal(1), Terminal(2), Terminal(3) retained
+ // (htop matches, so it's reused; vim and tail are retained)
+ // - Original floating terminal: Terminal(4) retained
+ // - New tiled terminal: Terminal(5) created (for top, htop matches)
+ // - New floating terminal: Terminal(6) created (different watch command)
+
+ // Verify NO close messages were sent
+ let closed_panes = collect_close_pane_messages(&pty_receiver);
+ assert_eq!(
+ closed_panes.len(),
+ 0,
+ "No terminal panes should be closed when retain_existing_terminal_panes is true"
+ );
+
+ // No plugins should be unloaded
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert!(unloaded_plugins.is_empty());
+
+ // Check tiled pane count (3 original + 1 new = 4)
+ assert_eq!(
+ tiled_panes.visible_panes_count(),
+ 4,
+ "Should have 4 tiled panes (3 original + 1 new)"
+ );
+
+ // Check floating pane count (1 original + 1 new = 2)
+ assert_eq!(
+ floating_panes.visible_panes_count(),
+ 2,
+ "Should have 2 floating panes (1 original + 1 new)"
+ );
+
+ // we're not asserting a snapshot here because adding panes uses unstable sorting and so the
+ // test would be flaky
+}
+
+#[test]
+fn test_override_retain_terminal_but_close_plugin_panes() {
+ // Test that when retain_existing_terminal_panes is true, the flag ONLY affects
+ // terminal panes and plugin panes are still closed as normal.
+ // Setup: Initial layout with 2 terminal panes + 1 plugin pane
+ let initial_kdl = r#"
+ layout {
+ pane command="htop"
+ pane command="vim"
+ pane {
+ plugin location="zellij:tab-bar"
+ }
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+ let terminal_ids = vec![(1, None), (2, None)];
+
+ let mut initial_plugin_ids = HashMap::new();
+ let tab_bar_plugin = RunPluginOrAlias::from_url("zellij:tab-bar", &None, None, None).unwrap();
+ initial_plugin_ids.insert(tab_bar_plugin.clone(), vec![100]);
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ vec![],
+ initial_plugin_ids,
+ 1,
+ )
+ .unwrap();
+
+ // Override: New layout with only htop (vim and tab-bar plugin not in new layout)
+ let override_kdl = r#"
+ layout {
+ pane command="htop"
+ }
+ "#;
+
+ let (override_tiled, _) = parse_kdl_layout(override_kdl);
+
+ // Override with retain_existing_terminal_panes = true
+ applier
+ .override_tiled_panes_layout_for_existing_panes(
+ &override_tiled,
+ vec![],
+ &mut HashMap::new(),
+ true, // retain_existing_terminal_panes
+ false,
+ 1,
+ )
+ .unwrap();
+
+ // With retain_existing_terminal_panes = true:
+ // - Terminal panes NOT closed: Terminal(1) matched (htop), Terminal(2) retained (vim)
+ // - Plugin pane IS closed: Plugin(100) unloaded (tab-bar)
+ // - Both terminals should exist, but plugin should be gone
+
+ // No terminal panes should be closed
+ let closed_panes = collect_close_pane_messages(&pty_receiver);
+ assert_eq!(
+ closed_panes.len(),
+ 0,
+ "No terminal panes should be closed when retain_existing_terminal_panes is true"
+ );
+
+ // Plugin pane should be unloaded
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert_eq!(
+ unloaded_plugins.len(),
+ 1,
+ "Plugin pane should be unloaded even with retain_existing_terminal_panes"
+ );
+ assert!(
+ unloaded_plugins.contains(&100),
+ "Tab-bar plugin (100) should be unloaded"
+ );
+
+ // Both terminals should exist
+ assert_eq!(
+ tiled_panes.visible_panes_count(),
+ 2,
+ "Both terminal panes should be retained"
+ );
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
+
+#[test]
+fn test_override_tiled_retain_plugin_panes_partial_match() {
+ // Verify that when retain_existing_plugin_panes = true, plugin panes that don't match
+ // the new layout are retained instead of being closed
+ let initial_kdl = r#"
+ layout {
+ pane
+ pane {
+ plugin location="zellij:tab-bar"
+ }
+ pane {
+ plugin location="zellij:status-bar"
+ }
+ pane {
+ plugin location="file:///path/to/custom.wasm"
+ }
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+
+ let terminal_ids = vec![(1, None)];
+
+ let mut initial_plugin_ids = HashMap::new();
+ let tab_bar = RunPluginOrAlias::from_url("zellij:tab-bar", &None, None, None).unwrap();
+ let status_bar = RunPluginOrAlias::from_url("zellij:status-bar", &None, None, None).unwrap();
+ let custom =
+ RunPluginOrAlias::from_url("file:///path/to/custom.wasm", &None, None, None).unwrap();
+ initial_plugin_ids.insert(tab_bar, vec![100]);
+ initial_plugin_ids.insert(status_bar, vec![101]);
+ initial_plugin_ids.insert(custom, vec![102]);
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ vec![],
+ initial_plugin_ids,
+ 1,
+ )
+ .unwrap();
+
+ let override_kdl = r#"
+ layout {
+ pane
+ pane {
+ plugin location="zellij:tab-bar"
+ }
+ pane {
+ plugin location="zellij:status-bar"
+ }
+ }
+ "#;
+
+ let (override_tiled, _) = parse_kdl_layout(override_kdl);
+
+ applier
+ .override_tiled_panes_layout_for_existing_panes(
+ &override_tiled,
+ vec![],
+ &mut HashMap::new(),
+ false,
+ true,
+ 1,
+ )
+ .unwrap();
+
+ // Verify NO plugin panes were unloaded
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert_eq!(
+ unloaded_plugins.len(),
+ 0,
+ "No plugin panes should be unloaded when retain_existing_plugin_panes is true"
+ );
+
+ // No terminals should be closed
+ let closed_panes = collect_close_pane_messages(&pty_receiver);
+ assert!(closed_panes.is_empty());
+
+ // All 4 panes should exist (1 terminal + 3 plugins)
+ assert_eq!(
+ tiled_panes.visible_panes_count(),
+ 4,
+ "All plugin panes should be retained"
+ );
+}
+
+#[test]
+fn test_override_tiled_retain_plugin_panes_no_matches() {
+ // When NO plugins match the new layout, all original plugins are retained AND new plugins are created
+ let initial_kdl = r#"
+ layout {
+ pane
+ pane {
+ plugin location="zellij:tab-bar"
+ }
+ pane {
+ plugin location="zellij:status-bar"
+ }
+ pane {
+ plugin location="zellij:compact-bar"
+ }
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+
+ let terminal_ids = vec![(1, None)];
+
+ let mut initial_plugin_ids = HashMap::new();
+ let tab_bar = RunPluginOrAlias::from_url("zellij:tab-bar", &None, None, None).unwrap();
+ let status_bar = RunPluginOrAlias::from_url("zellij:status-bar", &None, None, None).unwrap();
+ let compact_bar = RunPluginOrAlias::from_url("zellij:compact-bar", &None, None, None).unwrap();
+ initial_plugin_ids.insert(tab_bar, vec![100]);
+ initial_plugin_ids.insert(status_bar, vec![101]);
+ initial_plugin_ids.insert(compact_bar, vec![102]);
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ vec![],
+ initial_plugin_ids,
+ 1,
+ )
+ .unwrap();
+
+ let override_kdl = r#"
+ layout {
+ pane
+ pane {
+ plugin location="file:///path/to/plugin1.wasm"
+ }
+ pane {
+ plugin location="file:///path/to/plugin2.wasm"
+ }
+ pane {
+ plugin location="file:///path/to/plugin3.wasm"
+ }
+ }
+ "#;
+
+ let (override_tiled, _) = parse_kdl_layout(override_kdl);
+
+ let mut override_plugin_ids = HashMap::new();
+ let plugin1 =
+ RunPluginOrAlias::from_url("file:///path/to/plugin1.wasm", &None, None, None).unwrap();
+ let plugin2 =
+ RunPluginOrAlias::from_url("file:///path/to/plugin2.wasm", &None, None, None).unwrap();
+ let plugin3 =
+ RunPluginOrAlias::from_url("file:///path/to/plugin3.wasm", &None, None, None).unwrap();
+ override_plugin_ids.insert(plugin1, vec![103]);
+ override_plugin_ids.insert(plugin2, vec![104]);
+ override_plugin_ids.insert(plugin3, vec![105]);
+
+ applier
+ .override_tiled_panes_layout_for_existing_panes(
+ &override_tiled,
+ vec![],
+ &mut override_plugin_ids,
+ false,
+ true,
+ 1,
+ )
+ .unwrap();
+
+ // Verify NO plugin panes were unloaded
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert_eq!(
+ unloaded_plugins.len(),
+ 0,
+ "No plugin panes should be unloaded when retain_existing_plugin_panes is true"
+ );
+
+ // Total: 1 terminal + 6 plugins (3 original + 3 new)
+ assert_eq!(
+ tiled_panes.visible_panes_count(),
+ 7,
+ "All original plugins retained and new plugins created"
+ );
+}
+
+#[test]
+fn test_override_floating_retain_plugin_panes_partial_match() {
+ // Floating plugin panes that don't match the new layout are retained
+ let initial_kdl = r#"
+ layout {
+ pane
+ floating_panes {
+ pane {
+ x 10
+ y 10
+ width 40
+ height 20
+ plugin location="zellij:tab-bar"
+ }
+ pane {
+ x 20
+ y 20
+ width 50
+ height 25
+ plugin location="zellij:status-bar"
+ }
+ }
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+
+ let terminal_ids = vec![(1, None)];
+
+ let mut initial_plugin_ids = HashMap::new();
+ let tab_bar = RunPluginOrAlias::from_url("zellij:tab-bar", &None, None, None).unwrap();
+ let status_bar = RunPluginOrAlias::from_url("zellij:status-bar", &None, None, None).unwrap();
+ initial_plugin_ids.insert(tab_bar, vec![100]);
+ initial_plugin_ids.insert(status_bar, vec![101]);
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ vec![],
+ initial_plugin_ids,
+ 1,
+ )
+ .unwrap();
+
+ let override_kdl = r#"
+ layout {
+ pane
+ floating_panes {
+ pane {
+ x 15
+ y 15
+ width 45
+ height 22
+ plugin location="zellij:tab-bar"
+ }
+ }
+ }
+ "#;
+
+ let (_, override_floating) = parse_kdl_layout(override_kdl);
+
+ applier
+ .override_floating_panes_layout_for_existing_panes(
+ &override_floating,
+ vec![],
+ &mut HashMap::new(),
+ false,
+ true,
+ )
+ .unwrap();
+
+ // Verify NO plugin panes were unloaded
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert_eq!(
+ unloaded_plugins.len(),
+ 0,
+ "No floating plugin panes should be unloaded when retain_existing_plugin_panes is true"
+ );
+
+ // No terminals should be closed
+ let closed_panes = collect_close_pane_messages(&pty_receiver);
+ assert!(closed_panes.is_empty());
+
+ // Both floating plugins should still exist
+ assert_eq!(
+ floating_panes.visible_panes_count(),
+ 2,
+ "Both floating plugin panes should be retained"
+ );
+
+ // Verify tiled pane exists
+ assert_eq!(tiled_panes.visible_panes_count(), 1);
+}
+
+#[test]
+fn test_override_floating_retain_plugin_panes_no_matches() {
+ // All original floating plugins are retained AND new floating plugins are created when there are no matches
+ let initial_kdl = r#"
+ layout {
+ pane
+ floating_panes {
+ pane {
+ x 10
+ y 10
+ width 40
+ height 20
+ plugin location="zellij:tab-bar"
+ }
+ pane {
+ x 20
+ y 20
+ width 50
+ height 25
+ plugin location="zellij:status-bar"
+ }
+ }
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+
+ let terminal_ids = vec![(1, None)];
+
+ let mut initial_plugin_ids = HashMap::new();
+ let tab_bar = RunPluginOrAlias::from_url("zellij:tab-bar", &None, None, None).unwrap();
+ let status_bar = RunPluginOrAlias::from_url("zellij:status-bar", &None, None, None).unwrap();
+ initial_plugin_ids.insert(tab_bar, vec![100]);
+ initial_plugin_ids.insert(status_bar, vec![101]);
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ vec![],
+ initial_plugin_ids,
+ 1,
+ )
+ .unwrap();
+
+ let override_kdl = r#"
+ layout {
+ pane
+ floating_panes {
+ pane {
+ x 15
+ y 15
+ width 45
+ height 22
+ plugin location="file:///path/to/plugin1.wasm"
+ }
+ pane {
+ x 25
+ y 25
+ width 55
+ height 27
+ plugin location="file:///path/to/plugin2.wasm"
+ }
+ }
+ }
+ "#;
+
+ let (_, override_floating) = parse_kdl_layout(override_kdl);
+
+ let mut override_plugin_ids = HashMap::new();
+ let plugin1 =
+ RunPluginOrAlias::from_url("file:///path/to/plugin1.wasm", &None, None, None).unwrap();
+ let plugin2 =
+ RunPluginOrAlias::from_url("file:///path/to/plugin2.wasm", &None, None, None).unwrap();
+ override_plugin_ids.insert(plugin1, vec![102]);
+ override_plugin_ids.insert(plugin2, vec![103]);
+
+ applier
+ .override_floating_panes_layout_for_existing_panes(
+ &override_floating,
+ vec![],
+ &mut override_plugin_ids,
+ false,
+ true,
+ )
+ .unwrap();
+
+ // Verify NO plugin panes were unloaded
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert_eq!(
+ unloaded_plugins.len(),
+ 0,
+ "No floating plugin panes should be unloaded when retain_existing_plugin_panes is true"
+ );
+
+ // Total: 4 floating plugins (2 original + 2 new)
+ assert_eq!(
+ floating_panes.visible_panes_count(),
+ 4,
+ "All original floating plugins retained and new plugins created"
+ );
+
+ // 1 tiled terminal pane
+ assert_eq!(tiled_panes.visible_panes_count(), 1);
+}
+
+#[test]
+fn test_override_mixed_retain_plugin_panes_both_tiled_and_floating() {
+ // Both tiled and floating plugin panes are retained when the flag is true
+ let initial_kdl = r#"
+ layout {
+ pane command="htop"
+ pane command="vim"
+ pane {
+ plugin location="zellij:tab-bar"
+ }
+ floating_panes {
+ pane {
+ x 10
+ y 10
+ width 40
+ height 20
+ plugin location="zellij:status-bar"
+ }
+ }
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+
+ let terminal_ids = vec![(1, None), (2, None)];
+
+ let mut initial_plugin_ids = HashMap::new();
+ let tab_bar = RunPluginOrAlias::from_url("zellij:tab-bar", &None, None, None).unwrap();
+ let status_bar = RunPluginOrAlias::from_url("zellij:status-bar", &None, None, None).unwrap();
+ initial_plugin_ids.insert(tab_bar, vec![100]);
+ initial_plugin_ids.insert(status_bar, vec![101]);
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ vec![],
+ initial_plugin_ids,
+ 1,
+ )
+ .unwrap();
+
+ let override_kdl = r#"
+ layout {
+ pane command="htop"
+ pane {
+ plugin location="zellij:tab-bar"
+ }
+ pane {
+ plugin location="zellij:compact-bar"
+ }
+ floating_panes {
+ pane {
+ x 20
+ y 20
+ width 50
+ height 25
+ plugin location="file:///path/to/custom.wasm"
+ }
+ }
+ }
+ "#;
+
+ let (override_tiled, override_floating) = parse_kdl_layout(override_kdl);
+
+ let mut override_plugin_ids = HashMap::new();
+ let compact_bar = RunPluginOrAlias::from_url("zellij:compact-bar", &None, None, None).unwrap();
+ let custom =
+ RunPluginOrAlias::from_url("file:///path/to/custom.wasm", &None, None, None).unwrap();
+ override_plugin_ids.insert(compact_bar, vec![102]);
+ override_plugin_ids.insert(custom, vec![103]);
+
+ let retain_existing_terminal_panes = false;
+ let retain_existing_plugin_panes = true;
+ applier
+ .override_layout(
+ override_tiled,
+ override_floating,
+ vec![],
+ vec![],
+ override_plugin_ids,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ 1,
+ )
+ .unwrap();
+
+ // Verify NO plugin panes were unloaded
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert_eq!(
+ unloaded_plugins.len(),
+ 0,
+ "No plugin panes should be unloaded when retain_existing_plugin_panes is true"
+ );
+
+ // vim terminal should be closed (doesn't match layout)
+ let closed_panes = collect_close_pane_messages(&pty_receiver);
+ assert_eq!(closed_panes.len(), 1, "vim terminal should be closed");
+ assert!(
+ closed_panes.contains(&PaneId::Terminal(2)),
+ "Terminal(2) (vim) should be closed"
+ );
+
+ // Verify plugins exist (exact counts may vary based on where retained panes land)
+ assert!(
+ tiled_panes.visible_panes_count() >= 2,
+ "At least htop and matched plugins"
+ );
+ assert!(
+ floating_panes.visible_panes_count() >= 1,
+ "At least one floating plugin"
+ );
+}
+
+#[test]
+fn test_override_retain_plugin_but_close_terminal_panes() {
+ // Verify that retain_existing_plugin_panes = true ONLY affects plugin panes; terminals are still closed normally
+ let initial_kdl = r#"
+ layout {
+ pane command="htop"
+ pane command="vim"
+ pane {
+ plugin location="zellij:tab-bar"
+ }
+ }
+ "#;
+
+ let (initial_tiled, initial_floating) = parse_kdl_layout(initial_kdl);
+
+ let terminal_ids = vec![(1, None), (2, None)];
+
+ let mut initial_plugin_ids = HashMap::new();
+ let tab_bar = RunPluginOrAlias::from_url("zellij:tab-bar", &None, None, None).unwrap();
+ initial_plugin_ids.insert(tab_bar, vec![100]);
+
+ let size = Size {
+ cols: 120,
+ rows: 40,
+ };
+ let (
+ viewport,
+ senders,
+ sixel_image_store,
+ link_handler,
+ terminal_emulator_colors,
+ terminal_emulator_color_codes,
+ character_cell_size,
+ connected_clients,
+ style,
+ display_area,
+ mut tiled_panes,
+ mut floating_panes,
+ draw_pane_frames,
+ mut focus_pane_id,
+ os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ pty_receiver,
+ plugin_receiver,
+ ) = create_layout_applier_fixtures_with_receivers(size);
+
+ let mut applier = LayoutApplier::new(
+ &viewport,
+ &senders,
+ &sixel_image_store,
+ &link_handler,
+ &terminal_emulator_colors,
+ &terminal_emulator_color_codes,
+ &character_cell_size,
+ &connected_clients,
+ &style,
+ &display_area,
+ &mut tiled_panes,
+ &mut floating_panes,
+ draw_pane_frames,
+ &mut focus_pane_id,
+ &os_api,
+ debug,
+ arrow_fonts,
+ styled_underlines,
+ explicitly_disable_kitty_keyboard_protocol,
+ None,
+ );
+
+ applier
+ .apply_layout(
+ initial_tiled,
+ initial_floating,
+ terminal_ids,
+ vec![],
+ initial_plugin_ids,
+ 1,
+ )
+ .unwrap();
+
+ let override_kdl = r#"
+ layout {
+ pane command="htop"
+ }
+ "#;
+
+ let (override_tiled, _) = parse_kdl_layout(override_kdl);
+
+ applier
+ .override_tiled_panes_layout_for_existing_panes(
+ &override_tiled,
+ vec![],
+ &mut HashMap::new(),
+ false,
+ true,
+ 1,
+ )
+ .unwrap();
+
+ // vim terminal should be closed
+ let closed_panes = collect_close_pane_messages(&pty_receiver);
+ assert_eq!(
+ closed_panes.len(),
+ 1,
+ "Terminal pane should be closed even with retain_existing_plugin_panes"
+ );
+ assert!(
+ closed_panes.contains(&PaneId::Terminal(2)),
+ "vim (Terminal(2)) should be closed"
+ );
+
+ // Plugin should NOT be unloaded
+ let unloaded_plugins = collect_unload_plugin_messages(&plugin_receiver);
+ assert_eq!(
+ unloaded_plugins.len(),
+ 0,
+ "No plugin panes should be unloaded"
+ );
+
+ // Final panes: htop + tab-bar = 2
+ assert_eq!(
+ tiled_panes.visible_panes_count(),
+ 2,
+ "htop and tab-bar should remain"
+ );
+
+ assert_snapshot!(take_pane_state_snapshot(
+ &tiled_panes,
+ &floating_panes,
+ &focus_pane_id,
+ &viewport,
+ &display_area,
+ ));
+}
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_complex_nested_layout.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_complex_nested_layout.snap
new file mode 100644
index 00000000..0f75e61d
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_complex_nested_layout.snap
@@ -0,0 +1,48 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=200, rows=60
+DISPLAY: cols=200, rows=60
+FOCUS: None
+
+TILED PANES (5)
+ Terminal(1):
+ geom: x=0, y=0, cols=50, rows=60
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+ Terminal(2):
+ geom: x=50, y=0, cols=100, rows=36
+ logical_position: 1
+ run: None
+ selectable: true
+ title: Pane #2
+ borderless: false
+
+ Terminal(3):
+ geom: x=150, y=0, cols=50, rows=60
+ logical_position: 2
+ run: None
+ selectable: true
+ title: Pane #3
+ borderless: false
+
+ Terminal(4):
+ geom: x=50, y=36, cols=50, rows=24
+ logical_position: 3
+ run: None
+ selectable: true
+ title: Pane #4
+ borderless: false
+
+ Terminal(5):
+ geom: x=100, y=36, cols=50, rows=24
+ logical_position: 4
+ run: None
+ selectable: true
+ title: Pane #5
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_empty_layout.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_empty_layout.snap
new file mode 100644
index 00000000..0104e919
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_empty_layout.snap
@@ -0,0 +1,9 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: snapshot
+---
+VIEWPORT: x=0, y=0, cols=100, rows=50
+DISPLAY: cols=100, rows=50
+FOCUS: None
+
+TILED PANES (0)
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_horizontal_split_with_sizes.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_horizontal_split_with_sizes.snap
new file mode 100644
index 00000000..f3c2acd6
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_horizontal_split_with_sizes.snap
@@ -0,0 +1,24 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=100, rows=50
+DISPLAY: cols=100, rows=50
+FOCUS: None
+
+TILED PANES (2)
+ Terminal(1):
+ geom: x=0, y=0, cols=100, rows=15
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+ Terminal(2):
+ geom: x=0, y=15, cols=100, rows=35
+ logical_position: 1
+ run: None
+ selectable: true
+ title: Pane #2
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_borderless_panes.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_borderless_panes.snap
new file mode 100644
index 00000000..a5b7ff6e
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_borderless_panes.snap
@@ -0,0 +1,32 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=100, rows=50
+DISPLAY: cols=100, rows=50
+FOCUS: None
+
+TILED PANES (3)
+ Terminal(1):
+ geom: x=0, y=0, cols=100, rows=17
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: true
+
+ Terminal(2):
+ geom: x=0, y=17, cols=100, rows=17
+ logical_position: 1
+ run: None
+ selectable: true
+ title: Pane #2
+ borderless: false
+
+ Terminal(3):
+ geom: x=0, y=34, cols=100, rows=16
+ logical_position: 2
+ run: None
+ selectable: true
+ title: Pane #3
+ borderless: true
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_commands.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_commands.snap
new file mode 100644
index 00000000..6542b039
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_commands.snap
@@ -0,0 +1,32 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=100, rows=50
+DISPLAY: cols=100, rows=50
+FOCUS: None
+
+TILED PANES (3)
+ Terminal(1):
+ geom: x=0, y=0, cols=100, rows=17
+ logical_position: 0
+ run: Command(htop)
+ selectable: true
+ title: htop
+ borderless: false
+
+ Terminal(2):
+ geom: x=0, y=17, cols=100, rows=17
+ logical_position: 1
+ run: Command(tail) args=["-f", "/var/log/syslog"]
+ selectable: true
+ title: tail -f /var/log/syslog
+ borderless: false
+
+ Terminal(3):
+ geom: x=0, y=34, cols=100, rows=16
+ logical_position: 2
+ run: None
+ selectable: true
+ title: Pane #3
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_excess_terminal_ids.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_excess_terminal_ids.snap
new file mode 100644
index 00000000..c1decb8d
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_excess_terminal_ids.snap
@@ -0,0 +1,24 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=100, rows=50
+DISPLAY: cols=100, rows=50
+FOCUS: None
+
+TILED PANES (2)
+ Terminal(1):
+ geom: x=0, y=0, cols=100, rows=25
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+ Terminal(2):
+ geom: x=0, y=25, cols=100, rows=25
+ logical_position: 1
+ run: None
+ selectable: true
+ title: Pane #2
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_floating_pane_with_command.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_floating_pane_with_command.snap
new file mode 100644
index 00000000..de2feb2b
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_floating_pane_with_command.snap
@@ -0,0 +1,26 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=100, rows=50
+DISPLAY: cols=100, rows=50
+FOCUS: None
+
+TILED PANES (1)
+ Terminal(1):
+ geom: x=0, y=0, cols=100, rows=50
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+FLOATING PANES (1)
+ Terminal(2):
+ geom: x=10, y=10, cols=50, rows=25
+ logical_position: 0
+ run: Command(htop)
+ pinned: false
+ selectable: true
+ title: htop
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_floating_panes.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_floating_panes.snap
new file mode 100644
index 00000000..d443e93c
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_floating_panes.snap
@@ -0,0 +1,34 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=100, rows=50
+DISPLAY: cols=100, rows=50
+FOCUS: None
+
+TILED PANES (2)
+ Terminal(1):
+ geom: x=0, y=0, cols=100, rows=25
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+ Terminal(2):
+ geom: x=0, y=25, cols=100, rows=25
+ logical_position: 1
+ run: None
+ selectable: true
+ title: Pane #2
+ borderless: false
+
+FLOATING PANES (1)
+ Terminal(3):
+ geom: x=10, y=10, cols=40, rows=20
+ logical_position: 0
+ run: None
+ pinned: false
+ selectable: true
+ title: Pane #3
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_focus.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_focus.snap
new file mode 100644
index 00000000..ae9d8d2d
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_focus.snap
@@ -0,0 +1,32 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=100, rows=50
+DISPLAY: cols=100, rows=50
+FOCUS: Some(Terminal(2))
+
+TILED PANES (3)
+ Terminal(1):
+ geom: x=0, y=0, cols=100, rows=17
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+ Terminal(2):
+ geom: x=0, y=17, cols=100, rows=17
+ logical_position: 1
+ run: None
+ selectable: true
+ title: Pane #2
+ borderless: false
+
+ Terminal(3):
+ geom: x=0, y=34, cols=100, rows=16
+ logical_position: 2
+ run: None
+ selectable: true
+ title: Pane #3
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_missing_terminal_ids.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_missing_terminal_ids.snap
new file mode 100644
index 00000000..63a159a6
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_missing_terminal_ids.snap
@@ -0,0 +1,24 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=100, rows=50
+DISPLAY: cols=100, rows=50
+FOCUS: None
+
+TILED PANES (2)
+ Terminal(1):
+ geom: x=0, y=0, cols=100, rows=17
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+ Terminal(2):
+ geom: x=0, y=17, cols=100, rows=33
+ logical_position: 1
+ run: None
+ selectable: true
+ title: Pane #2
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_mixed_plugin_and_terminal_panes.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_mixed_plugin_and_terminal_panes.snap
new file mode 100644
index 00000000..c96bea2f
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_mixed_plugin_and_terminal_panes.snap
@@ -0,0 +1,40 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=200, rows=50
+DISPLAY: cols=200, rows=50
+FOCUS: None
+
+TILED PANES (4)
+ Terminal(1):
+ geom: x=40, y=0, cols=120, rows=25
+ logical_position: 1
+ run: Command(vim)
+ selectable: true
+ title: vim
+ borderless: false
+
+ Terminal(2):
+ geom: x=40, y=25, cols=120, rows=25
+ logical_position: 3
+ run: Command(cargo) args=["watch", "-x", "test"]
+ selectable: true
+ title: cargo watch -x test
+ borderless: false
+
+ Plugin(102):
+ geom: x=0, y=0, cols=40, rows=50
+ logical_position: 0
+ run: Plugin(file:/path/to/filebrowser.wasm)
+ selectable: true
+ title: file:/path/to/filebrowser.wasm
+ borderless: false
+
+ Plugin(103):
+ geom: x=160, y=0, cols=40, rows=50
+ logical_position: 2
+ run: Plugin(zellij:compact-bar)
+ selectable: true
+ title: zellij:compact-bar
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_mixed_tiled_and_floating_panes.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_mixed_tiled_and_floating_panes.snap
new file mode 100644
index 00000000..5dd5a441
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_mixed_tiled_and_floating_panes.snap
@@ -0,0 +1,51 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=150, rows=50
+DISPLAY: cols=150, rows=50
+FOCUS: Some(Terminal(2))
+
+TILED PANES (3)
+ Terminal(1):
+ geom: x=0, y=0, cols=90, rows=50
+ logical_position: 0
+ run: Command(vim)
+ selectable: true
+ title: vim
+ borderless: false
+
+ Terminal(2):
+ geom: x=90, y=0, cols=60, rows=25
+ logical_position: 1
+ run: None
+ selectable: true
+ title: terminal
+ borderless: false
+
+ Terminal(3):
+ geom: x=90, y=25, cols=60, rows=25
+ logical_position: 2
+ run: Command(tail) args=["-f", "/var/log/syslog"]
+ selectable: true
+ title: tail -f /var/log/syslog
+ borderless: false
+
+FLOATING PANES (2)
+ Terminal(4):
+ geom: x=5, y=5, cols=40, rows=15
+ logical_position: 0
+ run: Command(htop)
+ pinned: false
+ selectable: true
+ title: monitor
+ borderless: false
+
+ Terminal(5):
+ geom: x=75, y=10, cols=45, rows=20
+ logical_position: 1
+ run: None
+ pinned: false
+ selectable: true
+ title: notes
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_multiple_stacks.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_multiple_stacks.snap
new file mode 100644
index 00000000..57cfafbb
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_multiple_stacks.snap
@@ -0,0 +1,53 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=150, rows=40
+DISPLAY: cols=150, rows=40
+FOCUS: None
+
+TILED PANES (5)
+ Terminal(1):
+ geom: x=0, y=0, cols=75, rows=39
+ logical_position: 0
+ stacked: 0
+ run: None
+ selectable: true
+ title: left-1
+ borderless: false
+
+ Terminal(2):
+ geom: x=75, y=0, cols=75, rows=1
+ logical_position: 1
+ stacked: 1
+ run: None
+ selectable: true
+ title: right-1
+ borderless: false
+
+ Terminal(3):
+ geom: x=0, y=39, cols=75, rows=1
+ logical_position: 2
+ stacked: 0
+ run: None
+ selectable: true
+ title: left-2
+ borderless: false
+
+ Terminal(4):
+ geom: x=75, y=1, cols=75, rows=1
+ logical_position: 3
+ stacked: 1
+ run: None
+ selectable: true
+ title: right-2
+ borderless: false
+
+ Terminal(5):
+ geom: x=75, y=2, cols=75, rows=38
+ logical_position: 4
+ stacked: 1
+ run: None
+ selectable: true
+ title: right-3
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_named_panes.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_named_panes.snap
new file mode 100644
index 00000000..9bce2ed3
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_named_panes.snap
@@ -0,0 +1,32 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=100, rows=50
+DISPLAY: cols=100, rows=50
+FOCUS: None
+
+TILED PANES (3)
+ Terminal(1):
+ geom: x=0, y=0, cols=100, rows=17
+ logical_position: 0
+ run: None
+ selectable: true
+ title: editor
+ borderless: false
+
+ Terminal(2):
+ geom: x=0, y=17, cols=100, rows=17
+ logical_position: 1
+ run: None
+ selectable: true
+ title: terminal
+ borderless: false
+
+ Terminal(3):
+ geom: x=0, y=34, cols=100, rows=16
+ logical_position: 2
+ run: None
+ selectable: true
+ title: logs
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_plugin_panes.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_plugin_panes.snap
new file mode 100644
index 00000000..31ab79e8
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_plugin_panes.snap
@@ -0,0 +1,32 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=120, rows=40
+DISPLAY: cols=120, rows=40
+FOCUS: None
+
+TILED PANES (3)
+ Terminal(1):
+ geom: x=0, y=0, cols=120, rows=14
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+ Plugin(100):
+ geom: x=0, y=14, cols=120, rows=13
+ logical_position: 1
+ run: Plugin(zellij:tab-bar)
+ selectable: true
+ title: zellij:tab-bar
+ borderless: false
+
+ Plugin(101):
+ geom: x=0, y=27, cols=120, rows=13
+ logical_position: 2
+ run: Plugin(zellij:status-bar)
+ selectable: true
+ title: zellij:status-bar
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_stacked_panes.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_stacked_panes.snap
new file mode 100644
index 00000000..78cb82a6
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_layout_with_stacked_panes.snap
@@ -0,0 +1,43 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=120, rows=40
+DISPLAY: cols=120, rows=40
+FOCUS: None
+
+TILED PANES (4)
+ Terminal(1):
+ geom: x=0, y=0, cols=84, rows=38
+ logical_position: 0
+ stacked: 0
+ run: None
+ selectable: true
+ title: editor-1
+ borderless: false
+
+ Terminal(2):
+ geom: x=84, y=0, cols=36, rows=40
+ logical_position: 1
+ run: None
+ selectable: true
+ title: Pane #2
+ borderless: false
+
+ Terminal(3):
+ geom: x=0, y=38, cols=84, rows=1
+ logical_position: 2
+ stacked: 0
+ run: None
+ selectable: true
+ title: editor-2
+ borderless: false
+
+ Terminal(4):
+ geom: x=0, y=39, cols=84, rows=1
+ logical_position: 3
+ stacked: 0
+ run: None
+ selectable: true
+ title: editor-3
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_nested_layout.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_nested_layout.snap
new file mode 100644
index 00000000..45d8601e
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_nested_layout.snap
@@ -0,0 +1,32 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=120, rows=40
+DISPLAY: cols=120, rows=40
+FOCUS: None
+
+TILED PANES (3)
+ Terminal(1):
+ geom: x=0, y=0, cols=72, rows=40
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+ Terminal(2):
+ geom: x=72, y=0, cols=48, rows=20
+ logical_position: 1
+ run: None
+ selectable: true
+ title: Pane #2
+ borderless: false
+
+ Terminal(3):
+ geom: x=72, y=20, cols=48, rows=20
+ logical_position: 2
+ run: None
+ selectable: true
+ title: Pane #3
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_simple_two_pane_layout.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_simple_two_pane_layout.snap
new file mode 100644
index 00000000..7704828a
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_simple_two_pane_layout.snap
@@ -0,0 +1,24 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: snapshot
+---
+VIEWPORT: x=0, y=0, cols=100, rows=50
+DISPLAY: cols=100, rows=50
+FOCUS: None
+
+TILED PANES (2)
+ Terminal(1):
+ geom: x=0, y=0, cols=100, rows=25
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+ Terminal(2):
+ geom: x=0, y=25, cols=100, rows=25
+ logical_position: 1
+ run: None
+ selectable: true
+ title: Pane #2
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_three_pane_layout.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_three_pane_layout.snap
new file mode 100644
index 00000000..96068aad
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_three_pane_layout.snap
@@ -0,0 +1,32 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=100, rows=50
+DISPLAY: cols=100, rows=50
+FOCUS: None
+
+TILED PANES (3)
+ Terminal(1):
+ geom: x=0, y=0, cols=100, rows=17
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+ Terminal(2):
+ geom: x=0, y=17, cols=100, rows=17
+ logical_position: 1
+ run: None
+ selectable: true
+ title: Pane #2
+ borderless: false
+
+ Terminal(3):
+ geom: x=0, y=34, cols=100, rows=16
+ logical_position: 2
+ run: None
+ selectable: true
+ title: Pane #3
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_vertical_split_with_sizes.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_vertical_split_with_sizes.snap
new file mode 100644
index 00000000..226bc86a
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__apply_vertical_split_with_sizes.snap
@@ -0,0 +1,24 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=100, rows=50
+DISPLAY: cols=100, rows=50
+FOCUS: None
+
+TILED PANES (2)
+ Terminal(1):
+ geom: x=0, y=0, cols=60, rows=50
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+ Terminal(2):
+ geom: x=60, y=0, cols=40, rows=50
+ logical_position: 1
+ run: None
+ selectable: true
+ title: Pane #2
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_all_closed_no_matches.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_all_closed_no_matches.snap
new file mode 100644
index 00000000..6bc1b62e
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_all_closed_no_matches.snap
@@ -0,0 +1,35 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=120, rows=40
+DISPLAY: cols=120, rows=40
+FOCUS: None
+
+TILED PANES (1)
+ Terminal(1):
+ geom: x=0, y=0, cols=120, rows=40
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+FLOATING PANES (2)
+ Terminal(4):
+ geom: x=15, y=15, cols=45, rows=22
+ logical_position: 0
+ run: Command(top)
+ pinned: false
+ selectable: true
+ title: top
+ borderless: false
+
+ Terminal(5):
+ geom: x=25, y=13, cols=55, rows=27
+ logical_position: 1
+ run: Command(emacs)
+ pinned: false
+ selectable: true
+ title: emacs
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_exact_match_preservation.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_exact_match_preservation.snap
new file mode 100644
index 00000000..c4c6d2f0
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_exact_match_preservation.snap
@@ -0,0 +1,26 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=120, rows=40
+DISPLAY: cols=120, rows=40
+FOCUS: None
+
+TILED PANES (1)
+ Terminal(1):
+ geom: x=0, y=0, cols=120, rows=40
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+FLOATING PANES (1)
+ Terminal(2):
+ geom: x=50, y=18, cols=45, rows=22
+ logical_position: 0
+ run: Command(htop)
+ pinned: false
+ selectable: true
+ title: htop
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_focus_handling.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_focus_handling.snap
new file mode 100644
index 00000000..e14c7ba2
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_focus_handling.snap
@@ -0,0 +1,26 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=120, rows=40
+DISPLAY: cols=120, rows=40
+FOCUS: None
+
+TILED PANES (1)
+ Terminal(1):
+ geom: x=0, y=0, cols=120, rows=40
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+FLOATING PANES (1)
+ Terminal(2):
+ geom: x=30, y=10, cols=60, rows=30
+ logical_position: 0
+ run: None
+ pinned: false
+ selectable: true
+ title: Pane #2
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_new_panes_created.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_new_panes_created.snap
new file mode 100644
index 00000000..948a272d
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_new_panes_created.snap
@@ -0,0 +1,44 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=120, rows=40
+DISPLAY: cols=120, rows=40
+FOCUS: None
+
+TILED PANES (1)
+ Terminal(1):
+ geom: x=0, y=0, cols=120, rows=40
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+FLOATING PANES (3)
+ Terminal(2):
+ geom: x=10, y=10, cols=40, rows=20
+ logical_position: 0
+ run: Command(htop)
+ pinned: false
+ selectable: true
+ title: htop
+ borderless: false
+
+ Terminal(3):
+ geom: x=20, y=15, cols=50, rows=25
+ logical_position: 1
+ run: Command(vim)
+ pinned: false
+ selectable: true
+ title: vim
+ borderless: false
+
+ Terminal(4):
+ geom: x=30, y=18, cols=45, rows=22
+ logical_position: 2
+ run: None
+ pinned: false
+ selectable: true
+ title: Pane #4
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_position_and_size_update.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_position_and_size_update.snap
new file mode 100644
index 00000000..a8a2abd9
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_position_and_size_update.snap
@@ -0,0 +1,26 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=120, rows=50
+DISPLAY: cols=120, rows=50
+FOCUS: None
+
+TILED PANES (1)
+ Terminal(1):
+ geom: x=0, y=0, cols=120, rows=50
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+FLOATING PANES (1)
+ Terminal(2):
+ geom: x=50, y=20, cols=60, rows=30
+ logical_position: 0
+ run: Command(htop)
+ pinned: false
+ selectable: true
+ title: htop
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_retain_terminal_panes_no_matches.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_retain_terminal_panes_no_matches.snap
new file mode 100644
index 00000000..dde1b2f3
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_retain_terminal_panes_no_matches.snap
@@ -0,0 +1,53 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=120, rows=40
+DISPLAY: cols=120, rows=40
+FOCUS: None
+
+TILED PANES (1)
+ Terminal(1):
+ geom: x=0, y=0, cols=120, rows=40
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+FLOATING PANES (4)
+ Terminal(2):
+ geom: x=30, y=10, cols=60, rows=20
+ logical_position: 2
+ run: Command(htop)
+ pinned: false
+ selectable: true
+ title: htop
+ borderless: false
+
+ Terminal(3):
+ geom: x=32, y=12, cols=60, rows=20
+ logical_position: 2
+ run: Command(vim)
+ pinned: false
+ selectable: true
+ title: vim
+ borderless: false
+
+ Terminal(4):
+ geom: x=30, y=20, cols=40, rows=20
+ logical_position: 0
+ run: Command(top)
+ pinned: false
+ selectable: true
+ title: top
+ borderless: false
+
+ Terminal(5):
+ geom: x=40, y=15, cols=50, rows=25
+ logical_position: 1
+ run: Command(emacs)
+ pinned: false
+ selectable: true
+ title: emacs
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_retain_terminal_panes_partial_match.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_retain_terminal_panes_partial_match.snap
new file mode 100644
index 00000000..b85b40a1
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_retain_terminal_panes_partial_match.snap
@@ -0,0 +1,35 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=120, rows=40
+DISPLAY: cols=120, rows=40
+FOCUS: None
+
+TILED PANES (1)
+ Terminal(1):
+ geom: x=0, y=0, cols=120, rows=40
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+FLOATING PANES (2)
+ Terminal(2):
+ geom: x=50, y=18, cols=45, rows=22
+ logical_position: 0
+ run: Command(htop)
+ pinned: false
+ selectable: true
+ title: htop
+ borderless: false
+
+ Terminal(3):
+ geom: x=30, y=10, cols=60, rows=20
+ logical_position: 1
+ run: Command(vim)
+ pinned: false
+ selectable: true
+ title: vim
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_return_value_has_panes.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_return_value_has_panes.snap
new file mode 100644
index 00000000..c8b0490a
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_return_value_has_panes.snap
@@ -0,0 +1,26 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=100, rows=50
+DISPLAY: cols=100, rows=50
+FOCUS: None
+
+TILED PANES (1)
+ Terminal(1):
+ geom: x=0, y=0, cols=100, rows=50
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+FLOATING PANES (1)
+ Terminal(2):
+ geom: x=20, y=10, cols=50, rows=30
+ logical_position: 0
+ run: None
+ pinned: false
+ selectable: true
+ title: Pane #2
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_return_value_no_panes.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_return_value_no_panes.snap
new file mode 100644
index 00000000..16b74511
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_floating_return_value_no_panes.snap
@@ -0,0 +1,16 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=100, rows=50
+DISPLAY: cols=100, rows=50
+FOCUS: None
+
+TILED PANES (1)
+ Terminal(1):
+ geom: x=0, y=0, cols=100, rows=50
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_full_tiled_and_floating_together.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_full_tiled_and_floating_together.snap
new file mode 100644
index 00000000..5ed2a41c
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_full_tiled_and_floating_together.snap
@@ -0,0 +1,34 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=150, rows=50
+DISPLAY: cols=150, rows=50
+FOCUS: None
+
+TILED PANES (2)
+ Terminal(1):
+ geom: x=0, y=0, cols=150, rows=25
+ logical_position: 0
+ run: Command(htop)
+ selectable: true
+ title: htop
+ borderless: false
+
+ Terminal(6):
+ geom: x=0, y=25, cols=150, rows=25
+ logical_position: 1
+ run: Command(npm) args=["start"]
+ selectable: true
+ title: npm start
+ borderless: false
+
+FLOATING PANES (1)
+ Terminal(4):
+ geom: x=30, y=20, cols=60, rows=30
+ logical_position: 0
+ run: Command(cargo) args=["watch"]
+ pinned: false
+ selectable: true
+ title: cargo watch
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_layout_basic_with_both_tiled_and_floating.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_layout_basic_with_both_tiled_and_floating.snap
new file mode 100644
index 00000000..9c782f2b
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_layout_basic_with_both_tiled_and_floating.snap
@@ -0,0 +1,34 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=120, rows=40
+DISPLAY: cols=120, rows=40
+FOCUS: None
+
+TILED PANES (2)
+ Terminal(1):
+ geom: x=0, y=20, cols=120, rows=20
+ logical_position: 1
+ run: Command(htop)
+ selectable: true
+ title: htop
+ borderless: false
+
+ Terminal(4):
+ geom: x=0, y=0, cols=120, rows=20
+ logical_position: 0
+ run: Command(top)
+ selectable: true
+ title: top
+ borderless: false
+
+FLOATING PANES (1)
+ Terminal(5):
+ geom: x=20, y=15, cols=50, rows=25
+ logical_position: 0
+ run: Command(watch) args=["df", "-h"]
+ pinned: false
+ selectable: true
+ title: watch df -h
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_layout_hide_floating_panes_true.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_layout_hide_floating_panes_true.snap
new file mode 100644
index 00000000..776d59e8
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_layout_hide_floating_panes_true.snap
@@ -0,0 +1,34 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=100, rows=50
+DISPLAY: cols=100, rows=50
+FOCUS: None
+
+TILED PANES (2)
+ Terminal(1):
+ geom: x=0, y=0, cols=100, rows=25
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+ Terminal(3):
+ geom: x=0, y=25, cols=100, rows=25
+ logical_position: 1
+ run: None
+ selectable: true
+ title: Pane #3
+ borderless: false
+
+FLOATING PANES (1)
+ Terminal(2):
+ geom: x=15, y=15, cols=45, rows=22
+ logical_position: 0
+ run: None
+ pinned: false
+ selectable: true
+ title: Pane #2
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_layout_show_floating_panes.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_layout_show_floating_panes.snap
new file mode 100644
index 00000000..0bd05777
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_layout_show_floating_panes.snap
@@ -0,0 +1,26 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=100, rows=50
+DISPLAY: cols=100, rows=50
+FOCUS: None
+
+TILED PANES (1)
+ Terminal(1):
+ geom: x=0, y=0, cols=100, rows=50
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+FLOATING PANES (1)
+ Terminal(3):
+ geom: x=20, y=10, cols=50, rows=30
+ logical_position: 0
+ run: None
+ pinned: false
+ selectable: true
+ title: Pane #2
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_mixed_retain_terminal_panes_both_tiled_and_floating.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_mixed_retain_terminal_panes_both_tiled_and_floating.snap
new file mode 100644
index 00000000..a4937d41
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_mixed_retain_terminal_panes_both_tiled_and_floating.snap
@@ -0,0 +1,59 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=120, rows=40
+DISPLAY: cols=120, rows=40
+FOCUS: None
+
+TILED PANES (4)
+ Terminal(1):
+ geom: x=0, y=20, cols=60, rows=20
+ logical_position: 1
+ run: Command(htop)
+ selectable: true
+ title: htop
+ borderless: false
+
+ Terminal(2):
+ geom: x=60, y=20, cols=60, rows=20
+ logical_position: 1
+ run: Command(vim)
+ selectable: true
+ title: vim
+ borderless: false
+
+ Terminal(3):
+ geom: x=60, y=0, cols=60, rows=20
+ logical_position: 2
+ run: Command(tail) args=["-f", "/var/log/syslog"]
+ selectable: true
+ title: tail -f /var/log/syslog
+ borderless: false
+
+ Terminal(5):
+ geom: x=0, y=0, cols=60, rows=20
+ logical_position: 0
+ run: Command(top)
+ selectable: true
+ title: top
+ borderless: false
+
+FLOATING PANES (2)
+ Terminal(4):
+ geom: x=30, y=10, cols=60, rows=20
+ logical_position: 1
+ run: Command(watch) args=["df", "-h"]
+ pinned: false
+ selectable: true
+ title: watch df -h
+ borderless: false
+
+ Terminal(6):
+ geom: x=20, y=15, cols=50, rows=25
+ logical_position: 0
+ run: Command(watch) args=["free", "-h"]
+ pinned: false
+ selectable: true
+ title: watch free -h
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_retain_plugin_but_close_terminal_panes.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_retain_plugin_but_close_terminal_panes.snap
new file mode 100644
index 00000000..ab483e3c
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_retain_plugin_but_close_terminal_panes.snap
@@ -0,0 +1,24 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=120, rows=40
+DISPLAY: cols=120, rows=40
+FOCUS: None
+
+TILED PANES (2)
+ Terminal(1):
+ geom: x=0, y=0, cols=120, rows=20
+ logical_position: 0
+ run: Command(htop)
+ selectable: true
+ title: htop
+ borderless: false
+
+ Plugin(100):
+ geom: x=0, y=20, cols=120, rows=20
+ logical_position: 0
+ run: Plugin(zellij:tab-bar)
+ selectable: true
+ title: zellij:tab-bar
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_retain_terminal_but_close_plugin_panes.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_retain_terminal_but_close_plugin_panes.snap
new file mode 100644
index 00000000..819fef39
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_retain_terminal_but_close_plugin_panes.snap
@@ -0,0 +1,24 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=120, rows=40
+DISPLAY: cols=120, rows=40
+FOCUS: None
+
+TILED PANES (2)
+ Terminal(1):
+ geom: x=0, y=0, cols=120, rows=20
+ logical_position: 0
+ run: Command(htop)
+ selectable: true
+ title: htop
+ borderless: false
+
+ Terminal(2):
+ geom: x=0, y=20, cols=120, rows=20
+ logical_position: 0
+ run: Command(vim)
+ selectable: true
+ title: vim
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_all_panes_closed_no_matches.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_all_panes_closed_no_matches.snap
new file mode 100644
index 00000000..c328232e
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_all_panes_closed_no_matches.snap
@@ -0,0 +1,32 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=120, rows=40
+DISPLAY: cols=120, rows=40
+FOCUS: None
+
+TILED PANES (3)
+ Terminal(4):
+ geom: x=0, y=0, cols=120, rows=13
+ logical_position: 0
+ run: Command(cargo) args=["watch"]
+ selectable: true
+ title: cargo watch
+ borderless: false
+
+ Terminal(5):
+ geom: x=0, y=13, cols=120, rows=13
+ logical_position: 1
+ run: Command(npm) args=["start"]
+ selectable: true
+ title: npm start
+ borderless: false
+
+ Terminal(6):
+ geom: x=0, y=26, cols=120, rows=14
+ logical_position: 2
+ run: Command(python) args=["-m", "http.server"]
+ selectable: true
+ title: python -m http.server
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_empty_layout_closes_all.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_empty_layout_closes_all.snap
new file mode 100644
index 00000000..ea6257d6
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_empty_layout_closes_all.snap
@@ -0,0 +1,9 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=120, rows=40
+DISPLAY: cols=120, rows=40
+FOCUS: None
+
+TILED PANES (0)
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_exact_match_preservation_commands.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_exact_match_preservation_commands.snap
new file mode 100644
index 00000000..3f063bd1
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_exact_match_preservation_commands.snap
@@ -0,0 +1,24 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=120, rows=40
+DISPLAY: cols=120, rows=40
+FOCUS: None
+
+TILED PANES (2)
+ Terminal(1):
+ geom: x=0, y=20, cols=120, rows=20
+ logical_position: 1
+ run: Command(htop)
+ selectable: true
+ title: htop
+ borderless: false
+
+ Terminal(2):
+ geom: x=0, y=0, cols=120, rows=20
+ logical_position: 0
+ run: Command(vim)
+ selectable: true
+ title: vim
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_exact_match_preservation_plugins.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_exact_match_preservation_plugins.snap
new file mode 100644
index 00000000..688718e2
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_exact_match_preservation_plugins.snap
@@ -0,0 +1,16 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=120, rows=40
+DISPLAY: cols=120, rows=40
+FOCUS: None
+
+TILED PANES (1)
+ Plugin(100):
+ geom: x=0, y=0, cols=120, rows=40
+ logical_position: 0
+ run: Plugin(zellij:tab-bar)
+ selectable: true
+ title: zellij:tab-bar
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_focus_on_new_pane.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_focus_on_new_pane.snap
new file mode 100644
index 00000000..5061a53f
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_focus_on_new_pane.snap
@@ -0,0 +1,32 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=100, rows=50
+DISPLAY: cols=100, rows=50
+FOCUS: Some(Terminal(1))
+
+TILED PANES (3)
+ Terminal(1):
+ geom: x=0, y=0, cols=100, rows=16
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+ Terminal(2):
+ geom: x=0, y=16, cols=100, rows=16
+ logical_position: 1
+ run: None
+ selectable: true
+ title: Pane #2
+ borderless: false
+
+ Terminal(3):
+ geom: x=0, y=32, cols=100, rows=18
+ logical_position: 2
+ run: None
+ selectable: true
+ title: Pane #3
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_focus_when_focused_pane_closed.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_focus_when_focused_pane_closed.snap
new file mode 100644
index 00000000..212107ae
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_focus_when_focused_pane_closed.snap
@@ -0,0 +1,24 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=120, rows=40
+DISPLAY: cols=120, rows=40
+FOCUS: Some(Terminal(2))
+
+TILED PANES (2)
+ Terminal(1):
+ geom: x=0, y=0, cols=120, rows=20
+ logical_position: 0
+ run: Command(htop)
+ selectable: true
+ title: htop
+ borderless: false
+
+ Terminal(4):
+ geom: x=0, y=20, cols=120, rows=20
+ logical_position: 1
+ run: Command(cargo) args=["check"]
+ selectable: true
+ title: cargo check
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_mixed_some_matches_some_new.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_mixed_some_matches_some_new.snap
new file mode 100644
index 00000000..b6d0bf45
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_mixed_some_matches_some_new.snap
@@ -0,0 +1,32 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=120, rows=40
+DISPLAY: cols=120, rows=40
+FOCUS: None
+
+TILED PANES (3)
+ Terminal(1):
+ geom: x=0, y=0, cols=120, rows=13
+ logical_position: 0
+ run: Command(htop)
+ selectable: true
+ title: htop
+ borderless: false
+
+ Terminal(2):
+ geom: x=0, y=26, cols=120, rows=14
+ logical_position: 2
+ run: None
+ selectable: true
+ title: Pane #2
+ borderless: false
+
+ Terminal(3):
+ geom: x=0, y=13, cols=120, rows=13
+ logical_position: 1
+ run: Command(vim)
+ selectable: true
+ title: vim
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_new_panes_for_unmatched_positions.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_new_panes_for_unmatched_positions.snap
new file mode 100644
index 00000000..bd516b92
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_new_panes_for_unmatched_positions.snap
@@ -0,0 +1,40 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=150, rows=50
+DISPLAY: cols=150, rows=50
+FOCUS: None
+
+TILED PANES (4)
+ Terminal(1):
+ geom: x=0, y=0, cols=150, rows=12
+ logical_position: 0
+ run: Command(htop)
+ selectable: true
+ title: htop
+ borderless: false
+
+ Terminal(2):
+ geom: x=0, y=12, cols=150, rows=12
+ logical_position: 1
+ run: Command(vim)
+ selectable: true
+ title: vim
+ borderless: false
+
+ Terminal(3):
+ geom: x=0, y=24, cols=150, rows=12
+ logical_position: 2
+ run: None
+ selectable: true
+ title: Pane #3
+ borderless: false
+
+ Terminal(4):
+ geom: x=0, y=36, cols=150, rows=14
+ logical_position: 3
+ run: None
+ selectable: true
+ title: Pane #4
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_retain_terminal_panes_no_matches.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_retain_terminal_panes_no_matches.snap
new file mode 100644
index 00000000..2a7ac2fc
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_retain_terminal_panes_no_matches.snap
@@ -0,0 +1,56 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=120, rows=40
+DISPLAY: cols=120, rows=40
+FOCUS: None
+
+TILED PANES (6)
+ Terminal(1):
+ geom: x=60, y=26, cols=60, rows=14
+ logical_position: 2
+ run: Command(htop)
+ selectable: true
+ title: htop
+ borderless: false
+
+ Terminal(2):
+ geom: x=60, y=0, cols=60, rows=13
+ logical_position: 3
+ run: Command(vim)
+ selectable: true
+ title: vim
+ borderless: false
+
+ Terminal(3):
+ geom: x=60, y=13, cols=60, rows=13
+ logical_position: 4
+ run: Command(tail) args=["-f", "/var/log/syslog"]
+ selectable: true
+ title: tail -f /var/log/syslog
+ borderless: false
+
+ Terminal(4):
+ geom: x=0, y=0, cols=60, rows=13
+ logical_position: 0
+ run: Command(cargo)
+ selectable: true
+ title: cargo
+ borderless: false
+
+ Terminal(5):
+ geom: x=0, y=13, cols=60, rows=13
+ logical_position: 1
+ run: Command(npm)
+ selectable: true
+ title: npm
+ borderless: false
+
+ Terminal(6):
+ geom: x=0, y=26, cols=60, rows=14
+ logical_position: 2
+ run: Command(python)
+ selectable: true
+ title: python
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_retain_terminal_panes_partial_match.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_retain_terminal_panes_partial_match.snap
new file mode 100644
index 00000000..12e7bdb9
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_tiled_retain_terminal_panes_partial_match.snap
@@ -0,0 +1,32 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=120, rows=40
+DISPLAY: cols=120, rows=40
+FOCUS: None
+
+TILED PANES (3)
+ Terminal(1):
+ geom: x=0, y=20, cols=60, rows=20
+ logical_position: 1
+ run: Command(htop)
+ selectable: true
+ title: htop
+ borderless: false
+
+ Terminal(2):
+ geom: x=0, y=0, cols=120, rows=20
+ logical_position: 0
+ run: Command(vim)
+ selectable: true
+ title: vim
+ borderless: false
+
+ Terminal(3):
+ geom: x=60, y=20, cols=60, rows=20
+ logical_position: 1
+ run: Command(tail) args=["-f", "/var/log/syslog"]
+ selectable: true
+ title: tail -f /var/log/syslog
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_viewport_adjustment_with_borderless.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_viewport_adjustment_with_borderless.snap
new file mode 100644
index 00000000..2bef48e7
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__override_viewport_adjustment_with_borderless.snap
@@ -0,0 +1,24 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=120, rows=40
+DISPLAY: cols=120, rows=40
+FOCUS: None
+
+TILED PANES (2)
+ Terminal(1):
+ geom: x=0, y=0, cols=120, rows=20
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+ Terminal(2):
+ geom: x=0, y=20, cols=120, rows=20
+ logical_position: 1
+ run: None
+ selectable: true
+ title: Pane #2
+ borderless: true
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__reapply_floating_pane_layout.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__reapply_floating_pane_layout.snap
new file mode 100644
index 00000000..c5f8b6fc
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__reapply_floating_pane_layout.snap
@@ -0,0 +1,26 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=100, rows=50
+DISPLAY: cols=100, rows=50
+FOCUS: None
+
+TILED PANES (1)
+ Terminal(1):
+ geom: x=0, y=0, cols=100, rows=50
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+FLOATING PANES (1)
+ Terminal(2):
+ geom: x=20, y=20, cols=50, rows=25
+ logical_position: 0
+ run: None
+ pinned: false
+ selectable: true
+ title: Pane #2
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__reapply_layout_exact_match.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__reapply_layout_exact_match.snap
new file mode 100644
index 00000000..06a7a9ea
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__reapply_layout_exact_match.snap
@@ -0,0 +1,32 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: snapshot
+---
+VIEWPORT: x=0, y=0, cols=120, rows=40
+DISPLAY: cols=120, rows=40
+FOCUS: None
+
+TILED PANES (3)
+ Terminal(1):
+ geom: x=0, y=13, cols=120, rows=13
+ logical_position: 1
+ run: Command(htop)
+ selectable: true
+ title: htop
+ borderless: false
+
+ Terminal(2):
+ geom: x=0, y=26, cols=120, rows=14
+ logical_position: 2
+ run: Command(vim)
+ selectable: true
+ title: vim
+ borderless: false
+
+ Terminal(3):
+ geom: x=0, y=0, cols=120, rows=13
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #3
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__reapply_layout_logical_position_match.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__reapply_layout_logical_position_match.snap
new file mode 100644
index 00000000..60cf272b
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__reapply_layout_logical_position_match.snap
@@ -0,0 +1,32 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=120, rows=40
+DISPLAY: cols=120, rows=40
+FOCUS: None
+
+TILED PANES (3)
+ Terminal(1):
+ geom: x=0, y=0, cols=60, rows=20
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+ Terminal(2):
+ geom: x=0, y=20, cols=120, rows=20
+ logical_position: 1
+ run: None
+ selectable: true
+ title: Pane #2
+ borderless: false
+
+ Terminal(3):
+ geom: x=60, y=0, cols=60, rows=20
+ logical_position: 2
+ run: None
+ selectable: true
+ title: Pane #3
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__reapply_layout_with_fewer_positions.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__reapply_layout_with_fewer_positions.snap
new file mode 100644
index 00000000..ed8952b6
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__reapply_layout_with_fewer_positions.snap
@@ -0,0 +1,38 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=100, rows=50
+DISPLAY: cols=100, rows=50
+FOCUS: None
+
+TILED PANES (4)
+ Terminal(1):
+ geom: x=0, y=0, cols=50, rows=25
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+ Terminal(2):
+ geom: x=0, y=25, cols=50, rows=25
+ logical_position: 1
+ run: None
+ selectable: true
+ title: Pane #2
+ borderless: false
+
+ Terminal(3):
+ geom: x=50, y=25, cols=50, rows=25
+ run: None
+ selectable: true
+ title: Pane #3
+ borderless: false
+
+ Terminal(4):
+ geom: x=50, y=0, cols=50, rows=25
+ run: None
+ selectable: true
+ title: Pane #4
+ borderless: false
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__reapply_layout_with_more_positions.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__reapply_layout_with_more_positions.snap
new file mode 100644
index 00000000..c1decb8d
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__layout_applier_tests__reapply_layout_with_more_positions.snap
@@ -0,0 +1,24 @@
+---
+source: zellij-server/src/tab/./unit/layout_applier_tests.rs
+expression: "take_pane_state_snapshot(&tiled_panes, &floating_panes, &focus_pane_id,\n&viewport, &display_area,)"
+---
+VIEWPORT: x=0, y=0, cols=100, rows=50
+DISPLAY: cols=100, rows=50
+FOCUS: None
+
+TILED PANES (2)
+ Terminal(1):
+ geom: x=0, y=0, cols=100, rows=25
+ logical_position: 0
+ run: None
+ selectable: true
+ title: Pane #1
+ borderless: false
+
+ Terminal(2):
+ geom: x=0, y=25, cols=100, rows=25
+ logical_position: 1
+ run: None
+ selectable: true
+ title: Pane #2
+ borderless: false
diff --git a/zellij-server/src/thread_bus.rs b/zellij-server/src/thread_bus.rs
index 3247ecf4..ce997539 100644
--- a/zellij-server/src/thread_bus.rs
+++ b/zellij-server/src/thread_bus.rs
@@ -143,6 +143,11 @@ impl ThreadSenders {
// this is mostly used for the tests, see struct
self.to_pty_writer.replace(new_pty_writer);
}
+ #[allow(unused)]
+ pub fn replace_to_pty(&mut self, new_pty: SenderWithContext<PtyInstruction>) {
+ // this is mostly used for the tests, see struct
+ self.to_pty.replace(new_pty);
+ }
#[allow(unused)]
pub fn replace_to_plugin(&mut self, new_to_plugin: SenderWithContext<PluginInstruction>) {
diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_name_and_layout.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_name_and_layout.snap
index d61ae4d7..ff0a8382 100644
--- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_name_and_layout.snap
+++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_name_and_layout.snap
@@ -17,7 +17,7 @@ NewTab(
split_size: None,
run: Some(
Cwd(
- ".",
+ "./.",
),
),
borderless: false,
@@ -37,7 +37,7 @@ NewTab(
split_size: None,
run: Some(
Cwd(
- ".",
+ "./.",
),
),
borderless: false,
@@ -57,7 +57,7 @@ NewTab(
split_size: None,
run: Some(
Cwd(
- ".",
+ "./.",
),
),
borderless: false,
@@ -72,7 +72,11 @@ NewTab(
},
],
split_size: None,
- run: None,
+ run: Some(
+ Cwd(
+ ".",
+ ),
+ ),
borderless: false,
focus: None,
external_children_index: None,
diff --git a/zellij-utils/assets/plugins/about.wasm b/zellij-utils/assets/plugins/about.wasm
index ae99b4a1..812659bc 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 0d6ab290..75f94c4b 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 b448d343..ffa97232 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 407f41f1..cd74ed1d 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/multiple-select.wasm b/zellij-utils/assets/plugins/multiple-select.wasm
index 77f0310b..4412b8df 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 5c9fb461..cb8e2239 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 0a982532..d6286cb0 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 0a8460d0..ff97eb14 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 360e55a2..ed580f3f 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 ac1b076a..98e8e647 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 ec6de885..c33e5e2f 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 4a31b561..609fa718 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 46435021..ccf96860 100644
--- a/zellij-utils/assets/prost/api.action.rs
+++ b/zellij-utils/assets/prost/api.action.rs
@@ -257,10 +257,28 @@ pub struct NewTabPayload {
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct OverrideLayoutPayload {
+ #[prost(message, optional, tag="1")]
+ pub tiled_layout: ::core::option::Option<TiledPaneLayout>,
+ #[prost(message, repeated, tag="2")]
+ pub floating_layouts: ::prost::alloc::vec::Vec<FloatingPaneLayout>,
+ #[prost(message, repeated, tag="3")]
+ pub swap_tiled_layouts: ::prost::alloc::vec::Vec<SwapTiledLayout>,
+ #[prost(message, repeated, tag="4")]
+ 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")]
+ pub retain_existing_terminal_panes: bool,
+ #[prost(bool, tag="11")]
+ pub retain_existing_plugin_panes: bool,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Action {
#[prost(enumeration="ActionName", tag="1")]
pub name: i32,
- #[prost(oneof="action::OptionalPayload", 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, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52")]
+ #[prost(oneof="action::OptionalPayload", 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, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53")]
pub optional_payload: ::core::option::Option<action::OptionalPayload>,
}
/// Nested message and enum types in `Action`.
@@ -364,6 +382,8 @@ pub mod action {
NewTabPayload(super::NewTabPayload),
#[prost(message, tag="52")]
NewInPlacePanePayload(super::NewInPlacePanePayload),
+ #[prost(message, tag="53")]
+ OverrideLayoutPayload(super::OverrideLayoutPayload),
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
@@ -994,6 +1014,7 @@ pub enum ActionName {
SwitchSession = 90,
NewBlockingPane = 91,
NewInPlacePane = 92,
+ OverrideLayout = 93,
}
impl ActionName {
/// String value of the enum field names used in the ProtoBuf definition.
@@ -1092,6 +1113,7 @@ impl ActionName {
ActionName::SwitchSession => "SwitchSession",
ActionName::NewBlockingPane => "NewBlockingPane",
ActionName::NewInPlacePane => "NewInPlacePane",
+ ActionName::OverrideLayout => "OverrideLayout",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
@@ -1187,6 +1209,7 @@ impl ActionName {
"SwitchSession" => Some(Self::SwitchSession),
"NewBlockingPane" => Some(Self::NewBlockingPane),
"NewInPlacePane" => Some(Self::NewInPlacePane),
+ "OverrideLayout" => Some(Self::OverrideLayout),
_ => None,
}
}
diff --git a/zellij-utils/assets/prost_ipc/client_server_contract.rs b/zellij-utils/assets/prost_ipc/client_server_contract.rs
index 32031dba..b0a9be5b 100644
--- a/zellij-utils/assets/prost_ipc/client_server_contract.rs
+++ b/zellij-utils/assets/prost_ipc/client_server_contract.rs
@@ -119,7 +119,7 @@ pub struct RgbColor {
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Action {
- #[prost(oneof="action::ActionType", tags="1, 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, 51, 52, 53, 54, 55, 56, 57, 58, 59, 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")]
+ #[prost(oneof="action::ActionType", tags="1, 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, 51, 52, 53, 54, 55, 56, 57, 58, 59, 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")]
pub action_type: ::core::option::Option<action::ActionType>,
}
/// Nested message and enum types in `Action`.
@@ -313,6 +313,8 @@ pub mod action {
SwitchSession(super::SwitchSessionAction),
#[prost(message, tag="93")]
NewBlockingPane(super::NewBlockingPaneAction),
+ #[prost(message, tag="94")]
+ OverrideLayout(super::OverrideLayoutAction),
}
}
// Action message definitions (all 92 variants)
@@ -464,6 +466,24 @@ pub struct NextSwapLayoutAction {
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct OverrideLayoutAction {
+ #[prost(message, optional, tag="1")]
+ pub tiled_layout: ::core::option::Option<TiledPaneLayout>,
+ #[prost(message, repeated, tag="2")]
+ pub floating_layouts: ::prost::alloc::vec::Vec<FloatingPaneLayout>,
+ #[prost(message, repeated, tag="3")]
+ pub swap_tiled_layouts: ::prost::alloc::vec::Vec<SwapTiledLayout>,
+ #[prost(message, repeated, tag="4")]
+ 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")]
+ pub retain_existing_terminal_panes: bool,
+ #[prost(bool, tag="7")]
+ pub retain_existing_plugin_panes: bool,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
pub struct QueryTabNamesAction {
}
#[allow(clippy::derive_partial_eq_without_eq)]
diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs
index 7e4e7188..1d196df9 100644
--- a/zellij-utils/src/cli.rs
+++ b/zellij-utils/src/cli.rs
@@ -933,6 +933,24 @@ pub enum CliAction {
},
PreviousSwapLayout,
NextSwapLayout,
+ /// Override the layout of the active tab
+ OverrideLayout {
+ /// Path to the layout file
+ #[clap(value_parser)]
+ layout: PathBuf,
+
+ /// Default folder to look for layouts
+ #[clap(long, value_parser)]
+ layout_dir: Option<PathBuf>,
+
+ /// Retain existing terminal panes that do not fit in the layout (default: false)
+ #[clap(long, value_parser, takes_value(false), default_value("false"))]
+ retain_existing_terminal_panes: bool,
+
+ /// 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,
+ },
/// Query all tab names
QueryTabNames,
StartOrReloadPlugin {
diff --git a/zellij-utils/src/client_server_contract/common_types.proto b/zellij-utils/src/client_server_contract/common_types.proto
index e4561989..bbaeadad 100644
--- a/zellij-utils/src/client_server_contract/common_types.proto
+++ b/zellij-utils/src/client_server_contract/common_types.proto
@@ -229,6 +229,7 @@ message Action {
ToggleGroupMarkingAction toggle_group_marking = 91;
SwitchSessionAction switch_session = 92;
NewBlockingPaneAction new_blocking_pane = 93;
+ OverrideLayoutAction override_layout = 94;
}
}
@@ -271,6 +272,15 @@ message DenyAction {}
message ToggleMouseModeAction {}
message PreviousSwapLayoutAction {}
message NextSwapLayoutAction {}
+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;
+}
message QueryTabNamesAction {}
message BreakPaneAction {}
message BreakPaneRightAction {}
diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs
index 74c40534..78330d76 100644
--- a/zellij-utils/src/errors.rs
+++ b/zellij-utils/src/errors.rs
@@ -324,6 +324,8 @@ pub enum ScreenContext {
ClearPaneFrameColorOverride,
PreviousSwapLayout,
NextSwapLayout,
+ OverrideLayout,
+ OverrideLayoutComplete,
QueryTabNames,
NewTiledPluginPane,
StartOrReloadPluginPane,
@@ -405,6 +407,7 @@ pub enum PtyContext {
UpdateActivePane,
GoToTab,
NewTab,
+ OverrideLayout,
ClosePane,
CloseTab,
ReRunCommandInPane,
@@ -440,6 +443,7 @@ pub enum PluginContext {
AddClient,
RemoveClient,
NewTab,
+ OverrideLayout,
ApplyCachedEvents,
ApplyCachedWorkerMessages,
PostMessageToPluginWorker,
@@ -635,6 +639,9 @@ open an issue on GitHub:
#[error("The plugin does not exist")]
PluginDoesNotExist,
+
+ #[error("Ran out of room for spans")]
+ RanOutOfRoomForSpans,
}
#[cfg(not(target_family = "wasm"))]
diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs
index 92d697a9..42ec2037 100644
--- a/zellij-utils/src/input/actions.rs
+++ b/zellij-utils/src/input/actions.rs
@@ -349,6 +349,16 @@ pub enum Action {
ToggleMouseMode,
PreviousSwapLayout,
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>,
+ retain_existing_terminal_panes: bool,
+ retain_existing_plugin_panes: bool,
+ },
/// Query all tab names
QueryTabNames,
/// Open a new tiled (embedded, non-floating) plugin pane
@@ -473,6 +483,7 @@ impl Action {
(Action::NewTab { .. }, Action::NewTab { .. }) => true,
(Action::LaunchOrFocusPlugin { .. }, Action::LaunchOrFocusPlugin { .. }) => true,
(Action::LaunchPlugin { .. }, Action::LaunchPlugin { .. }) => true,
+ (Action::OverrideLayout { .. }, Action::OverrideLayout { .. }) => true,
_ => self == other_action,
}
}
@@ -973,6 +984,90 @@ impl Action {
},
CliAction::PreviousSwapLayout => Ok(vec![Action::PreviousSwapLayout]),
CliAction::NextSwapLayout => Ok(vec![Action::NextSwapLayout]),
+ CliAction::OverrideLayout {
+ layout,
+ layout_dir,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ } => {
+ // Determine layout_dir: CLI arg > config > default
+ let layout_dir = layout_dir
+ .or_else(|| config.and_then(|c| c.options.layout_dir))
+ .or_else(|| get_layout_dir(find_default_config_dir()));
+
+ // Load layout from URL or file path
+ let (path_to_raw_layout, raw_layout, swap_layouts) = if let Some(layout_url) =
+ layout.to_str().and_then(|l| {
+ if l.starts_with("http://") || l.starts_with("https://") {
+ Some(l)
+ } else {
+ None
+ }
+ }) {
+ (
+ layout_url.to_owned(),
+ Layout::stringified_from_url(layout_url)
+ .map_err(|e| format!("Failed to load layout from URL: {}", e))?,
+ None,
+ )
+ } else {
+ Layout::stringified_from_path_or_default(Some(&layout), layout_dir)
+ .map_err(|e| format!("Failed to load layout: {}", e))?
+ };
+
+ // Parse KDL layout
+ let layout = Layout::from_str(
+ &raw_layout,
+ path_to_raw_layout,
+ swap_layouts.as_ref().map(|(f, p)| (f.as_str(), p.as_str())),
+ None, // cwd
+ )
+ .map_err(|e| {
+ let stringified_error = match e {
+ ConfigError::KdlError(kdl_error) => {
+ let error = kdl_error.add_src(
+ layout.as_path().as_os_str().to_string_lossy().to_string(),
+ String::from(raw_layout),
+ );
+ let report: Report = error.into();
+ format!("{:?}", report)
+ },
+ ConfigError::KdlDeserializationError(kdl_error) => {
+ let error_message = kdl_error.to_string();
+ format!("Failed to deserialize KDL layout: {}", error_message)
+ },
+ e => format!("{}", e),
+ };
+ 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()));
+ }
+
+ 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)
+ };
+
+ Ok(vec![Action::OverrideLayout {
+ tiled_layout: Some(tiled_layout),
+ floating_layouts: floating_panes_layout,
+ swap_tiled_layouts,
+ swap_floating_layouts,
+ tab_name: name,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ }])
+ },
CliAction::QueryTabNames => Ok(vec![Action::QueryTabNames]),
CliAction::StartOrReloadPlugin { url, configuration } => {
let current_dir = get_current_dir();
diff --git a/zellij-utils/src/input/unit/layout_test.rs b/zellij-utils/src/input/unit/layout_test.rs
index d13dd2c0..292909e5 100644
--- a/zellij-utils/src/input/unit/layout_test.rs
+++ b/zellij-utils/src/input/unit/layout_test.rs
@@ -2242,3 +2242,122 @@ fn env_var_missing() {
let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None);
assert!(layout.is_err(), "invalid env var lookup should fail");
}
+
+#[test]
+fn layout_node_with_tab_name() {
+ let kdl_layout = r#"
+ layout name="my-layout" {
+ pane
+ }
+ "#;
+
+ let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap();
+
+ assert_eq!(layout.tabs.len(), 1);
+ assert_eq!(layout.tabs[0].0, Some("my-layout".to_string()));
+}
+
+#[test]
+fn layout_node_with_split_direction() {
+ let kdl_layout = r#"
+ layout split_direction="vertical" {
+ pane
+ pane
+ }
+ "#;
+
+ let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap();
+
+ assert_eq!(
+ layout.tabs[0].1.children_split_direction,
+ SplitDirection::Vertical
+ );
+}
+
+#[test]
+fn layout_node_with_hide_floating_panes() {
+ let kdl_layout = r#"
+ layout hide_floating_panes=true {
+ pane
+ }
+ "#;
+
+ let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap();
+
+ assert_eq!(layout.tabs[0].1.hide_floating_panes, true);
+}
+
+#[test]
+fn layout_node_with_cwd() {
+ let kdl_layout = r#"
+ layout cwd="/tmp" {
+ pane
+ }
+ "#;
+
+ let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap();
+
+ // Verify cwd was applied - check the first pane's run property
+ assert!(layout.tabs.len() > 0);
+ // The cwd should be propagated to children
+ if let Some(Run::Cwd(path)) = &layout.tabs[0].1.children[0].run {
+ assert_eq!(path.to_str(), Some("/tmp"));
+ } else {
+ panic!("Expected cwd to be set on pane");
+ }
+}
+
+#[test]
+fn layout_node_with_multiple_tab_properties() {
+ let kdl_layout = r#"
+ layout name="test" split_direction="vertical" hide_floating_panes=true {
+ pane
+ pane
+ }
+ "#;
+
+ let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap();
+
+ assert_eq!(layout.tabs[0].0, Some("test".to_string()));
+ assert_eq!(
+ layout.tabs[0].1.children_split_direction,
+ SplitDirection::Vertical
+ );
+ assert_eq!(layout.tabs[0].1.hide_floating_panes, true);
+}
+
+#[test]
+fn layout_node_with_tab_properties_and_explicit_tabs_errors() {
+ let kdl_layout = r#"
+ layout name="test" {
+ tab {
+ pane
+ }
+ }
+ "#;
+
+ let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None);
+
+ assert!(layout.is_err());
+ let error_message = format!("{:?}", layout.unwrap_err());
+ assert!(error_message.contains(
+ "Tab properties on the layout node can only be used when there are no explicit tab nodes"
+ ));
+}
+
+#[test]
+fn layout_node_with_name_and_only_floating_panes() {
+ let kdl_layout = r#"
+ layout name="floating-only" {
+ floating_panes {
+ pane
+ }
+ }
+ "#;
+
+ let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap();
+
+ // Verify the tab name was applied
+ assert_eq!(layout.tabs.len(), 1);
+ assert_eq!(layout.tabs[0].0, Some("floating-only".to_string()));
+}
diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__env_var_expansion.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__env_var_expansion.snap
index 165050f5..2c5b1bdc 100644
--- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__env_var_expansion.snap
+++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__env_var_expansion.snap
@@ -3,7 +3,245 @@ source: zellij-utils/src/input/./unit/layout_test.rs
expression: "format!(\"{layout:#?}\")"
---
Layout {
- tabs: [],
+ tabs: [
+ (
+ None,
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: Some(
+ Cwd(
+ "/abs/path/relative",
+ ),
+ ),
+ 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: Some(
+ Cwd(
+ "/another/abs",
+ ),
+ ),
+ 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: Some(
+ Cwd(
+ "/another/abs",
+ ),
+ ),
+ 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: Some(
+ Cwd(
+ "/abs/path/relative",
+ ),
+ ),
+ 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: Some(
+ Command(
+ RunCommand {
+ command: "ls",
+ args: [],
+ cwd: Some(
+ "/somewhere",
+ ),
+ hold_on_close: true,
+ hold_on_start: false,
+ originating_plugin: None,
+ use_terminal_title: false,
+ },
+ ),
+ ),
+ 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: Some(
+ EditFile(
+ "/somewhere/file.rs",
+ None,
+ Some(
+ "/somewhere",
+ ),
+ ),
+ ),
+ 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: Some(
+ EditFile(
+ "/home/aram/backup/file.rs",
+ None,
+ Some(
+ "/home/aram/backup",
+ ),
+ ),
+ ),
+ 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: Some(
+ Command(
+ RunCommand {
+ command: "/home/aram/backup/executable",
+ args: [],
+ cwd: Some(
+ "/abs/path",
+ ),
+ hold_on_close: true,
+ hold_on_start: false,
+ originating_plugin: None,
+ use_terminal_title: false,
+ },
+ ),
+ ),
+ 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: Some(
+ EditFile(
+ "/home/aram/backup/foo.txt",
+ None,
+ Some(
+ "/abs/path",
+ ),
+ ),
+ ),
+ 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: Some(
+ Cwd(
+ "/abs/path",
+ ),
+ ),
+ 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,
+ },
+ [],
+ ),
+ ],
focused_tab_index: None,
template: Some(
(
@@ -225,7 +463,11 @@ Layout {
},
],
split_size: None,
- run: None,
+ run: Some(
+ Cwd(
+ "/abs/path",
+ ),
+ ),
borderless: false,
focus: None,
external_children_index: None,
diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_given_to_panes_without_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_given_to_panes_without_cwd.snap
index a1ef07f5..eca5399e 100644
--- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_given_to_panes_without_cwd.snap
+++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_given_to_panes_without_cwd.snap
@@ -3,7 +3,83 @@ source: zellij-utils/src/input/./unit/layout_test.rs
expression: "format!(\"{:#?}\", layout)"
---
Layout {
- tabs: [],
+ tabs: [
+ (
+ None,
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
+ 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: Some(
+ Command(
+ RunCommand {
+ command: "tail",
+ args: [],
+ cwd: Some(
+ "/tmp",
+ ),
+ hold_on_close: true,
+ hold_on_start: false,
+ originating_plugin: None,
+ use_terminal_title: false,
+ },
+ ),
+ ),
+ 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: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
+ 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,
+ },
+ [],
+ ),
+ ],
focused_tab_index: None,
template: Some(
(
@@ -63,7 +139,11 @@ Layout {
},
],
split_size: None,
- run: None,
+ run: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
borderless: false,
focus: None,
external_children_index: None,
diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_passed_from_layout_constructor.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_passed_from_layout_constructor.snap
index a1ef07f5..0d5a8d8c 100644
--- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_passed_from_layout_constructor.snap
+++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_passed_from_layout_constructor.snap
@@ -63,7 +63,11 @@ Layout {
},
],
split_size: None,
- run: None,
+ run: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
borderless: false,
focus: None,
external_children_index: None,
diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_passed_from_layout_constructor_overrides_global_cwd_in_layout_file.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_passed_from_layout_constructor_overrides_global_cwd_in_layout_file.snap
index a1ef07f5..fd0fc3a7 100644
--- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_passed_from_layout_constructor_overrides_global_cwd_in_layout_file.snap
+++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_passed_from_layout_constructor_overrides_global_cwd_in_layout_file.snap
@@ -3,7 +3,83 @@ source: zellij-utils/src/input/./unit/layout_test.rs
expression: "format!(\"{:#?}\", layout)"
---
Layout {
- tabs: [],
+ tabs: [
+ (
+ None,
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
+ 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: Some(
+ Command(
+ RunCommand {
+ command: "tail",
+ args: [],
+ cwd: Some(
+ "/tmp",
+ ),
+ hold_on_close: true,
+ hold_on_start: false,
+ originating_plugin: None,
+ use_terminal_title: false,
+ },
+ ),
+ ),
+ 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: Some(
+ Cwd(
+ "/home",
+ ),
+ ),
+ 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,
+ },
+ [],
+ ),
+ ],
focused_tab_index: None,
template: Some(
(
@@ -63,7 +139,11 @@ Layout {
},
],
split_size: None,
- run: None,
+ run: Some(
+ Cwd(
+ "/home",
+ ),
+ ),
borderless: false,
focus: None,
external_children_index: None,
diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_prepended_to_panes_with_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_prepended_to_panes_with_cwd.snap
index 6e84d38d..62bce0d4 100644
--- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_prepended_to_panes_with_cwd.snap
+++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_prepended_to_panes_with_cwd.snap
@@ -3,7 +3,83 @@ source: zellij-utils/src/input/./unit/layout_test.rs
expression: "format!(\"{:#?}\", layout)"
---
Layout {
- tabs: [],
+ tabs: [
+ (
+ None,
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: Some(
+ Cwd(
+ "/tmp/foo",
+ ),
+ ),
+ 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: Some(
+ Command(
+ RunCommand {
+ command: "tail",
+ args: [],
+ cwd: Some(
+ "/home/foo",
+ ),
+ hold_on_close: true,
+ hold_on_start: false,
+ originating_plugin: None,
+ use_terminal_title: false,
+ },
+ ),
+ ),
+ 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: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
+ 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,
+ },
+ [],
+ ),
+ ],
focused_tab_index: None,
template: Some(
(
@@ -63,7 +139,11 @@ Layout {
},
],
split_size: None,
- run: None,
+ run: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
borderless: false,
focus: None,
external_children_index: None,
diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_is_overriden_by_its_consumers_bare_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_is_overriden_by_its_consumers_bare_cwd.snap
index 791e69a3..1698b575 100644
--- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_is_overriden_by_its_consumers_bare_cwd.snap
+++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_is_overriden_by_its_consumers_bare_cwd.snap
@@ -3,7 +3,63 @@ source: zellij-utils/src/input/./unit/layout_test.rs
expression: "format!(\"{:#?}\", layout)"
---
Layout {
- tabs: [],
+ tabs: [
+ (
+ None,
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: Some(
+ Command(
+ RunCommand {
+ command: "tail",
+ args: [],
+ cwd: Some(
+ "/tmp/bar",
+ ),
+ hold_on_close: true,
+ hold_on_start: false,
+ originating_plugin: None,
+ use_terminal_title: false,
+ },
+ ),
+ ),
+ 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: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
+ 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,
+ },
+ [],
+ ),
+ ],
focused_tab_index: None,
template: Some(
(
@@ -43,7 +99,11 @@ Layout {
},
],
split_size: None,
- run: None,
+ run: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
borderless: false,
focus: None,
external_children_index: None,
diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_overriden_by_its_consumers_command_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_overriden_by_its_consumers_command_cwd.snap
index 13e6a55d..ea74b798 100644
--- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_overriden_by_its_consumers_command_cwd.snap
+++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_overriden_by_its_consumers_command_cwd.snap
@@ -3,7 +3,63 @@ source: zellij-utils/src/input/./unit/layout_test.rs
expression: "format!(\"{:#?}\", layout)"
---
Layout {
- tabs: [],
+ tabs: [
+ (
+ None,
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: Some(
+ Command(
+ RunCommand {
+ command: "pwd",
+ args: [],
+ cwd: Some(
+ "/tmp/foo",
+ ),
+ hold_on_close: true,
+ hold_on_start: false,
+ originating_plugin: None,
+ use_terminal_title: false,
+ },
+ ),
+ ),
+ 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: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
+ 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,
+ },
+ [],
+ ),
+ ],
focused_tab_index: None,
template: Some(
(
@@ -43,7 +99,11 @@ Layout {
},
],
split_size: None,
- run: None,
+ run: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
borderless: false,
focus: None,
external_children_index: None,
diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_remains_when_its_consumer_command_does_not_have_a_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_remains_when_its_consumer_command_does_not_have_a_cwd.snap
index 5d34b55a..4ab7252f 100644
--- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_remains_when_its_consumer_command_does_not_have_a_cwd.snap
+++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_remains_when_its_consumer_command_does_not_have_a_cwd.snap
@@ -3,7 +3,63 @@ source: zellij-utils/src/input/./unit/layout_test.rs
expression: "format!(\"{:#?}\", layout)"
---
Layout {
- tabs: [],
+ tabs: [
+ (
+ None,
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: Some(
+ Command(
+ RunCommand {
+ command: "pwd",
+ args: [],
+ cwd: Some(
+ "/tmp/bar",
+ ),
+ hold_on_close: true,
+ hold_on_start: false,
+ originating_plugin: None,
+ use_terminal_title: false,
+ },
+ ),
+ ),
+ 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: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
+ 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,
+ },
+ [],
+ ),
+ ],
focused_tab_index: None,
template: Some(
(
@@ -43,7 +99,11 @@ Layout {
},
],
split_size: None,
- run: None,
+ run: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
borderless: false,
focus: None,
external_children_index: None,
diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_without_cwd_is_overriden_by_its_consumers_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_without_cwd_is_overriden_by_its_consumers_cwd.snap
index 5d34b55a..4ab7252f 100644
--- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_without_cwd_is_overriden_by_its_consumers_cwd.snap
+++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_without_cwd_is_overriden_by_its_consumers_cwd.snap
@@ -3,7 +3,63 @@ source: zellij-utils/src/input/./unit/layout_test.rs
expression: "format!(\"{:#?}\", layout)"
---
Layout {
- tabs: [],
+ tabs: [
+ (
+ None,
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: Some(
+ Command(
+ RunCommand {
+ command: "pwd",
+ args: [],
+ cwd: Some(
+ "/tmp/bar",
+ ),
+ hold_on_close: true,
+ hold_on_start: false,
+ originating_plugin: None,
+ use_terminal_title: false,
+ },
+ ),
+ ),
+ 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: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
+ 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,
+ },
+ [],
+ ),
+ ],
focused_tab_index: None,
template: Some(
(
@@ -43,7 +99,11 @@ Layout {
},
],
split_size: None,
- run: None,
+ run: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
borderless: false,
focus: None,
external_children_index: None,
diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_without_cwd_receives_its_consumers_bare_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_without_cwd_receives_its_consumers_bare_cwd.snap
index 791e69a3..1698b575 100644
--- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_without_cwd_receives_its_consumers_bare_cwd.snap
+++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_without_cwd_receives_its_consumers_bare_cwd.snap
@@ -3,7 +3,63 @@ source: zellij-utils/src/input/./unit/layout_test.rs
expression: "format!(\"{:#?}\", layout)"
---
Layout {
- tabs: [],
+ tabs: [
+ (
+ None,
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: Some(
+ Command(
+ RunCommand {
+ command: "tail",
+ args: [],
+ cwd: Some(
+ "/tmp/bar",
+ ),
+ hold_on_close: true,
+ hold_on_start: false,
+ originating_plugin: None,
+ use_terminal_title: false,
+ },
+ ),
+ ),
+ 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: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
+ 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,
+ },
+ [],
+ ),
+ ],
focused_tab_index: None,
template: Some(
(
@@ -43,7 +99,11 @@ Layout {
},
],
split_size: None,
- run: None,
+ run: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
borderless: false,
focus: None,
external_children_index: None,
diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_cwd_overriden_by_its_consumers_bare_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_cwd_overriden_by_its_consumers_bare_cwd.snap
index 62f8e038..9ff94a33 100644
--- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_cwd_overriden_by_its_consumers_bare_cwd.snap
+++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_cwd_overriden_by_its_consumers_bare_cwd.snap
@@ -1,10 +1,55 @@
---
source: zellij-utils/src/input/./unit/layout_test.rs
-assertion_line: 1594
expression: "format!(\"{:#?}\", layout)"
---
Layout {
- tabs: [],
+ tabs: [
+ (
+ None,
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: Some(
+ Cwd(
+ "/tmp/bar",
+ ),
+ ),
+ 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: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
+ 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,
+ },
+ [],
+ ),
+ ],
focused_tab_index: None,
template: Some(
(
@@ -34,7 +79,11 @@ Layout {
},
],
split_size: None,
- run: None,
+ run: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
borderless: false,
focus: None,
external_children_index: None,
diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_command_with_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_command_with_cwd.snap
index 791e69a3..1698b575 100644
--- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_command_with_cwd.snap
+++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_command_with_cwd.snap
@@ -3,7 +3,63 @@ source: zellij-utils/src/input/./unit/layout_test.rs
expression: "format!(\"{:#?}\", layout)"
---
Layout {
- tabs: [],
+ tabs: [
+ (
+ None,
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: Some(
+ Command(
+ RunCommand {
+ command: "tail",
+ args: [],
+ cwd: Some(
+ "/tmp/bar",
+ ),
+ hold_on_close: true,
+ hold_on_start: false,
+ originating_plugin: None,
+ use_terminal_title: false,
+ },
+ ),
+ ),
+ 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: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
+ 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,
+ },
+ [],
+ ),
+ ],
focused_tab_index: None,
template: Some(
(
@@ -43,7 +99,11 @@ Layout {
},
],
split_size: None,
- run: None,
+ run: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
borderless: false,
focus: None,
external_children_index: None,
diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_command_without_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_command_without_cwd.snap
index 3eba1d77..a33edb62 100644
--- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_command_without_cwd.snap
+++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_command_without_cwd.snap
@@ -3,7 +3,63 @@ source: zellij-utils/src/input/./unit/layout_test.rs
expression: "format!(\"{:#?}\", layout)"
---
Layout {
- tabs: [],
+ tabs: [
+ (
+ None,
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: Some(
+ Command(
+ RunCommand {
+ command: "tail",
+ args: [],
+ cwd: Some(
+ "/tmp/foo",
+ ),
+ hold_on_close: true,
+ hold_on_start: false,
+ originating_plugin: None,
+ use_terminal_title: false,
+ },
+ ),
+ ),
+ 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: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
+ 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,
+ },
+ [],
+ ),
+ ],
focused_tab_index: None,
template: Some(
(
@@ -43,7 +99,11 @@ Layout {
},
],
split_size: None,
- run: None,
+ run: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
borderless: false,
focus: None,
external_children_index: None,
diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_edit.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_edit.snap
index adaa2de1..b12ec15b 100644
--- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_edit.snap
+++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_edit.snap
@@ -1,10 +1,59 @@
---
source: zellij-utils/src/input/./unit/layout_test.rs
-assertion_line: 1644
expression: "format!(\"{:#?}\", layout)"
---
Layout {
- tabs: [],
+ tabs: [
+ (
+ None,
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: Some(
+ EditFile(
+ "/tmp/foo/bar",
+ None,
+ Some(
+ "/tmp/foo",
+ ),
+ ),
+ ),
+ 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: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
+ 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,
+ },
+ [],
+ ),
+ ],
focused_tab_index: None,
template: Some(
(
@@ -38,7 +87,11 @@ Layout {
},
],
split_size: None,
- run: None,
+ run: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
borderless: false,
focus: None,
external_children_index: None,
diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_command_propagated_to_its_consumer_edit.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_command_propagated_to_its_consumer_edit.snap
index eef3d4ba..c716ad07 100644
--- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_command_propagated_to_its_consumer_edit.snap
+++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_command_propagated_to_its_consumer_edit.snap
@@ -1,10 +1,59 @@
---
source: zellij-utils/src/input/./unit/layout_test.rs
-assertion_line: 1660
expression: "format!(\"{:#?}\", layout)"
---
Layout {
- tabs: [],
+ tabs: [
+ (
+ None,
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: Some(
+ EditFile(
+ "/tmp/foo/bar",
+ None,
+ Some(
+ "/tmp/foo/",
+ ),
+ ),
+ ),
+ 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: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
+ 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,
+ },
+ [],
+ ),
+ ],
focused_tab_index: None,
template: Some(
(
@@ -38,7 +87,11 @@ Layout {
},
],
split_size: None,
- run: None,
+ run: Some(
+ Cwd(
+ "/tmp",
+ ),
+ ),
borderless: false,
focus: None,
external_children_index: None,
diff --git a/zellij-utils/src/ipc.rs b/zellij-utils/src/ipc.rs
index f79af3a9..455fd4b3 100644
--- a/zellij-utils/src/ipc.rs
+++ b/zellij-utils/src/ipc.rs
@@ -302,10 +302,7 @@ where
None
},
},
- Err(e) => {
- warn!("Error in protobuf IpcReceiver.recv_client_msg(): {:?}", e);
- None
- },
+ Err(_e) => None,
}
}
@@ -318,10 +315,7 @@ where
None
},
},
- Err(e) => {
- warn!("Error in protobuf IpcReceiver.recv_server_msg(): {:?}", e);
- None
- },
+ Err(_e) => None,
}
}
@@ -392,10 +386,7 @@ pub fn recv_protobuf_client_to_server(
None
},
},
- Err(e) => {
- warn!("Error reading protobuf message: {:?}", e);
- None
- },
+ Err(_e) => None,
}
}
@@ -410,9 +401,6 @@ pub fn recv_protobuf_server_to_client(
None
},
},
- Err(e) => {
- warn!("Error reading protobuf message: {:?}", e);
- None
- },
+ Err(_e) => None,
}
}
diff --git a/zellij-utils/src/ipc/protobuf_conversion.rs b/zellij-utils/src/ipc/protobuf_conversion.rs
index cf7b9802..e803f51e 100644
--- a/zellij-utils/src/ipc/protobuf_conversion.rs
+++ b/zellij-utils/src/ipc/protobuf_conversion.rs
@@ -723,19 +723,19 @@ impl From<crate::input::actions::Action>
MoveTabAction, NewBlockingPaneAction, NewFloatingPaneAction,
NewFloatingPluginPaneAction, NewInPlacePaneAction, NewInPlacePluginPaneAction,
NewPaneAction, NewStackedPaneAction, NewTabAction, NewTiledPaneAction,
- NewTiledPluginPaneAction, NextSwapLayoutAction, NoOpAction, PageScrollDownAction,
- PageScrollUpAction, PaneIdWithPlugin, PaneNameInputAction, PreviousSwapLayoutAction,
- QueryTabNamesAction, QuitAction, RenamePluginPaneAction, RenameSessionAction,
- RenameTabAction, RenameTerminalPaneAction, ResizeAction, RunAction, ScrollDownAction,
- ScrollDownAtAction, ScrollToBottomAction, ScrollToTopAction, ScrollUpAction,
- ScrollUpAtAction, SearchAction, SearchInputAction, SearchToggleOptionAction,
- SkipConfirmAction, StackPanesAction, StartOrReloadPluginAction, SwitchFocusAction,
- SwitchModeForAllClientsAction, SwitchSessionAction, SwitchToModeAction,
- TabNameInputAction, ToggleActiveSyncTabAction, ToggleFloatingPanesAction,
- ToggleFocusFullscreenAction, ToggleGroupMarkingAction, ToggleMouseModeAction,
- TogglePaneEmbedOrFloatingAction, TogglePaneFramesAction, TogglePaneInGroupAction,
- TogglePanePinnedAction, ToggleTabAction, UndoRenamePaneAction, UndoRenameTabAction,
- WriteAction, WriteCharsAction,
+ NewTiledPluginPaneAction, NextSwapLayoutAction, NoOpAction, OverrideLayoutAction,
+ PageScrollDownAction, PageScrollUpAction, PaneIdWithPlugin, PaneNameInputAction,
+ PreviousSwapLayoutAction, QueryTabNamesAction, QuitAction, RenamePluginPaneAction,
+ RenameSessionAction, RenameTabAction, RenameTerminalPaneAction, ResizeAction,
+ RunAction, ScrollDownAction, ScrollDownAtAction, ScrollToBottomAction,
+ ScrollToTopAction, ScrollUpAction, ScrollUpAtAction, SearchAction, SearchInputAction,
+ SearchToggleOptionAction, SkipConfirmAction, StackPanesAction,
+ StartOrReloadPluginAction, SwitchFocusAction, SwitchModeForAllClientsAction,
+ SwitchSessionAction, SwitchToModeAction, TabNameInputAction, ToggleActiveSyncTabAction,
+ ToggleFloatingPanesAction, ToggleFocusFullscreenAction, ToggleGroupMarkingAction,
+ ToggleMouseModeAction, TogglePaneEmbedOrFloatingAction, TogglePaneFramesAction,
+ TogglePaneInGroupAction, TogglePanePinnedAction, ToggleTabAction, UndoRenamePaneAction,
+ UndoRenameTabAction, WriteAction, WriteCharsAction,
};
use std::collections::HashMap;
@@ -1097,6 +1097,27 @@ impl From<crate::input::actions::Action>
crate::input::actions::Action::NextSwapLayout => {
ActionType::NextSwapLayout(NextSwapLayoutAction {})
},
+ crate::input::actions::Action::OverrideLayout {
+ tiled_layout,
+ floating_layouts,
+ swap_tiled_layouts,
+ swap_floating_layouts,
+ tab_name,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ } => 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,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ }),
crate::input::actions::Action::QueryTabNames => {
ActionType::QueryTabNames(QueryTabNamesAction {})
},
@@ -1672,6 +1693,49 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Action>
Ok(crate::input::actions::Action::PreviousSwapLayout)
},
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
+ .into_iter()
+ .map(|l| l.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,
+ })
+ },
ActionType::QueryTabNames(_) => Ok(crate::input::actions::Action::QueryTabNames),
ActionType::NewTiledPluginPane(new_tiled_plugin_action) => {
Ok(crate::input::actions::Action::NewTiledPluginPane {
diff --git a/zellij-utils/src/ipc/tests/roundtrip_tests.rs b/zellij-utils/src/ipc/tests/roundtrip_tests.rs
index 61981f38..216c6e14 100644
--- a/zellij-utils/src/ipc/tests/roundtrip_tests.rs
+++ b/zellij-utils/src/ipc/tests/roundtrip_tests.rs
@@ -2053,6 +2053,20 @@ fn test_client_messages() {
is_cli_client: true,
});
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,
+ retain_existing_terminal_panes: false,
+ retain_existing_plugin_panes: false,
+ },
+ terminal_id: Some(1),
+ client_id: Some(100),
+ is_cli_client: true,
+ });
+ test_client_roundtrip!(ClientToServerMsg::Action {
action: Action::NoOp,
terminal_id: Some(1),
client_id: Some(100),
diff --git a/zellij-utils/src/kdl/kdl_layout_parser.rs b/zellij-utils/src/kdl/kdl_layout_parser.rs
index c38c0146..38cda189 100644
--- a/zellij-utils/src/kdl/kdl_layout_parser.rs
+++ b/zellij-utils/src/kdl/kdl_layout_parser.rs
@@ -2131,19 +2131,39 @@ impl<'a> KdlLayoutParser<'a> {
floating_panes: Vec<FloatingPaneLayout>,
swap_tiled_layouts: Vec<SwapTiledLayout>,
swap_floating_layouts: Vec<SwapFloatingLayout>,
+ tab_name: Option<String>,
+ split_direction: SplitDirection,
+ hide_floating_panes: bool,
+ tab_cwd: Option<PathBuf>,
) -> Result<Layout, ConfigError> {
- let main_tab_layout = TiledPaneLayout {
+ let mut main_tab_layout = TiledPaneLayout {
children: panes,
+ children_split_direction: split_direction,
+ hide_floating_panes,
..Default::default()
};
+ let mut floating_panes = floating_panes;
+ if let Some(cwd_prefix) = self.cwd_prefix(tab_cwd.as_ref())? {
+ main_tab_layout.add_cwd_to_layout(&cwd_prefix);
+ for floating_pane in floating_panes.iter_mut() {
+ floating_pane.add_cwd_to_layout(&cwd_prefix);
+ }
+ }
let default_template = self.default_template()?;
- let tabs = if default_template.is_none() && self.new_tab_template.is_none() {
- // in this case, the layout will be created as the default template and we don't need
- // to explicitly place it in the first tab
- vec![]
- } else {
- vec![(None, main_tab_layout.clone(), floating_panes.clone())]
- };
+ // Check if any tab properties are specified that would require creating an explicit tab
+ let has_tab_properties = tab_name.is_some()
+ || split_direction != SplitDirection::default()
+ || hide_floating_panes != false
+ || tab_cwd.is_some();
+ let tabs =
+ if default_template.is_none() && self.new_tab_template.is_none() && !has_tab_properties
+ {
+ // in this case, the layout will be created as the default template and we don't need
+ // to explicitly place it in the first tab
+ vec![]
+ } else {
+ vec![(tab_name, main_tab_layout.clone(), floating_panes.clone())]
+ };
let template = default_template
.map(|tiled_panes_template| (tiled_panes_template, floating_panes.clone()))
.or_else(|| self.new_tab_template.clone())
@@ -2162,16 +2182,51 @@ impl<'a> KdlLayoutParser<'a> {
child_floating_panes: Vec<FloatingPaneLayout>,
swap_tiled_layouts: Vec<SwapTiledLayout>,
swap_floating_layouts: Vec<SwapFloatingLayout>,
+ tab_name: Option<String>,
+ split_direction: SplitDirection,
+ hide_floating_panes: bool,
+ tab_cwd: Option<PathBuf>,
) -> Result<Layout, ConfigError> {
+ let mut child_floating_panes = child_floating_panes;
let template = if let Some(new_tab_template) = &self.new_tab_template {
Some(new_tab_template.clone())
} else {
- let default_tab_tiled_panes_template = self
+ let mut default_tab_tiled_panes_template = self
.default_template()?
.unwrap_or_else(|| TiledPaneLayout::default());
- Some((default_tab_tiled_panes_template, child_floating_panes))
+
+ default_tab_tiled_panes_template.children_split_direction = split_direction;
+ default_tab_tiled_panes_template.hide_floating_panes = hide_floating_panes;
+
+ if let Some(cwd_prefix) = self.cwd_prefix(tab_cwd.as_ref())? {
+ default_tab_tiled_panes_template.add_cwd_to_layout(&cwd_prefix);
+ for floating_pane in child_floating_panes.iter_mut() {
+ floating_pane.add_cwd_to_layout(&cwd_prefix);
+ }
+ }
+
+ Some((
+ default_tab_tiled_panes_template,
+ child_floating_panes.clone(),
+ ))
+ };
+ // Check if any tab properties are specified that would require creating an explicit tab
+ let has_tab_properties = tab_name.is_some()
+ || split_direction != SplitDirection::default()
+ || hide_floating_panes != false
+ || tab_cwd.is_some();
+ let tabs = if has_tab_properties {
+ // If we have tab properties, we need to create a tab with those properties
+ if let Some((ref tiled_layout, ref floating_panes)) = template {
+ vec![(tab_name, tiled_layout.clone(), floating_panes.clone())]
+ } else {
+ vec![]
+ }
+ } else {
+ vec![]
};
Ok(Layout {
+ tabs,
template,
swap_tiled_layouts,
swap_floating_layouts,
@@ -2423,6 +2478,23 @@ impl<'a> KdlLayoutParser<'a> {
}
}
if !child_tabs.is_empty() {
+ // Check if layout_node has tab properties when there are explicit tabs
+ let layout_has_tab_name =
+ kdl_get_string_property_or_child_value!(layout_node, "name").is_some();
+ let layout_has_split_direction =
+ kdl_get_string_property_or_child_value_with_error!(layout_node, "split_direction")
+ .is_some();
+ let layout_has_hide_floating =
+ kdl_get_bool_property_or_child_value!(layout_node, "hide_floating_panes").is_some();
+
+ if layout_has_tab_name || layout_has_split_direction || layout_has_hide_floating {
+ return Err(ConfigError::new_layout_kdl_error(
+ "Tab properties on the layout node can only be used when there are no explicit tab nodes".into(),
+ layout_node.span().offset(),
+ layout_node.span().len(),
+ ));
+ }
+
let has_more_than_one_focused_tab = child_tabs
.iter()
.filter(|(is_focused, _, _, _)| *is_focused)
@@ -2454,17 +2526,43 @@ impl<'a> KdlLayoutParser<'a> {
swap_floating_layouts,
)
} else if !child_panes.is_empty() {
+ // Extract tab properties from layout_node
+ let tab_name =
+ kdl_get_string_property_or_child_value!(layout_node, "name").map(|s| s.to_string());
+ let split_direction = self.parse_split_direction(layout_node)?;
+ let hide_floating_panes =
+ kdl_get_bool_property_or_child_value!(layout_node, "hide_floating_panes")
+ .unwrap_or(false);
+ let tab_cwd = self.parse_path(layout_node, "cwd")?;
+
self.layout_with_one_tab(
child_panes,
child_floating_panes,
swap_tiled_layouts,
swap_floating_layouts,
+ tab_name,
+ split_direction,
+ hide_floating_panes,
+ tab_cwd,
)
} else {
+ // Extract tab properties for layout_with_one_pane case
+ let tab_name =
+ kdl_get_string_property_or_child_value!(layout_node, "name").map(|s| s.to_string());
+ let split_direction = self.parse_split_direction(layout_node)?;
+ let hide_floating_panes =
+ kdl_get_bool_property_or_child_value!(layout_node, "hide_floating_panes")
+ .unwrap_or(false);
+ let tab_cwd = self.parse_path(layout_node, "cwd")?;
+
self.layout_with_one_pane(
child_floating_panes,
swap_tiled_layouts,
swap_floating_layouts,
+ tab_name,
+ split_direction,
+ hide_floating_panes,
+ tab_cwd,
)
}
}
diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs
index 5ca02d86..7603af3f 100644
--- a/zellij-utils/src/kdl/mod.rs
+++ b/zellij-utils/src/kdl/mod.rs
@@ -1723,6 +1723,110 @@ impl TryFrom<(&KdlNode, &Options)> for Action {
})
}
},
+ "OverrideLayout" => {
+ 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,
+ retain_existing_terminal_panes: false,
+ retain_existing_plugin_panes: false,
+ });
+ }
+
+ let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
+
+ let layout = command_metadata
+ .and_then(|c_m| kdl_child_string_value_for_entry(c_m, "layout"))
+ .map(|layout_string| PathBuf::from(layout_string))
+ .or_else(|| config_options.default_layout.clone());
+ let cwd = command_metadata
+ .and_then(|c_m| kdl_child_string_value_for_entry(c_m, "cwd"))
+ .map(|cwd_string| PathBuf::from(cwd_string))
+ .map(|cwd| current_dir.join(cwd));
+ let name = command_metadata
+ .and_then(|c_m| kdl_child_string_value_for_entry(c_m, "name"))
+ .map(|name_string| name_string.to_string());
+ let retain_existing_terminal_panes = command_metadata
+ .and_then(|c_m| {
+ kdl_child_bool_value_for_entry(c_m, "retain_existing_terminal_panes")
+ })
+ .unwrap_or(false);
+ let retain_existing_plugin_panes = command_metadata
+ .and_then(|c_m| {
+ kdl_child_bool_value_for_entry(c_m, "retain_existing_plugin_panes")
+ })
+ .unwrap_or(false);
+
+ let layout_dir = config_options
+ .layout_dir
+ .clone()
+ .or_else(|| get_layout_dir(find_default_config_dir()));
+ let (path_to_raw_layout, raw_layout, swap_layouts) =
+ Layout::stringified_from_path_or_default(layout.as_ref(), layout_dir).map_err(
+ |e| {
+ ConfigError::new_kdl_error(
+ format!("Failed to load layout: {}", e),
+ kdl_action.span().offset(),
+ kdl_action.span().len(),
+ )
+ },
+ )?;
+
+ let layout = Layout::from_str(
+ &raw_layout,
+ path_to_raw_layout,
+ swap_layouts.as_ref().map(|(f, p)| (f.as_str(), p.as_str())),
+ cwd.clone(),
+ )
+ .map_err(|e| {
+ ConfigError::new_kdl_error(
+ format!("Failed to load layout: {}", e),
+ kdl_action.span().offset(),
+ kdl_action.span().len(),
+ )
+ })?;
+
+ let swap_tiled_layouts = Some(layout.swap_tiled_layouts.clone());
+ let swap_floating_layouts = Some(layout.swap_floating_layouts.clone());
+
+ let mut tabs = layout.tabs();
+ if tabs.len() > 1 {
+ return Err(ConfigError::new_kdl_error(
+ "Tab layout cannot itself have tabs".to_string(),
+ kdl_action.span().offset(),
+ kdl_action.span().len(),
+ ));
+ } else if !tabs.is_empty() {
+ let (tab_name, layout, floating_panes_layout) = tabs.drain(..).next().unwrap();
+ let name = tab_name.or(name);
+
+ Ok(Action::OverrideLayout {
+ tiled_layout: Some(layout),
+ floating_layouts: floating_panes_layout,
+ swap_tiled_layouts,
+ swap_floating_layouts,
+ tab_name: name,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ })
+ } else {
+ let (layout, floating_panes_layout) = layout.new_tab();
+
+ Ok(Action::OverrideLayout {
+ tiled_layout: Some(layout),
+ floating_layouts: floating_panes_layout,
+ swap_tiled_layouts,
+ swap_floating_layouts,
+ tab_name: name,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ })
+ }
+ },
"GoToTab" => parse_kdl_action_u8_arguments!(action_name, action_arguments, kdl_action),
"TabNameInput" => {
parse_kdl_action_u8_arguments!(action_name, action_arguments, kdl_action)
diff --git a/zellij-utils/src/plugin_api/action.proto b/zellij-utils/src/plugin_api/action.proto
index c29884af..de8ef44f 100644
--- a/zellij-utils/src/plugin_api/action.proto
+++ b/zellij-utils/src/plugin_api/action.proto
@@ -162,6 +162,16 @@ message NewTabPayload {
optional UnblockCondition first_pane_unblock_condition = 9;
}
+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;
+}
+
message Action {
ActionName name = 1;
oneof optional_payload {
@@ -213,6 +223,7 @@ message Action {
NewBlockingPanePayload new_blocking_pane_payload = 50;
NewTabPayload new_tab_payload = 51;
NewInPlacePanePayload new_in_place_pane_payload = 52;
+ OverrideLayoutPayload override_layout_payload = 53;
}
}
@@ -429,6 +440,7 @@ enum ActionName {
SwitchSession = 90;
NewBlockingPane = 91;
NewInPlacePane = 92;
+ OverrideLayout = 93;
}
message Position {
diff --git a/zellij-utils/src/plugin_api/action.rs b/zellij-utils/src/plugin_api/action.rs
index f8de7b76..860b090b 100644
--- a/zellij-utils/src/plugin_api/action.rs
+++ b/zellij-utils/src/plugin_api/action.rs
@@ -37,6 +37,7 @@ pub use super::generated_api::api::{
NewPluginPanePayload,
NewTabPayload,
NewTiledPanePayload,
+ OverrideLayoutPayload,
PaneId as ProtobufPaneId,
PaneIdAndShouldFloat,
PaneRun as ProtobufPaneRun,
@@ -720,6 +721,55 @@ impl TryFrom<ProtobufAction> for Action {
Some(_) => Err("NextSwapLayout should not have a payload"),
None => Ok(Action::NextSwapLayout),
},
+ 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,
+ retain_existing_terminal_panes: payload.retain_existing_terminal_panes,
+ retain_existing_plugin_panes: payload.retain_existing_plugin_panes,
+ })
+ },
+ Some(_) => Err("Mismatched payload for OverrideLayout"),
+ None => Err("Missing payload for OverrideLayout"),
+ },
Some(ProtobufActionName::QueryTabNames) => match protobuf_action.optional_payload {
Some(_) => Err("QueryTabNames should not have a payload"),
None => Ok(Action::QueryTabNames),
@@ -1505,6 +1555,57 @@ impl TryFrom<Action> for ProtobufAction {
name: ProtobufActionName::NextSwapLayout as i32,
optional_payload: None,
}),
+ Action::OverrideLayout {
+ tiled_layout,
+ floating_layouts,
+ swap_tiled_layouts,
+ swap_floating_layouts,
+ tab_name,
+ 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
+ .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,
+ },
+ )),
+ })
+ },
Action::QueryTabNames => Ok(ProtobufAction {
name: ProtobufActionName::QueryTabNames as i32,
optional_payload: None,