Skip to main content

standard_plugin/
api.rs

1//! The account interfaces shared by UI and daemon plugins, and the viewer
2//! interfaces of UI plugins.
3//!
4//! Each interface is a zero-sized handle returned by a function of the same
5//! name ([`values()`], [`events()`], ...) and by the matching method of the
6//! plugin's context. Every call is checked against the manifest's grants; a
7//! call they do not cover returns [`Error::GrantDenied`] naming the grant.
8//! Payloads are JSON: typed values go through `serde`.
9
10use alloc::string::String;
11use alloc::vec::Vec;
12
13use serde::Serialize;
14use serde::de::DeserializeOwned;
15
16use crate::error::{Error, Result};
17use crate::host;
18
19/// A JSON document as the host passes it: a value, a live message, an
20/// event payload, a call's request or response.
21#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
22pub struct Json(pub String);
23
24impl Json {
25    /// Serializes `value`.
26    pub fn from_value<T: Serialize + ?Sized>(value: &T) -> Result<Self> {
27        serde_json::to_string(value).map(Self).map_err(Error::json)
28    }
29
30    /// Deserializes the document.
31    pub fn parse<T: DeserializeOwned>(&self) -> Result<T> {
32        serde_json::from_str(&self.0).map_err(Error::json)
33    }
34
35    pub fn as_str(&self) -> &str {
36        &self.0
37    }
38
39    pub fn into_string(self) -> String {
40        self.0
41    }
42}
43
44/// Where an event or call is delivered.
45#[derive(Clone, Debug, PartialEq, Eq, Hash)]
46pub enum Target {
47    /// Every listener on the account.
48    All,
49    /// Every viewer.
50    Viewers,
51    /// Every daemon.
52    Daemons,
53    /// The daemon on one machine, by machine id.
54    Machine(String),
55    /// The singleton daemon instance of the plugin.
56    Singleton,
57}
58
59/// Durable key-value state on the account. Keys under the plugin's own id
60/// (`<id>.<rest>`) need no grant; others need `values.read:<prefix>` /
61/// `values.write:<prefix>`.
62pub mod values {
63    use super::*;
64
65    #[derive(Clone, Copy, Debug, Default)]
66    pub struct Values;
67
68    impl Values {
69        pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
70            match host::values_get(key)? {
71                Some(json) => Json(json).parse().map(Some),
72                None => Ok(None),
73            }
74        }
75
76        pub fn get_json(&self, key: &str) -> Result<Option<Json>> {
77            Ok(host::values_get(key)?.map(Json))
78        }
79
80        pub fn set<T: Serialize + ?Sized>(&self, key: &str, value: &T) -> Result<()> {
81            host::values_set(key, &Json::from_value(value)?.0)
82        }
83
84        pub fn delete(&self, key: &str) -> Result<()> {
85            host::values_delete(key)
86        }
87
88        /// The keys under `prefix`.
89        pub fn keys(&self, prefix: &str) -> Result<Vec<String>> {
90            host::values_keys(prefix)
91        }
92
93        /// Asks for [`crate::Event::ValueChanged`] for keys under `prefix`.
94        pub fn watch(&self, prefix: &str) -> Result<()> {
95            host::values_watch(prefix)
96        }
97    }
98}
99
100/// The unstored latest-value channel: a publisher replaces the value, a
101/// subscriber sees the newest one ([`crate::Event::Live`]). The own
102/// namespace needs no grant; others need `live.publish:<prefix>` /
103/// `live.subscribe:<prefix>`.
104pub mod live {
105    use super::*;
106
107    #[derive(Clone, Copy, Debug, Default)]
108    pub struct Live;
109
110    impl Live {
111        pub fn publish<T: Serialize + ?Sized>(&self, key: &str, payload: &T) -> Result<()> {
112            host::live_publish(key, &Json::from_value(payload)?.0)
113        }
114
115        /// Withdraws the latest value of `key`: subscribers hear
116        /// [`crate::Event::LiveDeleted`] (a pane that closed, a machine
117        /// that has nothing to report), and later subscribers see nothing.
118        pub fn delete(&self, key: &str) -> Result<()> {
119            host::live_delete(key)
120        }
121
122        pub fn subscribe(&self, prefix: &str) -> Result<()> {
123            host::live_subscribe(prefix)
124        }
125
126        pub fn unsubscribe(&self, prefix: &str) -> Result<()> {
127            host::live_unsubscribe(prefix)
128        }
129    }
130}
131
132/// Named, unstored messages between plugins ([`crate::Event::Plugin`]).
133/// The plugin's own namespace (`<id>.*`) needs no grant; `global.*`,
134/// another plugin's namespace and `system.*` need `events.emit:<ns>` or
135/// `events.on:<ns>`. Nothing may emit into `system.*`.
136pub mod events {
137    use super::*;
138
139    #[derive(Clone, Copy, Debug, Default)]
140    pub struct Events;
141
142    impl Events {
143        pub fn emit<T: Serialize + ?Sized>(
144            &self,
145            name: &str,
146            payload: &T,
147            to: Target,
148        ) -> Result<()> {
149            host::events_emit(name, &Json::from_value(payload)?.0, &to)
150        }
151
152        /// Listens to events whose names match `pattern` (`git.*`).
153        pub fn on(&self, pattern: &str) -> Result<()> {
154            host::events_on(pattern)
155        }
156
157        pub fn off(&self, pattern: &str) -> Result<()> {
158            host::events_off(pattern)
159        }
160    }
161}
162
163/// Request and response to the plugin's companion daemon. Grant:
164/// `call:<plugin id>`.
165///
166/// A UI plugin prefers [`Calls::send`] and [`Calls::call_async`]: neither
167/// blocks, so `event` and `frame` stay within their budgets while the
168/// companion works. [`Calls::call`] waits (10 s at most unless told
169/// otherwise, 30 s at most ever) and, in a browser without JSPI, answers
170/// [`Error::Unavailable`]. At most 32 sends and async calls of one plugin
171/// are in flight at once; past that they answer [`Error::RateLimited`].
172pub mod calls {
173    use super::*;
174
175    /// Identifies a [`Calls::call_async`] in its
176    /// [`Event::CallResult`](crate::Event::CallResult).
177    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
178    pub struct CallId(pub u64);
179
180    #[derive(Clone, Copy, Debug, Default)]
181    pub struct Calls;
182
183    impl Calls {
184        /// Calls `method` with a typed request and waits for the typed
185        /// response, 10 s at most.
186        pub fn call<Req: Serialize + ?Sized, Resp: DeserializeOwned>(
187            &self,
188            method: &str,
189            request: &Req,
190            to: Target,
191        ) -> Result<Resp> {
192            self.call_json(method, &Json::from_value(request)?, to)?
193                .parse()
194        }
195
196        pub fn call_json(&self, method: &str, request: &Json, to: Target) -> Result<Json> {
197            host::call(method, &request.0, &to, None).map(Json)
198        }
199
200        /// [`Calls::call_json`] waiting at most `timeout_ms` (30 s at most).
201        pub fn call_json_timeout(
202            &self,
203            method: &str,
204            request: &Json,
205            to: Target,
206            timeout_ms: u32,
207        ) -> Result<Json> {
208            host::call(method, &request.0, &to, Some(timeout_ms)).map(Json)
209        }
210
211        /// Sends `method` and returns at once; nothing answers. For commands
212        /// whose outcome arrives another way (a value, a live message).
213        pub fn send<Req: Serialize + ?Sized>(
214            &self,
215            method: &str,
216            request: &Req,
217            to: Target,
218        ) -> Result<()> {
219            host::call_send(method, &Json::from_value(request)?.0, &to)
220        }
221
222        /// Starts a call and returns at once; its answer arrives as
223        /// [`Event::CallResult`](crate::Event::CallResult) with the id
224        /// returned here, after 10 s at most.
225        pub fn call_async<Req: Serialize + ?Sized>(
226            &self,
227            method: &str,
228            request: &Req,
229            to: Target,
230        ) -> Result<CallId> {
231            host::call_async(method, &Json::from_value(request)?.0, &to, None).map(CallId)
232        }
233
234        /// [`Calls::call_async`] answered within `timeout_ms` (30 s at most).
235        pub fn call_async_timeout<Req: Serialize + ?Sized>(
236            &self,
237            method: &str,
238            request: &Req,
239            to: Target,
240            timeout_ms: u32,
241        ) -> Result<CallId> {
242            host::call_async(method, &Json::from_value(request)?.0, &to, Some(timeout_ms))
243                .map(CallId)
244        }
245    }
246}
247
248/// The plugin's configuration document, as the account stores it. No grant.
249pub mod config {
250    use super::*;
251
252    /// The configuration, deserialized.
253    pub fn get<T: DeserializeOwned>() -> Result<T> {
254        Json(host::config()).parse()
255    }
256
257    /// The configuration document as the host passed it.
258    pub fn json() -> Json {
259        Json(host::config())
260    }
261}
262
263/// The plugin's own report of how it is doing: shown beside its runtime
264/// state in the Plugins view (a UI plugin's, in that viewer) and by
265/// `standard plugin health` (a daemon plugin's, per machine). Each report
266/// replaces the last; a plugin that never reports is `ok`. No grant.
267pub mod health {
268    use super::*;
269
270    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
271    pub enum Health {
272        #[default]
273        Ok,
274        /// Working, with a problem the user may want to know about.
275        Degraded,
276        /// Not doing its job until something changes.
277        Failed,
278    }
279
280    /// Reports `health` with a short message (200 characters are kept).
281    pub fn set(health: Health, message: &str) {
282        host::health_set(health, message);
283    }
284
285    pub fn ok() {
286        set(Health::Ok, "");
287    }
288
289    pub fn degraded(message: &str) {
290        set(Health::Degraded, message);
291    }
292
293    pub fn failed(message: &str) {
294        set(Health::Failed, message);
295    }
296}
297
298/// Named secrets the user entered for this plugin. Grant: `secret:<NAME>`.
299pub mod secrets {
300    use super::*;
301
302    pub fn get(name: &str) -> Result<Option<String>> {
303        host::secret(name)
304    }
305}
306
307/// Read-only account state. Grant: `account.read`. Never what is inside a
308/// pane: no screen, scrollback, input or output exists in this API.
309pub mod account {
310    use super::*;
311
312    #[derive(Clone, Debug, PartialEq, Eq)]
313    pub struct Machine {
314        pub id: String,
315        pub name: String,
316        pub online: bool,
317    }
318
319    #[derive(Clone, Debug, PartialEq, Eq)]
320    pub struct Project {
321        pub id: String,
322        pub name: String,
323        pub machine: String,
324        pub path: String,
325    }
326
327    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
328    pub enum AgentStatus {
329        #[default]
330        None,
331        Working,
332        Idle,
333        WaitingForInput,
334    }
335
336    #[derive(Clone, Debug, Default, PartialEq, Eq)]
337    pub struct Pane {
338        pub id: String,
339        /// The pane's generation: 1 at creation, advanced by every restart
340        /// and restore; 0 while unknown. `<id>@<generation>` names one run
341        /// of the pane.
342        pub generation: u64,
343        pub machine: String,
344        pub project: Option<String>,
345        pub title: String,
346        /// The pane's current directory when the host knows it, else its
347        /// project root. A new directory arrives as a `changed` pane.
348        pub cwd: String,
349        pub cols: u32,
350        pub rows: u32,
351        /// The foreground program name, when known.
352        pub program: Option<String>,
353        /// The agent the pane runs, when one is detected.
354        pub agent: Option<String>,
355        pub agent_status: AgentStatus,
356    }
357
358    #[derive(Clone, Debug, Default, PartialEq, Eq)]
359    pub struct AccountState {
360        pub machines: Vec<Machine>,
361        pub projects: Vec<Project>,
362        pub panes: Vec<Pane>,
363    }
364
365    pub fn state() -> Result<AccountState> {
366        host::account_state()
367    }
368
369    /// Asks for [`crate::Event::AccountChanged`] on every change.
370    pub fn watch() -> Result<()> {
371        host::account_watch()
372    }
373}
374
375/// Exactly-once effects across the viewers that run a UI plugin.
376///
377/// Every viewer on the account runs its own instance of a UI plugin, so an
378/// effect written naively happens once per open viewer. Claim a key first;
379/// only the instance whose claim succeeds performs the effect. Keys live in
380/// the plugin's own namespace. No grant.
381pub mod claims {
382    use super::*;
383
384    #[derive(Clone, Copy, Debug, Default)]
385    pub struct Claims;
386
387    impl Claims {
388        /// Claims `key` for `ttl_ms`: true when this plugin session holds it
389        /// (renewed if it already did); another viewer, even on this machine,
390        /// or another daemon of a fleet plugin gets false; the claim expires
391        /// after `ttl_ms` unless renewed. Both worlds.
392        pub fn claim(&self, key: &str, ttl_ms: u32) -> Result<bool> {
393            host::claim(key, ttl_ms)
394        }
395
396        pub fn release(&self, key: &str) -> Result<()> {
397            host::release(key)
398        }
399
400        /// Runs `effect` only in the instance that wins the claim on `key`.
401        /// Returns whether it ran.
402        pub fn once(&self, key: &str, ttl_ms: u32, effect: impl FnOnce()) -> Result<bool> {
403            let won = self.claim(key, ttl_ms)?;
404            if won {
405                effect();
406            }
407            Ok(won)
408        }
409    }
410}
411
412/// Opens web pages in the user's browser. UI plugins only. Grant:
413/// `url.open:<host>` (`*.<domain>` for its subdomains, `*` for any host).
414pub mod url {
415    use super::*;
416
417    /// Opens an `https://` URL in the user's browser, on the machine the
418    /// viewer runs on (a new tab in a browser viewer). Only as a direct
419    /// result of user input: while handling a key, paste, pointer press or
420    /// command, or within a second after one, and once per input;
421    /// `Error::Invalid` otherwise or for a URL that is not plain https,
422    /// `Error::GrantDenied { grant: "url.open:<host>" }` for a host no
423    /// grant names.
424    pub fn open(url: &str) -> Result<()> {
425        host::url_open(url)
426    }
427}
428
429/// Viewer-local state. UI plugins only; no grant.
430pub mod view {
431    use super::*;
432
433    /// The viewer's theme colours as `0xRRGGBB`.
434    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
435    pub struct Theme {
436        pub fg: u32,
437        pub bg: u32,
438        /// Text receded toward the background: "grayed out".
439        pub recede_fg: u32,
440        pub recede_bg: u32,
441        pub accent: u32,
442        /// The terminal's 16 ANSI colours as the viewer resolved them.
443        pub palette: [u32; 16],
444    }
445
446    /// The xterm defaults of the 16 ANSI colours.
447    pub const XTERM_PALETTE: [u32; 16] = [
448        0x000000, 0xcd0000, 0x00cd00, 0xcdcd00, 0x0000ee, 0xcd00cd, 0x00cdcd, 0xe5e5e5, 0x7f7f7f,
449        0xff0000, 0x00ff00, 0xffff00, 0x5c5cff, 0xff00ff, 0x00ffff, 0xffffff,
450    ];
451
452    /// How far a fully grayed-out colour recedes toward the background,
453    /// in percent: the viewer's own rule.
454    pub const GRAYED_OUT_PERCENT: u16 = 55;
455
456    impl Default for Theme {
457        fn default() -> Self {
458            Self {
459                fg: 0xd0d0d0,
460                bg: 0x000000,
461                recede_fg: 0x808080,
462                recede_bg: 0x101010,
463                accent: 0x5fafff,
464                palette: XTERM_PALETTE,
465            }
466        }
467    }
468
469    impl Theme {
470        /// `rgb` receded `percent` of the way toward this theme's
471        /// background: how the viewer grays things out, keeping the
472        /// colour's hue on light, dark and tinted themes alike (never a
473        /// fixed gray). [`GRAYED_OUT_PERCENT`] is fully grayed out.
474        pub fn recede(&self, rgb: u32, percent: u16) -> u32 {
475            crate::colour::recede(rgb, self.bg, percent)
476        }
477
478        /// ANSI colour `index` (0 to 15) as this viewer shows it.
479        pub fn ansi(&self, index: u8) -> u32 {
480            self.palette[usize::from(index & 15)]
481        }
482    }
483
484    /// Whether this viewer is the one the user is controlling. Several
485    /// viewers can show the same account at once; only the driving one
486    /// should act on the user's behalf.
487    pub fn is_driving() -> bool {
488        host::is_driving()
489    }
490
491    pub fn theme() -> Theme {
492        host::theme()
493    }
494
495    /// The focused pane's id, when one is focused.
496    pub fn focused_pane() -> Option<String> {
497        host::focused_pane()
498    }
499
500    /// A pane and its generation.
501    #[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
502    pub struct PaneRef {
503        pub id: String,
504        /// 0 while the viewer does not know it.
505        pub generation: u64,
506    }
507
508    impl PaneRef {
509        /// `<id>@<generation>`: a key for this run of the pane, so state
510        /// kept for a pane that restarted is not shown for its next run.
511        pub fn key(&self) -> String {
512            alloc::format!("{}@{}", self.id, self.generation)
513        }
514    }
515
516    /// The pane an instance surface (`<surface>@<pane>`, a `pane.footer`
517    /// or `pane.header` instance) belongs to, with its generation now.
518    pub fn surface_pane(surface: &str) -> Option<PaneRef> {
519        host::surface_pane(surface)
520    }
521
522    /// The machine an instance surface (`<surface>@<machine>`, a
523    /// `machine.after` instance) belongs to.
524    pub fn surface_machine(surface: &str) -> Option<String> {
525        host::surface_machine(surface)
526    }
527
528    /// The project an instance surface (`<surface>@<project>`, a
529    /// `project.after` instance) belongs to.
530    pub fn surface_project(surface: &str) -> Option<String> {
531        host::surface_project(surface)
532    }
533
534    /// The identity tint (`0xRRGGBB`) of what an instance surface belongs
535    /// to: a machine row's machine, a project row's project, a pane
536    /// footer's project (else its machine), as the viewer paints it.
537    pub fn surface_tint(surface: &str) -> Option<u32> {
538        host::surface_tint(surface)
539    }
540
541    /// The identity tint of a machine or project, by id.
542    pub fn identity_tint(id: &str) -> Option<u32> {
543        host::identity_tint(id)
544    }
545
546    /// The machine this viewer runs on, by the account's machine id; `None`
547    /// in a viewer that is not an enrolled machine (a browser).
548    pub fn machine_id() -> Option<String> {
549        host::machine_id()
550    }
551
552    /// This viewer instance: shared by every plugin of one running viewer,
553    /// different in every other viewer and after a restart. A key such as
554    /// `<plugin>.seen.<instance>` is this viewer's own.
555    pub fn instance_id() -> String {
556        host::instance_id()
557    }
558
559    /// The wall clock where the viewer runs, in milliseconds since the Unix
560    /// epoch: for dates, countdowns and "updated 3 min ago". Animation keeps
561    /// to [`Frame::now_ms`](crate::Frame::now_ms).
562    pub fn wall_ms() -> u64 {
563        host::wall_ms()
564    }
565
566    /// The viewer's local offset from UTC right now, in minutes east
567    /// (UTC+2 is 120). It follows daylight saving: read it when you format
568    /// a time, not once at activation.
569    pub fn utc_offset_minutes() -> i32 {
570        host::utc_offset_minutes()
571    }
572
573    /// The viewer's IANA time zone (`Europe/Berlin`), when known.
574    pub fn time_zone() -> Option<String> {
575        host::time_zone()
576    }
577
578    /// The viewer's local wall-clock time in milliseconds since the Unix
579    /// epoch shifted by the offset: `local_ms() / 86_400_000` is the local
580    /// day number, and the remainder the time of day.
581    pub fn local_ms() -> i64 {
582        host::wall_ms() as i64 + i64::from(host::utc_offset_minutes()) * 60_000
583    }
584}
585
586/// What this viewer can show. UI plugins only; no grant. Changes arrive as
587/// [`crate::Event::CapabilitiesChanged`].
588pub mod capabilities {
589    use super::*;
590
591    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
592    pub struct Capabilities {
593        /// Whether the graphics model paints as real graphics here. Without
594        /// it the viewer shows a pixels surface as half-block cells.
595        pub graphics: bool,
596        /// Whether graphics go through the kitty graphics protocol. The
597        /// viewer's only raster layer is kitty's, so this equals
598        /// `graphics` today.
599        pub kitty: bool,
600        /// The paced frame rate the viewer runs at right now.
601        pub frame_rate: u32,
602        /// The pixel size of one cell (zero when unknown).
603        pub cell_px: (u32, u32),
604        /// Device pixels per surface pixel: 2 while the viewer has halved
605        /// this plugin's pixel resolution because its frames ran long.
606        pub pixel_scale: u32,
607    }
608
609    pub fn get() -> Capabilities {
610        host::capabilities()
611    }
612}
613
614pub fn values() -> values::Values {
615    values::Values
616}
617
618pub fn live() -> live::Live {
619    live::Live
620}
621
622pub fn events() -> events::Events {
623    events::Events
624}
625
626pub fn calls() -> calls::Calls {
627    calls::Calls
628}
629
630pub fn claims() -> claims::Claims {
631    claims::Claims
632}