diff --git a/default-plugins/fixture-plugin-for-tests/src/main.rs b/default-plugins/fixture-plugin-for-tests/src/main.rs index 811a4507ea..cdbdc9f2a2 100644 --- a/default-plugins/fixture-plugin-for-tests/src/main.rs +++ b/default-plugins/fixture-plugin-for-tests/src/main.rs @@ -240,6 +240,23 @@ impl ZellijPlugin for State { Key::Ctrl('1') => { request_permission(&[PermissionType::ReadApplicationState]); }, + Key::Ctrl('2') => { + let mut context = BTreeMap::new(); + context.insert("user_key_1".to_owned(), "user_value_1".to_owned()); + run_command(&["ls", "-l"], context); + }, + Key::Ctrl('3') => { + let mut context = BTreeMap::new(); + context.insert("user_key_2".to_owned(), "user_value_2".to_owned()); + let mut env_vars = BTreeMap::new(); + env_vars.insert("VAR1".to_owned(), "some_value".to_owned()); + run_command_with_env_variables_and_cwd( + &["ls", "-l"], + env_vars, + std::path::PathBuf::from("/some/custom/folder"), + context, + ); + }, _ => {}, }, Event::CustomMessage(message, payload) => { diff --git a/default-plugins/session-manager/src/main.rs b/default-plugins/session-manager/src/main.rs index b65986319f..bd0b1f2b74 100644 --- a/default-plugins/session-manager/src/main.rs +++ b/default-plugins/session-manager/src/main.rs @@ -28,6 +28,7 @@ impl ZellijPlugin for State { EventType::ModeUpdate, EventType::SessionUpdate, EventType::Key, + EventType::RunCommandResult, ]); } diff --git a/zellij-server/src/background_jobs.rs b/zellij-server/src/background_jobs.rs index 375ba4ba31..57e13364c6 100644 --- a/zellij-server/src/background_jobs.rs +++ b/zellij-server/src/background_jobs.rs @@ -3,13 +3,14 @@ use zellij_utils::consts::{ session_info_cache_file_name, session_info_folder_for_session, session_layout_cache_file_name, ZELLIJ_SOCK_DIR, }; -use zellij_utils::data::SessionInfo; +use zellij_utils::data::{Event, SessionInfo}; use zellij_utils::errors::{prelude::*, BackgroundJobContext, ContextType}; use std::collections::{BTreeMap, HashMap}; use std::fs; use std::io::Write; use std::os::unix::fs::FileTypeExt; +use std::path::PathBuf; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, Mutex, @@ -17,8 +18,10 @@ use std::sync::{ use std::time::{Duration, Instant}; use crate::panes::PaneId; +use crate::plugins::{PluginId, PluginInstruction}; use crate::screen::ScreenInstruction; use crate::thread_bus::Bus; +use crate::ClientId; #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum BackgroundJob { @@ -28,6 +31,15 @@ pub enum BackgroundJob { ReadAllSessionInfosOnMachine, // u32 - plugin_id ReportSessionInfo(String, SessionInfo), // String - session name ReportLayoutInfo((String, BTreeMap)), // HashMap + RunCommand( + PluginId, + ClientId, + String, + Vec, + BTreeMap, + PathBuf, + BTreeMap, + ), // command, args, env_variables, cwd, context Exit, } @@ -44,6 +56,7 @@ impl From<&BackgroundJob> for BackgroundJobContext { }, BackgroundJob::ReportSessionInfo(..) => BackgroundJobContext::ReportSessionInfo, BackgroundJob::ReportLayoutInfo(..) => BackgroundJobContext::ReportLayoutInfo, + BackgroundJob::RunCommand(..) => BackgroundJobContext::RunCommand, BackgroundJob::Exit => BackgroundJobContext::Exit, } } @@ -226,6 +239,52 @@ pub(crate) fn background_jobs_main(bus: Bus) -> Result<()> { } }); }, + BackgroundJob::RunCommand( + plugin_id, + client_id, + command, + args, + env_variables, + cwd, + context, + ) => { + // when async_std::process stabilizes, we should change this to be async + std::thread::spawn({ + let senders = bus.senders.clone(); + move || { + let output = std::process::Command::new(&command) + .args(&args) + .envs(env_variables) + .current_dir(cwd) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .output(); + match output { + Ok(output) => { + let stdout = output.stdout.to_vec(); + let stderr = output.stderr.to_vec(); + let exit_code = output.status.code(); + let _ = senders.send_to_plugin(PluginInstruction::Update(vec![( + Some(plugin_id), + Some(client_id), + Event::RunCommandResult(exit_code, stdout, stderr, context), + )])); + }, + Err(e) => { + log::error!("Failed to run command: {}", e); + let stdout = vec![]; + let stderr = format!("{}", e).as_bytes().to_vec(); + let exit_code = Some(2); + let _ = senders.send_to_plugin(PluginInstruction::Update(vec![( + Some(plugin_id), + Some(client_id), + Event::RunCommandResult(exit_code, stdout, stderr, context), + )])); + }, + } + } + }); + }, BackgroundJob::Exit => { for loading_plugin in loading_plugins.values() { loading_plugin.store(false, Ordering::SeqCst); diff --git a/zellij-server/src/plugins/unit/plugin_tests.rs b/zellij-server/src/plugins/unit/plugin_tests.rs index 652623ccce..e866987d7e 100644 --- a/zellij-server/src/plugins/unit/plugin_tests.rs +++ b/zellij-server/src/plugins/unit/plugin_tests.rs @@ -473,6 +473,90 @@ fn create_plugin_thread_with_pty_receiver( (to_plugin, pty_receiver, screen_receiver, Box::new(teardown)) } +fn create_plugin_thread_with_background_jobs_receiver( + zellij_cwd: Option, +) -> ( + SenderWithContext, + Receiver<(BackgroundJob, ErrorContext)>, + Receiver<(ScreenInstruction, ErrorContext)>, + Box, +) { + let zellij_cwd = zellij_cwd.unwrap_or_else(|| PathBuf::from(".")); + let (to_server, _server_receiver): ChannelWithContext = + channels::bounded(50); + let to_server = SenderWithContext::new(to_server); + + let (to_screen, screen_receiver): ChannelWithContext = channels::unbounded(); + let to_screen = SenderWithContext::new(to_screen); + + let (to_plugin, plugin_receiver): ChannelWithContext = channels::unbounded(); + let to_plugin = SenderWithContext::new(to_plugin); + let (to_pty, _pty_receiver): ChannelWithContext = channels::unbounded(); + let to_pty = SenderWithContext::new(to_pty); + + let (to_pty_writer, _pty_writer_receiver): ChannelWithContext = + channels::unbounded(); + let to_pty_writer = SenderWithContext::new(to_pty_writer); + + let (to_background_jobs, background_jobs_receiver): ChannelWithContext = + channels::unbounded(); + let to_background_jobs = SenderWithContext::new(to_background_jobs); + + let plugin_bus = Bus::new( + vec![plugin_receiver], + Some(&to_screen), + Some(&to_pty), + Some(&to_plugin), + Some(&to_server), + Some(&to_pty_writer), + Some(&to_background_jobs), + None, + ) + .should_silently_fail(); + let store = Store::new(wasmer::Singlepass::default()); + let data_dir = PathBuf::from(tempdir().unwrap().path()); + let default_shell = PathBuf::from("."); + let plugin_capabilities = PluginCapabilities::default(); + let client_attributes = ClientAttributes::default(); + let default_shell_action = None; // TODO: change me + let plugin_thread = std::thread::Builder::new() + .name("plugin_thread".to_string()) + .spawn(move || { + set_var("ZELLIJ_SESSION_NAME", "zellij-test"); + plugin_thread_main( + plugin_bus, + store, + data_dir, + PluginsConfig::default(), + Box::new(Layout::default()), + default_shell, + zellij_cwd, + plugin_capabilities, + client_attributes, + default_shell_action, + ) + .expect("TEST") + }) + .unwrap(); + let teardown = { + let to_plugin = to_plugin.clone(); + move || { + let _ = to_pty.send(PtyInstruction::Exit); + let _ = to_pty_writer.send(PtyWriteInstruction::Exit); + let _ = to_screen.send(ScreenInstruction::Exit); + let _ = to_server.send(ServerInstruction::KillSession); + let _ = to_plugin.send(PluginInstruction::Exit); + let _ = plugin_thread.join(); + } + }; + ( + to_plugin, + background_jobs_receiver, + screen_receiver, + Box::new(teardown), + ) +} + lazy_static! { static ref PLUGIN_FIXTURE: String = format!( // to populate this file, make sure to run the build-e2e CI job @@ -5184,3 +5268,153 @@ pub fn denied_permission_request_result() { assert_snapshot!(format!("{:#?}", permissions)); } + +#[test] +#[ignore] +pub fn run_command_plugin_command() { + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + // destructor removes the directory + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, background_jobs_receiver, screen_receiver, teardown) = + create_plugin_thread_with_background_jobs_receiver(Some(plugin_host_folder)); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), + }; + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + let received_background_jobs_instructions = Arc::new(Mutex::new(vec![])); + let background_jobs_thread = log_actions_in_thread!( + received_background_jobs_instructions, + BackgroundJob::RunCommand, + background_jobs_receiver, + 1 + ); + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!( + received_screen_instructions, + ScreenInstruction::Exit, + screen_receiver, + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + false, + plugin_title, + run_plugin, + tab_index, + None, + client_id, + size, + )); + std::thread::sleep(std::time::Duration::from_millis(500)); + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(Key::Ctrl('2')), // this triggers the enent in the fixture plugin + )])); + background_jobs_thread.join().unwrap(); // this might take a while if the cache is cold + teardown(); + let new_tab_event = received_background_jobs_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let BackgroundJob::RunCommand(..) = i { + Some(i.clone()) + } else { + None + } + }) + .clone(); + assert_snapshot!(format!("{:#?}", new_tab_event)); +} + +#[test] +#[ignore] +pub fn run_command_with_env_vars_and_cwd_plugin_command() { + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + // destructor removes the directory + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, background_jobs_receiver, screen_receiver, teardown) = + create_plugin_thread_with_background_jobs_receiver(Some(plugin_host_folder)); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), + }; + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + let received_background_jobs_instructions = Arc::new(Mutex::new(vec![])); + let background_jobs_thread = log_actions_in_thread!( + received_background_jobs_instructions, + BackgroundJob::RunCommand, + background_jobs_receiver, + 1 + ); + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!( + received_screen_instructions, + ScreenInstruction::Exit, + screen_receiver, + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + false, + plugin_title, + run_plugin, + tab_index, + None, + client_id, + size, + )); + std::thread::sleep(std::time::Duration::from_millis(500)); + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(Key::Ctrl('3')), // this triggers the enent in the fixture plugin + )])); + background_jobs_thread.join().unwrap(); // this might take a while if the cache is cold + teardown(); + let new_tab_event = received_background_jobs_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let BackgroundJob::RunCommand(..) = i { + Some(i.clone()) + } else { + None + } + }) + .clone(); + assert_snapshot!(format!("{:#?}", new_tab_event)); +} diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_command_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_command_plugin_command.snap new file mode 100644 index 0000000000..a98f856816 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_command_plugin_command.snap @@ -0,0 +1,20 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +assertion_line: 5339 +expression: "format!(\"{:#?}\", new_tab_event)" +--- +Some( + RunCommand( + 0, + 1, + "ls", + [ + "-l", + ], + {}, + ".", + { + "user_key_1": "user_value_1", + }, + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_command_with_env_vars_and_cwd_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_command_with_env_vars_and_cwd_plugin_command.snap new file mode 100644 index 0000000000..5c5d03769e --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_command_with_env_vars_and_cwd_plugin_command.snap @@ -0,0 +1,22 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +assertion_line: 5414 +expression: "format!(\"{:#?}\", new_tab_event)" +--- +Some( + RunCommand( + 0, + 1, + "ls", + [ + "-l", + ], + { + "VAR1": "some_value", + }, + "/some/custom/folder", + { + "user_key_2": "user_value_2", + }, + ), +) diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs index 1608df8fb6..496c7d64ac 100644 --- a/zellij-server/src/plugins/zellij_exports.rs +++ b/zellij-server/src/plugins/zellij_exports.rs @@ -1,4 +1,5 @@ use super::PluginInstruction; +use crate::background_jobs::BackgroundJob; use crate::plugins::plugin_map::{PluginEnv, Subscriptions}; use crate::plugins::wasm_bridge::handle_plugin_crash; use crate::route::route_action; @@ -126,6 +127,9 @@ fn host_run_plugin_command(env: FunctionEnvMut) { PluginCommand::SwitchTabTo(tab_index) => switch_tab_to(env, tab_index), PluginCommand::SetTimeout(seconds) => set_timeout(env, seconds), PluginCommand::ExecCmd(command_line) => exec_cmd(env, command_line), + PluginCommand::RunCommand(command_line, env_variables, cwd, context) => { + run_command(env, command_line, env_variables, cwd, context) + }, PluginCommand::PostMessageTo(plugin_message) => { post_message_to(env, plugin_message)? }, @@ -572,6 +576,7 @@ fn set_timeout(env: &ForeignFunctionEnv, secs: f64) { } fn exec_cmd(env: &ForeignFunctionEnv, mut command_line: Vec) { + log::warn!("The ExecCmd plugin command is deprecated and will be removed in a future version. Please use RunCmd instead (it has all the things and can even show you STDOUT/STDERR and an exit code!)"); let err_context = || { format!( "failed to execute command on host for plugin '{}'", @@ -595,6 +600,38 @@ fn exec_cmd(env: &ForeignFunctionEnv, mut command_line: Vec) { .non_fatal(); } +fn run_command( + env: &ForeignFunctionEnv, + mut command_line: Vec, + env_variables: BTreeMap, + cwd: PathBuf, + context: BTreeMap, +) { + let err_context = || { + format!( + "failed to execute command on host for plugin '{}'", + env.plugin_env.name() + ) + }; + if command_line.is_empty() { + log::error!("Command cannot be empty"); + } else { + let command = command_line.remove(0); + let _ = env + .plugin_env + .senders + .send_to_background_jobs(BackgroundJob::RunCommand( + env.plugin_env.plugin_id, + env.plugin_env.client_id, + command, + command_line, + env_variables, + cwd, + context, + )); + } +} + fn post_message_to(env: &ForeignFunctionEnv, plugin_message: PluginMessage) -> Result<()> { let worker_name = plugin_message .worker_name @@ -1159,6 +1196,7 @@ fn check_command_permission( PluginCommand::OpenCommandPane(..) | PluginCommand::OpenCommandPaneFloating(..) | PluginCommand::OpenCommandPaneInPlace(..) + | PluginCommand::RunCommand(..) | PluginCommand::ExecCmd(..) => PermissionType::RunCommands, PluginCommand::Write(..) | PluginCommand::WriteChars(..) => PermissionType::WriteToStdin, PluginCommand::SwitchTabTo(..) diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs index 09bb7f5970..97065a2a3b 100644 --- a/zellij-tile/src/shim.rs +++ b/zellij-tile/src/shim.rs @@ -1,6 +1,9 @@ use serde::{de::DeserializeOwned, Serialize}; -use std::collections::HashSet; -use std::{io, path::Path}; +use std::collections::{BTreeMap, HashSet}; +use std::{ + io, + path::{Path, PathBuf}, +}; use zellij_utils::data::*; use zellij_utils::errors::prelude::*; pub use zellij_utils::plugin_api; @@ -171,6 +174,39 @@ pub fn exec_cmd(cmd: &[&str]) { unsafe { host_run_plugin_command() }; } +/// Run this command in the background on the host machine, optionally being notified of its output +/// if subscribed to the `RunCommandResult` Event +pub fn run_command(cmd: &[&str], context: BTreeMap) { + let plugin_command = PluginCommand::RunCommand( + cmd.iter().cloned().map(|s| s.to_owned()).collect(), + BTreeMap::new(), + PathBuf::from("."), + context, + ); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; +} + +/// Run this command in the background on the host machine, providing environment variables and a +/// cwd. Optionally being notified of its output if subscribed to the `RunCommandResult` Event +pub fn run_command_with_env_variables_and_cwd( + cmd: &[&str], + env_variables: BTreeMap, + cwd: PathBuf, + context: BTreeMap, +) { + let plugin_command = PluginCommand::RunCommand( + cmd.iter().cloned().map(|s| s.to_owned()).collect(), + env_variables, + cwd, + context, + ); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; +} + /// Hide the plugin pane (suppress it) from the UI pub fn hide_self() { let plugin_command = PluginCommand::HideSelf; diff --git a/zellij-utils/assets/prost/api.event.rs b/zellij-utils/assets/prost/api.event.rs index 39bc97705b..9b0097a5da 100644 --- a/zellij-utils/assets/prost/api.event.rs +++ b/zellij-utils/assets/prost/api.event.rs @@ -9,7 +9,10 @@ pub struct EventNameList { pub struct Event { #[prost(enumeration = "EventType", tag = "1")] pub name: i32, - #[prost(oneof = "event::Payload", tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13")] + #[prost( + oneof = "event::Payload", + tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14" + )] pub payload: ::core::option::Option, } /// Nested message and enum types in `Event`. @@ -41,6 +44,8 @@ pub mod event { PermissionRequestResultPayload(super::PermissionRequestResultPayload), #[prost(message, tag = "13")] SessionUpdatePayload(super::SessionUpdatePayload), + #[prost(message, tag = "14")] + RunCommandResultPayload(super::RunCommandResultPayload), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -51,6 +56,26 @@ pub struct SessionUpdatePayload { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct RunCommandResultPayload { + #[prost(int32, optional, tag = "1")] + pub exit_code: ::core::option::Option, + #[prost(bytes = "vec", tag = "2")] + pub stdout: ::prost::alloc::vec::Vec, + #[prost(bytes = "vec", tag = "3")] + pub stderr: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag = "4")] + pub context: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ContextItem { + #[prost(string, tag = "1")] + pub name: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub value: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct PermissionRequestResultPayload { #[prost(bool, tag = "1")] pub granted: bool, @@ -261,6 +286,7 @@ pub enum EventType { FileSystemDelete = 14, PermissionRequestResult = 15, SessionUpdate = 16, + RunCommandResult = 17, } impl EventType { /// String value of the enum field names used in the ProtoBuf definition. @@ -286,6 +312,7 @@ impl EventType { EventType::FileSystemDelete => "FileSystemDelete", EventType::PermissionRequestResult => "PermissionRequestResult", EventType::SessionUpdate => "SessionUpdate", + EventType::RunCommandResult => "RunCommandResult", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -308,6 +335,7 @@ impl EventType { "FileSystemDelete" => Some(Self::FileSystemDelete), "PermissionRequestResult" => Some(Self::PermissionRequestResult), "SessionUpdate" => Some(Self::SessionUpdate), + "RunCommandResult" => Some(Self::RunCommandResult), _ => None, } } diff --git a/zellij-utils/assets/prost/api.plugin_command.rs b/zellij-utils/assets/prost/api.plugin_command.rs index b86f954912..d8cd13c1fa 100644 --- a/zellij-utils/assets/prost/api.plugin_command.rs +++ b/zellij-utils/assets/prost/api.plugin_command.rs @@ -5,7 +5,7 @@ pub struct PluginCommand { 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" + 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" )] pub payload: ::core::option::Option, } @@ -96,6 +96,8 @@ pub mod plugin_command { OpenTerminalInPlacePayload(super::OpenFilePayload), #[prost(message, tag = "42")] OpenCommandPaneInPlacePayload(super::OpenCommandPanePayload), + #[prost(message, tag = "43")] + RunCommandPayload(super::RunCommandPayload), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -164,6 +166,34 @@ pub struct ExecCmdPayload { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct RunCommandPayload { + #[prost(string, repeated, tag = "1")] + pub command_line: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + #[prost(message, repeated, tag = "2")] + pub env_variables: ::prost::alloc::vec::Vec, + #[prost(string, tag = "3")] + pub cwd: ::prost::alloc::string::String, + #[prost(message, repeated, tag = "4")] + pub context: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EnvVariable { + #[prost(string, tag = "1")] + pub name: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub value: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ContextItem { + #[prost(string, tag = "1")] + pub name: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub value: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct PluginMessagePayload { #[prost(message, optional, tag = "1")] pub message: ::core::option::Option, @@ -263,6 +293,7 @@ pub enum CommandName { OpenTerminalInPlace = 68, OpenCommandInPlace = 69, OpenFileInPlace = 70, + RunCommand = 71, } impl CommandName { /// String value of the enum field names used in the ProtoBuf definition. @@ -342,6 +373,7 @@ impl CommandName { CommandName::OpenTerminalInPlace => "OpenTerminalInPlace", CommandName::OpenCommandInPlace => "OpenCommandInPlace", CommandName::OpenFileInPlace => "OpenFileInPlace", + CommandName::RunCommand => "RunCommand", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -418,6 +450,7 @@ impl CommandName { "OpenTerminalInPlace" => Some(Self::OpenTerminalInPlace), "OpenCommandInPlace" => Some(Self::OpenCommandInPlace), "OpenFileInPlace" => Some(Self::OpenFileInPlace), + "RunCommand" => Some(Self::RunCommand), _ => None, } } diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs index 3702dace4c..8ff43836b6 100644 --- a/zellij-utils/src/data.rs +++ b/zellij-utils/src/data.rs @@ -2,7 +2,7 @@ use crate::input::actions::Action; use crate::input::config::ConversionError; use clap::ArgEnum; use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::fmt; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -496,6 +496,8 @@ pub enum Event { /// A Result of plugin permission request PermissionRequestResult(PermissionStatus), SessionUpdate(Vec), + RunCommandResult(Option, Vec, Vec, BTreeMap), // exit_code, STDOUT, STDERR, + // context } #[derive( @@ -1064,4 +1066,13 @@ pub enum PluginCommand { OpenTerminalInPlace(FileToOpen), // only used for the path as cwd OpenFileInPlace(FileToOpen), OpenCommandPaneInPlace(CommandToRun), + RunCommand( + Vec, + BTreeMap, + PathBuf, + BTreeMap, + ), // command, + // env_Variables, + // cwd, + // context } diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 03fa6c813b..cee9839144 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -445,6 +445,7 @@ pub enum BackgroundJobContext { ReadAllSessionInfosOnMachine, ReportSessionInfo, ReportLayoutInfo, + RunCommand, Exit, } diff --git a/zellij-utils/src/plugin_api/event.proto b/zellij-utils/src/plugin_api/event.proto index 4d5b805d4e..1463d8f0b1 100644 --- a/zellij-utils/src/plugin_api/event.proto +++ b/zellij-utils/src/plugin_api/event.proto @@ -40,6 +40,7 @@ enum EventType { FileSystemDelete = 14; PermissionRequestResult = 15; SessionUpdate = 16; + RunCommandResult = 17; } message EventNameList { @@ -61,6 +62,7 @@ message Event { FileListPayload file_list_payload = 11; PermissionRequestResultPayload permission_request_result_payload = 12; SessionUpdatePayload session_update_payload = 13; + RunCommandResultPayload run_command_result_payload = 14; } } @@ -68,6 +70,18 @@ message SessionUpdatePayload { repeated SessionManifest session_manifests = 1; } +message RunCommandResultPayload { + optional int32 exit_code = 1; + bytes stdout = 2; + bytes stderr = 3; + repeated ContextItem context = 4; +} + +message ContextItem { + string name = 1; + string value = 2; +} + message PermissionRequestResultPayload { bool granted = 1; } diff --git a/zellij-utils/src/plugin_api/event.rs b/zellij-utils/src/plugin_api/event.rs index 9b0e98860b..6547f383c1 100644 --- a/zellij-utils/src/plugin_api/event.rs +++ b/zellij-utils/src/plugin_api/event.rs @@ -183,6 +183,21 @@ impl TryFrom for Event { }, _ => Err("Malformed payload for the SessionUpdate Event"), }, + Some(ProtobufEventType::RunCommandResult) => match protobuf_event.payload { + Some(ProtobufEventPayload::RunCommandResultPayload(run_command_result_payload)) => { + Ok(Event::RunCommandResult( + run_command_result_payload.exit_code, + run_command_result_payload.stdout, + run_command_result_payload.stderr, + run_command_result_payload + .context + .into_iter() + .map(|c_i| (c_i.name, c_i.value)) + .collect(), + )) + }, + _ => Err("Malformed payload for the RunCommandResult Event"), + }, None => Err("Unknown Protobuf Event"), } } @@ -338,6 +353,23 @@ impl TryFrom for ProtobufEvent { payload: Some(event::Payload::SessionUpdatePayload(session_update_payload)), }) }, + Event::RunCommandResult(exit_code, stdout, stderr, context) => { + let run_command_result_payload = RunCommandResultPayload { + exit_code, + stdout, + stderr, + context: context + .into_iter() + .map(|(name, value)| ContextItem { name, value }) + .collect(), + }; + Ok(ProtobufEvent { + name: ProtobufEventType::RunCommandResult as i32, + payload: Some(event::Payload::RunCommandResultPayload( + run_command_result_payload, + )), + }) + }, } } } @@ -783,6 +815,7 @@ impl TryFrom for EventType { ProtobufEventType::FileSystemDelete => EventType::FileSystemDelete, ProtobufEventType::PermissionRequestResult => EventType::PermissionRequestResult, ProtobufEventType::SessionUpdate => EventType::SessionUpdate, + ProtobufEventType::RunCommandResult => EventType::RunCommandResult, }) } } @@ -808,6 +841,7 @@ impl TryFrom for ProtobufEventType { EventType::FileSystemDelete => ProtobufEventType::FileSystemDelete, EventType::PermissionRequestResult => ProtobufEventType::PermissionRequestResult, EventType::SessionUpdate => ProtobufEventType::SessionUpdate, + EventType::RunCommandResult => ProtobufEventType::RunCommandResult, }) } } diff --git a/zellij-utils/src/plugin_api/plugin_command.proto b/zellij-utils/src/plugin_api/plugin_command.proto index 17b153810d..e0b2618472 100644 --- a/zellij-utils/src/plugin_api/plugin_command.proto +++ b/zellij-utils/src/plugin_api/plugin_command.proto @@ -82,6 +82,7 @@ enum CommandName { OpenTerminalInPlace = 68; OpenCommandInPlace = 69; OpenFileInPlace = 70; + RunCommand = 71; } message PluginCommand { @@ -128,6 +129,7 @@ message PluginCommand { OpenFilePayload open_file_in_place_payload = 40; OpenFilePayload open_terminal_in_place_payload = 41; OpenCommandPanePayload open_command_pane_in_place_payload = 42; + RunCommandPayload run_command_payload = 43; } } @@ -170,6 +172,23 @@ message ExecCmdPayload { repeated string command_line = 1; } +message RunCommandPayload { + repeated string command_line = 1; + repeated EnvVariable env_variables = 2; + string cwd = 3; + repeated ContextItem context = 4; +} + +message EnvVariable { + string name = 1; + string value = 2; +} + +message ContextItem { + string name = 1; + string value = 2; +} + message PluginMessagePayload { api.message.Message message = 1; } diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs index 3d34f10fcd..181953d41c 100644 --- a/zellij-utils/src/plugin_api/plugin_command.rs +++ b/zellij-utils/src/plugin_api/plugin_command.rs @@ -3,9 +3,10 @@ pub use super::generated_api::api::{ event::EventNameList as ProtobufEventNameList, input_mode::InputMode as ProtobufInputMode, plugin_command::{ - plugin_command::Payload, CommandName, ExecCmdPayload, IdAndNewName, MovePayload, - OpenCommandPanePayload, OpenFilePayload, PluginCommand as ProtobufPluginCommand, - PluginMessagePayload, RequestPluginPermissionPayload, ResizePayload, SetTimeoutPayload, + plugin_command::Payload, CommandName, ContextItem, EnvVariable, ExecCmdPayload, + IdAndNewName, MovePayload, OpenCommandPanePayload, OpenFilePayload, + PluginCommand as ProtobufPluginCommand, PluginMessagePayload, + RequestPluginPermissionPayload, ResizePayload, RunCommandPayload, SetTimeoutPayload, SubscribePayload, SwitchSessionPayload, SwitchTabToPayload, UnsubscribePayload, }, plugin_permission::PermissionType as ProtobufPermissionType, @@ -14,7 +15,9 @@ pub use super::generated_api::api::{ use crate::data::{ConnectToSession, PermissionType, PluginCommand}; +use std::collections::BTreeMap; use std::convert::TryFrom; +use std::path::PathBuf; impl TryFrom for PluginCommand { type Error = &'static str; @@ -553,6 +556,27 @@ impl TryFrom for PluginCommand { }, _ => Err("Mismatched payload for OpenCommandPaneInPlace"), }, + Some(CommandName::RunCommand) => match protobuf_plugin_command.payload { + Some(Payload::RunCommandPayload(run_command_payload)) => { + let env_variables: BTreeMap = run_command_payload + .env_variables + .into_iter() + .map(|e| (e.name, e.value)) + .collect(); + let context: BTreeMap = run_command_payload + .context + .into_iter() + .map(|e| (e.name, e.value)) + .collect(); + Ok(PluginCommand::RunCommand( + run_command_payload.command_line, + env_variables, + PathBuf::from(run_command_payload.cwd), + context, + )) + }, + _ => Err("Mismatched payload for RunCommand"), + }, None => Err("Unrecognized plugin command"), } } @@ -928,6 +952,26 @@ impl TryFrom for ProtobufPluginCommand { }, )), }), + PluginCommand::RunCommand(command_line, env_variables, cwd, context) => { + let env_variables: Vec<_> = env_variables + .into_iter() + .map(|(name, value)| EnvVariable { name, value }) + .collect(); + let context: Vec<_> = context + .into_iter() + .map(|(name, value)| ContextItem { name, value }) + .collect(); + let cwd = cwd.display().to_string(); + Ok(ProtobufPluginCommand { + name: CommandName::RunCommand as i32, + payload: Some(Payload::RunCommandPayload(RunCommandPayload { + command_line, + env_variables, + cwd, + context, + })), + }) + }, } } }