standard-plugin-sdk 0.1.1

Write Standard Code plugins in Rust: wasm components against standard:plugin@2.0.0
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
//! UI plugins: the [`UiPlugin`] trait, what it is called with, and the
//! runtime the `#[ui]` export forwards to.

use alloc::string::String;
use alloc::vec::Vec;
use core::cell::{Cell, RefCell};

use serde::de::DeserializeOwned;

use crate::api::{self, Json};
use crate::bindings::ui as wit;
use crate::error::Result;
use crate::geometry::Geometry;
pub use crate::input::{Button, Key, KeyCode, KeyPhase, Modifiers, Pointer, PointerKind};

/// A UI plugin: one instance runs in every viewer on the account.
///
/// It paints its [`crate::Surface`]s and reads account state; it is
/// side-effect free. Anything that must happen once (a write, a
/// notification, a request) is either claimed first ([`claims`](mod@crate::claims)) or
/// asked of a companion daemon ([`calls`](mod@crate::calls)); [`crate::view::is_driving`]
/// says whether the user is controlling this viewer.
pub trait UiPlugin: Sized + 'static {
    /// Called once after instantiation. Create surfaces and read the
    /// configuration here.
    fn activate(cx: &mut Context) -> Self;

    /// Called when a paced frame is due and one of the plugin's surfaces is
    /// visible. Paint, commit, and say when the next frame is wanted
    /// ([`Frame::request_frame`], [`Frame::wake_at`]); asking for nothing
    /// waits for an event.
    fn frame(&mut self, frame: &Frame);

    /// Everything else the host tells the plugin. Surfaces are already
    /// rebound when a [`Event::Resize`] arrives.
    fn event(&mut self, event: Event, cx: &mut Context) {
        let _ = (event, cx);
    }

    /// Called before the instance is dropped.
    fn deactivate(&mut self) {}
}

/// What a plugin is called with besides a frame: its configuration, and
/// handles to every interface.
#[derive(Debug, Default)]
pub struct Context {
    config: String,
}

impl Context {
    pub const fn new() -> Self {
        Self {
            config: String::new(),
        }
    }

    /// The configuration document `activate` was called with.
    pub fn config_json(&self) -> Json {
        Json(self.config.clone())
    }

    /// The configuration, deserialized (an empty document is `null`).
    pub fn config<T: DeserializeOwned>(&self) -> Result<T> {
        if self.config.trim().is_empty() {
            return Json(String::from("null")).parse();
        }
        Json(self.config.clone()).parse()
    }

    pub fn values(&self) -> api::values::Values {
        api::values()
    }

    pub fn live(&self) -> api::live::Live {
        api::live()
    }

    pub fn events(&self) -> api::events::Events {
        api::events()
    }

    pub fn calls(&self) -> api::calls::Calls {
        api::calls()
    }

    pub fn claims(&self) -> api::claims::Claims {
        api::claims()
    }

    pub fn is_driving(&self) -> bool {
        api::view::is_driving()
    }

    pub fn capabilities(&self) -> api::capabilities::Capabilities {
        api::capabilities::get()
    }

    /// Asks for a frame on the next paced frame, from an event handler
    /// (inside [`UiPlugin::frame`], use [`Frame::request_frame`]). A commit
    /// in `event` is otherwise sampled on the next frame something else
    /// causes.
    pub fn request_frame(&self) {
        crate::host::request_frame();
    }

    /// Asks for a frame at `at_ms` on the viewer's clock.
    pub fn wake_at(&self, at_ms: u64) {
        crate::host::wake_at(at_ms);
    }

    /// Opens a `stage` surface. Only while handling a user gesture: a key,
    /// a paste, a pointer press on one of the plugin's surfaces, or one of
    /// its commands.
    pub fn open(&self, surface: &str) -> crate::Result<()> {
        crate::host::surface_open(surface)
    }

    /// Shows a pane in this viewer's workspace and gives it focus, as a
    /// click on its sidebar row does. Only while handling a user gesture,
    /// as [`Context::open`]; a pane the viewer does not know is ignored.
    pub fn focus_pane(&self, pane: &str) -> crate::Result<()> {
        crate::host::focus_pane(pane)
    }

    /// Opens an `https://` URL in the user's browser ([`crate::url::open`]):
    /// while handling user input, or within a second after it.
    pub fn open_url(&self, url: &str) -> crate::Result<()> {
        crate::host::url_open(url)
    }

    /// Closes a surface [`Context::open`] opened.
    pub fn close(&self, surface: &str) -> crate::Result<()> {
        crate::host::surface_close(surface)
    }

    /// Asks for `surface` to be `cols` by `rows` cells (0: the viewer's
    /// choice in that dimension) instead of the manifest's size. The
    /// viewer clamps it where it places the surface and sends
    /// [`Event::Resize`] when the size changes. A card grows to show more
    /// rows; a popover fits its content.
    pub fn request_size(&self, surface: &str, cols: u32, rows: u32) -> crate::Result<()> {
        crate::host::surface_request_size(surface, cols, rows)
    }

    /// Sets a short label the viewer shows at the right end of `surface`'s
    /// chrome title: a boxed card's top border, a pane's header, a column's
    /// title row. `None` removes it. At most 48 characters with no control
    /// characters.
    pub fn set_label(&self, surface: &str, label: Option<&str>) -> crate::Result<()> {
        crate::host::surface_set_label(surface, label)
    }

    /// Places the viewer's own text cursor at (`col`, `row`) of `surface`
    /// while the surface takes keys (an open stage, popover or pane
    /// overlay, or the surface a click or `open` gave input focus), so a
    /// text field draws no cursor of its own; the cursor trail moves to it.
    /// `None` removes it. Stays until set again.
    pub fn set_caret(&self, surface: &str, caret: Option<(u32, u32)>) -> crate::Result<()> {
        crate::host::surface_set_caret(surface, caret)
    }
}

/// Whether the viewer's machine is saving power.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum Power {
    #[default]
    Mains,
    SavePower,
}

impl From<wit::Power> for Power {
    fn from(power: wit::Power) -> Self {
        match power {
            wit::Power::Mains => Self::Mains,
            wit::Power::SavePower => Self::SavePower,
        }
    }
}

/// One frame callback.
#[derive(Debug)]
pub struct Frame {
    now_ms: u64,
    interval_ms: u32,
    power: Power,
    elapsed_ms: u64,
    next: Cell<Option<u64>>,
}

impl Frame {
    /// A frame at `now_ms` (for tests; the runtime makes the real ones).
    pub fn new(now_ms: u64, interval_ms: u32, power: Power) -> Self {
        Self {
            now_ms,
            interval_ms,
            power,
            elapsed_ms: 0,
            next: Cell::new(None),
        }
    }

    /// Milliseconds since the previous frame; zero on the first frame, and
    /// on the first frame after the plugin's surfaces were all hidden (a
    /// hidden period is not play time).
    pub fn elapsed(&self) -> u64 {
        self.elapsed_ms
    }

    /// The viewer's monotonic clock, in milliseconds.
    pub fn now_ms(&self) -> u64 {
        self.now_ms
    }

    /// The paced frame interval.
    pub fn interval_ms(&self) -> u32 {
        self.interval_ms
    }

    pub fn power(&self) -> Power {
        self.power
    }

    /// Asks for the next paced frame (an animation).
    pub fn request_frame(&self) {
        self.wake_at(self.now_ms);
    }

    /// Asks for a frame at `at_ms` on the viewer's clock (a clock asks for
    /// the next second). The earliest request wins.
    pub fn wake_at(&self, at_ms: u64) {
        let next = self.next.get().map_or(at_ms, |next| next.min(at_ms));
        self.next.set(Some(next));
    }

    /// Asks for a frame `after_ms` from now.
    pub fn wake_after(&self, after_ms: u64) {
        self.wake_at(self.now_ms.saturating_add(after_ms));
    }

    /// When the plugin asked to be called next.
    pub fn next_wake(&self) -> Option<u64> {
        self.next.get()
    }
}

/// What the host tells a plugin besides frames.
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Event {
    /// A surface changed size. The SDK has rebound it; repaint it.
    Resize { surface: String, geometry: Geometry },
    /// A surface became visible or hidden.
    Visibility { surface: String, visible: bool },
    /// Graphics support, cell pixel size or frame rate changed
    /// ([`capabilities`](mod@crate::capabilities)).
    CapabilitiesChanged,
    /// [`crate::view::is_driving`] changed.
    Driving(bool),
    /// A watched value changed ([`crate::values::Values::watch`]).
    ValueChanged { key: String, value: Option<Json> },
    /// A live message ([`crate::live::Live::subscribe`]).
    Live { key: String, payload: Json },
    /// A live key was withdrawn ([`crate::live::Live::delete`]).
    LiveDeleted { key: String },
    /// A plugin event ([`crate::events::Events::on`]).
    Plugin { name: String, payload: Json },
    /// The account changed ([`crate::account::watch`]).
    AccountChanged,
    /// A key on the surface with input focus.
    Key(Key),
    /// Text pasted into the surface with input focus.
    Paste { surface: String, text: String },
    /// The pointer over one of the plugin's surfaces.
    Pointer(Pointer),
    /// The viewer's theme changed ([`crate::view::theme`]).
    ThemeChanged,
    /// A surface gained or lost input focus.
    Focus { surface: String, focused: bool },
    /// The user ran one of the manifest's commands, by id.
    Command(String),
    /// A [`Calls::call_async`](crate::calls::Calls::call_async) finished:
    /// the companion's answer, or why there is none (a timeout is
    /// [`Error::Unavailable`](crate::Error::Unavailable)).
    CallResult {
        id: crate::api::calls::CallId,
        result: Result<Json>,
    },
    /// Daemon plugins: files under a [`watch`](crate::daemon::watch)
    /// changed; every path that changed since the last delivery.
    FileChanged { watch: u32, paths: Vec<String> },
    /// Daemon plugins: a watch stopped delivering (the watcher's error).
    WatchFailed { watch: u32, error: String },
    /// Daemon plugins: output from a child's piped stream.
    ProcessOutput {
        pid: u32,
        stderr: bool,
        bytes: Vec<u8>,
    },
    /// Daemon plugins: a child exited (a signal is negated), after the
    /// last of its output.
    ProcessExited { pid: u32, status: i32 },
    /// Daemon plugins: a pane on the daemon's machine changed
    /// ([`panes::subscribe`](crate::daemon::panes::subscribe)).
    PaneChanged {
        kind: crate::daemon::PaneChangeKind,
        pane: crate::api::account::Pane,
    },
    /// Daemon plugins: which of the plugin's UI surfaces the account's
    /// viewers show changed ([`Context::interest`](crate::daemon::Context::interest)).
    /// Start reading an outside source when [`any`](crate::daemon::Interest::any)
    /// turns true and stop when it turns false.
    Interest(crate::daemon::Interest),
}

impl From<wit::Event> for Event {
    fn from(event: wit::Event) -> Self {
        match event {
            wit::Event::Resize((surface, g)) => Self::Resize {
                surface,
                geometry: Geometry {
                    cols: g.cols,
                    rows: g.rows,
                    px_w: g.px_w,
                    px_h: g.px_h,
                    cell_px_w: g.cell_px_w,
                    cell_px_h: g.cell_px_h,
                },
            },
            wit::Event::Visibility((surface, visible)) => Self::Visibility { surface, visible },
            wit::Event::CapabilitiesChanged => Self::CapabilitiesChanged,
            wit::Event::Driving(driving) => Self::Driving(driving),
            wit::Event::ValueChanged((key, value)) => Self::ValueChanged {
                key,
                value: value.map(Json),
            },
            wit::Event::Live((key, payload)) => Self::Live {
                key,
                payload: Json(payload),
            },
            wit::Event::LiveDeleted(key) => Self::LiveDeleted { key },
            wit::Event::PluginEvent((name, payload)) => Self::Plugin {
                name,
                payload: Json(payload),
            },
            wit::Event::AccountChanged => Self::AccountChanged,
            wit::Event::Key(key) => Self::Key(Key {
                surface: key.surface,
                code: KeyCode::parse(&key.code),
                text: key.text,
                modifiers: Modifiers(key.modifiers),
                phase: match key.phase {
                    wit::standard::plugin::event_types::KeyPhase::Press => KeyPhase::Press,
                    wit::standard::plugin::event_types::KeyPhase::Repeat => KeyPhase::Repeat,
                    wit::standard::plugin::event_types::KeyPhase::Release => KeyPhase::Release,
                },
            }),
            wit::Event::Paste((surface, text)) => Self::Paste { surface, text },
            wit::Event::Pointer(pointer) => Self::Pointer(Pointer {
                surface: pointer.surface,
                x: pointer.x,
                y: pointer.y,
                col: pointer.col,
                row: pointer.row,
                button: Button::from_wire(pointer.button),
                kind: match pointer.kind {
                    wit::standard::plugin::event_types::PointerKind::Down => PointerKind::Down,
                    wit::standard::plugin::event_types::PointerKind::Up => PointerKind::Up,
                    wit::standard::plugin::event_types::PointerKind::Move => PointerKind::Move,
                    wit::standard::plugin::event_types::PointerKind::Drag => PointerKind::Drag,
                    wit::standard::plugin::event_types::PointerKind::Wheel => PointerKind::Wheel,
                    wit::standard::plugin::event_types::PointerKind::Enter => PointerKind::Enter,
                    wit::standard::plugin::event_types::PointerKind::Leave => PointerKind::Leave,
                },
                modifiers: Modifiers(pointer.modifiers),
            }),
            wit::Event::ThemeChanged => Self::ThemeChanged,
            wit::Event::Focus((surface, focused)) => Self::Focus { surface, focused },
            wit::Event::Command(id) => Self::Command(id),
            wit::Event::CallResult(answer) => Self::CallResult {
                id: crate::api::calls::CallId(answer.id),
                result: answer.outcome.map(Json).map_err(Into::into),
            },
        }
    }
}

/// The instance's plugin and context, driven by the exports.
#[doc(hidden)]
pub struct UiRuntime<P> {
    plugin: RefCell<Option<P>>,
    cx: RefCell<Context>,
    /// The previous frame's clock; `None` at the start and after every
    /// surface was hidden.
    last_frame_ms: Cell<Option<u64>>,
    /// Surfaces the viewer shows now.
    visible: RefCell<alloc::collections::BTreeSet<String>>,
}

// SAFETY: a component instance runs on one thread and the host calls one
// export at a time; the exports are the only users of the static runtime.
// On other targets the static is never reached (tests drive their own
// runtime through `testing::Harness`).
unsafe impl<P> Sync for UiRuntime<P> {}

impl<P> Default for UiRuntime<P> {
    fn default() -> Self {
        Self::new()
    }
}

impl<P> UiRuntime<P> {
    pub const fn new() -> Self {
        Self {
            plugin: RefCell::new(None),
            cx: RefCell::new(Context::new()),
            last_frame_ms: Cell::new(None),
            visible: RefCell::new(alloc::collections::BTreeSet::new()),
        }
    }
}

impl<P: UiPlugin> UiRuntime<P> {
    pub fn activate(&self, config: String) {
        let mut cx = self.cx.borrow_mut();
        cx.config = config;
        let plugin = P::activate(&mut cx);
        *self.plugin.borrow_mut() = Some(plugin);
    }

    pub fn frame(&self, now_ms: u64, interval_ms: u32, power: Power) -> Option<u64> {
        let mut frame = Frame::new(now_ms, interval_ms, power);
        frame.elapsed_ms = self
            .last_frame_ms
            .get()
            .map_or(0, |last| now_ms.saturating_sub(last));
        self.last_frame_ms.set(Some(now_ms));
        if let Some(plugin) = self.plugin.borrow_mut().as_mut() {
            plugin.frame(&frame);
        }
        frame.next_wake()
    }

    pub fn event(&self, event: Event) {
        match &event {
            Event::Resize { surface, .. } => crate::surface::rebind_surfaces(surface),
            Event::Visibility { surface, visible } => {
                let mut shown = self.visible.borrow_mut();
                if *visible {
                    shown.insert(surface.clone());
                } else {
                    shown.remove(surface);
                    if shown.is_empty() {
                        self.last_frame_ms.set(None);
                    }
                }
            }
            _ => {}
        }
        let mut cx = self.cx.borrow_mut();
        if let Some(plugin) = self.plugin.borrow_mut().as_mut() {
            plugin.event(event, &mut cx);
        }
    }

    pub fn deactivate(&self) {
        let plugin = self.plugin.borrow_mut().take();
        if let Some(mut plugin) = plugin {
            plugin.deactivate();
        }
    }

    /// The plugin, for tests.
    pub fn with_plugin<R>(&self, f: impl FnOnce(&mut P) -> R) -> Option<R> {
        self.plugin.borrow_mut().as_mut().map(f)
    }
}