diff options
| author | Aram Drevekenin <aram@poor.dev> | 2026-03-30 16:02:09 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-03-30 16:02:09 +0200 |
| commit | 8ce05f0bbd58acbbca95f95ae14fdc4e866f7391 (patch) | |
| tree | 57c6d9ee2231a556f938df4f2629511a711518df /zellij-server/src | |
| parent | docs(changelog): recent windows fixes (diff) | |
| download | sl0ppij-8ce05f0bbd58acbbca95f95ae14fdc4e866f7391.tar.gz sl0ppij-8ce05f0bbd58acbbca95f95ae14fdc4e866f7391.tar.xz sl0ppij-8ce05f0bbd58acbbca95f95ae14fdc4e866f7391.zip | |
Layout string flag (#4956)
* allow sending a stringified layout with --layout-string
* add --tab-id to all the things
* fix windows tests
* add pr
Diffstat (limited to 'zellij-server/src')
7 files changed, 927 insertions, 177 deletions
diff --git a/zellij-server/src/plugins/wasm_bridge.rs b/zellij-server/src/plugins/wasm_bridge.rs index 47b6e493..31d7e74b 100644 --- a/zellij-server/src/plugins/wasm_bridge.rs +++ b/zellij-server/src/plugins/wasm_bridge.rs @@ -287,7 +287,7 @@ impl WasmBridge { skip_cache: bool, client_id: Option<ClientId>, ) -> 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 026be937..24113d0b 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<String start_suppressed, coordinates: None, near_current_pane: false, + tab_id: None, }; let result = apply_action!(action, error_msg, env); @@ -1505,6 +1507,7 @@ fn open_file_floating( start_suppressed, coordinates: floating_pane_coordinates, near_current_pane: false, + tab_id: None, }; let result = apply_action!(action, error_msg, env); @@ -1545,6 +1548,7 @@ fn open_file_in_place( start_suppressed, coordinates: None, near_current_pane: false, + tab_id: None, }; let result = apply_action!(action, error_msg, env); @@ -1705,6 +1709,7 @@ fn open_terminal(env: &PluginEnv, cwd: PathBuf) { pane_name: None, near_current_pane: false, borderless: None, + tab_id: None, }; let result = apply_action!(action, error_msg, env); @@ -1780,6 +1785,7 @@ fn open_terminal_floating( pane_name: None, coordinates: floating_pane_coordinates, near_current_pane: false, + tab_id: None, }; let result = apply_action!(action, error_msg, env); @@ -1855,6 +1861,7 @@ fn open_terminal_in_place(env: &PluginEnv, cwd: PathBuf) { near_current_pane: false, pane_id_to_replace: None, close_replaced_pane: false, + tab_id: None, }; let result = apply_action!(action, error_msg, env); @@ -2139,6 +2146,7 @@ fn open_command_pane( pane_name: name, near_current_pane: false, borderless: None, + tab_id: None, }; let result = apply_action!(action, error_msg, env); @@ -2246,6 +2254,7 @@ fn open_command_pane_floating( pane_name: name, coordinates: floating_pane_coordinates, near_current_pane: false, + tab_id: None, }; let result = apply_action!(action, error_msg, env); @@ -2356,6 +2365,7 @@ fn open_command_pane_in_place( near_current_pane: false, pane_id_to_replace: None, close_replaced_pane: false, + tab_id: None, }; let result = apply_action!(action, error_msg, env); @@ -4423,6 +4433,7 @@ fn try_edit_layout( start_suppressed: false, coordinates: None, near_current_pane: true, + tab_id: None, }; // Route the action - this is fallible diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index cb124c65..2ffbd5cb 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -637,6 +637,7 @@ pub(crate) fn route_action( command, unblock_condition, near_current_pane, + tab_id, } => { 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 0be349f0..6f1bf70b 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -547,6 +547,7 @@ pub enum ScreenInstruction { Option<PathBuf>, ClientId, Option<NotificationEnd>, + Option<usize>, // tab_id ), // Option<String> is // optional pane title, bool is skip cache, Option<PathBuf> is an optional cwd NewFloatingPluginPane( @@ -557,6 +558,7 @@ pub enum ScreenInstruction { Option<FloatingPaneCoordinates>, ClientId, Option<NotificationEnd>, + Option<usize>, // tab_id ), // Option<String> is an // optional pane title, bool // is skip cache, Option<PathBuf> is an optional cwd @@ -568,6 +570,7 @@ pub enum ScreenInstruction { bool, ClientId, Option<NotificationEnd>, + Option<usize>, // tab_id ), // Option<String> is an // optional pane title, first bool is skip cache, second bool is close_replaced_pane StartOrReloadPluginPane(RunPluginOrAlias, Option<String>, Option<NotificationEnd>), @@ -601,6 +604,7 @@ pub enum ScreenInstruction { bool, ClientId, Option<NotificationEnd>, + Option<usize>, // tab_id ), // bools are: should_float, move_to_focused_tab, should_open_in_place, close_replaced_pane, Option<PaneId> is the pane id to replace, bool following it is skip_cache LaunchPlugin( RunPluginOrAlias, @@ -612,6 +616,7 @@ pub enum ScreenInstruction { Option<PathBuf>, ClientId, Option<NotificationEnd>, + Option<usize>, // tab_id ), // bools are: should_float, should_open_in_place, close_replaced_pane, Option<PaneId> is the pane id to replace, Option<PathBuf> 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 491beefb..0897ec49 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 00000000..b12db36c --- /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 00000000..b12db36c --- /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, + }, + ), +) |
