diff --git a/CHANGELOG.md b/CHANGELOG.md index ba590b58..e477da10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * feat: add "stack" keybinding and CLI action to add a stacked pane to the current pane (https://github.com/zellij-org/zellij/pull/4255) * fix: support multiline hyperlinks (https://github.com/zellij-org/zellij/pull/4264) * fix: use terminal title when spawning terminal panes from plugin (https://github.com/zellij-org/zellij/pull/4272) +* fix: allow specifying CWD for tabs without necessitating a layout (https://github.com/zellij-org/zellij/pull/4273) ## [0.42.2] - 2025-04-15 * refactor(terminal): track scroll_region as tuple rather than Option (https://github.com/zellij-org/zellij/pull/4082) diff --git a/default-plugins/compact-bar/src/keybind_utils.rs b/default-plugins/compact-bar/src/keybind_utils.rs index c871d7c5..2a86dc88 100644 --- a/default-plugins/compact-bar/src/keybind_utils.rs +++ b/default-plugins/compact-bar/src/keybind_utils.rs @@ -348,7 +348,10 @@ impl KeybindProcessor { |action: &Action| matches!(action, Action::GoToPreviousTab), |action: &Action| matches!(action, Action::GoToNextTab), |action: &Action| { - matches!(action, Action::NewTab(None, _, None, None, None, true)) + matches!( + action, + Action::NewTab(None, _, None, None, None, true, None) + ) }, |action: &Action| matches!(action, Action::CloseTab), |action: &Action| matches!(action, Action::SwitchToMode(InputMode::RenameTab)), diff --git a/default-plugins/fixture-plugin-for-tests/src/main.rs b/default-plugins/fixture-plugin-for-tests/src/main.rs index 7f66fa4e..47e1295a 100644 --- a/default-plugins/fixture-plugin-for-tests/src/main.rs +++ b/default-plugins/fixture-plugin-for-tests/src/main.rs @@ -92,7 +92,9 @@ impl ZellijPlugin for State { }", ); }, - BareKey::Char('c') if key.has_no_modifiers() => new_tab(), + BareKey::Char('c') if key.has_no_modifiers() => { + new_tab(Some("new_tab_name"), Some("/path/to/my/cwd")) + }, BareKey::Char('d') if key.has_no_modifiers() => go_to_next_tab(), BareKey::Char('e') if key.has_no_modifiers() => go_to_previous_tab(), BareKey::Char('f') if key.has_no_modifiers() => { diff --git a/default-plugins/status-bar/src/one_line_ui.rs b/default-plugins/status-bar/src/one_line_ui.rs index 1902ac63..8ab5687d 100644 --- a/default-plugins/status-bar/src/one_line_ui.rs +++ b/default-plugins/status-bar/src/one_line_ui.rs @@ -1296,7 +1296,7 @@ fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec Vec<(String, String, Vec Vec<(String, String, Vec Vec<(String, String, Vec) { PluginCommand::NewTabsWithLayoutInfo(layout_info) => { new_tabs_with_layout_info(env, layout_info)? }, - PluginCommand::NewTab => new_tab(env), + PluginCommand::NewTab { name, cwd } => new_tab(env, name, cwd), PluginCommand::GoToNextTab => go_to_next_tab(env), PluginCommand::GoToPreviousTab => go_to_previous_tab(env), PluginCommand::Resize(resize_payload) => resize(env, resize_payload), @@ -1451,6 +1451,7 @@ fn new_tabs_with_layout_info(env: &PluginEnv, layout_info: LayoutInfo) -> Result fn apply_layout(env: &PluginEnv, layout: Layout) { let mut tabs_to_open = vec![]; let tabs = layout.tabs(); + let cwd = None; // TODO: add this to the plugin API if tabs.is_empty() { let swap_tiled_layouts = Some(layout.swap_tiled_layouts.clone()); let swap_floating_layouts = Some(layout.swap_floating_layouts.clone()); @@ -1461,6 +1462,7 @@ fn apply_layout(env: &PluginEnv, layout: Layout) { swap_floating_layouts, None, true, + cwd, ); tabs_to_open.push(action); } else { @@ -1478,6 +1480,7 @@ fn apply_layout(env: &PluginEnv, layout: Layout) { swap_floating_layouts, tab_name, should_focus_tab, + cwd.clone(), ); tabs_to_open.push(action); } @@ -1488,8 +1491,9 @@ fn apply_layout(env: &PluginEnv, layout: Layout) { } } -fn new_tab(env: &PluginEnv) { - let action = Action::NewTab(None, vec![], None, None, None, true); +fn new_tab(env: &PluginEnv, name: Option, cwd: Option) { + let cwd = cwd.map(|c| PathBuf::from(c)); + let action = Action::NewTab(None, vec![], None, None, name, true, cwd); let error_msg = || format!("Failed to open new tab"); apply_action!(action, error_msg, env); } @@ -2575,7 +2579,7 @@ fn check_command_permission( | PluginCommand::SwitchToMode(..) | PluginCommand::NewTabsWithLayout(..) | PluginCommand::NewTabsWithLayoutInfo(..) - | PluginCommand::NewTab + | PluginCommand::NewTab { .. } | PluginCommand::GoToNextTab | PluginCommand::GoToPreviousTab | PluginCommand::Resize(..) diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index 678c1558..b21535b0 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -462,6 +462,7 @@ pub(crate) fn route_action( swap_floating_layouts, tab_name, should_change_focus_to_new_tab, + cwd, ) => { let shell = default_shell.clone(); let swap_tiled_layouts = @@ -471,7 +472,7 @@ pub(crate) fn route_action( let is_web_client = false; // actions cannot be initiated directly from the web senders .send_to_screen(ScreenInstruction::NewTab( - None, + cwd, shell, tab_layout, floating_panes_layout, diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs index 608fa2e7..b81b410b 100644 --- a/zellij-tile/src/shim.rs +++ b/zellij-tile/src/shim.rs @@ -434,8 +434,13 @@ pub fn new_tabs_with_layout_info(layout_info: LayoutInfo) { } /// Open a new tab with the default layout -pub fn new_tab() { - let plugin_command = PluginCommand::NewTab; +pub fn new_tab>(name: Option, cwd: Option) +where + S: ToString, +{ + let name = name.map(|s| s.to_string()); + let cwd = cwd.map(|s| s.to_string()); + let plugin_command = PluginCommand::NewTab { name, cwd }; let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); object_to_stdout(&protobuf_plugin_command.encode_to_vec()); unsafe { host_run_plugin_command() }; diff --git a/zellij-utils/assets/prost/api.plugin_command.rs b/zellij-utils/assets/prost/api.plugin_command.rs index e3c63626..0c290fc3 100644 --- a/zellij-utils/assets/prost/api.plugin_command.rs +++ b/zellij-utils/assets/prost/api.plugin_command.rs @@ -3,7 +3,7 @@ pub struct PluginCommand { #[prost(enumeration="CommandName", tag="1")] pub name: i32, - #[prost(oneof="plugin_command::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111")] + #[prost(oneof="plugin_command::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112")] pub payload: ::core::option::Option, } /// Nested message and enum types in `PluginCommand`. @@ -213,10 +213,20 @@ pub mod plugin_command { RenameWebLoginTokenPayload(super::RenameWebLoginTokenPayload), #[prost(message, tag="111")] ReplacePaneWithExistingPanePayload(super::ReplacePaneWithExistingPanePayload), + #[prost(message, tag="112")] + NewTabPayload(super::NewTabPayload), } } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct NewTabPayload { + #[prost(string, optional, tag="1")] + pub name: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag="2")] + pub cwd: ::core::option::Option<::prost::alloc::string::String>, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct ReplacePaneWithExistingPanePayload { #[prost(message, optional, tag="1")] pub pane_id_to_replace: ::core::option::Option, diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs index 164f5999..b85d975f 100644 --- a/zellij-utils/src/cli.rs +++ b/zellij-utils/src/cli.rs @@ -722,7 +722,7 @@ pub enum CliAction { name: Option, /// Change the working directory of the new tab - #[clap(short, long, value_parser, requires("layout"))] + #[clap(short, long, value_parser)] cwd: Option, }, /// Move the focused tab in the specified direction. [right|left] diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs index 0520e15d..190011f9 100644 --- a/zellij-utils/src/data.rs +++ b/zellij-utils/src/data.rs @@ -2344,7 +2344,10 @@ pub enum PluginCommand { ShowSelf(bool), // bool - should float if hidden SwitchToMode(InputMode), NewTabsWithLayout(String), // raw kdl layout - NewTab, + NewTab { + name: Option, + cwd: Option, + }, GoToNextTab, GoToPreviousTab, Resize(Resize), diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index 38c46833..5f2b2af0 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -198,7 +198,8 @@ pub enum Action { Option>, Option>, Option, - bool, // should_change_focus_to_new_tab + bool, // should_change_focus_to_new_tab + Option, // cwd ), // the String is the tab name /// Do nothing. NoOp, @@ -621,6 +622,7 @@ impl Action { swap_floating_layouts.clone(), name, should_change_focus_to_new_tab, + None, // the cwd is done through the layout )); } Ok(new_tab_actions) @@ -636,6 +638,7 @@ impl Action { swap_floating_layouts, name, should_change_focus_to_new_tab, + None, // the cwd is done through the layout )]) } } else { @@ -647,6 +650,7 @@ impl Action { None, name, should_change_focus_to_new_tab, + cwd, )]) } }, diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs index 724f52be..ec6abf72 100644 --- a/zellij-utils/src/kdl/mod.rs +++ b/zellij-utils/src/kdl/mod.rs @@ -696,11 +696,10 @@ impl Action { Some(node) }, Action::UndoRenamePane => Some(KdlNode::new("UndoRenamePane")), - Action::NewTab(_, _, _, _, name, should_change_focus_to_new_tab) => { - log::warn!("Converting new tab action without arguments, original action saved to .bak.kdl file"); + Action::NewTab(_, _, _, _, name, should_change_focus_to_new_tab, cwd) => { let mut node = KdlNode::new("NewTab"); + let mut children = KdlDocument::new(); if let Some(name) = name { - let mut children = KdlDocument::new(); let mut name_node = KdlNode::new("name"); if !should_change_focus_to_new_tab { let mut should_change_focus_to_new_tab_node = @@ -712,6 +711,13 @@ impl Action { } name_node.push(name.clone()); children.nodes_mut().push(name_node); + } + if let Some(cwd) = cwd { + let mut cwd_node = KdlNode::new("cwd"); + cwd_node.push(cwd.display().to_string()); + children.nodes_mut().push(cwd_node); + } + if name.is_some() || cwd.is_some() { node.set_children(children); } Some(node) @@ -1472,7 +1478,7 @@ impl TryFrom<(&KdlNode, &Options)> for Action { "NewTab" => { let command_metadata = action_children.iter().next(); if command_metadata.is_none() { - return Ok(Action::NewTab(None, vec![], None, None, None, true)); + return Ok(Action::NewTab(None, vec![], None, None, None, true, None)); } let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); @@ -1508,7 +1514,7 @@ impl TryFrom<(&KdlNode, &Options)> for Action { &raw_layout, path_to_raw_layout, swap_layouts.as_ref().map(|(f, p)| (f.as_str(), p.as_str())), - cwd, + cwd.clone(), ) .map_err(|e| { ConfigError::new_kdl_error( @@ -1540,6 +1546,7 @@ impl TryFrom<(&KdlNode, &Options)> for Action { swap_floating_layouts, name, should_change_focus_to_new_tab, + cwd, )) } else { let (layout, floating_panes_layout) = layout.new_tab(); @@ -1552,6 +1559,7 @@ impl TryFrom<(&KdlNode, &Options)> for Action { swap_floating_layouts, name, should_change_focus_to_new_tab, + cwd, )) } }, diff --git a/zellij-utils/src/plugin_api/action.rs b/zellij-utils/src/plugin_api/action.rs index d018ea1b..3aaa6ee1 100644 --- a/zellij-utils/src/plugin_api/action.rs +++ b/zellij-utils/src/plugin_api/action.rs @@ -315,7 +315,7 @@ impl TryFrom for Action { Some(_) => Err("NewTab should not have a payload"), None => { // we do not serialize the layouts of this action - Ok(Action::NewTab(None, vec![], None, None, None, true)) + Ok(Action::NewTab(None, vec![], None, None, None, true, None)) }, } }, diff --git a/zellij-utils/src/plugin_api/plugin_command.proto b/zellij-utils/src/plugin_api/plugin_command.proto index b1797214..d46ba1e3 100644 --- a/zellij-utils/src/plugin_api/plugin_command.proto +++ b/zellij-utils/src/plugin_api/plugin_command.proto @@ -265,9 +265,15 @@ message PluginCommand { RevokeWebLoginTokenPayload revoke_web_login_token_payload = 109; RenameWebLoginTokenPayload rename_web_login_token_payload = 110; ReplacePaneWithExistingPanePayload replace_pane_with_existing_pane_payload = 111; + NewTabPayload new_tab_payload = 112; } } +message NewTabPayload { + optional string name = 1; + optional string cwd = 2; +} + message ReplacePaneWithExistingPanePayload { PaneId pane_id_to_replace = 1; PaneId existing_pane_id = 2; diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs index 296a8a74..073284bc 100644 --- a/zellij-utils/src/plugin_api/plugin_command.rs +++ b/zellij-utils/src/plugin_api/plugin_command.rs @@ -16,7 +16,7 @@ pub use super::generated_api::api::{ HttpVerb as ProtobufHttpVerb, IdAndNewName, KeyToRebind, KeyToUnbind, KillSessionsPayload, ListTokensResponse, LoadNewPluginPayload, MessageToPluginPayload, MovePaneWithPaneIdInDirectionPayload, MovePaneWithPaneIdPayload, MovePayload, - NewPluginArgs as ProtobufNewPluginArgs, NewTabsWithLayoutInfoPayload, + NewPluginArgs as ProtobufNewPluginArgs, NewTabPayload, NewTabsWithLayoutInfoPayload, OpenCommandPaneFloatingNearPluginPayload, OpenCommandPaneInPlaceOfPluginPayload, OpenCommandPaneNearPluginPayload, OpenCommandPanePayload, OpenFileFloatingNearPluginPayload, OpenFileInPlaceOfPluginPayload, @@ -474,11 +474,18 @@ impl TryFrom for PluginCommand { }, _ => Err("Mismatched payload for NewTabsWithLayout"), }, - Some(CommandName::NewTab) => { - if protobuf_plugin_command.payload.is_some() { - return Err("NewTab should not have a payload"); - } - Ok(PluginCommand::NewTab) + Some(CommandName::NewTab) => match protobuf_plugin_command.payload { + Some(Payload::NewTabPayload(protobuf_new_tab_payload)) => { + Ok(PluginCommand::NewTab { + name: protobuf_new_tab_payload.name, + cwd: protobuf_new_tab_payload.cwd, + }) + }, + None => Ok(PluginCommand::NewTab { + name: None, + cwd: None, + }), + _ => Err("Mismatched payload for NewTab"), }, Some(CommandName::GoToNextTab) => { if protobuf_plugin_command.payload.is_some() { @@ -1886,9 +1893,9 @@ impl TryFrom for ProtobufPluginCommand { name: CommandName::NewTabsWithLayout as i32, payload: Some(Payload::NewTabsWithLayoutPayload(raw_layout)), }), - PluginCommand::NewTab => Ok(ProtobufPluginCommand { + PluginCommand::NewTab { name, cwd } => Ok(ProtobufPluginCommand { name: CommandName::NewTab as i32, - payload: None, + payload: Some(Payload::NewTabPayload(NewTabPayload { name, cwd })), }), PluginCommand::GoToNextTab => Ok(ProtobufPluginCommand { name: CommandName::GoToNextTab as i32, 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 3dc47894..1bceb902 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 @@ -1852,6 +1852,7 @@ Config { None, None, true, + None, ), SwitchToMode( Normal, @@ -5582,6 +5583,7 @@ Config { None, None, true, + None, ), SwitchToMode( 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 f1f79aa4..2281e405 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 @@ -1852,6 +1852,7 @@ Config { None, None, true, + None, ), SwitchToMode( Normal, @@ -5582,6 +5583,7 @@ Config { None, None, true, + None, ), SwitchToMode( 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 92fa7f7f..45f351b0 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 @@ -1852,6 +1852,7 @@ Config { None, None, true, + None, ), SwitchToMode( Normal, @@ -5582,6 +5583,7 @@ Config { None, None, true, + None, ), SwitchToMode( 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 391cbea5..2e4e0dff 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 @@ -1852,6 +1852,7 @@ Config { None, None, true, + None, ), SwitchToMode( Normal, @@ -5582,6 +5583,7 @@ Config { None, None, true, + None, ), SwitchToMode( Normal,