Skip to main content

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}