Skip to main content

together_ui/
together_ui.rs

1//! The UI half of a plugin that works together with its companion daemon
2//! (`together_companion.rs`). It runs in every viewer, so it only *shows*
3//! state and *asks*; the companion does the work once.
4//!
5//! - The companion keeps the build status in a durable value
6//!   (`together.status`) and announces changes as a plugin event
7//!   (`together.changed`); this card reads the value and repaints when the
8//!   event arrives. Both are the plugin's own namespace: no grant.
9//! - A click on the card asks the companion to rebuild through a call
10//!   (grant `call:together`). Only the viewer the user is driving sends
11//!   it, and a claim makes sure one request goes out even if two viewers
12//!   think they are driving.
13//!
14//! Manifest:
15//!
16//! ```json
17//! {
18//!   "apiVersion": 2, "kind": "ui", "id": "together", "version": "0.1.0",
19//!   "module": "together_ui.wasm",
20//!   "surfaces": [{ "id": "card", "anchor": "sidebar.card", "model": "cells", "height": 1 }],
21//!   "grants": { "call:together": "Asks the companion on your machine to start a build" }
22//! }
23//! ```
24#![no_std]
25
26use serde::Deserialize;
27use standard_plugin::prelude::*;
28use standard_plugin::{claims, view};
29
30#[derive(Debug, Default, Deserialize)]
31pub struct Status {
32    pub state: String,
33    pub builds: u32,
34}
35
36#[standard_plugin::ui]
37pub struct Together {
38    card: Surface<Cells>,
39    status: Status,
40}
41
42impl Together {
43    fn reload(&mut self, cx: &Context) {
44        // A missing value (the companion has not run yet) keeps the default.
45        if let Ok(Some(status)) = cx.values().get::<Status>("together.status") {
46            self.status = status;
47        }
48    }
49
50    /// Paints the whole card and commits. Called from `frame` and straight
51    /// from `event`: a commit wakes the viewer, so an event handler can
52    /// repaint without waiting for a frame.
53    fn paint(&mut self) {
54        let (cols, _) = self.card.size();
55        self.card.clear();
56        let line = format!("{} ยท {} builds", self.status.state, self.status.builds);
57        self.card.text(0, 0, &line, Style::fg(Colour::FG));
58        let hint = "rebuild";
59        let x = cols.saturating_sub(hint.len() as u32);
60        self.card
61            .text(x, 0, hint, Style::fg(Colour::ACCENT).underline());
62        let _ = self.card.commit();
63    }
64}
65
66impl UiPlugin for Together {
67    fn activate(cx: &mut Context) -> Self {
68        let _ = cx.events().on("together.*");
69        let mut plugin = Self {
70            card: Surface::new("card"),
71            status: Status::default(),
72        };
73        plugin.reload(cx);
74        plugin
75    }
76
77    /// The first frame after the card becomes visible; after that the card
78    /// repaints on events and asks for no frames.
79    fn frame(&mut self, _frame: &Frame) {
80        self.paint();
81    }
82
83    fn event(&mut self, event: Event, cx: &mut Context) {
84        match event {
85            Event::Plugin { name, .. } if name == "together.changed" => {
86                self.reload(cx);
87                self.paint();
88                // A commit from an event is sampled on the next frame;
89                // ask for one.
90                cx.request_frame();
91            }
92            Event::Resize { .. } => {
93                self.paint();
94                cx.request_frame();
95            }
96            Event::Pointer(pointer) if pointer.kind == PointerKind::Down => {
97                // Side-effect free unless this viewer is the one the user
98                // is driving, and then exactly once across viewers.
99                if !view::is_driving() {
100                    return;
101                }
102                // `send` returns at once: the companion's answer arrives as
103                // the value it writes, not here.
104                let _ = claims().once("together.rebuild", 5_000, || {
105                    let _ = cx.calls().send("rebuild", &(), Target::Singleton);
106                });
107            }
108            _ => {}
109        }
110    }
111}