Skip to content

Commit d780bd9

Browse files
authored
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
1 parent f6d5729 commit d780bd9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+3071
-305
lines changed

default-plugins/fixture-plugin-for-tests/src/main.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ struct State {
1111
received_events: Vec<Event>,
1212
received_payload: Option<String>,
1313
configuration: BTreeMap<String, String>,
14+
message_to_plugin_payload: Option<String>,
1415
}
1516

1617
#[derive(Default, Serialize, Deserialize)]
@@ -34,9 +35,12 @@ impl<'de> ZellijWorker<'de> for TestWorker {
3435
}
3536
}
3637

38+
#[cfg(target_family = "wasm")]
3739
register_plugin!(State);
40+
#[cfg(target_family = "wasm")]
3841
register_worker!(TestWorker, test_worker, TEST_WORKER);
3942

43+
#[cfg(target_family = "wasm")]
4044
impl ZellijPlugin for State {
4145
fn load(&mut self, configuration: BTreeMap<String, String>) {
4246
request_permission(&[
@@ -49,6 +53,8 @@ impl ZellijPlugin for State {
4953
PermissionType::OpenTerminalsOrPlugins,
5054
PermissionType::WriteToStdin,
5155
PermissionType::WebAccess,
56+
PermissionType::ReadCliPipes,
57+
PermissionType::MessageAndLaunchOtherPlugins,
5258
]);
5359
self.configuration = configuration;
5460
subscribe(&[
@@ -295,10 +301,35 @@ impl ZellijPlugin for State {
295301
self.received_events.push(event);
296302
should_render
297303
}
304+
fn pipe(&mut self, pipe_message: PipeMessage) -> bool {
305+
let input_pipe_id = match pipe_message.source {
306+
PipeSource::Cli(id) => id.clone(),
307+
PipeSource::Plugin(id) => format!("{}", id),
308+
};
309+
let name = pipe_message.name;
310+
let payload = pipe_message.payload;
311+
if name == "message_name" && payload == Some("message_payload".to_owned()) {
312+
unblock_cli_pipe_input(&input_pipe_id);
313+
} else if name == "message_name_block" {
314+
block_cli_pipe_input(&input_pipe_id);
315+
} else if name == "pipe_output" {
316+
cli_pipe_output(&name, "this_is_my_output");
317+
} else if name == "pipe_message_to_plugin" {
318+
pipe_message_to_plugin(
319+
MessageToPlugin::new("message_to_plugin").with_payload("my_cool_payload"),
320+
);
321+
} else if name == "message_to_plugin" {
322+
self.message_to_plugin_payload = payload.clone();
323+
}
324+
let should_render = true;
325+
should_render
326+
}
298327

299328
fn render(&mut self, rows: usize, cols: usize) {
300329
if let Some(payload) = self.received_payload.as_ref() {
301330
println!("Payload from worker: {:?}", payload);
331+
} else if let Some(payload) = self.message_to_plugin_payload.take() {
332+
println!("Payload from self: {:?}", payload);
302333
} else {
303334
println!(
304335
"Rows: {:?}, Cols: {:?}, Received events: {:?}",

src/main.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,31 @@ fn main() {
111111
commands::convert_old_theme_file(old_theme_file);
112112
std::process::exit(0);
113113
}
114+
if let Some(Command::Sessions(Sessions::Pipe {
115+
name,
116+
payload,
117+
args,
118+
plugin,
119+
plugin_configuration,
120+
})) = opts.command
121+
{
122+
let command_cli_action = CliAction::Pipe {
123+
name,
124+
payload,
125+
args,
126+
plugin,
127+
plugin_configuration,
128+
129+
force_launch_plugin: false,
130+
skip_plugin_cache: false,
131+
floating_plugin: None,
132+
in_place_plugin: None,
133+
plugin_cwd: None,
134+
plugin_title: None,
135+
};
136+
commands::send_action_to_session(command_cli_action, opts.session, config);
137+
std::process::exit(0);
138+
}
114139
}
115140

116141
if let Some(Command::Sessions(Sessions::ListSessions {

zellij-client/src/cli_client.rs

Lines changed: 177 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
//! The `[cli_client]` is used to attach to a running server session
22
//! and dispatch actions, that are specified through the command line.
3+
use std::collections::BTreeMap;
4+
use std::io::BufRead;
35
use std::process;
46
use std::{fs, path::PathBuf};
57

68
use crate::os_input_output::ClientOsApi;
79
use zellij_utils::{
10+
errors::prelude::*,
811
input::actions::Action,
9-
ipc::{ClientToServerMsg, ServerToClientMsg},
12+
ipc::{ClientToServerMsg, ExitReason, ServerToClientMsg},
13+
uuid::Uuid,
1014
};
1115

12-
pub fn start_cli_client(os_input: Box<dyn ClientOsApi>, session_name: &str, actions: Vec<Action>) {
16+
pub fn start_cli_client(
17+
mut os_input: Box<dyn ClientOsApi>,
18+
session_name: &str,
19+
actions: Vec<Action>,
20+
) {
1321
let zellij_ipc_pipe: PathBuf = {
1422
let mut sock_dir = zellij_utils::consts::ZELLIJ_SOCK_DIR.clone();
1523
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
2129
let pane_id = os_input
2230
.env_variable("ZELLIJ_PANE_ID")
2331
.and_then(|e| e.trim().parse().ok());
32+
2433
for action in actions {
25-
let msg = ClientToServerMsg::Action(action, pane_id, None);
26-
os_input.send_to_server(msg);
34+
match action {
35+
Action::CliPipe {
36+
pipe_id,
37+
name,
38+
payload,
39+
plugin,
40+
args,
41+
configuration,
42+
launch_new,
43+
skip_cache,
44+
floating,
45+
in_place,
46+
cwd,
47+
pane_title,
48+
} => {
49+
pipe_client(
50+
&mut os_input,
51+
pipe_id,
52+
name,
53+
payload,
54+
plugin,
55+
args,
56+
configuration,
57+
launch_new,
58+
skip_cache,
59+
floating,
60+
in_place,
61+
pane_id,
62+
cwd,
63+
pane_title,
64+
);
65+
},
66+
action => {
67+
single_message_client(&mut os_input, action, pane_id);
68+
},
69+
}
2770
}
71+
}
72+
73+
fn pipe_client(
74+
os_input: &mut Box<dyn ClientOsApi>,
75+
pipe_id: String,
76+
mut name: Option<String>,
77+
payload: Option<String>,
78+
plugin: Option<String>,
79+
args: Option<BTreeMap<String, String>>,
80+
mut configuration: Option<BTreeMap<String, String>>,
81+
launch_new: bool,
82+
skip_cache: bool,
83+
floating: Option<bool>,
84+
in_place: Option<bool>,
85+
pane_id: Option<u32>,
86+
cwd: Option<PathBuf>,
87+
pane_title: Option<String>,
88+
) {
89+
let mut stdin = os_input.get_stdin_reader();
90+
let name = name.take().or_else(|| Some(Uuid::new_v4().to_string()));
91+
if launch_new {
92+
// we do this to make sure the plugin is unique (has a unique configuration parameter) so
93+
// that a new one would be launched, but we'll still send it to the same instance rather
94+
// than launching a new one in every iteration of the loop
95+
configuration
96+
.get_or_insert_with(BTreeMap::new)
97+
.insert("_zellij_id".to_owned(), Uuid::new_v4().to_string());
98+
}
99+
let create_msg = |payload: Option<String>| -> ClientToServerMsg {
100+
ClientToServerMsg::Action(
101+
Action::CliPipe {
102+
pipe_id: pipe_id.clone(),
103+
name: name.clone(),
104+
payload,
105+
args: args.clone(),
106+
plugin: plugin.clone(),
107+
configuration: configuration.clone(),
108+
floating,
109+
in_place,
110+
launch_new,
111+
skip_cache,
112+
cwd: cwd.clone(),
113+
pane_title: pane_title.clone(),
114+
},
115+
pane_id,
116+
None,
117+
)
118+
};
119+
loop {
120+
if payload.is_some() {
121+
// we got payload from the command line, we should use it and not wait for more
122+
let msg = create_msg(payload);
123+
os_input.send_to_server(msg);
124+
break;
125+
}
126+
// we didn't get payload from the command line, meaning we listen on STDIN because this
127+
// signifies the user is about to pipe more (eg. cat my-large-file | zellij pipe ...)
128+
let mut buffer = String::new();
129+
let _ = stdin.read_line(&mut buffer);
130+
if buffer.is_empty() {
131+
// end of pipe, send an empty message down the pipe
132+
let msg = create_msg(None);
133+
os_input.send_to_server(msg);
134+
break;
135+
} else {
136+
// we've got data! send it down the pipe (most common)
137+
let msg = create_msg(Some(buffer));
138+
os_input.send_to_server(msg);
139+
}
140+
loop {
141+
// wait for a response and act accordingly
142+
match os_input.recv_from_server() {
143+
Some((ServerToClientMsg::UnblockCliPipeInput(pipe_name), _)) => {
144+
// unblock this pipe, meaning we need to stop waiting for a response and read
145+
// once more from STDIN
146+
if pipe_name == pipe_id {
147+
break;
148+
}
149+
},
150+
Some((ServerToClientMsg::CliPipeOutput(pipe_name, output), _)) => {
151+
// send data to STDOUT, this *does not* mean we need to unblock the input
152+
let err_context = "Failed to write to stdout";
153+
if pipe_name == pipe_id {
154+
let mut stdout = os_input.get_stdout_writer();
155+
stdout
156+
.write_all(output.as_bytes())
157+
.context(err_context)
158+
.non_fatal();
159+
stdout.flush().context(err_context).non_fatal();
160+
}
161+
},
162+
Some((ServerToClientMsg::Log(log_lines), _)) => {
163+
log_lines.iter().for_each(|line| println!("{line}"));
164+
process::exit(0);
165+
},
166+
Some((ServerToClientMsg::LogError(log_lines), _)) => {
167+
log_lines.iter().for_each(|line| eprintln!("{line}"));
168+
process::exit(2);
169+
},
170+
Some((ServerToClientMsg::Exit(exit_reason), _)) => match exit_reason {
171+
ExitReason::Error(e) => {
172+
eprintln!("{}", e);
173+
process::exit(2);
174+
},
175+
_ => {
176+
process::exit(0);
177+
},
178+
},
179+
_ => {},
180+
}
181+
}
182+
}
183+
}
184+
185+
fn single_message_client(
186+
os_input: &mut Box<dyn ClientOsApi>,
187+
action: Action,
188+
pane_id: Option<u32>,
189+
) {
190+
let msg = ClientToServerMsg::Action(action, pane_id, None);
191+
os_input.send_to_server(msg);
28192
loop {
29193
match os_input.recv_from_server() {
30194
Some((ServerToClientMsg::UnblockInputThread, _)) => {
@@ -39,6 +203,15 @@ pub fn start_cli_client(os_input: Box<dyn ClientOsApi>, session_name: &str, acti
39203
log_lines.iter().for_each(|line| eprintln!("{line}"));
40204
process::exit(2);
41205
},
206+
Some((ServerToClientMsg::Exit(exit_reason), _)) => match exit_reason {
207+
ExitReason::Error(e) => {
208+
eprintln!("{}", e);
209+
process::exit(2);
210+
},
211+
_ => {
212+
process::exit(0);
213+
},
214+
},
42215
_ => {},
43216
}
44217
}

zellij-client/src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ pub(crate) enum ClientInstruction {
4949
LogError(Vec<String>),
5050
SwitchSession(ConnectToSession),
5151
SetSynchronizedOutput(Option<SyncOutput>),
52+
UnblockCliPipeInput(String), // String -> pipe name
53+
CliPipeOutput(String, String), // String -> pipe name, String -> output
5254
}
5355

5456
impl From<ServerToClientMsg> for ClientInstruction {
@@ -67,6 +69,12 @@ impl From<ServerToClientMsg> for ClientInstruction {
6769
ServerToClientMsg::SwitchSession(connect_to_session) => {
6870
ClientInstruction::SwitchSession(connect_to_session)
6971
},
72+
ServerToClientMsg::UnblockCliPipeInput(pipe_name) => {
73+
ClientInstruction::UnblockCliPipeInput(pipe_name)
74+
},
75+
ServerToClientMsg::CliPipeOutput(pipe_name, output) => {
76+
ClientInstruction::CliPipeOutput(pipe_name, output)
77+
},
7078
}
7179
}
7280
}
@@ -87,6 +95,8 @@ impl From<&ClientInstruction> for ClientContext {
8795
ClientInstruction::DoneParsingStdinQuery => ClientContext::DoneParsingStdinQuery,
8896
ClientInstruction::SwitchSession(..) => ClientContext::SwitchSession,
8997
ClientInstruction::SetSynchronizedOutput(..) => ClientContext::SetSynchronisedOutput,
98+
ClientInstruction::UnblockCliPipeInput(..) => ClientContext::UnblockCliPipeInput,
99+
ClientInstruction::CliPipeOutput(..) => ClientContext::CliPipeOutput,
90100
}
91101
}
92102
}

zellij-client/src/os_input_output.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ pub trait ClientOsApi: Send + Sync {
9595
fn unset_raw_mode(&self, fd: RawFd) -> Result<(), nix::Error>;
9696
/// Returns the writer that allows writing to standard output.
9797
fn get_stdout_writer(&self) -> Box<dyn io::Write>;
98-
fn get_stdin_reader(&self) -> Box<dyn io::Read>;
98+
/// Returns a BufReader that allows to read from STDIN line by line, also locks STDIN
99+
fn get_stdin_reader(&self) -> Box<dyn io::BufRead>;
99100
fn update_session_name(&mut self, new_session_name: String);
100101
/// Returns the raw contents of standard input.
101102
fn read_from_stdin(&mut self) -> Result<Vec<u8>, &'static str>;
@@ -186,9 +187,10 @@ impl ClientOsApi for ClientOsInputOutput {
186187
let stdout = ::std::io::stdout();
187188
Box::new(stdout)
188189
}
189-
fn get_stdin_reader(&self) -> Box<dyn io::Read> {
190+
191+
fn get_stdin_reader(&self) -> Box<dyn io::BufRead> {
190192
let stdin = ::std::io::stdin();
191-
Box::new(stdin)
193+
Box::new(stdin.lock())
192194
}
193195

194196
fn send_to_server(&self, msg: ClientToServerMsg) {

zellij-client/src/unit/stdin_tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,10 @@ impl ClientOsApi for FakeClientOsApi {
151151
let fake_stdout_writer = FakeStdoutWriter::new(self.stdout_buffer.clone());
152152
Box::new(fake_stdout_writer)
153153
}
154-
fn get_stdin_reader(&self) -> Box<dyn io::Read> {
154+
fn get_stdin_reader(&self) -> Box<dyn io::BufRead> {
155155
unimplemented!()
156156
}
157-
fn update_session_name(&mut self, new_session_name: String) {}
157+
fn update_session_name(&mut self, _new_session_name: String) {}
158158
fn read_from_stdin(&mut self) -> Result<Vec<u8>, &'static str> {
159159
Ok(self.stdin_buffer.drain(..).collect())
160160
}

0 commit comments

Comments
 (0)