From 8ce05f0bbd58acbbca95f95ae14fdc4e866f7391 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Mon, 30 Mar 2026 16:02:09 +0200 Subject: [PATCH] Layout string flag (#4956) * allow sending a stringified layout with --layout-string * add --tab-id to all the things * fix windows tests * add pr --- CHANGELOG.md | 1 + .../compact-bar/src/keybind_utils.rs | 1 + default-plugins/status-bar/src/one_line_ui.rs | 2 +- default-plugins/status-bar/src/second_line.rs | 2 +- src/main.rs | 11 +- zellij-client/src/lib.rs | 36 +- zellij-server/src/plugins/wasm_bridge.rs | 21 +- zellij-server/src/plugins/zellij_exports.rs | 11 + zellij-server/src/route.rs | 176 ++-- zellij-server/src/screen.rs | 244 +++--- zellij-server/src/unit/screen_tests.rs | 414 ++++++++++ ...cli_new_tab_action_with_layout_string.snap | 119 +++ ...ab_action_with_layout_string_and_name.snap | 119 +++ .../prost_ipc/client_server_contract.rs | 22 + zellij-utils/src/cli.rs | 70 +- .../client_server_contract/common_types.proto | 11 + zellij-utils/src/input/actions.rs | 779 +++++++++++++++++- zellij-utils/src/ipc/protobuf_conversion.rs | 44 + zellij-utils/src/ipc/tests/roundtrip_tests.rs | 181 +++- zellij-utils/src/kdl/mod.rs | 15 +- zellij-utils/src/plugin_api/action.rs | 24 + zellij-utils/src/setup.rs | 25 +- ..._default_config_with_no_cli_arguments.snap | 7 + ...out_env_vars_override_config_env_vars.snap | 7 + ..._layout_themes_override_config_themes.snap | 7 + ..._ui_config_overrides_config_ui_config.snap | 7 + 26 files changed, 2108 insertions(+), 248 deletions(-) create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_layout_string.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_layout_string_and_name.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index ad47444ad..76ad03df0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * feat: support and forward OSC-99 desktop notifications (https://github.com/zellij-org/zellij/pull/4931) * fix: normalize temp socket paths in windows (https://github.com/zellij-org/zellij/pull/4923) * fix: compilation warnings in windows (https://github.com/zellij-org/zellij/pull/4890) +* feat: add --layout-string to allow for in-line layouts, add --tab-id to all new-pane CLI commands (https://github.com/zellij-org/zellij/pull/4956) ## [0.44.0] - 2026-03-23 * fix: meta key handling in web client (https://github.com/zellij-org/zellij/pull/4376) diff --git a/default-plugins/compact-bar/src/keybind_utils.rs b/default-plugins/compact-bar/src/keybind_utils.rs index 77599d533..692dde0fd 100644 --- a/default-plugins/compact-bar/src/keybind_utils.rs +++ b/default-plugins/compact-bar/src/keybind_utils.rs @@ -442,6 +442,7 @@ impl KeybindProcessor { command: None, pane_name: None, near_current_pane: false, + .. } ) }, diff --git a/default-plugins/status-bar/src/one_line_ui.rs b/default-plugins/status-bar/src/one_line_ui.rs index 33558725a..d997fb367 100644 --- a/default-plugins/status-bar/src/one_line_ui.rs +++ b/default-plugins/status-bar/src/one_line_ui.rs @@ -1386,7 +1386,7 @@ fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec Vec<(String, String, Vec, ) -> Result<(PluginId, ClientId)> { - let err_context = move || format!("failed to load plugin"); + let _err_context = move || format!("failed to load plugin"); let client_id = client_id .and_then(|client_id| { @@ -321,9 +321,22 @@ impl WasmBridge { match run { Some(run) => { - let plugin = PluginConfig::from_run_plugin(run) - .with_context(|| format!("failed to resolve plugin {run:?}")) - .with_context(err_context)?; + let plugin = match PluginConfig::from_run_plugin(run) { + Some(plugin) => plugin, + None => { + self.next_plugin_id += 1; + let mut loading_indication = + LoadingIndication::new(run.location.to_string()); + handle_plugin_loading_failure( + &self.senders, + plugin_id, + &mut loading_indication, + format!("Failed to resolve plugin: {}", run.location), + Some(client_id), + ); + return Ok((plugin_id, client_id)); + }, + }; let plugin_name = run.location.to_string(); self.cached_events_for_pending_plugins diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs index 026be9370..24113d0b8 100644 --- a/zellij-server/src/plugins/zellij_exports.rs +++ b/zellij-server/src/plugins/zellij_exports.rs @@ -1164,6 +1164,7 @@ fn open_plugin_pane_floating( skip_cache: false, cwd: Some(env.plugin_cwd.clone()), coordinates: floating_pane_coordinates, + tab_id: None, }; let error_msg = || format!("Failed to open floating plugin pane"); let result = apply_action!(action, error_msg, env); @@ -1403,6 +1404,7 @@ fn open_file(env: &PluginEnv, file_to_open: FileToOpen, context: BTreeMap { let command = command .map(|cmd| TerminalAction::RunCommand(cmd.into())) @@ -668,7 +669,9 @@ pub(crate) fn route_action( _ => pane_id, }; - let client_tab_index_or_paneid = if near_current_pane && pane_id.is_some() { + let client_tab_index_or_paneid = if let Some(tab_id) = tab_id { + ClientTabIndexOrPaneId::TabIndex(tab_id) + } else if near_current_pane && pane_id.is_some() { ClientTabIndexOrPaneId::PaneId(pane_id.unwrap()) } else { ClientTabIndexOrPaneId::ClientId(client_id) @@ -695,27 +698,31 @@ pub(crate) fn route_action( start_suppressed, coordinates: floating_pane_coordinates, near_current_pane, + tab_id, } => { let title = format!("Editing: {}", open_file_payload.path.display()); let open_file = TerminalAction::OpenFile(open_file_payload); let pty_instr = if should_open_in_place { - match pane_id { - Some(pane_id) if near_current_pane => PtyInstruction::SpawnInPlaceTerminal( - Some(open_file), - Some(title), - close_replaced_pane, - ClientTabIndexOrPaneId::PaneId(pane_id), - Some(NotificationEnd::new(completion_tx)), - ), - _ => PtyInstruction::SpawnInPlaceTerminal( - Some(open_file), - Some(title), - close_replaced_pane, - ClientTabIndexOrPaneId::ClientId(client_id), - Some(NotificationEnd::new(completion_tx)), - ), - } + let client_tab_index_or_paneid = if let Some(tab_id) = tab_id { + ClientTabIndexOrPaneId::TabIndex(tab_id) + } else if near_current_pane && pane_id.is_some() { + ClientTabIndexOrPaneId::PaneId(pane_id.unwrap()) + } else { + ClientTabIndexOrPaneId::ClientId(client_id) + }; + PtyInstruction::SpawnInPlaceTerminal( + Some(open_file), + Some(title), + close_replaced_pane, + client_tab_index_or_paneid, + Some(NotificationEnd::new(completion_tx)), + ) } else { + let client_tab_index_or_paneid = if let Some(tab_id) = tab_id { + ClientTabIndexOrPaneId::TabIndex(tab_id) + } else { + ClientTabIndexOrPaneId::ClientId(client_id) + }; PtyInstruction::SpawnTerminal( Some(open_file), Some(title), @@ -728,7 +735,7 @@ pub(crate) fn route_action( } }, start_suppressed, - ClientTabIndexOrPaneId::ClientId(client_id), + client_tab_index_or_paneid, Some(NotificationEnd::new(completion_tx)), false, // set_blocking ) @@ -761,11 +768,14 @@ pub(crate) fn route_action( pane_name: name, coordinates: floating_pane_coordinates, near_current_pane, + tab_id, } => { let run_cmd = run_command .map(|cmd| TerminalAction::RunCommand(cmd.into())) .or_else(|| default_shell.clone()); - let client_tab_index_or_paneid = if near_current_pane && pane_id.is_some() { + let client_tab_index_or_paneid = if let Some(tab_id) = tab_id { + ClientTabIndexOrPaneId::TabIndex(tab_id) + } else if near_current_pane && pane_id.is_some() { ClientTabIndexOrPaneId::PaneId(pane_id.unwrap()) } else { ClientTabIndexOrPaneId::ClientId(client_id) @@ -788,6 +798,7 @@ pub(crate) fn route_action( near_current_pane, pane_id_to_replace, close_replaced_pane, + tab_id, } => { let run_cmd = run_command .map(|cmd| TerminalAction::RunCommand(cmd.into())) @@ -796,74 +807,70 @@ pub(crate) fn route_action( Some(pane_id_to_replace) => pane_id_to_replace.try_into().ok(), None => pane_id, }; - match pane_id { - Some(pane_id) if near_current_pane => { - senders - .send_to_pty(PtyInstruction::SpawnInPlaceTerminal( - run_cmd, - name, - close_replaced_pane, - ClientTabIndexOrPaneId::PaneId(pane_id), - Some(NotificationEnd::new(completion_tx)), - )) - .with_context(err_context)?; - }, - _ => { - senders - .send_to_pty(PtyInstruction::SpawnInPlaceTerminal( - run_cmd, - name, - close_replaced_pane, - ClientTabIndexOrPaneId::ClientId(client_id), - Some(NotificationEnd::new(completion_tx)), - )) - .with_context(err_context)?; - }, - } + let client_tab_index_or_paneid = if let Some(tab_id) = tab_id { + ClientTabIndexOrPaneId::TabIndex(tab_id) + } else if near_current_pane && pane_id.is_some() { + ClientTabIndexOrPaneId::PaneId(pane_id.unwrap()) + } else { + ClientTabIndexOrPaneId::ClientId(client_id) + }; + senders + .send_to_pty(PtyInstruction::SpawnInPlaceTerminal( + run_cmd, + name, + close_replaced_pane, + client_tab_index_or_paneid, + Some(NotificationEnd::new(completion_tx)), + )) + .with_context(err_context)?; }, Action::NewStackedPane { command: run_command, pane_name: name, near_current_pane, + tab_id, } => { let run_cmd = run_command .map(|cmd| TerminalAction::RunCommand(cmd.into())) .or_else(|| default_shell.clone()); - match pane_id { - Some(pane_id) if near_current_pane => { - senders - .send_to_pty(PtyInstruction::SpawnTerminal( - run_cmd, - name, - NewPanePlacement::Stacked { - pane_id_to_stack_under: Some(pane_id.into()), - borderless: None, - }, - false, - ClientTabIndexOrPaneId::PaneId(pane_id), - Some(NotificationEnd::new(completion_tx)), - false, // set_blocking - )) - .with_context(err_context)?; - }, - _ => { - senders - .send_to_pty(PtyInstruction::SpawnTerminal( - run_cmd, - name, - NewPanePlacement::Stacked { - pane_id_to_stack_under: None, - borderless: None, - }, - false, - ClientTabIndexOrPaneId::ClientId(client_id), - Some(NotificationEnd::new(completion_tx)), - false, // set_blocking - )) - .with_context(err_context)?; - }, - } + let (pane_placement, client_tab_index_or_paneid) = if let Some(tab_id) = tab_id { + ( + NewPanePlacement::Stacked { + pane_id_to_stack_under: None, + borderless: None, + }, + ClientTabIndexOrPaneId::TabIndex(tab_id), + ) + } else if near_current_pane && pane_id.is_some() { + let pane_id = pane_id.unwrap(); + ( + NewPanePlacement::Stacked { + pane_id_to_stack_under: Some(pane_id.into()), + borderless: None, + }, + ClientTabIndexOrPaneId::PaneId(pane_id), + ) + } else { + ( + NewPanePlacement::Stacked { + pane_id_to_stack_under: None, + borderless: None, + }, + ClientTabIndexOrPaneId::ClientId(client_id), + ) + }; + senders + .send_to_pty(PtyInstruction::SpawnTerminal( + run_cmd, + name, + pane_placement, + false, + client_tab_index_or_paneid, + Some(NotificationEnd::new(completion_tx)), + false, // set_blocking + )) + .with_context(err_context)?; }, Action::NewTiledPane { direction, @@ -871,11 +878,14 @@ pub(crate) fn route_action( pane_name: name, near_current_pane, borderless, + tab_id, } => { let run_cmd = run_command .map(|cmd| TerminalAction::RunCommand(cmd.into())) .or_else(|| default_shell.clone()); - let client_tab_index_or_paneid = if near_current_pane && pane_id.is_some() { + let client_tab_index_or_paneid = if let Some(tab_id) = tab_id { + ClientTabIndexOrPaneId::TabIndex(tab_id) + } else if near_current_pane && pane_id.is_some() { ClientTabIndexOrPaneId::PaneId(pane_id.unwrap()) } else { ClientTabIndexOrPaneId::ClientId(client_id) @@ -1296,6 +1306,7 @@ pub(crate) fn route_action( pane_name: name, skip_cache, cwd, + tab_id, } => { senders .send_to_screen(ScreenInstruction::NewTiledPluginPane( @@ -1305,6 +1316,7 @@ pub(crate) fn route_action( cwd, client_id, Some(NotificationEnd::new(completion_tx)), + tab_id, )) .with_context(err_context)?; }, @@ -1314,6 +1326,7 @@ pub(crate) fn route_action( skip_cache, cwd, coordinates: floating_pane_coordinates, + tab_id, } => { senders .send_to_screen(ScreenInstruction::NewFloatingPluginPane( @@ -1324,6 +1337,7 @@ pub(crate) fn route_action( floating_pane_coordinates, client_id, Some(NotificationEnd::new(completion_tx)), + tab_id, )) .with_context(err_context)?; }, @@ -1332,6 +1346,7 @@ pub(crate) fn route_action( pane_name: name, skip_cache, close_replaced_pane, + tab_id, } => { if let Some(pane_id) = pane_id { senders @@ -1343,6 +1358,7 @@ pub(crate) fn route_action( close_replaced_pane, client_id, Some(NotificationEnd::new(completion_tx)), + tab_id, )) .with_context(err_context)?; } else { @@ -1365,6 +1381,7 @@ pub(crate) fn route_action( should_open_in_place, close_replaced_pane, skip_cache, + tab_id, } => { senders .send_to_screen(ScreenInstruction::LaunchOrFocusPlugin( @@ -1377,6 +1394,7 @@ pub(crate) fn route_action( skip_cache, client_id, Some(NotificationEnd::new(completion_tx)), + tab_id, )) .with_context(err_context)?; }, @@ -1387,6 +1405,7 @@ pub(crate) fn route_action( close_replaced_pane, skip_cache, cwd, + tab_id, } => { senders .send_to_screen(ScreenInstruction::LaunchPlugin( @@ -1399,6 +1418,7 @@ pub(crate) fn route_action( cwd, client_id, Some(NotificationEnd::new(completion_tx)), + tab_id, )) .with_context(err_context)?; }, diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 0be349f0e..6f1bf70b6 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -547,6 +547,7 @@ pub enum ScreenInstruction { Option, ClientId, Option, + Option, // tab_id ), // Option is // optional pane title, bool is skip cache, Option is an optional cwd NewFloatingPluginPane( @@ -557,6 +558,7 @@ pub enum ScreenInstruction { Option, ClientId, Option, + Option, // tab_id ), // Option is an // optional pane title, bool // is skip cache, Option is an optional cwd @@ -568,6 +570,7 @@ pub enum ScreenInstruction { bool, ClientId, Option, + Option, // tab_id ), // Option is an // optional pane title, first bool is skip cache, second bool is close_replaced_pane StartOrReloadPluginPane(RunPluginOrAlias, Option, Option), @@ -601,6 +604,7 @@ pub enum ScreenInstruction { bool, ClientId, Option, + Option, // tab_id ), // bools are: should_float, move_to_focused_tab, should_open_in_place, close_replaced_pane, Option is the pane id to replace, bool following it is skip_cache LaunchPlugin( RunPluginOrAlias, @@ -612,6 +616,7 @@ pub enum ScreenInstruction { Option, ClientId, Option, + Option, // tab_id ), // bools are: should_float, should_open_in_place, close_replaced_pane, Option is the pane id to replace, Option is an optional cwd, bool after is skip_cache SuppressPane(PaneId, ClientId), UnsuppressPane(PaneId, bool), // bool -> should float if hidden @@ -5136,6 +5141,29 @@ pub(crate) fn screen_thread_main( } }, ClientTabIndexOrPaneId::TabIndex(tab_index) => { + // Some placements (directional split, stacked without a + // target pane) need a client_id to know which pane to + // split relative to. Only resolve one when required. + let needs_client_id = matches!( + new_pane_placement, + NewPanePlacement::Tiled { + direction: Some(_), + .. + } | NewPanePlacement::Stacked { + pane_id_to_stack_under: None, + .. + } + ); + let client_id = if needs_client_id { + screen + .active_tab_ids + .iter() + .find(|(_, tid)| **tid == tab_index) + .map(|(cid, _)| *cid) + .or_else(|| screen.active_tab_ids.keys().next().copied()) + } else { + None + }; if let Some(active_tab) = screen.tabs.get_mut(&tab_index) { active_tab.new_pane( pid, @@ -5144,7 +5172,7 @@ pub(crate) fn screen_thread_main( start_suppressed, true, new_pane_placement, - None, + client_id, blocking_notification, )?; if let Some(hold_for_command) = hold_for_command { @@ -7028,8 +7056,10 @@ pub(crate) fn screen_thread_main( cwd, client_id, completion_tx, + explicit_tab_id, ) => { - let tab_index = screen.active_tab_ids.values().next().unwrap_or(&1); + let tab_index = explicit_tab_id + .unwrap_or_else(|| *screen.active_tab_ids.values().next().unwrap_or(&1)); let size = Size::default(); let should_float = Some(false); let should_be_opened_in_place = false; @@ -7042,7 +7072,7 @@ pub(crate) fn screen_thread_main( false, // close_replaced_pane pane_title, run_plugin, - *tab_index, + tab_index, None, client_id, size, @@ -7061,36 +7091,41 @@ pub(crate) fn screen_thread_main( floating_pane_coordinates, client_id, completion_tx, - ) => match screen.active_tab_ids.values().next() { - Some(tab_index) => { - let size = Size::default(); - let should_float = Some(true); - let should_be_opened_in_place = false; - screen - .bus - .senders - .send_to_pty(PtyInstruction::FillPluginCwd( - should_float, - should_be_opened_in_place, - false, // close_replaced_pane - pane_title, - run_plugin, - *tab_index, - None, - client_id, - size, - skip_cache, - cwd, - None, - floating_pane_coordinates, - completion_tx, - ))?; - }, - None => { - log::error!( - "Could not find an active tab - is there at least 1 connected user?" - ); - }, + explicit_tab_id, + ) => { + let resolved_tab_index = + explicit_tab_id.or_else(|| screen.active_tab_ids.values().next().copied()); + match resolved_tab_index { + Some(tab_index) => { + let size = Size::default(); + let should_float = Some(true); + let should_be_opened_in_place = false; + screen + .bus + .senders + .send_to_pty(PtyInstruction::FillPluginCwd( + should_float, + should_be_opened_in_place, + false, // close_replaced_pane + pane_title, + run_plugin, + tab_index, + None, + client_id, + size, + skip_cache, + cwd, + None, + floating_pane_coordinates, + completion_tx, + ))?; + }, + None => { + log::error!( + "Could not find an active tab - is there at least 1 connected user?" + ); + }, + } }, ScreenInstruction::NewInPlacePluginPane( run_plugin, @@ -7100,36 +7135,41 @@ pub(crate) fn screen_thread_main( close_replaced_pane, client_id, completion_tx, - ) => match screen.active_tab_ids.values().next() { - Some(tab_index) => { - let size = Size::default(); - let should_float = None; - let should_be_in_place = true; - screen - .bus - .senders - .send_to_pty(PtyInstruction::FillPluginCwd( - should_float, - should_be_in_place, - close_replaced_pane, - pane_title, - run_plugin, - *tab_index, - Some(pane_id_to_replace), - client_id, - size, - skip_cache, - None, - None, - None, - completion_tx, - ))?; - }, - None => { - log::error!( - "Could not find an active tab - is there at least 1 connected user?" - ); - }, + explicit_tab_id, + ) => { + let resolved_tab_index = + explicit_tab_id.or_else(|| screen.active_tab_ids.values().next().copied()); + match resolved_tab_index { + Some(tab_index) => { + let size = Size::default(); + let should_float = None; + let should_be_in_place = true; + screen + .bus + .senders + .send_to_pty(PtyInstruction::FillPluginCwd( + should_float, + should_be_in_place, + close_replaced_pane, + pane_title, + run_plugin, + tab_index, + Some(pane_id_to_replace), + client_id, + size, + skip_cache, + None, + None, + None, + completion_tx, + ))?; + }, + None => { + log::error!( + "Could not find an active tab - is there at least 1 connected user?" + ); + }, + } }, ScreenInstruction::StartOrReloadPluginPane(run_plugin, pane_title, completion_tx) => { let tab_index = screen.active_tab_ids.values().next().unwrap_or(&1); @@ -7326,9 +7366,12 @@ pub(crate) fn screen_thread_main( skip_cache, client_id, mut completion_tx, + explicit_tab_id, ) => match pane_id_to_replace { Some(pane_id_to_replace) if should_open_in_place => { - match screen.active_tab_ids.values().next() { + let resolved_tab_index = + explicit_tab_id.or_else(|| screen.active_tab_ids.values().next().copied()); + match resolved_tab_index { Some(tab_index) => { let size = Size::default(); screen @@ -7340,7 +7383,7 @@ pub(crate) fn screen_thread_main( close_replaced_pane, None, run_plugin, - *tab_index, + tab_index, Some(pane_id_to_replace), client_id, size, @@ -7370,7 +7413,10 @@ pub(crate) fn screen_thread_main( .get(&client_id) .map(|tab_index| (*tab_index, client_id)) }); - match client_id_and_focused_tab { + let resolved_tab_and_client = explicit_tab_id + .and_then(|tid| client_id.map(|cid| (tid, cid))) + .or(client_id_and_focused_tab); + match resolved_tab_and_client { Some((tab_index, client_id)) => { if screen.focus_plugin_pane( &run_plugin, @@ -7420,35 +7466,40 @@ pub(crate) fn screen_thread_main( cwd, client_id, completion_tx, + explicit_tab_id, ) => match pane_id_to_replace { - Some(pane_id_to_replace) => match screen.active_tab_ids.values().next() { - Some(tab_index) => { - let size = Size::default(); - screen - .bus - .senders - .send_to_pty(PtyInstruction::FillPluginCwd( - Some(should_float), - should_open_in_place, - close_replaced_pane, - None, - run_plugin, - *tab_index, - Some(pane_id_to_replace), - client_id, - size, - skip_cache, - cwd, - None, - None, - completion_tx, - ))?; - }, - None => { - log::error!( - "Could not find an active tab - is there at least 1 connected user?" - ); - }, + Some(pane_id_to_replace) => { + let resolved_tab_index = + explicit_tab_id.or_else(|| screen.active_tab_ids.values().next().copied()); + match resolved_tab_index { + Some(tab_index) => { + let size = Size::default(); + screen + .bus + .senders + .send_to_pty(PtyInstruction::FillPluginCwd( + Some(should_float), + should_open_in_place, + close_replaced_pane, + None, + run_plugin, + tab_index, + Some(pane_id_to_replace), + client_id, + size, + skip_cache, + cwd, + None, + None, + completion_tx, + ))?; + }, + None => { + log::error!( + "Could not find an active tab - is there at least 1 connected user?" + ); + }, + } }, None => { let client_id = if screen.active_tab_ids.contains_key(&client_id) { @@ -7462,7 +7513,10 @@ pub(crate) fn screen_thread_main( .get(&client_id) .map(|tab_index| (*tab_index, client_id)) }); - match client_id_and_focused_tab { + let resolved_tab_and_client = explicit_tab_id + .and_then(|tid| client_id.map(|cid| (tid, cid))) + .or(client_id_and_focused_tab); + match resolved_tab_and_client { Some((tab_index, client_id)) => { screen .bus diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index 491beefb0..0897ec49d 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -3094,6 +3094,7 @@ pub fn send_cli_new_pane_action_with_default_parameters() { unblock_condition: None, near_current_pane: false, borderless: Some(false), + tab_id: None, }; send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -3147,6 +3148,7 @@ pub fn send_cli_new_pane_action_with_split_direction() { unblock_condition: None, near_current_pane: false, borderless: Some(false), + tab_id: None, }; send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -3200,6 +3202,7 @@ pub fn send_cli_new_pane_action_with_command_and_cwd() { unblock_condition: None, near_current_pane: false, borderless: Some(false), + tab_id: None, }; send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -3264,6 +3267,7 @@ pub fn send_cli_new_pane_action_with_floating_pane_and_coordinates() { unblock_condition: None, near_current_pane: false, borderless: Some(false), + tab_id: None, }; send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -3306,6 +3310,7 @@ pub fn send_cli_edit_action_with_default_parameters() { pinned: None, borderless: Some(false), near_current_pane: false, + tab_id: None, }; send_cli_action_to_server(&session_metadata, cli_edit_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -3348,6 +3353,7 @@ pub fn send_cli_edit_action_with_line_number() { pinned: None, borderless: Some(false), near_current_pane: false, + tab_id: None, }; send_cli_action_to_server(&session_metadata, cli_edit_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -3390,6 +3396,7 @@ pub fn send_cli_edit_action_with_split_direction() { pinned: None, borderless: Some(false), near_current_pane: false, + tab_id: None, }; send_cli_action_to_server(&session_metadata, cli_edit_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -3580,6 +3587,7 @@ pub fn send_cli_new_tab_action_default_params() { let new_tab_action = CliAction::NewTab { name: None, layout: None, + layout_string: None, layout_dir: None, cwd: None, initial_command: vec![], @@ -3627,6 +3635,7 @@ pub fn send_cli_new_tab_action_with_name_and_layout() { "{}/src/unit/fixtures/layout-with-three-panes.kdl", env!("CARGO_MANIFEST_DIR") ))), + layout_string: None, layout_dir: None, cwd: None, initial_command: vec![], @@ -3959,6 +3968,7 @@ pub fn send_cli_launch_or_focus_plugin_action() { url: "file:/path/to/fake/plugin".to_owned(), configuration: Default::default(), skip_plugin_cache: false, + tab_id: None, }; send_cli_action_to_server(&session_metadata, cli_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -4021,6 +4031,7 @@ pub fn send_cli_launch_or_focus_plugin_action_when_plugin_is_already_loaded() { url: "file:/path/to/fake/plugin".to_owned(), configuration: Default::default(), skip_plugin_cache: false, + tab_id: None, }; send_cli_action_to_server(&session_metadata, cli_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -4105,6 +4116,7 @@ pub fn send_cli_launch_or_focus_plugin_action_when_plugin_is_already_loaded_for_ url: "fixture_plugin_for_tests".to_owned(), configuration: Default::default(), skip_plugin_cache: false, + tab_id: None, }; send_cli_action_to_server(&session_metadata, cli_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -5097,6 +5109,7 @@ pub fn send_cli_new_pane_in_place_with_close_replaced_pane() { unblock_condition: None, near_current_pane: false, borderless: None, + tab_id: None, }; send_cli_action_to_server(&session_metadata, cli_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); @@ -5147,6 +5160,7 @@ pub fn send_cli_edit_in_place_with_close_replaced_pane() { pinned: None, near_current_pane: false, borderless: None, + tab_id: None, }; send_cli_action_to_server(&session_metadata, cli_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); @@ -5192,6 +5206,7 @@ pub fn send_cli_launch_or_focus_plugin_in_place_with_close_replaced_pane() { url: "file:/path/to/fake/plugin".to_owned(), configuration: Default::default(), skip_plugin_cache: false, + tab_id: None, }; send_cli_action_to_server(&session_metadata, cli_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); @@ -7663,3 +7678,402 @@ pub fn inactive_tab_plugins_get_fresh_state_on_activation() { active_tab.position ); } + +#[test] +pub fn send_cli_new_tab_action_with_layout_string() { + let size = Size { cols: 80, rows: 10 }; + let client_id = 10; + let mut initial_layout = TiledPaneLayout::default(); + initial_layout.children_split_direction = SplitDirection::Vertical; + initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; + let mut mock_screen = MockScreen::new(size); + let session_metadata = mock_screen.clone_session_metadata(); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); + let received_plugin_instructions = Arc::new(Mutex::new(vec![])); + let plugin_receiver = mock_screen.plugin_receiver.take().unwrap(); + let plugin_thread = log_actions_in_thread!( + received_plugin_instructions, + PluginInstruction::Exit, + plugin_receiver + ); + // Same layout as layout-with-three-panes.kdl but passed as a string + let new_tab_action = CliAction::NewTab { + name: None, + layout: None, + layout_string: Some("layout {\n pane\n pane\n pane\n}\n".into()), + layout_dir: None, + cwd: None, + initial_command: vec![], + initial_plugin: None, + close_on_exit: Default::default(), + start_suspended: Default::default(), + block_until_exit: false, + block_until_exit_success: false, + block_until_exit_failure: false, + }; + send_cli_action_to_server(&session_metadata, new_tab_action, client_id); + std::thread::sleep(std::time::Duration::from_millis(100)); + mock_screen.teardown(vec![plugin_thread, screen_thread]); + let new_tab_instruction = received_plugin_instructions + .lock() + .unwrap() + .iter() + .rev() + .find(|i| { + if let PluginInstruction::NewTab(..) = i { + return true; + } else { + return false; + } + }) + .unwrap() + .clone(); + let output = format!("{:#?}", new_tab_instruction); + // Normalize Windows path separators for cross-platform snapshot consistency + let output = output.replace("\\\\", "/"); + assert_snapshot!(output); +} + +#[test] +pub fn send_cli_new_tab_action_with_layout_string_and_name() { + let size = Size { cols: 80, rows: 10 }; + let client_id = 10; + let mut initial_layout = TiledPaneLayout::default(); + initial_layout.children_split_direction = SplitDirection::Vertical; + initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; + let mut mock_screen = MockScreen::new(size); + let session_metadata = mock_screen.clone_session_metadata(); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); + let received_plugin_instructions = Arc::new(Mutex::new(vec![])); + let plugin_receiver = mock_screen.plugin_receiver.take().unwrap(); + let plugin_thread = log_actions_in_thread!( + received_plugin_instructions, + PluginInstruction::Exit, + plugin_receiver + ); + let new_tab_action = CliAction::NewTab { + name: Some("my-string-layout-tab".into()), + layout: None, + layout_string: Some("layout {\n pane\n pane\n pane\n}\n".into()), + layout_dir: None, + cwd: None, + initial_command: vec![], + initial_plugin: None, + close_on_exit: Default::default(), + start_suspended: Default::default(), + block_until_exit: false, + block_until_exit_success: false, + block_until_exit_failure: false, + }; + send_cli_action_to_server(&session_metadata, new_tab_action, client_id); + std::thread::sleep(std::time::Duration::from_millis(100)); + mock_screen.teardown(vec![plugin_thread, screen_thread]); + let new_tab_instruction = received_plugin_instructions + .lock() + .unwrap() + .iter() + .rev() + .find(|i| { + if let PluginInstruction::NewTab(..) = i { + return true; + } else { + return false; + } + }) + .unwrap() + .clone(); + let output = format!("{:#?}", new_tab_instruction); + // Normalize Windows path separators for cross-platform snapshot consistency + let output = output.replace("\\\\", "/"); + assert_snapshot!(output); +} + +#[test] +pub fn send_cli_new_pane_action_with_tab_id() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 10; + let mut mock_screen = MockScreen::new(size); + let pty_receiver = mock_screen.pty_receiver.take().unwrap(); + let session_metadata = mock_screen.clone_session_metadata(); + let mut initial_layout = TiledPaneLayout::default(); + initial_layout.children_split_direction = SplitDirection::Vertical; + initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); + let received_pty_instructions = Arc::new(Mutex::new(vec![])); + let pty_thread = log_actions_in_thread!( + received_pty_instructions, + PtyInstruction::Exit, + pty_receiver + ); + let cli_new_pane_action = CliAction::NewPane { + direction: Some(Direction::Right), + command: vec![], + plugin: None, + cwd: None, + floating: false, + in_place: false, + close_replaced_pane: false, + name: None, + close_on_exit: false, + start_suspended: false, + configuration: None, + skip_plugin_cache: false, + x: None, + y: None, + width: None, + height: None, + pinned: None, + stacked: false, + blocking: false, + block_until_exit_success: false, + block_until_exit_failure: false, + block_until_exit: false, + unblock_condition: None, + near_current_pane: false, + borderless: Some(false), + tab_id: Some(0), + }; + send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id); + std::thread::sleep(std::time::Duration::from_millis(100)); + mock_screen.teardown(vec![pty_thread, screen_thread]); + let pty_instructions = received_pty_instructions.lock().unwrap(); + // Verify that the PTY instruction uses TabIndex(0) instead of ClientId + let pty_debug = format!("{:?}", *pty_instructions); + assert!( + pty_debug.contains("TabIndex(0)"), + "Expected TabIndex(0) in PTY instructions, got: {}", + pty_debug + ); +} + +#[test] +pub fn send_cli_new_floating_pane_action_with_tab_id() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 10; + let mut mock_screen = MockScreen::new(size); + let pty_receiver = mock_screen.pty_receiver.take().unwrap(); + let session_metadata = mock_screen.clone_session_metadata(); + let mut initial_layout = TiledPaneLayout::default(); + initial_layout.children_split_direction = SplitDirection::Vertical; + initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); + let received_pty_instructions = Arc::new(Mutex::new(vec![])); + let pty_thread = log_actions_in_thread!( + received_pty_instructions, + PtyInstruction::Exit, + pty_receiver + ); + let cli_new_pane_action = CliAction::NewPane { + direction: None, + command: vec![], + plugin: None, + cwd: None, + floating: true, + in_place: false, + close_replaced_pane: false, + name: None, + close_on_exit: false, + start_suspended: false, + configuration: None, + skip_plugin_cache: false, + x: None, + y: None, + width: None, + height: None, + pinned: None, + stacked: false, + blocking: false, + block_until_exit_success: false, + block_until_exit_failure: false, + block_until_exit: false, + unblock_condition: None, + near_current_pane: false, + borderless: None, + tab_id: Some(0), + }; + send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id); + std::thread::sleep(std::time::Duration::from_millis(100)); + mock_screen.teardown(vec![pty_thread, screen_thread]); + let pty_instructions = received_pty_instructions.lock().unwrap(); + let pty_debug = format!("{:?}", *pty_instructions); + assert!( + pty_debug.contains("TabIndex(0)"), + "Expected TabIndex(0) in PTY instructions for floating pane, got: {}", + pty_debug + ); +} + +#[test] +pub fn send_cli_edit_action_with_tab_id() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 10; + let mut mock_screen = MockScreen::new(size); + let pty_receiver = mock_screen.pty_receiver.take().unwrap(); + let session_metadata = mock_screen.clone_session_metadata(); + let mut initial_layout = TiledPaneLayout::default(); + initial_layout.children_split_direction = SplitDirection::Vertical; + initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); + let received_pty_instructions = Arc::new(Mutex::new(vec![])); + let pty_thread = log_actions_in_thread!( + received_pty_instructions, + PtyInstruction::Exit, + pty_receiver + ); + let cli_edit_action = CliAction::Edit { + file: PathBuf::from("/tmp/test.rs"), + direction: None, + line_number: None, + floating: false, + in_place: false, + close_replaced_pane: false, + cwd: None, + x: None, + y: None, + width: None, + height: None, + pinned: None, + near_current_pane: false, + borderless: None, + tab_id: Some(0), + }; + send_cli_action_to_server(&session_metadata, cli_edit_action, client_id); + std::thread::sleep(std::time::Duration::from_millis(100)); + mock_screen.teardown(vec![pty_thread, screen_thread]); + let pty_instructions = received_pty_instructions.lock().unwrap(); + let pty_debug = format!("{:?}", *pty_instructions); + assert!( + pty_debug.contains("TabIndex(0)"), + "Expected TabIndex(0) in PTY instructions for edit, got: {}", + pty_debug + ); +} + +#[test] +pub fn send_cli_new_pane_action_with_tab_id_and_direction() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 10; + let mut mock_screen = MockScreen::new(size); + let pty_receiver = mock_screen.pty_receiver.take().unwrap(); + let session_metadata = mock_screen.clone_session_metadata(); + let mut initial_layout = TiledPaneLayout::default(); + initial_layout.children_split_direction = SplitDirection::Vertical; + initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); + let received_pty_instructions = Arc::new(Mutex::new(vec![])); + let pty_thread = log_actions_in_thread!( + received_pty_instructions, + PtyInstruction::Exit, + pty_receiver + ); + let cli_new_pane_action = CliAction::NewPane { + direction: Some(Direction::Right), + command: vec![], + plugin: None, + cwd: None, + floating: false, + in_place: false, + close_replaced_pane: false, + name: None, + close_on_exit: false, + start_suspended: false, + configuration: None, + skip_plugin_cache: false, + x: None, + y: None, + width: None, + height: None, + pinned: None, + stacked: false, + blocking: false, + block_until_exit_success: false, + block_until_exit_failure: false, + block_until_exit: false, + unblock_condition: None, + near_current_pane: false, + borderless: Some(false), + tab_id: Some(0), + }; + send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id); + std::thread::sleep(std::time::Duration::from_millis(100)); + mock_screen.teardown(vec![pty_thread, screen_thread]); + let pty_instructions = received_pty_instructions.lock().unwrap(); + let pty_debug = format!("{:?}", *pty_instructions); + assert!( + pty_debug.contains("TabIndex(0)"), + "Expected TabIndex(0) in PTY instructions with direction, got: {}", + pty_debug + ); +} + +#[test] +pub fn send_cli_new_pane_action_with_tab_id_and_stacked() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 10; + let mut mock_screen = MockScreen::new(size); + let pty_receiver = mock_screen.pty_receiver.take().unwrap(); + let session_metadata = mock_screen.clone_session_metadata(); + let mut initial_layout = TiledPaneLayout::default(); + initial_layout.children_split_direction = SplitDirection::Vertical; + initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); + let received_pty_instructions = Arc::new(Mutex::new(vec![])); + let pty_thread = log_actions_in_thread!( + received_pty_instructions, + PtyInstruction::Exit, + pty_receiver + ); + let cli_new_pane_action = CliAction::NewPane { + direction: None, + command: vec!["ls".into()], + plugin: None, + cwd: None, + floating: false, + in_place: false, + close_replaced_pane: false, + name: None, + close_on_exit: false, + start_suspended: false, + configuration: None, + skip_plugin_cache: false, + x: None, + y: None, + width: None, + height: None, + pinned: None, + stacked: true, + blocking: false, + block_until_exit_success: false, + block_until_exit_failure: false, + block_until_exit: false, + unblock_condition: None, + near_current_pane: false, + borderless: None, + tab_id: Some(0), + }; + send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id); + std::thread::sleep(std::time::Duration::from_millis(100)); + mock_screen.teardown(vec![pty_thread, screen_thread]); + let pty_instructions = received_pty_instructions.lock().unwrap(); + let pty_debug = format!("{:?}", *pty_instructions); + assert!( + pty_debug.contains("TabIndex(0)"), + "Expected TabIndex(0) in PTY instructions with stacked, got: {}", + pty_debug + ); +} diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_layout_string.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_layout_string.snap new file mode 100644 index 000000000..b12db36c0 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_layout_string.snap @@ -0,0 +1,119 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +expression: "format!(\"{:#?}\", new_tab_instruction)" +--- +NewTab( + None, + None, + Some( + TiledPaneLayout { + children_split_direction: Horizontal, + name: None, + children: [ + TiledPaneLayout { + children_split_direction: Horizontal, + name: None, + children: [], + split_size: None, + run: Some( + Cwd( + "./.", + ), + ), + borderless: None, + 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, + default_fg: None, + default_bg: None, + }, + TiledPaneLayout { + children_split_direction: Horizontal, + name: None, + children: [], + split_size: None, + run: Some( + Cwd( + "./.", + ), + ), + borderless: None, + 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, + default_fg: None, + default_bg: None, + }, + TiledPaneLayout { + children_split_direction: Horizontal, + name: None, + children: [], + split_size: None, + run: Some( + Cwd( + "./.", + ), + ), + borderless: None, + 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, + default_fg: None, + default_bg: None, + }, + ], + split_size: None, + run: Some( + Cwd( + ".", + ), + ), + borderless: None, + 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, + default_fg: None, + default_bg: None, + }, + ), + [], + 1, + None, + false, + true, + ( + 10, + false, + ), + Some( + NotificationEnd { + channel: None, + exit_status: None, + unblock_condition: None, + affected_pane_id: None, + affected_tab_id: None, + error_message: None, + stdout_message: None, + }, + ), +) diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_layout_string_and_name.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_layout_string_and_name.snap new file mode 100644 index 000000000..b12db36c0 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_layout_string_and_name.snap @@ -0,0 +1,119 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +expression: "format!(\"{:#?}\", new_tab_instruction)" +--- +NewTab( + None, + None, + Some( + TiledPaneLayout { + children_split_direction: Horizontal, + name: None, + children: [ + TiledPaneLayout { + children_split_direction: Horizontal, + name: None, + children: [], + split_size: None, + run: Some( + Cwd( + "./.", + ), + ), + borderless: None, + 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, + default_fg: None, + default_bg: None, + }, + TiledPaneLayout { + children_split_direction: Horizontal, + name: None, + children: [], + split_size: None, + run: Some( + Cwd( + "./.", + ), + ), + borderless: None, + 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, + default_fg: None, + default_bg: None, + }, + TiledPaneLayout { + children_split_direction: Horizontal, + name: None, + children: [], + split_size: None, + run: Some( + Cwd( + "./.", + ), + ), + borderless: None, + 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, + default_fg: None, + default_bg: None, + }, + ], + split_size: None, + run: Some( + Cwd( + ".", + ), + ), + borderless: None, + 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, + default_fg: None, + default_bg: None, + }, + ), + [], + 1, + None, + false, + true, + ( + 10, + false, + ), + Some( + NotificationEnd { + channel: None, + exit_status: None, + unblock_condition: None, + affected_pane_id: None, + affected_tab_id: None, + error_message: None, + stdout_message: None, + }, + ), +) diff --git a/zellij-utils/assets/prost_ipc/client_server_contract.rs b/zellij-utils/assets/prost_ipc/client_server_contract.rs index d1425cfc3..5ae8460b1 100644 --- a/zellij-utils/assets/prost_ipc/client_server_contract.rs +++ b/zellij-utils/assets/prost_ipc/client_server_contract.rs @@ -808,6 +808,8 @@ pub struct EditFileAction { pub near_current_pane: bool, #[prost(bool, tag="8")] pub close_replaced_pane: bool, + #[prost(uint32, optional, tag="9")] + pub tab_id: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -820,6 +822,8 @@ pub struct NewFloatingPaneAction { pub coordinates: ::core::option::Option, #[prost(bool, tag="7")] pub near_current_pane: bool, + #[prost(uint32, optional, tag="8")] + pub tab_id: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -834,6 +838,8 @@ pub struct NewTiledPaneAction { pub near_current_pane: bool, #[prost(bool, optional, tag="8")] pub borderless: ::core::option::Option, + #[prost(uint32, optional, tag="9")] + pub tab_id: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -848,6 +854,8 @@ pub struct NewInPlacePaneAction { pub pane_id_to_replace: ::core::option::Option, #[prost(bool, tag="5")] pub close_replaced_pane: bool, + #[prost(uint32, optional, tag="6")] + pub tab_id: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -858,6 +866,8 @@ pub struct NewStackedPaneAction { pub pane_name: ::core::option::Option<::prost::alloc::string::String>, #[prost(bool, tag="3")] pub near_current_pane: bool, + #[prost(uint32, optional, tag="4")] + pub tab_id: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -872,6 +882,8 @@ pub struct NewBlockingPaneAction { pub unblock_condition: ::core::option::Option, #[prost(bool, tag="5")] pub near_current_pane: bool, + #[prost(uint32, optional, tag="6")] + pub tab_id: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -950,6 +962,8 @@ pub struct LaunchOrFocusPluginAction { pub skip_cache: bool, #[prost(bool, tag="6")] pub close_replaced_pane: bool, + #[prost(uint32, optional, tag="7")] + pub tab_id: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -966,6 +980,8 @@ pub struct LaunchPluginAction { pub cwd: ::core::option::Option<::prost::alloc::string::String>, #[prost(bool, tag="6")] pub close_replaced_pane: bool, + #[prost(uint32, optional, tag="7")] + pub tab_id: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -1008,6 +1024,8 @@ pub struct NewTiledPluginPaneAction { pub skip_cache: bool, #[prost(string, optional, tag="4")] pub cwd: ::core::option::Option<::prost::alloc::string::String>, + #[prost(uint32, optional, tag="5")] + pub tab_id: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -1022,6 +1040,8 @@ pub struct NewFloatingPluginPaneAction { pub cwd: ::core::option::Option<::prost::alloc::string::String>, #[prost(message, optional, tag="5")] pub coordinates: ::core::option::Option, + #[prost(uint32, optional, tag="6")] + pub tab_id: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -1034,6 +1054,8 @@ pub struct NewInPlacePluginPaneAction { pub skip_cache: bool, #[prost(bool, tag="4")] pub close_replaced_pane: bool, + #[prost(uint32, optional, tag="5")] + pub tab_id: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs index 121172654..f02ace984 100644 --- a/zellij-utils/src/cli.rs +++ b/zellij-utils/src/cli.rs @@ -59,6 +59,12 @@ pub struct CliArgs { #[clap(short, long, value_parser, overrides_with = "layout")] pub layout: Option, + /// Raw KDL layout string to use directly (instead of a file path) + /// if inside a session (or using the --session flag) will be added to the session as a new tab + /// or tabs, otherwise will start a new session + #[clap(long, value_parser, conflicts_with_all = &["layout", "new-session-with-layout"])] + pub layout_string: Option, + /// Name of a predefined layout inside the layout directory or the path to a layout file /// Will always start a new session, even if inside an existing session #[clap(short, long, value_parser, overrides_with = "new_session_with_layout")] @@ -515,6 +521,14 @@ pub enum Sessions { /// mouse) #[clap(short, long, value_parser)] borderless: Option, + /// Target a specific tab by ID + #[clap( + long, + value_parser, + conflicts_with("near-current-pane"), + conflicts_with("in-place") + )] + tab_id: Option, }, /// Load a plugin /// Returns: Created pane ID (format: plugin_) @@ -575,6 +589,9 @@ pub enum Sessions { /// mouse) #[clap(short, long, value_parser)] borderless: Option, + /// Target a specific tab by ID + #[clap(long, value_parser, conflicts_with("in-place"))] + tab_id: Option, }, /// Edit file with default $EDITOR / $VISUAL /// Returns: Created pane ID (format: terminal_) @@ -641,6 +658,14 @@ pub enum Sessions { /// mouse) #[clap(short, long, value_parser)] borderless: Option, + /// Target a specific tab by ID + #[clap( + long, + value_parser, + conflicts_with("near-current-pane"), + conflicts_with("in-place") + )] + tab_id: Option, }, ConvertConfig { old_config_file: PathBuf, @@ -1010,6 +1035,14 @@ pub enum CliAction { /// mouse) #[clap(long, value_parser)] borderless: Option, + /// Target a specific tab by ID + #[clap( + long, + value_parser, + conflicts_with("near-current-pane"), + conflicts_with("in-place") + )] + tab_id: Option, }, /// Open the specified file in a new zellij pane with your default EDITOR /// Returns: Created pane ID (format: terminal_) @@ -1075,6 +1108,14 @@ pub enum CliAction { /// mouse) #[clap(short, long, value_parser)] borderless: Option, + /// Target a specific tab by ID + #[clap( + long, + value_parser, + conflicts_with("near-current-pane"), + conflicts_with("in-place") + )] + tab_id: Option, }, /// Switch input mode of all connected clients [locked|pane|tab|resize|move|search|session] SwitchMode { @@ -1187,9 +1228,13 @@ pub enum CliAction { /// Returns: The created tab's ID as a single number on stdout NewTab { /// Layout to use for the new tab - #[clap(short, long, value_parser)] + #[clap(short, long, value_parser, conflicts_with = "layout-string")] layout: Option, + /// Raw KDL layout string to use directly (instead of a layout file path) + #[clap(long, value_parser, conflicts_with = "layout")] + layout_string: Option, + /// Default folder to look for layouts #[clap(long, value_parser, requires("layout"))] layout_dir: Option, @@ -1292,8 +1337,16 @@ pub enum CliAction { /// Override the layout of the active tab OverrideLayout { /// Path to the layout file - #[clap(value_parser)] - layout: PathBuf, + #[clap( + value_parser, + required_unless_present = "layout-string", + conflicts_with = "layout-string" + )] + layout: Option, + + /// Raw KDL layout string to use directly (instead of a layout file path) + #[clap(long, value_parser, conflicts_with = "layout")] + layout_string: Option, /// Default folder to look for layouts #[clap(long, value_parser)] @@ -1341,6 +1394,9 @@ pub enum CliAction { configuration: Option, #[clap(short, long, value_parser)] skip_plugin_cache: bool, + /// Target a specific tab by ID + #[clap(long, value_parser, conflicts_with("in-place"))] + tab_id: Option, }, /// Returns: Plugin pane ID (format: plugin_) LaunchPlugin { @@ -1362,6 +1418,9 @@ pub enum CliAction { configuration: Option, #[clap(short, long, value_parser)] skip_plugin_cache: bool, + /// Target a specific tab by ID + #[clap(long, value_parser, conflicts_with("in-place"))] + tab_id: Option, }, RenameSession { name: String, @@ -1573,8 +1632,11 @@ tail -f /tmp/my-live-logfile | zellij action pipe --name logs --plugin https://e #[clap(long)] pane_id: Option, /// Layout to apply when switching to the session (relative paths start at layout-dir) - #[clap(short, long, value_parser)] + #[clap(short, long, value_parser, conflicts_with = "layout-string")] layout: Option, + /// Raw KDL layout string to use directly + #[clap(long, value_parser, conflicts_with = "layout")] + layout_string: Option, /// Default folder to look for layouts #[clap(long, value_parser, requires("layout"))] layout_dir: Option, diff --git a/zellij-utils/src/client_server_contract/common_types.proto b/zellij-utils/src/client_server_contract/common_types.proto index 6a6f211c3..e610eb2a6 100644 --- a/zellij-utils/src/client_server_contract/common_types.proto +++ b/zellij-utils/src/client_server_contract/common_types.proto @@ -454,6 +454,7 @@ message EditFileAction { optional FloatingPaneCoordinates coordinates = 6; bool near_current_pane = 7; bool close_replaced_pane = 8; + optional uint32 tab_id = 9; } message NewFloatingPaneAction { @@ -461,6 +462,7 @@ message NewFloatingPaneAction { optional string pane_name = 2; optional FloatingPaneCoordinates coordinates = 3; bool near_current_pane = 7; + optional uint32 tab_id = 8; } message NewTiledPaneAction { @@ -469,6 +471,7 @@ message NewTiledPaneAction { optional string pane_name = 3; bool near_current_pane = 7; optional bool borderless = 8; + optional uint32 tab_id = 9; } message NewInPlacePaneAction { @@ -477,12 +480,14 @@ message NewInPlacePaneAction { bool near_current_pane = 3; optional PaneId pane_id_to_replace = 4; bool close_replaced_pane = 5; + optional uint32 tab_id = 6; } message NewStackedPaneAction { optional RunCommandAction command = 1; optional string pane_name = 2; bool near_current_pane = 3; + optional uint32 tab_id = 4; } message NewBlockingPaneAction { @@ -491,6 +496,7 @@ message NewBlockingPaneAction { optional RunCommandAction command = 3; optional UnblockCondition unblock_condition = 4; bool near_current_pane = 5; + optional uint32 tab_id = 6; } message PaneNameInputAction { @@ -538,6 +544,7 @@ message LaunchOrFocusPluginAction { bool should_open_in_place = 4; bool skip_cache = 5; bool close_replaced_pane = 6; + optional uint32 tab_id = 7; } message LaunchPluginAction { @@ -547,6 +554,7 @@ message LaunchPluginAction { bool skip_cache = 4; optional string cwd = 5; bool close_replaced_pane = 6; + optional uint32 tab_id = 7; } message MouseEventAction { @@ -574,6 +582,7 @@ message NewTiledPluginPaneAction { optional string pane_name = 2; bool skip_cache = 3; optional string cwd = 4; + optional uint32 tab_id = 5; } message NewFloatingPluginPaneAction { @@ -582,6 +591,7 @@ message NewFloatingPluginPaneAction { bool skip_cache = 3; optional string cwd = 4; optional FloatingPaneCoordinates coordinates = 5; + optional uint32 tab_id = 6; } message NewInPlacePluginPaneAction { @@ -589,6 +599,7 @@ message NewInPlacePluginPaneAction { optional string pane_name = 2; bool skip_cache = 3; bool close_replaced_pane = 4; + optional uint32 tab_id = 5; } message StartOrReloadPluginAction { diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index 6dda2b7e3..0ce1c5071 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -232,6 +232,7 @@ pub enum Action { command: Option, unblock_condition: Option, near_current_pane: bool, + tab_id: Option, }, /// Open the file in a new pane using the default editor /// Returns: Created pane ID (format: terminal_) @@ -244,6 +245,7 @@ pub enum Action { start_suppressed: bool, coordinates: Option, near_current_pane: bool, + tab_id: Option, }, /// Open a new floating pane /// Returns: Created pane ID (format: terminal_ or plugin_) @@ -252,6 +254,7 @@ pub enum Action { pane_name: Option, coordinates: Option, near_current_pane: bool, + tab_id: Option, }, /// Open a new tiled (embedded, non-floating) pane /// Returns: Created pane ID (format: terminal_ or plugin_) @@ -261,6 +264,7 @@ pub enum Action { pane_name: Option, near_current_pane: bool, borderless: Option, + tab_id: Option, }, /// Open a new pane in place of the focused one, suppressing it instead /// Returns: Created pane ID (format: terminal_ or plugin_) @@ -270,12 +274,14 @@ pub enum Action { near_current_pane: bool, pane_id_to_replace: Option, close_replaced_pane: bool, + tab_id: Option, }, /// Returns: Created pane ID (format: terminal_ or plugin_) NewStackedPane { command: Option, pane_name: Option, near_current_pane: bool, + tab_id: Option, }, /// Embed focused pane in tab if floating or float focused pane if embedded TogglePaneEmbedOrFloating, @@ -363,6 +369,7 @@ pub enum Action { should_open_in_place: bool, close_replaced_pane: bool, skip_cache: bool, + tab_id: Option, }, /// Returns: Plugin pane ID (format: plugin_) LaunchPlugin { @@ -372,6 +379,7 @@ pub enum Action { close_replaced_pane: bool, skip_cache: bool, cwd: Option, + tab_id: Option, }, MouseEvent { event: MouseEvent, @@ -416,6 +424,7 @@ pub enum Action { pane_name: Option, skip_cache: bool, cwd: Option, + tab_id: Option, }, /// Returns: Created pane ID (format: plugin_) NewFloatingPluginPane { @@ -424,6 +433,7 @@ pub enum Action { skip_cache: bool, cwd: Option, coordinates: Option, + tab_id: Option, }, /// Returns: Created pane ID (format: plugin_) NewInPlacePluginPane { @@ -431,6 +441,7 @@ pub enum Action { pane_name: Option, skip_cache: bool, close_replaced_pane: bool, + tab_id: Option, }, StartOrReloadPlugin { plugin: RunPluginOrAlias, @@ -1016,6 +1027,7 @@ impl Action { unblock_condition, near_current_pane, borderless, + tab_id, } => { let current_dir = get_current_dir(); // cwd should only be specified in a plugin alias if it was explicitly given to us, @@ -1087,6 +1099,7 @@ impl Action { command, unblock_condition, near_current_pane, + tab_id, }]) } else if let Some(plugin) = plugin { let plugin = match RunPluginLocation::parse(&plugin, cwd.clone()) { @@ -1118,6 +1131,7 @@ impl Action { coordinates: FloatingPaneCoordinates::new( x, y, width, height, pinned, borderless, ), + tab_id, }]) } else if in_place { Ok(vec![Action::NewInPlacePluginPane { @@ -1125,6 +1139,7 @@ impl Action { pane_name: name, skip_cache: skip_plugin_cache, close_replaced_pane, + tab_id, }]) } else { // it is intentional that a new tiled plugin pane cannot include a @@ -1140,6 +1155,7 @@ impl Action { pane_name: name, skip_cache: skip_plugin_cache, cwd, + tab_id, }]) } } else if !command.is_empty() { @@ -1164,6 +1180,7 @@ impl Action { x, y, width, height, pinned, borderless, ), near_current_pane, + tab_id, }]) } else if in_place { Ok(vec![Action::NewInPlacePane { @@ -1172,12 +1189,14 @@ impl Action { near_current_pane, pane_id_to_replace: None, // TODO: support this close_replaced_pane, + tab_id, }]) } else if stacked { Ok(vec![Action::NewStackedPane { command: Some(run_command_action), pane_name: name, near_current_pane, + tab_id, }]) } else { Ok(vec![Action::NewTiledPane { @@ -1186,6 +1205,7 @@ impl Action { pane_name: name, near_current_pane, borderless, + tab_id, }]) } } else { @@ -1197,6 +1217,7 @@ impl Action { x, y, width, height, pinned, borderless, ), near_current_pane, + tab_id, }]) } else if in_place { Ok(vec![Action::NewInPlacePane { @@ -1205,12 +1226,14 @@ impl Action { near_current_pane, pane_id_to_replace: None, // TODO: support this close_replaced_pane, + tab_id, }]) } else if stacked { Ok(vec![Action::NewStackedPane { command: None, pane_name: name, near_current_pane, + tab_id, }]) } else { Ok(vec![Action::NewTiledPane { @@ -1219,6 +1242,7 @@ impl Action { pane_name: name, near_current_pane, borderless, + tab_id, }]) } } @@ -1238,6 +1262,7 @@ impl Action { pinned, near_current_pane, borderless, + tab_id, } => { let mut file = file; let current_dir = get_current_dir(); @@ -1261,6 +1286,7 @@ impl Action { x, y, width, height, pinned, borderless, ), near_current_pane, + tab_id, }]) }, CliAction::SwitchMode { input_mode } => Ok(vec![Action::SwitchToMode { input_mode }]), @@ -1357,6 +1383,7 @@ impl Action { CliAction::NewTab { name, layout, + layout_string, layout_dir, cwd, initial_command, @@ -1421,35 +1448,16 @@ impl Action { } else { None }; - if let Some(layout_path) = layout { - let layout_dir = layout_dir - .or_else(|| config.and_then(|c| c.options.layout_dir)) - .or_else(|| get_layout_dir(find_default_config_dir())); - - let mut should_start_layout_commands_suspended = false; - let (path_to_raw_layout, raw_layout, swap_layouts) = if let Some(layout_url) = - layout_path.to_str().and_then(|l| { - if l.starts_with("http://") || l.starts_with("https://") { - Some(l) - } else { - None - } - }) { - should_start_layout_commands_suspended = true; - ( - layout_url.to_owned(), - Layout::stringified_from_url(layout_url) - .map_err(|e| format!("Failed to load layout: {}", e))?, - None, - ) - } else { - Layout::stringified_from_path_or_default(Some(&layout_path), layout_dir) - .map_err(|e| format!("Failed to load layout: {}", e))? - }; + if let Some(raw_layout) = layout_string { + let layout_source_name = "layout-string".to_owned(); + let path_to_raw_layout = layout_source_name.clone(); + let swap_layouts: Option<(String, String)> = None; + let should_start_layout_commands_suspended = false; + let raw_layout_for_error = raw_layout.clone(); let mut layout = Layout::from_str(&raw_layout, path_to_raw_layout, swap_layouts.as_ref().map(|(f, p)| (f.as_str(), p.as_str())), cwd).map_err(|e| { let stringified_error = match e { ConfigError::KdlError(kdl_error) => { - let error = kdl_error.add_src(layout_path.as_path().as_os_str().to_string_lossy().to_string(), String::from(raw_layout)); + let error = kdl_error.add_src(layout_source_name.clone(), raw_layout_for_error); let report: Report = error.into(); format!("{:?}", report) } @@ -1466,7 +1474,123 @@ impl Action { }; let kdl_error = KdlError { error_message, - src: Some(NamedSource::new(layout_path.as_path().as_os_str().to_string_lossy().to_string(), String::from(raw_layout))), + src: Some(NamedSource::new(layout_source_name.clone(), raw_layout_for_error)), + offset: Some(kdl_error.span.offset()), + len: Some(kdl_error.span.len()), + help_message: None, + }; + let report: Report = kdl_error.into(); + format!("{:?}", report) + }, + e => format!("{}", e) + }; + stringified_error + })?; + if should_start_layout_commands_suspended { + layout.recursively_add_start_suspended_including_template(Some(true)); + } + let mut tabs = layout.tabs(); + if !tabs.is_empty() { + let swap_tiled_layouts = Some(layout.swap_tiled_layouts.clone()); + let swap_floating_layouts = Some(layout.swap_floating_layouts.clone()); + let mut new_tab_actions = vec![]; + let mut has_focused_tab = tabs + .iter() + .any(|(_, layout, _)| layout.focus.unwrap_or(false)); + for (tab_name, layout, floating_panes_layout) in tabs.drain(..) { + let name = tab_name.or_else(|| name.clone()); + let should_change_focus_to_new_tab = + layout.focus.unwrap_or_else(|| { + if !has_focused_tab { + has_focused_tab = true; + true + } else { + false + } + }); + new_tab_actions.push(Action::NewTab { + tiled_layout: Some(layout), + floating_layouts: floating_panes_layout, + swap_tiled_layouts: swap_tiled_layouts.clone(), + swap_floating_layouts: swap_floating_layouts.clone(), + tab_name: name, + should_change_focus_to_new_tab, + cwd: None, + initial_panes: initial_panes.clone(), + first_pane_unblock_condition, + }); + } + Ok(new_tab_actions) + } else { + let swap_tiled_layouts = Some(layout.swap_tiled_layouts.clone()); + let swap_floating_layouts = Some(layout.swap_floating_layouts.clone()); + let (layout, floating_panes_layout) = layout.new_tab(); + let should_change_focus_to_new_tab = true; + Ok(vec![Action::NewTab { + tiled_layout: Some(layout), + floating_layouts: floating_panes_layout, + swap_tiled_layouts, + swap_floating_layouts, + tab_name: name, + should_change_focus_to_new_tab, + cwd: None, + initial_panes, + first_pane_unblock_condition, + }]) + } + } else if let Some(layout_path) = layout { + let layout_dir = layout_dir + .or_else(|| config.and_then(|c| c.options.layout_dir)) + .or_else(|| get_layout_dir(find_default_config_dir())); + + let mut should_start_layout_commands_suspended = false; + let layout_source_name; + let (path_to_raw_layout, raw_layout, swap_layouts) = if let Some(layout_url) = + layout_path.to_str().and_then(|l| { + if l.starts_with("http://") || l.starts_with("https://") { + Some(l) + } else { + None + } + }) { + should_start_layout_commands_suspended = true; + layout_source_name = layout_url.to_owned(); + ( + layout_url.to_owned(), + Layout::stringified_from_url(layout_url) + .map_err(|e| format!("Failed to load layout: {}", e))?, + None, + ) + } else { + layout_source_name = layout_path + .as_path() + .as_os_str() + .to_string_lossy() + .to_string(); + Layout::stringified_from_path_or_default(Some(&layout_path), layout_dir) + .map_err(|e| format!("Failed to load layout: {}", e))? + }; + let mut layout = Layout::from_str(&raw_layout, path_to_raw_layout, swap_layouts.as_ref().map(|(f, p)| (f.as_str(), p.as_str())), cwd).map_err(|e| { + let stringified_error = match e { + ConfigError::KdlError(kdl_error) => { + let error = kdl_error.add_src(layout_source_name.clone(), String::from(raw_layout)); + let report: Report = error.into(); + format!("{:?}", report) + } + ConfigError::KdlDeserializationError(kdl_error) => { + let error_message = match kdl_error.kind { + kdl::KdlErrorKind::Context("valid node terminator") => { + format!("Failed to deserialize KDL node. \nPossible reasons:\n{}\n{}\n{}\n{}", + "- Missing `;` after a node name, eg. { node; another_node; }", + "- Missing quotations (\") around an argument node eg. { first_node \"argument_node\"; }", + "- Missing an equal sign (=) between node arguments on a title line. eg. argument=\"value\"", + "- Found an extraneous equal sign (=) between node child arguments and their values. eg. { argument=\"value\" }") + }, + _ => String::from(kdl_error.help.unwrap_or("Kdl Deserialization Error")), + }; + let kdl_error = KdlError { + error_message, + src: Some(NamedSource::new(layout_source_name.clone(), String::from(raw_layout))), offset: Some(kdl_error.span.offset()), len: Some(kdl_error.span.len()), help_message: None, @@ -1555,6 +1679,7 @@ impl Action { }, CliAction::OverrideLayout { layout, + layout_string, layout_dir, retain_existing_terminal_panes, retain_existing_plugin_panes, @@ -1565,24 +1690,39 @@ impl Action { .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| { + // Load layout from string, URL, or file path + let layout_source_name; + let (path_to_raw_layout, raw_layout, swap_layouts) = if let Some(raw) = + layout_string + { + layout_source_name = "layout-string".to_owned(); + (layout_source_name.clone(), raw, None) + } else if let Some(layout_path) = &layout { + if let Some(layout_url) = layout_path.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, - ) + layout_source_name = layout_url.to_owned(); + ( + layout_url.to_owned(), + Layout::stringified_from_url(layout_url) + .map_err(|e| format!("Failed to load layout from URL: {}", e))?, + None, + ) + } else { + layout_source_name = layout_path + .as_path() + .as_os_str() + .to_string_lossy() + .to_string(); + Layout::stringified_from_path_or_default(Some(layout_path), layout_dir) + .map_err(|e| format!("Failed to load layout: {}", e))? + } } else { - Layout::stringified_from_path_or_default(Some(&layout), layout_dir) - .map_err(|e| format!("Failed to load layout: {}", e))? + return Err("Either layout or layout-string must be provided".to_string()); }; // Parse KDL layout @@ -1595,10 +1735,8 @@ impl Action { .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 error = kdl_error + .add_src(layout_source_name.clone(), String::from(raw_layout)); let report: Report = error.into(); format!("{:?}", report) }, @@ -1669,6 +1807,7 @@ impl Action { move_to_focused_tab, configuration, skip_plugin_cache, + tab_id, } => { let current_dir = get_current_dir(); let run_plugin_or_alias = RunPluginOrAlias::from_url( @@ -1684,6 +1823,7 @@ impl Action { should_open_in_place: in_place, close_replaced_pane, skip_cache: skip_plugin_cache, + tab_id, }]) }, CliAction::LaunchPlugin { @@ -1693,6 +1833,7 @@ impl Action { close_replaced_pane, configuration, skip_plugin_cache, + tab_id, } => { let current_dir = get_current_dir(); let run_plugin_or_alias = RunPluginOrAlias::from_url( @@ -1708,6 +1849,7 @@ impl Action { close_replaced_pane, skip_cache: skip_plugin_cache, cwd: Some(current_dir), + tab_id, }]) }, CliAction::RenameSession { name } => Ok(vec![Action::RenameSession { name }]), @@ -1919,6 +2061,7 @@ impl Action { tab_position, pane_id, layout, + layout_string, layout_dir, cwd, } => { @@ -1946,10 +2089,83 @@ impl Action { current_dir.join(layout_dir) }); - let layout_info = if let Some(layout_path) = layout { + let layout_info = if let Some(layout_string) = layout_string { + // validate the layout string before sending it to the target session + let layout_source_name = "layout-string".to_owned(); + let raw_layout_for_error = layout_string.clone(); + Layout::from_str(&layout_string, layout_source_name.clone(), None, None) + .map_err(|e| { + match e { + ConfigError::KdlError(kdl_error) => { + let error = kdl_error.add_src(layout_source_name, raw_layout_for_error); + let report: Report = error.into(); + format!("{:?}", report) + }, + ConfigError::KdlDeserializationError(kdl_error) => { + let error_message = match kdl_error.kind { + kdl::KdlErrorKind::Context("valid node terminator") => { + format!("Failed to deserialize KDL node. \nPossible reasons:\n{}\n{}\n{}\n{}", + "- Missing `;` after a node name, eg. {{ node; another_node; }}", + "- Missing quotations (\") around an argument node eg. {{ first_node \"argument_node\"; }}", + "- Missing an equal sign (=) between node arguments on a title line. eg. argument=\"value\"", + "- Found an extraneous equal sign (=) between node child arguments and their values. eg. {{ argument=\"value\" }}") + }, + _ => String::from(kdl_error.help.unwrap_or("Kdl Deserialization Error")), + }; + let kdl_error = KdlError { + error_message, + src: Some(NamedSource::new(layout_source_name, raw_layout_for_error)), + offset: Some(kdl_error.span.offset()), + len: Some(kdl_error.span.len()), + help_message: None, + }; + let report: Report = kdl_error.into(); + format!("{:?}", report) + }, + e => format!("{}", e), + } + })?; + Some(LayoutInfo::Stringified(layout_string)) + } else if let Some(layout_path) = layout { let layout_dir = layout_dir .or_else(|| config.and_then(|c| c.options.layout_dir.clone())) .or_else(|| get_layout_dir(find_default_config_dir())); + // validate the layout file before sending it to the target session + let layout_source_name = layout_path.display().to_string(); + Layout::from_path_or_default_without_config( + Some(&layout_path), + layout_dir.clone(), + ) + .map_err(|e| { + match e { + ConfigError::KdlError(kdl_error) => { + let report: Report = kdl_error.into(); + format!("{:?}", report) + }, + ConfigError::KdlDeserializationError(kdl_error) => { + let error_message = match kdl_error.kind { + kdl::KdlErrorKind::Context("valid node terminator") => { + format!("Failed to deserialize KDL node. \nPossible reasons:\n{}\n{}\n{}\n{}", + "- Missing `;` after a node name, eg. {{ node; another_node; }}", + "- Missing quotations (\") around an argument node eg. {{ first_node \"argument_node\"; }}", + "- Missing an equal sign (=) between node arguments on a title line. eg. argument=\"value\"", + "- Found an extraneous equal sign (=) between node child arguments and their values. eg. {{ argument=\"value\" }}") + }, + _ => String::from(kdl_error.help.unwrap_or("Kdl Deserialization Error")), + }; + let kdl_error = KdlError { + error_message, + src: Some(NamedSource::new(layout_source_name, String::new())), + offset: Some(kdl_error.span.offset()), + len: Some(kdl_error.span.len()), + help_message: None, + }; + let report: Report = kdl_error.into(); + format!("{:?}", report) + }, + e => format!("{}", e), + } + })?; LayoutInfo::from_config(&layout_dir, &Some(layout_path)) } else { None @@ -3177,4 +3393,481 @@ mod tests { let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None); assert!(result.is_err()); } + + #[test] + fn test_new_tab_with_layout_string() { + let cli_action = CliAction::NewTab { + name: None, + layout: None, + layout_string: Some("layout {\n pane\n pane\n}\n".into()), + layout_dir: None, + cwd: None, + initial_command: vec![], + initial_plugin: None, + close_on_exit: Default::default(), + start_suspended: Default::default(), + block_until_exit: false, + block_until_exit_success: false, + block_until_exit_failure: false, + }; + let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None); + assert!(result.is_ok()); + let actions = result.unwrap(); + assert_eq!(actions.len(), 1); + match &actions[0] { + Action::NewTab { + tiled_layout, + floating_layouts, + .. + } => { + assert!(tiled_layout.is_some()); + let layout = tiled_layout.as_ref().unwrap(); + // layout { pane; pane } produces a layout with 2 children + assert_eq!(layout.children.len(), 2); + assert!(floating_layouts.is_empty()); + }, + _ => panic!("Expected NewTab action"), + } + } + + #[test] + fn test_new_tab_with_invalid_layout_string() { + let cli_action = CliAction::NewTab { + name: None, + layout: None, + layout_string: Some("invalid { kdl".into()), + layout_dir: None, + cwd: None, + initial_command: vec![], + initial_plugin: None, + close_on_exit: Default::default(), + start_suspended: Default::default(), + block_until_exit: false, + block_until_exit_success: false, + block_until_exit_failure: false, + }; + let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None); + assert!(result.is_err()); + } + + #[test] + fn test_override_layout_with_layout_string() { + let cli_action = CliAction::OverrideLayout { + layout: None, + layout_string: Some("layout {\n pane\n pane\n}\n".into()), + layout_dir: None, + retain_existing_terminal_panes: false, + retain_existing_plugin_panes: false, + apply_only_to_active_tab: false, + }; + let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None); + assert!(result.is_ok()); + let actions = result.unwrap(); + assert_eq!(actions.len(), 1); + match &actions[0] { + Action::OverrideLayout { tabs, .. } => { + assert!(!tabs.is_empty()); + }, + _ => panic!("Expected OverrideLayout action"), + } + } + + #[test] + fn test_switch_session_with_layout_string() { + let cli_action = CliAction::SwitchSession { + name: "test-session".into(), + tab_position: None, + pane_id: None, + layout: None, + layout_string: Some("layout {\n pane\n}\n".into()), + layout_dir: None, + cwd: None, + }; + let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None); + assert!(result.is_ok()); + let actions = result.unwrap(); + assert_eq!(actions.len(), 1); + match &actions[0] { + Action::SwitchSession { layout, .. } => { + assert!(matches!( + layout, + Some(crate::data::LayoutInfo::Stringified(_)) + )); + }, + _ => panic!("Expected SwitchSession action"), + } + } + + #[test] + fn test_switch_session_with_invalid_layout_string() { + let cli_action = CliAction::SwitchSession { + name: "test-session".into(), + tab_position: None, + pane_id: None, + layout: None, + layout_string: Some("invalid { kdl".into()), + layout_dir: None, + cwd: None, + }; + let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None); + assert!(result.is_err()); + } + + // Tab-targeting for pane creation commands + + #[test] + fn test_new_pane_tiled_with_tab_id() { + let cli_action = CliAction::NewPane { + direction: Some(Direction::Right), + command: vec![], + plugin: None, + cwd: None, + floating: false, + in_place: false, + close_replaced_pane: false, + name: None, + close_on_exit: false, + start_suspended: false, + configuration: None, + skip_plugin_cache: false, + x: None, + y: None, + width: None, + height: None, + pinned: None, + stacked: false, + blocking: false, + block_until_exit_success: false, + block_until_exit_failure: false, + block_until_exit: false, + unblock_condition: None, + near_current_pane: false, + borderless: None, + tab_id: Some(3), + }; + let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None); + assert!(result.is_ok()); + let actions = result.unwrap(); + assert_eq!(actions.len(), 1); + match &actions[0] { + Action::NewTiledPane { tab_id, .. } => { + assert_eq!(*tab_id, Some(3)); + }, + _ => panic!("Expected NewTiledPane action"), + } + } + + #[test] + fn test_new_pane_tiled_without_tab_id() { + let cli_action = CliAction::NewPane { + direction: None, + command: vec![], + plugin: None, + cwd: None, + floating: false, + in_place: false, + close_replaced_pane: false, + name: None, + close_on_exit: false, + start_suspended: false, + configuration: None, + skip_plugin_cache: false, + x: None, + y: None, + width: None, + height: None, + pinned: None, + stacked: false, + blocking: false, + block_until_exit_success: false, + block_until_exit_failure: false, + block_until_exit: false, + unblock_condition: None, + near_current_pane: false, + borderless: None, + tab_id: None, + }; + let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None); + assert!(result.is_ok()); + let actions = result.unwrap(); + assert_eq!(actions.len(), 1); + match &actions[0] { + Action::NewTiledPane { tab_id, .. } => { + assert_eq!(*tab_id, None); + }, + _ => panic!("Expected NewTiledPane action"), + } + } + + #[test] + fn test_new_pane_floating_with_tab_id() { + let cli_action = CliAction::NewPane { + direction: None, + command: vec![], + plugin: None, + cwd: None, + floating: true, + in_place: false, + close_replaced_pane: false, + name: None, + close_on_exit: false, + start_suspended: false, + configuration: None, + skip_plugin_cache: false, + x: None, + y: None, + width: None, + height: None, + pinned: None, + stacked: false, + blocking: false, + block_until_exit_success: false, + block_until_exit_failure: false, + block_until_exit: false, + unblock_condition: None, + near_current_pane: false, + borderless: None, + tab_id: Some(5), + }; + let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None); + assert!(result.is_ok()); + let actions = result.unwrap(); + assert_eq!(actions.len(), 1); + match &actions[0] { + Action::NewFloatingPane { tab_id, .. } => { + assert_eq!(*tab_id, Some(5)); + }, + _ => panic!("Expected NewFloatingPane action"), + } + } + + #[test] + fn test_new_pane_stacked_with_tab_id() { + let cli_action = CliAction::NewPane { + direction: None, + command: vec!["ls".into()], + plugin: None, + cwd: None, + floating: false, + in_place: false, + close_replaced_pane: false, + name: None, + close_on_exit: false, + start_suspended: false, + configuration: None, + skip_plugin_cache: false, + x: None, + y: None, + width: None, + height: None, + pinned: None, + stacked: true, + blocking: false, + block_until_exit_success: false, + block_until_exit_failure: false, + block_until_exit: false, + unblock_condition: None, + near_current_pane: false, + borderless: None, + tab_id: Some(1), + }; + let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None); + assert!(result.is_ok()); + let actions = result.unwrap(); + assert_eq!(actions.len(), 1); + match &actions[0] { + Action::NewStackedPane { tab_id, .. } => { + assert_eq!(*tab_id, Some(1)); + }, + _ => panic!("Expected NewStackedPane action"), + } + } + + #[test] + fn test_new_pane_blocking_with_tab_id() { + let cli_action = CliAction::NewPane { + direction: None, + command: vec!["ls".into()], + plugin: None, + cwd: None, + floating: false, + in_place: false, + close_replaced_pane: false, + name: None, + close_on_exit: false, + start_suspended: false, + configuration: None, + skip_plugin_cache: false, + x: None, + y: None, + width: None, + height: None, + pinned: None, + stacked: false, + blocking: true, + block_until_exit_success: false, + block_until_exit_failure: false, + block_until_exit: false, + unblock_condition: None, + near_current_pane: false, + borderless: None, + tab_id: Some(2), + }; + let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None); + assert!(result.is_ok()); + let actions = result.unwrap(); + assert_eq!(actions.len(), 1); + match &actions[0] { + Action::NewBlockingPane { tab_id, .. } => { + assert_eq!(*tab_id, Some(2)); + }, + _ => panic!("Expected NewBlockingPane action"), + } + } + + #[test] + fn test_edit_with_tab_id() { + let cli_action = CliAction::Edit { + file: PathBuf::from("/tmp/test.rs"), + direction: None, + line_number: None, + floating: false, + in_place: false, + close_replaced_pane: false, + cwd: None, + x: None, + y: None, + width: None, + height: None, + pinned: None, + near_current_pane: false, + borderless: None, + tab_id: Some(4), + }; + let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None); + assert!(result.is_ok()); + let actions = result.unwrap(); + assert_eq!(actions.len(), 1); + match &actions[0] { + Action::EditFile { tab_id, .. } => { + assert_eq!(*tab_id, Some(4)); + }, + _ => panic!("Expected EditFile action"), + } + } + + #[test] + fn test_edit_without_tab_id() { + let cli_action = CliAction::Edit { + file: PathBuf::from("/tmp/test.rs"), + direction: None, + line_number: None, + floating: false, + in_place: false, + close_replaced_pane: false, + cwd: None, + x: None, + y: None, + width: None, + height: None, + pinned: None, + near_current_pane: false, + borderless: None, + tab_id: None, + }; + let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None); + assert!(result.is_ok()); + let actions = result.unwrap(); + assert_eq!(actions.len(), 1); + match &actions[0] { + Action::EditFile { tab_id, .. } => { + assert_eq!(*tab_id, None); + }, + _ => panic!("Expected EditFile action"), + } + } + + #[test] + fn test_new_pane_plugin_tiled_with_tab_id() { + let cli_action = CliAction::NewPane { + direction: None, + command: vec![], + plugin: Some("zellij:strider".into()), + cwd: None, + floating: false, + in_place: false, + close_replaced_pane: false, + name: None, + close_on_exit: false, + start_suspended: false, + configuration: None, + skip_plugin_cache: false, + x: None, + y: None, + width: None, + height: None, + pinned: None, + stacked: false, + blocking: false, + block_until_exit_success: false, + block_until_exit_failure: false, + block_until_exit: false, + unblock_condition: None, + near_current_pane: false, + borderless: None, + tab_id: Some(2), + }; + let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None); + assert!(result.is_ok()); + let actions = result.unwrap(); + assert_eq!(actions.len(), 1); + match &actions[0] { + Action::NewTiledPluginPane { tab_id, .. } => { + assert_eq!(*tab_id, Some(2)); + }, + _ => panic!("Expected NewTiledPluginPane action"), + } + } + + #[test] + fn test_new_pane_plugin_floating_with_tab_id() { + let cli_action = CliAction::NewPane { + direction: None, + command: vec![], + plugin: Some("zellij:strider".into()), + cwd: None, + floating: true, + in_place: false, + close_replaced_pane: false, + name: None, + close_on_exit: false, + start_suspended: false, + configuration: None, + skip_plugin_cache: false, + x: None, + y: None, + width: None, + height: None, + pinned: None, + stacked: false, + blocking: false, + block_until_exit_success: false, + block_until_exit_failure: false, + block_until_exit: false, + unblock_condition: None, + near_current_pane: false, + borderless: None, + tab_id: Some(1), + }; + let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None); + assert!(result.is_ok()); + let actions = result.unwrap(); + assert_eq!(actions.len(), 1); + match &actions[0] { + Action::NewFloatingPluginPane { tab_id, .. } => { + assert_eq!(*tab_id, Some(1)); + }, + _ => panic!("Expected NewFloatingPluginPane action"), + } + } } diff --git a/zellij-utils/src/ipc/protobuf_conversion.rs b/zellij-utils/src/ipc/protobuf_conversion.rs index 5c78a4453..189c13706 100644 --- a/zellij-utils/src/ipc/protobuf_conversion.rs +++ b/zellij-utils/src/ipc/protobuf_conversion.rs @@ -1095,6 +1095,8 @@ impl From start_suppressed, coordinates, near_current_pane, + tab_id, + .. } => ActionType::EditFile(EditFileAction { payload: Some(payload.into()), direction: direction.map(|d| direction_to_proto_i32(d)), @@ -1104,17 +1106,21 @@ impl From start_suppressed, coordinates: coordinates.map(|c| c.into()), near_current_pane, + tab_id: tab_id.map(|t| t as u32), }), crate::input::actions::Action::NewFloatingPane { command, pane_name, coordinates, near_current_pane, + tab_id, + .. } => ActionType::NewFloatingPane(NewFloatingPaneAction { command: command.map(|c| c.into()), pane_name, coordinates: coordinates.map(|c| c.into()), near_current_pane, + tab_id: tab_id.map(|t| t as u32), }), crate::input::actions::Action::NewTiledPane { direction, @@ -1122,12 +1128,15 @@ impl From pane_name, near_current_pane, borderless, + tab_id, + .. } => ActionType::NewTiledPane(NewTiledPaneAction { direction: direction.map(|d| direction_to_proto_i32(d)), command: command.map(|c| c.into()), pane_name, near_current_pane, borderless, + tab_id: tab_id.map(|t| t as u32), }), crate::input::actions::Action::NewInPlacePane { command, @@ -1135,21 +1144,27 @@ impl From near_current_pane, pane_id_to_replace, close_replaced_pane, + tab_id, + .. } => ActionType::NewInPlacePane(NewInPlacePaneAction { command: command.map(|c| c.into()), pane_name, near_current_pane, pane_id_to_replace: pane_id_to_replace.and_then(|p| p.try_into().ok()), close_replaced_pane, + tab_id: tab_id.map(|t| t as u32), }), crate::input::actions::Action::NewStackedPane { command, pane_name, near_current_pane, + tab_id, + .. } => ActionType::NewStackedPane(NewStackedPaneAction { command: command.map(|c| c.into()), pane_name, near_current_pane, + tab_id: tab_id.map(|t| t as u32), }), crate::input::actions::Action::NewBlockingPane { placement, @@ -1157,12 +1172,15 @@ impl From command, unblock_condition, near_current_pane, + tab_id, + .. } => ActionType::NewBlockingPane(NewBlockingPaneAction { placement: Some(placement.into()), pane_name, command: command.map(|c| c.into()), unblock_condition: unblock_condition.map(|c| unblock_condition_to_proto_i32(c)), near_current_pane, + tab_id: tab_id.map(|t| t as u32), }), crate::input::actions::Action::TogglePaneEmbedOrFloating => { ActionType::TogglePaneEmbedOrFloating(TogglePaneEmbedOrFloatingAction {}) @@ -1283,6 +1301,8 @@ impl From should_open_in_place, close_replaced_pane, skip_cache, + tab_id, + .. } => ActionType::LaunchOrFocusPlugin(LaunchOrFocusPluginAction { plugin: Some(plugin.into()), should_float, @@ -1290,6 +1310,7 @@ impl From should_open_in_place, close_replaced_pane, skip_cache, + tab_id: tab_id.map(|t| t as u32), }), crate::input::actions::Action::LaunchPlugin { plugin, @@ -1298,6 +1319,8 @@ impl From close_replaced_pane, skip_cache, cwd, + tab_id, + .. } => ActionType::LaunchPlugin(LaunchPluginAction { plugin: Some(plugin.into()), should_float, @@ -1305,6 +1328,7 @@ impl From close_replaced_pane, skip_cache, cwd: cwd.map(|p| p.to_string_lossy().to_string()), + tab_id: tab_id.map(|t| t as u32), }), crate::input::actions::Action::MouseEvent { event } => { ActionType::MouseEvent(MouseEventAction { @@ -1362,11 +1386,14 @@ impl From pane_name, skip_cache, cwd, + tab_id, + .. } => ActionType::NewTiledPluginPane(NewTiledPluginPaneAction { plugin: Some(plugin.into()), pane_name, skip_cache, cwd: cwd.map(|p| p.to_string_lossy().to_string()), + tab_id: tab_id.map(|t| t as u32), }), crate::input::actions::Action::NewFloatingPluginPane { plugin, @@ -1374,23 +1401,29 @@ impl From skip_cache, cwd, coordinates, + tab_id, + .. } => ActionType::NewFloatingPluginPane(NewFloatingPluginPaneAction { plugin: Some(plugin.into()), pane_name, skip_cache, cwd: cwd.map(|p| p.to_string_lossy().to_string()), coordinates: coordinates.map(|c| c.into()), + tab_id: tab_id.map(|t| t as u32), }), crate::input::actions::Action::NewInPlacePluginPane { plugin, pane_name, skip_cache, close_replaced_pane, + tab_id, + .. } => ActionType::NewInPlacePluginPane(NewInPlacePluginPaneAction { plugin: Some(plugin.into()), pane_name, skip_cache, close_replaced_pane, + tab_id: tab_id.map(|t| t as u32), }), crate::input::actions::Action::StartOrReloadPlugin { plugin } => { ActionType::StartOrReloadPlugin(StartOrReloadPluginAction { @@ -1916,6 +1949,7 @@ impl TryFrom .map(|c| c.try_into()) .transpose()?, near_current_pane: edit_file_action.near_current_pane, + tab_id: edit_file_action.tab_id.map(|t| t as usize), }), ActionType::NewFloatingPane(new_floating_action) => { Ok(crate::input::actions::Action::NewFloatingPane { @@ -1929,6 +1963,7 @@ impl TryFrom .map(|c| c.try_into()) .transpose()?, near_current_pane: new_floating_action.near_current_pane, + tab_id: new_floating_action.tab_id.map(|t| t as usize), }) }, ActionType::NewTiledPane(new_tiled_action) => { @@ -1941,6 +1976,7 @@ impl TryFrom pane_name: new_tiled_action.pane_name, near_current_pane: new_tiled_action.near_current_pane, borderless: new_tiled_action.borderless, + tab_id: new_tiled_action.tab_id.map(|t| t as usize), }) }, ActionType::NewInPlacePane(new_in_place_action) => { @@ -1955,6 +1991,7 @@ impl TryFrom .pane_id_to_replace .and_then(|p| p.try_into().ok()), close_replaced_pane: new_in_place_action.close_replaced_pane, + tab_id: new_in_place_action.tab_id.map(|t| t as usize), }) }, ActionType::NewStackedPane(new_stacked_action) => { @@ -1965,6 +2002,7 @@ impl TryFrom .transpose()?, pane_name: new_stacked_action.pane_name, near_current_pane: new_stacked_action.near_current_pane, + tab_id: new_stacked_action.tab_id.map(|t| t as usize), }) }, ActionType::NewBlockingPane(new_blocking_action) => { @@ -1983,6 +2021,7 @@ impl TryFrom .map(|c| proto_i32_to_unblock_condition(c)) .transpose()?, near_current_pane: new_blocking_action.near_current_pane, + tab_id: new_blocking_action.tab_id.map(|t| t as usize), }) }, ActionType::TogglePaneEmbedOrFloating(_) => { @@ -2126,6 +2165,7 @@ impl TryFrom should_open_in_place: launch_plugin_action.should_open_in_place, close_replaced_pane: launch_plugin_action.close_replaced_pane, skip_cache: launch_plugin_action.skip_cache, + tab_id: launch_plugin_action.tab_id.map(|t| t as usize), }) }, ActionType::LaunchPlugin(launch_plugin_action) => { @@ -2139,6 +2179,7 @@ impl TryFrom close_replaced_pane: launch_plugin_action.close_replaced_pane, skip_cache: launch_plugin_action.skip_cache, cwd: launch_plugin_action.cwd.map(PathBuf::from), + tab_id: launch_plugin_action.tab_id.map(|t| t as usize), }) }, ActionType::MouseEvent(mouse_event_action) => { @@ -2210,6 +2251,7 @@ impl TryFrom pane_name: new_tiled_plugin_action.pane_name, skip_cache: new_tiled_plugin_action.skip_cache, cwd: new_tiled_plugin_action.cwd.map(PathBuf::from), + tab_id: new_tiled_plugin_action.tab_id.map(|t| t as usize), }) }, ActionType::NewFloatingPluginPane(new_floating_plugin_action) => { @@ -2225,6 +2267,7 @@ impl TryFrom .coordinates .map(|c| c.try_into()) .transpose()?, + tab_id: new_floating_plugin_action.tab_id.map(|t| t as usize), }) }, ActionType::NewInPlacePluginPane(new_in_place_plugin_action) => { @@ -2236,6 +2279,7 @@ impl TryFrom pane_name: new_in_place_plugin_action.pane_name, skip_cache: new_in_place_plugin_action.skip_cache, close_replaced_pane: new_in_place_plugin_action.close_replaced_pane, + tab_id: new_in_place_plugin_action.tab_id.map(|t| t as usize), }) }, ActionType::StartOrReloadPlugin(start_plugin_action) => { diff --git a/zellij-utils/src/ipc/tests/roundtrip_tests.rs b/zellij-utils/src/ipc/tests/roundtrip_tests.rs index cfe882a1a..7ef7ac89a 100644 --- a/zellij-utils/src/ipc/tests/roundtrip_tests.rs +++ b/zellij-utils/src/ipc/tests/roundtrip_tests.rs @@ -1,8 +1,8 @@ use super::test_framework::*; use crate::data::{ BareKey, CommandOrPlugin, ConnectToSession, Direction, FloatingPaneCoordinates, InputMode, - KeyModifier, KeyWithModifier, LayoutInfo, LayoutMetadata, OriginatingPlugin, PaneId, PluginTag, - Resize, WebSharing, + KeyModifier, KeyWithModifier, LayoutInfo, LayoutMetadata, NewPanePlacement, OriginatingPlugin, + PaneId, PluginTag, Resize, WebSharing, }; use crate::input::actions::{Action, SearchDirection, SearchOption}; use crate::input::cli_assets::CliAssets; @@ -1054,6 +1054,7 @@ fn test_client_messages() { start_suppressed: false, coordinates: None, near_current_pane: false, + tab_id: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1078,6 +1079,7 @@ fn test_client_messages() { start_suppressed: false, coordinates: None, near_current_pane: false, + tab_id: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1102,6 +1104,7 @@ fn test_client_messages() { start_suppressed: false, coordinates: None, near_current_pane: false, + tab_id: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1126,6 +1129,7 @@ fn test_client_messages() { start_suppressed: true, coordinates: FloatingPaneCoordinates::new(None, None, None, None, None, Some(false)), near_current_pane: false, + tab_id: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1157,6 +1161,7 @@ fn test_client_messages() { Some(false), ), near_current_pane: false, + tab_id: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1188,6 +1193,7 @@ fn test_client_messages() { Some(false), ), near_current_pane: false, + tab_id: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1219,6 +1225,7 @@ fn test_client_messages() { Some(false), ), near_current_pane: false, + tab_id: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1250,6 +1257,7 @@ fn test_client_messages() { Some(false) ), near_current_pane: false, + tab_id: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1261,6 +1269,7 @@ fn test_client_messages() { pane_name: None, coordinates: None, near_current_pane: false, + tab_id: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1288,6 +1297,7 @@ fn test_client_messages() { Some(false), ), near_current_pane: false, + tab_id: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1319,6 +1329,7 @@ fn test_client_messages() { Some(false), ), near_current_pane: false, + tab_id: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1331,6 +1342,7 @@ fn test_client_messages() { pane_name: None, near_current_pane: false, borderless: None, + tab_id: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1356,6 +1368,7 @@ fn test_client_messages() { pane_name: Some("my_pane_name".to_owned()), near_current_pane: false, borderless: Some(true), + tab_id: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1381,6 +1394,7 @@ fn test_client_messages() { near_current_pane: false, pane_id_to_replace: None, close_replaced_pane: false, + tab_id: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1393,6 +1407,7 @@ fn test_client_messages() { near_current_pane: false, pane_id_to_replace: None, close_replaced_pane: false, + tab_id: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1416,6 +1431,7 @@ fn test_client_messages() { }), pane_name: Some("my_pane_name".to_owned()), near_current_pane: false, + tab_id: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1426,6 +1442,7 @@ fn test_client_messages() { command: None, pane_name: None, near_current_pane: false, + tab_id: None, }, terminal_id: Some(1), client_id: Some(100), @@ -2230,6 +2247,7 @@ fn test_client_messages() { should_open_in_place: true, close_replaced_pane: false, skip_cache: true, + tab_id: None, }, terminal_id: Some(1), client_id: Some(100), @@ -2244,6 +2262,7 @@ fn test_client_messages() { should_open_in_place: false, close_replaced_pane: false, skip_cache: false, + tab_id: None, }, terminal_id: Some(1), client_id: Some(100), @@ -2258,6 +2277,7 @@ fn test_client_messages() { close_replaced_pane: false, skip_cache: true, cwd: None, + tab_id: None, }, terminal_id: Some(1), client_id: Some(100), @@ -2272,6 +2292,7 @@ fn test_client_messages() { close_replaced_pane: false, skip_cache: false, cwd: Some(PathBuf::from("/path/to/cwd")), + tab_id: None, }, terminal_id: Some(1), client_id: Some(100), @@ -2438,6 +2459,7 @@ fn test_client_messages() { pane_name: Some("my_pane_name".to_owned()), skip_cache: false, cwd: Some(PathBuf::from("relative/path/to/cwd")), + tab_id: None, }, terminal_id: Some(1), client_id: Some(100), @@ -2450,6 +2472,7 @@ fn test_client_messages() { skip_cache: true, cwd: Some(PathBuf::from("relative/path/to/cwd")), coordinates: None, + tab_id: None, }, terminal_id: Some(1), client_id: Some(100), @@ -2469,6 +2492,7 @@ fn test_client_messages() { Some(true), Some(false), ), + tab_id: None, }, terminal_id: Some(1), client_id: Some(100), @@ -2480,6 +2504,159 @@ fn test_client_messages() { pane_name: Some("my_pane_name".to_owned()), skip_cache: true, close_replaced_pane: false, + tab_id: None, + }, + terminal_id: Some(1), + client_id: Some(100), + is_cli_client: true, + }); + // tab_id roundtrip tests - verify tab_id survives serialization + test_client_roundtrip!(ClientToServerMsg::Action { + action: Action::NewTiledPane { + direction: Some(Direction::Right), + command: None, + pane_name: None, + near_current_pane: false, + borderless: None, + tab_id: Some(3), + }, + terminal_id: Some(1), + client_id: Some(100), + is_cli_client: true, + }); + test_client_roundtrip!(ClientToServerMsg::Action { + action: Action::NewFloatingPane { + command: None, + pane_name: None, + coordinates: None, + near_current_pane: false, + tab_id: Some(5), + }, + terminal_id: Some(1), + client_id: Some(100), + is_cli_client: true, + }); + test_client_roundtrip!(ClientToServerMsg::Action { + action: Action::NewStackedPane { + command: None, + pane_name: None, + near_current_pane: false, + tab_id: Some(2), + }, + terminal_id: Some(1), + client_id: Some(100), + is_cli_client: true, + }); + test_client_roundtrip!(ClientToServerMsg::Action { + action: Action::NewBlockingPane { + placement: NewPanePlacement::Tiled { + direction: None, + borderless: None + }, + pane_name: None, + command: None, + unblock_condition: None, + near_current_pane: false, + tab_id: Some(1), + }, + terminal_id: Some(1), + client_id: Some(100), + is_cli_client: true, + }); + test_client_roundtrip!(ClientToServerMsg::Action { + action: Action::EditFile { + payload: OpenFilePayload { + path: PathBuf::from("/file/path"), + line_number: None, + cwd: None, + originating_plugin: None, + }, + direction: None, + floating: false, + in_place: false, + close_replaced_pane: false, + start_suppressed: false, + coordinates: None, + near_current_pane: false, + tab_id: Some(4), + }, + terminal_id: Some(1), + client_id: Some(100), + is_cli_client: true, + }); + test_client_roundtrip!(ClientToServerMsg::Action { + action: Action::NewInPlacePane { + command: None, + pane_name: None, + near_current_pane: false, + pane_id_to_replace: None, + close_replaced_pane: false, + tab_id: Some(7), + }, + terminal_id: Some(1), + client_id: Some(100), + is_cli_client: true, + }); + test_client_roundtrip!(ClientToServerMsg::Action { + action: Action::LaunchOrFocusPlugin { + plugin: RunPluginOrAlias::RunPlugin(RunPlugin::default()), + should_float: true, + move_to_focused_tab: false, + should_open_in_place: false, + close_replaced_pane: false, + skip_cache: false, + tab_id: Some(2), + }, + terminal_id: Some(1), + client_id: Some(100), + is_cli_client: true, + }); + test_client_roundtrip!(ClientToServerMsg::Action { + action: Action::LaunchPlugin { + plugin: RunPluginOrAlias::RunPlugin(RunPlugin::default()), + should_float: false, + should_open_in_place: false, + close_replaced_pane: false, + skip_cache: false, + cwd: None, + tab_id: Some(6), + }, + terminal_id: Some(1), + client_id: Some(100), + is_cli_client: true, + }); + test_client_roundtrip!(ClientToServerMsg::Action { + action: Action::NewTiledPluginPane { + plugin: RunPluginOrAlias::RunPlugin(RunPlugin::default()), + pane_name: None, + skip_cache: false, + cwd: None, + tab_id: Some(1), + }, + terminal_id: Some(1), + client_id: Some(100), + is_cli_client: true, + }); + test_client_roundtrip!(ClientToServerMsg::Action { + action: Action::NewFloatingPluginPane { + plugin: RunPluginOrAlias::RunPlugin(RunPlugin::default()), + pane_name: None, + skip_cache: false, + cwd: None, + coordinates: None, + tab_id: Some(3), + }, + terminal_id: Some(1), + client_id: Some(100), + is_cli_client: true, + }); + test_client_roundtrip!(ClientToServerMsg::Action { + action: Action::NewInPlacePluginPane { + plugin: RunPluginOrAlias::RunPlugin(RunPlugin::default()), + pane_name: None, + skip_cache: false, + close_replaced_pane: false, + tab_id: Some(2), }, terminal_id: Some(1), client_id: Some(100), diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs index 919c865f7..2b009e1a6 100644 --- a/zellij-utils/src/kdl/mod.rs +++ b/zellij-utils/src/kdl/mod.rs @@ -558,6 +558,7 @@ impl Action { command: None, pane_name: None, near_current_pane: false, + tab_id: None, }); } else { let direction = Direction::from_str(string.as_str()).map_err(|_| { @@ -840,6 +841,7 @@ impl Action { pane_name: name, near_current_pane: false, borderless: _, + .. } => { let mut node = KdlNode::new("Run"); let mut node_children = KdlDocument::new(); @@ -890,6 +892,7 @@ impl Action { pane_name: name, coordinates: floating_pane_coordinates, near_current_pane: false, + .. } => { let mut node = KdlNode::new("Run"); let mut node_children = KdlDocument::new(); @@ -983,6 +986,7 @@ impl Action { near_current_pane: false, pane_id_to_replace: None, close_replaced_pane, + .. } => { let mut node = KdlNode::new("Run"); let mut node_children = KdlDocument::new(); @@ -1029,6 +1033,7 @@ impl Action { command: run_command_action, pane_name: name, near_current_pane: _, + .. } => match run_command_action { Some(run_command_action) => { let mut node = KdlNode::new("Run"); @@ -1108,6 +1113,7 @@ impl Action { should_open_in_place, close_replaced_pane, skip_cache: skip_plugin_cache, + .. } => { let mut node = KdlNode::new("LaunchOrFocusPlugin"); let mut node_children = KdlDocument::new(); @@ -1157,6 +1163,7 @@ impl Action { close_replaced_pane, skip_cache: skip_plugin_cache, cwd, + .. } => { let mut node = KdlNode::new("LaunchPlugin"); let mut node_children = KdlDocument::new(); @@ -1993,6 +2000,7 @@ impl TryFrom<(&KdlNode, &Options)> for Action { x, y, width, height, pinned, borderless, ), near_current_pane: false, + tab_id: None, }) } else if in_place { Ok(Action::NewInPlacePane { @@ -2001,12 +2009,14 @@ impl TryFrom<(&KdlNode, &Options)> for Action { near_current_pane: false, pane_id_to_replace: None, close_replaced_pane, + tab_id: None, }) } else if stacked { Ok(Action::NewStackedPane { command: Some(run_command_action), pane_name: name, near_current_pane: false, + tab_id: None, }) } else { Ok(Action::NewTiledPane { @@ -2015,6 +2025,7 @@ impl TryFrom<(&KdlNode, &Options)> for Action { pane_name: name, near_current_pane: false, borderless: None, + tab_id: None, }) } }, @@ -2071,6 +2082,7 @@ impl TryFrom<(&KdlNode, &Options)> for Action { should_open_in_place, close_replaced_pane, skip_cache: skip_plugin_cache, + tab_id: None, }) }, "LaunchPlugin" => { @@ -2120,7 +2132,8 @@ impl TryFrom<(&KdlNode, &Options)> for Action { close_replaced_pane, skip_cache: skip_plugin_cache, cwd: None, // we explicitly do not send the current dir here so that it will be - // filled from the active pane == better UX + // filled from the active pane == better UX + tab_id: None, }) }, "PreviousSwapLayout" => Ok(Action::PreviousSwapLayout), diff --git a/zellij-utils/src/plugin_api/action.rs b/zellij-utils/src/plugin_api/action.rs index 4384eb689..cbd62e5b8 100644 --- a/zellij-utils/src/plugin_api/action.rs +++ b/zellij-utils/src/plugin_api/action.rs @@ -338,6 +338,7 @@ impl TryFrom for Action { start_suppressed: false, coordinates: None, near_current_pane, + tab_id: None, }) }, _ => Err("Wrong payload for Action::NewPane"), @@ -353,6 +354,7 @@ impl TryFrom for Action { pane_name, coordinates: None, near_current_pane, + tab_id: None, }) } else { Ok(Action::NewFloatingPane { @@ -360,6 +362,7 @@ impl TryFrom for Action { pane_name: None, coordinates: None, near_current_pane, + tab_id: None, }) } }, @@ -382,6 +385,7 @@ impl TryFrom for Action { pane_name, near_current_pane, borderless, + tab_id: None, }) } else { Ok(Action::NewTiledPane { @@ -390,6 +394,7 @@ impl TryFrom for Action { pane_name: None, near_current_pane, borderless, + tab_id: None, }) } }, @@ -658,6 +663,7 @@ impl TryFrom for Action { should_open_in_place, close_replaced_pane: false, skip_cache: skip_plugin_cache, + tab_id: None, }) }, _ => Err("Wrong payload for Action::LaunchOrFocusPlugin"), @@ -688,6 +694,7 @@ impl TryFrom for Action { close_replaced_pane: false, skip_cache: skip_plugin_cache, cwd: None, + tab_id: None, }) }, _ => Err("Wrong payload for Action::LaunchOrFocusPlugin"), @@ -807,6 +814,7 @@ impl TryFrom for Action { pane_name, skip_cache: skip_plugin_cache, cwd: None, + tab_id: None, }) }, _ => Err("Wrong payload for Action::NewTiledPluginPane"), @@ -832,6 +840,7 @@ impl TryFrom for Action { skip_cache: skip_plugin_cache, cwd: None, coordinates: None, + tab_id: None, }) }, _ => Err("Wrong payload for Action::MiddleClick"), @@ -984,6 +993,7 @@ impl TryFrom for Action { command: None, pane_name: None, near_current_pane: false, + tab_id: None, }), }, Some(ProtobufActionName::NewBlockingPane) => match protobuf_action.optional_payload { @@ -1005,6 +1015,7 @@ impl TryFrom for Action { command, unblock_condition, near_current_pane, + tab_id: None, }) }, _ => Err("Wrong payload for Action::NewBlockingPane"), @@ -1024,6 +1035,7 @@ impl TryFrom for Action { near_current_pane, pane_id_to_replace, close_replaced_pane, + tab_id: None, }) } else { Ok(Action::NewInPlacePane { @@ -1032,6 +1044,7 @@ impl TryFrom for Action { near_current_pane, pane_id_to_replace, close_replaced_pane, + tab_id: None, }) } }, @@ -1298,6 +1311,7 @@ impl TryFrom for ProtobufAction { start_suppressed: _start_suppressed, coordinates: _floating_pane_coordinates, near_current_pane, + .. } => { let file_to_edit = open_file_payload.path.display().to_string(); let cwd = open_file_payload.cwd.map(|cwd| cwd.display().to_string()); @@ -1322,6 +1336,7 @@ impl TryFrom for ProtobufAction { pane_name, coordinates: _coordinates, near_current_pane, + .. } => { let command = run_command_action.and_then(|r| { let mut protobuf_run_command_action: ProtobufRunCommandAction = @@ -1345,6 +1360,7 @@ impl TryFrom for ProtobufAction { pane_name, near_current_pane, borderless, + .. } => { let direction = direction.and_then(|direction| { let protobuf_direction: ProtobufResizeDirection = direction.try_into().ok()?; @@ -1558,6 +1574,7 @@ impl TryFrom for ProtobufAction { should_open_in_place, close_replaced_pane: _close_replaced_pane, skip_cache: skip_plugin_cache, + .. } => { let configuration = run_plugin_or_alias.get_configuration().unwrap_or_default(); Ok(ProtobufAction { @@ -1581,6 +1598,7 @@ impl TryFrom for ProtobufAction { close_replaced_pane: _close_replaced_pane, skip_cache: skip_plugin_cache, cwd: _cwd, + .. } => { let configuration = run_plugin_or_alias.get_configuration().unwrap_or_default(); Ok(ProtobufAction { @@ -1668,6 +1686,7 @@ impl TryFrom for ProtobufAction { pane_name, skip_cache: skip_plugin_cache, cwd: _cwd, + .. } => Ok(ProtobufAction { name: ProtobufActionName::NewTiledPluginPane as i32, optional_payload: Some(OptionalPayload::NewTiledPluginPanePayload( @@ -1684,6 +1703,7 @@ impl TryFrom for ProtobufAction { skip_cache: skip_plugin_cache, cwd: _cwd, coordinates: _coordinates, + .. } => Ok(ProtobufAction { name: ProtobufActionName::NewFloatingPluginPane as i32, optional_payload: Some(OptionalPayload::NewFloatingPluginPanePayload( @@ -1806,6 +1826,7 @@ impl TryFrom for ProtobufAction { command: _, pane_name: _, near_current_pane: _, + .. } => Ok(ProtobufAction { name: ProtobufActionName::NewStackedPane as i32, optional_payload: None, @@ -1816,6 +1837,7 @@ impl TryFrom for ProtobufAction { command, unblock_condition, near_current_pane, + .. } => { let placement: ProtobufNewPanePlacement = placement.try_into()?; let command = command.and_then(|c| { @@ -1847,6 +1869,7 @@ impl TryFrom for ProtobufAction { near_current_pane, pane_id_to_replace, close_replaced_pane, + .. } => { let command = run_command_action.and_then(|r| { let mut protobuf_run_command_action: ProtobufRunCommandAction = @@ -1876,6 +1899,7 @@ impl TryFrom for ProtobufAction { pane_name: _, skip_cache: _, close_replaced_pane: _, + .. } | Action::Deny | Action::Copy diff --git a/zellij-utils/src/setup.rs b/zellij-utils/src/setup.rs index 6d74187d3..d2eafe7ec 100644 --- a/zellij-utils/src/setup.rs +++ b/zellij-utils/src/setup.rs @@ -612,7 +612,9 @@ impl Setup { // the chosen layout can either be a path relative to the layout_dir or a name of one // of our assets, this distinction is made when parsing the layout - TODO: ideally, this // logic should not be split up and all the decisions should happen here - let (layout_info, chosen_layout) = if let Some(chosen_layout) = cli_args.layout.clone() { + let (layout_info, chosen_layout) = if let Some(ref layout_string) = cli_args.layout_string { + (Some(LayoutInfo::Stringified(layout_string.clone())), None) + } else if let Some(chosen_layout) = cli_args.layout.clone() { let layout_info = LayoutInfo::from_cli( &layout_dir, &Some(chosen_layout.clone()), @@ -631,6 +633,10 @@ impl Setup { Some(LayoutInfo::Url(ref layout_url)) => { Layout::from_url(layout_url, config).map(|(_layout, config)| (layout_info, config)) }, + Some(LayoutInfo::Stringified(ref raw_layout)) => { + Layout::from_stringified_layout(raw_layout, config) + .map(|(_layout, config)| (layout_info, config)) + }, _ => Layout::from_path_or_default(chosen_layout.as_ref(), layout_dir.clone(), config) .map(|(_layout, config)| (layout_info, config)), } @@ -879,4 +885,21 @@ mod setup_test { let expected = cwd.join("assets/layouts/compact"); assert_eq!(layout_path, expected.display().to_string()); } + + #[test] + fn layout_string_cli_argument() { + let layout_kdl = "layout {\n pane\n pane\n}\n".to_string(); + let cli_args = CliArgs { + layout_string: Some(layout_kdl.clone()), + ..Default::default() + }; + let (_, layout_info, _, _, _) = Setup::from_cli_args(&cli_args).unwrap(); + let Some(LayoutInfo::Stringified(content)) = layout_info else { + panic!( + "layout info should be Stringified variant, got: {:#?}", + layout_info + ); + }; + assert_eq!(content, layout_kdl); + } } diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap index ff0a3ae40..243ea7ba1 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap @@ -1349,6 +1349,7 @@ Config { command: None, pane_name: None, near_current_pane: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, @@ -4190,6 +4191,7 @@ Config { should_open_in_place: false, close_replaced_pane: false, skip_cache: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, @@ -4231,6 +4233,7 @@ Config { should_open_in_place: false, close_replaced_pane: false, skip_cache: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, @@ -4352,6 +4355,7 @@ Config { should_open_in_place: false, close_replaced_pane: false, skip_cache: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, @@ -4443,6 +4447,7 @@ Config { should_open_in_place: false, close_replaced_pane: false, skip_cache: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, @@ -4517,6 +4522,7 @@ Config { should_open_in_place: false, close_replaced_pane: false, skip_cache: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, @@ -4570,6 +4576,7 @@ Config { should_open_in_place: false, close_replaced_pane: false, skip_cache: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap index 04e30a094..6c3522562 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap @@ -1349,6 +1349,7 @@ Config { command: None, pane_name: None, near_current_pane: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, @@ -4190,6 +4191,7 @@ Config { should_open_in_place: false, close_replaced_pane: false, skip_cache: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, @@ -4231,6 +4233,7 @@ Config { should_open_in_place: false, close_replaced_pane: false, skip_cache: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, @@ -4352,6 +4355,7 @@ Config { should_open_in_place: false, close_replaced_pane: false, skip_cache: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, @@ -4443,6 +4447,7 @@ Config { should_open_in_place: false, close_replaced_pane: false, skip_cache: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, @@ -4517,6 +4522,7 @@ Config { should_open_in_place: false, close_replaced_pane: false, skip_cache: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, @@ -4570,6 +4576,7 @@ Config { should_open_in_place: false, close_replaced_pane: false, skip_cache: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap index 9cb4295dc..027cee699 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap @@ -1349,6 +1349,7 @@ Config { command: None, pane_name: None, near_current_pane: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, @@ -4190,6 +4191,7 @@ Config { should_open_in_place: false, close_replaced_pane: false, skip_cache: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, @@ -4231,6 +4233,7 @@ Config { should_open_in_place: false, close_replaced_pane: false, skip_cache: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, @@ -4352,6 +4355,7 @@ Config { should_open_in_place: false, close_replaced_pane: false, skip_cache: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, @@ -4443,6 +4447,7 @@ Config { should_open_in_place: false, close_replaced_pane: false, skip_cache: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, @@ -4517,6 +4522,7 @@ Config { should_open_in_place: false, close_replaced_pane: false, skip_cache: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, @@ -4570,6 +4576,7 @@ Config { should_open_in_place: false, close_replaced_pane: false, skip_cache: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap index eb72f1085..70b23c183 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap @@ -1349,6 +1349,7 @@ Config { command: None, pane_name: None, near_current_pane: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, @@ -4190,6 +4191,7 @@ Config { should_open_in_place: false, close_replaced_pane: false, skip_cache: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, @@ -4231,6 +4233,7 @@ Config { should_open_in_place: false, close_replaced_pane: false, skip_cache: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, @@ -4352,6 +4355,7 @@ Config { should_open_in_place: false, close_replaced_pane: false, skip_cache: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, @@ -4443,6 +4447,7 @@ Config { should_open_in_place: false, close_replaced_pane: false, skip_cache: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, @@ -4517,6 +4522,7 @@ Config { should_open_in_place: false, close_replaced_pane: false, skip_cache: false, + tab_id: None, }, SwitchToMode { input_mode: Normal, @@ -4570,6 +4576,7 @@ Config { should_open_in_place: false, close_replaced_pane: false, skip_cache: false, + tab_id: None, }, SwitchToMode { input_mode: Normal,