From d780bd91052d8282ba5a7f06c6fb7faa7ca7cc18 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Wed, 17 Jan 2024 12:10:49 +0100 Subject: feat(plugins): introduce 'pipes', allowing users to pipe data to and control plugins from the command line (#3066) * prototype - working with message from the cli * prototype - pipe from the CLI to plugins * prototype - pipe from the CLI to plugins and back again * prototype - working with better cli interface * prototype - working after removing unused stuff * prototype - working with launching plugin if it is not launched, also fixed event ordering * refactor: change message to cli-message * prototype - allow plugins to send messages to each other * fix: allow cli messages to send plugin parameters (and implement backpressure) * fix: use input_pipe_id to identify cli pipes instead of their message name * fix: come cleanups and add skip_cache parameter * fix: pipe/client-server communication robustness * fix: leaking messages between plugins while loading * feat: allow plugins to specify how a new plugin instance is launched when sending messages * fix: add permissions * refactor: adjust cli api * fix: improve cli plugin loading error messages * docs: cli pipe * fix: take plugin configuration into account when messaging between plugins * refactor: pipe message protobuf interface * refactor: update(event) -> pipe * refactor - rename CliMessage to CliPipe * fix: add is_private to pipes and change some naming * refactor - cli client * refactor: various cleanups * style(fmt): rustfmt * fix(pipes): backpressure across multiple plugins * style: some cleanups * style(fmt): rustfmt * style: fix merge conflict mistake * style(wording): clarify pipe permission --- zellij-utils/assets/prost/api.action.rs | 19 ++- zellij-utils/assets/prost/api.pipe_message.rs | 52 ++++++++ zellij-utils/assets/prost/api.plugin_command.rs | 94 +++++++++++++- zellij-utils/assets/prost/api.plugin_permission.rs | 8 ++ zellij-utils/assets/prost/generated_plugin_api.rs | 3 + zellij-utils/src/cli.rs | 112 ++++++++++++++++ zellij-utils/src/data.rs | 125 +++++++++++++++++- zellij-utils/src/errors.rs | 11 ++ zellij-utils/src/input/actions.rs | 51 ++++++++ zellij-utils/src/input/layout.rs | 3 + zellij-utils/src/ipc.rs | 2 + zellij-utils/src/lib.rs | 2 +- zellij-utils/src/plugin_api/action.proto | 9 ++ zellij-utils/src/plugin_api/action.rs | 1 + zellij-utils/src/plugin_api/mod.rs | 1 + zellij-utils/src/plugin_api/pipe_message.proto | 23 ++++ zellij-utils/src/plugin_api/pipe_message.rs | 71 ++++++++++ zellij-utils/src/plugin_api/plugin_command.proto | 40 ++++++ zellij-utils/src/plugin_api/plugin_command.rs | 144 ++++++++++++++++++++- .../src/plugin_api/plugin_permission.proto | 2 + zellij-utils/src/plugin_api/plugin_permission.rs | 8 ++ 21 files changed, 773 insertions(+), 8 deletions(-) create mode 100644 zellij-utils/assets/prost/api.pipe_message.rs create mode 100644 zellij-utils/src/plugin_api/pipe_message.proto create mode 100644 zellij-utils/src/plugin_api/pipe_message.rs (limited to 'zellij-utils') diff --git a/zellij-utils/assets/prost/api.action.rs b/zellij-utils/assets/prost/api.action.rs index 908fffa77..4096b0740 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, 46" + 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" )] pub optional_payload: ::core::option::Option, } @@ -104,10 +104,24 @@ pub mod action { RenameSessionPayload(::prost::alloc::string::String), #[prost(message, tag = "46")] LaunchPluginPayload(super::LaunchOrFocusPluginPayload), + #[prost(message, tag = "47")] + MessagePayload(super::CliPipePayload), } } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct CliPipePayload { + #[prost(string, optional, tag = "1")] + pub name: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, tag = "2")] + pub payload: ::prost::alloc::string::String, + #[prost(message, repeated, tag = "3")] + pub args: ::prost::alloc::vec::Vec, + #[prost(string, optional, tag = "4")] + pub plugin: ::core::option::Option<::prost::alloc::string::String>, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct IdAndName { #[prost(bytes = "vec", tag = "1")] pub name: ::prost::alloc::vec::Vec, @@ -410,6 +424,7 @@ pub enum ActionName { BreakPaneLeft = 79, RenameSession = 80, LaunchPlugin = 81, + CliPipe = 82, } impl ActionName { /// String value of the enum field names used in the ProtoBuf definition. @@ -500,6 +515,7 @@ impl ActionName { ActionName::BreakPaneLeft => "BreakPaneLeft", ActionName::RenameSession => "RenameSession", ActionName::LaunchPlugin => "LaunchPlugin", + ActionName::CliPipe => "CliPipe", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -587,6 +603,7 @@ impl ActionName { "BreakPaneLeft" => Some(Self::BreakPaneLeft), "RenameSession" => Some(Self::RenameSession), "LaunchPlugin" => Some(Self::LaunchPlugin), + "CliPipe" => Some(Self::CliPipe), _ => None, } } diff --git a/zellij-utils/assets/prost/api.pipe_message.rs b/zellij-utils/assets/prost/api.pipe_message.rs new file mode 100644 index 000000000..96c566ff8 --- /dev/null +++ b/zellij-utils/assets/prost/api.pipe_message.rs @@ -0,0 +1,52 @@ +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PipeMessage { + #[prost(enumeration = "PipeSource", tag = "1")] + pub source: i32, + #[prost(string, optional, tag = "2")] + pub cli_source_id: ::core::option::Option<::prost::alloc::string::String>, + #[prost(uint32, optional, tag = "3")] + pub plugin_source_id: ::core::option::Option, + #[prost(string, tag = "4")] + pub name: ::prost::alloc::string::String, + #[prost(string, optional, tag = "5")] + pub payload: ::core::option::Option<::prost::alloc::string::String>, + #[prost(message, repeated, tag = "6")] + pub args: ::prost::alloc::vec::Vec, + #[prost(bool, tag = "7")] + pub is_private: bool, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Arg { + #[prost(string, tag = "1")] + pub key: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub value: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum PipeSource { + Cli = 0, + Plugin = 1, +} +impl PipeSource { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + PipeSource::Cli => "Cli", + PipeSource::Plugin => "Plugin", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "Cli" => Some(Self::Cli), + "Plugin" => Some(Self::Plugin), + _ => None, + } + } +} diff --git a/zellij-utils/assets/prost/api.plugin_command.rs b/zellij-utils/assets/prost/api.plugin_command.rs index 0dd5f6814..d9b528ae6 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, 43, 44, 45, 46" + 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" )] pub payload: ::core::option::Option, } @@ -104,10 +104,64 @@ pub mod plugin_command { DeleteDeadSessionPayload(::prost::alloc::string::String), #[prost(string, tag = "46")] RenameSessionPayload(::prost::alloc::string::String), + #[prost(string, tag = "47")] + UnblockCliPipeInputPayload(::prost::alloc::string::String), + #[prost(string, tag = "48")] + BlockCliPipeInputPayload(::prost::alloc::string::String), + #[prost(message, tag = "49")] + CliPipeOutputPayload(super::CliPipeOutputPayload), + #[prost(message, tag = "50")] + MessageToPluginPayload(super::MessageToPluginPayload), } } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct CliPipeOutputPayload { + #[prost(string, tag = "1")] + pub pipe_name: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub output: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MessageToPluginPayload { + #[prost(string, optional, tag = "1")] + pub plugin_url: ::core::option::Option<::prost::alloc::string::String>, + #[prost(message, repeated, tag = "2")] + pub plugin_config: ::prost::alloc::vec::Vec, + #[prost(string, tag = "3")] + pub message_name: ::prost::alloc::string::String, + #[prost(string, optional, tag = "4")] + pub message_payload: ::core::option::Option<::prost::alloc::string::String>, + #[prost(message, repeated, tag = "5")] + pub message_args: ::prost::alloc::vec::Vec, + #[prost(message, optional, tag = "6")] + pub new_plugin_args: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct NewPluginArgs { + #[prost(bool, optional, tag = "1")] + pub should_float: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub pane_id_to_replace: ::core::option::Option, + #[prost(string, optional, tag = "3")] + pub pane_title: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "4")] + pub cwd: ::core::option::Option<::prost::alloc::string::String>, + #[prost(bool, tag = "5")] + pub skip_cache: bool, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PaneId { + #[prost(enumeration = "PaneType", tag = "1")] + pub pane_type: i32, + #[prost(uint32, tag = "2")] + pub id: u32, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct SwitchSessionPayload { #[prost(string, optional, tag = "1")] pub name: ::core::option::Option<::prost::alloc::string::String>, @@ -318,6 +372,10 @@ pub enum CommandName { DeleteDeadSession = 73, DeleteAllDeadSessions = 74, RenameSession = 75, + UnblockCliPipeInput = 76, + BlockCliPipeInput = 77, + CliPipeOutput = 78, + MessageToPlugin = 79, } impl CommandName { /// String value of the enum field names used in the ProtoBuf definition. @@ -402,6 +460,10 @@ impl CommandName { CommandName::DeleteDeadSession => "DeleteDeadSession", CommandName::DeleteAllDeadSessions => "DeleteAllDeadSessions", CommandName::RenameSession => "RenameSession", + CommandName::UnblockCliPipeInput => "UnblockCliPipeInput", + CommandName::BlockCliPipeInput => "BlockCliPipeInput", + CommandName::CliPipeOutput => "CliPipeOutput", + CommandName::MessageToPlugin => "MessageToPlugin", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -483,6 +545,36 @@ impl CommandName { "DeleteDeadSession" => Some(Self::DeleteDeadSession), "DeleteAllDeadSessions" => Some(Self::DeleteAllDeadSessions), "RenameSession" => Some(Self::RenameSession), + "UnblockCliPipeInput" => Some(Self::UnblockCliPipeInput), + "BlockCliPipeInput" => Some(Self::BlockCliPipeInput), + "CliPipeOutput" => Some(Self::CliPipeOutput), + "MessageToPlugin" => Some(Self::MessageToPlugin), + _ => None, + } + } +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum PaneType { + Terminal = 0, + Plugin = 1, +} +impl PaneType { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + PaneType::Terminal => "Terminal", + PaneType::Plugin => "Plugin", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "Terminal" => Some(Self::Terminal), + "Plugin" => Some(Self::Plugin), _ => None, } } diff --git a/zellij-utils/assets/prost/api.plugin_permission.rs b/zellij-utils/assets/prost/api.plugin_permission.rs index d33fa950a..f928625da 100644 --- a/zellij-utils/assets/prost/api.plugin_permission.rs +++ b/zellij-utils/assets/prost/api.plugin_permission.rs @@ -8,6 +8,8 @@ pub enum PermissionType { OpenTerminalsOrPlugins = 4, WriteToStdin = 5, WebAccess = 6, + ReadCliPipes = 7, + MessageAndLaunchOtherPlugins = 8, } impl PermissionType { /// String value of the enum field names used in the ProtoBuf definition. @@ -23,6 +25,10 @@ impl PermissionType { PermissionType::OpenTerminalsOrPlugins => "OpenTerminalsOrPlugins", PermissionType::WriteToStdin => "WriteToStdin", PermissionType::WebAccess => "WebAccess", + PermissionType::ReadCliPipes => "ReadCliPipes", + PermissionType::MessageAndLaunchOtherPlugins => { + "MessageAndLaunchOtherPlugins" + } } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -35,6 +41,8 @@ impl PermissionType { "OpenTerminalsOrPlugins" => Some(Self::OpenTerminalsOrPlugins), "WriteToStdin" => Some(Self::WriteToStdin), "WebAccess" => Some(Self::WebAccess), + "ReadCliPipes" => Some(Self::ReadCliPipes), + "MessageAndLaunchOtherPlugins" => Some(Self::MessageAndLaunchOtherPlugins), _ => None, } } diff --git a/zellij-utils/assets/prost/generated_plugin_api.rs b/zellij-utils/assets/prost/generated_plugin_api.rs index a7e73f652..ee70c4709 100644 --- a/zellij-utils/assets/prost/generated_plugin_api.rs +++ b/zellij-utils/assets/prost/generated_plugin_api.rs @@ -20,6 +20,9 @@ pub mod api { pub mod message { include!("api.message.rs"); } + pub mod pipe_message { + include!("api.pipe_message.rs"); + } pub mod plugin_command { include!("api.plugin_command.rs"); } diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs index 97a0cd778..bf8f8880d 100644 --- a/zellij-utils/src/cli.rs +++ b/zellij-utils/src/cli.rs @@ -289,6 +289,43 @@ pub enum Sessions { ConvertTheme { old_theme_file: PathBuf, }, + /// Send data to one or more plugins, launch them if they are not running. + #[clap(override_usage( +r#" +zellij pipe [OPTIONS] [--] + +* Send data to a specific plugin: + +zellij pipe --plugin file:/path/to/my/plugin.wasm --name my_pipe_name -- my_arbitrary_data + +* To all running plugins (that are listening): + +zellij pipe --name my_pipe_name -- my_arbitrary_data + +* Pipe data into this command's STDIN and get output from the plugin on this command's STDOUT + +tail -f /tmp/my-live-logfile | zellij pipe --name logs --plugin https://example.com/my-plugin.wasm | wc -l +"#))] + Pipe { + /// The name of the pipe + #[clap(short, long, value_parser, display_order(1))] + name: Option, + /// The data to send down this pipe (if blank, will listen to STDIN) + payload: Option, + + #[clap(short, long, value_parser, display_order(2))] + /// The args of the pipe + args: Option, // TODO: we might want to not re-use + // PluginUserConfiguration + /// The plugin url (eg. file:/tmp/my-plugin.wasm) to direct this pipe to, if not specified, + /// will be sent to all plugins, if specified and is not running, the plugin will be launched + #[clap(short, long, value_parser, display_order(3))] + plugin: Option, + /// The plugin configuration (note: the same plugin with different configuration is + /// considered a different plugin for the purposes of determining the pipe destination) + #[clap(short('c'), long, value_parser, display_order(4))] + plugin_configuration: Option, + }, } #[derive(Debug, Subcommand, Clone, Serialize, Deserialize)] @@ -549,4 +586,79 @@ pub enum CliAction { RenameSession { name: String, }, + /// Send data to one or more plugins, launch them if they are not running. + #[clap(override_usage( +r#" +zellij action pipe [OPTIONS] [--] + +* Send data to a specific plugin: + +zellij action pipe --plugin file:/path/to/my/plugin.wasm --name my_pipe_name -- my_arbitrary_data + +* To all running plugins (that are listening): + +zellij action pipe --name my_pipe_name -- my_arbitrary_data + +* Pipe data into this command's STDIN and get output from the plugin on this command's STDOUT + +tail -f /tmp/my-live-logfile | zellij action pipe --name logs --plugin https://example.com/my-plugin.wasm | wc -l +"#))] + Pipe { + /// The name of the pipe + #[clap(short, long, value_parser, display_order(1))] + name: Option, + /// The data to send down this pipe (if blank, will listen to STDIN) + payload: Option, + + #[clap(short, long, value_parser, display_order(2))] + /// The args of the pipe + args: Option, // TODO: we might want to not re-use + // PluginUserConfiguration + /// The plugin url (eg. file:/tmp/my-plugin.wasm) to direct this pipe to, if not specified, + /// will be sent to all plugins, if specified and is not running, the plugin will be launched + #[clap(short, long, value_parser, display_order(3))] + plugin: Option, + /// The plugin configuration (note: the same plugin with different configuration is + /// considered a different plugin for the purposes of determining the pipe destination) + #[clap(short('c'), long, value_parser, display_order(4))] + plugin_configuration: Option, + /// Launch a new plugin even if one is already running + #[clap( + short('l'), + long, + value_parser, + takes_value(false), + default_value("false"), + display_order(5) + )] + force_launch_plugin: bool, + /// If launching a new plugin, skip cache and force-compile the plugin + #[clap( + short('s'), + long, + value_parser, + takes_value(false), + default_value("false"), + display_order(6) + )] + skip_plugin_cache: bool, + /// If launching a plugin, should it be floating or not, defaults to floating + #[clap(short('f'), long, value_parser, display_order(7))] + floating_plugin: Option, + /// If launching a plugin, launch it in-place (on top of the current pane) + #[clap( + short('i'), + long, + value_parser, + conflicts_with("floating-plugin"), + display_order(8) + )] + in_place_plugin: Option, + /// If launching a plugin, specify its working directory + #[clap(short('w'), long, value_parser, display_order(9))] + plugin_cwd: Option, + /// If launching a plugin, specify its pane title + #[clap(short('t'), long, value_parser, display_order(10))] + plugin_title: Option, + }, } diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs index bed6f4747..f1e01115d 100644 --- a/zellij-utils/src/data.rs +++ b/zellij-utils/src/data.rs @@ -538,6 +538,8 @@ pub enum Permission { OpenTerminalsOrPlugins, WriteToStdin, WebAccess, + ReadCliPipes, + MessageAndLaunchOtherPlugins, } impl PermissionType { @@ -554,6 +556,10 @@ impl PermissionType { PermissionType::OpenTerminalsOrPlugins => "Start new terminals and plugins".to_owned(), PermissionType::WriteToStdin => "Write to standard input (STDIN)".to_owned(), PermissionType::WebAccess => "Make web requests".to_owned(), + PermissionType::ReadCliPipes => "Control command line pipes and output".to_owned(), + PermissionType::MessageAndLaunchOtherPlugins => { + "Send messages to and launch other plugins".to_owned() + }, } } } @@ -975,6 +981,86 @@ impl CommandToRun { } } +#[derive(Debug, Default, Clone)] +pub struct MessageToPlugin { + pub plugin_url: Option, + pub plugin_config: BTreeMap, + pub message_name: String, + pub message_payload: Option, + pub message_args: BTreeMap, + /// these will only be used in case we need to launch a new plugin to send this message to, + /// since none are running + pub new_plugin_args: Option, +} + +#[derive(Debug, Default, Clone)] +pub struct NewPluginArgs { + pub should_float: Option, + pub pane_id_to_replace: Option, + pub pane_title: Option, + pub cwd: Option, + pub skip_cache: bool, +} + +#[derive(Debug, Clone, Copy)] +pub enum PaneId { + Terminal(u32), + Plugin(u32), +} + +impl MessageToPlugin { + pub fn new(message_name: impl Into) -> Self { + MessageToPlugin { + message_name: message_name.into(), + ..Default::default() + } + } + pub fn with_plugin_url(mut self, url: impl Into) -> Self { + self.plugin_url = Some(url.into()); + self + } + pub fn with_plugin_config(mut self, plugin_config: BTreeMap) -> Self { + self.plugin_config = plugin_config; + self + } + pub fn with_payload(mut self, payload: impl Into) -> Self { + self.message_payload = Some(payload.into()); + self + } + pub fn with_args(mut self, args: BTreeMap) -> Self { + self.message_args = args; + self + } + pub fn new_plugin_instance_should_float(mut self, should_float: bool) -> Self { + let mut new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default); + new_plugin_args.should_float = Some(should_float); + self + } + pub fn new_plugin_instance_should_replace_pane(mut self, pane_id: PaneId) -> Self { + let mut new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default); + new_plugin_args.pane_id_to_replace = Some(pane_id); + self + } + pub fn new_plugin_instance_should_have_pane_title( + mut self, + pane_title: impl Into, + ) -> Self { + let mut new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default); + new_plugin_args.pane_title = Some(pane_title.into()); + self + } + pub fn new_plugin_instance_should_have_cwd(mut self, cwd: PathBuf) -> Self { + let mut new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default); + new_plugin_args.cwd = Some(cwd); + self + } + pub fn new_plugin_instance_should_skip_cache(mut self) -> Self { + let mut new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default); + new_plugin_args.skip_cache = true; + self + } +} + #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct ConnectToSession { pub name: Option, @@ -1014,6 +1100,39 @@ pub enum HttpVerb { Delete, } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum PipeSource { + Cli(String), // String is the pipe_id of the CLI pipe (used for blocking/unblocking) + Plugin(u32), // u32 is the lugin id +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PipeMessage { + pub source: PipeSource, + pub name: String, + pub payload: Option, + pub args: BTreeMap, + pub is_private: bool, +} + +impl PipeMessage { + pub fn new( + source: PipeSource, + name: impl Into, + payload: &Option, + args: &Option>, + is_private: bool, + ) -> Self { + PipeMessage { + source, + name: name.into(), + payload: payload.clone(), + args: args.clone().unwrap_or_else(|| Default::default()), + is_private, + } + } +} + #[derive(Debug, Clone, EnumDiscriminants, ToString)] #[strum_discriminants(derive(EnumString, Hash, Serialize, Deserialize))] #[strum_discriminants(name(CommandType))] @@ -1104,5 +1223,9 @@ pub enum PluginCommand { Vec, // body BTreeMap, // context ), - RenameSession(String), // String -> new session name + RenameSession(String), // String -> new session name + UnblockCliPipeInput(String), // String => pipe name + BlockCliPipeInput(String), // String => pipe name + CliPipeOutput(String, String), // String => pipe name, String => output + MessageToPlugin(MessageToPlugin), } diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index aff97f2d8..26ad521c3 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -393,6 +393,11 @@ pub enum PluginContext { PermissionRequestResult, DumpLayout, LogLayoutToHd, + CliPipe, + Message, + CachePluginEvents, + MessageFromPlugin, + UnblockCliPipes, } /// Stack call representations corresponding to the different types of [`ClientInstruction`]s. @@ -413,6 +418,8 @@ pub enum ClientContext { DoneParsingStdinQuery, SwitchSession, SetSynchronisedOutput, + UnblockCliPipeInput, + CliPipeOutput, } /// Stack call representations corresponding to the different types of [`ServerInstruction`]s. @@ -430,7 +437,11 @@ pub enum ServerContext { ConnStatus, ActiveClients, Log, + LogError, SwitchSession, + UnblockCliPipeInput, + CliPipeOutput, + AssociatePipeWithClient, } #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index 8af5d28ee..74c6c1e2e 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -13,6 +13,8 @@ use crate::input::config::{Config, ConfigError, KdlError}; use crate::input::options::OnForceClose; use miette::{NamedSource, Report}; use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use uuid::Uuid; use std::path::PathBuf; use std::str::FromStr; @@ -256,6 +258,20 @@ pub enum Action { BreakPaneRight, BreakPaneLeft, RenameSession(String), + CliPipe { + pipe_id: String, + name: Option, + payload: Option, + args: Option>, + plugin: Option, + configuration: Option>, + launch_new: bool, + skip_cache: bool, + floating: Option, + in_place: Option, + cwd: Option, + pane_title: Option, + }, } impl Action { @@ -582,6 +598,41 @@ impl Action { )]) }, CliAction::RenameSession { name } => Ok(vec![Action::RenameSession(name)]), + CliAction::Pipe { + name, + payload, + args, + plugin, + plugin_configuration, + force_launch_plugin, + skip_plugin_cache, + floating_plugin, + in_place_plugin, + plugin_cwd, + plugin_title, + } => { + let current_dir = get_current_dir(); + let cwd = plugin_cwd + .map(|cwd| current_dir.join(cwd)) + .or_else(|| Some(current_dir)); + let skip_cache = skip_plugin_cache; + let pipe_id = Uuid::new_v4().to_string(); + Ok(vec![Action::CliPipe { + pipe_id, + name, + payload, + args: args.map(|a| a.inner().clone()), // TODO: no clone somehow + plugin, + configuration: plugin_configuration.map(|a| a.inner().clone()), // TODO: no clone + // somehow + launch_new: force_launch_plugin, + floating: floating_plugin, + in_place: in_place_plugin, + cwd, + pane_title: plugin_title, + skip_cache, + }]) + }, } } } diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs index 40ca72097..82dab15ee 100644 --- a/zellij-utils/src/input/layout.rs +++ b/zellij-utils/src/input/layout.rs @@ -265,6 +265,9 @@ impl PluginUserConfiguration { pub fn inner(&self) -> &BTreeMap { &self.0 } + pub fn insert(&mut self, config_key: impl Into, config_value: impl Into) { + self.0.insert(config_key.into(), config_value.into()); + } } impl FromStr for PluginUserConfiguration { diff --git a/zellij-utils/src/ipc.rs b/zellij-utils/src/ipc.rs index b6e837228..902d02352 100644 --- a/zellij-utils/src/ipc.rs +++ b/zellij-utils/src/ipc.rs @@ -103,6 +103,8 @@ pub enum ServerToClientMsg { Log(Vec), LogError(Vec), SwitchSession(ConnectToSession), + UnblockCliPipeInput(String), // String -> pipe name + CliPipeOutput(String, String), // String -> pipe name, String -> Output } #[derive(Serialize, Deserialize, Debug, Clone)] diff --git a/zellij-utils/src/lib.rs b/zellij-utils/src/lib.rs index 05752e21a..de5848eca 100644 --- a/zellij-utils/src/lib.rs +++ b/zellij-utils/src/lib.rs @@ -27,7 +27,7 @@ pub mod logging; // Requires log4rs pub use ::{ anyhow, async_channel, async_std, clap, common_path, humantime, interprocess, lazy_static, libc, miette, nix, notify_debouncer_full, regex, serde, signal_hook, surf, tempfile, termwiz, - vte, + url, uuid, vte, }; pub use ::prost; diff --git a/zellij-utils/src/plugin_api/action.proto b/zellij-utils/src/plugin_api/action.proto index da10d82a8..0ed5b3b7a 100644 --- a/zellij-utils/src/plugin_api/action.proto +++ b/zellij-utils/src/plugin_api/action.proto @@ -53,9 +53,17 @@ message Action { IdAndName rename_tab_payload = 44; string rename_session_payload = 45; LaunchOrFocusPluginPayload launch_plugin_payload = 46; + CliPipePayload message_payload = 47; } } +message CliPipePayload { + optional string name = 1; + string payload = 2; + repeated NameAndValue args = 3; + optional string plugin = 4; +} + message IdAndName { bytes name = 1; uint32 id = 2; @@ -227,6 +235,7 @@ enum ActionName { BreakPaneLeft = 79; RenameSession = 80; LaunchPlugin = 81; + CliPipe = 82; } message Position { diff --git a/zellij-utils/src/plugin_api/action.rs b/zellij-utils/src/plugin_api/action.rs index 71eaaa130..82de4c7ee 100644 --- a/zellij-utils/src/plugin_api/action.rs +++ b/zellij-utils/src/plugin_api/action.rs @@ -1246,6 +1246,7 @@ impl TryFrom for ProtobufAction { | Action::Deny | Action::Copy | Action::DumpLayout + | Action::CliPipe { .. } | Action::SkipConfirm(..) => Err("Unsupported action"), } } diff --git a/zellij-utils/src/plugin_api/mod.rs b/zellij-utils/src/plugin_api/mod.rs index 40057fde0..0e26485e9 100644 --- a/zellij-utils/src/plugin_api/mod.rs +++ b/zellij-utils/src/plugin_api/mod.rs @@ -5,6 +5,7 @@ pub mod file; pub mod input_mode; pub mod key; pub mod message; +pub mod pipe_message; pub mod plugin_command; pub mod plugin_ids; pub mod plugin_permission; diff --git a/zellij-utils/src/plugin_api/pipe_message.proto b/zellij-utils/src/plugin_api/pipe_message.proto new file mode 100644 index 000000000..5f488a758 --- /dev/null +++ b/zellij-utils/src/plugin_api/pipe_message.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package api.pipe_message; + +message PipeMessage { + PipeSource source = 1; + optional string cli_source_id = 2; + optional uint32 plugin_source_id = 3; + string name = 4; + optional string payload = 5; + repeated Arg args = 6; + bool is_private = 7; +} + +enum PipeSource { + Cli = 0; + Plugin = 1; +} + +message Arg { + string key = 1; + string value = 2; +} diff --git a/zellij-utils/src/plugin_api/pipe_message.rs b/zellij-utils/src/plugin_api/pipe_message.rs new file mode 100644 index 000000000..dbfe49305 --- /dev/null +++ b/zellij-utils/src/plugin_api/pipe_message.rs @@ -0,0 +1,71 @@ +pub use super::generated_api::api::pipe_message::{ + Arg as ProtobufArg, PipeMessage as ProtobufPipeMessage, PipeSource as ProtobufPipeSource, +}; +use crate::data::{PipeMessage, PipeSource}; + +use std::convert::TryFrom; + +impl TryFrom for PipeMessage { + type Error = &'static str; + fn try_from(protobuf_pipe_message: ProtobufPipeMessage) -> Result { + let source = match ( + ProtobufPipeSource::from_i32(protobuf_pipe_message.source), + protobuf_pipe_message.cli_source_id, + protobuf_pipe_message.plugin_source_id, + ) { + (Some(ProtobufPipeSource::Cli), Some(cli_source_id), _) => { + PipeSource::Cli(cli_source_id) + }, + (Some(ProtobufPipeSource::Plugin), _, Some(plugin_source_id)) => { + PipeSource::Plugin(plugin_source_id) + }, + _ => return Err("Invalid PipeSource or payload"), + }; + let name = protobuf_pipe_message.name; + let payload = protobuf_pipe_message.payload; + let args = protobuf_pipe_message + .args + .into_iter() + .map(|arg| (arg.key, arg.value)) + .collect(); + let is_private = protobuf_pipe_message.is_private; + Ok(PipeMessage { + source, + name, + payload, + args, + is_private, + }) + } +} + +impl TryFrom for ProtobufPipeMessage { + type Error = &'static str; + fn try_from(pipe_message: PipeMessage) -> Result { + let (source, cli_source_id, plugin_source_id) = match pipe_message.source { + PipeSource::Cli(input_pipe_id) => { + (ProtobufPipeSource::Cli as i32, Some(input_pipe_id), None) + }, + PipeSource::Plugin(plugin_id) => { + (ProtobufPipeSource::Plugin as i32, None, Some(plugin_id)) + }, + }; + let name = pipe_message.name; + let payload = pipe_message.payload; + let args: Vec<_> = pipe_message + .args + .into_iter() + .map(|(key, value)| ProtobufArg { key, value }) + .collect(); + let is_private = pipe_message.is_private; + Ok(ProtobufPipeMessage { + source, + cli_source_id, + plugin_source_id, + name, + payload, + args, + is_private, + }) + } +} diff --git a/zellij-utils/src/plugin_api/plugin_command.proto b/zellij-utils/src/plugin_api/plugin_command.proto index 53994a88c..6ffb0345b 100644 --- a/zellij-utils/src/plugin_api/plugin_command.proto +++ b/zellij-utils/src/plugin_api/plugin_command.proto @@ -87,6 +87,10 @@ enum CommandName { DeleteDeadSession = 73; DeleteAllDeadSessions = 74; RenameSession = 75; + UnblockCliPipeInput = 76; + BlockCliPipeInput = 77; + CliPipeOutput = 78; + MessageToPlugin = 79; } message PluginCommand { @@ -137,9 +141,45 @@ message PluginCommand { WebRequestPayload web_request_payload = 44; string delete_dead_session_payload = 45; string rename_session_payload = 46; + string unblock_cli_pipe_input_payload = 47; + string block_cli_pipe_input_payload = 48; + CliPipeOutputPayload cli_pipe_output_payload = 49; + MessageToPluginPayload message_to_plugin_payload = 50; } } +message CliPipeOutputPayload { + string pipe_name = 1; + string output = 2; +} + +message MessageToPluginPayload { + optional string plugin_url = 1; + repeated ContextItem plugin_config = 2; + string message_name = 3; + optional string message_payload = 4; + repeated ContextItem message_args = 5; + optional NewPluginArgs new_plugin_args = 6; +} + +message NewPluginArgs { + optional bool should_float = 1; + optional PaneId pane_id_to_replace = 2; + optional string pane_title = 3; + optional string cwd = 4; + bool skip_cache = 5; +} + +message PaneId { + PaneType pane_type = 1; + uint32 id = 2; +} + +enum PaneType { + Terminal = 0; + Plugin = 1; +} + message SwitchSessionPayload { optional string name = 1; optional uint32 tab_position = 2; diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs index ed476687c..fa570a267 100644 --- a/zellij-utils/src/plugin_api/plugin_command.rs +++ b/zellij-utils/src/plugin_api/plugin_command.rs @@ -3,9 +3,11 @@ pub use super::generated_api::api::{ event::{EventNameList as ProtobufEventNameList, Header}, input_mode::InputMode as ProtobufInputMode, plugin_command::{ - plugin_command::Payload, CommandName, ContextItem, EnvVariable, ExecCmdPayload, - HttpVerb as ProtobufHttpVerb, IdAndNewName, MovePayload, OpenCommandPanePayload, - OpenFilePayload, PluginCommand as ProtobufPluginCommand, PluginMessagePayload, + plugin_command::Payload, CliPipeOutputPayload, CommandName, ContextItem, EnvVariable, + ExecCmdPayload, HttpVerb as ProtobufHttpVerb, IdAndNewName, MessageToPluginPayload, + MovePayload, NewPluginArgs as ProtobufNewPluginArgs, OpenCommandPanePayload, + OpenFilePayload, PaneId as ProtobufPaneId, PaneType as ProtobufPaneType, + PluginCommand as ProtobufPluginCommand, PluginMessagePayload, RequestPluginPermissionPayload, ResizePayload, RunCommandPayload, SetTimeoutPayload, SubscribePayload, SwitchSessionPayload, SwitchTabToPayload, UnsubscribePayload, WebRequestPayload, @@ -14,7 +16,10 @@ pub use super::generated_api::api::{ resize::ResizeAction as ProtobufResizeAction, }; -use crate::data::{ConnectToSession, HttpVerb, PermissionType, PluginCommand}; +use crate::data::{ + ConnectToSession, HttpVerb, MessageToPlugin, NewPluginArgs, PaneId, PermissionType, + PluginCommand, +}; use std::collections::BTreeMap; use std::convert::TryFrom; @@ -42,6 +47,33 @@ impl Into for HttpVerb { } } +impl TryFrom for PaneId { + type Error = &'static str; + fn try_from(protobuf_pane_id: ProtobufPaneId) -> Result { + match ProtobufPaneType::from_i32(protobuf_pane_id.pane_type) { + Some(ProtobufPaneType::Terminal) => Ok(PaneId::Terminal(protobuf_pane_id.id)), + Some(ProtobufPaneType::Plugin) => Ok(PaneId::Plugin(protobuf_pane_id.id)), + None => Err("Failed to convert PaneId"), + } + } +} + +impl TryFrom for ProtobufPaneId { + type Error = &'static str; + fn try_from(pane_id: PaneId) -> Result { + match pane_id { + PaneId::Terminal(id) => Ok(ProtobufPaneId { + pane_type: ProtobufPaneType::Terminal as i32, + id, + }), + PaneId::Plugin(id) => Ok(ProtobufPaneId { + pane_type: ProtobufPaneType::Plugin as i32, + id, + }), + } + } +} + impl TryFrom for PluginCommand { type Error = &'static str; fn try_from(protobuf_plugin_command: ProtobufPluginCommand) -> Result { @@ -641,6 +673,62 @@ impl TryFrom for PluginCommand { }, _ => Err("Mismatched payload for RenameSession"), }, + Some(CommandName::UnblockCliPipeInput) => match protobuf_plugin_command.payload { + Some(Payload::UnblockCliPipeInputPayload(pipe_name)) => { + Ok(PluginCommand::UnblockCliPipeInput(pipe_name)) + }, + _ => Err("Mismatched payload for UnblockPipeInput"), + }, + Some(CommandName::BlockCliPipeInput) => match protobuf_plugin_command.payload { + Some(Payload::BlockCliPipeInputPayload(pipe_name)) => { + Ok(PluginCommand::BlockCliPipeInput(pipe_name)) + }, + _ => Err("Mismatched payload for BlockPipeInput"), + }, + Some(CommandName::CliPipeOutput) => match protobuf_plugin_command.payload { + Some(Payload::CliPipeOutputPayload(CliPipeOutputPayload { pipe_name, output })) => { + Ok(PluginCommand::CliPipeOutput(pipe_name, output)) + }, + _ => Err("Mismatched payload for PipeOutput"), + }, + Some(CommandName::MessageToPlugin) => match protobuf_plugin_command.payload { + Some(Payload::MessageToPluginPayload(MessageToPluginPayload { + plugin_url, + plugin_config, + message_name, + message_payload, + message_args, + new_plugin_args, + })) => { + let plugin_config: BTreeMap = plugin_config + .into_iter() + .map(|e| (e.name, e.value)) + .collect(); + let message_args: BTreeMap = message_args + .into_iter() + .map(|e| (e.name, e.value)) + .collect(); + Ok(PluginCommand::MessageToPlugin(MessageToPlugin { + plugin_url, + plugin_config, + message_name, + message_payload, + message_args, + new_plugin_args: new_plugin_args.and_then(|protobuf_new_plugin_args| { + Some(NewPluginArgs { + should_float: protobuf_new_plugin_args.should_float, + pane_id_to_replace: protobuf_new_plugin_args + .pane_id_to_replace + .and_then(|p_id| PaneId::try_from(p_id).ok()), + pane_title: protobuf_new_plugin_args.pane_title, + cwd: protobuf_new_plugin_args.cwd.map(|cwd| PathBuf::from(cwd)), + skip_cache: protobuf_new_plugin_args.skip_cache, + }) + }), + })) + }, + _ => Err("Mismatched payload for PipeOutput"), + }, None => Err("Unrecognized plugin command"), } } @@ -1069,6 +1157,54 @@ impl TryFrom for ProtobufPluginCommand { name: CommandName::RenameSession as i32, payload: Some(Payload::RenameSessionPayload(new_session_name)), }), + PluginCommand::UnblockCliPipeInput(pipe_name) => Ok(ProtobufPluginCommand { + name: CommandName::UnblockCliPipeInput as i32, + payload: Some(Payload::UnblockCliPipeInputPayload(pipe_name)), + }), + PluginCommand::BlockCliPipeInput(pipe_name) => Ok(ProtobufPluginCommand { + name: CommandName::BlockCliPipeInput as i32, + payload: Some(Payload::BlockCliPipeInputPayload(pipe_name)), + }), + PluginCommand::CliPipeOutput(pipe_name, output) => Ok(ProtobufPluginCommand { + name: CommandName::CliPipeOutput as i32, + payload: Some(Payload::CliPipeOutputPayload(CliPipeOutputPayload { + pipe_name, + output, + })), + }), + PluginCommand::MessageToPlugin(message_to_plugin) => { + let plugin_config: Vec<_> = message_to_plugin + .plugin_config + .into_iter() + .map(|(name, value)| ContextItem { name, value }) + .collect(); + let message_args: Vec<_> = message_to_plugin + .message_args + .into_iter() + .map(|(name, value)| ContextItem { name, value }) + .collect(); + Ok(ProtobufPluginCommand { + name: CommandName::MessageToPlugin as i32, + payload: Some(Payload::MessageToPluginPayload(MessageToPluginPayload { + plugin_url: message_to_plugin.plugin_url, + plugin_config, + message_name: message_to_plugin.message_name, + message_payload: message_to_plugin.message_payload, + message_args, + new_plugin_args: message_to_plugin.new_plugin_args.map(|m_t_p| { + ProtobufNewPluginArgs { + should_float: m_t_p.should_float, + pane_id_to_replace: m_t_p + .pane_id_to_replace + .and_then(|p_id| ProtobufPaneId::try_from(p_id).ok()), + pane_title: m_t_p.pane_title, + cwd: m_t_p.cwd.map(|cwd| cwd.display().to_string()), + skip_cache: m_t_p.skip_cache, + } + }), + })), + }) + }, } } } diff --git a/zellij-utils/src/plugin_api/plugin_permission.proto b/zellij-utils/src/plugin_api/plugin_permission.proto index a796c7481..761384c1d 100644 --- a/zellij-utils/src/plugin_api/plugin_permission.proto +++ b/zellij-utils/src/plugin_api/plugin_permission.proto @@ -10,4 +10,6 @@ enum PermissionType { OpenTerminalsOrPlugins = 4; WriteToStdin = 5; WebAccess = 6; + ReadCliPipes = 7; + MessageAndLaunchOtherPlugins = 8; } diff --git a/zellij-utils/src/plugin_api/plugin_permission.rs b/zellij-utils/src/plugin_api/plugin_permission.rs index c9f0d49f5..4f258ac28 100644 --- a/zellij-utils/src/plugin_api/plugin_permission.rs +++ b/zellij-utils/src/plugin_api/plugin_permission.rs @@ -20,6 +20,10 @@ impl TryFrom for PermissionType { }, ProtobufPermissionType::WriteToStdin => Ok(PermissionType::WriteToStdin), ProtobufPermissionType::WebAccess => Ok(PermissionType::WebAccess), + ProtobufPermissionType::ReadCliPipes => Ok(PermissionType::ReadCliPipes), + ProtobufPermissionType::MessageAndLaunchOtherPlugins => { + Ok(PermissionType::MessageAndLaunchOtherPlugins) + }, } } } @@ -41,6 +45,10 @@ impl TryFrom for ProtobufPermissionType { }, PermissionType::WriteToStdin => Ok(ProtobufPermissionType::WriteToStdin), PermissionType::WebAccess => Ok(ProtobufPermissionType::WebAccess), + PermissionType::ReadCliPipes => Ok(ProtobufPermissionType::ReadCliPipes), + PermissionType::MessageAndLaunchOtherPlugins => { + Ok(ProtobufPermissionType::MessageAndLaunchOtherPlugins) + }, } } } -- cgit v1.2.3