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        /// The exit status once the program exited.
479        pub fn try_wait(self) -> Result<Option<i32>> {
480            host::process_try_wait(self.pid)
481        }
482
483        /// Writes to a [`Stdio::Piped`] stdin.
484        pub fn write(self, bytes: &[u8]) -> Result<()> {
485            host::process_write(self.pid, bytes)
486        }
487
488        /// Closes a piped stdin: the program reads end of file.
489        pub fn close_stdin(self) -> Result<()> {
490            host::process_close_stdin(self.pid)
491        }
492
493        /// Kills the program's process group.
494        pub fn kill(self) -> Result<()> {
495            host::process_kill(self.pid)
496        }
497    }
498}
499
500/// File and directory change notifications, debounced by the host and
501/// delivered as [`Event::FileChanged`](crate::Event::FileChanged). Grant:
502/// `fs.read:<path>` covering the path. At most 256 watches per plugin.
503pub mod watch {
504    use super::*;
505
506    /// A watch; dropping it keeps watching, [`Watch::unwatch`] stops.
507    #[derive(Debug, PartialEq, Eq)]
508    pub struct Watch {
509        pub handle: u32,
510    }
511
512    /// What a watch covers.
513    #[derive(Clone, Debug, PartialEq, Eq)]
514    pub struct Options {
515        /// A directory's whole tree (the default), or its own entries only.
516        pub recursive: bool,
517        /// Globs relative to the watched path whose changes never arrive:
518        /// `*` within a component, `?` one character, `**` any number of
519        /// components; a glob without `/` matches a component at any depth
520        /// (`target`, `node_modules`, `*.log`), one with `/` is anchored at
521        /// the watched path (`.git/objects`), and a leading `/` anchors a
522        /// single name there, as in `.gitignore` (`/target` is the watched
523        /// path's own `target` only). Excluding a directory excludes
524        /// everything in it. At most 64.
525        pub exclude: Vec<String>,
526    }
527
528    impl Default for Options {
529        fn default() -> Self {
530            Self {
531                recursive: true,
532                exclude: Vec::new(),
533            }
534        }
535    }
536
537    impl Options {
538        /// A directory's own entries only.
539        pub fn shallow() -> Self {
540            Self {
541                recursive: false,
542                exclude: Vec::new(),
543            }
544        }
545
546        /// Drops changes the glob matches.
547        pub fn exclude(mut self, glob: &str) -> Self {
548            self.exclude.push(String::from(glob));
549            self
550        }
551    }
552
553    /// Watches a file, or a directory and everything under it.
554    pub fn watch(path: &str) -> Result<Watch> {
555        watch_with(path, &Options::default())
556    }
557
558    /// Watches `path` as `options` say.
559    pub fn watch_with(path: &str, options: &Options) -> Result<Watch> {
560        host::watch(path, options).map(|handle| Watch { handle })
561    }
562
563    impl Watch {
564        pub fn unwatch(self) -> Result<()> {
565            host::unwatch(self.handle)
566        }
567    }
568}
569
570/// WebSockets the host holds for the plugin (`daemon-net`). Grant:
571/// `socket.connect:<host>:<port>` for each server (`wss://` only). News
572/// arrives as [`crate::Event::Plugin`] named [`net::WEBSOCKET_EVENT`];
573/// [`net::SocketEvent::from_event`] reads it.
574pub mod net {
575    use super::*;
576
577    /// The plugin event a socket's news arrives as.
578    pub const WEBSOCKET_EVENT: &str = "system.net.websocket";
579
580    /// An open (or opening) WebSocket.
581    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
582    pub struct WebSocket {
583        pub socket: u32,
584    }
585
586    /// What happened to a socket.
587    #[derive(Clone, Debug, PartialEq, Eq)]
588    pub enum SocketEvent {
589        Open { socket: u32 },
590        Message { socket: u32, text: String },
591        Closed { socket: u32, reason: String },
592    }
593
594    impl SocketEvent {
595        /// The socket news an event carries, if it is some.
596        pub fn from_event(event: &crate::Event) -> Option<Self> {
597            let crate::Event::Plugin { name, payload } = event else {
598                return None;
599            };
600            if name != WEBSOCKET_EVENT {
601                return None;
602            }
603            let value: serde_json::Value = serde_json::from_str(&payload.0).ok()?;
604            let socket = u32::try_from(value.get("socket")?.as_u64()?).ok()?;
605            let text = |field: &str| -> String {
606                value
607                    .get(field)
608                    .and_then(serde_json::Value::as_str)
609                    .unwrap_or_default()
610                    .into()
611            };
612            match value.get("kind")?.as_str()? {
613                "open" => Some(Self::Open { socket }),
614                "message" => Some(Self::Message {
615                    socket,
616                    text: text("text"),
617                }),
618                "closed" => Some(Self::Closed {
619                    socket,
620                    reason: text("reason"),
621                }),
622                _ => None,
623            }
624        }
625    }
626
627    impl WebSocket {
628        /// Starts connecting to `url` with extra request `headers`; the
629        /// socket's `Open` or `Closed` event follows.
630        pub fn open(url: &str, headers: &[(String, String)]) -> Result<Self> {
631            host::websocket_open(url, headers).map(|socket| Self { socket })
632        }
633
634        /// Sends one text frame (64 KiB at most).
635        pub fn send(&self, text: &str) -> Result<()> {
636            host::websocket_send(self.socket, text)
637        }
638
639        /// Closes it; its `Closed` event follows.
640        pub fn close(self) {
641            host::websocket_close(self.socket);
642        }
643    }
644}
645
646/// Panes on the daemon's machine. Grants: `panes.read`, `panes.write`.
647pub mod panes {
648    use super::*;
649    pub use crate::api::account::Pane;
650
651    pub fn list() -> Result<Vec<Pane>> {
652        host::panes()
653    }
654
655    /// A pane [`create`] opened: its id and generation, together naming
656    /// this run of it.
657    #[derive(Clone, Debug, PartialEq, Eq)]
658    pub struct Created {
659        pub id: String,
660        /// 1 for a new pane; a restart advances it.
661        pub generation: u64,
662    }
663
664    impl Created {
665        /// `<id>@<generation>`: the key state kept for this run of the pane
666        /// goes under.
667        pub fn key(&self) -> String {
668            alloc::format!("{}@{}", self.id, self.generation)
669        }
670    }
671
672    /// A pane to open: its directory (a project root of this machine or a
673    /// directory inside one), its command (the user's shell when none) and
674    /// variables on top of its login environment.
675    ///
676    /// ```rust,ignore
677    /// let pane = NewPane::new("/work/app/web")
678    ///     .command("pnpm dev")
679    ///     .env("PORT", "4000")
680    ///     .create()?;
681    /// ```
682    #[derive(Clone, Debug, Default, PartialEq, Eq)]
683    pub struct NewPane {
684        pub cwd: String,
685        pub command: Option<String>,
686        pub env: Vec<(String, String)>,
687        /// The pane's title; the plugin's id when none.
688        pub title: Option<String>,
689    }
690
691    impl NewPane {
692        pub fn new(cwd: &str) -> Self {
693            Self {
694                cwd: cwd.into(),
695                ..Self::default()
696            }
697        }
698
699        pub fn command(mut self, command: &str) -> Self {
700            self.command = Some(command.into());
701            self
702        }
703
704        /// Sets one variable (at most 64; a name is not empty and has no
705        /// `=`).
706        pub fn env(mut self, name: &str, value: &str) -> Self {
707            self.env.retain(|(key, _)| key != name);
708            self.env.push((name.into(), value.into()));
709            self
710        }
711
712        /// Titles the pane (1 to 128 characters, no control characters)
713        /// instead of the plugin's id.
714        pub fn title(mut self, title: &str) -> Self {
715            self.title = Some(title.into());
716            self
717        }
718
719        /// Opens it. `Invalid` for a directory outside every project root,
720        /// variables a pane cannot be given or a title it cannot show.
721        pub fn create(&self) -> Result<Created> {
722            // Untitled panes use `create`, which every host has.
723            match &self.title {
724                None => host::pane_create(&self.cwd, self.command.as_deref(), &self.env),
725                Some(title) => host::pane_create_with(
726                    &self.cwd,
727                    self.command.as_deref(),
728                    &self.env,
729                    Some(title),
730                ),
731            }
732            .map(|(id, generation)| Created { id, generation })
733        }
734    }
735
736    /// Opens a pane in `cwd` (a project root or a directory inside one),
737    /// running `command` or the user's shell; [`NewPane`] adds variables.
738    pub fn create(cwd: &str, command: Option<&str>) -> Result<Created> {
739        NewPane {
740            cwd: cwd.into(),
741            command: command.map(Into::into),
742            env: Vec::new(),
743            title: None,
744        }
745        .create()
746    }
747
748    /// Types `bytes` into a pane.
749    pub fn input(pane: &str, bytes: &[u8]) -> Result<()> {
750        host::pane_input(pane, bytes)
751    }
752
753    pub fn close(pane: &str) -> Result<()> {
754        host::pane_close(pane)
755    }
756
757    /// Asks for [`Event::PaneChanged`](crate::Event::PaneChanged) for every
758    /// pane on this machine.
759    pub fn subscribe() -> Result<()> {
760        host::panes_subscribe(true)
761    }
762
763    pub fn unsubscribe() -> Result<()> {
764        host::panes_subscribe(false)
765    }
766
767    /// Blocks the plugin until `pane` exits or closes, for at most
768    /// `timeout_ms` (30 s at most); whether it did.
769    pub fn wait(pane: &str, timeout_ms: u32) -> Result<bool> {
770        host::pane_wait(pane, timeout_ms)
771    }
772}