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}