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
This commit is contained in:
Aram Drevekenin 2024-01-17 12:10:49 +01:00 committed by GitHub
parent f6d57295a0
commit d780bd9105
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 3071 additions and 305 deletions

View file

@ -11,6 +11,7 @@ struct State {
received_events: Vec<Event>, received_events: Vec<Event>,
received_payload: Option<String>, received_payload: Option<String>,
configuration: BTreeMap<String, String>, configuration: BTreeMap<String, String>,
message_to_plugin_payload: Option<String>,
} }
#[derive(Default, Serialize, Deserialize)] #[derive(Default, Serialize, Deserialize)]
@ -34,9 +35,12 @@ impl<'de> ZellijWorker<'de> for TestWorker {
} }
} }
#[cfg(target_family = "wasm")]
register_plugin!(State); register_plugin!(State);
#[cfg(target_family = "wasm")]
register_worker!(TestWorker, test_worker, TEST_WORKER); register_worker!(TestWorker, test_worker, TEST_WORKER);
#[cfg(target_family = "wasm")]
impl ZellijPlugin for State { impl ZellijPlugin for State {
fn load(&mut self, configuration: BTreeMap<String, String>) { fn load(&mut self, configuration: BTreeMap<String, String>) {
request_permission(&[ request_permission(&[
@ -49,6 +53,8 @@ impl ZellijPlugin for State {
PermissionType::OpenTerminalsOrPlugins, PermissionType::OpenTerminalsOrPlugins,
PermissionType::WriteToStdin, PermissionType::WriteToStdin,
PermissionType::WebAccess, PermissionType::WebAccess,
PermissionType::ReadCliPipes,
PermissionType::MessageAndLaunchOtherPlugins,
]); ]);
self.configuration = configuration; self.configuration = configuration;
subscribe(&[ subscribe(&[
@ -295,10 +301,35 @@ impl ZellijPlugin for State {
self.received_events.push(event); self.received_events.push(event);
should_render should_render
} }
fn pipe(&mut self, pipe_message: PipeMessage) -> bool {
let input_pipe_id = match pipe_message.source {
PipeSource::Cli(id) => id.clone(),
PipeSource::Plugin(id) => format!("{}", id),
};
let name = pipe_message.name;
let payload = pipe_message.payload;
if name == "message_name" && payload == Some("message_payload".to_owned()) {
unblock_cli_pipe_input(&input_pipe_id);
} else if name == "message_name_block" {
block_cli_pipe_input(&input_pipe_id);
} else if name == "pipe_output" {
cli_pipe_output(&name, "this_is_my_output");
} else if name == "pipe_message_to_plugin" {
pipe_message_to_plugin(
MessageToPlugin::new("message_to_plugin").with_payload("my_cool_payload"),
);
} else if name == "message_to_plugin" {
self.message_to_plugin_payload = payload.clone();
}
let should_render = true;
should_render
}
fn render(&mut self, rows: usize, cols: usize) { fn render(&mut self, rows: usize, cols: usize) {
if let Some(payload) = self.received_payload.as_ref() { if let Some(payload) = self.received_payload.as_ref() {
println!("Payload from worker: {:?}", payload); println!("Payload from worker: {:?}", payload);
} else if let Some(payload) = self.message_to_plugin_payload.take() {
println!("Payload from self: {:?}", payload);
} else { } else {
println!( println!(
"Rows: {:?}, Cols: {:?}, Received events: {:?}", "Rows: {:?}, Cols: {:?}, Received events: {:?}",

View file

@ -111,6 +111,31 @@ fn main() {
commands::convert_old_theme_file(old_theme_file); commands::convert_old_theme_file(old_theme_file);
std::process::exit(0); std::process::exit(0);
} }
if let Some(Command::Sessions(Sessions::Pipe {
name,
payload,
args,
plugin,
plugin_configuration,
})) = opts.command
{
let command_cli_action = CliAction::Pipe {
name,
payload,
args,
plugin,
plugin_configuration,
force_launch_plugin: false,
skip_plugin_cache: false,
floating_plugin: None,
in_place_plugin: None,
plugin_cwd: None,
plugin_title: None,
};
commands::send_action_to_session(command_cli_action, opts.session, config);
std::process::exit(0);
}
} }
if let Some(Command::Sessions(Sessions::ListSessions { if let Some(Command::Sessions(Sessions::ListSessions {

View file

@ -1,15 +1,23 @@
//! The `[cli_client]` is used to attach to a running server session //! The `[cli_client]` is used to attach to a running server session
//! and dispatch actions, that are specified through the command line. //! and dispatch actions, that are specified through the command line.
use std::collections::BTreeMap;
use std::io::BufRead;
use std::process; use std::process;
use std::{fs, path::PathBuf}; use std::{fs, path::PathBuf};
use crate::os_input_output::ClientOsApi; use crate::os_input_output::ClientOsApi;
use zellij_utils::{ use zellij_utils::{
errors::prelude::*,
input::actions::Action, input::actions::Action,
ipc::{ClientToServerMsg, ServerToClientMsg}, ipc::{ClientToServerMsg, ExitReason, ServerToClientMsg},
uuid::Uuid,
}; };
pub fn start_cli_client(os_input: Box<dyn ClientOsApi>, session_name: &str, actions: Vec<Action>) { pub fn start_cli_client(
mut os_input: Box<dyn ClientOsApi>,
session_name: &str,
actions: Vec<Action>,
) {
let zellij_ipc_pipe: PathBuf = { let zellij_ipc_pipe: PathBuf = {
let mut sock_dir = zellij_utils::consts::ZELLIJ_SOCK_DIR.clone(); let mut sock_dir = zellij_utils::consts::ZELLIJ_SOCK_DIR.clone();
fs::create_dir_all(&sock_dir).unwrap(); fs::create_dir_all(&sock_dir).unwrap();
@ -21,10 +29,166 @@ pub fn start_cli_client(os_input: Box<dyn ClientOsApi>, session_name: &str, acti
let pane_id = os_input let pane_id = os_input
.env_variable("ZELLIJ_PANE_ID") .env_variable("ZELLIJ_PANE_ID")
.and_then(|e| e.trim().parse().ok()); .and_then(|e| e.trim().parse().ok());
for action in actions { for action in actions {
let msg = ClientToServerMsg::Action(action, pane_id, None); match action {
Action::CliPipe {
pipe_id,
name,
payload,
plugin,
args,
configuration,
launch_new,
skip_cache,
floating,
in_place,
cwd,
pane_title,
} => {
pipe_client(
&mut os_input,
pipe_id,
name,
payload,
plugin,
args,
configuration,
launch_new,
skip_cache,
floating,
in_place,
pane_id,
cwd,
pane_title,
);
},
action => {
single_message_client(&mut os_input, action, pane_id);
},
}
}
}
fn pipe_client(
os_input: &mut Box<dyn ClientOsApi>,
pipe_id: String,
mut name: Option<String>,
payload: Option<String>,
plugin: Option<String>,
args: Option<BTreeMap<String, String>>,
mut configuration: Option<BTreeMap<String, String>>,
launch_new: bool,
skip_cache: bool,
floating: Option<bool>,
in_place: Option<bool>,
pane_id: Option<u32>,
cwd: Option<PathBuf>,
pane_title: Option<String>,
) {
let mut stdin = os_input.get_stdin_reader();
let name = name.take().or_else(|| Some(Uuid::new_v4().to_string()));
if launch_new {
// we do this to make sure the plugin is unique (has a unique configuration parameter) so
// that a new one would be launched, but we'll still send it to the same instance rather
// than launching a new one in every iteration of the loop
configuration
.get_or_insert_with(BTreeMap::new)
.insert("_zellij_id".to_owned(), Uuid::new_v4().to_string());
}
let create_msg = |payload: Option<String>| -> ClientToServerMsg {
ClientToServerMsg::Action(
Action::CliPipe {
pipe_id: pipe_id.clone(),
name: name.clone(),
payload,
args: args.clone(),
plugin: plugin.clone(),
configuration: configuration.clone(),
floating,
in_place,
launch_new,
skip_cache,
cwd: cwd.clone(),
pane_title: pane_title.clone(),
},
pane_id,
None,
)
};
loop {
if payload.is_some() {
// we got payload from the command line, we should use it and not wait for more
let msg = create_msg(payload);
os_input.send_to_server(msg);
break;
}
// we didn't get payload from the command line, meaning we listen on STDIN because this
// signifies the user is about to pipe more (eg. cat my-large-file | zellij pipe ...)
let mut buffer = String::new();
let _ = stdin.read_line(&mut buffer);
if buffer.is_empty() {
// end of pipe, send an empty message down the pipe
let msg = create_msg(None);
os_input.send_to_server(msg);
break;
} else {
// we've got data! send it down the pipe (most common)
let msg = create_msg(Some(buffer));
os_input.send_to_server(msg); os_input.send_to_server(msg);
} }
loop {
// wait for a response and act accordingly
match os_input.recv_from_server() {
Some((ServerToClientMsg::UnblockCliPipeInput(pipe_name), _)) => {
// unblock this pipe, meaning we need to stop waiting for a response and read
// once more from STDIN
if pipe_name == pipe_id {
break;
}
},
Some((ServerToClientMsg::CliPipeOutput(pipe_name, output), _)) => {
// send data to STDOUT, this *does not* mean we need to unblock the input
let err_context = "Failed to write to stdout";
if pipe_name == pipe_id {
let mut stdout = os_input.get_stdout_writer();
stdout
.write_all(output.as_bytes())
.context(err_context)
.non_fatal();
stdout.flush().context(err_context).non_fatal();
}
},
Some((ServerToClientMsg::Log(log_lines), _)) => {
log_lines.iter().for_each(|line| println!("{line}"));
process::exit(0);
},
Some((ServerToClientMsg::LogError(log_lines), _)) => {
log_lines.iter().for_each(|line| eprintln!("{line}"));
process::exit(2);
},
Some((ServerToClientMsg::Exit(exit_reason), _)) => match exit_reason {
ExitReason::Error(e) => {
eprintln!("{}", e);
process::exit(2);
},
_ => {
process::exit(0);
},
},
_ => {},
}
}
}
}
fn single_message_client(
os_input: &mut Box<dyn ClientOsApi>,
action: Action,
pane_id: Option<u32>,
) {
let msg = ClientToServerMsg::Action(action, pane_id, None);
os_input.send_to_server(msg);
loop { loop {
match os_input.recv_from_server() { match os_input.recv_from_server() {
Some((ServerToClientMsg::UnblockInputThread, _)) => { Some((ServerToClientMsg::UnblockInputThread, _)) => {
@ -39,6 +203,15 @@ pub fn start_cli_client(os_input: Box<dyn ClientOsApi>, session_name: &str, acti
log_lines.iter().for_each(|line| eprintln!("{line}")); log_lines.iter().for_each(|line| eprintln!("{line}"));
process::exit(2); process::exit(2);
}, },
Some((ServerToClientMsg::Exit(exit_reason), _)) => match exit_reason {
ExitReason::Error(e) => {
eprintln!("{}", e);
process::exit(2);
},
_ => {
process::exit(0);
},
},
_ => {}, _ => {},
} }
} }

View file

@ -49,6 +49,8 @@ pub(crate) enum ClientInstruction {
LogError(Vec<String>), LogError(Vec<String>),
SwitchSession(ConnectToSession), SwitchSession(ConnectToSession),
SetSynchronizedOutput(Option<SyncOutput>), SetSynchronizedOutput(Option<SyncOutput>),
UnblockCliPipeInput(String), // String -> pipe name
CliPipeOutput(String, String), // String -> pipe name, String -> output
} }
impl From<ServerToClientMsg> for ClientInstruction { impl From<ServerToClientMsg> for ClientInstruction {
@ -67,6 +69,12 @@ impl From<ServerToClientMsg> for ClientInstruction {
ServerToClientMsg::SwitchSession(connect_to_session) => { ServerToClientMsg::SwitchSession(connect_to_session) => {
ClientInstruction::SwitchSession(connect_to_session) ClientInstruction::SwitchSession(connect_to_session)
}, },
ServerToClientMsg::UnblockCliPipeInput(pipe_name) => {
ClientInstruction::UnblockCliPipeInput(pipe_name)
},
ServerToClientMsg::CliPipeOutput(pipe_name, output) => {
ClientInstruction::CliPipeOutput(pipe_name, output)
},
} }
} }
} }
@ -87,6 +95,8 @@ impl From<&ClientInstruction> for ClientContext {
ClientInstruction::DoneParsingStdinQuery => ClientContext::DoneParsingStdinQuery, ClientInstruction::DoneParsingStdinQuery => ClientContext::DoneParsingStdinQuery,
ClientInstruction::SwitchSession(..) => ClientContext::SwitchSession, ClientInstruction::SwitchSession(..) => ClientContext::SwitchSession,
ClientInstruction::SetSynchronizedOutput(..) => ClientContext::SetSynchronisedOutput, ClientInstruction::SetSynchronizedOutput(..) => ClientContext::SetSynchronisedOutput,
ClientInstruction::UnblockCliPipeInput(..) => ClientContext::UnblockCliPipeInput,
ClientInstruction::CliPipeOutput(..) => ClientContext::CliPipeOutput,
} }
} }
} }

View file

@ -95,7 +95,8 @@ pub trait ClientOsApi: Send + Sync {
fn unset_raw_mode(&self, fd: RawFd) -> Result<(), nix::Error>; fn unset_raw_mode(&self, fd: RawFd) -> Result<(), nix::Error>;
/// Returns the writer that allows writing to standard output. /// Returns the writer that allows writing to standard output.
fn get_stdout_writer(&self) -> Box<dyn io::Write>; fn get_stdout_writer(&self) -> Box<dyn io::Write>;
fn get_stdin_reader(&self) -> Box<dyn io::Read>; /// Returns a BufReader that allows to read from STDIN line by line, also locks STDIN
fn get_stdin_reader(&self) -> Box<dyn io::BufRead>;
fn update_session_name(&mut self, new_session_name: String); fn update_session_name(&mut self, new_session_name: String);
/// Returns the raw contents of standard input. /// Returns the raw contents of standard input.
fn read_from_stdin(&mut self) -> Result<Vec<u8>, &'static str>; fn read_from_stdin(&mut self) -> Result<Vec<u8>, &'static str>;
@ -186,9 +187,10 @@ impl ClientOsApi for ClientOsInputOutput {
let stdout = ::std::io::stdout(); let stdout = ::std::io::stdout();
Box::new(stdout) Box::new(stdout)
} }
fn get_stdin_reader(&self) -> Box<dyn io::Read> {
fn get_stdin_reader(&self) -> Box<dyn io::BufRead> {
let stdin = ::std::io::stdin(); let stdin = ::std::io::stdin();
Box::new(stdin) Box::new(stdin.lock())
} }
fn send_to_server(&self, msg: ClientToServerMsg) { fn send_to_server(&self, msg: ClientToServerMsg) {

View file

@ -151,10 +151,10 @@ impl ClientOsApi for FakeClientOsApi {
let fake_stdout_writer = FakeStdoutWriter::new(self.stdout_buffer.clone()); let fake_stdout_writer = FakeStdoutWriter::new(self.stdout_buffer.clone());
Box::new(fake_stdout_writer) Box::new(fake_stdout_writer)
} }
fn get_stdin_reader(&self) -> Box<dyn io::Read> { fn get_stdin_reader(&self) -> Box<dyn io::BufRead> {
unimplemented!() unimplemented!()
} }
fn update_session_name(&mut self, new_session_name: String) {} fn update_session_name(&mut self, _new_session_name: String) {}
fn read_from_stdin(&mut self) -> Result<Vec<u8>, &'static str> { fn read_from_stdin(&mut self) -> Result<Vec<u8>, &'static str> {
Ok(self.stdin_buffer.drain(..).collect()) Ok(self.stdin_buffer.drain(..).collect())
} }

View file

@ -85,14 +85,21 @@ pub enum ServerInstruction {
ConnStatus(ClientId), ConnStatus(ClientId),
ActiveClients(ClientId), ActiveClients(ClientId),
Log(Vec<String>, ClientId), Log(Vec<String>, ClientId),
LogError(Vec<String>, ClientId),
SwitchSession(ConnectToSession, ClientId), SwitchSession(ConnectToSession, ClientId),
UnblockCliPipeInput(String), // String -> Pipe name
CliPipeOutput(String, String), // String -> Pipe name, String -> Output
AssociatePipeWithClient {
pipe_id: String,
client_id: ClientId,
},
} }
impl From<&ServerInstruction> for ServerContext { impl From<&ServerInstruction> for ServerContext {
fn from(server_instruction: &ServerInstruction) -> Self { fn from(server_instruction: &ServerInstruction) -> Self {
match *server_instruction { match *server_instruction {
ServerInstruction::NewClient(..) => ServerContext::NewClient, ServerInstruction::NewClient(..) => ServerContext::NewClient,
ServerInstruction::Render(_) => ServerContext::Render, ServerInstruction::Render(..) => ServerContext::Render,
ServerInstruction::UnblockInputThread => ServerContext::UnblockInputThread, ServerInstruction::UnblockInputThread => ServerContext::UnblockInputThread,
ServerInstruction::ClientExit(..) => ServerContext::ClientExit, ServerInstruction::ClientExit(..) => ServerContext::ClientExit,
ServerInstruction::RemoveClient(..) => ServerContext::RemoveClient, ServerInstruction::RemoveClient(..) => ServerContext::RemoveClient,
@ -103,7 +110,13 @@ impl From<&ServerInstruction> for ServerContext {
ServerInstruction::ConnStatus(..) => ServerContext::ConnStatus, ServerInstruction::ConnStatus(..) => ServerContext::ConnStatus,
ServerInstruction::ActiveClients(_) => ServerContext::ActiveClients, ServerInstruction::ActiveClients(_) => ServerContext::ActiveClients,
ServerInstruction::Log(..) => ServerContext::Log, ServerInstruction::Log(..) => ServerContext::Log,
ServerInstruction::LogError(..) => ServerContext::LogError,
ServerInstruction::SwitchSession(..) => ServerContext::SwitchSession, ServerInstruction::SwitchSession(..) => ServerContext::SwitchSession,
ServerInstruction::UnblockCliPipeInput(..) => ServerContext::UnblockCliPipeInput,
ServerInstruction::CliPipeOutput(..) => ServerContext::CliPipeOutput,
ServerInstruction::AssociatePipeWithClient { .. } => {
ServerContext::AssociatePipeWithClient
},
} }
} }
} }
@ -186,12 +199,14 @@ macro_rules! send_to_client {
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub(crate) struct SessionState { pub(crate) struct SessionState {
clients: HashMap<ClientId, Option<Size>>, clients: HashMap<ClientId, Option<Size>>,
pipes: HashMap<String, ClientId>, // String => pipe_id
} }
impl SessionState { impl SessionState {
pub fn new() -> Self { pub fn new() -> Self {
SessionState { SessionState {
clients: HashMap::new(), clients: HashMap::new(),
pipes: HashMap::new(),
} }
} }
pub fn new_client(&mut self) -> ClientId { pub fn new_client(&mut self) -> ClientId {
@ -207,8 +222,12 @@ impl SessionState {
self.clients.insert(next_client_id, None); self.clients.insert(next_client_id, None);
next_client_id next_client_id
} }
pub fn associate_pipe_with_client(&mut self, pipe_id: String, client_id: ClientId) {
self.pipes.insert(pipe_id, client_id);
}
pub fn remove_client(&mut self, client_id: ClientId) { pub fn remove_client(&mut self, client_id: ClientId) {
self.clients.remove(&client_id); self.clients.remove(&client_id);
self.pipes.retain(|_p_id, c_id| c_id != &client_id);
} }
pub fn set_client_size(&mut self, client_id: ClientId, size: Size) { pub fn set_client_size(&mut self, client_id: ClientId, size: Size) {
self.clients.insert(client_id, Some(size)); self.clients.insert(client_id, Some(size));
@ -240,6 +259,17 @@ impl SessionState {
pub fn client_ids(&self) -> Vec<ClientId> { pub fn client_ids(&self) -> Vec<ClientId> {
self.clients.keys().copied().collect() self.clients.keys().copied().collect()
} }
pub fn active_clients_are_connected(&self) -> bool {
let ids_of_pipe_clients: HashSet<ClientId> = self.pipes.values().copied().collect();
let mut active_clients_connected = false;
for client_id in self.clients.keys() {
if ids_of_pipe_clients.contains(client_id) {
continue;
}
active_clients_connected = true;
}
active_clients_connected
}
} }
pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) { pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
@ -490,6 +520,52 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
); );
} }
}, },
ServerInstruction::UnblockCliPipeInput(pipe_name) => {
match session_state.read().unwrap().pipes.get(&pipe_name) {
Some(client_id) => {
send_to_client!(
*client_id,
os_input,
ServerToClientMsg::UnblockCliPipeInput(pipe_name.clone()),
session_state
);
},
None => {
// send to all clients, this pipe might not have been associated yet
for client_id in session_state.read().unwrap().clients.keys() {
send_to_client!(
*client_id,
os_input,
ServerToClientMsg::UnblockCliPipeInput(pipe_name.clone()),
session_state
);
}
},
}
},
ServerInstruction::CliPipeOutput(pipe_name, output) => {
match session_state.read().unwrap().pipes.get(&pipe_name) {
Some(client_id) => {
send_to_client!(
*client_id,
os_input,
ServerToClientMsg::CliPipeOutput(pipe_name.clone(), output.clone()),
session_state
);
},
None => {
// send to all clients, this pipe might not have been associated yet
for client_id in session_state.read().unwrap().clients.keys() {
send_to_client!(
*client_id,
os_input,
ServerToClientMsg::CliPipeOutput(pipe_name.clone(), output.clone()),
session_state
);
}
},
}
},
ServerInstruction::ClientExit(client_id) => { ServerInstruction::ClientExit(client_id) => {
let _ = let _ =
os_input.send_to_client(client_id, ServerToClientMsg::Exit(ExitReason::Normal)); os_input.send_to_client(client_id, ServerToClientMsg::Exit(ExitReason::Normal));
@ -520,8 +596,19 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
.senders .senders
.send_to_plugin(PluginInstruction::RemoveClient(client_id)) .send_to_plugin(PluginInstruction::RemoveClient(client_id))
.unwrap(); .unwrap();
if session_state.read().unwrap().clients.is_empty() { if !session_state.read().unwrap().active_clients_are_connected() {
*session_data.write().unwrap() = None; *session_data.write().unwrap() = None;
let client_ids_to_cleanup: Vec<ClientId> = session_state
.read()
.unwrap()
.clients
.keys()
.copied()
.collect();
// these are just the pipes
for client_id in client_ids_to_cleanup {
remove_client!(client_id, os_input, session_state);
}
break; break;
} }
}, },
@ -654,6 +741,14 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
session_state session_state
); );
}, },
ServerInstruction::LogError(lines_to_log, client_id) => {
send_to_client!(
client_id,
os_input,
ServerToClientMsg::LogError(lines_to_log),
session_state
);
},
ServerInstruction::SwitchSession(connect_to_session, client_id) => { ServerInstruction::SwitchSession(connect_to_session, client_id) => {
if let Some(min_size) = session_state.read().unwrap().min_client_terminal_size() { if let Some(min_size) = session_state.read().unwrap().min_client_terminal_size() {
session_data session_data
@ -689,6 +784,12 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
); );
remove_client!(client_id, os_input, session_state); remove_client!(client_id, os_input, session_state);
}, },
ServerInstruction::AssociatePipeWithClient { pipe_id, client_id } => {
session_state
.write()
.unwrap()
.associate_pipe_with_client(pipe_id, client_id);
},
} }
} }
@ -825,7 +926,7 @@ fn init_session(
.spawn({ .spawn({
let plugin_bus = Bus::new( let plugin_bus = Bus::new(
vec![plugin_receiver], vec![plugin_receiver],
Some(&to_screen), Some(&to_screen_bounded),
Some(&to_pty), Some(&to_pty),
Some(&to_plugin), Some(&to_plugin),
Some(&to_server), Some(&to_server),

View file

@ -16,7 +16,7 @@ use std::time::{self, Instant};
use zellij_utils::input::command::RunCommand; use zellij_utils::input::command::RunCommand;
use zellij_utils::pane_size::Offset; use zellij_utils::pane_size::Offset;
use zellij_utils::{ use zellij_utils::{
data::{InputMode, Palette, PaletteColor, Style}, data::{InputMode, Palette, PaletteColor, PaneId as ZellijUtilsPaneId, Style},
errors::prelude::*, errors::prelude::*,
input::layout::Run, input::layout::Run,
pane_size::PaneGeom, pane_size::PaneGeom,
@ -85,6 +85,16 @@ pub enum PaneId {
Plugin(u32), // FIXME: Drop the trait object, make this a wrapper for the struct? Plugin(u32), // FIXME: Drop the trait object, make this a wrapper for the struct?
} }
// because crate architecture and reasons...
impl From<ZellijUtilsPaneId> for PaneId {
fn from(zellij_utils_pane_id: ZellijUtilsPaneId) -> Self {
match zellij_utils_pane_id {
ZellijUtilsPaneId::Terminal(id) => PaneId::Terminal(id),
ZellijUtilsPaneId::Plugin(id) => PaneId::Plugin(id),
}
}
}
type IsFirstRun = bool; type IsFirstRun = bool;
// FIXME: This should hold an os_api handle so that terminal panes can set their own size via FD in // FIXME: This should hold an os_api handle so that terminal panes can set their own size via FD in

View file

@ -1,3 +1,4 @@
mod pipes;
mod plugin_loader; mod plugin_loader;
mod plugin_map; mod plugin_map;
mod plugin_worker; mod plugin_worker;
@ -6,7 +7,7 @@ mod watch_filesystem;
mod zellij_exports; mod zellij_exports;
use log::info; use log::info;
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{BTreeMap, HashMap, HashSet},
fs, fs,
path::PathBuf, path::PathBuf,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
@ -19,11 +20,15 @@ use crate::screen::ScreenInstruction;
use crate::session_layout_metadata::SessionLayoutMetadata; use crate::session_layout_metadata::SessionLayoutMetadata;
use crate::{pty::PtyInstruction, thread_bus::Bus, ClientId, ServerInstruction}; use crate::{pty::PtyInstruction, thread_bus::Bus, ClientId, ServerInstruction};
pub use wasm_bridge::PluginRenderAsset;
use wasm_bridge::WasmBridge; use wasm_bridge::WasmBridge;
use zellij_utils::{ use zellij_utils::{
async_std::{channel, future::timeout, task}, async_std::{channel, future::timeout, task},
data::{Event, EventType, PermissionStatus, PermissionType, PluginCapabilities}, data::{
Event, EventType, MessageToPlugin, PermissionStatus, PermissionType, PipeMessage,
PipeSource, PluginCapabilities,
},
errors::{prelude::*, ContextType, PluginContext}, errors::{prelude::*, ContextType, PluginContext},
input::{ input::{
command::TerminalAction, command::TerminalAction,
@ -73,7 +78,10 @@ pub enum PluginInstruction {
usize, // tab_index usize, // tab_index
ClientId, ClientId,
), ),
ApplyCachedEvents(Vec<PluginId>), ApplyCachedEvents {
plugin_ids: Vec<PluginId>,
done_receiving_permissions: bool,
},
ApplyCachedWorkerMessages(PluginId), ApplyCachedWorkerMessages(PluginId),
PostMessagesToPluginWorker( PostMessagesToPluginWorker(
PluginId, PluginId,
@ -100,6 +108,28 @@ pub enum PluginInstruction {
), ),
DumpLayout(SessionLayoutMetadata, ClientId), DumpLayout(SessionLayoutMetadata, ClientId),
LogLayoutToHd(SessionLayoutMetadata), LogLayoutToHd(SessionLayoutMetadata),
CliPipe {
pipe_id: String,
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,
},
MessageFromPlugin {
source_plugin_id: u32,
message: MessageToPlugin,
},
UnblockCliPipes(Vec<PluginRenderAsset>),
Exit, Exit,
} }
@ -115,7 +145,7 @@ impl From<&PluginInstruction> for PluginContext {
PluginInstruction::AddClient(_) => PluginContext::AddClient, PluginInstruction::AddClient(_) => PluginContext::AddClient,
PluginInstruction::RemoveClient(_) => PluginContext::RemoveClient, PluginInstruction::RemoveClient(_) => PluginContext::RemoveClient,
PluginInstruction::NewTab(..) => PluginContext::NewTab, PluginInstruction::NewTab(..) => PluginContext::NewTab,
PluginInstruction::ApplyCachedEvents(..) => PluginContext::ApplyCachedEvents, PluginInstruction::ApplyCachedEvents { .. } => PluginContext::ApplyCachedEvents,
PluginInstruction::ApplyCachedWorkerMessages(..) => { PluginInstruction::ApplyCachedWorkerMessages(..) => {
PluginContext::ApplyCachedWorkerMessages PluginContext::ApplyCachedWorkerMessages
}, },
@ -131,6 +161,10 @@ impl From<&PluginInstruction> for PluginContext {
}, },
PluginInstruction::DumpLayout(..) => PluginContext::DumpLayout, PluginInstruction::DumpLayout(..) => PluginContext::DumpLayout,
PluginInstruction::LogLayoutToHd(..) => PluginContext::LogLayoutToHd, PluginInstruction::LogLayoutToHd(..) => PluginContext::LogLayoutToHd,
PluginInstruction::CliPipe { .. } => PluginContext::CliPipe,
PluginInstruction::CachePluginEvents { .. } => PluginContext::CachePluginEvents,
PluginInstruction::MessageFromPlugin { .. } => PluginContext::MessageFromPlugin,
PluginInstruction::UnblockCliPipes { .. } => PluginContext::UnblockCliPipes,
} }
} }
} }
@ -188,19 +222,20 @@ pub(crate) fn plugin_thread_main(
skip_cache, skip_cache,
) => match wasm_bridge.load_plugin( ) => match wasm_bridge.load_plugin(
&run, &run,
tab_index, Some(tab_index),
size, size,
cwd.clone(), cwd.clone(),
skip_cache, skip_cache,
Some(client_id), Some(client_id),
None,
) { ) {
Ok(plugin_id) => { Ok((plugin_id, client_id)) => {
drop(bus.senders.send_to_screen(ScreenInstruction::AddPlugin( drop(bus.senders.send_to_screen(ScreenInstruction::AddPlugin(
should_float, should_float,
should_be_open_in_place, should_be_open_in_place,
run, run,
pane_title, pane_title,
tab_index, Some(tab_index),
plugin_id, plugin_id,
pane_id_to_replace, pane_id_to_replace,
cwd, cwd,
@ -230,17 +265,23 @@ pub(crate) fn plugin_thread_main(
// we intentionally do not provide the client_id here because it belongs to // we intentionally do not provide the client_id here because it belongs to
// the cli who spawned the command and is not an existing client_id // the cli who spawned the command and is not an existing client_id
let skip_cache = true; // when reloading we always skip cache let skip_cache = true; // when reloading we always skip cache
match wasm_bridge match wasm_bridge.load_plugin(
.load_plugin(&run, tab_index, size, None, skip_cache, None) &run,
{ Some(tab_index),
Ok(plugin_id) => { size,
None,
skip_cache,
None,
None,
) {
Ok((plugin_id, _client_id)) => {
let should_be_open_in_place = false; let should_be_open_in_place = false;
drop(bus.senders.send_to_screen(ScreenInstruction::AddPlugin( drop(bus.senders.send_to_screen(ScreenInstruction::AddPlugin(
should_float, should_float,
should_be_open_in_place, should_be_open_in_place,
run, run,
pane_title, pane_title,
tab_index, Some(tab_index),
plugin_id, plugin_id,
None, None,
None, None,
@ -298,13 +339,14 @@ pub(crate) fn plugin_thread_main(
for run_instruction in extracted_run_instructions { for run_instruction in extracted_run_instructions {
if let Some(Run::Plugin(run)) = run_instruction { if let Some(Run::Plugin(run)) = run_instruction {
let skip_cache = false; let skip_cache = false;
let plugin_id = wasm_bridge.load_plugin( let (plugin_id, _client_id) = wasm_bridge.load_plugin(
&run, &run,
tab_index, Some(tab_index),
size, size,
None, None,
skip_cache, skip_cache,
Some(client_id), Some(client_id),
None,
)?; )?;
plugin_ids plugin_ids
.entry((run.location, run.configuration)) .entry((run.location, run.configuration))
@ -322,8 +364,15 @@ pub(crate) fn plugin_thread_main(
client_id, client_id,
))); )));
}, },
PluginInstruction::ApplyCachedEvents(plugin_id) => { PluginInstruction::ApplyCachedEvents {
wasm_bridge.apply_cached_events(plugin_id, shutdown_send.clone())?; plugin_ids,
done_receiving_permissions,
} => {
wasm_bridge.apply_cached_events(
plugin_ids,
done_receiving_permissions,
shutdown_send.clone(),
)?;
}, },
PluginInstruction::ApplyCachedWorkerMessages(plugin_id) => { PluginInstruction::ApplyCachedWorkerMessages(plugin_id) => {
wasm_bridge.apply_cached_worker_messages(plugin_id)?; wasm_bridge.apply_cached_worker_messages(plugin_id)?;
@ -383,6 +432,12 @@ pub(crate) fn plugin_thread_main(
Event::PermissionRequestResult(status), Event::PermissionRequestResult(status),
)]; )];
wasm_bridge.update_plugins(updates, shutdown_send.clone())?; wasm_bridge.update_plugins(updates, shutdown_send.clone())?;
let done_receiving_permissions = true;
wasm_bridge.apply_cached_events(
vec![plugin_id],
done_receiving_permissions,
shutdown_send.clone(),
)?;
}, },
PluginInstruction::DumpLayout(mut session_layout_metadata, client_id) => { PluginInstruction::DumpLayout(mut session_layout_metadata, client_id) => {
populate_session_layout_metadata(&mut session_layout_metadata, &wasm_bridge); populate_session_layout_metadata(&mut session_layout_metadata, &wasm_bridge);
@ -398,6 +453,128 @@ pub(crate) fn plugin_thread_main(
.send_to_pty(PtyInstruction::LogLayoutToHd(session_layout_metadata)), .send_to_pty(PtyInstruction::LogLayoutToHd(session_layout_metadata)),
); );
}, },
PluginInstruction::CliPipe {
pipe_id,
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::Cli(pipe_id.clone()),
&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,
);
},
None => {
// no specific destination, send to all plugins
pipe_to_all_plugins(
PipeSource::Cli(pipe_id.clone()),
&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);
},
PluginInstruction::MessageFromPlugin {
source_plugin_id,
message,
} => {
let cwd = message.new_plugin_args.as_ref().and_then(|n| n.cwd.clone());
let mut pipe_messages = vec![];
let skip_cache = message
.new_plugin_args
.as_ref()
.map(|n| n.skip_cache)
.unwrap_or(false);
let should_float = message
.new_plugin_args
.as_ref()
.and_then(|n| n.should_float)
.unwrap_or(true);
let pane_title = message
.new_plugin_args
.as_ref()
.and_then(|n| n.pane_title.clone());
let pane_id_to_replace = message
.new_plugin_args
.as_ref()
.and_then(|n| n.pane_id_to_replace);
match message.plugin_url {
Some(plugin_url) => {
// send to specific plugin(s)
pipe_to_specific_plugins(
PipeSource::Plugin(source_plugin_id),
&plugin_url,
&Some(message.plugin_config),
&cwd,
skip_cache,
should_float,
&pane_id_to_replace.map(|p| p.into()),
&pane_title,
None,
&mut pipe_messages,
&message.message_name,
&message.message_payload,
&Some(message.message_args),
&bus,
&mut wasm_bridge,
);
},
None => {
// send to all plugins
pipe_to_all_plugins(
PipeSource::Plugin(source_plugin_id),
&message.message_name,
&message.message_payload,
&Some(message.message_args),
&mut wasm_bridge,
&mut pipe_messages,
);
},
}
wasm_bridge.pipe_messages(pipe_messages, shutdown_send.clone())?;
},
PluginInstruction::UnblockCliPipes(pipes_to_unblock) => {
let pipes_to_unblock = wasm_bridge.update_cli_pipe_state(pipes_to_unblock);
for pipe_name in pipes_to_unblock {
let _ = bus
.senders
.send_to_server(ServerInstruction::UnblockCliPipeInput(pipe_name))
.context("failed to unblock input pipe");
}
},
PluginInstruction::Exit => { PluginInstruction::Exit => {
break; break;
}, },
@ -448,6 +625,82 @@ fn populate_session_layout_metadata(
session_layout_metadata.update_plugin_cmds(plugin_ids_to_cmds); session_layout_metadata.update_plugin_cmds(plugin_ids_to_cmds);
} }
fn pipe_to_all_plugins(
pipe_source: PipeSource,
name: &str,
payload: &Option<String>,
args: &Option<BTreeMap<String, String>>,
wasm_bridge: &mut WasmBridge,
pipe_messages: &mut Vec<(Option<PluginId>, Option<ClientId>, PipeMessage)>,
) {
let is_private = false;
let all_plugin_ids = wasm_bridge.all_plugin_ids();
for (plugin_id, client_id) in all_plugin_ids {
pipe_messages.push((
Some(plugin_id),
Some(client_id),
PipeMessage::new(pipe_source.clone(), name, payload, &args, is_private),
));
}
}
fn pipe_to_specific_plugins(
pipe_source: PipeSource,
plugin_url: &str,
configuration: &Option<BTreeMap<String, String>>,
cwd: &Option<PathBuf>,
skip_cache: bool,
should_float: bool,
pane_id_to_replace: &Option<PaneId>,
pane_title: &Option<String>,
cli_client_id: Option<ClientId>,
pipe_messages: &mut Vec<(Option<PluginId>, Option<ClientId>, PipeMessage)>,
name: &str,
payload: &Option<String>,
args: &Option<BTreeMap<String, String>>,
bus: &Bus<PluginInstruction>,
wasm_bridge: &mut WasmBridge,
) {
let is_private = true;
let size = Size::default();
match RunPlugin::from_url(&plugin_url) {
Ok(mut run_plugin) => {
if let Some(configuration) = configuration {
run_plugin.configuration = PluginUserConfiguration::new(configuration.clone());
}
let all_plugin_ids = wasm_bridge.get_or_load_plugins(
run_plugin,
size,
cwd.clone(),
skip_cache,
should_float,
pane_id_to_replace.is_some(),
pane_title.clone(),
pane_id_to_replace.clone(),
cli_client_id,
);
for (plugin_id, client_id) in all_plugin_ids {
pipe_messages.push((
Some(plugin_id),
client_id,
PipeMessage::new(pipe_source.clone(), name, payload, args, is_private), // PipeMessage::new(PipeSource::Cli(pipe_id.clone()), &name, &payload, &args, is_private)
));
}
},
Err(e) => match cli_client_id {
Some(cli_client_id) => {
let _ = bus.senders.send_to_server(ServerInstruction::LogError(
vec![format!("Failed to parse plugin url: {}", e)],
cli_client_id,
));
},
None => {
log::error!("Failed to parse plugin url: {}", e);
},
},
}
}
const EXIT_TIMEOUT: Duration = Duration::from_secs(3); const EXIT_TIMEOUT: Duration = Duration::from_secs(3);
#[path = "./unit/plugin_tests.rs"] #[path = "./unit/plugin_tests.rs"]

View file

@ -0,0 +1,257 @@
use super::{PluginId, PluginInstruction};
use crate::plugins::plugin_map::RunningPlugin;
use crate::plugins::wasm_bridge::PluginRenderAsset;
use crate::plugins::zellij_exports::{wasi_read_string, wasi_write_object};
use std::collections::{HashMap, HashSet};
use wasmer::Value;
use zellij_utils::data::{PipeMessage, PipeSource};
use zellij_utils::plugin_api::pipe_message::ProtobufPipeMessage;
use zellij_utils::errors::prelude::*;
use zellij_utils::prost::Message;
use crate::{thread_bus::ThreadSenders, ClientId};
#[derive(Debug, Clone)]
pub enum PipeStateChange {
NoChange,
Block,
Unblock,
}
#[derive(Debug, Clone, Default)]
pub struct PendingPipes {
pipes: HashMap<String, PendingPipeInfo>,
}
impl PendingPipes {
pub fn mark_being_processed(
&mut self,
pipe_id: &str,
plugin_id: &PluginId,
client_id: &ClientId,
) {
if self.pipes.contains_key(pipe_id) {
self.pipes.get_mut(pipe_id).map(|pending_pipe_info| {
pending_pipe_info.add_processing_plugin(plugin_id, client_id)
});
} else {
self.pipes.insert(
pipe_id.to_owned(),
PendingPipeInfo::new(plugin_id, client_id),
);
}
}
// returns a list of pipes that are no longer pending and should be unblocked
pub fn update_pipe_state_change(
&mut self,
cli_pipe_name: &str,
pipe_state_change: PipeStateChange,
plugin_id: &PluginId,
client_id: &ClientId,
) -> Vec<String> {
let mut pipe_names_to_unblock = vec![];
match self.pipes.get_mut(cli_pipe_name) {
Some(pending_pipe_info) => {
let should_unblock_this_pipe =
pending_pipe_info.update_state_change(pipe_state_change, plugin_id, client_id);
if should_unblock_this_pipe {
pipe_names_to_unblock.push(cli_pipe_name.to_owned());
}
},
None => {
// state somehow corrupted, let's recover...
pipe_names_to_unblock.push(cli_pipe_name.to_owned());
},
}
for pipe_name in &pipe_names_to_unblock {
self.pipes.remove(pipe_name);
}
pipe_names_to_unblock
}
// returns a list of pipes that are no longer pending and should be unblocked
pub fn unload_plugin(&mut self, plugin_id: &PluginId) -> Vec<String> {
let mut pipe_names_to_unblock = vec![];
for (pipe_name, pending_pipe_info) in self.pipes.iter_mut() {
let should_unblock_this_pipe = pending_pipe_info.unload_plugin(plugin_id);
if should_unblock_this_pipe {
pipe_names_to_unblock.push(pipe_name.to_owned());
}
}
for pipe_name in &pipe_names_to_unblock {
self.pipes.remove(pipe_name);
}
pipe_names_to_unblock
}
}
#[derive(Debug, Clone, Default)]
pub struct PendingPipeInfo {
is_explicitly_blocked: bool,
currently_being_processed_by: HashSet<(PluginId, ClientId)>,
}
impl PendingPipeInfo {
pub fn new(plugin_id: &PluginId, client_id: &ClientId) -> Self {
let mut currently_being_processed_by = HashSet::new();
currently_being_processed_by.insert((*plugin_id, *client_id));
PendingPipeInfo {
currently_being_processed_by,
..Default::default()
}
}
pub fn add_processing_plugin(&mut self, plugin_id: &PluginId, client_id: &ClientId) {
self.currently_being_processed_by
.insert((*plugin_id, *client_id));
}
// returns true if this pipe should be unblocked
pub fn update_state_change(
&mut self,
pipe_state_change: PipeStateChange,
plugin_id: &PluginId,
client_id: &ClientId,
) -> bool {
match pipe_state_change {
PipeStateChange::Block => {
self.is_explicitly_blocked = true;
},
PipeStateChange::Unblock => {
self.is_explicitly_blocked = false;
},
_ => {},
};
self.currently_being_processed_by
.remove(&(*plugin_id, *client_id));
let pipe_should_be_unblocked =
self.currently_being_processed_by.is_empty() && !self.is_explicitly_blocked;
pipe_should_be_unblocked
}
// returns true if this pipe should be unblocked
pub fn unload_plugin(&mut self, plugin_id_to_unload: &PluginId) -> bool {
self.currently_being_processed_by
.retain(|(plugin_id, _)| plugin_id != plugin_id_to_unload);
if self.currently_being_processed_by.is_empty() && !self.is_explicitly_blocked {
true
} else {
false
}
}
}
pub fn apply_pipe_message_to_plugin(
plugin_id: PluginId,
client_id: ClientId,
running_plugin: &mut RunningPlugin,
pipe_message: &PipeMessage,
plugin_render_assets: &mut Vec<PluginRenderAsset>,
senders: &ThreadSenders,
) -> Result<()> {
let instance = &running_plugin.instance;
let plugin_env = &running_plugin.plugin_env;
let rows = running_plugin.rows;
let columns = running_plugin.columns;
let err_context = || format!("Failed to apply event to plugin {plugin_id}");
let protobuf_pipe_message: ProtobufPipeMessage = pipe_message
.clone()
.try_into()
.map_err(|e| anyhow!("Failed to convert to protobuf: {:?}", e))?;
match instance.exports.get_function("pipe") {
Ok(pipe) => {
wasi_write_object(&plugin_env.wasi_env, &protobuf_pipe_message.encode_to_vec())
.with_context(err_context)?;
let pipe_return = pipe
.call(&mut running_plugin.store, &[])
.with_context(err_context)?;
let should_render = match pipe_return.get(0) {
Some(Value::I32(n)) => *n == 1,
_ => false,
};
if rows > 0 && columns > 0 && should_render {
let rendered_bytes = instance
.exports
.get_function("render")
.map_err(anyError::new)
.and_then(|render| {
render
.call(
&mut running_plugin.store,
&[Value::I32(rows as i32), Value::I32(columns as i32)],
)
.map_err(anyError::new)
})
.and_then(|_| wasi_read_string(&plugin_env.wasi_env))
.with_context(err_context)?;
let pipes_to_block_or_unblock =
pipes_to_block_or_unblock(running_plugin, Some(&pipe_message.source));
let plugin_render_asset = PluginRenderAsset::new(
plugin_id,
client_id,
rendered_bytes.as_bytes().to_vec(),
)
.with_pipes(pipes_to_block_or_unblock);
plugin_render_assets.push(plugin_render_asset);
} else {
let pipes_to_block_or_unblock =
pipes_to_block_or_unblock(running_plugin, Some(&pipe_message.source));
let plugin_render_asset = PluginRenderAsset::new(plugin_id, client_id, vec![])
.with_pipes(pipes_to_block_or_unblock);
let _ = senders
.send_to_plugin(PluginInstruction::UnblockCliPipes(vec![
plugin_render_asset,
]))
.context("failed to unblock input pipe");
}
},
Err(_e) => {
// no-op, this is probably an old plugin that does not have this interface
// we don't log this error because if we do the logs will be super crowded
let pipes_to_block_or_unblock =
pipes_to_block_or_unblock(running_plugin, Some(&pipe_message.source));
let plugin_render_asset = PluginRenderAsset::new(
plugin_id,
client_id,
vec![], // nothing to render
)
.with_pipes(pipes_to_block_or_unblock);
let _ = senders
.send_to_plugin(PluginInstruction::UnblockCliPipes(vec![
plugin_render_asset,
]))
.context("failed to unblock input pipe");
},
}
Ok(())
}
pub fn pipes_to_block_or_unblock(
running_plugin: &mut RunningPlugin,
current_pipe: Option<&PipeSource>,
) -> HashMap<String, PipeStateChange> {
let mut pipe_state_changes = HashMap::new();
let mut input_pipes_to_unblock: HashSet<String> = running_plugin
.plugin_env
.input_pipes_to_unblock
.lock()
.unwrap()
.drain()
.collect();
let mut input_pipes_to_block: HashSet<String> = running_plugin
.plugin_env
.input_pipes_to_block
.lock()
.unwrap()
.drain()
.collect();
if let Some(PipeSource::Cli(current_pipe)) = current_pipe {
pipe_state_changes.insert(current_pipe.to_owned(), PipeStateChange::NoChange);
}
for pipe in input_pipes_to_block.drain() {
pipe_state_changes.insert(pipe, PipeStateChange::Block);
}
for pipe in input_pipes_to_unblock.drain() {
// unblock has priority over block if they happened simultaneously
pipe_state_changes.insert(pipe, PipeStateChange::Unblock);
}
pipe_state_changes
}

View file

@ -56,7 +56,7 @@ pub struct PluginLoader<'a> {
store: Arc<Mutex<Store>>, store: Arc<Mutex<Store>>,
plugin: PluginConfig, plugin: PluginConfig,
plugin_dir: &'a PathBuf, plugin_dir: &'a PathBuf,
tab_index: usize, tab_index: Option<usize>,
plugin_own_data_dir: PathBuf, plugin_own_data_dir: PathBuf,
size: Size, size: Size,
wasm_blob_on_hd: Option<(Vec<u8>, PathBuf)>, wasm_blob_on_hd: Option<(Vec<u8>, PathBuf)>,
@ -133,7 +133,7 @@ impl<'a> PluginLoader<'a> {
plugin_id: PluginId, plugin_id: PluginId,
client_id: ClientId, client_id: ClientId,
plugin: &PluginConfig, plugin: &PluginConfig,
tab_index: usize, tab_index: Option<usize>,
plugin_dir: PathBuf, plugin_dir: PathBuf,
plugin_cache: Arc<Mutex<HashMap<PathBuf, Module>>>, plugin_cache: Arc<Mutex<HashMap<PathBuf, Module>>>,
senders: ThreadSenders, senders: ThreadSenders,
@ -339,7 +339,7 @@ impl<'a> PluginLoader<'a> {
store: Arc<Mutex<Store>>, store: Arc<Mutex<Store>>,
plugin: PluginConfig, plugin: PluginConfig,
plugin_dir: &'a PathBuf, plugin_dir: &'a PathBuf,
tab_index: usize, tab_index: Option<usize>,
size: Size, size: Size,
path_to_default_shell: PathBuf, path_to_default_shell: PathBuf,
zellij_cwd: PathBuf, zellij_cwd: PathBuf,
@ -814,7 +814,9 @@ impl<'a> PluginLoader<'a> {
.import_object(store_mut, &module) .import_object(store_mut, &module)
.with_context(err_context)?; .with_context(err_context)?;
let mut mut_plugin = self.plugin.clone(); let mut mut_plugin = self.plugin.clone();
mut_plugin.set_tab_index(self.tab_index); if let Some(tab_index) = self.tab_index {
mut_plugin.set_tab_index(tab_index);
}
let plugin_env = PluginEnv { let plugin_env = PluginEnv {
plugin_id: self.plugin_id, plugin_id: self.plugin_id,
client_id: self.client_id, client_id: self.client_id,
@ -830,6 +832,8 @@ impl<'a> PluginLoader<'a> {
default_shell: self.default_shell.clone(), default_shell: self.default_shell.clone(),
default_layout: self.default_layout.clone(), default_layout: self.default_layout.clone(),
plugin_cwd: self.zellij_cwd.clone(), plugin_cwd: self.zellij_cwd.clone(),
input_pipes_to_unblock: Arc::new(Mutex::new(HashSet::new())),
input_pipes_to_block: Arc::new(Mutex::new(HashSet::new())),
}; };
let subscriptions = Arc::new(Mutex::new(HashSet::new())); let subscriptions = Arc::new(Mutex::new(HashSet::new()));

View file

@ -15,7 +15,7 @@ use zellij_utils::{
data::EventType, data::EventType,
data::PluginCapabilities, data::PluginCapabilities,
input::command::TerminalAction, input::command::TerminalAction,
input::layout::{Layout, RunPlugin, RunPluginLocation}, input::layout::{Layout, PluginUserConfiguration, RunPlugin, RunPluginLocation},
input::plugins::PluginConfig, input::plugins::PluginConfig,
ipc::ClientAttributes, ipc::ClientAttributes,
}; };
@ -157,13 +157,19 @@ impl PluginMap {
pub fn all_plugin_ids_for_plugin_location( pub fn all_plugin_ids_for_plugin_location(
&self, &self,
plugin_location: &RunPluginLocation, plugin_location: &RunPluginLocation,
plugin_configuration: &PluginUserConfiguration,
) -> Result<Vec<PluginId>> { ) -> Result<Vec<PluginId>> {
let err_context = || format!("Failed to get plugin ids for location {plugin_location}"); let err_context = || format!("Failed to get plugin ids for location {plugin_location}");
let plugin_ids: Vec<PluginId> = self let plugin_ids: Vec<PluginId> = self
.plugin_assets .plugin_assets
.iter() .iter()
.filter(|(_, (running_plugin, _subscriptions, _workers))| { .filter(|(_, (running_plugin, _subscriptions, _workers))| {
&running_plugin.lock().unwrap().plugin_env.plugin.location == plugin_location let running_plugin = running_plugin.lock().unwrap();
let running_plugin_location = &running_plugin.plugin_env.plugin.location;
let running_plugin_configuration =
&running_plugin.plugin_env.plugin.userspace_configuration;
running_plugin_location == plugin_location
&& running_plugin_configuration == plugin_configuration
}) })
.map(|((plugin_id, _client_id), _)| *plugin_id) .map(|((plugin_id, _client_id), _)| *plugin_id)
.collect(); .collect();
@ -172,6 +178,49 @@ impl PluginMap {
} }
Ok(plugin_ids) Ok(plugin_ids)
} }
pub fn clone_plugin_assets(
&self,
) -> HashMap<RunPluginLocation, HashMap<PluginUserConfiguration, Vec<(PluginId, ClientId)>>>
{
let mut cloned_plugin_assets: HashMap<
RunPluginLocation,
HashMap<PluginUserConfiguration, Vec<(PluginId, ClientId)>>,
> = HashMap::new();
for ((plugin_id, client_id), (running_plugin, _, _)) in self.plugin_assets.iter() {
let running_plugin = running_plugin.lock().unwrap();
let running_plugin_location = &running_plugin.plugin_env.plugin.location;
let running_plugin_configuration =
&running_plugin.plugin_env.plugin.userspace_configuration;
match cloned_plugin_assets.get_mut(running_plugin_location) {
Some(location_map) => match location_map.get_mut(running_plugin_configuration) {
Some(plugin_instances_info) => {
plugin_instances_info.push((*plugin_id, *client_id));
},
None => {
location_map.insert(
running_plugin_configuration.clone(),
vec![(*plugin_id, *client_id)],
);
},
},
None => {
let mut location_map = HashMap::new();
location_map.insert(
running_plugin_configuration.clone(),
vec![(*plugin_id, *client_id)],
);
cloned_plugin_assets.insert(running_plugin_location.clone(), location_map);
},
}
}
cloned_plugin_assets
}
pub fn all_plugin_ids(&self) -> Vec<(PluginId, ClientId)> {
self.plugin_assets
.iter()
.map(|((plugin_id, client_id), _)| (*plugin_id, *client_id))
.collect()
}
pub fn insert( pub fn insert(
&mut self, &mut self,
plugin_id: PluginId, plugin_id: PluginId,
@ -218,7 +267,7 @@ pub struct PluginEnv {
pub permissions: Arc<Mutex<Option<HashSet<PermissionType>>>>, pub permissions: Arc<Mutex<Option<HashSet<PermissionType>>>>,
pub senders: ThreadSenders, pub senders: ThreadSenders,
pub wasi_env: WasiEnv, pub wasi_env: WasiEnv,
pub tab_index: usize, pub tab_index: Option<usize>,
pub client_id: ClientId, pub client_id: ClientId,
#[allow(dead_code)] #[allow(dead_code)]
pub plugin_own_data_dir: PathBuf, pub plugin_own_data_dir: PathBuf,
@ -228,6 +277,8 @@ pub struct PluginEnv {
pub default_shell: Option<TerminalAction>, pub default_shell: Option<TerminalAction>,
pub default_layout: Box<Layout>, pub default_layout: Box<Layout>,
pub plugin_cwd: PathBuf, pub plugin_cwd: PathBuf,
pub input_pipes_to_unblock: Arc<Mutex<HashSet<String>>>,
pub input_pipes_to_block: Arc<Mutex<HashSet<String>>>,
} }
impl PluginEnv { impl PluginEnv {

View file

@ -204,35 +204,6 @@ macro_rules! grant_permissions_and_log_actions_in_thread_naked_variant {
}; };
} }
macro_rules! log_actions_in_thread_naked_variant {
( $arc_mutex_log:expr, $exit_event:path, $receiver:expr, $exit_after_count:expr ) => {
std::thread::Builder::new()
.name("logger thread".to_string())
.spawn({
let log = $arc_mutex_log.clone();
let mut exit_event_count = 0;
move || loop {
let (event, _err_ctx) = $receiver
.recv()
.expect("failed to receive event on channel");
match event {
$exit_event => {
exit_event_count += 1;
log.lock().unwrap().push(event);
if exit_event_count == $exit_after_count {
break;
}
},
_ => {
log.lock().unwrap().push(event);
},
}
}
})
.unwrap()
};
}
fn create_plugin_thread( fn create_plugin_thread(
zellij_cwd: Option<PathBuf>, zellij_cwd: Option<PathBuf>,
) -> ( ) -> (
@ -372,7 +343,7 @@ fn create_plugin_thread_with_server_receiver(
client_attributes, client_attributes,
default_shell_action, default_shell_action,
) )
.expect("TEST") .expect("TEST");
}) })
.unwrap(); .unwrap();
let teardown = { let teardown = {
@ -599,7 +570,7 @@ pub fn load_new_plugin_from_hd() {
received_screen_instructions, received_screen_instructions,
ScreenInstruction::PluginBytes, ScreenInstruction::PluginBytes,
screen_receiver, screen_receiver,
2, 1,
&PermissionType::ChangeApplicationState, &PermissionType::ChangeApplicationState,
cache_path, cache_path,
plugin_thread_sender, plugin_thread_sender,
@ -632,11 +603,14 @@ pub fn load_new_plugin_from_hd() {
.unwrap() .unwrap()
.iter() .iter()
.find_map(|i| { .find_map(|i| {
if let ScreenInstruction::PluginBytes(plugin_bytes) = i { if let ScreenInstruction::PluginBytes(plugin_render_assets) = i {
for (plugin_id, client_id, plugin_bytes) in plugin_bytes { for plugin_render_asset in plugin_render_assets {
let plugin_bytes = String::from_utf8_lossy(plugin_bytes).to_string(); let plugin_id = plugin_render_asset.plugin_id;
let client_id = plugin_render_asset.client_id;
let plugin_bytes = plugin_render_asset.bytes.clone();
let plugin_bytes = String::from_utf8_lossy(plugin_bytes.as_slice()).to_string();
if plugin_bytes.contains("InputReceived") { if plugin_bytes.contains("InputReceived") {
return Some((*plugin_id, *client_id, plugin_bytes)); return Some((plugin_id, client_id, plugin_bytes));
} }
} }
} }
@ -671,7 +645,7 @@ pub fn plugin_workers() {
received_screen_instructions, received_screen_instructions,
ScreenInstruction::PluginBytes, ScreenInstruction::PluginBytes,
screen_receiver, screen_receiver,
3, 2,
&PermissionType::ChangeApplicationState, &PermissionType::ChangeApplicationState,
cache_path, cache_path,
plugin_thread_sender, plugin_thread_sender,
@ -708,11 +682,14 @@ pub fn plugin_workers() {
.unwrap() .unwrap()
.iter() .iter()
.find_map(|i| { .find_map(|i| {
if let ScreenInstruction::PluginBytes(plugin_bytes) = i { if let ScreenInstruction::PluginBytes(plugin_render_assets) = i {
for (plugin_id, client_id, plugin_bytes) in plugin_bytes { for plugin_render_asset in plugin_render_assets {
let plugin_bytes = String::from_utf8_lossy(plugin_bytes).to_string(); let plugin_id = plugin_render_asset.plugin_id;
let client_id = plugin_render_asset.client_id;
let plugin_bytes = plugin_render_asset.bytes.clone();
let plugin_bytes = String::from_utf8_lossy(plugin_bytes.as_slice()).to_string();
if plugin_bytes.contains("Payload from worker") { if plugin_bytes.contains("Payload from worker") {
return Some((*plugin_id, *client_id, plugin_bytes)); return Some((plugin_id, client_id, plugin_bytes));
} }
} }
} }
@ -747,7 +724,7 @@ pub fn plugin_workers_persist_state() {
received_screen_instructions, received_screen_instructions,
ScreenInstruction::PluginBytes, ScreenInstruction::PluginBytes,
screen_receiver, screen_receiver,
5, 4,
&PermissionType::ChangeApplicationState, &PermissionType::ChangeApplicationState,
cache_path, cache_path,
plugin_thread_sender, plugin_thread_sender,
@ -774,12 +751,13 @@ pub fn plugin_workers_persist_state() {
// we do this a second time so that the worker will log the first message on its own state and // we do this a second time so that the worker will log the first message on its own state and
// then send us the "received 2 messages" indication we check for below, letting us know it // then send us the "received 2 messages" indication we check for below, letting us know it
// managed to persist its own state and act upon it // managed to persist its own state and act upon it
std::thread::sleep(std::time::Duration::from_millis(500)); //std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
Event::SystemClipboardFailure, Event::SystemClipboardFailure,
)])); )]));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -792,11 +770,14 @@ pub fn plugin_workers_persist_state() {
.unwrap() .unwrap()
.iter() .iter()
.find_map(|i| { .find_map(|i| {
if let ScreenInstruction::PluginBytes(plugin_bytes) = i { if let ScreenInstruction::PluginBytes(plugin_render_assets) = i {
for (plugin_id, client_id, plugin_bytes) in plugin_bytes { for plugin_render_asset in plugin_render_assets {
let plugin_bytes = String::from_utf8_lossy(plugin_bytes).to_string(); let plugin_bytes = plugin_render_asset.bytes.clone();
let plugin_id = plugin_render_asset.plugin_id;
let client_id = plugin_render_asset.client_id;
let plugin_bytes = String::from_utf8_lossy(plugin_bytes.as_slice()).to_string();
if plugin_bytes.contains("received 2 messages") { if plugin_bytes.contains("received 2 messages") {
return Some((*plugin_id, *client_id, plugin_bytes)); return Some((plugin_id, client_id, plugin_bytes));
} }
} }
} }
@ -811,6 +792,7 @@ pub fn can_subscribe_to_hd_events() {
let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
// destructor removes the directory // destructor removes the directory
let plugin_host_folder = PathBuf::from(temp_folder.path()); let plugin_host_folder = PathBuf::from(temp_folder.path());
let cache_path = plugin_host_folder.join("permissions_test.kdl");
let (plugin_thread_sender, screen_receiver, teardown) = let (plugin_thread_sender, screen_receiver, teardown) =
create_plugin_thread(Some(plugin_host_folder)); create_plugin_thread(Some(plugin_host_folder));
let plugin_should_float = Some(false); let plugin_should_float = Some(false);
@ -827,11 +809,15 @@ pub fn can_subscribe_to_hd_events() {
rows: 20, rows: 20,
}; };
let received_screen_instructions = Arc::new(Mutex::new(vec![])); let received_screen_instructions = Arc::new(Mutex::new(vec![]));
let screen_thread = log_actions_in_thread!( let screen_thread = grant_permissions_and_log_actions_in_thread!(
received_screen_instructions, received_screen_instructions,
ScreenInstruction::PluginBytes, ScreenInstruction::PluginBytes,
screen_receiver, screen_receiver,
2 2,
&PermissionType::ChangeApplicationState,
cache_path,
plugin_thread_sender,
client_id
); );
let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@ -861,11 +847,14 @@ pub fn can_subscribe_to_hd_events() {
.unwrap() .unwrap()
.iter() .iter()
.find_map(|i| { .find_map(|i| {
if let ScreenInstruction::PluginBytes(plugin_bytes) = i { if let ScreenInstruction::PluginBytes(plugin_render_assets) = i {
for (plugin_id, client_id, plugin_bytes) in plugin_bytes { for plugin_render_asset in plugin_render_assets {
let plugin_bytes = String::from_utf8_lossy(plugin_bytes).to_string(); let plugin_id = plugin_render_asset.plugin_id;
let client_id = plugin_render_asset.client_id;
let plugin_bytes = plugin_render_asset.bytes.clone();
let plugin_bytes = String::from_utf8_lossy(plugin_bytes.as_slice()).to_string();
if plugin_bytes.contains("FileSystemCreate") { if plugin_bytes.contains("FileSystemCreate") {
return Some((*plugin_id, *client_id, plugin_bytes)); return Some((plugin_id, client_id, plugin_bytes));
} }
} }
} }
@ -4471,6 +4460,7 @@ pub fn hide_self_plugin_command() {
let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
// destructor removes the directory // destructor removes the directory
let plugin_host_folder = PathBuf::from(temp_folder.path()); let plugin_host_folder = PathBuf::from(temp_folder.path());
let cache_path = plugin_host_folder.join("permissions_test.kdl");
let (plugin_thread_sender, screen_receiver, teardown) = let (plugin_thread_sender, screen_receiver, teardown) =
create_plugin_thread(Some(plugin_host_folder)); create_plugin_thread(Some(plugin_host_folder));
let plugin_should_float = Some(false); let plugin_should_float = Some(false);
@ -4487,11 +4477,15 @@ pub fn hide_self_plugin_command() {
rows: 20, rows: 20,
}; };
let received_screen_instructions = Arc::new(Mutex::new(vec![])); let received_screen_instructions = Arc::new(Mutex::new(vec![]));
let screen_thread = log_actions_in_thread!( let screen_thread = grant_permissions_and_log_actions_in_thread!(
received_screen_instructions, received_screen_instructions,
ScreenInstruction::SuppressPane, ScreenInstruction::SuppressPane,
screen_receiver, screen_receiver,
1 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::AddClient(client_id));
@ -4536,6 +4530,7 @@ pub fn show_self_plugin_command() {
let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
// destructor removes the directory // destructor removes the directory
let plugin_host_folder = PathBuf::from(temp_folder.path()); let plugin_host_folder = PathBuf::from(temp_folder.path());
let cache_path = plugin_host_folder.join("permissions_test.kdl");
let (plugin_thread_sender, screen_receiver, teardown) = let (plugin_thread_sender, screen_receiver, teardown) =
create_plugin_thread(Some(plugin_host_folder)); create_plugin_thread(Some(plugin_host_folder));
let plugin_should_float = Some(false); let plugin_should_float = Some(false);
@ -4552,13 +4547,16 @@ pub fn show_self_plugin_command() {
rows: 20, rows: 20,
}; };
let received_screen_instructions = Arc::new(Mutex::new(vec![])); let received_screen_instructions = Arc::new(Mutex::new(vec![]));
let screen_thread = log_actions_in_thread!( let screen_thread = grant_permissions_and_log_actions_in_thread!(
received_screen_instructions, received_screen_instructions,
ScreenInstruction::FocusPaneWithId, ScreenInstruction::FocusPaneWithId,
screen_receiver, screen_receiver,
1 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::AddClient(client_id));
let _ = plugin_thread_sender.send(PluginInstruction::Load( let _ = plugin_thread_sender.send(PluginInstruction::Load(
plugin_should_float, plugin_should_float,
@ -5634,3 +5632,341 @@ pub fn web_request_plugin_command() {
.clone(); .clone();
assert_snapshot!(format!("{:#?}", new_tab_event)); assert_snapshot!(format!("{:#?}", new_tab_event));
} }
#[test]
#[ignore]
pub fn unblock_input_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, screen_receiver, teardown) =
create_plugin_thread(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_screen_instructions = Arc::new(Mutex::new(vec![]));
let screen_thread = grant_permissions_and_log_actions_in_thread!(
received_screen_instructions,
ScreenInstruction::PluginBytes,
screen_receiver,
1,
&PermissionType::ReadCliPipes,
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,
None,
false,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::CliPipe {
pipe_id: "input_pipe_id".to_owned(),
name: "message_name".to_owned(),
payload: Some("message_payload".to_owned()),
plugin: None, // broadcast
args: None,
configuration: None,
floating: None,
pane_id_to_replace: None,
pane_title: None,
cwd: None,
skip_cache: false,
cli_client_id: client_id,
});
screen_thread.join().unwrap(); // this might take a while if the cache is cold
teardown();
let plugin_bytes_events = received_screen_instructions
.lock()
.unwrap()
.iter()
.rev()
.find_map(|i| {
if let ScreenInstruction::PluginBytes(..) = i {
Some(i.clone())
} else {
None
}
})
.clone();
assert_snapshot!(format!("{:#?}", plugin_bytes_events));
}
#[test]
#[ignore]
pub fn block_input_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, screen_receiver, teardown) =
create_plugin_thread(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_screen_instructions = Arc::new(Mutex::new(vec![]));
let screen_thread = grant_permissions_and_log_actions_in_thread!(
received_screen_instructions,
ScreenInstruction::PluginBytes,
screen_receiver,
1,
&PermissionType::ReadCliPipes,
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,
None,
false,
));
// extra long time because we only start the fs watcher on plugin load
std::thread::sleep(std::time::Duration::from_millis(5000));
let _ = plugin_thread_sender.send(PluginInstruction::CliPipe {
pipe_id: "input_pipe_id".to_owned(),
name: "message_name_block".to_owned(),
payload: Some("message_payload".to_owned()),
plugin: None, // broadcast
args: None,
configuration: None,
floating: None,
pane_id_to_replace: None,
pane_title: None,
cwd: None,
skip_cache: false,
cli_client_id: client_id,
});
screen_thread.join().unwrap(); // this might take a while if the cache is cold
teardown();
let plugin_bytes_events = received_screen_instructions
.lock()
.unwrap()
.iter()
.rev()
.find_map(|i| {
if let ScreenInstruction::PluginBytes(..) = i {
Some(i.clone())
} else {
None
}
})
.clone();
assert_snapshot!(format!("{:#?}", plugin_bytes_events));
}
#[test]
#[ignore]
pub fn pipe_output_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, server_receiver, screen_receiver, teardown) =
create_plugin_thread_with_server_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_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 received_server_instruction = Arc::new(Mutex::new(vec![]));
let server_thread = log_actions_in_thread!(
received_server_instruction,
ServerInstruction::CliPipeOutput,
server_receiver,
1
);
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,
None,
false,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::CliPipe {
pipe_id: "input_pipe_id".to_owned(),
name: "pipe_output".to_owned(),
payload: Some("message_payload".to_owned()),
plugin: None, // broadcast
args: None,
configuration: None,
floating: None,
pane_id_to_replace: None,
pane_title: None,
cwd: None,
skip_cache: false,
cli_client_id: client_id,
});
std::thread::sleep(std::time::Duration::from_millis(500));
teardown();
server_thread.join().unwrap(); // this might take a while if the cache is cold
let plugin_bytes_events = received_server_instruction
.lock()
.unwrap()
.iter()
.rev()
.find_map(|i| {
if let ServerInstruction::CliPipeOutput(..) = i {
Some(i.clone())
} else {
None
}
})
.clone();
assert_snapshot!(format!("{:#?}", plugin_bytes_events));
}
#[test]
#[ignore]
pub fn pipe_message_to_plugin_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, screen_receiver, teardown) =
create_plugin_thread(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_screen_instructions = Arc::new(Mutex::new(vec![]));
let screen_thread = grant_permissions_and_log_actions_in_thread!(
received_screen_instructions,
ScreenInstruction::PluginBytes,
screen_receiver,
2,
&PermissionType::ReadCliPipes,
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,
None,
false,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::CliPipe {
pipe_id: "input_pipe_id".to_owned(),
name: "pipe_message_to_plugin".to_owned(),
payload: Some("payload_sent_to_self".to_owned()),
plugin: None, // broadcast
args: None,
configuration: None,
floating: None,
pane_id_to_replace: None,
pane_title: None,
cwd: None,
skip_cache: false,
cli_client_id: client_id,
});
std::thread::sleep(std::time::Duration::from_millis(500));
teardown();
screen_thread.join().unwrap(); // this might take a while if the cache is cold
let plugin_bytes_event = received_screen_instructions
.lock()
.unwrap()
.iter()
.find_map(|i| {
if let ScreenInstruction::PluginBytes(plugin_render_assets) = i {
for plugin_render_asset in plugin_render_assets {
let plugin_id = plugin_render_asset.plugin_id;
let client_id = plugin_render_asset.client_id;
let plugin_bytes = plugin_render_asset.bytes.clone();
let plugin_bytes = String::from_utf8_lossy(plugin_bytes.as_slice()).to_string();
if plugin_bytes.contains("Payload from self:") {
return Some((plugin_id, client_id, plugin_bytes));
}
}
}
None
});
assert_snapshot!(format!("{:#?}", plugin_bytes_event));
}

View file

@ -0,0 +1,62 @@
---
source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 5812
expression: "format!(\"{:#?}\", plugin_bytes_events)"
---
Some(
PluginBytes(
[
PluginRenderAsset {
client_id: 1,
plugin_id: 0,
bytes: [
82,
111,
119,
115,
58,
32,
50,
48,
44,
32,
67,
111,
108,
115,
58,
32,
49,
50,
49,
44,
32,
82,
101,
99,
101,
105,
118,
101,
100,
32,
101,
118,
101,
110,
116,
115,
58,
32,
91,
93,
10,
13,
],
cli_pipes: {
"input_pipe_id": Block,
},
},
],
),
)

View file

@ -1,6 +1,6 @@
--- ---
source: zellij-server/src/plugins/./unit/plugin_tests.rs source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 5189 assertion_line: 5307
expression: "format!(\"{:#?}\", permissions)" expression: "format!(\"{:#?}\", permissions)"
--- ---
Some( Some(
@ -12,5 +12,7 @@ Some(
OpenTerminalsOrPlugins, OpenTerminalsOrPlugins,
WriteToStdin, WriteToStdin,
WebAccess, WebAccess,
ReadCliPipes,
MessageAndLaunchOtherPlugins,
], ],
) )

View file

@ -0,0 +1,12 @@
---
source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 5961
expression: "format!(\"{:#?}\", plugin_bytes_event)"
---
Some(
(
0,
1,
"Payload from self: \"my_cool_payload\"\n\r",
),
)

View file

@ -0,0 +1,11 @@
---
source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 5771
expression: "format!(\"{:#?}\", plugin_bytes_events)"
---
Some(
CliPipeOutput(
"pipe_output",
"this_is_my_output",
),
)

View file

@ -1,6 +1,6 @@
--- ---
source: zellij-server/src/plugins/./unit/plugin_tests.rs source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 5101 assertion_line: 5217
expression: "format!(\"{:#?}\", new_tab_event)" expression: "format!(\"{:#?}\", new_tab_event)"
--- ---
Some( Some(
@ -14,5 +14,7 @@ Some(
OpenTerminalsOrPlugins, OpenTerminalsOrPlugins,
WriteToStdin, WriteToStdin,
WebAccess, WebAccess,
ReadCliPipes,
MessageAndLaunchOtherPlugins,
], ],
) )

View file

@ -0,0 +1,12 @@
---
source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 5856
expression: "format!(\"{:#?}\", plugin_bytes_event)"
---
Some(
(
0,
1,
"Payload from self: \"my_cool_payload\"\n\r",
),
)

View file

@ -0,0 +1,62 @@
---
source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 5730
expression: "format!(\"{:#?}\", plugin_bytes_events)"
---
Some(
PluginBytes(
[
PluginRenderAsset {
client_id: 1,
plugin_id: 0,
bytes: [
82,
111,
119,
115,
58,
32,
50,
48,
44,
32,
67,
111,
108,
115,
58,
32,
49,
50,
49,
44,
32,
82,
101,
99,
101,
105,
118,
101,
100,
32,
101,
118,
101,
110,
116,
115,
58,
32,
91,
93,
10,
13,
],
cli_pipes: {
"input_pipe_id": Unblock,
},
},
],
),
)

View file

@ -1,4 +1,7 @@
use super::{PluginId, PluginInstruction}; use super::{PluginId, PluginInstruction};
use crate::plugins::pipes::{
apply_pipe_message_to_plugin, pipes_to_block_or_unblock, PendingPipes, PipeStateChange,
};
use crate::plugins::plugin_loader::PluginLoader; use crate::plugins::plugin_loader::PluginLoader;
use crate::plugins::plugin_map::{AtomicEvent, PluginEnv, PluginMap, RunningPlugin, Subscriptions}; use crate::plugins::plugin_map::{AtomicEvent, PluginEnv, PluginMap, RunningPlugin, Subscriptions};
use crate::plugins::plugin_worker::MessageToWorker; use crate::plugins::plugin_worker::MessageToWorker;
@ -16,7 +19,7 @@ use wasmer::{Module, Store, Value};
use zellij_utils::async_channel::Sender; use zellij_utils::async_channel::Sender;
use zellij_utils::async_std::task::{self, JoinHandle}; use zellij_utils::async_std::task::{self, JoinHandle};
use zellij_utils::consts::ZELLIJ_CACHE_DIR; use zellij_utils::consts::ZELLIJ_CACHE_DIR;
use zellij_utils::data::{PermissionStatus, PermissionType}; use zellij_utils::data::{PermissionStatus, PermissionType, PipeMessage, PipeSource};
use zellij_utils::downloader::Downloader; use zellij_utils::downloader::Downloader;
use zellij_utils::input::permission::PermissionCache; use zellij_utils::input::permission::PermissionCache;
use zellij_utils::notify_debouncer_full::{notify::RecommendedWatcher, Debouncer, FileIdMap}; use zellij_utils::notify_debouncer_full::{notify::RecommendedWatcher, Debouncer, FileIdMap};
@ -24,22 +27,53 @@ use zellij_utils::plugin_api::event::ProtobufEvent;
use zellij_utils::prost::Message; use zellij_utils::prost::Message;
use crate::panes::PaneId;
use crate::{ use crate::{
background_jobs::BackgroundJob, screen::ScreenInstruction, thread_bus::ThreadSenders, background_jobs::BackgroundJob, screen::ScreenInstruction, thread_bus::ThreadSenders,
ui::loading_indication::LoadingIndication, ClientId, ui::loading_indication::LoadingIndication, ClientId, ServerInstruction,
}; };
use zellij_utils::{ use zellij_utils::{
data::{Event, EventType, PluginCapabilities}, data::{Event, EventType, PluginCapabilities},
errors::prelude::*, errors::prelude::*,
input::{ input::{
command::TerminalAction, command::TerminalAction,
layout::{Layout, RunPlugin, RunPluginLocation}, layout::{Layout, PluginUserConfiguration, RunPlugin, RunPluginLocation},
plugins::PluginsConfig, plugins::PluginsConfig,
}, },
ipc::ClientAttributes, ipc::ClientAttributes,
pane_size::Size, pane_size::Size,
}; };
#[derive(Debug, Clone)]
pub enum EventOrPipeMessage {
Event(Event),
PipeMessage(PipeMessage),
}
#[derive(Debug, Clone, Default)]
pub struct PluginRenderAsset {
// TODO: naming
pub client_id: ClientId,
pub plugin_id: PluginId,
pub bytes: Vec<u8>,
pub cli_pipes: HashMap<String, PipeStateChange>,
}
impl PluginRenderAsset {
pub fn new(plugin_id: PluginId, client_id: ClientId, bytes: Vec<u8>) -> Self {
PluginRenderAsset {
client_id,
plugin_id,
bytes,
..Default::default()
}
}
pub fn with_pipes(mut self, cli_pipes: HashMap<String, PipeStateChange>) -> Self {
self.cli_pipes = cli_pipes;
self
}
}
pub struct WasmBridge { pub struct WasmBridge {
connected_clients: Arc<Mutex<Vec<ClientId>>>, connected_clients: Arc<Mutex<Vec<ClientId>>>,
plugins: PluginsConfig, plugins: PluginsConfig,
@ -49,7 +83,8 @@ pub struct WasmBridge {
plugin_cache: Arc<Mutex<HashMap<PathBuf, Module>>>, plugin_cache: Arc<Mutex<HashMap<PathBuf, Module>>>,
plugin_map: Arc<Mutex<PluginMap>>, plugin_map: Arc<Mutex<PluginMap>>,
next_plugin_id: PluginId, next_plugin_id: PluginId,
cached_events_for_pending_plugins: HashMap<PluginId, Vec<Event>>, plugin_ids_waiting_for_permission_request: HashSet<PluginId>,
cached_events_for_pending_plugins: HashMap<PluginId, Vec<EventOrPipeMessage>>,
cached_resizes_for_pending_plugins: HashMap<PluginId, (usize, usize)>, // (rows, columns) cached_resizes_for_pending_plugins: HashMap<PluginId, (usize, usize)>, // (rows, columns)
cached_worker_messages: HashMap<PluginId, Vec<(ClientId, String, String, String)>>, // Vec<clientid, cached_worker_messages: HashMap<PluginId, Vec<(ClientId, String, String, String)>>, // Vec<clientid,
// worker_name, // worker_name,
@ -64,6 +99,9 @@ pub struct WasmBridge {
client_attributes: ClientAttributes, client_attributes: ClientAttributes,
default_shell: Option<TerminalAction>, default_shell: Option<TerminalAction>,
default_layout: Box<Layout>, default_layout: Box<Layout>,
cached_plugin_map:
HashMap<RunPluginLocation, HashMap<PluginUserConfiguration, Vec<(PluginId, ClientId)>>>,
pending_pipes: PendingPipes,
} }
impl WasmBridge { impl WasmBridge {
@ -96,6 +134,7 @@ impl WasmBridge {
watcher, watcher,
next_plugin_id: 0, next_plugin_id: 0,
cached_events_for_pending_plugins: HashMap::new(), cached_events_for_pending_plugins: HashMap::new(),
plugin_ids_waiting_for_permission_request: HashSet::new(),
cached_resizes_for_pending_plugins: HashMap::new(), cached_resizes_for_pending_plugins: HashMap::new(),
cached_worker_messages: HashMap::new(), cached_worker_messages: HashMap::new(),
loading_plugins: HashMap::new(), loading_plugins: HashMap::new(),
@ -105,17 +144,20 @@ impl WasmBridge {
client_attributes, client_attributes,
default_shell, default_shell,
default_layout, default_layout,
cached_plugin_map: HashMap::new(),
pending_pipes: Default::default(),
} }
} }
pub fn load_plugin( pub fn load_plugin(
&mut self, &mut self,
run: &RunPlugin, run: &RunPlugin,
tab_index: usize, tab_index: Option<usize>,
size: Size, size: Size,
cwd: Option<PathBuf>, cwd: Option<PathBuf>,
skip_cache: bool, skip_cache: bool,
client_id: Option<ClientId>, client_id: Option<ClientId>,
) -> Result<PluginId> { cli_client_id: Option<ClientId>,
) -> Result<(PluginId, ClientId)> {
// returns the plugin id // returns the plugin id
let err_context = move || format!("failed to load plugin"); let err_context = move || format!("failed to load plugin");
@ -179,6 +221,7 @@ impl WasmBridge {
plugin_id, plugin_id,
&mut loading_indication, &mut loading_indication,
e, e,
cli_client_id,
), ),
} }
} }
@ -210,16 +253,19 @@ impl WasmBridge {
plugin_id, plugin_id,
&mut loading_indication, &mut loading_indication,
e, e,
cli_client_id,
), ),
} }
let _ = let _ = senders.send_to_plugin(PluginInstruction::ApplyCachedEvents {
senders.send_to_plugin(PluginInstruction::ApplyCachedEvents(vec![plugin_id])); plugin_ids: vec![plugin_id],
done_receiving_permissions: false,
});
} }
}); });
self.loading_plugins self.loading_plugins
.insert((plugin_id, run.clone()), load_plugin_task); .insert((plugin_id, run.clone()), load_plugin_task);
self.next_plugin_id += 1; self.next_plugin_id += 1;
Ok(plugin_id) Ok((plugin_id, client_id))
} }
pub fn unload_plugin(&mut self, pid: PluginId) -> Result<()> { pub fn unload_plugin(&mut self, pid: PluginId) -> Result<()> {
info!("Bye from plugin {}", &pid); info!("Bye from plugin {}", &pid);
@ -234,6 +280,14 @@ impl WasmBridge {
log::error!("Failed to remove cache dir for plugin: {:?}", e); log::error!("Failed to remove cache dir for plugin: {:?}", e);
} }
} }
self.cached_plugin_map.clear();
let mut pipes_to_unblock = self.pending_pipes.unload_plugin(&pid);
for pipe_name in pipes_to_unblock.drain(..) {
let _ = self
.senders
.send_to_server(ServerInstruction::UnblockCliPipeInput(pipe_name))
.context("failed to unblock input pipe");
}
Ok(()) Ok(())
} }
pub fn reload_plugin(&mut self, run_plugin: &RunPlugin) -> Result<()> { pub fn reload_plugin(&mut self, run_plugin: &RunPlugin) -> Result<()> {
@ -242,7 +296,8 @@ impl WasmBridge {
return Ok(()); return Ok(());
} }
let plugin_ids = self.all_plugin_ids_for_plugin_location(&run_plugin.location)?; let plugin_ids = self
.all_plugin_ids_for_plugin_location(&run_plugin.location, &run_plugin.configuration)?;
for plugin_id in &plugin_ids { for plugin_id in &plugin_ids {
let (rows, columns) = self.size_of_plugin_id(*plugin_id).unwrap_or((0, 0)); let (rows, columns) = self.size_of_plugin_id(*plugin_id).unwrap_or((0, 0));
self.cached_events_for_pending_plugins self.cached_events_for_pending_plugins
@ -315,6 +370,7 @@ impl WasmBridge {
*plugin_id, *plugin_id,
&mut loading_indication, &mut loading_indication,
e, e,
None,
), ),
} }
} }
@ -326,11 +382,15 @@ impl WasmBridge {
*plugin_id, *plugin_id,
&mut loading_indication, &mut loading_indication,
&e, &e,
None,
); );
} }
}, },
} }
let _ = senders.send_to_plugin(PluginInstruction::ApplyCachedEvents(plugin_ids)); let _ = senders.send_to_plugin(PluginInstruction::ApplyCachedEvents {
plugin_ids,
done_receiving_permissions: false,
});
} }
}); });
self.loading_plugins self.loading_plugins
@ -402,9 +462,12 @@ impl WasmBridge {
let mut running_plugin = running_plugin.lock().unwrap(); let mut running_plugin = running_plugin.lock().unwrap();
let _s = _s; // guard to allow the task to complete before cleanup/shutdown let _s = _s; // guard to allow the task to complete before cleanup/shutdown
if running_plugin.apply_event_id(AtomicEvent::Resize, event_id) { if running_plugin.apply_event_id(AtomicEvent::Resize, event_id) {
let old_rows = running_plugin.rows;
let old_columns = running_plugin.columns;
running_plugin.rows = new_rows; running_plugin.rows = new_rows;
running_plugin.columns = new_columns; running_plugin.columns = new_columns;
if old_rows != new_rows || old_columns != new_columns {
let rendered_bytes = running_plugin let rendered_bytes = running_plugin
.instance .instance
.clone() .clone()
@ -422,25 +485,28 @@ impl WasmBridge {
) )
.map_err(anyError::new) .map_err(anyError::new)
}) })
.and_then(|_| wasi_read_string(&running_plugin.plugin_env.wasi_env)) .and_then(|_| {
wasi_read_string(&running_plugin.plugin_env.wasi_env)
})
.with_context(err_context); .with_context(err_context);
match rendered_bytes { match rendered_bytes {
Ok(rendered_bytes) => { Ok(rendered_bytes) => {
let plugin_bytes = vec![( let plugin_render_asset = PluginRenderAsset::new(
plugin_id, plugin_id,
client_id, client_id,
rendered_bytes.as_bytes().to_vec(), rendered_bytes.as_bytes().to_vec(),
)]; );
senders senders
.send_to_screen(ScreenInstruction::PluginBytes( .send_to_screen(ScreenInstruction::PluginBytes(vec![
plugin_bytes, plugin_render_asset,
)) ]))
.unwrap(); .unwrap();
}, },
Err(e) => log::error!("{}", e), Err(e) => log::error!("{}", e),
} }
} }
} }
}
}); });
} }
} }
@ -484,10 +550,7 @@ impl WasmBridge {
let event_type = let event_type =
EventType::from_str(&event.to_string()).with_context(err_context)?; EventType::from_str(&event.to_string()).with_context(err_context)?;
if (subs.contains(&event_type) || event_type == EventType::PermissionRequestResult) if (subs.contains(&event_type) || event_type == EventType::PermissionRequestResult)
&& ((pid.is_none() && cid.is_none()) && Self::message_is_directed_at_plugin(pid, cid, plugin_id, client_id)
|| (pid.is_none() && cid == Some(*client_id))
|| (cid.is_none() && pid == Some(*plugin_id))
|| (cid == Some(*client_id) && pid == Some(*plugin_id)))
{ {
task::spawn({ task::spawn({
let senders = self.senders.clone(); let senders = self.senders.clone();
@ -498,18 +561,18 @@ impl WasmBridge {
let _s = shutdown_sender.clone(); let _s = shutdown_sender.clone();
async move { async move {
let mut running_plugin = running_plugin.lock().unwrap(); let mut running_plugin = running_plugin.lock().unwrap();
let mut plugin_bytes = vec![]; let mut plugin_render_assets = vec![];
let _s = _s; // guard to allow the task to complete before cleanup/shutdown let _s = _s; // guard to allow the task to complete before cleanup/shutdown
match apply_event_to_plugin( match apply_event_to_plugin(
plugin_id, plugin_id,
client_id, client_id,
&mut running_plugin, &mut running_plugin,
&event, &event,
&mut plugin_bytes, &mut plugin_render_assets,
) { ) {
Ok(()) => { Ok(()) => {
let _ = senders.send_to_screen(ScreenInstruction::PluginBytes( let _ = senders.send_to_screen(ScreenInstruction::PluginBytes(
plugin_bytes, plugin_render_assets,
)); ));
}, },
Err(e) => { Err(e) => {
@ -532,7 +595,112 @@ impl WasmBridge {
} }
for (plugin_id, cached_events) in self.cached_events_for_pending_plugins.iter_mut() { for (plugin_id, cached_events) in self.cached_events_for_pending_plugins.iter_mut() {
if pid.is_none() || pid.as_ref() == Some(plugin_id) { if pid.is_none() || pid.as_ref() == Some(plugin_id) {
cached_events.push(event.clone()); cached_events.push(EventOrPipeMessage::Event(event.clone()));
}
}
}
Ok(())
}
pub fn pipe_messages(
&mut self,
mut messages: Vec<(Option<PluginId>, Option<ClientId>, PipeMessage)>,
shutdown_sender: Sender<()>,
) -> Result<()> {
let plugins_to_update: Vec<(
PluginId,
ClientId,
Arc<Mutex<RunningPlugin>>,
Arc<Mutex<Subscriptions>>,
)> = self
.plugin_map
.lock()
.unwrap()
.running_plugins_and_subscriptions()
.iter()
.cloned()
.filter(|(plugin_id, _client_id, _running_plugin, _subscriptions)| {
!&self
.cached_events_for_pending_plugins
.contains_key(&plugin_id)
})
.collect();
for (message_pid, message_cid, pipe_message) in messages.drain(..) {
for (plugin_id, client_id, running_plugin, _subscriptions) in &plugins_to_update {
if Self::message_is_directed_at_plugin(
message_pid,
message_cid,
plugin_id,
client_id,
) {
if let PipeSource::Cli(pipe_id) = &pipe_message.source {
self.pending_pipes
.mark_being_processed(pipe_id, plugin_id, client_id);
}
task::spawn({
let senders = self.senders.clone();
let running_plugin = running_plugin.clone();
let pipe_message = pipe_message.clone();
let plugin_id = *plugin_id;
let client_id = *client_id;
let _s = shutdown_sender.clone();
async move {
let mut running_plugin = running_plugin.lock().unwrap();
let mut plugin_render_assets = vec![];
let _s = _s; // guard to allow the task to complete before cleanup/shutdown
match apply_pipe_message_to_plugin(
plugin_id,
client_id,
&mut running_plugin,
&pipe_message,
&mut plugin_render_assets,
&senders,
) {
Ok(()) => {
let _ = senders.send_to_screen(ScreenInstruction::PluginBytes(
plugin_render_assets,
));
},
Err(e) => {
log::error!("{:?}", e);
// https://stackoverflow.com/questions/66450942/in-rust-is-there-a-way-to-make-literal-newlines-in-r-using-windows-c
let stringified_error =
format!("{:?}", e).replace("\n", "\n\r");
handle_plugin_crash(
plugin_id,
stringified_error,
senders.clone(),
);
},
}
}
});
}
}
let all_connected_clients: Vec<ClientId> = self
.connected_clients
.lock()
.unwrap()
.iter()
.copied()
.collect();
for (plugin_id, cached_events) in self.cached_events_for_pending_plugins.iter_mut() {
if message_pid.is_none() || message_pid.as_ref() == Some(plugin_id) {
cached_events.push(EventOrPipeMessage::PipeMessage(pipe_message.clone()));
if let PipeSource::Cli(pipe_id) = &pipe_message.source {
for client_id in &all_connected_clients {
if Self::message_is_directed_at_plugin(
message_pid,
message_cid,
plugin_id,
client_id,
) {
self.pending_pipes
.mark_being_processed(pipe_id, plugin_id, client_id);
}
}
}
} }
} }
} }
@ -541,16 +709,27 @@ impl WasmBridge {
pub fn apply_cached_events( pub fn apply_cached_events(
&mut self, &mut self,
plugin_ids: Vec<PluginId>, plugin_ids: Vec<PluginId>,
done_receiving_permissions: bool,
shutdown_sender: Sender<()>, shutdown_sender: Sender<()>,
) -> Result<()> { ) -> Result<()> {
let mut applied_plugin_paths = HashSet::new(); let mut applied_plugin_paths = HashSet::new();
for plugin_id in plugin_ids { for plugin_id in plugin_ids {
if !done_receiving_permissions
&& self
.plugin_ids_waiting_for_permission_request
.contains(&plugin_id)
{
continue;
}
self.plugin_ids_waiting_for_permission_request
.remove(&plugin_id);
self.apply_cached_events_and_resizes_for_plugin(plugin_id, shutdown_sender.clone())?; self.apply_cached_events_and_resizes_for_plugin(plugin_id, shutdown_sender.clone())?;
if let Some(run_plugin) = self.run_plugin_of_loading_plugin_id(plugin_id) { if let Some(run_plugin) = self.run_plugin_of_loading_plugin_id(plugin_id) {
applied_plugin_paths.insert(run_plugin.clone()); applied_plugin_paths.insert(run_plugin.clone());
} }
self.loading_plugins self.loading_plugins
.retain(|(p_id, _run_plugin), _| p_id != &plugin_id); .retain(|(p_id, _run_plugin), _| p_id != &plugin_id);
self.clear_plugin_map_cache();
} }
for run_plugin in applied_plugin_paths.drain() { for run_plugin in applied_plugin_paths.drain() {
if self.pending_plugin_reloads.remove(&run_plugin) { if self.pending_plugin_reloads.remove(&run_plugin) {
@ -595,7 +774,9 @@ impl WasmBridge {
shutdown_sender: Sender<()>, shutdown_sender: Sender<()>,
) -> Result<()> { ) -> Result<()> {
let err_context = || format!("Failed to apply cached events to plugin"); let err_context = || format!("Failed to apply cached events to plugin");
if let Some(events) = self.cached_events_for_pending_plugins.remove(&plugin_id) { if let Some(events_or_pipe_messages) =
self.cached_events_for_pending_plugins.remove(&plugin_id)
{
let all_connected_clients: Vec<ClientId> = self let all_connected_clients: Vec<ClientId> = self
.connected_clients .connected_clients
.lock() .lock()
@ -610,44 +791,93 @@ impl WasmBridge {
.unwrap() .unwrap()
.get_running_plugin_and_subscriptions(plugin_id, *client_id) .get_running_plugin_and_subscriptions(plugin_id, *client_id)
{ {
let subs = subscriptions.lock().unwrap().clone();
for event in events.clone() {
let event_type =
EventType::from_str(&event.to_string()).with_context(err_context)?;
if !subs.contains(&event_type) {
continue;
}
task::spawn({ task::spawn({
let senders = self.senders.clone(); let senders = self.senders.clone();
let running_plugin = running_plugin.clone(); let running_plugin = running_plugin.clone();
let client_id = *client_id; let client_id = *client_id;
let _s = shutdown_sender.clone(); let _s = shutdown_sender.clone();
let events_or_pipe_messages = events_or_pipe_messages.clone();
async move { async move {
let mut running_plugin = running_plugin.lock().unwrap(); let subs = subscriptions.lock().unwrap().clone();
let mut plugin_bytes = vec![];
let _s = _s; // guard to allow the task to complete before cleanup/shutdown let _s = _s; // guard to allow the task to complete before cleanup/shutdown
for event_or_pipe_message in events_or_pipe_messages {
match event_or_pipe_message {
EventOrPipeMessage::Event(event) => {
match EventType::from_str(&event.to_string())
.with_context(err_context)
{
Ok(event_type) => {
if !subs.contains(&event_type) {
continue;
}
let mut running_plugin =
running_plugin.lock().unwrap();
let mut plugin_render_assets = vec![];
match apply_event_to_plugin( match apply_event_to_plugin(
plugin_id, plugin_id,
client_id, client_id,
&mut running_plugin, &mut running_plugin,
&event, &event,
&mut plugin_bytes, &mut plugin_render_assets,
) { ) {
Ok(()) => { Ok(()) => {
let _ = senders.send_to_screen( let _ = senders.send_to_screen(
ScreenInstruction::PluginBytes(plugin_bytes), ScreenInstruction::PluginBytes(
plugin_render_assets,
),
); );
}, },
Err(e) => { Err(e) => {
log::error!("{}", e); log::error!("{}", e);
}, },
} }
},
Err(e) => {
log::error!("Failed to apply event: {:?}", e);
},
}
},
EventOrPipeMessage::PipeMessage(pipe_message) => {
let mut running_plugin = running_plugin.lock().unwrap();
let mut plugin_render_assets = vec![];
match apply_pipe_message_to_plugin(
plugin_id,
client_id,
&mut running_plugin,
&pipe_message,
&mut plugin_render_assets,
&senders,
) {
Ok(()) => {
let _ = senders.send_to_screen(
ScreenInstruction::PluginBytes(
plugin_render_assets,
),
);
},
Err(e) => {
log::error!("{:?}", e);
// https://stackoverflow.com/questions/66450942/in-rust-is-there-a-way-to-make-literal-newlines-in-r-using-windows-c
let stringified_error =
format!("{:?}", e).replace("\n", "\n\r");
handle_plugin_crash(
plugin_id,
stringified_error,
senders.clone(),
);
},
}
},
}
}
} }
}); });
} }
} }
} }
}
if let Some((rows, columns)) = self.cached_resizes_for_pending_plugins.remove(&plugin_id) { if let Some((rows, columns)) = self.cached_resizes_for_pending_plugins.remove(&plugin_id) {
self.resize_plugin(plugin_id, columns, rows, shutdown_sender.clone())?; self.resize_plugin(plugin_id, columns, rows, shutdown_sender.clone())?;
} }
@ -676,14 +906,55 @@ impl WasmBridge {
.find(|((_plugin_id, run_plugin), _)| &run_plugin.location == plugin_location) .find(|((_plugin_id, run_plugin), _)| &run_plugin.location == plugin_location)
.is_some() .is_some()
} }
fn plugin_id_of_loading_plugin(
&self,
plugin_location: &RunPluginLocation,
plugin_configuration: &PluginUserConfiguration,
) -> Option<PluginId> {
self.loading_plugins
.iter()
.find_map(|((plugin_id, run_plugin), _)| {
if &run_plugin.location == plugin_location
&& &run_plugin.configuration == plugin_configuration
{
Some(*plugin_id)
} else {
None
}
})
}
fn all_plugin_ids_for_plugin_location( fn all_plugin_ids_for_plugin_location(
&self, &self,
plugin_location: &RunPluginLocation, plugin_location: &RunPluginLocation,
plugin_configuration: &PluginUserConfiguration,
) -> Result<Vec<PluginId>> { ) -> Result<Vec<PluginId>> {
self.plugin_map self.plugin_map
.lock() .lock()
.unwrap() .unwrap()
.all_plugin_ids_for_plugin_location(plugin_location) .all_plugin_ids_for_plugin_location(plugin_location, plugin_configuration)
}
pub fn all_plugin_and_client_ids_for_plugin_location(
&mut self,
plugin_location: &RunPluginLocation,
plugin_configuration: &PluginUserConfiguration,
) -> Vec<(PluginId, Option<ClientId>)> {
if self.cached_plugin_map.is_empty() {
self.cached_plugin_map = self.plugin_map.lock().unwrap().clone_plugin_assets();
}
match self
.cached_plugin_map
.get(plugin_location)
.and_then(|m| m.get(plugin_configuration))
{
Some(plugin_and_client_ids) => plugin_and_client_ids
.iter()
.map(|(plugin_id, client_id)| (*plugin_id, Some(*client_id)))
.collect(),
None => vec![],
}
}
pub fn all_plugin_ids(&self) -> Vec<(PluginId, ClientId)> {
self.plugin_map.lock().unwrap().all_plugin_ids()
} }
fn size_of_plugin_id(&self, plugin_id: PluginId) -> Option<(usize, usize)> { fn size_of_plugin_id(&self, plugin_id: PluginId) -> Option<(usize, usize)> {
// (rows/colums) // (rows/colums)
@ -793,6 +1064,117 @@ impl WasmBridge {
permission_cache.write_to_file().with_context(err_context) permission_cache.write_to_file().with_context(err_context)
} }
pub fn cache_plugin_events(&mut self, plugin_id: PluginId) {
self.plugin_ids_waiting_for_permission_request
.insert(plugin_id);
self.cached_events_for_pending_plugins
.entry(plugin_id)
.or_insert_with(Default::default);
}
// gets all running plugins details matching this run_plugin, if none are running, loads one and
// returns its details
pub fn get_or_load_plugins(
&mut self,
run_plugin: RunPlugin,
size: Size,
cwd: Option<PathBuf>,
skip_cache: bool,
should_float: bool,
should_be_open_in_place: bool,
pane_title: Option<String>,
pane_id_to_replace: Option<PaneId>,
cli_client_id: Option<ClientId>,
) -> Vec<(PluginId, Option<ClientId>)> {
let all_plugin_ids = self.all_plugin_and_client_ids_for_plugin_location(
&run_plugin.location,
&run_plugin.configuration,
);
if all_plugin_ids.is_empty() {
if let Some(loading_plugin_id) =
self.plugin_id_of_loading_plugin(&run_plugin.location, &run_plugin.configuration)
{
return vec![(loading_plugin_id, None)];
}
match self.load_plugin(
&run_plugin,
None,
size,
cwd.clone(),
skip_cache,
None,
cli_client_id,
) {
Ok((plugin_id, client_id)) => {
drop(self.senders.send_to_screen(ScreenInstruction::AddPlugin(
Some(should_float),
should_be_open_in_place,
run_plugin,
pane_title,
None,
plugin_id,
pane_id_to_replace,
cwd,
Some(client_id),
)));
vec![(plugin_id, Some(client_id))]
},
Err(e) => {
log::error!("Failed to load plugin: {e}");
if let Some(cli_client_id) = cli_client_id {
let _ = self.senders.send_to_server(ServerInstruction::LogError(
vec![format!("Failed to log plugin: {e}")],
cli_client_id,
));
}
vec![]
},
}
} else {
all_plugin_ids
}
}
pub fn clear_plugin_map_cache(&mut self) {
self.cached_plugin_map.clear();
}
// returns the pipe names to unblock
pub fn update_cli_pipe_state(
&mut self,
pipe_state_changes: Vec<PluginRenderAsset>,
) -> Vec<String> {
let mut pipe_names_to_unblock = vec![];
for pipe_state_change in pipe_state_changes {
let client_id = pipe_state_change.client_id;
let plugin_id = pipe_state_change.plugin_id;
for (cli_pipe_name, pipe_state_change) in pipe_state_change.cli_pipes {
pipe_names_to_unblock.append(&mut self.pending_pipes.update_pipe_state_change(
&cli_pipe_name,
pipe_state_change,
&plugin_id,
&client_id,
));
}
}
let pipe_names_to_unblock =
pipe_names_to_unblock
.into_iter()
.fold(HashSet::new(), |mut acc, p| {
acc.insert(p);
acc
});
pipe_names_to_unblock.into_iter().collect()
}
fn message_is_directed_at_plugin(
message_pid: Option<PluginId>,
message_cid: Option<ClientId>,
plugin_id: &PluginId,
client_id: &ClientId,
) -> bool {
message_pid.is_none() && message_cid.is_none()
|| (message_pid.is_none() && message_cid == Some(*client_id))
|| (message_cid.is_none() && message_pid == Some(*plugin_id))
|| (message_cid == Some(*client_id) && message_pid == Some(*plugin_id))
}
} }
fn handle_plugin_successful_loading(senders: &ThreadSenders, plugin_id: PluginId) { fn handle_plugin_successful_loading(senders: &ThreadSenders, plugin_id: PluginId) {
@ -805,6 +1187,7 @@ fn handle_plugin_loading_failure(
plugin_id: PluginId, plugin_id: PluginId,
loading_indication: &mut LoadingIndication, loading_indication: &mut LoadingIndication,
error: impl std::fmt::Debug, error: impl std::fmt::Debug,
cli_client_id: Option<ClientId>,
) { ) {
log::error!("{:?}", error); log::error!("{:?}", error);
let _ = senders.send_to_background_jobs(BackgroundJob::StopPluginLoadingAnimation(plugin_id)); let _ = senders.send_to_background_jobs(BackgroundJob::StopPluginLoadingAnimation(plugin_id));
@ -813,6 +1196,12 @@ fn handle_plugin_loading_failure(
plugin_id, plugin_id,
loading_indication.clone(), loading_indication.clone(),
)); ));
if let Some(cli_client_id) = cli_client_id {
let _ = senders.send_to_server(ServerInstruction::LogError(
vec![format!("{:?}", error)],
cli_client_id,
));
}
} }
// TODO: move to permissions? // TODO: move to permissions?
@ -850,7 +1239,7 @@ pub fn apply_event_to_plugin(
client_id: ClientId, client_id: ClientId,
running_plugin: &mut RunningPlugin, running_plugin: &mut RunningPlugin,
event: &Event, event: &Event,
plugin_bytes: &mut Vec<(PluginId, ClientId, Vec<u8>)>, plugin_render_assets: &mut Vec<PluginRenderAsset>,
) -> Result<()> { ) -> Result<()> {
let instance = &running_plugin.instance; let instance = &running_plugin.instance;
let plugin_env = &running_plugin.plugin_env; let plugin_env = &running_plugin.plugin_env;
@ -897,7 +1286,14 @@ pub fn apply_event_to_plugin(
}) })
.and_then(|_| wasi_read_string(&plugin_env.wasi_env)) .and_then(|_| wasi_read_string(&plugin_env.wasi_env))
.with_context(err_context)?; .with_context(err_context)?;
plugin_bytes.push((plugin_id, client_id, rendered_bytes.as_bytes().to_vec())); let pipes_to_block_or_unblock = pipes_to_block_or_unblock(running_plugin, None);
let plugin_render_asset = PluginRenderAsset::new(
plugin_id,
client_id,
rendered_bytes.as_bytes().to_vec(),
)
.with_pipes(pipes_to_block_or_unblock);
plugin_render_assets.push(plugin_render_asset);
} }
}, },
(PermissionStatus::Denied, permission) => { (PermissionStatus::Denied, permission) => {

View file

@ -18,7 +18,8 @@ use std::{
use wasmer::{imports, AsStoreMut, Function, FunctionEnv, FunctionEnvMut, Imports}; use wasmer::{imports, AsStoreMut, Function, FunctionEnv, FunctionEnvMut, Imports};
use wasmer_wasi::WasiEnv; use wasmer_wasi::WasiEnv;
use zellij_utils::data::{ use zellij_utils::data::{
CommandType, ConnectToSession, HttpVerb, PermissionStatus, PermissionType, PluginPermission, CommandType, ConnectToSession, HttpVerb, MessageToPlugin, PermissionStatus, PermissionType,
PluginPermission,
}; };
use zellij_utils::input::permission::PermissionCache; use zellij_utils::input::permission::PermissionCache;
@ -58,6 +59,7 @@ macro_rules! apply_action {
$env.plugin_env.client_attributes.clone(), $env.plugin_env.client_attributes.clone(),
$env.plugin_env.default_shell.clone(), $env.plugin_env.default_shell.clone(),
$env.plugin_env.default_layout.clone(), $env.plugin_env.default_layout.clone(),
None,
) { ) {
log::error!("{}: {:?}", $error_message(), e); log::error!("{}: {:?}", $error_message(), e);
} }
@ -240,6 +242,16 @@ fn host_run_plugin_command(env: FunctionEnvMut<ForeignFunctionEnv>) {
PluginCommand::RenameSession(new_session_name) => { PluginCommand::RenameSession(new_session_name) => {
rename_session(env, new_session_name) rename_session(env, new_session_name)
}, },
PluginCommand::UnblockCliPipeInput(pipe_name) => {
unblock_cli_pipe_input(env, pipe_name)
},
PluginCommand::BlockCliPipeInput(pipe_name) => {
block_cli_pipe_input(env, pipe_name)
},
PluginCommand::CliPipeOutput(pipe_name, output) => {
cli_pipe_output(env, pipe_name, output)?
},
PluginCommand::MessageToPlugin(message) => message_to_plugin(env, message)?,
}, },
(PermissionStatus::Denied, permission) => { (PermissionStatus::Denied, permission) => {
log::error!( log::error!(
@ -272,6 +284,39 @@ fn subscribe(env: &ForeignFunctionEnv, event_list: HashSet<EventType>) -> Result
)) ))
} }
fn unblock_cli_pipe_input(env: &ForeignFunctionEnv, pipe_name: String) {
env.plugin_env
.input_pipes_to_unblock
.lock()
.unwrap()
.insert(pipe_name);
}
fn block_cli_pipe_input(env: &ForeignFunctionEnv, pipe_name: String) {
env.plugin_env
.input_pipes_to_block
.lock()
.unwrap()
.insert(pipe_name);
}
fn cli_pipe_output(env: &ForeignFunctionEnv, pipe_name: String, output: String) -> Result<()> {
env.plugin_env
.senders
.send_to_server(ServerInstruction::CliPipeOutput(pipe_name, output))
.context("failed to send pipe output")
}
fn message_to_plugin(env: &ForeignFunctionEnv, message_to_plugin: MessageToPlugin) -> Result<()> {
env.plugin_env
.senders
.send_to_plugin(PluginInstruction::MessageFromPlugin {
source_plugin_id: env.plugin_env.plugin_id,
message: message_to_plugin,
})
.context("failed to send message to plugin")
}
fn unsubscribe(env: &ForeignFunctionEnv, event_list: HashSet<EventType>) -> Result<()> { fn unsubscribe(env: &ForeignFunctionEnv, event_list: HashSet<EventType>) -> Result<()> {
env.subscriptions env.subscriptions
.lock() .lock()
@ -325,6 +370,15 @@ fn request_permission(env: &ForeignFunctionEnv, permissions: Vec<PermissionType>
)); ));
} }
// we do this so that messages that have arrived while the user is seeing the permission screen
// will be cached and reapplied once the permission is granted
let _ = env
.plugin_env
.senders
.send_to_plugin(PluginInstruction::CachePluginEvents {
plugin_id: env.plugin_env.plugin_id,
});
env.plugin_env env.plugin_env
.senders .senders
.send_to_screen(ScreenInstruction::RequestPluginPermissions( .send_to_screen(ScreenInstruction::RequestPluginPermissions(
@ -1353,6 +1407,10 @@ fn check_command_permission(
| PluginCommand::DeleteAllDeadSessions | PluginCommand::DeleteAllDeadSessions
| PluginCommand::RenameSession(..) | PluginCommand::RenameSession(..)
| PluginCommand::RenameTab(..) => PermissionType::ChangeApplicationState, | PluginCommand::RenameTab(..) => PermissionType::ChangeApplicationState,
PluginCommand::UnblockCliPipeInput(..)
| PluginCommand::BlockCliPipeInput(..)
| PluginCommand::CliPipeOutput(..) => PermissionType::ReadCliPipes,
PluginCommand::MessageToPlugin(..) => PermissionType::MessageAndLaunchOtherPlugins,
_ => return (PermissionStatus::Granted, None), _ => return (PermissionStatus::Granted, None),
}; };

View file

@ -1,4 +1,4 @@
use std::collections::VecDeque; use std::collections::{HashSet, VecDeque};
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use crate::thread_bus::ThreadSenders; use crate::thread_bus::ThreadSenders;
@ -36,6 +36,7 @@ pub(crate) fn route_action(
client_attributes: ClientAttributes, client_attributes: ClientAttributes,
default_shell: Option<TerminalAction>, default_shell: Option<TerminalAction>,
default_layout: Box<Layout>, default_layout: Box<Layout>,
mut seen_cli_pipes: Option<&mut HashSet<String>>,
) -> Result<bool> { ) -> Result<bool> {
let mut should_break = false; let mut should_break = false;
let err_context = || format!("failed to route action for client {client_id}"); let err_context = || format!("failed to route action for client {client_id}");
@ -803,6 +804,57 @@ pub(crate) fn route_action(
.send_to_screen(ScreenInstruction::RenameSession(name, client_id)) .send_to_screen(ScreenInstruction::RenameSession(name, client_id))
.with_context(err_context)?; .with_context(err_context)?;
}, },
Action::CliPipe {
pipe_id,
mut name,
payload,
plugin,
args,
configuration,
floating,
in_place,
skip_cache,
cwd,
pane_title,
..
} => {
if let Some(seen_cli_pipes) = seen_cli_pipes.as_mut() {
if !seen_cli_pipes.contains(&pipe_id) {
seen_cli_pipes.insert(pipe_id.clone());
senders
.send_to_server(ServerInstruction::AssociatePipeWithClient {
pipe_id: pipe_id.clone(),
client_id,
})
.with_context(err_context)?;
}
}
if let Some(name) = name.take() {
let should_open_in_place = in_place.unwrap_or(false);
if should_open_in_place && pane_id.is_none() {
log::error!("Was asked to open a new plugin in-place, but cannot identify the pane id... is the ZELLIJ_PANE_ID variable set?");
}
let pane_id_to_replace = if should_open_in_place { pane_id } else { None };
senders
.send_to_plugin(PluginInstruction::CliPipe {
pipe_id,
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) Ok(should_break)
} }
@ -833,12 +885,13 @@ pub(crate) fn route_thread_main(
) -> Result<()> { ) -> Result<()> {
let mut retry_queue = VecDeque::new(); let mut retry_queue = VecDeque::new();
let err_context = || format!("failed to handle instruction for client {client_id}"); let err_context = || format!("failed to handle instruction for client {client_id}");
let mut seen_cli_pipes = HashSet::new();
'route_loop: loop { 'route_loop: loop {
match receiver.recv() { match receiver.recv() {
Some((instruction, err_ctx)) => { Some((instruction, err_ctx)) => {
err_ctx.update_thread_ctx(); err_ctx.update_thread_ctx();
let rlocked_sessions = session_data.read().to_anyhow().with_context(err_context)?; let rlocked_sessions = session_data.read().to_anyhow().with_context(err_context)?;
let handle_instruction = |instruction: ClientToServerMsg, let mut handle_instruction = |instruction: ClientToServerMsg,
mut retry_queue: Option< mut retry_queue: Option<
&mut VecDeque<ClientToServerMsg>, &mut VecDeque<ClientToServerMsg>,
>| >|
@ -868,6 +921,7 @@ pub(crate) fn route_thread_main(
rlocked_sessions.client_attributes.clone(), rlocked_sessions.client_attributes.clone(),
rlocked_sessions.default_shell.clone(), rlocked_sessions.default_shell.clone(),
rlocked_sessions.layout.clone(), rlocked_sessions.layout.clone(),
Some(&mut seen_cli_pipes),
)? { )? {
should_break = true; should_break = true;
} }

View file

@ -35,7 +35,7 @@ use crate::{
output::Output, output::Output,
panes::sixel::SixelImageStore, panes::sixel::SixelImageStore,
panes::PaneId, panes::PaneId,
plugins::PluginInstruction, plugins::{PluginInstruction, PluginRenderAsset},
pty::{ClientTabIndexOrPaneId, PtyInstruction, VteBytes}, pty::{ClientTabIndexOrPaneId, PtyInstruction, VteBytes},
tab::Tab, tab::Tab,
thread_bus::Bus, thread_bus::Bus,
@ -138,7 +138,7 @@ type HoldForCommand = Option<RunCommand>;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum ScreenInstruction { pub enum ScreenInstruction {
PtyBytes(u32, VteBytes), PtyBytes(u32, VteBytes),
PluginBytes(Vec<(u32, ClientId, VteBytes)>), // u32 is plugin_id PluginBytes(Vec<PluginRenderAsset>),
Render, Render,
NewPane( NewPane(
PaneId, PaneId,
@ -285,7 +285,7 @@ pub enum ScreenInstruction {
bool, // should be opened in place bool, // should be opened in place
RunPlugin, RunPlugin,
Option<String>, // pane title Option<String>, // pane title
usize, // tab index Option<usize>, // tab index
u32, // plugin id u32, // plugin id
Option<PaneId>, Option<PaneId>,
Option<PathBuf>, // cwd Option<PathBuf>, // cwd
@ -822,7 +822,7 @@ impl Screen {
self.log_and_report_session_state() self.log_and_report_session_state()
.with_context(err_context)?; .with_context(err_context)?;
return self.render().with_context(err_context); return self.render(None).with_context(err_context);
}, },
Err(err) => Err::<(), _>(err).with_context(err_context).non_fatal(), Err(err) => Err::<(), _>(err).with_context(err_context).non_fatal(),
} }
@ -956,7 +956,7 @@ impl Screen {
} }
self.log_and_report_session_state() self.log_and_report_session_state()
.with_context(err_context)?; .with_context(err_context)?;
self.render().with_context(err_context) self.render(None).with_context(err_context)
} }
} }
@ -994,7 +994,7 @@ impl Screen {
} }
self.log_and_report_session_state() self.log_and_report_session_state()
.with_context(err_context)?; .with_context(err_context)?;
self.render().with_context(err_context) self.render(None).with_context(err_context)
} }
pub fn update_pixel_dimensions(&mut self, pixel_dimensions: PixelDimensions) { pub fn update_pixel_dimensions(&mut self, pixel_dimensions: PixelDimensions) {
@ -1038,7 +1038,7 @@ impl Screen {
} }
/// Renders this [`Screen`], which amounts to rendering its active [`Tab`]. /// Renders this [`Screen`], which amounts to rendering its active [`Tab`].
pub fn render(&mut self) -> Result<()> { pub fn render(&mut self, plugin_render_assets: Option<Vec<PluginRenderAsset>>) -> Result<()> {
let err_context = "failed to render screen"; let err_context = "failed to render screen";
let mut output = Output::new( let mut output = Output::new(
@ -1059,13 +1059,20 @@ impl Screen {
} }
if output.is_dirty() { if output.is_dirty() {
let serialized_output = output.serialize().context(err_context)?; let serialized_output = output.serialize().context(err_context)?;
self.bus let _ = self
.bus
.senders .senders
.send_to_server(ServerInstruction::Render(Some(serialized_output))) .send_to_server(ServerInstruction::Render(Some(serialized_output)))
.context(err_context) .context(err_context);
} else {
Ok(())
} }
if let Some(plugin_render_assets) = plugin_render_assets {
let _ = self
.bus
.senders
.send_to_plugin(PluginInstruction::UnblockCliPipes(plugin_render_assets))
.context("failed to unblock input pipe");
}
Ok(())
} }
/// Returns a mutable reference to this [`Screen`]'s tabs. /// Returns a mutable reference to this [`Screen`]'s tabs.
@ -1264,7 +1271,7 @@ impl Screen {
} }
self.log_and_report_session_state() self.log_and_report_session_state()
.and_then(|_| self.render()) .and_then(|_| self.render(None))
.with_context(err_context) .with_context(err_context)
} }
@ -1694,7 +1701,7 @@ impl Screen {
self.log_and_report_session_state() self.log_and_report_session_state()
.context("failed to toggle tabs")?; .context("failed to toggle tabs")?;
self.render() self.render(None)
} }
pub fn focus_plugin_pane( pub fn focus_plugin_pane(
@ -1902,7 +1909,7 @@ impl Screen {
.with_context(err_context)?; .with_context(err_context)?;
} }
self.unblock_input()?; self.unblock_input()?;
self.render()?; self.render(None)?;
Ok(()) Ok(())
} }
pub fn replace_pane( pub fn replace_pane(
@ -2155,21 +2162,25 @@ pub(crate) fn screen_thread_main(
} }
} }
}, },
ScreenInstruction::PluginBytes(mut plugin_bytes) => { ScreenInstruction::PluginBytes(mut plugin_render_assets) => {
for (pid, client_id, vte_bytes) in plugin_bytes.drain(..) { for plugin_render_asset in plugin_render_assets.iter_mut() {
let plugin_id = plugin_render_asset.plugin_id;
let client_id = plugin_render_asset.client_id;
let vte_bytes = plugin_render_asset.bytes.drain(..).collect();
let all_tabs = screen.get_tabs_mut(); let all_tabs = screen.get_tabs_mut();
for tab in all_tabs.values_mut() { for tab in all_tabs.values_mut() {
if tab.has_plugin(pid) { if tab.has_plugin(plugin_id) {
tab.handle_plugin_bytes(pid, client_id, vte_bytes) tab.handle_plugin_bytes(plugin_id, client_id, vte_bytes)
.context("failed to process plugin bytes")?; .context("failed to process plugin bytes")?;
break; break;
} }
} }
} }
screen.render()?; screen.render(Some(plugin_render_assets))?;
}, },
ScreenInstruction::Render => { ScreenInstruction::Render => {
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::NewPane( ScreenInstruction::NewPane(
pid, pid,
@ -2227,7 +2238,7 @@ pub(crate) fn screen_thread_main(
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::OpenInPlaceEditor(pid, client_id) => { ScreenInstruction::OpenInPlaceEditor(pid, client_id) => {
active_tab!(screen, client_id, |tab: &mut Tab| tab active_tab!(screen, client_id, |tab: &mut Tab| tab
@ -2235,7 +2246,7 @@ pub(crate) fn screen_thread_main(
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::TogglePaneEmbedOrFloating(client_id) => { ScreenInstruction::TogglePaneEmbedOrFloating(client_id) => {
active_tab_and_connected_client_id!(screen, client_id, |tab: &mut Tab, client_id: ClientId| tab active_tab_and_connected_client_id!(screen, client_id, |tab: &mut Tab, client_id: ClientId| tab
@ -2243,7 +2254,7 @@ pub(crate) fn screen_thread_main(
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::ToggleFloatingPanes(client_id, default_shell) => { ScreenInstruction::ToggleFloatingPanes(client_id, default_shell) => {
active_tab_and_connected_client_id!(screen, client_id, |tab: &mut Tab, client_id: ClientId| tab active_tab_and_connected_client_id!(screen, client_id, |tab: &mut Tab, client_id: ClientId| tab
@ -2251,7 +2262,7 @@ pub(crate) fn screen_thread_main(
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::HorizontalSplit( ScreenInstruction::HorizontalSplit(
pid, pid,
@ -2280,7 +2291,7 @@ pub(crate) fn screen_thread_main(
} }
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::VerticalSplit( ScreenInstruction::VerticalSplit(
pid, pid,
@ -2309,7 +2320,7 @@ pub(crate) fn screen_thread_main(
} }
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::WriteCharacter(bytes, client_id) => { ScreenInstruction::WriteCharacter(bytes, client_id) => {
let mut state_changed = false; let mut state_changed = false;
@ -2340,7 +2351,7 @@ pub(crate) fn screen_thread_main(
? ?
); );
screen.unblock_input()?; screen.unblock_input()?;
screen.render()?; screen.render(None)?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::SwitchFocus(client_id) => { ScreenInstruction::SwitchFocus(client_id) => {
@ -2350,7 +2361,7 @@ pub(crate) fn screen_thread_main(
|tab: &mut Tab, client_id: ClientId| tab.focus_next_pane(client_id) |tab: &mut Tab, client_id: ClientId| tab.focus_next_pane(client_id)
); );
screen.unblock_input()?; screen.unblock_input()?;
screen.render()?; screen.render(None)?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::FocusNextPane(client_id) => { ScreenInstruction::FocusNextPane(client_id) => {
@ -2359,7 +2370,7 @@ pub(crate) fn screen_thread_main(
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.focus_next_pane(client_id) |tab: &mut Tab, client_id: ClientId| tab.focus_next_pane(client_id)
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::FocusPreviousPane(client_id) => { ScreenInstruction::FocusPreviousPane(client_id) => {
@ -2368,7 +2379,7 @@ pub(crate) fn screen_thread_main(
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.focus_previous_pane(client_id) |tab: &mut Tab, client_id: ClientId| tab.focus_previous_pane(client_id)
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
}, },
@ -2379,14 +2390,14 @@ pub(crate) fn screen_thread_main(
|tab: &mut Tab, client_id: ClientId| tab.move_focus_left(client_id), |tab: &mut Tab, client_id: ClientId| tab.move_focus_left(client_id),
? ?
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::MoveFocusLeftOrPreviousTab(client_id) => { ScreenInstruction::MoveFocusLeftOrPreviousTab(client_id) => {
screen.move_focus_left_or_previous_tab(client_id)?; screen.move_focus_left_or_previous_tab(client_id)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.render()?; screen.render(None)?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::MoveFocusDown(client_id) => { ScreenInstruction::MoveFocusDown(client_id) => {
@ -2396,7 +2407,7 @@ pub(crate) fn screen_thread_main(
|tab: &mut Tab, client_id: ClientId| tab.move_focus_down(client_id), |tab: &mut Tab, client_id: ClientId| tab.move_focus_down(client_id),
? ?
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
}, },
@ -2407,14 +2418,14 @@ pub(crate) fn screen_thread_main(
|tab: &mut Tab, client_id: ClientId| tab.move_focus_right(client_id), |tab: &mut Tab, client_id: ClientId| tab.move_focus_right(client_id),
? ?
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::MoveFocusRightOrNextTab(client_id) => { ScreenInstruction::MoveFocusRightOrNextTab(client_id) => {
screen.move_focus_right_or_next_tab(client_id)?; screen.move_focus_right_or_next_tab(client_id)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.render()?; screen.render(None)?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::MoveFocusUp(client_id) => { ScreenInstruction::MoveFocusUp(client_id) => {
@ -2424,7 +2435,7 @@ pub(crate) fn screen_thread_main(
|tab: &mut Tab, client_id: ClientId| tab.move_focus_up(client_id), |tab: &mut Tab, client_id: ClientId| tab.move_focus_up(client_id),
? ?
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
}, },
@ -2437,7 +2448,7 @@ pub(crate) fn screen_thread_main(
), ),
? ?
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::DumpScreen(file, client_id, full) => { ScreenInstruction::DumpScreen(file, client_id, full) => {
@ -2451,7 +2462,7 @@ pub(crate) fn screen_thread_main(
), ),
? ?
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::DumpLayout(default_shell, client_id) => { ScreenInstruction::DumpLayout(default_shell, client_id) => {
@ -2473,7 +2484,7 @@ pub(crate) fn screen_thread_main(
|tab: &mut Tab, client_id: ClientId| tab.edit_scrollback(client_id), |tab: &mut Tab, client_id: ClientId| tab.edit_scrollback(client_id),
? ?
); );
screen.render()?; screen.render(None)?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::ScrollUp(client_id) => { ScreenInstruction::ScrollUp(client_id) => {
@ -2483,7 +2494,7 @@ pub(crate) fn screen_thread_main(
|tab: &mut Tab, client_id: ClientId| tab.scroll_active_terminal_up(client_id) |tab: &mut Tab, client_id: ClientId| tab.scroll_active_terminal_up(client_id)
); );
screen.unblock_input()?; screen.unblock_input()?;
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::MovePane(client_id) => { ScreenInstruction::MovePane(client_id) => {
active_tab_and_connected_client_id!( active_tab_and_connected_client_id!(
@ -2491,7 +2502,7 @@ pub(crate) fn screen_thread_main(
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.move_active_pane(client_id) |tab: &mut Tab, client_id: ClientId| tab.move_active_pane(client_id)
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
}, },
@ -2501,7 +2512,7 @@ pub(crate) fn screen_thread_main(
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.move_active_pane_backwards(client_id) |tab: &mut Tab, client_id: ClientId| tab.move_active_pane_backwards(client_id)
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
}, },
@ -2511,7 +2522,7 @@ pub(crate) fn screen_thread_main(
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.move_active_pane_down(client_id) |tab: &mut Tab, client_id: ClientId| tab.move_active_pane_down(client_id)
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
}, },
@ -2521,7 +2532,7 @@ pub(crate) fn screen_thread_main(
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.move_active_pane_up(client_id) |tab: &mut Tab, client_id: ClientId| tab.move_active_pane_up(client_id)
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
}, },
@ -2531,7 +2542,7 @@ pub(crate) fn screen_thread_main(
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.move_active_pane_right(client_id) |tab: &mut Tab, client_id: ClientId| tab.move_active_pane_right(client_id)
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
}, },
@ -2541,7 +2552,7 @@ pub(crate) fn screen_thread_main(
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.move_active_pane_left(client_id) |tab: &mut Tab, client_id: ClientId| tab.move_active_pane_left(client_id)
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
}, },
@ -2552,7 +2563,7 @@ pub(crate) fn screen_thread_main(
|tab: &mut Tab, client_id: ClientId| tab |tab: &mut Tab, client_id: ClientId| tab
.handle_scrollwheel_up(&point, 3, client_id), ? .handle_scrollwheel_up(&point, 3, client_id), ?
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::ScrollDown(client_id) => { ScreenInstruction::ScrollDown(client_id) => {
@ -2561,7 +2572,7 @@ pub(crate) fn screen_thread_main(
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.scroll_active_terminal_down(client_id), ? |tab: &mut Tab, client_id: ClientId| tab.scroll_active_terminal_down(client_id), ?
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::ScrollDownAt(point, client_id) => { ScreenInstruction::ScrollDownAt(point, client_id) => {
@ -2571,7 +2582,7 @@ pub(crate) fn screen_thread_main(
|tab: &mut Tab, client_id: ClientId| tab |tab: &mut Tab, client_id: ClientId| tab
.handle_scrollwheel_down(&point, 3, client_id), ? .handle_scrollwheel_down(&point, 3, client_id), ?
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::ScrollToBottom(client_id) => { ScreenInstruction::ScrollToBottom(client_id) => {
@ -2581,7 +2592,7 @@ pub(crate) fn screen_thread_main(
|tab: &mut Tab, client_id: ClientId| tab |tab: &mut Tab, client_id: ClientId| tab
.scroll_active_terminal_to_bottom(client_id), ? .scroll_active_terminal_to_bottom(client_id), ?
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::ScrollToTop(client_id) => { ScreenInstruction::ScrollToTop(client_id) => {
@ -2591,7 +2602,7 @@ pub(crate) fn screen_thread_main(
|tab: &mut Tab, client_id: ClientId| tab |tab: &mut Tab, client_id: ClientId| tab
.scroll_active_terminal_to_top(client_id), ? .scroll_active_terminal_to_top(client_id), ?
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::PageScrollUp(client_id) => { ScreenInstruction::PageScrollUp(client_id) => {
@ -2601,7 +2612,7 @@ pub(crate) fn screen_thread_main(
|tab: &mut Tab, client_id: ClientId| tab |tab: &mut Tab, client_id: ClientId| tab
.scroll_active_terminal_up_page(client_id) .scroll_active_terminal_up_page(client_id)
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::PageScrollDown(client_id) => { ScreenInstruction::PageScrollDown(client_id) => {
@ -2611,7 +2622,7 @@ pub(crate) fn screen_thread_main(
|tab: &mut Tab, client_id: ClientId| tab |tab: &mut Tab, client_id: ClientId| tab
.scroll_active_terminal_down_page(client_id), ? .scroll_active_terminal_down_page(client_id), ?
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::HalfPageScrollUp(client_id) => { ScreenInstruction::HalfPageScrollUp(client_id) => {
@ -2621,7 +2632,7 @@ pub(crate) fn screen_thread_main(
|tab: &mut Tab, client_id: ClientId| tab |tab: &mut Tab, client_id: ClientId| tab
.scroll_active_terminal_up_half_page(client_id) .scroll_active_terminal_up_half_page(client_id)
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::HalfPageScrollDown(client_id) => { ScreenInstruction::HalfPageScrollDown(client_id) => {
@ -2631,7 +2642,7 @@ pub(crate) fn screen_thread_main(
|tab: &mut Tab, client_id: ClientId| tab |tab: &mut Tab, client_id: ClientId| tab
.scroll_active_terminal_down_half_page(client_id), ? .scroll_active_terminal_down_half_page(client_id), ?
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::ClearScroll(client_id) => { ScreenInstruction::ClearScroll(client_id) => {
@ -2641,7 +2652,7 @@ pub(crate) fn screen_thread_main(
|tab: &mut Tab, client_id: ClientId| tab |tab: &mut Tab, client_id: ClientId| tab
.clear_active_terminal_scroll(client_id), ? .clear_active_terminal_scroll(client_id), ?
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::CloseFocusedPane(client_id) => { ScreenInstruction::CloseFocusedPane(client_id) => {
@ -2650,7 +2661,7 @@ pub(crate) fn screen_thread_main(
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.close_focused_pane(client_id), ? |tab: &mut Tab, client_id: ClientId| tab.close_focused_pane(client_id), ?
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
}, },
@ -2666,7 +2677,7 @@ pub(crate) fn screen_thread_main(
|tab| tab.set_pane_selectable(id, selectable), |tab| tab.set_pane_selectable(id, selectable),
); );
screen.render()?; screen.render(None)?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::ClosePane(id, client_id) => { ScreenInstruction::ClosePane(id, client_id) => {
@ -2726,7 +2737,7 @@ pub(crate) fn screen_thread_main(
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.update_active_pane_name(c, client_id), ? |tab: &mut Tab, client_id: ClientId| tab.update_active_pane_name(c, client_id), ?
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
}, },
@ -2736,7 +2747,7 @@ pub(crate) fn screen_thread_main(
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.undo_active_rename_pane(client_id), ? |tab: &mut Tab, client_id: ClientId| tab.undo_active_rename_pane(client_id), ?
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::ToggleActiveTerminalFullscreen(client_id) => { ScreenInstruction::ToggleActiveTerminalFullscreen(client_id) => {
@ -2746,7 +2757,7 @@ pub(crate) fn screen_thread_main(
|tab: &mut Tab, client_id: ClientId| tab |tab: &mut Tab, client_id: ClientId| tab
.toggle_active_pane_fullscreen(client_id) .toggle_active_pane_fullscreen(client_id)
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
}, },
@ -2755,24 +2766,24 @@ pub(crate) fn screen_thread_main(
for tab in screen.tabs.values_mut() { for tab in screen.tabs.values_mut() {
tab.set_pane_frames(screen.draw_pane_frames); tab.set_pane_frames(screen.draw_pane_frames);
} }
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::SwitchTabNext(client_id) => { ScreenInstruction::SwitchTabNext(client_id) => {
screen.switch_tab_next(None, true, client_id)?; screen.switch_tab_next(None, true, client_id)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::SwitchTabPrev(client_id) => { ScreenInstruction::SwitchTabPrev(client_id) => {
screen.switch_tab_prev(None, true, client_id)?; screen.switch_tab_prev(None, true, client_id)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::CloseTab(client_id) => { ScreenInstruction::CloseTab(client_id) => {
screen.close_tab(client_id)?; screen.close_tab(client_id)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::NewTab( ScreenInstruction::NewTab(
cwd, cwd,
@ -2829,13 +2840,13 @@ pub(crate) fn screen_thread_main(
plugin_loading_message_cache.remove(plugin_id) plugin_loading_message_cache.remove(plugin_id)
{ {
screen.update_plugin_loading_stage(*plugin_id, loading_indication); screen.update_plugin_loading_stage(*plugin_id, loading_indication);
screen.render()?; screen.render(None)?;
} }
} }
} }
screen.unblock_input()?; screen.unblock_input()?;
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::GoToTab(tab_index, client_id) => { ScreenInstruction::GoToTab(tab_index, client_id) => {
let client_id_to_switch = if client_id.is_none() { let client_id_to_switch = if client_id.is_none() {
@ -2857,7 +2868,7 @@ pub(crate) fn screen_thread_main(
Some(client_id) if pending_tab_ids.is_empty() => { Some(client_id) if pending_tab_ids.is_empty() => {
screen.go_to_tab(tab_index as usize, client_id)?; screen.go_to_tab(tab_index as usize, client_id)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.render()?; screen.render(None)?;
}, },
_ => { _ => {
if let Some(client_id) = client_id { if let Some(client_id) = client_id {
@ -2886,7 +2897,7 @@ pub(crate) fn screen_thread_main(
if let Some(client_id) = client_id { if let Some(client_id) = client_id {
if let Ok(tab_exists) = screen.go_to_tab_name(tab_name.clone(), client_id) { if let Ok(tab_exists) = screen.go_to_tab_name(tab_name.clone(), client_id) {
screen.unblock_input()?; screen.unblock_input()?;
screen.render()?; screen.render(None)?;
if create && !tab_exists { if create && !tab_exists {
let tab_index = screen.get_new_tab_index(); let tab_index = screen.get_new_tab_index();
screen.new_tab(tab_index, swap_layouts, Some(tab_name), client_id)?; screen.new_tab(tab_index, swap_layouts, Some(tab_name), client_id)?;
@ -2908,17 +2919,17 @@ pub(crate) fn screen_thread_main(
ScreenInstruction::UpdateTabName(c, client_id) => { ScreenInstruction::UpdateTabName(c, client_id) => {
screen.update_active_tab_name(c, client_id)?; screen.update_active_tab_name(c, client_id)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::UndoRenameTab(client_id) => { ScreenInstruction::UndoRenameTab(client_id) => {
screen.undo_active_rename_tab(client_id)?; screen.undo_active_rename_tab(client_id)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::TerminalResize(new_size) => { ScreenInstruction::TerminalResize(new_size) => {
screen.resize_to_screen(new_size)?; screen.resize_to_screen(new_size)?;
screen.log_and_report_session_state()?; // update tabs so that the ui indication will be send to the plugins screen.log_and_report_session_state()?; // update tabs so that the ui indication will be send to the plugins
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::TerminalPixelDimensions(pixel_dimensions) => { ScreenInstruction::TerminalPixelDimensions(pixel_dimensions) => {
screen.update_pixel_dimensions(pixel_dimensions); screen.update_pixel_dimensions(pixel_dimensions);
@ -2934,12 +2945,12 @@ pub(crate) fn screen_thread_main(
}, },
ScreenInstruction::ChangeMode(mode_info, client_id) => { ScreenInstruction::ChangeMode(mode_info, client_id) => {
screen.change_mode(mode_info, client_id)?; screen.change_mode(mode_info, client_id)?;
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::ChangeModeForAllClients(mode_info) => { ScreenInstruction::ChangeModeForAllClients(mode_info) => {
screen.change_mode_for_all_clients(mode_info)?; screen.change_mode_for_all_clients(mode_info)?;
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::ToggleActiveSyncTab(client_id) => { ScreenInstruction::ToggleActiveSyncTab(client_id) => {
@ -2949,65 +2960,65 @@ pub(crate) fn screen_thread_main(
|tab: &mut Tab, _client_id: ClientId| tab.toggle_sync_panes_is_active() |tab: &mut Tab, _client_id: ClientId| tab.toggle_sync_panes_is_active()
); );
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::LeftClick(point, client_id) => { ScreenInstruction::LeftClick(point, client_id) => {
active_tab!(screen, client_id, |tab: &mut Tab| tab active_tab!(screen, client_id, |tab: &mut Tab| tab
.handle_left_click(&point, client_id), ?); .handle_left_click(&point, client_id), ?);
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::RightClick(point, client_id) => { ScreenInstruction::RightClick(point, client_id) => {
active_tab!(screen, client_id, |tab: &mut Tab| tab active_tab!(screen, client_id, |tab: &mut Tab| tab
.handle_right_click(&point, client_id), ?); .handle_right_click(&point, client_id), ?);
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::MiddleClick(point, client_id) => { ScreenInstruction::MiddleClick(point, client_id) => {
active_tab!(screen, client_id, |tab: &mut Tab| tab active_tab!(screen, client_id, |tab: &mut Tab| tab
.handle_middle_click(&point, client_id), ?); .handle_middle_click(&point, client_id), ?);
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::LeftMouseRelease(point, client_id) => { ScreenInstruction::LeftMouseRelease(point, client_id) => {
active_tab!(screen, client_id, |tab: &mut Tab| tab active_tab!(screen, client_id, |tab: &mut Tab| tab
.handle_left_mouse_release(&point, client_id), ?); .handle_left_mouse_release(&point, client_id), ?);
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::RightMouseRelease(point, client_id) => { ScreenInstruction::RightMouseRelease(point, client_id) => {
active_tab!(screen, client_id, |tab: &mut Tab| tab active_tab!(screen, client_id, |tab: &mut Tab| tab
.handle_right_mouse_release(&point, client_id), ?); .handle_right_mouse_release(&point, client_id), ?);
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::MiddleMouseRelease(point, client_id) => { ScreenInstruction::MiddleMouseRelease(point, client_id) => {
active_tab!(screen, client_id, |tab: &mut Tab| tab active_tab!(screen, client_id, |tab: &mut Tab| tab
.handle_middle_mouse_release(&point, client_id), ?); .handle_middle_mouse_release(&point, client_id), ?);
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::MouseHoldLeft(point, client_id) => { ScreenInstruction::MouseHoldLeft(point, client_id) => {
active_tab!(screen, client_id, |tab: &mut Tab| tab active_tab!(screen, client_id, |tab: &mut Tab| tab
.handle_mouse_hold_left(&point, client_id), ?); .handle_mouse_hold_left(&point, client_id), ?);
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::MouseHoldRight(point, client_id) => { ScreenInstruction::MouseHoldRight(point, client_id) => {
active_tab!(screen, client_id, |tab: &mut Tab| tab active_tab!(screen, client_id, |tab: &mut Tab| tab
.handle_mouse_hold_right(&point, client_id), ?); .handle_mouse_hold_right(&point, client_id), ?);
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::MouseHoldMiddle(point, client_id) => { ScreenInstruction::MouseHoldMiddle(point, client_id) => {
active_tab!(screen, client_id, |tab: &mut Tab| tab active_tab!(screen, client_id, |tab: &mut Tab| tab
.handle_mouse_hold_middle(&point, client_id), ?); .handle_mouse_hold_middle(&point, client_id), ?);
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::Copy(client_id) => { ScreenInstruction::Copy(client_id) => {
active_tab!(screen, client_id, |tab: &mut Tab| tab active_tab!(screen, client_id, |tab: &mut Tab| tab
.copy_selection(client_id), ?); .copy_selection(client_id), ?);
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::Exit => { ScreenInstruction::Exit => {
break; break;
@ -3015,7 +3026,7 @@ pub(crate) fn screen_thread_main(
ScreenInstruction::ToggleTab(client_id) => { ScreenInstruction::ToggleTab(client_id) => {
screen.toggle_tab(client_id)?; screen.toggle_tab(client_id)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::AddClient(client_id, tab_position_to_focus, pane_id_to_focus) => { ScreenInstruction::AddClient(client_id, tab_position_to_focus, pane_id_to_focus) => {
screen.add_client(client_id)?; screen.add_client(client_id)?;
@ -3032,12 +3043,12 @@ pub(crate) fn screen_thread_main(
screen.go_to_tab(tab_position_to_focus, client_id)?; screen.go_to_tab(tab_position_to_focus, client_id)?;
} }
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::RemoveClient(client_id) => { ScreenInstruction::RemoveClient(client_id) => {
screen.remove_client(client_id)?; screen.remove_client(client_id)?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::AddOverlay(overlay, _client_id) => { ScreenInstruction::AddOverlay(overlay, _client_id) => {
screen.get_active_overlays_mut().pop(); screen.get_active_overlays_mut().pop();
@ -3046,7 +3057,7 @@ pub(crate) fn screen_thread_main(
}, },
ScreenInstruction::RemoveOverlay(_client_id) => { ScreenInstruction::RemoveOverlay(_client_id) => {
screen.get_active_overlays_mut().pop(); screen.get_active_overlays_mut().pop();
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::ConfirmPrompt(_client_id) => { ScreenInstruction::ConfirmPrompt(_client_id) => {
@ -3063,7 +3074,7 @@ pub(crate) fn screen_thread_main(
}, },
ScreenInstruction::DenyPrompt(_client_id) => { ScreenInstruction::DenyPrompt(_client_id) => {
screen.get_active_overlays_mut().pop(); screen.get_active_overlays_mut().pop();
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::UpdateSearch(c, client_id) => { ScreenInstruction::UpdateSearch(c, client_id) => {
@ -3072,7 +3083,7 @@ pub(crate) fn screen_thread_main(
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.update_search_term(c, client_id), ? |tab: &mut Tab, client_id: ClientId| tab.update_search_term(c, client_id), ?
); );
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::SearchDown(client_id) => { ScreenInstruction::SearchDown(client_id) => {
active_tab_and_connected_client_id!( active_tab_and_connected_client_id!(
@ -3080,7 +3091,7 @@ pub(crate) fn screen_thread_main(
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.search_down(client_id) |tab: &mut Tab, client_id: ClientId| tab.search_down(client_id)
); );
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::SearchUp(client_id) => { ScreenInstruction::SearchUp(client_id) => {
active_tab_and_connected_client_id!( active_tab_and_connected_client_id!(
@ -3088,7 +3099,7 @@ pub(crate) fn screen_thread_main(
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.search_up(client_id) |tab: &mut Tab, client_id: ClientId| tab.search_up(client_id)
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::SearchToggleCaseSensitivity(client_id) => { ScreenInstruction::SearchToggleCaseSensitivity(client_id) => {
@ -3098,7 +3109,7 @@ pub(crate) fn screen_thread_main(
|tab: &mut Tab, client_id: ClientId| tab |tab: &mut Tab, client_id: ClientId| tab
.toggle_search_case_sensitivity(client_id) .toggle_search_case_sensitivity(client_id)
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::SearchToggleWrap(client_id) => { ScreenInstruction::SearchToggleWrap(client_id) => {
@ -3107,7 +3118,7 @@ pub(crate) fn screen_thread_main(
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.toggle_search_wrap(client_id) |tab: &mut Tab, client_id: ClientId| tab.toggle_search_wrap(client_id)
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::SearchToggleWholeWord(client_id) => { ScreenInstruction::SearchToggleWholeWord(client_id) => {
@ -3116,7 +3127,7 @@ pub(crate) fn screen_thread_main(
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.toggle_search_whole_words(client_id) |tab: &mut Tab, client_id: ClientId| tab.toggle_search_whole_words(client_id)
); );
screen.render()?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::AddRedPaneFrameColorOverride(pane_ids, error_text) => { ScreenInstruction::AddRedPaneFrameColorOverride(pane_ids, error_text) => {
@ -3129,7 +3140,7 @@ pub(crate) fn screen_thread_main(
} }
} }
} }
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::ClearPaneFrameColorOverride(pane_ids) => { ScreenInstruction::ClearPaneFrameColorOverride(pane_ids) => {
let all_tabs = screen.get_tabs_mut(); let all_tabs = screen.get_tabs_mut();
@ -3141,7 +3152,7 @@ pub(crate) fn screen_thread_main(
} }
} }
} }
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::PreviousSwapLayout(client_id) => { ScreenInstruction::PreviousSwapLayout(client_id) => {
active_tab_and_connected_client_id!( active_tab_and_connected_client_id!(
@ -3150,7 +3161,7 @@ pub(crate) fn screen_thread_main(
|tab: &mut Tab, client_id: ClientId| tab.previous_swap_layout(Some(client_id)), |tab: &mut Tab, client_id: ClientId| tab.previous_swap_layout(Some(client_id)),
? ?
); );
screen.render()?; screen.render(None)?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
@ -3161,7 +3172,7 @@ pub(crate) fn screen_thread_main(
|tab: &mut Tab, client_id: ClientId| tab.next_swap_layout(Some(client_id), true), |tab: &mut Tab, client_id: ClientId| tab.next_swap_layout(Some(client_id), true),
? ?
); );
screen.render()?; screen.render(None)?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
@ -3325,7 +3336,19 @@ pub(crate) fn screen_thread_main(
} else { } else {
log::error!("Must have pane id to replace or connected client_id if replacing a pane"); log::error!("Must have pane id to replace or connected client_id if replacing a pane");
} }
} else if let Some(active_tab) = screen.tabs.get_mut(&tab_index) { } else if let Some(client_id) = client_id {
active_tab!(screen, client_id, |active_tab: &mut Tab| {
active_tab.new_pane(
PaneId::Plugin(plugin_id),
Some(pane_title),
should_float,
Some(run_plugin),
None,
)
}, ?);
} else if let Some(active_tab) =
tab_index.and_then(|tab_index| screen.tabs.get_mut(&tab_index))
{
active_tab.new_pane( active_tab.new_pane(
PaneId::Plugin(plugin_id), PaneId::Plugin(plugin_id),
Some(pane_title), Some(pane_title),
@ -3338,7 +3361,7 @@ pub(crate) fn screen_thread_main(
} }
if let Some(loading_indication) = plugin_loading_message_cache.remove(&plugin_id) { if let Some(loading_indication) = plugin_loading_message_cache.remove(&plugin_id) {
screen.update_plugin_loading_stage(plugin_id, loading_indication); screen.update_plugin_loading_stage(plugin_id, loading_indication);
screen.render()?; screen.render(None)?;
} }
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
screen.unblock_input()?; screen.unblock_input()?;
@ -3349,7 +3372,7 @@ pub(crate) fn screen_thread_main(
if !found_plugin { if !found_plugin {
plugin_loading_message_cache.insert(pid, loading_indication); plugin_loading_message_cache.insert(pid, loading_indication);
} }
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::StartPluginLoadingIndication(pid, loading_indication) => { ScreenInstruction::StartPluginLoadingIndication(pid, loading_indication) => {
let all_tabs = screen.get_tabs_mut(); let all_tabs = screen.get_tabs_mut();
@ -3359,7 +3382,7 @@ pub(crate) fn screen_thread_main(
break; break;
} }
} }
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::ProgressPluginLoadingOffset(pid) => { ScreenInstruction::ProgressPluginLoadingOffset(pid) => {
let all_tabs = screen.get_tabs_mut(); let all_tabs = screen.get_tabs_mut();
@ -3369,7 +3392,7 @@ pub(crate) fn screen_thread_main(
break; break;
} }
} }
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::RequestStateUpdateForPlugins => { ScreenInstruction::RequestStateUpdateForPlugins => {
let all_tabs = screen.get_tabs_mut(); let all_tabs = screen.get_tabs_mut();
@ -3377,7 +3400,7 @@ pub(crate) fn screen_thread_main(
tab.update_input_modes()?; tab.update_input_modes()?;
} }
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::LaunchOrFocusPlugin( ScreenInstruction::LaunchOrFocusPlugin(
run_plugin, run_plugin,
@ -3432,7 +3455,7 @@ pub(crate) fn screen_thread_main(
move_to_focused_tab, move_to_focused_tab,
client_id, client_id,
)? { )? {
screen.render()?; screen.render(None)?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
} else { } else {
screen screen
@ -3529,7 +3552,7 @@ pub(crate) fn screen_thread_main(
for tab in all_tabs.values_mut() { for tab in all_tabs.values_mut() {
if tab.has_non_suppressed_pane_with_pid(&pane_id) { if tab.has_non_suppressed_pane_with_pid(&pane_id) {
tab.suppress_pane(pane_id, client_id); tab.suppress_pane(pane_id, client_id);
drop(screen.render()); drop(screen.render(None));
break; break;
} }
} }
@ -3544,7 +3567,7 @@ pub(crate) fn screen_thread_main(
for tab in all_tabs.values_mut() { for tab in all_tabs.values_mut() {
if tab.has_pane_with_pid(&pane_id) { if tab.has_pane_with_pid(&pane_id) {
match tab.rename_pane(new_name, pane_id) { match tab.rename_pane(new_name, pane_id) {
Ok(()) => drop(screen.render()), Ok(()) => drop(screen.render(None)),
Err(e) => log::error!("Failed to rename pane: {:?}", e), Err(e) => log::error!("Failed to rename pane: {:?}", e),
} }
break; break;
@ -3611,7 +3634,7 @@ pub(crate) fn screen_thread_main(
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
screen.render()?; screen.render(None)?;
}, },
ScreenInstruction::DumpLayoutToHd => { ScreenInstruction::DumpLayoutToHd => {
if screen.session_serialization { if screen.session_serialization {

View file

@ -125,6 +125,7 @@ fn send_cli_action_to_server(
client_attributes.clone(), client_attributes.clone(),
default_shell.clone(), default_shell.clone(),
default_layout.clone(), default_layout.clone(),
None,
) )
.unwrap(); .unwrap();
} }

View file

@ -21,7 +21,7 @@ pub mod ui_components;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use zellij_utils::data::Event; use zellij_utils::data::{Event, PipeMessage};
// use zellij_tile::shim::plugin_api::event::ProtobufEvent; // use zellij_tile::shim::plugin_api::event::ProtobufEvent;
@ -37,6 +37,12 @@ pub trait ZellijPlugin: Default {
fn update(&mut self, event: Event) -> bool { fn update(&mut self, event: Event) -> bool {
false false
} // return true if it should render } // return true if it should render
/// Will be called when data is being piped to the plugin, a PipeMessage.payload of None signifies the pipe
/// has ended
/// If the plugin returns `true` from this function, Zellij will know it should be rendered and call its `render` function.
fn pipe(&mut self, pipe_message: PipeMessage) -> bool {
false
} // return true if it should render
/// Will be called either after an `update` that requested it, or when the plugin otherwise needs to be re-rendered (eg. on startup, or when the plugin is resized). /// Will be called either after an `update` that requested it, or when the plugin otherwise needs to be re-rendered (eg. on startup, or when the plugin is resized).
/// The `rows` and `cols` values represent the "content size" of the plugin (this will not include its surrounding frame if the user has pane frames enabled). /// The `rows` and `cols` values represent the "content size" of the plugin (this will not include its surrounding frame if the user has pane frames enabled).
fn render(&mut self, rows: usize, cols: usize) {} fn render(&mut self, rows: usize, cols: usize) {}
@ -136,6 +142,21 @@ macro_rules! register_plugin {
}) })
} }
#[no_mangle]
pub fn pipe() -> bool {
let err_context = "Failed to deserialize pipe message";
use std::convert::TryInto;
use zellij_tile::shim::plugin_api::pipe_message::ProtobufPipeMessage;
use zellij_tile::shim::prost::Message;
STATE.with(|state| {
let protobuf_bytes: Vec<u8> = $crate::shim::object_from_stdin().unwrap();
let protobuf_pipe_message: ProtobufPipeMessage =
ProtobufPipeMessage::decode(protobuf_bytes.as_slice()).unwrap();
let pipe_message = protobuf_pipe_message.try_into().unwrap();
state.borrow_mut().pipe(pipe_message)
})
}
#[no_mangle] #[no_mangle]
pub fn render(rows: i32, cols: i32) { pub fn render(rows: i32, cols: i32) {
STATE.with(|state| { STATE.with(|state| {

View file

@ -694,6 +694,38 @@ pub fn rename_session(name: &str) {
unsafe { host_run_plugin_command() }; unsafe { host_run_plugin_command() };
} }
/// Unblock the input side of a pipe, requesting the next message be sent if there is one
pub fn unblock_cli_pipe_input(pipe_name: &str) {
let plugin_command = PluginCommand::UnblockCliPipeInput(pipe_name.to_owned());
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
unsafe { host_run_plugin_command() };
}
/// Block the input side of a pipe, will only be released once this or another plugin unblocks it
pub fn block_cli_pipe_input(pipe_name: &str) {
let plugin_command = PluginCommand::BlockCliPipeInput(pipe_name.to_owned());
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
unsafe { host_run_plugin_command() };
}
/// Send output to the output side of a pipe, ths does not affect the input side of same pipe
pub fn cli_pipe_output(pipe_name: &str, output: &str) {
let plugin_command = PluginCommand::CliPipeOutput(pipe_name.to_owned(), output.to_owned());
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
unsafe { host_run_plugin_command() };
}
/// Send a message to a plugin, it will be launched if it is not already running
pub fn pipe_message_to_plugin(message_to_plugin: MessageToPlugin) {
let plugin_command = PluginCommand::MessageToPlugin(message_to_plugin);
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
unsafe { host_run_plugin_command() };
}
// Utility Functions // Utility Functions
#[allow(unused)] #[allow(unused)]

View file

@ -5,7 +5,7 @@ pub struct Action {
pub name: i32, pub name: i32,
#[prost( #[prost(
oneof = "action::OptionalPayload", 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<action::OptionalPayload>, pub optional_payload: ::core::option::Option<action::OptionalPayload>,
} }
@ -104,10 +104,24 @@ pub mod action {
RenameSessionPayload(::prost::alloc::string::String), RenameSessionPayload(::prost::alloc::string::String),
#[prost(message, tag = "46")] #[prost(message, tag = "46")]
LaunchPluginPayload(super::LaunchOrFocusPluginPayload), LaunchPluginPayload(super::LaunchOrFocusPluginPayload),
#[prost(message, tag = "47")]
MessagePayload(super::CliPipePayload),
} }
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[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<NameAndValue>,
#[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 { pub struct IdAndName {
#[prost(bytes = "vec", tag = "1")] #[prost(bytes = "vec", tag = "1")]
pub name: ::prost::alloc::vec::Vec<u8>, pub name: ::prost::alloc::vec::Vec<u8>,
@ -410,6 +424,7 @@ pub enum ActionName {
BreakPaneLeft = 79, BreakPaneLeft = 79,
RenameSession = 80, RenameSession = 80,
LaunchPlugin = 81, LaunchPlugin = 81,
CliPipe = 82,
} }
impl ActionName { impl ActionName {
/// String value of the enum field names used in the ProtoBuf definition. /// String value of the enum field names used in the ProtoBuf definition.
@ -500,6 +515,7 @@ impl ActionName {
ActionName::BreakPaneLeft => "BreakPaneLeft", ActionName::BreakPaneLeft => "BreakPaneLeft",
ActionName::RenameSession => "RenameSession", ActionName::RenameSession => "RenameSession",
ActionName::LaunchPlugin => "LaunchPlugin", ActionName::LaunchPlugin => "LaunchPlugin",
ActionName::CliPipe => "CliPipe",
} }
} }
/// Creates an enum from field names used in the ProtoBuf definition. /// Creates an enum from field names used in the ProtoBuf definition.
@ -587,6 +603,7 @@ impl ActionName {
"BreakPaneLeft" => Some(Self::BreakPaneLeft), "BreakPaneLeft" => Some(Self::BreakPaneLeft),
"RenameSession" => Some(Self::RenameSession), "RenameSession" => Some(Self::RenameSession),
"LaunchPlugin" => Some(Self::LaunchPlugin), "LaunchPlugin" => Some(Self::LaunchPlugin),
"CliPipe" => Some(Self::CliPipe),
_ => None, _ => None,
} }
} }

View file

@ -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<u32>,
#[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<Arg>,
#[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<Self> {
match value {
"Cli" => Some(Self::Cli),
"Plugin" => Some(Self::Plugin),
_ => None,
}
}
}

View file

@ -5,7 +5,7 @@ pub struct PluginCommand {
pub name: i32, pub name: i32,
#[prost( #[prost(
oneof = "plugin_command::Payload", 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<plugin_command::Payload>, pub payload: ::core::option::Option<plugin_command::Payload>,
} }
@ -104,10 +104,64 @@ pub mod plugin_command {
DeleteDeadSessionPayload(::prost::alloc::string::String), DeleteDeadSessionPayload(::prost::alloc::string::String),
#[prost(string, tag = "46")] #[prost(string, tag = "46")]
RenameSessionPayload(::prost::alloc::string::String), 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)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[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<ContextItem>,
#[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<ContextItem>,
#[prost(message, optional, tag = "6")]
pub new_plugin_args: ::core::option::Option<NewPluginArgs>,
}
#[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<bool>,
#[prost(message, optional, tag = "2")]
pub pane_id_to_replace: ::core::option::Option<PaneId>,
#[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 { pub struct SwitchSessionPayload {
#[prost(string, optional, tag = "1")] #[prost(string, optional, tag = "1")]
pub name: ::core::option::Option<::prost::alloc::string::String>, pub name: ::core::option::Option<::prost::alloc::string::String>,
@ -318,6 +372,10 @@ pub enum CommandName {
DeleteDeadSession = 73, DeleteDeadSession = 73,
DeleteAllDeadSessions = 74, DeleteAllDeadSessions = 74,
RenameSession = 75, RenameSession = 75,
UnblockCliPipeInput = 76,
BlockCliPipeInput = 77,
CliPipeOutput = 78,
MessageToPlugin = 79,
} }
impl CommandName { impl CommandName {
/// String value of the enum field names used in the ProtoBuf definition. /// String value of the enum field names used in the ProtoBuf definition.
@ -402,6 +460,10 @@ impl CommandName {
CommandName::DeleteDeadSession => "DeleteDeadSession", CommandName::DeleteDeadSession => "DeleteDeadSession",
CommandName::DeleteAllDeadSessions => "DeleteAllDeadSessions", CommandName::DeleteAllDeadSessions => "DeleteAllDeadSessions",
CommandName::RenameSession => "RenameSession", 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. /// Creates an enum from field names used in the ProtoBuf definition.
@ -483,6 +545,36 @@ impl CommandName {
"DeleteDeadSession" => Some(Self::DeleteDeadSession), "DeleteDeadSession" => Some(Self::DeleteDeadSession),
"DeleteAllDeadSessions" => Some(Self::DeleteAllDeadSessions), "DeleteAllDeadSessions" => Some(Self::DeleteAllDeadSessions),
"RenameSession" => Some(Self::RenameSession), "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<Self> {
match value {
"Terminal" => Some(Self::Terminal),
"Plugin" => Some(Self::Plugin),
_ => None, _ => None,
} }
} }

View file

@ -8,6 +8,8 @@ pub enum PermissionType {
OpenTerminalsOrPlugins = 4, OpenTerminalsOrPlugins = 4,
WriteToStdin = 5, WriteToStdin = 5,
WebAccess = 6, WebAccess = 6,
ReadCliPipes = 7,
MessageAndLaunchOtherPlugins = 8,
} }
impl PermissionType { impl PermissionType {
/// String value of the enum field names used in the ProtoBuf definition. /// String value of the enum field names used in the ProtoBuf definition.
@ -23,6 +25,10 @@ impl PermissionType {
PermissionType::OpenTerminalsOrPlugins => "OpenTerminalsOrPlugins", PermissionType::OpenTerminalsOrPlugins => "OpenTerminalsOrPlugins",
PermissionType::WriteToStdin => "WriteToStdin", PermissionType::WriteToStdin => "WriteToStdin",
PermissionType::WebAccess => "WebAccess", PermissionType::WebAccess => "WebAccess",
PermissionType::ReadCliPipes => "ReadCliPipes",
PermissionType::MessageAndLaunchOtherPlugins => {
"MessageAndLaunchOtherPlugins"
}
} }
} }
/// Creates an enum from field names used in the ProtoBuf definition. /// Creates an enum from field names used in the ProtoBuf definition.
@ -35,6 +41,8 @@ impl PermissionType {
"OpenTerminalsOrPlugins" => Some(Self::OpenTerminalsOrPlugins), "OpenTerminalsOrPlugins" => Some(Self::OpenTerminalsOrPlugins),
"WriteToStdin" => Some(Self::WriteToStdin), "WriteToStdin" => Some(Self::WriteToStdin),
"WebAccess" => Some(Self::WebAccess), "WebAccess" => Some(Self::WebAccess),
"ReadCliPipes" => Some(Self::ReadCliPipes),
"MessageAndLaunchOtherPlugins" => Some(Self::MessageAndLaunchOtherPlugins),
_ => None, _ => None,
} }
} }

View file

@ -20,6 +20,9 @@ pub mod api {
pub mod message { pub mod message {
include!("api.message.rs"); include!("api.message.rs");
} }
pub mod pipe_message {
include!("api.pipe_message.rs");
}
pub mod plugin_command { pub mod plugin_command {
include!("api.plugin_command.rs"); include!("api.plugin_command.rs");
} }

View file

@ -289,6 +289,43 @@ pub enum Sessions {
ConvertTheme { ConvertTheme {
old_theme_file: PathBuf, 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] [--] <PAYLOAD>
* 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<String>,
/// The data to send down this pipe (if blank, will listen to STDIN)
payload: Option<String>,
#[clap(short, long, value_parser, display_order(2))]
/// The args of the pipe
args: Option<PluginUserConfiguration>, // 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<String>,
/// 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<PluginUserConfiguration>,
},
} }
#[derive(Debug, Subcommand, Clone, Serialize, Deserialize)] #[derive(Debug, Subcommand, Clone, Serialize, Deserialize)]
@ -549,4 +586,79 @@ pub enum CliAction {
RenameSession { RenameSession {
name: String, name: String,
}, },
/// Send data to one or more plugins, launch them if they are not running.
#[clap(override_usage(
r#"
zellij action pipe [OPTIONS] [--] <PAYLOAD>
* 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<String>,
/// The data to send down this pipe (if blank, will listen to STDIN)
payload: Option<String>,
#[clap(short, long, value_parser, display_order(2))]
/// The args of the pipe
args: Option<PluginUserConfiguration>, // 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<String>,
/// 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<PluginUserConfiguration>,
/// 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<bool>,
/// 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<bool>,
/// If launching a plugin, specify its working directory
#[clap(short('w'), long, value_parser, display_order(9))]
plugin_cwd: Option<PathBuf>,
/// If launching a plugin, specify its pane title
#[clap(short('t'), long, value_parser, display_order(10))]
plugin_title: Option<String>,
},
} }

View file

@ -538,6 +538,8 @@ pub enum Permission {
OpenTerminalsOrPlugins, OpenTerminalsOrPlugins,
WriteToStdin, WriteToStdin,
WebAccess, WebAccess,
ReadCliPipes,
MessageAndLaunchOtherPlugins,
} }
impl PermissionType { impl PermissionType {
@ -554,6 +556,10 @@ impl PermissionType {
PermissionType::OpenTerminalsOrPlugins => "Start new terminals and plugins".to_owned(), PermissionType::OpenTerminalsOrPlugins => "Start new terminals and plugins".to_owned(),
PermissionType::WriteToStdin => "Write to standard input (STDIN)".to_owned(), PermissionType::WriteToStdin => "Write to standard input (STDIN)".to_owned(),
PermissionType::WebAccess => "Make web requests".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<String>,
pub plugin_config: BTreeMap<String, String>,
pub message_name: String,
pub message_payload: Option<String>,
pub message_args: BTreeMap<String, String>,
/// 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<NewPluginArgs>,
}
#[derive(Debug, Default, Clone)]
pub struct NewPluginArgs {
pub should_float: Option<bool>,
pub pane_id_to_replace: Option<PaneId>,
pub pane_title: Option<String>,
pub cwd: Option<PathBuf>,
pub skip_cache: bool,
}
#[derive(Debug, Clone, Copy)]
pub enum PaneId {
Terminal(u32),
Plugin(u32),
}
impl MessageToPlugin {
pub fn new(message_name: impl Into<String>) -> Self {
MessageToPlugin {
message_name: message_name.into(),
..Default::default()
}
}
pub fn with_plugin_url(mut self, url: impl Into<String>) -> Self {
self.plugin_url = Some(url.into());
self
}
pub fn with_plugin_config(mut self, plugin_config: BTreeMap<String, String>) -> Self {
self.plugin_config = plugin_config;
self
}
pub fn with_payload(mut self, payload: impl Into<String>) -> Self {
self.message_payload = Some(payload.into());
self
}
pub fn with_args(mut self, args: BTreeMap<String, String>) -> 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<String>,
) -> 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)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct ConnectToSession { pub struct ConnectToSession {
pub name: Option<String>, pub name: Option<String>,
@ -1014,6 +1100,39 @@ pub enum HttpVerb {
Delete, 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<String>,
pub args: BTreeMap<String, String>,
pub is_private: bool,
}
impl PipeMessage {
pub fn new(
source: PipeSource,
name: impl Into<String>,
payload: &Option<String>,
args: &Option<BTreeMap<String, String>>,
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)] #[derive(Debug, Clone, EnumDiscriminants, ToString)]
#[strum_discriminants(derive(EnumString, Hash, Serialize, Deserialize))] #[strum_discriminants(derive(EnumString, Hash, Serialize, Deserialize))]
#[strum_discriminants(name(CommandType))] #[strum_discriminants(name(CommandType))]
@ -1105,4 +1224,8 @@ pub enum PluginCommand {
BTreeMap<String, String>, // context BTreeMap<String, String>, // 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),
} }

View file

@ -393,6 +393,11 @@ pub enum PluginContext {
PermissionRequestResult, PermissionRequestResult,
DumpLayout, DumpLayout,
LogLayoutToHd, LogLayoutToHd,
CliPipe,
Message,
CachePluginEvents,
MessageFromPlugin,
UnblockCliPipes,
} }
/// Stack call representations corresponding to the different types of [`ClientInstruction`]s. /// Stack call representations corresponding to the different types of [`ClientInstruction`]s.
@ -413,6 +418,8 @@ pub enum ClientContext {
DoneParsingStdinQuery, DoneParsingStdinQuery,
SwitchSession, SwitchSession,
SetSynchronisedOutput, SetSynchronisedOutput,
UnblockCliPipeInput,
CliPipeOutput,
} }
/// Stack call representations corresponding to the different types of [`ServerInstruction`]s. /// Stack call representations corresponding to the different types of [`ServerInstruction`]s.
@ -430,7 +437,11 @@ pub enum ServerContext {
ConnStatus, ConnStatus,
ActiveClients, ActiveClients,
Log, Log,
LogError,
SwitchSession, SwitchSession,
UnblockCliPipeInput,
CliPipeOutput,
AssociatePipeWithClient,
} }
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]

View file

@ -13,6 +13,8 @@ use crate::input::config::{Config, ConfigError, KdlError};
use crate::input::options::OnForceClose; use crate::input::options::OnForceClose;
use miette::{NamedSource, Report}; use miette::{NamedSource, Report};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use uuid::Uuid;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
@ -256,6 +258,20 @@ pub enum Action {
BreakPaneRight, BreakPaneRight,
BreakPaneLeft, BreakPaneLeft,
RenameSession(String), RenameSession(String),
CliPipe {
pipe_id: String,
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 { impl Action {
@ -582,6 +598,41 @@ impl Action {
)]) )])
}, },
CliAction::RenameSession { name } => Ok(vec![Action::RenameSession(name)]), 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,
}])
},
} }
} }
} }

View file

@ -265,6 +265,9 @@ impl PluginUserConfiguration {
pub fn inner(&self) -> &BTreeMap<String, String> { pub fn inner(&self) -> &BTreeMap<String, String> {
&self.0 &self.0
} }
pub fn insert(&mut self, config_key: impl Into<String>, config_value: impl Into<String>) {
self.0.insert(config_key.into(), config_value.into());
}
} }
impl FromStr for PluginUserConfiguration { impl FromStr for PluginUserConfiguration {

View file

@ -103,6 +103,8 @@ pub enum ServerToClientMsg {
Log(Vec<String>), Log(Vec<String>),
LogError(Vec<String>), LogError(Vec<String>),
SwitchSession(ConnectToSession), SwitchSession(ConnectToSession),
UnblockCliPipeInput(String), // String -> pipe name
CliPipeOutput(String, String), // String -> pipe name, String -> Output
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]

View file

@ -27,7 +27,7 @@ pub mod logging; // Requires log4rs
pub use ::{ pub use ::{
anyhow, async_channel, async_std, clap, common_path, humantime, interprocess, lazy_static, 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, libc, miette, nix, notify_debouncer_full, regex, serde, signal_hook, surf, tempfile, termwiz,
vte, url, uuid, vte,
}; };
pub use ::prost; pub use ::prost;

View file

@ -53,9 +53,17 @@ message Action {
IdAndName rename_tab_payload = 44; IdAndName rename_tab_payload = 44;
string rename_session_payload = 45; string rename_session_payload = 45;
LaunchOrFocusPluginPayload launch_plugin_payload = 46; 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 { message IdAndName {
bytes name = 1; bytes name = 1;
uint32 id = 2; uint32 id = 2;
@ -227,6 +235,7 @@ enum ActionName {
BreakPaneLeft = 79; BreakPaneLeft = 79;
RenameSession = 80; RenameSession = 80;
LaunchPlugin = 81; LaunchPlugin = 81;
CliPipe = 82;
} }
message Position { message Position {

View file

@ -1246,6 +1246,7 @@ impl TryFrom<Action> for ProtobufAction {
| Action::Deny | Action::Deny
| Action::Copy | Action::Copy
| Action::DumpLayout | Action::DumpLayout
| Action::CliPipe { .. }
| Action::SkipConfirm(..) => Err("Unsupported action"), | Action::SkipConfirm(..) => Err("Unsupported action"),
} }
} }

View file

@ -5,6 +5,7 @@ pub mod file;
pub mod input_mode; pub mod input_mode;
pub mod key; pub mod key;
pub mod message; pub mod message;
pub mod pipe_message;
pub mod plugin_command; pub mod plugin_command;
pub mod plugin_ids; pub mod plugin_ids;
pub mod plugin_permission; pub mod plugin_permission;

View file

@ -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;
}

View file

@ -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<ProtobufPipeMessage> for PipeMessage {
type Error = &'static str;
fn try_from(protobuf_pipe_message: ProtobufPipeMessage) -> Result<Self, &'static str> {
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<PipeMessage> for ProtobufPipeMessage {
type Error = &'static str;
fn try_from(pipe_message: PipeMessage) -> Result<Self, &'static str> {
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,
})
}
}

View file

@ -87,6 +87,10 @@ enum CommandName {
DeleteDeadSession = 73; DeleteDeadSession = 73;
DeleteAllDeadSessions = 74; DeleteAllDeadSessions = 74;
RenameSession = 75; RenameSession = 75;
UnblockCliPipeInput = 76;
BlockCliPipeInput = 77;
CliPipeOutput = 78;
MessageToPlugin = 79;
} }
message PluginCommand { message PluginCommand {
@ -137,9 +141,45 @@ message PluginCommand {
WebRequestPayload web_request_payload = 44; WebRequestPayload web_request_payload = 44;
string delete_dead_session_payload = 45; string delete_dead_session_payload = 45;
string rename_session_payload = 46; 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 { message SwitchSessionPayload {
optional string name = 1; optional string name = 1;
optional uint32 tab_position = 2; optional uint32 tab_position = 2;

View file

@ -3,9 +3,11 @@ pub use super::generated_api::api::{
event::{EventNameList as ProtobufEventNameList, Header}, event::{EventNameList as ProtobufEventNameList, Header},
input_mode::InputMode as ProtobufInputMode, input_mode::InputMode as ProtobufInputMode,
plugin_command::{ plugin_command::{
plugin_command::Payload, CommandName, ContextItem, EnvVariable, ExecCmdPayload, plugin_command::Payload, CliPipeOutputPayload, CommandName, ContextItem, EnvVariable,
HttpVerb as ProtobufHttpVerb, IdAndNewName, MovePayload, OpenCommandPanePayload, ExecCmdPayload, HttpVerb as ProtobufHttpVerb, IdAndNewName, MessageToPluginPayload,
OpenFilePayload, PluginCommand as ProtobufPluginCommand, PluginMessagePayload, MovePayload, NewPluginArgs as ProtobufNewPluginArgs, OpenCommandPanePayload,
OpenFilePayload, PaneId as ProtobufPaneId, PaneType as ProtobufPaneType,
PluginCommand as ProtobufPluginCommand, PluginMessagePayload,
RequestPluginPermissionPayload, ResizePayload, RunCommandPayload, SetTimeoutPayload, RequestPluginPermissionPayload, ResizePayload, RunCommandPayload, SetTimeoutPayload,
SubscribePayload, SwitchSessionPayload, SwitchTabToPayload, UnsubscribePayload, SubscribePayload, SwitchSessionPayload, SwitchTabToPayload, UnsubscribePayload,
WebRequestPayload, WebRequestPayload,
@ -14,7 +16,10 @@ pub use super::generated_api::api::{
resize::ResizeAction as ProtobufResizeAction, 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::collections::BTreeMap;
use std::convert::TryFrom; use std::convert::TryFrom;
@ -42,6 +47,33 @@ impl Into<ProtobufHttpVerb> for HttpVerb {
} }
} }
impl TryFrom<ProtobufPaneId> for PaneId {
type Error = &'static str;
fn try_from(protobuf_pane_id: ProtobufPaneId) -> Result<Self, &'static str> {
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<PaneId> for ProtobufPaneId {
type Error = &'static str;
fn try_from(pane_id: PaneId) -> Result<Self, &'static str> {
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<ProtobufPluginCommand> for PluginCommand { impl TryFrom<ProtobufPluginCommand> for PluginCommand {
type Error = &'static str; type Error = &'static str;
fn try_from(protobuf_plugin_command: ProtobufPluginCommand) -> Result<Self, &'static str> { fn try_from(protobuf_plugin_command: ProtobufPluginCommand) -> Result<Self, &'static str> {
@ -641,6 +673,62 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
}, },
_ => Err("Mismatched payload for RenameSession"), _ => 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<String, String> = plugin_config
.into_iter()
.map(|e| (e.name, e.value))
.collect();
let message_args: BTreeMap<String, String> = 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"), None => Err("Unrecognized plugin command"),
} }
} }
@ -1069,6 +1157,54 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
name: CommandName::RenameSession as i32, name: CommandName::RenameSession as i32,
payload: Some(Payload::RenameSessionPayload(new_session_name)), 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,
}
}),
})),
})
},
} }
} }
} }

View file

@ -10,4 +10,6 @@ enum PermissionType {
OpenTerminalsOrPlugins = 4; OpenTerminalsOrPlugins = 4;
WriteToStdin = 5; WriteToStdin = 5;
WebAccess = 6; WebAccess = 6;
ReadCliPipes = 7;
MessageAndLaunchOtherPlugins = 8;
} }

View file

@ -20,6 +20,10 @@ impl TryFrom<ProtobufPermissionType> for PermissionType {
}, },
ProtobufPermissionType::WriteToStdin => Ok(PermissionType::WriteToStdin), ProtobufPermissionType::WriteToStdin => Ok(PermissionType::WriteToStdin),
ProtobufPermissionType::WebAccess => Ok(PermissionType::WebAccess), ProtobufPermissionType::WebAccess => Ok(PermissionType::WebAccess),
ProtobufPermissionType::ReadCliPipes => Ok(PermissionType::ReadCliPipes),
ProtobufPermissionType::MessageAndLaunchOtherPlugins => {
Ok(PermissionType::MessageAndLaunchOtherPlugins)
},
} }
} }
} }
@ -41,6 +45,10 @@ impl TryFrom<PermissionType> for ProtobufPermissionType {
}, },
PermissionType::WriteToStdin => Ok(ProtobufPermissionType::WriteToStdin), PermissionType::WriteToStdin => Ok(ProtobufPermissionType::WriteToStdin),
PermissionType::WebAccess => Ok(ProtobufPermissionType::WebAccess), PermissionType::WebAccess => Ok(ProtobufPermissionType::WebAccess),
PermissionType::ReadCliPipes => Ok(ProtobufPermissionType::ReadCliPipes),
PermissionType::MessageAndLaunchOtherPlugins => {
Ok(ProtobufPermissionType::MessageAndLaunchOtherPlugins)
},
} }
} }
} }