aboutsummaryrefslogtreecommitdiffstats
path: root/zellij-utils/src
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 /zellij-utils/src
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
Diffstat (limited to 'zellij-utils/src')
-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
12 files changed, 1102 insertions, 27 deletions
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,