aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2026-03-30 16:02:09 +0200
committerGitHub <noreply@github.com>2026-03-30 16:02:09 +0200
commit8ce05f0bbd58acbbca95f95ae14fdc4e866f7391 (patch)
tree57c6d9ee2231a556f938df4f2629511a711518df
parentdocs(changelog): recent windows fixes (diff)
downloadsl0ppij-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
-rw-r--r--CHANGELOG.md1
-rw-r--r--default-plugins/compact-bar/src/keybind_utils.rs1
-rw-r--r--default-plugins/status-bar/src/one_line_ui.rs2
-rw-r--r--default-plugins/status-bar/src/second_line.rs2
-rw-r--r--src/main.rs11
-rw-r--r--zellij-client/src/lib.rs36
-rw-r--r--zellij-server/src/plugins/wasm_bridge.rs21
-rw-r--r--zellij-server/src/plugins/zellij_exports.rs11
-rw-r--r--zellij-server/src/route.rs176
-rw-r--r--zellij-server/src/screen.rs244
-rw-r--r--zellij-server/src/unit/screen_tests.rs414
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_layout_string.snap119
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_layout_string_and_name.snap119
-rw-r--r--zellij-utils/assets/prost_ipc/client_server_contract.rs22
-rw-r--r--zellij-utils/src/cli.rs70
-rw-r--r--zellij-utils/src/client_server_contract/common_types.proto11
-rw-r--r--zellij-utils/src/input/actions.rs731
-rw-r--r--zellij-utils/src/ipc/protobuf_conversion.rs44
-rw-r--r--zellij-utils/src/ipc/tests/roundtrip_tests.rs181
-rw-r--r--zellij-utils/src/kdl/mod.rs15
-rw-r--r--zellij-utils/src/plugin_api/action.rs24
-rw-r--r--zellij-utils/src/setup.rs25
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap7
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap7
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap7
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap7
26 files changed, 2084 insertions, 224 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ad47444a..76ad03df 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 77599d53..692dde0f 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 33558725..d997fb36 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<KeyWithModifier
(s("Toggle Embed"), s("Embed"), single_action_key(&km, &[A::TogglePaneEmbedOrFloating, TO_NORMAL])),
(s("Split Right"), s("Right"), single_action_key(&km, &[A::NewPane{direction: Some(Direction::Right), pane_name: None, start_suppressed: false}, TO_NORMAL])),
(s("Split Down"), s("Down"), single_action_key(&km, &[A::NewPane{direction: Some(Direction::Down), pane_name: None, start_suppressed: false}, TO_NORMAL])),
- (s("Stack"), s("Stack"), single_action_key(&km, &[A::NewStackedPane{command: None, pane_name: None, near_current_pane: false}, TO_NORMAL])),
+ (s("Stack"), s("Stack"), single_action_key(&km, &[A::NewStackedPane{command: None, pane_name: None, near_current_pane: false, tab_id: None}, TO_NORMAL])),
(s("Select pane"), s("Select"), to_basemode_key),
]} else if mi.mode == IM::Tab {
// With the default bindings, "Move focus" for tabs is tricky: It binds all the arrow keys
diff --git a/default-plugins/status-bar/src/second_line.rs b/default-plugins/status-bar/src/second_line.rs
index 0ac127a4..921f3c75 100644
--- a/default-plugins/status-bar/src/second_line.rs
+++ b/default-plugins/status-bar/src/second_line.rs
@@ -254,7 +254,7 @@ fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec<KeyWithModifier
action_key(&km, &[A::SearchToggleOption{option: SOpt::WholeWord}])),
]} else if mi.mode == IM::Session { vec![
(s("Detach"), s("Detach"), action_key(&km, &[Action::Detach])),
- (s("Session Manager"), s("Manager"), action_key(&km, &[A::LaunchOrFocusPlugin{plugin: Default::default(), should_float: true, move_to_focused_tab: true, should_open_in_place: false, close_replaced_pane: false, skip_cache: false}, TO_NORMAL])), // not entirely accurate
+ (s("Session Manager"), s("Manager"), action_key(&km, &[A::LaunchOrFocusPlugin{plugin: Default::default(), should_float: true, move_to_focused_tab: true, should_open_in_place: false, close_replaced_pane: false, skip_cache: false, tab_id: None}, TO_NORMAL])), // not entirely accurate
(s("Select pane"), s("Select"), to_normal_key),
]} else if mi.mode == IM::Tmux { vec![
(s("Move focus"), s("Move"), action_key_group(&km, &[
diff --git a/src/main.rs b/src/main.rs
index 7faf7f77..c6ebebea 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -51,6 +51,7 @@ fn main() {
block_until_exit,
near_current_pane,
borderless,
+ tab_id,
})) = opts.command
{
let cwd = cwd.or_else(|| std::env::current_dir().ok());
@@ -93,6 +94,7 @@ fn main() {
unblock_condition,
near_current_pane,
borderless,
+ tab_id,
};
commands::send_action_to_session(command_cli_action, opts.session, config);
std::process::exit(0);
@@ -110,6 +112,7 @@ fn main() {
height,
pinned,
borderless,
+ tab_id,
})) = opts.command
{
let cwd = None;
@@ -142,6 +145,7 @@ fn main() {
unblock_condition,
near_current_pane: false,
borderless,
+ tab_id,
};
commands::send_action_to_session(command_cli_action, opts.session, config);
std::process::exit(0);
@@ -161,6 +165,7 @@ fn main() {
pinned,
near_current_pane,
borderless,
+ tab_id,
})) = opts.command
{
let mut file = file;
@@ -185,6 +190,7 @@ fn main() {
pinned,
near_current_pane,
borderless,
+ tab_id,
};
commands::send_action_to_session(command_cli_action, opts.session, config);
std::process::exit(0);
@@ -256,7 +262,7 @@ fn main() {
commands::delete_session(target_session, force);
} else if let Some(path) = opts.server {
commands::start_server(path, opts.debug);
- } else if let Some(layout) = &opts.layout {
+ } else if opts.layout.is_some() || opts.layout_string.is_some() {
if let Some(session_name) = opts
.session
.as_ref()
@@ -266,7 +272,8 @@ fn main() {
let config = Config::try_from(&opts).ok();
let options = Setup::from_cli_args(&opts).ok().map(|r| r.2);
let new_layout_cli_action = CliAction::NewTab {
- layout: Some(layout.clone()),
+ layout: opts.layout.clone(),
+ layout_string: opts.layout_string.clone(),
layout_dir: options.as_ref().and_then(|o| o.layout_dir.clone()),
name: None,
cwd: options.as_ref().and_then(|o| o.default_cwd.clone()),
diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs
index 482a105d..b4fb2e15 100644
--- a/zellij-client/src/lib.rs
+++ b/zellij-client/src/lib.rs
@@ -772,22 +772,26 @@ pub fn start_client(
config_dir: cli_args.config_dir.clone(),
should_ignore_config: cli_args.is_setup_clean(),
configuration_options: Some(config_options.clone()),
- layout: cli_args
- .layout
- .as_ref()
- .and_then(|l| {
- LayoutInfo::from_cli(
- &config_options.layout_dir,
- &Some(l.clone()),
- std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
- )
- })
- .or_else(|| {
- LayoutInfo::from_config(
- &config_options.layout_dir,
- &config_options.default_layout,
- )
- }),
+ layout: if let Some(layout_string) = &cli_args.layout_string {
+ Some(LayoutInfo::Stringified(layout_string.clone()))
+ } else {
+ cli_args
+ .layout
+ .as_ref()
+ .and_then(|l| {
+ LayoutInfo::from_cli(
+ &config_options.layout_dir,
+ &Some(l.clone()),
+ std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
+ )
+ })
+ .or_else(|| {
+ LayoutInfo::from_config(
+ &config_options.layout_dir,
+ &config_options.default_layout,
+ )
+ })
+ },
terminal_window_size: full_screen_ws,
data_dir: cli_args.data_dir.clone(),
is_debug: cli_args.debug,
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,
+ },
+ ),
+)
diff --git a/zellij-utils/assets/prost_ipc/client_server_contract.rs b/zellij-utils/assets/prost_ipc/client_server_contract.rs
index d1425cfc..5ae8460b 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<u32>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
@@ -820,6 +822,8 @@ pub struct NewFloatingPaneAction {
pub coordinates: ::core::option::Option<FloatingPaneCoordinates>,
#[prost(bool, tag="7")]
pub near_current_pane: bool,
+ #[prost(uint32, optional, tag="8")]
+ pub tab_id: ::core::option::Option<u32>,
}
#[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<bool>,
+ #[prost(uint32, optional, tag="9")]
+ pub tab_id: ::core::option::Option<u32>,
}
#[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<PaneId>,
#[prost(bool, tag="5")]
pub close_replaced_pane: bool,
+ #[prost(uint32, optional, tag="6")]
+ pub tab_id: ::core::option::Option<u32>,
}
#[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<u32>,
}
#[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<i32>,
#[prost(bool, tag="5")]
pub near_current_pane: bool,
+ #[prost(uint32, optional, tag="6")]
+ pub tab_id: ::core::option::Option<u32>,
}
#[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<u32>,
}
#[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<u32>,
}
#[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<u32>,
}
#[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<FloatingPaneCoordinates>,
+ #[prost(uint32, optional, tag="6")]
+ pub tab_id: ::core::option::Option<u32>,
}
#[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<u32>,
}
#[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 12117265..f02ace98 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<PathBuf>,
+ /// 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<String>,
+
/// 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<bool>,
+ /// Target a specific tab by ID
+ #[clap(
+ long,
+ value_parser,
+ conflicts_with("near-current-pane"),
+ conflicts_with("in-place")
+ )]
+ tab_id: Option<usize>,
},
/// Load a plugin
/// Returns: Created pane ID (format: plugin_<id>)
@@ -575,6 +589,9 @@ pub enum Sessions {
/// mouse)
#[clap(short, long, value_parser)]
borderless: Option<bool>,
+ /// Target a specific tab by ID
+ #[clap(long, value_parser, conflicts_with("in-place"))]
+ tab_id: Option<usize>,
},
/// Edit file with default $EDITOR / $VISUAL
/// Returns: Created pane ID (format: terminal_<id>)
@@ -641,6 +658,14 @@ pub enum Sessions {
/// mouse)
#[clap(short, long, value_parser)]
borderless: Option<bool>,
+ /// Target a specific tab by ID
+ #[clap(
+ long,
+ value_parser,
+ conflicts_with("near-current-pane"),
+ conflicts_with("in-place")
+ )]
+ tab_id: Option<usize>,
},
ConvertConfig {
old_config_file: PathBuf,
@@ -1010,6 +1035,14 @@ pub enum CliAction {
/// mouse)
#[clap(long, value_parser)]
borderless: Option<bool>,
+ /// Target a specific tab by ID
+ #[clap(
+ long,
+ value_parser,
+ conflicts_with("near-current-pane"),
+ conflicts_with("in-place")
+ )]
+ tab_id: Option<usize>,
},
/// Open the specified file in a new zellij pane with your default EDITOR
/// Returns: Created pane ID (format: terminal_<id>)
@@ -1075,6 +1108,14 @@ pub enum CliAction {
/// mouse)
#[clap(short, long, value_parser)]
borderless: Option<bool>,
+ /// Target a specific tab by ID
+ #[clap(
+ long,
+ value_parser,
+ conflicts_with("near-current-pane"),
+ conflicts_with("in-place")
+ )]
+ tab_id: Option<usize>,
},
/// 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<PathBuf>,
+ /// Raw KDL layout string to use directly (instead of a layout file path)
+ #[clap(long, value_parser, conflicts_with = "layout")]
+ layout_string: Option<String>,
+
/// Default folder to look for layouts
#[clap(long, value_parser, requires("layout"))]
layout_dir: Option<PathBuf>,
@@ -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<PathBuf>,
+
+ /// Raw KDL layout string to use directly (instead of a layout file path)
+ #[clap(long, value_parser, conflicts_with = "layout")]
+ layout_string: Option<String>,
/// Default folder to look for layouts
#[clap(long, value_parser)]
@@ -1341,6 +1394,9 @@ pub enum CliAction {
configuration: Option<PluginUserConfiguration>,
#[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<usize>,
},
/// Returns: Plugin pane ID (format: plugin_<id>)
LaunchPlugin {
@@ -1362,6 +1418,9 @@ pub enum CliAction {
configuration: Option<PluginUserConfiguration>,
#[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<usize>,
},
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<String>,
/// 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<PathBuf>,
+ /// Raw KDL layout string to use directly
+ #[clap(long, value_parser, conflicts_with = "layout")]
+ layout_string: Option<String>,
/// Default folder to look for layouts
#[clap(long, value_parser, requires("layout"))]
layout_dir: Option<PathBuf>,
diff --git a/zellij-utils/src/client_server_contract/common_types.proto b/zellij-utils/src/client_server_contract/common_types.proto
index 6a6f211c..e610eb2a 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 6dda2b7e..0ce1c507 100644
--- a/zellij-utils/src/input/actions.rs
+++ b/zellij-utils/src/input/actions.rs
@@ -232,6 +232,7 @@ pub enum Action {
command: Option<RunCommandAction>,
unblock_condition: Option<UnblockCondition>,
near_current_pane: bool,
+ tab_id: Option<usize>,
},
/// Open the file in a new pane using the default editor
/// Returns: Created pane ID (format: terminal_<id>)
@@ -244,6 +245,7 @@ pub enum Action {
start_suppressed: bool,
coordinates: Option<FloatingPaneCoordinates>,
near_current_pane: bool,
+ tab_id: Option<usize>,
},
/// Open a new floating pane
/// Returns: Created pane ID (format: terminal_<id> or plugin_<id>)
@@ -252,6 +254,7 @@ pub enum Action {
pane_name: Option<String>,
coordinates: Option<FloatingPaneCoordinates>,
near_current_pane: bool,
+ tab_id: Option<usize>,
},
/// Open a new tiled (embedded, non-floating) pane
/// Returns: Created pane ID (format: terminal_<id> or plugin_<id>)
@@ -261,6 +264,7 @@ pub enum Action {
pane_name: Option<String>,
near_current_pane: bool,
borderless: Option<bool>,
+ tab_id: Option<usize>,
},
/// Open a new pane in place of the focused one, suppressing it instead
/// Returns: Created pane ID (format: terminal_<id> or plugin_<id>)
@@ -270,12 +274,14 @@ pub enum Action {
near_current_pane: bool,
pane_id_to_replace: Option<PaneId>,
close_replaced_pane: bool,
+ tab_id: Option<usize>,
},
/// Returns: Created pane ID (format: terminal_<id> or plugin_<id>)
NewStackedPane {
command: Option<RunCommandAction>,
pane_name: Option<String>,
near_current_pane: bool,
+ tab_id: Option<usize>,
},
/// 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<usize>,
},
/// Returns: Plugin pane ID (format: plugin_<id>)
LaunchPlugin {
@@ -372,6 +379,7 @@ pub enum Action {
close_replaced_pane: bool,
skip_cache: bool,
cwd: Option<PathBuf>,
+ tab_id: Option<usize>,
},
MouseEvent {
event: MouseEvent,
@@ -416,6 +424,7 @@ pub enum Action {
pane_name: Option<String>,
skip_cache: bool,
cwd: Option<PathBuf>,
+ tab_id: Option<usize>,
},
/// Returns: Created pane ID (format: plugin_<id>)
NewFloatingPluginPane {
@@ -424,6 +433,7 @@ pub enum Action {
skip_cache: bool,
cwd: Option<PathBuf>,
coordinates: Option<FloatingPaneCoordinates>,
+ tab_id: Option<usize>,
},
/// Returns: Created pane ID (format: plugin_<id>)
NewInPlacePluginPane {
@@ -431,6 +441,7 @@ pub enum Action {
pane_name: Option<String>,
skip_cache: bool,
close_replaced_pane: bool,
+ tab_id: Option<usize>,
},
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,12 +1448,103 @@ impl Action {
} else {
None
};
- if let Some(layout_path) = layout {
+ 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_source_name.clone(), 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.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://") {
@@ -1436,6 +1554,7 @@ impl Action {
}
}) {
should_start_layout_commands_suspended = true;
+ layout_source_name = layout_url.to_owned();
(
layout_url.to_owned(),
Layout::stringified_from_url(layout_url)
@@ -1443,13 +1562,18 @@ impl Action {
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_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(), String::from(raw_layout));
let report: Report = error.into();
format!("{:?}", report)
}
@@ -1466,7 +1590,7 @@ 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(), 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 5c78a445..189c1370 100644
--- a/zellij-utils/src/ipc/protobuf_conversion.rs
+++ b/zellij-utils/src/ipc/protobuf_conversion.rs
@@ -1095,6 +1095,8 @@ impl From<crate::input::actions::Action>
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<crate::input::actions::Action>
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<crate::input::actions::Action>
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<crate::input::actions::Action>
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<crate::input::actions::Action>
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<crate::input::actions::Action>
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<crate::input::actions::Action>
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<crate::input::actions::Action>
close_replaced_pane,
skip_cache,
cwd,
+ tab_id,
+ ..
} => ActionType::LaunchPlugin(LaunchPluginAction {
plugin: Some(plugin.into()),
should_float,
@@ -1305,6 +1328,7 @@ impl From<crate::input::actions::Action>
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<crate::input::actions::Action>
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<crate::input::actions::Action>
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<crate::client_server_contract::client_server_contract::Action>
.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<crate::client_server_contract::client_server_contract::Action>
.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<crate::client_server_contract::client_server_contract::Action>
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<crate::client_server_contract::client_server_contract::Action>
.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<crate::client_server_contract::client_server_contract::Action>
.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<crate::client_server_contract::client_server_contract::Action>
.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<crate::client_server_contract::client_server_contract::Action>
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<crate::client_server_contract::client_server_contract::Action>
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<crate::client_server_contract::client_server_contract::Action>
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<crate::client_server_contract::client_server_contract::Action>
.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<crate::client_server_contract::client_server_contract::Action>
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 cfe882a1..7ef7ac89 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 919c865f..2b009e1a 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 4384eb68..cbd62e5b 100644
--- a/zellij-utils/src/plugin_api/action.rs
+++ b/zellij-utils/src/plugin_api/action.rs
@@ -338,6 +338,7 @@ impl TryFrom<ProtobufAction> 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<ProtobufAction> for Action {
pane_name,
coordinates: None,
near_current_pane,
+ tab_id: None,
})
} else {
Ok(Action::NewFloatingPane {
@@ -360,6 +362,7 @@ impl TryFrom<ProtobufAction> for Action {
pane_name: None,
coordinates: None,
near_current_pane,
+ tab_id: None,
})
}
},
@@ -382,6 +385,7 @@ impl TryFrom<ProtobufAction> for Action {
pane_name,
near_current_pane,
borderless,
+ tab_id: None,
})
} else {
Ok(Action::NewTiledPane {
@@ -390,6 +394,7 @@ impl TryFrom<ProtobufAction> for Action {
pane_name: None,
near_current_pane,
borderless,
+ tab_id: None,
})
}
},
@@ -658,6 +663,7 @@ impl TryFrom<ProtobufAction> 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<ProtobufAction> 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<ProtobufAction> 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<ProtobufAction> 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<ProtobufAction> 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<ProtobufAction> for Action {
command,
unblock_condition,
near_current_pane,
+ tab_id: None,
})
},
_ => Err("Wrong payload for Action::NewBlockingPane"),
@@ -1024,6 +1035,7 @@ impl TryFrom<ProtobufAction> 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<ProtobufAction> for Action {
near_current_pane,
pane_id_to_replace,
close_replaced_pane,
+ tab_id: None,
})
}
},
@@ -1298,6 +1311,7 @@ impl TryFrom<Action> 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<Action> 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<Action> 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<Action> 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<Action> 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<Action> 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<Action> 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<Action> for ProtobufAction {
command: _,
pane_name: _,
near_current_pane: _,
+ ..
} => Ok(ProtobufAction {
name: ProtobufActionName::NewStackedPane as i32,
optional_payload: None,
@@ -1816,6 +1837,7 @@ impl TryFrom<Action> 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<Action> 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<Action> 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 6d74187d..d2eafe7e 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 ff0a3ae4..243ea7ba 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 04e30a09..6c352256 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 9cb4295d..027cee69 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 eb72f108..70b23c18 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,