Skip to main content

standard_plugin/
lib.rs

1//! Write Standard Code plugins in Rust.
2//!
3//! Every plugin is a wasm component against the WIT package
4//! `standard:plugin@2.0.0`, built for `wasm32-wasip2`. This crate wraps the
5//! generated bindings in a safe API:
6//!
7//! - **UI plugins** ([`UiPlugin`], [`#[ui]`](macro@ui)) run in every viewer and paint
8//!   [`Surface`]s in the cell model ([`Cells`]) or the graphics model
9//!   ([`Pixels`]). They are side-effect free: effects that must happen once
10//!   go through [`claims`](mod@claims) or a companion daemon ([`calls`](mod@calls)), and
11//!   [`view::is_driving`] says whether the user is controlling this viewer.
12//! - **Daemon plugins** ([`daemon::DaemonPlugin`], [`#[daemon]`](macro@daemon))
13//!   run inside `standardd` with an async `run` loop on a small executor.
14//!
15//! The account interfaces ([`values`](mod@values), [`live`](mod@live), [`events`](mod@events), [`calls`](mod@calls),
16//! [`config`](mod@config), [`secrets`](mod@secrets), [`account`](mod@account)) are shared by both. Every call the
17//! plugin's manifest grants do not cover returns [`Error::GrantDenied`].
18//!
19//! A UI plugin is `#![no_std]` (the `std` library would import the WASI CLI
20//! world, which a viewer does not offer); `alloc` is available and this
21//! crate's default `runtime` feature supplies the allocator and panic
22//! handler. A UI plugin, in full:
23//!
24//! ```rust,ignore
25#![doc = include_str!("../examples/clock_card.rs")]
26//! ```
27//!
28//! More: the [`guide`] pages (the same Markdown files are this crate's
29//! `docs/` directory, and `standard-plugin guide` prints them), and the
30//! compiled examples in this crate's `examples/` directory. `testing` (on
31//! non-wasm targets) runs a plugin against an in-process host in ordinary
32//! `cargo test`.
33#![no_std]
34
35extern crate alloc;
36#[cfg(any(feature = "std", not(target_arch = "wasm32")))]
37extern crate std;
38
39#[cfg(target_endian = "big")]
40compile_error!("the shared surface buffer is little-endian");
41
42mod bindings;
43mod colour;
44mod error;
45mod geometry;
46mod host;
47mod input;
48mod local;
49#[cfg(all(target_arch = "wasm32", not(feature = "std")))]
50mod runtime;
51mod ui_runtime;
52
53pub mod api;
54pub mod daemon;
55pub mod draw;
56pub mod surface;
57#[cfg(not(target_arch = "wasm32"))]
58pub mod testing;
59
60pub use api::{
61    Json, Target, account, calls, capabilities, claims, config, events, health, live, secrets, url,
62    values, view,
63};
64pub use colour::{Attrs, Colour, Rgba, Style, ThemeSlot, recede};
65pub use draw::{Area, Image, Shapes, font};
66pub use error::{Error, Result};
67pub use geometry::{Geometry, Rect};
68pub use input::{Button, Key, KeyCode, KeyPhase, Modifiers, Pointer, PointerKind};
69pub use surface::{AnySurface, AnySurfaceMut, Cell, Cells, Instances, Pixels, Surface};
70pub use ui_runtime::{Context, Event, Frame, Power, UiPlugin};
71
72/// Exports the annotated type, which implements [`UiPlugin`], as the
73/// component's `ui-plugin` world. Put it on the plugin's struct or enum, or
74/// on its `impl UiPlugin for ...` block; once per plugin crate.
75pub use standard_plugin_macros::ui;
76
77/// Exports the annotated type, which implements [`daemon::DaemonPlugin`],
78/// as the component's `daemon-plugin` world.
79pub use standard_plugin_macros::daemon;
80
81/// Floating-point maths for `no_std` plugins (`libm::sinf`, `sqrtf`, ...):
82/// `core` has no `f32::sin`.
83pub use libm;
84
85/// What most plugins import: `use standard_plugin::prelude::*;`.
86pub mod prelude {
87    pub use crate::daemon::DaemonPlugin;
88    pub use crate::{
89        AnySurface, Area, Attrs, Button, Cells, Colour, Context, Error, Event, Frame, Geometry,
90        Instances, Json, Key, KeyCode, Modifiers, Pixels, Pointer, PointerKind, Rect, Rgba, Shapes,
91        Style, Surface, Target, ThemeSlot, UiPlugin,
92    };
93    pub use alloc::borrow::ToOwned;
94    pub use alloc::boxed::Box;
95    pub use alloc::format;
96    pub use alloc::string::{String, ToString};
97    pub use alloc::vec;
98    pub use alloc::vec::Vec;
99}
100
101/// The sources this SDK version is built from, for tools: the
102/// `standard-plugin` CLI checks components against [`sources::WIT`] and
103/// scaffolds new plugins from the examples, so both always match the SDK
104/// the new plugin depends on.
105pub mod sources {
106    /// The WIT package `standard:plugin@2.0.0` (`wit/plugin.wit`): the one
107    /// copy the SDK bindings, the viewer and daemon hosts and the CLI use.
108    pub const WIT: &str = include_str!("../wit/plugin.wit");
109
110    /// The compiled examples `standard-plugin new` starts from.
111    pub mod examples {
112        /// A cell-model sidebar card (`examples/clock_card.rs`).
113        pub const CLOCK_CARD: &str = include_str!("../examples/clock_card.rs");
114        /// A daemon plugin (`examples/heartbeat_daemon.rs`).
115        pub const HEARTBEAT_DAEMON: &str = include_str!("../examples/heartbeat_daemon.rs");
116        /// The UI half of a companion pair (`examples/together_ui.rs`).
117        pub const TOGETHER_UI: &str = include_str!("../examples/together_ui.rs");
118        /// The companion daemon (`examples/together_companion.rs`).
119        pub const TOGETHER_COMPANION: &str = include_str!("../examples/together_companion.rs");
120    }
121
122    /// One guide of this SDK version: its name, the file stem in `docs/`
123    /// (`standard-plugin guide <name>`), and its Markdown text.
124    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
125    pub struct Guide {
126        pub name: &'static str,
127        pub text: &'static str,
128    }
129
130    impl Guide {
131        /// The text of its first heading, else its name.
132        #[must_use]
133        pub fn title(&self) -> &'static str {
134            self.text
135                .lines()
136                .next()
137                .and_then(|line| line.strip_prefix("# "))
138                .unwrap_or(self.name)
139        }
140    }
141
142    /// Every guide in this crate's `docs/` directory, in reading order,
143    /// the index first ([`crate::guide`] shows the same pages).
144    pub const GUIDES: &[Guide] = &[
145        Guide {
146            name: "README",
147            text: include_str!("../docs/README.md"),
148        },
149        Guide {
150            name: "getting-started",
151            text: include_str!("../docs/getting-started.md"),
152        },
153        Guide {
154            name: "agent-guide",
155            text: include_str!("../docs/agent-guide.md"),
156        },
157        Guide {
158            name: "manifest",
159            text: include_str!("../docs/manifest.md"),
160        },
161        Guide {
162            name: "grants",
163            text: include_str!("../docs/grants.md"),
164        },
165        Guide {
166            name: "rendering",
167            text: include_str!("../docs/rendering.md"),
168        },
169        Guide {
170            name: "input",
171            text: include_str!("../docs/input.md"),
172        },
173        Guide {
174            name: "working-together",
175            text: include_str!("../docs/working-together.md"),
176        },
177        Guide {
178            name: "daemon-plugins",
179            text: include_str!("../docs/daemon-plugins.md"),
180        },
181        Guide {
182            name: "testing",
183            text: include_str!("../docs/testing.md"),
184        },
185        Guide {
186            name: "publishing",
187            text: include_str!("../docs/publishing.md"),
188        },
189        Guide {
190            name: "abi",
191            text: include_str!("../docs/abi.md"),
192        },
193        Guide {
194            name: "other-languages",
195            text: include_str!("../docs/other-languages.md"),
196        },
197    ];
198}
199
200/// The guides, as pages of this documentation. The same Markdown files are
201/// this crate's `docs/` directory, where links between guides resolve, and
202/// `standard-plugin guide` prints them for the SDK version a plugin's
203/// `Cargo.lock` resolves.
204pub mod guide {
205    #[doc = include_str!("../docs/README.md")]
206    pub mod index {}
207    #[doc = include_str!("../docs/getting-started.md")]
208    pub mod getting_started {}
209    #[doc = include_str!("../docs/agent-guide.md")]
210    pub mod agent_guide {}
211    #[doc = include_str!("../docs/manifest.md")]
212    pub mod manifest {}
213    #[doc = include_str!("../docs/grants.md")]
214    pub mod grants {}
215    #[doc = include_str!("../docs/rendering.md")]
216    pub mod rendering {}
217    #[doc = include_str!("../docs/input.md")]
218    pub mod input {}
219    #[doc = include_str!("../docs/working-together.md")]
220    pub mod working_together {}
221    #[doc = include_str!("../docs/daemon-plugins.md")]
222    pub mod daemon_plugins {}
223    #[doc = include_str!("../docs/testing.md")]
224    pub mod testing {}
225    #[doc = include_str!("../docs/publishing.md")]
226    pub mod publishing {}
227    #[doc = include_str!("../docs/abi.md")]
228    pub mod abi {}
229    #[doc = include_str!("../docs/other-languages.md")]
230    pub mod other_languages {}
231}
232
233/// Used by the export macros; not part of the public API.
234#[doc(hidden)]
235pub mod __private {
236    pub use crate::bindings::{daemon as daemon_bindings, ui as ui_bindings};
237    pub use crate::daemon::runtime::DaemonRuntime;
238    pub use crate::ui_runtime::UiRuntime;
239    pub use alloc::string::String;
240}
241
242/// Exports a [`UiPlugin`] type; `#[standard_plugin::ui]` expands to this.
243#[doc(hidden)]
244#[macro_export]
245macro_rules! __export_ui {
246    ($plugin:ty) => {
247        const _: () = {
248            static RUNTIME: $crate::__private::UiRuntime<$plugin> =
249                $crate::__private::UiRuntime::new();
250
251            struct Export;
252
253            impl $crate::__private::ui_bindings::Guest for Export {
254                fn activate(config: $crate::__private::String) {
255                    RUNTIME.activate(config);
256                }
257
258                fn frame(
259                    now_ms: u64,
260                    interval_ms: u32,
261                    power: $crate::__private::ui_bindings::Power,
262                ) -> Option<u64> {
263                    RUNTIME.frame(now_ms, interval_ms, power.into())
264                }
265
266                fn event(event: $crate::__private::ui_bindings::Event) {
267                    RUNTIME.event(event.into());
268                }
269
270                fn deactivate() {
271                    RUNTIME.deactivate();
272                }
273            }
274
275    $crate::__export_ui_plugin_impl!(Export with_types_in $crate::__private::ui_bindings);
276        };
277    };
278}
279
280/// Exports a [`daemon::DaemonPlugin`] type; `#[standard_plugin::daemon]`
281/// expands to this.
282#[doc(hidden)]
283#[macro_export]
284macro_rules! __export_daemon {
285    ($plugin:ty) => {
286        const _: () = {
287            static RUNTIME: $crate::__private::DaemonRuntime<$plugin> =
288                $crate::__private::DaemonRuntime::new();
289
290            struct Export;
291
292            impl $crate::__private::daemon_bindings::Guest for Export {
293                fn activate(config: $crate::__private::String) {
294                    RUNTIME.activate(config);
295                }
296
297                fn event(event: $crate::__private::daemon_bindings::Event) {
298                    RUNTIME.event(event.into());
299                }
300
301                fn drive(now_ms: u64) -> Option<u64> {
302                    RUNTIME.drive(now_ms)
303                }
304
305                fn deactivate() {
306                    RUNTIME.deactivate();
307                }
308            }
309
310            impl $crate::__private::daemon_bindings::exports::standard::plugin::daemon_events::Guest
311                for Export
312            {
313                fn handle_event(
314                    event: $crate::__private::daemon_bindings::exports::standard::plugin::daemon_events::DaemonEvent,
315                ) {
316                    RUNTIME.event(event.into());
317                }
318            }
319
320            impl $crate::__private::daemon_bindings::exports::standard::plugin::companion::Guest
321                for Export
322            {
323                fn handle_call(
324                    method: $crate::__private::String,
325                    payload: $crate::__private::String,
326                    from: $crate::__private::daemon_bindings::exports::standard::plugin::companion::Caller,
327                ) -> Result<
328                    $crate::__private::String,
329                    $crate::__private::ui_bindings::standard::plugin::types::CallError,
330                > {
331                    RUNTIME
332                        .handle_call(&method, payload, from.into())
333                        .map_err(Into::into)
334                }
335            }
336
337            // Only a component exports. Natively, interface exports are
338            // `<interface>#<function>` symbols, and `#` starts a comment in
339            // the macOS linker's exported-symbols list.
340            #[cfg(target_family = "wasm")]
341            $crate::__export_daemon_plugin_impl!(
342                Export with_types_in $crate::__private::daemon_bindings
343            );
344
345            // A native build (tests, `cargo check`) still uses the plugin
346            // through the same entry points.
347            #[cfg(not(target_family = "wasm"))]
348            #[allow(dead_code)]
349            fn native_entry_points() {
350                use $crate::__private::daemon_bindings::Guest as _;
351                use $crate::__private::daemon_bindings::exports::standard::plugin::companion::Guest as _;
352                use $crate::__private::daemon_bindings::exports::standard::plugin::daemon_events::Guest as _;
353                let _ = (
354                    Export::activate,
355                    Export::event,
356                    Export::handle_event,
357                    Export::drive,
358                    Export::deactivate,
359                    Export::handle_call,
360                );
361            }
362        };
363    };
364}