standard-plugin-sdk 0.1.1

Write Standard Code plugins in Rust: wasm components against standard:plugin@2.0.0
Documentation
//! The UI half of a plugin that works together with its companion daemon
//! (`together_companion.rs`). It runs in every viewer, so it only *shows*
//! state and *asks*; the companion does the work once.
//!
//! - The companion keeps the build status in a durable value
//!   (`together.status`) and announces changes as a plugin event
//!   (`together.changed`); this card reads the value and repaints when the
//!   event arrives. Both are the plugin's own namespace: no grant.
//! - A click on the card asks the companion to rebuild through a call
//!   (grant `call:together`). Only the viewer the user is driving sends
//!   it, and a claim makes sure one request goes out even if two viewers
//!   think they are driving.
//!
//! Manifest:
//!
//! ```json
//! {
//!   "apiVersion": 2, "kind": "ui", "id": "together", "version": "0.1.0",
//!   "module": "together_ui.wasm",
//!   "surfaces": [{ "id": "card", "anchor": "sidebar.card", "model": "cells", "height": 1 }],
//!   "grants": { "call:together": "Asks the companion on your machine to start a build" }
//! }
//! ```
#![no_std]

use serde::Deserialize;
use standard_plugin::prelude::*;
use standard_plugin::{claims, view};

#[derive(Debug, Default, Deserialize)]
pub struct Status {
    pub state: String,
    pub builds: u32,
}

#[standard_plugin::ui]
pub struct Together {
    card: Surface<Cells>,
    status: Status,
}

impl Together {
    fn reload(&mut self, cx: &Context) {
        // A missing value (the companion has not run yet) keeps the default.
        if let Ok(Some(status)) = cx.values().get::<Status>("together.status") {
            self.status = status;
        }
    }

    /// Paints the whole card and commits. Called from `frame` and straight
    /// from `event`: a commit wakes the viewer, so an event handler can
    /// repaint without waiting for a frame.
    fn paint(&mut self) {
        let (cols, _) = self.card.size();
        self.card.clear();
        let line = format!("{} ยท {} builds", self.status.state, self.status.builds);
        self.card.text(0, 0, &line, Style::fg(Colour::FG));
        let hint = "rebuild";
        let x = cols.saturating_sub(hint.len() as u32);
        self.card
            .text(x, 0, hint, Style::fg(Colour::ACCENT).underline());
        let _ = self.card.commit();
    }
}

impl UiPlugin for Together {
    fn activate(cx: &mut Context) -> Self {
        let _ = cx.events().on("together.*");
        let mut plugin = Self {
            card: Surface::new("card"),
            status: Status::default(),
        };
        plugin.reload(cx);
        plugin
    }

    /// The first frame after the card becomes visible; after that the card
    /// repaints on events and asks for no frames.
    fn frame(&mut self, _frame: &Frame) {
        self.paint();
    }

    fn event(&mut self, event: Event, cx: &mut Context) {
        match event {
            Event::Plugin { name, .. } if name == "together.changed" => {
                self.reload(cx);
                self.paint();
                // A commit from an event is sampled on the next frame;
                // ask for one.
                cx.request_frame();
            }
            Event::Resize { .. } => {
                self.paint();
                cx.request_frame();
            }
            Event::Pointer(pointer) if pointer.kind == PointerKind::Down => {
                // Side-effect free unless this viewer is the one the user
                // is driving, and then exactly once across viewers.
                if !view::is_driving() {
                    return;
                }
                // `send` returns at once: the companion's answer arrives as
                // the value it writes, not here.
                let _ = claims().once("together.rebuild", 5_000, || {
                    let _ = cx.calls().send("rebuild", &(), Target::Singleton);
                });
            }
            _ => {}
        }
    }
}