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}