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}