feat(pipes): allow piping messages to plugins from keybindings (#3212)

This commit is contained in:
Aram Drevekenin 2024-03-21 16:52:15 +01:00 committed by GitHub
parent 8504881e4e
commit 07b76ee610
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 227 additions and 22 deletions

View file

@ -339,6 +339,7 @@ impl ZellijPlugin for State {
let input_pipe_id = match pipe_message.source {
PipeSource::Cli(id) => id.clone(),
PipeSource::Plugin(id) => format!("{}", id),
PipeSource::Keybind => format!("keybind"),
};
let name = pipe_message.name;
let payload = pipe_message.payload;

View file

@ -119,6 +119,19 @@ pub enum PluginInstruction {
skip_cache: bool,
cli_client_id: ClientId,
},
KeybindPipe {
name: String,
payload: Option<String>,
plugin: Option<String>,
args: Option<BTreeMap<String, String>>,
configuration: Option<BTreeMap<String, String>>,
floating: Option<bool>,
pane_id_to_replace: Option<PaneId>,
pane_title: Option<String>,
cwd: Option<PathBuf>,
skip_cache: bool,
cli_client_id: ClientId,
},
CachePluginEvents {
plugin_id: PluginId,
},
@ -164,6 +177,7 @@ impl From<&PluginInstruction> for PluginContext {
PluginInstruction::MessageFromPlugin { .. } => PluginContext::MessageFromPlugin,
PluginInstruction::UnblockCliPipes { .. } => PluginContext::UnblockCliPipes,
PluginInstruction::WatchFilesystem => PluginContext::WatchFilesystem,
PluginInstruction::KeybindPipe { .. } => PluginContext::KeybindPipe,
}
}
}
@ -429,7 +443,7 @@ pub(crate) fn plugin_thread_main(
)];
wasm_bridge.update_plugins(updates, shutdown_send.clone())?;
},
PluginInstruction::PluginSubscribedToEvents(_plugin_id, _client_id, events) => {
PluginInstruction::PluginSubscribedToEvents(_plugin_id, _client_id, _events) => {
// no-op, there used to be stuff we did here - now there isn't, but we might want
// to add stuff here in the future
},
@ -529,6 +543,57 @@ pub(crate) fn plugin_thread_main(
}
wasm_bridge.pipe_messages(pipe_messages, shutdown_send.clone())?;
},
PluginInstruction::KeybindPipe {
name,
payload,
plugin,
args,
configuration,
floating,
pane_id_to_replace,
pane_title,
cwd,
skip_cache,
cli_client_id,
} => {
let should_float = floating.unwrap_or(true);
let mut pipe_messages = vec![];
match plugin {
Some(plugin_url) => {
// send to specific plugin(s)
pipe_to_specific_plugins(
PipeSource::Keybind,
&plugin_url,
&configuration,
&cwd,
skip_cache,
should_float,
&pane_id_to_replace,
&pane_title,
Some(cli_client_id),
&mut pipe_messages,
&name,
&payload,
&args,
&bus,
&mut wasm_bridge,
&plugin_aliases,
);
},
None => {
// no specific destination, send to all plugins
pipe_to_all_plugins(
PipeSource::Keybind,
&name,
&payload,
&args,
&mut wasm_bridge,
&mut pipe_messages,
);
},
}
wasm_bridge.pipe_messages(pipe_messages, shutdown_send.clone())?;
},
PluginInstruction::CachePluginEvents { plugin_id } => {
wasm_bridge.cache_plugin_events(plugin_id);
},

View file

@ -1,4 +1,4 @@
use std::collections::{HashSet, VecDeque};
use std::collections::{BTreeMap, HashSet, VecDeque};
use std::sync::{Arc, RwLock};
use crate::thread_bus::ThreadSenders;
@ -10,6 +10,7 @@ use crate::{
screen::ScreenInstruction,
ServerInstruction, SessionMetaData, SessionState,
};
use uuid::Uuid;
use zellij_utils::{
channels::SenderWithContext,
data::{Direction, Event, PluginCapabilities, ResizeStrategy},
@ -883,6 +884,48 @@ pub(crate) fn route_action(
log::error!("Message must have a name");
}
},
Action::KeybindPipe {
mut name,
payload,
plugin,
args,
mut configuration,
floating,
in_place,
skip_cache,
cwd,
pane_title,
launch_new,
..
} => {
if let Some(name) = name.take() {
let should_open_in_place = in_place.unwrap_or(false);
let pane_id_to_replace = if should_open_in_place { pane_id } else { None };
if launch_new {
// we do this to make sure the plugin is unique (has a unique configuration parameter)
configuration
.get_or_insert_with(BTreeMap::new)
.insert("_zellij_id".to_owned(), Uuid::new_v4().to_string());
}
senders
.send_to_plugin(PluginInstruction::KeybindPipe {
name,
payload,
plugin,
args,
configuration,
floating,
pane_id_to_replace,
cwd,
pane_title,
skip_cache,
cli_client_id: client_id,
})
.with_context(err_context)?;
} else {
log::error!("Message must have a name");
}
},
}
Ok(should_break)
}

View file

@ -454,6 +454,7 @@ pub enum ActionName {
LaunchPlugin = 81,
CliPipe = 82,
MoveTab = 83,
KeybindPipe = 84,
}
impl ActionName {
/// String value of the enum field names used in the ProtoBuf definition.
@ -546,6 +547,7 @@ impl ActionName {
ActionName::LaunchPlugin => "LaunchPlugin",
ActionName::CliPipe => "CliPipe",
ActionName::MoveTab => "MoveTab",
ActionName::KeybindPipe => "KeybindPipe",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
@ -635,6 +637,7 @@ impl ActionName {
"LaunchPlugin" => Some(Self::LaunchPlugin),
"CliPipe" => Some(Self::CliPipe),
"MoveTab" => Some(Self::MoveTab),
"KeybindPipe" => Some(Self::KeybindPipe),
_ => None,
}
}

View file

@ -29,6 +29,7 @@ pub struct Arg {
pub enum PipeSource {
Cli = 0,
Plugin = 1,
Keybind = 2,
}
impl PipeSource {
/// String value of the enum field names used in the ProtoBuf definition.
@ -39,6 +40,7 @@ impl PipeSource {
match self {
PipeSource::Cli => "Cli",
PipeSource::Plugin => "Plugin",
PipeSource::Keybind => "Keybind",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
@ -46,6 +48,7 @@ impl PipeSource {
match value {
"Cli" => Some(Self::Cli),
"Plugin" => Some(Self::Plugin),
"Keybind" => Some(Self::Keybind),
_ => None,
}
}

View file

@ -1165,6 +1165,7 @@ pub enum HttpVerb {
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
Keybind, // TODO: consider including the actual keybind here?
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]

View file

@ -401,6 +401,7 @@ pub enum PluginContext {
MessageFromPlugin,
UnblockCliPipes,
WatchFilesystem,
KeybindPipe,
}
/// Stack call representations corresponding to the different types of [`ClientInstruction`]s.

View file

@ -284,6 +284,19 @@ pub enum Action {
cwd: Option<PathBuf>,
pane_title: Option<String>,
},
KeybindPipe {
name: Option<String>,
payload: Option<String>,
args: Option<BTreeMap<String, String>>,
plugin: Option<String>,
configuration: Option<BTreeMap<String, String>>,
launch_new: bool,
skip_cache: bool,
floating: Option<bool>,
in_place: Option<bool>,
cwd: Option<PathBuf>,
pane_title: Option<String>,
},
}
impl Action {

View file

@ -15,6 +15,7 @@ use crate::input::theme::{FrameConfig, Theme, Themes, UiConfig};
use kdl_layout_parser::KdlLayoutParser;
use std::collections::{BTreeMap, HashMap, HashSet};
use strum::IntoEnumIterator;
use uuid::Uuid;
use miette::NamedSource;
@ -1078,6 +1079,64 @@ impl TryFrom<(&KdlNode, &Options)> for Action {
action_arguments,
kdl_action
),
"MessagePlugin" => {
let arguments = action_arguments.iter().copied();
let mut args = kdl_arguments_that_are_strings(arguments)?;
let plugin_path = if args.is_empty() {
None
} else {
Some(args.remove(0))
};
let command_metadata = action_children.iter().next();
let launch_new = command_metadata
.and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "launch_new"))
.unwrap_or(false);
let skip_cache = command_metadata
.and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "skip_cache"))
.unwrap_or(false);
let should_float = command_metadata
.and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "floating"))
.unwrap_or(false);
let name = command_metadata
.and_then(|c_m| kdl_child_string_value_for_entry(c_m, "name"))
.map(|n| n.to_owned());
let payload = command_metadata
.and_then(|c_m| kdl_child_string_value_for_entry(c_m, "payload"))
.map(|p| p.to_owned());
let title = command_metadata
.and_then(|c_m| kdl_child_string_value_for_entry(c_m, "title"))
.map(|t| t.to_owned());
let configuration = KdlLayoutParser::parse_plugin_user_configuration(&kdl_action)?;
let configuration = if configuration.inner().is_empty() {
None
} else {
Some(configuration.inner().clone())
};
let cwd = kdl_get_string_property_or_child_value!(kdl_action, "cwd")
.map(|s| PathBuf::from(s));
let name = name
// first we try to take the explicitly supplied message name
// then we use the plugin, to facilitate using aliases
.or_else(|| plugin_path.clone())
// then we use a uuid to at least have some sort of identifier for this message
.or_else(|| Some(Uuid::new_v4().to_string()));
Ok(Action::KeybindPipe {
name,
payload,
args: None, // TODO: consider supporting this if there's a need
plugin: plugin_path,
configuration,
launch_new,
skip_cache,
floating: Some(should_float),
in_place: None, // TODO: support this
cwd,
pane_title: title,
})
},
_ => Err(ConfigError::new_kdl_error(
format!("Unsupported action: {}", action_name).into(),
kdl_action.span().offset(),
@ -1854,7 +1913,7 @@ impl PluginAliases {
{
let configuration =
KdlLayoutParser::parse_plugin_user_configuration(&alias_definition)?;
let mut initial_cwd =
let initial_cwd =
kdl_get_string_property_or_child_value!(alias_definition, "cwd")
.map(|s| PathBuf::from(s));
let run_plugin = RunPlugin::from_url(string_url)?

View file

@ -243,6 +243,7 @@ enum ActionName {
LaunchPlugin = 81;
CliPipe = 82;
MoveTab = 83;
KeybindPipe = 84;
}
message Position {

View file

@ -689,6 +689,23 @@ impl TryFrom<ProtobufAction> for Action {
},
_ => Err("Wrong payload for Action::RenameSession"),
},
Some(ProtobufActionName::KeybindPipe) => match protobuf_action.optional_payload {
Some(_) => Err("KeybindPipe should not have a payload"),
// TODO: at some point we might want to support a payload here
None => Ok(Action::KeybindPipe {
name: None,
payload: None,
args: None,
plugin: None,
configuration: None,
launch_new: false,
skip_cache: false,
floating: None,
in_place: None,
cwd: None,
pane_title: None,
}),
},
_ => Err("Unknown Action"),
}
}
@ -1181,25 +1198,16 @@ impl TryFrom<Action> for ProtobufAction {
skip_plugin_cache,
_cwd,
_coordinates,
) => {
// let plugin_url: Url = match run_plugin {
// RunPluginOrAlias::RunPlugin(run_plugin) => Url::from(&run_plugin.location),
// RunPluginOrAlias::Alias(plugin_alias) => {
// // TODO: support plugin alias
// unimplemented!()
// }
// };
Ok(ProtobufAction {
name: ProtobufActionName::NewFloatingPluginPane as i32,
optional_payload: Some(OptionalPayload::NewFloatingPluginPanePayload(
NewPluginPanePayload {
plugin_url: run_plugin.location_string(),
pane_name,
skip_plugin_cache,
},
)),
})
},
) => Ok(ProtobufAction {
name: ProtobufActionName::NewFloatingPluginPane as i32,
optional_payload: Some(OptionalPayload::NewFloatingPluginPanePayload(
NewPluginPanePayload {
plugin_url: run_plugin.location_string(),
pane_name,
skip_plugin_cache,
},
)),
}),
Action::StartOrReloadPlugin(run_plugin) => Ok(ProtobufAction {
name: ProtobufActionName::StartOrReloadPlugin as i32,
optional_payload: Some(OptionalPayload::StartOrReloadPluginPayload(
@ -1273,6 +1281,10 @@ impl TryFrom<Action> for ProtobufAction {
name: ProtobufActionName::RenameSession as i32,
optional_payload: Some(OptionalPayload::RenameSessionPayload(session_name)),
}),
Action::KeybindPipe { .. } => Ok(ProtobufAction {
name: ProtobufActionName::KeybindPipe as i32,
optional_payload: None,
}),
Action::NoOp
| Action::Confirm
| Action::NewInPlacePane(..)

View file

@ -15,6 +15,7 @@ message PipeMessage {
enum PipeSource {
Cli = 0;
Plugin = 1;
Keybind = 2;
}
message Arg {

View file

@ -19,6 +19,7 @@ impl TryFrom<ProtobufPipeMessage> for PipeMessage {
(Some(ProtobufPipeSource::Plugin), _, Some(plugin_source_id)) => {
PipeSource::Plugin(plugin_source_id)
},
(Some(ProtobufPipeSource::Keybind), _, _) => PipeSource::Keybind,
_ => return Err("Invalid PipeSource or payload"),
};
let name = protobuf_pipe_message.name;
@ -49,6 +50,7 @@ impl TryFrom<PipeMessage> for ProtobufPipeMessage {
PipeSource::Plugin(plugin_id) => {
(ProtobufPipeSource::Plugin as i32, None, Some(plugin_id))
},
PipeSource::Keybind => (ProtobufPipeSource::Keybind as i32, None, None),
};
let name = pipe_message.name;
let payload = pipe_message.payload;