Skip to main content

together_companion/
together_companion.rs

1//! The companion daemon of `together_ui.rs`: the same plugin id, kind
2//! `companion`. It runs once (the singleton daemon), does the work, stores
3//! the result as a durable value and announces it with an event; the UI
4//! halves in every viewer read the value.
5//!
6//! Manifest:
7//!
8//! ```json
9//! {
10//!   "apiVersion": 2, "kind": "companion", "id": "together", "version": "0.1.0",
11//!   "module": "together_companion.wasm",
12//!   "grants": { "process.exec:make": "Runs make in your project when you ask for a build" }
13//! }
14//! ```
15#![no_std]
16
17extern crate alloc;
18
19use alloc::rc::Rc;
20use core::cell::Cell;
21
22use serde::Serialize;
23use standard_plugin::daemon::{Context, DaemonPlugin, process};
24use standard_plugin::prelude::{Error, Json, Target, format};
25
26#[derive(Serialize)]
27struct Status<'a> {
28    state: &'a str,
29    builds: u32,
30}
31
32/// Stores the status where every viewer's UI half reads it, and tells
33/// them it changed.
34fn publish(cx: &Context, state: &str, builds: u32) {
35    let _ = cx
36        .values()
37        .set("together.status", &Status { state, builds });
38    let _ = cx.events().emit("together.changed", &(), Target::Viewers);
39}
40
41/// One build, run as a task on the daemon's executor.
42async fn rebuild(cx: Context, builds: Rc<Cell<u32>>, running: Rc<Cell<bool>>) {
43    publish(&cx, "building", builds.get());
44    let passed = process::spawn("make", &[], None)
45        .and_then(|child| child.wait())
46        .is_ok_and(|status| status == 0);
47    builds.set(builds.get() + 1);
48    running.set(false);
49    publish(&cx, if passed { "passed" } else { "failed" }, builds.get());
50}
51
52#[standard_plugin::daemon]
53struct Companion {
54    builds: Rc<Cell<u32>>,
55    running: Rc<Cell<bool>>,
56}
57
58impl DaemonPlugin for Companion {
59    fn activate(_cx: &Context) -> Self {
60        Self {
61            builds: Rc::new(Cell::new(0)),
62            running: Rc::new(Cell::new(false)),
63        }
64    }
65
66    async fn run(&self, cx: Context) {
67        publish(&cx, "idle", self.builds.get());
68    }
69
70    /// Calls arrive from the UI half. A handler answers at once; longer
71    /// work goes to a task, which runs when the host next drives the
72    /// plugin (right after the call).
73    fn call(&self, method: &str, _request: Json, cx: &Context) -> Result<Json, Error> {
74        match method {
75            "rebuild" => {
76                let started = !self.running.replace(true);
77                if started {
78                    cx.spawn(rebuild(
79                        cx.clone(),
80                        self.builds.clone(),
81                        self.running.clone(),
82                    ));
83                }
84                Json::from_value(&started)
85            }
86            other => Err(Error::Invalid(format!("unknown method {other}"))),
87        }
88    }
89}