Quickstart
From an example directory to pixels on the device — build, render with no hardware, push in under a second.
A device app is a Rust no_std crate that compiles to a wasm32 cdylib. You build it, render it through the exact renderer the device runs — no board attached — and push it to a real device over the wire in under a second. The whole loop is three just recipes.
There is no cargo zeph new. You copy an example directory and rename it.
Copy the hello example
hello is the smallest complete app: a #![no_std] crate whose [lib] crate-type is ["cdylib"], depending only on zeph-app-sdk. Copy its directory, rename the crate to zeph-guest-<yourname>, and you have a working starting point.
#![no_std]
extern crate alloc;
use alloc::vec;
use zeph_app_sdk::{zeph_app, UiNode, ZephApp};
#[derive(Default)]
struct Hello;
impl ZephApp for Hello {
fn render(&self) -> (&'static str, UiNode) {
(
"hello.default",
UiNode::screen(vec![
UiNode::eyebrow("HELLO APP"),
UiNode::big("hello, zeph"),
UiNode::text("a WASM app rendering a surface"),
]),
)
}
}
zeph_app!(Hello);render returns a surface ref and a UiNode tree. The ref must be "<app_id>.default" — that is what the launcher opens. zeph_app! generates the WASM exports the runtime calls.
Build it to wasm32
just wasm-build helloThis compiles zeph-guest-hello for wasm32-unknown-unknown and prints the size of the resulting .wasm. Build from the recipe rather than a bare cargo build — the recipe caps the guest's shadow stack so its linear memory stays about one page instead of blowing the runtime's memory budget.
See it with no hardware
just wasm-shot hello out.pngThis runs the guest off-device, captures the surface it pushes as ZephUI CBOR, and feeds that straight into the same zui_cbor_parse + renderer the device uses — fullscreen, into out.png. What you see is what the device draws.
Pass a target board to preview another form factor: just wasm-shot hello out.png 2 renders as the round 1.75C.
Push it to a device
just wasm-push helloWith a device connected, this builds the guest and streams it over the transfer plane. The push is transient — it runs from PSRAM, nothing is flashed. Edit your Rust, run just wasm-push again, and the new render lands in under a second. To make it persist to the on-device app store instead, push it with the CLI's --install flag (zeph app wasm-push … --install).
The arena resets before every handler
Your app struct holds scalars, never a Vec or a String — anything heap-allocated is gone by the next call. This is the single most common way a first app breaks. See The app model.
wasm-push grants the capability floor by default: surface, nav, and log. An app that needs a gated capability — a timer, config reads — passes it explicitly with --caps.