diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs index 5285abff48..2086268d89 100644 --- a/zellij-server/src/plugins/mod.rs +++ b/zellij-server/src/plugins/mod.rs @@ -184,7 +184,8 @@ pub(crate) fn plugin_thread_main( client_id, size, cwd, - ) => match wasm_bridge.load_plugin(&run, tab_index, size, cwd, Some(client_id)) { + ) => match wasm_bridge.load_plugin(&run, tab_index, size, cwd.clone(), Some(client_id)) + { Ok(plugin_id) => { drop(bus.senders.send_to_screen(ScreenInstruction::AddPlugin( should_float, @@ -194,6 +195,7 @@ pub(crate) fn plugin_thread_main( tab_index, plugin_id, pane_id_to_replace, + cwd, Some(client_id), ))); }, @@ -231,6 +233,7 @@ pub(crate) fn plugin_thread_main( plugin_id, None, None, + None, ))); }, Err(e) => { diff --git a/zellij-server/src/plugins/plugin_loader.rs b/zellij-server/src/plugins/plugin_loader.rs index 4d551e3831..602b2c4bdb 100644 --- a/zellij-server/src/plugins/plugin_loader.rs +++ b/zellij-server/src/plugins/plugin_loader.rs @@ -805,6 +805,7 @@ impl<'a> PluginLoader<'a> { client_attributes: self.client_attributes.clone(), default_shell: self.default_shell.clone(), default_layout: self.default_layout.clone(), + plugin_cwd: self.zellij_cwd.clone(), }; let subscriptions = Arc::new(Mutex::new(HashSet::new())); diff --git a/zellij-server/src/plugins/plugin_map.rs b/zellij-server/src/plugins/plugin_map.rs index 91b61c13ed..067c178fa1 100644 --- a/zellij-server/src/plugins/plugin_map.rs +++ b/zellij-server/src/plugins/plugin_map.rs @@ -227,6 +227,7 @@ pub struct PluginEnv { pub client_attributes: ClientAttributes, pub default_shell: Option, pub default_layout: Box, + pub plugin_cwd: PathBuf, } impl PluginEnv { diff --git a/zellij-server/src/plugins/unit/plugin_tests.rs b/zellij-server/src/plugins/unit/plugin_tests.rs index 35e613bc6d..08ac5b8326 100644 --- a/zellij-server/src/plugins/unit/plugin_tests.rs +++ b/zellij-server/src/plugins/unit/plugin_tests.rs @@ -5398,7 +5398,7 @@ pub fn run_command_plugin_command() { )])); background_jobs_thread.join().unwrap(); // this might take a while if the cache is cold teardown(); - let new_tab_event = received_background_jobs_instructions + let new_background_job = received_background_jobs_instructions .lock() .unwrap() .iter() @@ -5410,7 +5410,7 @@ pub fn run_command_plugin_command() { } }) .clone(); - assert_snapshot!(format!("{:#?}", new_tab_event)); + assert!(format!("{:#?}", new_background_job).contains("user_value_1")); } #[test] diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs index ca24c015a2..62dbb2b5c1 100644 --- a/zellij-server/src/plugins/zellij_exports.rs +++ b/zellij-server/src/plugins/zellij_exports.rs @@ -374,10 +374,14 @@ fn open_file(env: &ForeignFunctionEnv, file_to_open: FileToOpen) { let error_msg = || format!("failed to open file in plugin {}", env.plugin_env.name()); let floating = false; let in_place = false; + let path = env.plugin_env.plugin_cwd.join(file_to_open.path); + let cwd = file_to_open + .cwd + .map(|cwd| env.plugin_env.plugin_cwd.join(cwd)); let action = Action::EditFile( - file_to_open.path, + path, file_to_open.line_number, - file_to_open.cwd, + cwd, None, floating, in_place, @@ -389,10 +393,14 @@ fn open_file_floating(env: &ForeignFunctionEnv, file_to_open: FileToOpen) { let error_msg = || format!("failed to open file in plugin {}", env.plugin_env.name()); let floating = true; let in_place = false; + let path = env.plugin_env.plugin_cwd.join(file_to_open.path); + let cwd = file_to_open + .cwd + .map(|cwd| env.plugin_env.plugin_cwd.join(cwd)); let action = Action::EditFile( - file_to_open.path, + path, file_to_open.line_number, - file_to_open.cwd, + cwd, None, floating, in_place, @@ -404,10 +412,14 @@ fn open_file_in_place(env: &ForeignFunctionEnv, file_to_open: FileToOpen) { let error_msg = || format!("failed to open file in plugin {}", env.plugin_env.name()); let floating = false; let in_place = true; + let path = env.plugin_env.plugin_cwd.join(file_to_open.path); + let cwd = file_to_open + .cwd + .map(|cwd| env.plugin_env.plugin_cwd.join(cwd)); let action = Action::EditFile( - file_to_open.path, + path, file_to_open.line_number, - file_to_open.cwd, + cwd, None, floating, in_place, @@ -417,6 +429,7 @@ fn open_file_in_place(env: &ForeignFunctionEnv, file_to_open: FileToOpen) { fn open_terminal(env: &ForeignFunctionEnv, cwd: PathBuf) { let error_msg = || format!("failed to open file in plugin {}", env.plugin_env.name()); + let cwd = env.plugin_env.plugin_cwd.join(cwd); let mut default_shell = env .plugin_env .default_shell @@ -433,6 +446,7 @@ fn open_terminal(env: &ForeignFunctionEnv, cwd: PathBuf) { fn open_terminal_floating(env: &ForeignFunctionEnv, cwd: PathBuf) { let error_msg = || format!("failed to open file in plugin {}", env.plugin_env.name()); + let cwd = env.plugin_env.plugin_cwd.join(cwd); let mut default_shell = env .plugin_env .default_shell @@ -449,6 +463,7 @@ fn open_terminal_floating(env: &ForeignFunctionEnv, cwd: PathBuf) { fn open_terminal_in_place(env: &ForeignFunctionEnv, cwd: PathBuf) { let error_msg = || format!("failed to open file in plugin {}", env.plugin_env.name()); + let cwd = env.plugin_env.plugin_cwd.join(cwd); let mut default_shell = env .plugin_env .default_shell @@ -466,7 +481,9 @@ fn open_terminal_in_place(env: &ForeignFunctionEnv, cwd: PathBuf) { fn open_command_pane(env: &ForeignFunctionEnv, command_to_run: CommandToRun) { let error_msg = || format!("failed to open command in plugin {}", env.plugin_env.name()); let command = command_to_run.path; - let cwd = command_to_run.cwd; + let cwd = command_to_run + .cwd + .map(|cwd| env.plugin_env.plugin_cwd.join(cwd)); let args = command_to_run.args; let direction = None; let hold_on_close = true; @@ -487,7 +504,9 @@ fn open_command_pane(env: &ForeignFunctionEnv, command_to_run: CommandToRun) { fn open_command_pane_floating(env: &ForeignFunctionEnv, command_to_run: CommandToRun) { let error_msg = || format!("failed to open command in plugin {}", env.plugin_env.name()); let command = command_to_run.path; - let cwd = command_to_run.cwd; + let cwd = command_to_run + .cwd + .map(|cwd| env.plugin_env.plugin_cwd.join(cwd)); let args = command_to_run.args; let direction = None; let hold_on_close = true; @@ -508,7 +527,9 @@ fn open_command_pane_floating(env: &ForeignFunctionEnv, command_to_run: CommandT fn open_command_pane_in_place(env: &ForeignFunctionEnv, command_to_run: CommandToRun) { let error_msg = || format!("failed to open command in plugin {}", env.plugin_env.name()); let command = command_to_run.path; - let cwd = command_to_run.cwd; + let cwd = command_to_run + .cwd + .map(|cwd| env.plugin_env.plugin_cwd.join(cwd)); let args = command_to_run.args; let direction = None; let hold_on_close = true; @@ -621,6 +642,7 @@ fn run_command( log::error!("Command cannot be empty"); } else { let command = command_line.remove(0); + let cwd = env.plugin_env.plugin_cwd.join(cwd); let _ = env .plugin_env .senders diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index fdc83ac64d..8c9da6b57b 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -705,6 +705,17 @@ pub(crate) fn route_action( )) .with_context(err_context)?; }, + Action::LaunchPlugin(run_plugin, should_float, should_open_in_place) => { + senders + .send_to_screen(ScreenInstruction::LaunchPlugin( + run_plugin, + should_float, + should_open_in_place, + pane_id, + client_id, + )) + .with_context(err_context)?; + }, Action::CloseTerminalPane(terminal_pane_id) => { senders .send_to_screen(ScreenInstruction::ClosePane( diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index d541663e70..902351c5d5 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -286,6 +286,7 @@ pub enum ScreenInstruction { usize, // tab index u32, // plugin id Option, + Option, // cwd Option, ), UpdatePluginLoadingStage(u32, LoadingIndication), // u32 - plugin_id @@ -293,8 +294,9 @@ pub enum ScreenInstruction { ProgressPluginLoadingOffset(u32), // u32 - plugin id RequestStateUpdateForPlugins, LaunchOrFocusPlugin(RunPlugin, bool, bool, bool, Option, ClientId), // bools are: should_float, move_to_focused_tab, should_open_in_place Option is the pane id to replace - SuppressPane(PaneId, ClientId), // bool is should_float - FocusPaneWithId(PaneId, bool, ClientId), // bool is should_float + LaunchPlugin(RunPlugin, bool, bool, Option, ClientId), // bools are: should_float, should_open_in_place Option is the pane id to replace + SuppressPane(PaneId, ClientId), // bool is should_float + FocusPaneWithId(PaneId, bool, ClientId), // bool is should_float RenamePane(PaneId, Vec), RenameTab(usize, Vec), RequestPluginPermissions( @@ -481,6 +483,7 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenContext::RequestStateUpdateForPlugins }, ScreenInstruction::LaunchOrFocusPlugin(..) => ScreenContext::LaunchOrFocusPlugin, + ScreenInstruction::LaunchPlugin(..) => ScreenContext::LaunchPlugin, ScreenInstruction::SuppressPane(..) => ScreenContext::SuppressPane, ScreenInstruction::FocusPaneWithId(..) => ScreenContext::FocusPaneWithId, ScreenInstruction::RenamePane(..) => ScreenContext::RenamePane, @@ -3242,10 +3245,17 @@ pub(crate) fn screen_thread_main( tab_index, plugin_id, pane_id_to_replace, + cwd, client_id, ) => { - let pane_title = - pane_title.unwrap_or_else(|| run_plugin_location.location.to_string()); + let pane_title = pane_title.unwrap_or_else(|| { + format!( + "({}) - {}", + cwd.map(|cwd| cwd.display().to_string()) + .unwrap_or(".".to_owned()), + run_plugin_location.location + ) + }); let run_plugin = Run::Plugin(run_plugin_location); if should_be_in_place { @@ -3399,6 +3409,70 @@ pub(crate) fn screen_thread_main( } }, }, + ScreenInstruction::LaunchPlugin( + run_plugin, + should_float, + should_open_in_place, + pane_id_to_replace, + client_id, + ) => match pane_id_to_replace { + Some(pane_id_to_replace) => match screen.active_tab_indices.values().next() { + Some(tab_index) => { + let size = Size::default(); + screen + .bus + .senders + .send_to_pty(PtyInstruction::FillPluginCwd( + Some(should_float), + should_open_in_place, + None, + run_plugin, + *tab_index, + Some(pane_id_to_replace), + client_id, + size, + ))?; + }, + None => { + log::error!( + "Could not find an active tab - is there at least 1 connected user?" + ); + }, + }, + None => { + let client_id = if screen.active_tab_indices.contains_key(&client_id) { + Some(client_id) + } else { + screen.get_first_client_id() + }; + let client_id_and_focused_tab = client_id.and_then(|client_id| { + screen + .active_tab_indices + .get(&client_id) + .map(|tab_index| (*tab_index, client_id)) + }); + match client_id_and_focused_tab { + Some((tab_index, client_id)) => { + screen + .bus + .senders + .send_to_pty(PtyInstruction::FillPluginCwd( + Some(should_float), + should_open_in_place, + None, + run_plugin, + tab_index, + None, + client_id, + Size::default(), + ))?; + }, + None => { + log::error!("No connected clients found - cannot load or focus plugin") + }, + } + }, + }, ScreenInstruction::SuppressPane(pane_id, client_id) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { diff --git a/zellij-utils/assets/prost/api.action.rs b/zellij-utils/assets/prost/api.action.rs index de46a38b43..171ae804aa 100644 --- a/zellij-utils/assets/prost/api.action.rs +++ b/zellij-utils/assets/prost/api.action.rs @@ -5,7 +5,7 @@ pub struct Action { pub name: i32, #[prost( oneof = "action::OptionalPayload", - 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" + 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" )] pub optional_payload: ::core::option::Option, } @@ -102,6 +102,8 @@ pub mod action { RenameTabPayload(super::IdAndName), #[prost(string, tag = "45")] RenameSessionPayload(::prost::alloc::string::String), + #[prost(message, tag = "46")] + LaunchPluginPayload(super::LaunchOrFocusPluginPayload), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -403,6 +405,7 @@ pub enum ActionName { BreakPaneRight = 78, BreakPaneLeft = 79, RenameSession = 80, + LaunchPlugin = 81, } impl ActionName { /// String value of the enum field names used in the ProtoBuf definition. @@ -492,6 +495,7 @@ impl ActionName { ActionName::BreakPaneRight => "BreakPaneRight", ActionName::BreakPaneLeft => "BreakPaneLeft", ActionName::RenameSession => "RenameSession", + ActionName::LaunchPlugin => "LaunchPlugin", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -578,6 +582,7 @@ impl ActionName { "BreakPaneRight" => Some(Self::BreakPaneRight), "BreakPaneLeft" => Some(Self::BreakPaneLeft), "RenameSession" => Some(Self::RenameSession), + "LaunchPlugin" => Some(Self::LaunchPlugin), _ => None, } } diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs index 02380ba4b4..cdb3f42f17 100644 --- a/zellij-utils/src/cli.rs +++ b/zellij-utils/src/cli.rs @@ -527,6 +527,15 @@ pub enum CliAction { #[clap(short, long, value_parser)] configuration: Option, }, + LaunchPlugin { + #[clap(short, long, value_parser)] + floating: bool, + #[clap(short, long, value_parser)] + in_place: bool, + url: Url, + #[clap(short, long, value_parser)] + configuration: Option, + }, RenameSession { name: String, }, diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 726824d82d..aff97f2d8b 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -335,6 +335,7 @@ pub enum ScreenContext { StartPluginLoadingIndication, RequestStateUpdateForPlugins, LaunchOrFocusPlugin, + LaunchPlugin, SuppressPane, FocusPaneWithId, RenamePane, diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index c36579c373..8fe0a3285e 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -211,6 +211,8 @@ pub enum Action { MiddleClick(Position), LaunchOrFocusPlugin(RunPlugin, bool, bool, bool), // bools => should float, // move_to_focused_tab, should_open_in_place + LaunchPlugin(RunPlugin, bool, bool), // bools => should float, + // should_open_in_place LeftMouseRelease(Position), RightMouseRelease(Position), MiddleMouseRelease(Position), @@ -259,6 +261,7 @@ impl Action { match (self, other_action) { (Action::NewTab(..), Action::NewTab(..)) => true, (Action::LaunchOrFocusPlugin(..), Action::LaunchOrFocusPlugin(..)) => true, + (Action::LaunchPlugin(..), Action::LaunchPlugin(..)) => true, _ => self == other_action, } } @@ -538,6 +541,22 @@ impl Action { in_place, )]) }, + CliAction::LaunchPlugin { + url, + floating, + in_place, + configuration, + } => { + let current_dir = get_current_dir(); + let run_plugin_location = RunPluginLocation::parse(url.as_str(), Some(current_dir)) + .map_err(|e| format!("Failed to parse plugin location: {}", e))?; + let run_plugin = RunPlugin { + location: run_plugin_location, + _allow_exec_host_cmd: false, + configuration: configuration.unwrap_or_default(), + }; + Ok(vec![Action::LaunchPlugin(run_plugin, floating, in_place)]) + }, CliAction::RenameSession { name } => Ok(vec![Action::RenameSession(name)]), } } diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs index 061e24ebc5..8fce144d39 100644 --- a/zellij-utils/src/kdl/mod.rs +++ b/zellij-utils/src/kdl/mod.rs @@ -953,6 +953,39 @@ impl TryFrom<(&KdlNode, &Options)> for Action { should_open_in_place, )) }, + "LaunchPlugin" => { + let arguments = action_arguments.iter().copied(); + let mut args = kdl_arguments_that_are_strings(arguments)?; + if args.is_empty() { + return Err(ConfigError::new_kdl_error( + "No plugin found to launch in LaunchPlugin".into(), + kdl_action.span().offset(), + kdl_action.span().len(), + )); + } + let plugin_path = args.remove(0); + + let command_metadata = action_children.iter().next(); + let should_float = command_metadata + .and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "floating")) + .unwrap_or(false); + let should_open_in_place = command_metadata + .and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "in_place")) + .unwrap_or(false); + let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + let location = RunPluginLocation::parse(&plugin_path, Some(current_dir))?; + let configuration = KdlLayoutParser::parse_plugin_user_configuration(&kdl_action)?; + let run_plugin = RunPlugin { + location, + _allow_exec_host_cmd: false, + configuration, + }; + Ok(Action::LaunchPlugin( + run_plugin, + should_float, + should_open_in_place, + )) + }, "PreviousSwapLayout" => Ok(Action::PreviousSwapLayout), "NextSwapLayout" => Ok(Action::NextSwapLayout), "BreakPane" => Ok(Action::BreakPane), diff --git a/zellij-utils/src/plugin_api/action.proto b/zellij-utils/src/plugin_api/action.proto index 59eea7af10..e9d2a293f7 100644 --- a/zellij-utils/src/plugin_api/action.proto +++ b/zellij-utils/src/plugin_api/action.proto @@ -52,6 +52,7 @@ message Action { IdAndName rename_plugin_pane_payload = 43; IdAndName rename_tab_payload = 44; string rename_session_payload = 45; + LaunchOrFocusPluginPayload launch_plugin_payload = 46; } } @@ -223,6 +224,7 @@ enum ActionName { BreakPaneRight = 78; BreakPaneLeft = 79; RenameSession = 80; + LaunchPlugin = 81; } message Position { diff --git a/zellij-utils/src/plugin_api/action.rs b/zellij-utils/src/plugin_api/action.rs index 1048d91f67..dc8c293801 100644 --- a/zellij-utils/src/plugin_api/action.rs +++ b/zellij-utils/src/plugin_api/action.rs @@ -413,6 +413,32 @@ impl TryFrom for Action { _ => Err("Wrong payload for Action::LaunchOrFocusPlugin"), } }, + Some(ProtobufActionName::LaunchPlugin) => match protobuf_action.optional_payload { + Some(OptionalPayload::LaunchOrFocusPluginPayload(payload)) => { + let run_plugin_location = + RunPluginLocation::parse(&payload.plugin_url, None) + .map_err(|_| "Malformed LaunchOrFocusPlugin payload")?; + let configuration: PluginUserConfiguration = payload + .plugin_configuration + .and_then(|p| PluginUserConfiguration::try_from(p).ok()) + .unwrap_or_default(); + let run_plugin = RunPlugin { + _allow_exec_host_cmd: false, + location: run_plugin_location, + configuration, + }; + let should_float = payload.should_float; + let _move_to_focused_tab = payload.move_to_focused_tab; // not actually used in + // this action + let should_open_in_place = payload.should_open_in_place; + Ok(Action::LaunchPlugin( + run_plugin, + should_float, + should_open_in_place, + )) + }, + _ => Err("Wrong payload for Action::LaunchOrFocusPlugin"), + }, Some(ProtobufActionName::LeftMouseRelease) => match protobuf_action.optional_payload { Some(OptionalPayload::LeftMouseReleasePayload(payload)) => { let position = payload.try_into()?; @@ -996,6 +1022,21 @@ impl TryFrom for ProtobufAction { )), }) }, + Action::LaunchPlugin(run_plugin, should_float, should_open_in_place) => { + let url: Url = Url::from(&run_plugin.location); + Ok(ProtobufAction { + name: ProtobufActionName::LaunchPlugin as i32, + optional_payload: Some(OptionalPayload::LaunchOrFocusPluginPayload( + LaunchOrFocusPluginPayload { + plugin_url: url.into(), + should_float, + move_to_focused_tab: false, + should_open_in_place, + plugin_configuration: Some(run_plugin.configuration.try_into()?), + }, + )), + }) + }, Action::LeftMouseRelease(position) => { let position: ProtobufPosition = position.try_into()?; Ok(ProtobufAction {