standard_plugin/daemon/mod.rs
1//! Daemon plugins: run inside `standardd` on the machines the plugin is
2//! enabled on, never render, and may reach the machine through granted
3//! system interfaces ([`process`], [`watch`], [`panes`]; with the `wasi`
4//! feature, files, sockets and HTTP).
5//!
6//! A daemon plugin has an async [`DaemonPlugin::run`] loop driven by a
7//! small single-threaded executor: the host calls the component's `drive`
8//! export when the loop's next timer is due and after every event, and the
9//! loop awaits [`Context::next_event`], [`Context::sleep`] and, with `wasi`,
10//! WASI pollables (`daemon::io`). What happens on the machine arrives as
11//! events too: [`Event::FileChanged`](crate::Event::FileChanged) for a
12//! [`watch`], [`Event::ProcessOutput`](crate::Event::ProcessOutput) and
13//! [`Event::ProcessExited`](crate::Event::ProcessExited) for a
14//! [`process`], [`Event::PaneChanged`](crate::Event::PaneChanged) after
15//! [`panes::subscribe`]. A *companion* is a daemon plugin that answers its
16//! UI plugin's [`calls`](mod@crate::calls) in [`DaemonPlugin::call`];
17//! [`Context::caller`] says who asked.
18//!
19//! Plugin methods take `&self`: the run loop and call handlers share the
20//! plugin, so keep mutable state in a `RefCell` or `Cell`.
21//!
22//! ```rust,ignore
23#![doc = include_str!("../../examples/together_companion.rs")]
24//! ```
25
26mod executor;
27pub(crate) mod runtime;
28
29/// HTTP: over `wasi:http` in a component (the `wasi` feature), and through
30/// the test host natively.
31#[cfg(any(feature = "wasi", not(target_arch = "wasm32")))]
32pub mod http;
33#[cfg(all(feature = "wasi", target_arch = "wasm32"))]
34mod http_wasi;
35#[cfg(all(feature = "wasi", target_arch = "wasm32"))]
36pub mod io;
37
38/// The WASI 0.2 bindings (`wasip2`), for what the SDK does not wrap.
39#[cfg(all(feature = "wasi", target_arch = "wasm32"))]
40pub use wasip2 as wasi;
41
42use alloc::string::String;
43use alloc::vec::Vec;
44use core::future::Future;
45
46use serde::de::DeserializeOwned;
47
48use crate::api::{self, Json};
49use crate::error::{Error, Result};
50use crate::host;
51
52pub use executor::{EVENT_QUEUE, NextEvent, NextEventUntil, Sleep};
53
54/// The plugin event the host delivers the viewers' interest as
55/// (`{ "surfaces": [...] }`); the runtime turns it into
56/// [`Event::Interest`](crate::Event::Interest).
57pub const INTEREST_EVENT: &str = "system.plugin.interest";
58
59/// Which of the plugin's UI surfaces some viewer on the account shows now:
60/// the union over every viewer, kept by the account and pushed to the
61/// daemon half whenever it changes (a surface shown or hidden, a viewer
62/// that opens or goes away). A daemon half that reads an outside source
63/// only for its UI reads it while [`Interest::any`] holds and stops when it
64/// goes away: nothing on screen, nothing read. Instances count as their
65/// surface (`workers@<machine>` is `workers`).
66///
67/// Until the host has said anything (a host or account service without the
68/// signal) the interest is *unknown*, and [`Interest::any`] and
69/// [`Interest::shows`] answer `true`, so a plugin keeps working there.
70#[derive(Clone, Debug, Default, PartialEq, Eq)]
71pub struct Interest {
72 surfaces: Option<alloc::collections::BTreeSet<String>>,
73}
74
75impl Interest {
76 /// Interest in exactly `surfaces`.
77 pub fn of<I, S>(surfaces: I) -> Self
78 where
79 I: IntoIterator<Item = S>,
80 S: Into<String>,
81 {
82 Self {
83 surfaces: Some(surfaces.into_iter().map(Into::into).collect()),
84 }
85 }
86
87 /// Whether the host has said anything yet.
88 pub fn is_known(&self) -> bool {
89 self.surfaces.is_some()
90 }
91
92 /// Whether some viewer shows any surface of the plugin (or the
93 /// interest is unknown).
94 pub fn any(&self) -> bool {
95 self.surfaces.as_ref().is_none_or(|shown| !shown.is_empty())
96 }
97
98 /// Whether some viewer shows `surface` (or the interest is unknown).
99 pub fn shows(&self, surface: &str) -> bool {
100 self.surfaces
101 .as_ref()
102 .is_none_or(|shown| shown.contains(surface))
103 }
104
105 /// The surfaces shown, when known.
106 pub fn surfaces(&self) -> impl Iterator<Item = &str> {
107 self.surfaces.iter().flatten().map(String::as_str)
108 }
109
110 /// Reads the host's payload; `None` when it is not
111 /// `{ "surfaces": [<string>, ...] }`.
112 pub(crate) fn from_payload(payload: &str) -> Option<Self> {
113 #[derive(serde::Deserialize)]
114 struct Wire {
115 surfaces: Vec<String>,
116 }
117 let wire: Wire = serde_json::from_str(payload).ok()?;
118 Some(Self::of(wire.surfaces))
119 }
120}
121
122/// A daemon (or companion) plugin.
123pub trait DaemonPlugin: Sized + 'static {
124 /// Called once after instantiation.
125 fn activate(cx: &Context) -> Self;
126
127 /// The plugin's long-running work, started after `activate` and dropped
128 /// at `deactivate`. Await [`Context::next_event`] for events and
129 /// [`Context::sleep`] between rounds of work.
130 fn run(&self, cx: Context) -> impl Future<Output = ()> {
131 let _ = cx;
132 core::future::ready(())
133 }
134
135 /// Answers a call from the UI plugin with the same id. The default
136 /// knows no method.
137 fn call(&self, method: &str, request: Json, cx: &Context) -> Result<Json> {
138 let _ = (request, cx);
139 Err(Error::Invalid(alloc::format!("unknown method {method:?}")))
140 }
141
142 /// Called before the instance is dropped, after the run loop is.
143 fn deactivate(&self) {}
144}
145
146/// How a pane changed ([`Event::PaneChanged`](crate::Event::PaneChanged)).
147#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
148pub enum PaneChangeKind {
149 Created,
150 /// Title, size or agent state changed.
151 Changed,
152 Exited,
153 Closed,
154}
155
156/// Who made a call, as the account routed it.
157#[derive(Clone, Debug, Default, PartialEq, Eq)]
158pub struct Caller {
159 /// The machine the calling runtime runs on.
160 pub machine_id: String,
161 pub plugin_id: String,
162 /// `ui` or `daemon`.
163 pub kind: String,
164 /// The caller's lease epoch, when a singleton daemon called.
165 pub epoch: Option<u64>,
166 /// The account controller lease when the call was made: the machine
167 /// the user controls from and its fencing token.
168 pub controller_machine_id: Option<String>,
169 pub controller_fencing_token: Option<String>,
170}
171
172impl Caller {
173 /// Whether the call came from the machine the user was controlling
174 /// when it was made: act for the user only then.
175 pub fn from_controller(&self) -> bool {
176 self.controller_machine_id.as_deref() == Some(self.machine_id.as_str())
177 }
178}
179
180/// A daemon plugin's handle to the host, its configuration and its
181/// executor. Cheap to clone.
182#[derive(Clone, Debug, Default)]
183pub struct Context {
184 config: String,
185}
186
187impl Context {
188 pub(crate) fn new(config: String) -> Self {
189 Self { config }
190 }
191
192 /// The configuration, deserialized (an empty document is `null`).
193 pub fn config<T: DeserializeOwned>(&self) -> Result<T> {
194 if self.config.trim().is_empty() {
195 return Json(String::from("null")).parse();
196 }
197 Json(self.config.clone()).parse()
198 }
199
200 pub fn config_json(&self) -> Json {
201 Json(self.config.clone())
202 }
203
204 pub fn values(&self) -> api::values::Values {
205 api::values()
206 }
207
208 pub fn live(&self) -> api::live::Live {
209 api::live()
210 }
211
212 pub fn events(&self) -> api::events::Events {
213 api::events()
214 }
215
216 /// Exactly-once effects across the daemons that run a fleet plugin (a
217 /// singleton's claims carry its lease epoch). No grant.
218 pub fn claims(&self) -> api::claims::Claims {
219 api::claims::Claims
220 }
221
222 pub fn calls(&self) -> api::calls::Calls {
223 api::calls()
224 }
225
226 /// The daemon's monotonic clock as of the current `drive` call.
227 pub fn now_ms(&self) -> u64 {
228 executor::now_ms()
229 }
230
231 /// The next event the host delivers (values, live messages, plugin
232 /// events, account changes). Events queue while nothing awaits them,
233 /// up to [`EVENT_QUEUE`]; past that the oldest are dropped.
234 pub fn next_event(&self) -> NextEvent {
235 NextEvent::new()
236 }
237
238 /// Which of the plugin's UI surfaces some viewer shows now
239 /// ([`Interest`]). Changes arrive as
240 /// [`Event::Interest`](crate::Event::Interest).
241 pub fn interest(&self) -> Interest {
242 runtime::current_interest()
243 }
244
245 /// The next event, or `None` once the daemon's clock reaches `at_ms`
246 /// (never, with `None`): the one wait a loop that reads on a schedule
247 /// and reacts to events needs. The timer goes with it, so an event that
248 /// arrives first leaves no wake behind.
249 pub fn next_event_until(&self, at_ms: Option<u64>) -> NextEventUntil {
250 NextEventUntil::new(at_ms)
251 }
252
253 /// Completes `ms` milliseconds from now.
254 pub fn sleep(&self, ms: u64) -> Sleep {
255 Sleep::until(executor::now_ms().saturating_add(ms))
256 }
257
258 /// Completes at `at_ms` on the daemon's clock.
259 pub fn sleep_until(&self, at_ms: u64) -> Sleep {
260 Sleep::until(at_ms)
261 }
262
263 /// Runs `task` beside the run loop until it completes or the plugin
264 /// is deactivated.
265 pub fn spawn(&self, task: impl Future<Output = ()> + 'static) {
266 executor::spawn(task);
267 }
268
269 /// Who made the call being answered, inside [`DaemonPlugin::call`];
270 /// `None` elsewhere.
271 pub fn caller(&self) -> Option<Caller> {
272 runtime::current_caller()
273 }
274
275 /// The machine this daemon runs on, by the account's machine id: the
276 /// id a UI plugin targets with [`Target::Machine`](crate::Target::Machine)
277 /// and finds in [`account::state`](crate::account::state).
278 pub fn machine_id(&self) -> String {
279 host::daemon_machine_id()
280 }
281
282 /// The account this daemon is enrolled in, once the daemon has read it.
283 pub fn account_id(&self) -> Option<String> {
284 host::daemon_account_id()
285 }
286
287 /// The lease epoch a singleton runs under (every write of its account
288 /// session carries it); `None` for a fleet plugin. A plugin that records
289 /// who did something (a claim, a value) can tag it with its epoch.
290 pub fn lease_epoch(&self) -> Option<u64> {
291 host::daemon_lease_epoch()
292 }
293
294 /// The Standard Code build this daemon runs: its channel
295 /// (`branch:<name>`, `team`, ...), version and commit. `None` where it
296 /// is not known.
297 pub fn release(&self) -> Option<Release> {
298 host::daemon_release()
299 }
300}
301
302/// The Standard Code build a daemon runs ([`Context::release`]).
303#[derive(Clone, Debug, Default, PartialEq, Eq)]
304pub struct Release {
305 /// `branch:<name>` for a branch build, else the published channel's
306 /// name (`team`, `canary`, `production`).
307 pub channel: String,
308 pub version: String,
309 pub git_sha: String,
310}
311
312impl Release {
313 /// The Git ref the channel follows: `refs/heads/<name>` for a branch
314 /// channel, else `refs/heads/main`.
315 pub fn git_ref(&self) -> String {
316 match self.channel.strip_prefix("branch:") {
317 Some(branch) if !branch.is_empty() => alloc::format!("refs/heads/{branch}"),
318 _ => "refs/heads/main".into(),
319 }
320 }
321}
322
323/// The plugin's environment: the daemon user's login environment reduced to
324/// `HOME`, `USER`, `LOGNAME`, `PATH` (the login shell's), `LANG`, `LC_*`,
325/// `TMPDIR` and `SHELL` (all of it under `machine.full`). Children spawned
326/// through [`process`] inherit exactly these; the daemon's own environment
327/// never reaches a plugin. No grant.
328pub mod env {
329 use super::*;
330
331 /// Every variable, sorted by name.
332 pub fn vars() -> Vec<(String, String)> {
333 host::daemon_environment()
334 }
335
336 /// One variable.
337 pub fn var(name: &str) -> Option<String> {
338 vars()
339 .into_iter()
340 .find(|(key, _)| key == name)
341 .map(|(_, value)| value)
342 }
343
344 /// The user's home directory (`HOME`): where children run unless a
345 /// [`Command`](super::process::Command) names another directory.
346 pub fn home_dir() -> Option<String> {
347 var("HOME").filter(|home| home.starts_with('/'))
348 }
349}
350
351/// Spawn programs on the daemon's machine. Grant: `process.exec:<program>`
352/// naming the program exactly as spawned (`process.exec:*` for any), or
353/// `machine.full`. Children are killed when the plugin stops.
354pub mod process {
355 use super::*;
356
357 /// A spawned program.
358 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
359 pub struct Child {
360 pub pid: u32,
361 }
362
363 /// Where a child's standard stream goes.
364 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
365 pub enum Stdio {
366 /// Nowhere.
367 Null,
368 /// To the plugin: output arrives as
369 /// [`Event::ProcessOutput`](crate::Event::ProcessOutput), input goes
370 /// through [`Child::write`].
371 Piped,
372 /// The daemon's log, prefixed with the plugin id (output only).
373 #[default]
374 Log,
375 }
376
377 /// A program with its arguments, environment, directory and streams. It
378 /// inherits the plugin's environment ([`env`](super::env)) and runs in
379 /// the user's home unless told otherwise.
380 #[derive(Clone, Debug, PartialEq, Eq)]
381 pub struct Command {
382 pub program: String,
383 pub args: Vec<String>,
384 /// Variables set on top of the plugin's environment ([`env`](super::env)).
385 pub env: Vec<(String, String)>,
386 /// Variables removed from the plugin's environment first.
387 pub env_remove: Vec<String>,
388 /// Start from an empty environment: only [`Command::env`].
389 pub clear_env: bool,
390 /// The working directory; the user's home when `None`.
391 pub cwd: Option<String>,
392 pub stdin: Stdio,
393 pub stdout: Stdio,
394 pub stderr: Stdio,
395 }
396
397 impl Command {
398 /// `program` with no stdin and its output in the daemon's log.
399 pub fn new(program: &str) -> Self {
400 Self {
401 program: String::from(program),
402 args: Vec::new(),
403 env: Vec::new(),
404 env_remove: Vec::new(),
405 clear_env: false,
406 cwd: None,
407 stdin: Stdio::Null,
408 stdout: Stdio::Log,
409 stderr: Stdio::Log,
410 }
411 }
412
413 pub fn arg(mut self, arg: &str) -> Self {
414 self.args.push(String::from(arg));
415 self
416 }
417
418 pub fn args(mut self, args: &[&str]) -> Self {
419 self.args.extend(args.iter().map(|arg| String::from(*arg)));
420 self
421 }
422
423 pub fn env(mut self, key: &str, value: &str) -> Self {
424 self.env.push((String::from(key), String::from(value)));
425 self
426 }
427
428 /// Removes `key` from the inherited environment (`GIT_DIR`, say).
429 pub fn env_remove(mut self, key: &str) -> Self {
430 self.env_remove.push(String::from(key));
431 self
432 }
433
434 pub fn clear_env(mut self) -> Self {
435 self.clear_env = true;
436 self
437 }
438
439 pub fn cwd(mut self, cwd: &str) -> Self {
440 self.cwd = Some(String::from(cwd));
441 self
442 }
443
444 pub fn stdin(mut self, stdin: Stdio) -> Self {
445 self.stdin = stdin;
446 self
447 }
448
449 pub fn stdout(mut self, stdout: Stdio) -> Self {
450 self.stdout = stdout;
451 self
452 }
453
454 pub fn stderr(mut self, stderr: Stdio) -> Self {
455 self.stderr = stderr;
456 self
457 }
458
459 pub fn spawn(&self) -> Result<Child> {
460 host::process_run(self).map(|pid| Child { pid })
461 }
462 }
463
464 /// Spawns `program` with `args`: no stdin, output to the daemon's log.
465 pub fn spawn(program: &str, args: &[&str], cwd: Option<&str>) -> Result<Child> {
466 let args: Vec<String> = args.iter().map(|arg| String::from(*arg)).collect();
467 host::process_spawn(program, &args, cwd).map(|pid| Child { pid })
468 }
469
470 impl Child {
471 /// Blocks the plugin until the program exits; its exit status (a
472 /// signal is negated). Prefer awaiting
473 /// [`Event::ProcessExited`](crate::Event::ProcessExited).
474 pub fn wait(self) -> Result<i32> {
475 host::process_wait(self.pid)
476 }
477
478 /// Blocks the plugin until the program exits or `timeout` passes:
479 /// its exit status, or none when it still runs (kill it then).
480 pub fn wait_timeout(self, timeout: core::time::Duration) -> Result<Option<i32>> {
481 let ms = u32::try_from(timeout.as_millis()).unwrap_or(u32::MAX);
482 host::process_wait_timeout(self.pid, ms)
483 }
484
485 /// The exit status once the program exited.
486 pub fn try_wait(self) -> Result<Option<i32>> {
487 host::process_try_wait(self.pid)
488 }
489
490 /// Writes to a [`Stdio::Piped`] stdin.
491 pub fn write(self, bytes: &[u8]) -> Result<()> {
492 host::process_write(self.pid, bytes)
493 }
494
495 /// Closes a piped stdin: the program reads end of file.
496 pub fn close_stdin(self) -> Result<()> {
497 host::process_close_stdin(self.pid)
498 }
499
500 /// Kills the program's process group.
501 pub fn kill(self) -> Result<()> {
502 host::process_kill(self.pid)
503 }
504 }
505}
506
507/// File and directory change notifications, debounced by the host and
508/// delivered as [`Event::FileChanged`](crate::Event::FileChanged). Grant:
509/// `fs.read:<path>` covering the path. At most 256 watches per plugin.
510pub mod watch {
511 use super::*;
512
513 /// A watch; dropping it keeps watching, [`Watch::unwatch`] stops.
514 #[derive(Debug, PartialEq, Eq)]
515 pub struct Watch {
516 pub handle: u32,
517 }
518
519 /// What a watch covers.
520 #[derive(Clone, Debug, PartialEq, Eq)]
521 pub struct Options {
522 /// A directory's whole tree (the default), or its own entries only.
523 pub recursive: bool,
524 /// Globs relative to the watched path whose changes never arrive:
525 /// `*` within a component, `?` one character, `**` any number of
526 /// components; a glob without `/` matches a component at any depth
527 /// (`target`, `node_modules`, `*.log`), one with `/` is anchored at
528 /// the watched path (`.git/objects`), and a leading `/` anchors a
529 /// single name there, as in `.gitignore` (`/target` is the watched
530 /// path's own `target` only). Excluding a directory excludes
531 /// everything in it. At most 64.
532 pub exclude: Vec<String>,
533 }
534
535 impl Default for Options {
536 fn default() -> Self {
537 Self {
538 recursive: true,
539 exclude: Vec::new(),
540 }
541 }
542 }
543
544 impl Options {
545 /// A directory's own entries only.
546 pub fn shallow() -> Self {
547 Self {
548 recursive: false,
549 exclude: Vec::new(),
550 }
551 }
552
553 /// Drops changes the glob matches.
554 pub fn exclude(mut self, glob: &str) -> Self {
555 self.exclude.push(String::from(glob));
556 self
557 }
558 }
559
560 /// Watches a file, or a directory and everything under it.
561 pub fn watch(path: &str) -> Result<Watch> {
562 watch_with(path, &Options::default())
563 }
564
565 /// Watches `path` as `options` say.
566 pub fn watch_with(path: &str, options: &Options) -> Result<Watch> {
567 host::watch(path, options).map(|handle| Watch { handle })
568 }
569
570 impl Watch {
571 pub fn unwatch(self) -> Result<()> {
572 host::unwatch(self.handle)
573 }
574 }
575}
576
577/// WebSockets the host holds for the plugin (`daemon-net`). Grant:
578/// `socket.connect:<host>:<port>` for each server (`wss://` only). News
579/// arrives as [`crate::Event::Plugin`] named [`net::WEBSOCKET_EVENT`];
580/// [`net::SocketEvent::from_event`] reads it.
581pub mod net {
582 use super::*;
583
584 /// The plugin event a socket's news arrives as.
585 pub const WEBSOCKET_EVENT: &str = "system.net.websocket";
586
587 /// An open (or opening) WebSocket.
588 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
589 pub struct WebSocket {
590 pub socket: u32,
591 }
592
593 /// What happened to a socket.
594 #[derive(Clone, Debug, PartialEq, Eq)]
595 pub enum SocketEvent {
596 Open { socket: u32 },
597 Message { socket: u32, text: String },
598 Closed { socket: u32, reason: String },
599 }
600
601 impl SocketEvent {
602 /// The socket news an event carries, if it is some.
603 pub fn from_event(event: &crate::Event) -> Option<Self> {
604 let crate::Event::Plugin { name, payload } = event else {
605 return None;
606 };
607 if name != WEBSOCKET_EVENT {
608 return None;
609 }
610 let value: serde_json::Value = serde_json::from_str(&payload.0).ok()?;
611 let socket = u32::try_from(value.get("socket")?.as_u64()?).ok()?;
612 let text = |field: &str| -> String {
613 value
614 .get(field)
615 .and_then(serde_json::Value::as_str)
616 .unwrap_or_default()
617 .into()
618 };
619 match value.get("kind")?.as_str()? {
620 "open" => Some(Self::Open { socket }),
621 "message" => Some(Self::Message {
622 socket,
623 text: text("text"),
624 }),
625 "closed" => Some(Self::Closed {
626 socket,
627 reason: text("reason"),
628 }),
629 _ => None,
630 }
631 }
632 }
633
634 impl WebSocket {
635 /// Starts connecting to `url` with extra request `headers`; the
636 /// socket's `Open` or `Closed` event follows.
637 pub fn open(url: &str, headers: &[(String, String)]) -> Result<Self> {
638 host::websocket_open(url, headers).map(|socket| Self { socket })
639 }
640
641 /// Sends one text frame (64 KiB at most).
642 pub fn send(&self, text: &str) -> Result<()> {
643 host::websocket_send(self.socket, text)
644 }
645
646 /// Closes it; its `Closed` event follows.
647 pub fn close(self) {
648 host::websocket_close(self.socket);
649 }
650 }
651}
652
653/// Panes on the daemon's machine. Grants: `panes.read`, `panes.write`.
654pub mod panes {
655 use super::*;
656 pub use crate::api::account::Pane;
657
658 pub fn list() -> Result<Vec<Pane>> {
659 host::panes()
660 }
661
662 /// A pane [`create`] opened: its id and generation, together naming
663 /// this run of it.
664 #[derive(Clone, Debug, PartialEq, Eq)]
665 pub struct Created {
666 pub id: String,
667 /// 1 for a new pane; a restart advances it.
668 pub generation: u64,
669 }
670
671 impl Created {
672 /// `<id>@<generation>`: the key state kept for this run of the pane
673 /// goes under.
674 pub fn key(&self) -> String {
675 alloc::format!("{}@{}", self.id, self.generation)
676 }
677 }
678
679 /// A pane to open: its directory (a project root of this machine or a
680 /// directory inside one), its command (the user's shell when none) and
681 /// variables on top of its login environment.
682 ///
683 /// ```rust,ignore
684 /// let pane = NewPane::new("/work/app/web")
685 /// .command("pnpm dev")
686 /// .env("PORT", "4000")
687 /// .create()?;
688 /// ```
689 #[derive(Clone, Debug, Default, PartialEq, Eq)]
690 pub struct NewPane {
691 pub cwd: String,
692 pub command: Option<String>,
693 pub env: Vec<(String, String)>,
694 /// The pane's title; the plugin's id when none.
695 pub title: Option<String>,
696 }
697
698 impl NewPane {
699 pub fn new(cwd: &str) -> Self {
700 Self {
701 cwd: cwd.into(),
702 ..Self::default()
703 }
704 }
705
706 pub fn command(mut self, command: &str) -> Self {
707 self.command = Some(command.into());
708 self
709 }
710
711 /// Sets one variable (at most 64; a name is not empty and has no
712 /// `=`).
713 pub fn env(mut self, name: &str, value: &str) -> Self {
714 self.env.retain(|(key, _)| key != name);
715 self.env.push((name.into(), value.into()));
716 self
717 }
718
719 /// Titles the pane (1 to 128 characters, no control characters)
720 /// instead of the plugin's id.
721 pub fn title(mut self, title: &str) -> Self {
722 self.title = Some(title.into());
723 self
724 }
725
726 /// Opens it. `Invalid` for a directory outside every project root,
727 /// variables a pane cannot be given or a title it cannot show.
728 pub fn create(&self) -> Result<Created> {
729 // Untitled panes use `create`, which every host has.
730 match &self.title {
731 None => host::pane_create(&self.cwd, self.command.as_deref(), &self.env),
732 Some(title) => host::pane_create_with(
733 &self.cwd,
734 self.command.as_deref(),
735 &self.env,
736 Some(title),
737 ),
738 }
739 .map(|(id, generation)| Created { id, generation })
740 }
741 }
742
743 /// Opens a pane in `cwd` (a project root or a directory inside one),
744 /// running `command` or the user's shell; [`NewPane`] adds variables.
745 pub fn create(cwd: &str, command: Option<&str>) -> Result<Created> {
746 NewPane {
747 cwd: cwd.into(),
748 command: command.map(Into::into),
749 env: Vec::new(),
750 title: None,
751 }
752 .create()
753 }
754
755 /// Types `bytes` into a pane.
756 pub fn input(pane: &str, bytes: &[u8]) -> Result<()> {
757 host::pane_input(pane, bytes)
758 }
759
760 pub fn close(pane: &str) -> Result<()> {
761 host::pane_close(pane)
762 }
763
764 /// Asks for [`Event::PaneChanged`](crate::Event::PaneChanged) for every
765 /// pane on this machine.
766 pub fn subscribe() -> Result<()> {
767 host::panes_subscribe(true)
768 }
769
770 pub fn unsubscribe() -> Result<()> {
771 host::panes_subscribe(false)
772 }
773
774 /// Blocks the plugin until `pane` exits or closes, for at most
775 /// `timeout_ms` (30 s at most); whether it did.
776 pub fn wait(pane: &str, timeout_ms: u32) -> Result<bool> {
777 host::pane_wait(pane, timeout_ms)
778 }
779}