# Krate Mode

You are writing a Krate app. Read this whole file before you write any code, and
follow it exactly.

## What Krate is

A Krate app is a **WebAssembly component compiled from ordinary Rust**. It is not
a config file, not a JSON agent spec, and not a plugin manifest -- there is no
`add_tool` to call and no schema to fill in. You write real Rust against the API
in this document, and a compiler turns it into one file.

That one file is **capability-gated**: it can reach the filesystem, the network,
or the clipboard only where its manifest declares it, and the person running it
grants it. A component that tries to reach the operating system any other way is
refused before it runs.

The result is **one `.krate` file that runs unchanged on macOS, Windows, and
Linux** -- no per-app installer and no per-platform build. The person you send
it to installs the Krate player once; every app after that is just the file.

---

## Output rules -- these are hard

**Emit three complete files, every time.** Not fragments, not diffs, not "the
rest stays the same". A Krate app directory is exactly:

    Cargo.toml
    src/lib.rs
    manifest.toml

The build tool checks for all three before it does anything else, and a missing
one fails immediately. Give the person the full contents of each, in its own
fenced code block, labelled with its filename.

**Never invent a capability name.** The manifest may only declare names from the
capability table below. Anything else is refused when the app is packed. If the
app needs something not in that table, say so in plain words instead of guessing
a name that looks plausible.

**Never invent a function.** Call only what appears in the API sections below. If
a function you want is not listed, it does not exist -- solve the problem with
what is there, or say what is missing. Inventing a call is the single most common
way a generated Krate app fails, and it fails at compile time with a confusing
error the person has to debug.

**Never reach the operating system through `std`.** No `std::fs`, `std::io`
(including `println!` and `eprintln!`), `std::time`, `std::env`, `std::process`,
`std::net`, or `std::thread`. Use the `krate::*` equivalents. Ordinary in-memory
std -- `String`, `Vec`, `HashMap`, iterators -- is fine and does not leak.

**Handle the argument `quick`.** Before any other argument parsing, check for the
bare word `quick` (not `--quick`, not a flag). On `quick`, do the app's real work
once against a small built-in sample, print what the app is holding, and exit 0
-- never wait for input, never sit on an open window. The verification run passes
exactly this argument, and an app that parses arguments strictly and rejects it
fails after building perfectly.

**Print one `key:value` per line, and make the keys mean something.** This is the
only way anything outside the app can tell whether it did what was asked --
`check-app` reads it, CI reads it, and the benchmark reads it. Printing just
`ok` is not enough: such an app builds, runs, paints a frame, and proves
nothing about whether it works.

Print the state a person would look at to judge the app. A to-do list prints how
many items it holds and how many are done; a tip calculator prints the tip and
the total; a game prints the score and whether it is over:

    items:5
    done:2
    saved:yes

Lower-case keys, no spaces around the colon, one pair per line, numbers as bare
digits. Seed enough state in the `quick` path that the numbers are interesting --
a to-do list that prints `items:0` has proved nothing either.

This is not a style note. Five apps were measured on 2026-08-05 and every one of
them worked and could not prove it: a tip calculator computed the right answer
and printed `bill:60 tip%:18 people:2 total_cents:7080` -- three keys on one
line, with invented names -- and a dice roller printed nothing at all. All five
scored zero.

---

## The two Cargo.toml templates

Copy one of these verbatim and change only `NAME`. The `[profile.release]`
settings are load-bearing, not tuning: without `panic = "abort"` and
`opt-level = "s"`, dead-code elimination is not aggressive enough to drop std's
latent OS imports, and the component is rejected.

`PREFIX` is the path from the app directory to the Krate source checkout. Leave
it as the literal `PREFIX` and tell the person to replace it, or better, tell
them to run `krate create` (see the handoff at the end), which fills these paths
in correctly for their machine.

### A CLI app (no window)

```toml
# An empty [workspace] table makes the app its own workspace root, so it builds
# standalone even inside another cargo workspace's directory tree.
[workspace]

[package]
name = "NAME"
version = "0.1.0-dev"
edition = "2021"
rust-version = "1.91"

[dependencies]
krate = { path = "PREFIX/crates/bindings-rust" }
wit-bindgen-rt = { version = "0.44.0", features = ["bitflags"] }

[lib]
crate-type = ["cdylib"]

[package.metadata.component]
package = "krate:NAME"

[package.metadata.component.target]
path = "PREFIX/wit/krate/phase2"
world = "cli"

[package.metadata.component.target.dependencies]
"krate:io" = { path = "PREFIX/wit/krate/phase2/deps/io" }
"krate:fs" = { path = "PREFIX/wit/krate/phase2/deps/fs" }
"krate:net" = { path = "PREFIX/wit/krate/phase2/deps/net" }
"krate:time" = { path = "PREFIX/wit/krate/phase2/deps/time" }
"krate:locale" = { path = "PREFIX/wit/krate/phase2/deps/locale" }
"krate:resources" = { path = "PREFIX/wit/krate/phase2/deps/resources" }
"krate:store" = { path = "PREFIX/wit/krate/phase2/deps/store" }
"krate:random" = { path = "PREFIX/wit/krate/phase2/deps/random" }

[profile.release]
panic = "abort"
lto = true
codegen-units = 1
opt-level = "s"
```

A CLI app writes `krate::export!(Component);` at the end of `src/lib.rs` and
implements `krate::Guest`.

### A GUI app (a window)

Same as above with four changes: the `krate` dependency turns on its `gui`
feature (that is what makes the component declare the gui world), the WIT
world is `gui` under `phase4`, four more WIT packages are listed, and the
bindings need `std_feature = true`. The windowing, widget, event, drawing,
sound, camera and speech interfaces are then `krate::ui::...`,
`krate::gfx::...`, `krate::audio::...`, `krate::camera::...` and
`krate::speech::...`; the Phase 2 helpers work unchanged.

```toml
[workspace]

[package]
name = "NAME"
version = "0.1.0-dev"
edition = "2021"
rust-version = "1.91"

[dependencies]
krate = { path = "PREFIX/crates/bindings-rust", features = ["gui"] }
wit-bindgen-rt = { version = "0.44.0", features = ["bitflags"] }

[lib]
crate-type = ["cdylib"]

# Puts the generated `impl std::error::Error` behind a feature nobody turns on,
# which is what lets a windowed app be #![no_std] at all.
[package.metadata.component.bindings]
std_feature = true

[package.metadata.component]
package = "krate:NAME"

[package.metadata.component.target]
path = "PREFIX/wit/krate/phase4"
world = "gui"

[package.metadata.component.target.dependencies]
"krate:io" = { path = "PREFIX/wit/krate/phase4/deps/io" }
"krate:fs" = { path = "PREFIX/wit/krate/phase4/deps/fs" }
"krate:net" = { path = "PREFIX/wit/krate/phase4/deps/net" }
"krate:time" = { path = "PREFIX/wit/krate/phase4/deps/time" }
"krate:locale" = { path = "PREFIX/wit/krate/phase4/deps/locale" }
"krate:resources" = { path = "PREFIX/wit/krate/phase4/deps/resources" }
"krate:store" = { path = "PREFIX/wit/krate/phase4/deps/store" }
"krate:random" = { path = "PREFIX/wit/krate/phase4/deps/random" }
"krate:ui" = { path = "PREFIX/wit/krate/phase4/deps/ui" }
"krate:gfx" = { path = "PREFIX/wit/krate/phase4/deps/gfx" }
"krate:audio" = { path = "PREFIX/wit/krate/phase4/deps/audio" }
"krate:speech" = { path = "PREFIX/wit/krate/phase4/deps/speech" }

[profile.release]
panic = "abort"
lto = true
codegen-units = 1
opt-level = "s"
```

A GUI app reaches the API through the SDK -- `krate::ui::*` and `krate::gfx::*`
for the window and the drawing, `krate::bindings::krate::io::*` for the Phase 2
interfaces underneath -- implements `krate::Guest`, and ends with
`krate::export!(Component);`. Do **not** write a `bindings` module: the SDK's
`gui` feature carries the gui world's bindings, and `features = ["gui"]` is
what makes the component declare that world.

---

## `no_std`, and the one mistake that breaks everything

A Krate component may import only `krate:*` interfaces. Reaching the OS through
`std` pulls in `wasi:*` imports and the app is rejected at the import check.

The trap is that this happens **without you calling anything**. A reachable
panic makes std's failure path reachable: it formats a message, writes it to
stderr, and exits, which is three separate OS interfaces arriving at once. One
panic site can take a component from zero imports to more than thirty.

### Which to write

- **No dependencies beyond the SDK, and simple logic?** Plain std is fine: give
  the `krate` dependency `features = ["gui", "std"]`, which tells the SDK the
  guest brings its own allocator and panic handler.
- **Any real dependency (a parser, a decoder, `rand`), or enough logic that a
  stray panic is likely?** Write `#![no_std]`. This is the safer default, and it
  is what the worked examples below do.

### The `no_std` checklist

Miss a step and it fails to build with "no global memory allocator found",
"`#[panic_handler]` required" or "duplicate lang item":

1. `#![no_std]` at the top of `src/lib.rs`, then `extern crate alloc;`, and take
   `String`, `Vec`, `format!` and `vec!` from `alloc`:
   `use alloc::{format, string::{String, ToString}, vec, vec::Vec};`
2. **KEEP the `krate` dependency in `Cargo.toml`, and drop `"std"` from its
   features** (`features = ["gui"]` for a windowed app, no features for a CLI
   app). Without `std` the SDK provides the global allocator, the
   `#[panic_handler]`, and the memory intrinsics a `no_std` guest needs; with
   `std` it leaves them to std. A `#![no_std]` guest that still says `"std"`
   fails with "`#[panic_handler]` function required"; a std guest that drops
   `"std"` fails with "duplicate lang item `panic_impl`". **The two lines move
   together, and this is the step that is missed most often.**
3. Keep `panic = "abort"` and `opt-level = "s"` in `[profile.release]`.

### What to avoid in `no_std`

- **No `format!` and no `.to_string()`.** They route through the allocator's
  out-of-memory handler. Format numbers by hand into a fixed `[u8; N]` buffer --
  the checklist example below has a `push_num` helper to copy.
- **No `.unwrap()` or `.expect()`.** Use `let Some(x) = ... else { ... }`,
  `if let`, or `.unwrap_or(default)`.
- **No indexing with `[i]`.** `buf[i]` carries a bounds check that can panic. Use
  `.get(i)` / `.get_mut(i)` and handle the `None`.
- **No panicking arithmetic.** Prefer `.saturating_sub()`, `.checked_div()`, and
  friends over bare `-` and `/` on values that could underflow or be zero.

### If the app needs randomness

`rand`, `uuid`, and `getrandom` do not build on a target with no OS entropy
unless a backend is registered. Do not hand-shim it. Add
`features = ["getrandom-backend"]` to the `krate` dependency, add a
`.cargo/config.toml` containing
`rustflags = ["--cfg", "getrandom_backend=\"custom\""]`, and declare the
`random.bytes` capability. `apps/krate-diceroll` is the working example.

---

# The API surface

Everything from here to the worked examples is generated directly from the Krate
source: the SDK crate, the capability registry, and the interface definitions.
It is exactly what the compiler will accept. **These are the only functions and
capability names that exist.**

The three sections keep the numbering they have in Krate's own reference file, so
you will see 1, 2, and 4 -- their 3 and 5 are the `no_std` rules and the example
index, which this document has already covered in its own words.


---

# 1. The SDK: every `krate::*` function you can call

This is the whole CLI-and-shared API surface, generated from the SDK source.
GUI apps also use the interfaces in section 4, reached through the generated
`bindings` module.

## Every function you can call

This is the whole guest API. If something you want is not here, it does not
exist -- do not invent a call. Work with what is listed, or say in your report
that the behaviour cannot be ported.


### Widget kinds

Build a window from `types::WidgetNode` values. Every kind here draws on
macOS, Windows, and Linux -- there is no kind that works on one system only.

- `Stack` -- flex row or column; the usual root
- `Grid` -- wrapping grid
- `Scroll` -- scrolls its children
- `Tabs` -- tab strip; `selected` picks the visible panel
- `Button` -- `label` is the title
- `Checkbox` -- `checked` is the state
- `Radio` -- one of a set; `checked` is the state
- `Switch` -- on/off; `checked` is the state
- `Slider` -- `value` is 0.0..=1.0
- `Progress` -- `value` is 0.0..=1.0
- `Text` -- static label
- `TextField` -- one line the person can type in
- `TextArea` -- many lines the person can type in
- `ListView` -- rows; `selected` is the chosen index
- `TreeView` -- nested rows; `selected` is the chosen index
- `Image` -- a picture; fill it with `image::set_pixels`, see "Showing a picture"
- `Canvas` -- a region the app positions children in
- `Overlay` -- children all fill it and paint back to front: a HUD over a game, a pause menu over a board, a caption over a photo

### `assets`

- `assets::list(path: &str) -> Result<Vec<String>, ResourceError>`
- `assets::read(path: &str) -> Result<Vec<u8>, ResourceError>`
- `assets::read_text(path: &str) -> Result<String, ResourceError>`

### `fs`

- `fs::list(path: &str) -> Result<Vec<String>, FsError>`
- `fs::mkdir(path: &str) -> Result<(), FsError>`
- `fs::open(path: &str, mode: OpenMode) -> Result<File, FsError>`
- `fs::read(path: &str) -> Result<Vec<u8>, FsError>`
- `fs::read_to_string(path: &str) -> Result<String, FsError>`
- `fs::remove_dir(path: &str) -> Result<(), FsError>`
- `fs::remove_file(path: &str) -> Result<(), FsError>`
- `fs::rename(from: &str, to: &str) -> Result<(), FsError>`
- `fs::stat(path: &str) -> Result<FileStat, FsError>`
- `fs::write(path: &str, bytes: &[u8]) -> Result<(), FsError>`

### `fs::watch`

- `fs::watch::changes(watch: u64) -> Result<Vec<Change>, FsError>`
- `fs::watch::start(path: &str) -> Result<u64, FsError>`
- `fs::watch::stop(watch: u64) -> ()`

### `group`

- `group::delete(group: &str, key: &str) -> Result<(), GroupError>`
- `group::get(group: &str, key: &str) -> Result<Option<Vec<u8>>, GroupError>`
- `group::get_text(group: &str, key: &str) -> Result<Option<String>, GroupError>`
- `group::keys(group: &str) -> Result<Vec<String>, GroupError>`
- `group::set(group: &str, key: &str, value: &[u8]) -> Result<(), GroupError>`
- `group::set_text(group: &str, key: &str, value: &str) -> Result<(), GroupError>`

### `io::args`

- `io::args::all() -> Vec<String>`
- `io::args::first() -> Option<String>`
- `io::args::first_raw(raw: &str) -> Option<&str>`
- `io::args::raw() -> String`
- `io::args::split_raw(raw: &str) -> impl Iterator<Item = &str>`

### `io::stdio`

- `io::stdio::eprint(value: &str) -> Result<(), IoError>`
- `io::stdio::eprintln(value: &str) -> Result<(), IoError>`
- `io::stdio::ewrite(bytes: &[u8]) -> Result<(), IoError>`
- `io::stdio::print(value: &str) -> Result<(), IoError>`
- `io::stdio::println(value: &str) -> Result<(), IoError>`
- `io::stdio::write(bytes: &[u8]) -> Result<(), IoError>`

### `locale`

- `locale::current() -> LocaleId`
- `locale::format_date(millis: u64, tz: &str, style: DateStyle, loc: &LocaleId) -> String`
- `locale::format_number(value: f64, style: NumberStyle, loc: &LocaleId) -> String`
- `locale::timezone() -> String`

### `mem`

- `mem::have_room_for(bytes: usize) -> bool`
- `mem::try_grow(v: &mut Vec<T>, additional: usize) -> bool`
- `mem::try_grow_string(s: &mut String, additional: usize) -> bool`
- `mem::try_string(capacity: usize) -> Option<String>`
- `mem::try_vec(capacity: usize) -> Option<Vec<T>>`

### `motion`

- `motion::ease_in_out(t: f32) -> f32`
- `motion::ease_out(t: f32) -> f32`
- `motion::ease_out_back(t: f32) -> f32`
- `motion::mix(a: u32, b: u32, t: f32) -> u32`
- `motion::pulse(now_ms: u64, period_ms: u64) -> f32`
- `motion::smoothstep(t: f32) -> f32`
- `motion::stagger(elapsed_ms: u64, index: usize, step_ms: u64, duration_ms: u64) -> f32`

### `net`

- `net::begin(req: Request) -> Result<u64, NetError>`
- `net::cancel(handle: u64) -> ()`
- `net::fetch(req: Request) -> Result<Response, NetError>`
- `net::get(url: &str) -> Result<Vec<u8>, NetError>`
- `net::get_text(url: &str) -> Result<String, NetError>`
- `net::poll(handle: u64) -> FetchStatus`

### `net::ws`

- `net::ws::close(handle: u64) -> ()`
- `net::ws::open(url: &str) -> Result<u64, NetError>`
- `net::ws::send(handle: u64, message: WsMessage) -> Result<(), NetError>`
- `net::ws::send_binary(handle: u64, bytes: &[u8]) -> Result<(), NetError>`
- `net::ws::send_text(handle: u64, text: &str) -> Result<(), NetError>`

### `random`

- `random::below(bound: u64) -> Result<u64, RandomError>`
- `random::bytes(count: u32) -> Result<Vec<u8>, RandomError>`
- `random::fill(buf: &mut [u8]) -> Result<(), RandomError>`
- `random::shuffle(items: &mut [T]) -> Result<(), RandomError>`
- `random::u64() -> Result<u64, RandomError>`

### `secret`

- `secret::delete(name: &str) -> Result<(), SecretError>`
- `secret::get(name: &str) -> Result<Option<Vec<u8>>, SecretError>`
- `secret::get_text(name: &str) -> Result<Option<String>, SecretError>`
- `secret::names() -> Result<Vec<String>, SecretError>`
- `secret::set(name: &str, secret: &[u8]) -> Result<(), SecretError>`
- `secret::set_text(name: &str, secret: &str) -> Result<(), SecretError>`

### `shared`

- `shared::code() -> Result<Option<String>, SharedError>`
- `shared::create() -> Result<String, SharedError>`
- `shared::delete(key: &str) -> Result<(), SharedError>`
- `shared::get(key: &str) -> Result<Option<Vec<u8>>, SharedError>`
- `shared::get_text(key: &str) -> Result<Option<String>, SharedError>`
- `shared::join(code: &str) -> Result<(), SharedError>`
- `shared::keys() -> Result<Vec<String>, SharedError>`
- `shared::leave() -> Result<(), SharedError>`
- `shared::set(key: &str, value: &[u8]) -> Result<(), SharedError>`
- `shared::set_text(key: &str, value: &str) -> Result<(), SharedError>`
- `shared::sync() -> Result<bool, SharedError>`

### `sql`

- `sql::execute(statement: &str, params: &[Value]) -> Result<u64, SqlError>`
- `sql::query(statement: &str, params: &[Value]) -> Result<QueryResult, SqlError>`
- `sql::query_one_text(statement: &str, params: &[Value]) -> Result<Option<String>, SqlError>`
- `sql::query_texts(statement: &str, params: &[Value]) -> Result<Vec<String>, SqlError>`
- `sql::transaction(statements: &[String]) -> Result<(), SqlError>`

### `store`

- `store::clear() -> Result<(), StoreError>`
- `store::delete(key: &str) -> Result<(), StoreError>`
- `store::get(key: &str) -> Result<Option<Vec<u8>>, StoreError>`
- `store::get_text(key: &str) -> Result<Option<String>, StoreError>`
- `store::keys() -> Result<Vec<String>, StoreError>`
- `store::set(key: &str, value: &[u8]) -> Result<(), StoreError>`
- `store::set_text(key: &str, value: &str) -> Result<(), StoreError>`

### `time`

- `time::monotonic_nanos() -> u64`
- `time::now_millis() -> u64`
- `time::sleep_millis(millis: u32) -> ()`

### Methods, called on a value

These are not paths. Get the value first -- `io::stdio::stdout()`,
`fs::open(..)` -- then call the method on it.

- `<>.fract() -> $t`
- `<>.mul_add(a: $t, b: $t) -> $t`
- `<>.powi(n: i32) -> $t`
- `<>.rem_euclid(rhs: $t) -> $t`
- `<>.sin_cos() -> ($t, $t)`

`File`

- `<File>.read_text() -> Result<String, FsError>`
- `<File>.read_to_end() -> Result<Vec<u8>, FsError>`
- `<File>.write_all(bytes: &[u8]) -> Result<(), FsError>`
- `<File>.write_text(value: &str) -> Result<(), FsError>`

`InputStream`

- `<InputStream>.read_text() -> Result<String, IoError>`
- `<InputStream>.read_to_end() -> Result<Vec<u8>, IoError>`

`OutputStream`

- `<OutputStream>.write_bytes(bytes: &[u8]) -> Result<(), IoError>`
- `<OutputStream>.write_line(value: &str) -> Result<(), IoError>`
- `<OutputStream>.write_text(value: &str) -> Result<(), IoError>`

---

# 2. Capabilities: what a manifest may declare

Declare in `manifest.toml` only the capabilities the app actually uses. A
name outside this list is refused when the app is packed. The ones marked
*default* are granted to every app and must NOT be declared -- declaring one
is an error. Scoped names (`<path-glob>`, `<host>:<port>`) must be narrowed
to exactly what the app needs, e.g. `fs.read:notes/**`.

## Available to every app (CLI and GUI)

| capability | default-granted? | notes |
|---|---|---|
| `io.stdin` | yes | read stdin |
| `io.stdout` | yes | print to stdout |
| `io.stderr` | yes | print to stderr |
| `io.args` | yes | read command-line args |
| `io.log` | yes | structured logging |
| `fs.read:<path-glob>` | no | read files under a folder |
| `fs.write:<path-glob>` | no | write files under a folder |
| `fs.list:<path-glob>` | no | list a folder |
| `store.kv` | no | the app's own key-value store: keys up to 256 bytes, values up to 1 MiB, 16 MiB per app; set returns an error past these, so check it. Settings and lists go here; a document goes in a file |
| `store.sql` | no | the app's own SQL database |
| `store.secret` | no | secrets encrypted at rest (passwords, tokens); not the OS keychain |
| `store.shared` | no | share a key-value bucket with everyone holding its invite code |
| `store.group:<group-name>` | no | share a key-value store with the same publisher's other apps, in a named group its signed list admits |
| `memory.budget:<MiB>` | no | more memory than the default, in MiB (up to 3584); the person sees it at consent |
| `random.bytes` | no | entropy (also what getrandom/rand need) |
| `fs.remove:<path-glob>` | no | delete under a folder |
| `fs.mkdir:<path-glob>` | no | make folders |
| `net.connect:<host>:<port>` | no | reach a host and port |
| `time.clock` | yes | wall-clock time |
| `time.monotonic` | yes | a monotonic timer |
| `time.sleep` | yes | sleep |
| `locale.info` | yes | the user's locale |
| `locale.format` | yes | locale-aware number/date formatting |

## GUI apps only (a window, drawing, sound)

| capability | default-granted? | notes |
|---|---|---|
| `ui.window:create` | yes | open a window (every GUI app declares this) |
| `ui.clipboard:read` | no | read/write the clipboard |
| `ui.clipboard:write` | no | read/write the clipboard |
| `ui.menu:system` | no | a system menu |
| `ui.open-url` | no | hand a link to the browser |
| `ui.notify` | no | a desktop notification |
| `ui.dropzone:<mime-type>` | no | accept dragged files |
| `ui.dialog:message` | yes | a message box (no file access; default-granted) |
| `ui.dialog:confirm` | yes | a yes/no question box (no file access; default-granted) |
| `ui.dialog:*` | no | system file dialogs (open a file or folder, save a file); the person's pick is the grant |
| `ui.dialog:file-open` | no | system file dialogs (open a file or folder, save a file); the person's pick is the grant |
| `ui.dialog:file-save` | no | system file dialogs (open a file or folder, save a file); the person's pick is the grant |
| `ui.dialog:open-folder` | no | system file dialogs (open a file or folder, save a file); the person's pick is the grant |
| `gfx.gpu:basic` | yes | GPU drawing (canvas2d present today) |
| `gfx.gpu:compute` | no | GPU compute -- NOT implemented yet; do not declare it |
| `audio.playback` | yes | play sound |
| `audio.capture` | no | record from the microphone |
| `camera.capture` | no | see through the camera |

Mark `required = true` on a capability the app cannot begin without -- the verification run withholds it and the app must refuse to start (exit 5). A saving app marks `fs.write` required. `ui.window:create` is declared `required = true` by convention, but withholding a window just closes the app, so it is not the withheld gate; a GUI app whose only non-default capability is its window has nothing to withhold, which is fine. Do not mark required a capability the `quick` verification path never reaches, or the app fails its own wall test after building cleanly.

"Cannot begin without" means the app it CLAIMS to be, not just a window that opens. A music player without `audio.playback` is not a music player -- it is a silent picture of one -- so mark that capability required. Ask: if this one capability were withheld, would the app still be the thing the request asked for? If no, it is required. A drawing app's canvas, a music player's audio, a recorder's microphone, **a webcam app's camera**: required. A capability the app only uses for a nice-to-have corner is genuinely optional; the app's whole reason for existing is not.

This has real consequences, not just tidiness. The person is only asked about capabilities marked required -- an optional one is never mentioned and never granted, so the app opens without it and without a question. A generated webcam viewer marked `camera.capture` optional and opened to a permanently empty viewfinder: no camera, no prompt, nothing on screen explaining why. It looked broken, and from the person's side it was.

---

# 4. The GUI world: ui / gfx / audio / speech

A windowed app reaches these through the SDK, the same crate a CLI app
uses, with its `gui` feature on:

```toml
[dependencies]
krate = { path = "<sdk>/crates/bindings-rust", features = ["gui"] }
```

Then the interfaces below are `krate::<package>::<interface>::<fn>`, e.g.
`krate::ui::window::create("Title", size)`,
`krate::gfx::canvas2d::present(canvas)`, `krate::ui::events::wait(None)`.
Records live in each package's `types` module (`krate::ui::types::WindowSize`)
and are re-exported beside the functions that use them. Signatures are WIT,
so `list<u8>` is a Rust `Vec<u8>`/`&[u8]`, `result<t, e>` is `Result<T, E>`,
and kebab-case names become snake_case in Rust. The app implements
`krate::Guest` and ends with `krate::export!(Component);` -- no generated
`bindings` module of its own, no `mod bindings;`.

The `gui` feature is what makes the component declare the gui world, so a
windowed app MUST have it and a CLI app MUST NOT.

The shared Phase 2 helpers in section 1 (`krate::store::get(key)`,
`krate::random::bytes(count)`, `krate::fs`, `krate::net`) work unchanged in a
GUI app: the gui world imports everything the cli world does. The raw
bindings are still reachable as `krate::bindings::krate::<package>::...` when a
shape the helpers do not cover is needed. Every shipped GUI app under `apps/`
takes this path, so any of them is a model for the wiring.

## `gfx`

- `canvas2d::bind: func(window: u64, widget: u64) -> result<u64, gfx-error>`
- `canvas2d::canvas-size: func(canvas: u64) -> result<size, gfx-error>`
- `canvas2d::set-design-size: func(canvas: u64, size: size) -> result<_, gfx-error>`
- `canvas2d::set-clip: func(canvas: u64, x: f32, y: f32, w: f32, h: f32) -> result<_, gfx-error>`
- `canvas2d::clear-clip: func(canvas: u64) -> result<_, gfx-error>`
- `canvas2d::clear: func(canvas: u64, fill: color) -> result<_, gfx-error>`
- `canvas2d::fill-rect: func(canvas: u64, area: rect, fill: color) -> result<_, gfx-error>`
- `canvas2d::stroke-rect: func(canvas: u64, area: rect, stroke: color, width: f32) -> result<_, gfx-error>`
- `canvas2d::fill-round-rect: func(canvas: u64, area: rect, radii: corner-radii, fill: color) -> result<_, gfx-error>`
- `canvas2d::stroke-round-rect: func(canvas: u64, area: rect, radii: corner-radii, width: f32, stroke: color) -> result<_, gfx-error>`
- `canvas2d::drop-shadow-round-rect: func(canvas: u64, area: rect, radii: corner-radii, blur: f32, shadow: color) -> result<_, gfx-error>`
- `canvas2d::linear-gradient-stops: func(canvas: u64, area: rect, angle-degrees: f32, stops: list<gradient-stop>) -> result<_, gfx-error>`
- `canvas2d::draw-text-styled: func(canvas: u64, text: string, origin: point, font-size: f32, ink: color, style: text-style) -> result<_, gfx-error>`
- `canvas2d::measure-text-styled: func(canvas: u64, text: string, font-size: f32, style: text-style) -> result<text-metrics, gfx-error>`
- `canvas2d::stroke-arc: func(canvas: u64, center: point, radius: f32, start-degrees: f32, sweep-degrees: f32, width: f32, stroke: color) -> result<_, gfx-error>`
- `canvas2d::draw-pixels-round: func(canvas: u64, area: rect, radii: corner-radii, width: u32, height: u32, rgba: list<u8>) -> result<_, gfx-error>`
- `canvas2d::fill-circle: func(canvas: u64, center: point, radius: f32, fill: color) -> result<_, gfx-error>`
- `canvas2d::stroke-circle: func(canvas: u64, center: point, radius: f32, width: f32, stroke: color) -> result<_, gfx-error>`
- `canvas2d::radial-gradient: func(canvas: u64, center: point, radius: f32, inner: color, outer: color) -> result<_, gfx-error>`
- `canvas2d::linear-gradient: func(canvas: u64, area: rect, top: color, bottom: color) -> result<_, gfx-error>`
- `canvas2d::draw-text: func(canvas: u64, text: string, origin: point, font-size: f32, ink: color) -> result<_, gfx-error>`
- `canvas2d::measure-text: func(canvas: u64, text: string, font-size: f32) -> result<text-metrics, gfx-error>`
- `canvas2d::draw-pixels: func(canvas: u64, area: rect, width: u32, height: u32, rgba: list<u8>) -> result<_, gfx-error>`
- `canvas2d::draw-sprite: func(canvas: u64, center: point, dst: size, angle: f32, width: u32, height: u32, rgba: list<u8>) -> result<_, gfx-error>`
- `canvas2d::present: func(canvas: u64) -> result<_, gfx-error>`
- `scene3d::bind: func(window: u64, widget: u64) -> result<u64, gfx-error>`
- `scene3d::clear: func(scene: u64, sky: color) -> result<_, gfx-error>`
- `scene3d::camera: func(scene: u64, eye: list<f32>, look-at: list<f32>, fov-degrees: f32) -> result<_, gfx-error>`
- `scene3d::light: func(scene: u64, direction: list<f32>) -> result<_, gfx-error>`
- `scene3d::triangles: func(scene: u64, vertices: list<f32>, tint: color) -> result<_, gfx-error>`
- `scene3d::place: func(scene: u64, vertices: list<f32>, translate: list<f32>, rotate-degrees: list<f32>, scale: f32, tint: color) -> result<_, gfx-error>`
- `scene3d::upload-texture: func(scene: u64, width: u32, height: u32, rgba: list<u8>) -> result<u64, gfx-error>`
- `scene3d::textured: func(scene: u64, vertices: list<f32>, uvs: list<f32>, texture: u64, tint: color) -> result<_, gfx-error>`
- `scene3d::smooth: func(scene: u64, vertices: list<f32>, normals: list<f32>, uvs: list<f32>, texture: u64, tint: color) -> result<_, gfx-error>`
- `scene3d::set-lighting: func(scene: u64, lighting: lighting) -> result<_, gfx-error>`
- `scene3d::normal-mapped: func(scene: u64, vertices: list<f32>, normals: list<f32>, tangents: list<f32>, uvs: list<f32>, texture: u64, normal-texture: u64, tint: color) -> result<_, gfx-error>`
- `scene3d::unlit: func(scene: u64, vertices: list<f32>, uvs: list<f32>, texture: u64, tint: color) -> result<_, gfx-error>`
- `scene3d::cull-back-faces: func(scene: u64, enabled: bool) -> result<_, gfx-error>`
- `scene3d::present: func(scene: u64) -> result<_, gfx-error>`

## `ui`

- `window::create: func(title: string, size: window-size) -> result<u64, ui-error>`
- `window::show: func(window: u64) -> result<_, ui-error>`
- `window::close: func(window: u64) -> result<_, ui-error>`
- `window::set-title: func(window: u64, title: string) -> result<_, ui-error>`
- `window::set-size: func(window: u64, size: window-size) -> result<_, ui-error>`
- `window::set-state: func(window: u64, state: window-state) -> result<_, ui-error>`
- `window::request-redraw: func(window: u64) -> result<_, ui-error>`
- `window::set-full-bleed: func(window: u64, enabled: bool) -> result<_, ui-error>`
- `tree::set-root: func(window: u64, root: widget-node) -> result<_, ui-error>`
- `tree::upsert-node: func(window: u64, node: widget-node) -> result<_, ui-error>`
- `tree::remove-node: func(window: u64, widget: u64) -> result<_, ui-error>`
- `tree::focus-node: func(window: u64, widget: u64) -> result<_, ui-error>`
- `tree::set-enabled: func(window: u64, widget: u64, enabled: bool) -> result<_, ui-error>`
- `image::set-pixels: func(window: u64, widget: u64, pixels: image-pixels) -> result<_, ui-error>`
- `image::clear: func(window: u64, widget: u64) -> result<_, ui-error>`
- `events::poll: func() -> option<event>`
- `events::wait: func(timeout-millis: option<u32>) -> option<event>`
- `events::key-held: func(key: string) -> bool`
- `events::gamepad-connected: func() -> bool`
- `events::gamepad-held: func(button: string) -> bool`
- `events::gamepad-axis: func(axis: string) -> f32`
- `dialog::message: func(window: u64, title: string, body: string) -> result<_, ui-error>`
- `dialog::confirm: func(window: u64, title: string, body: string) -> result<bool, ui-error>`
- `dialog::open-file: func(window: u64, title: string, filter: string) -> result<option<chosen-file>, ui-error>`
- `dialog::save-file: func(window: u64, title: string, suggested: string, filter: string) -> result<option<chosen-file>, ui-error>`
- `dialog::open-folder: func(window: u64, title: string) -> result<option<chosen-folder>, ui-error>`
- `clipboard::read-text: func() -> result<string, ui-error>`
- `clipboard::write-text: func(text: string) -> result<_, ui-error>`
- `menu::set-items: func(window: u64, items: list<menu-item>) -> result<_, ui-error>`
- `launcher::open-url: func(url: string) -> result<_, launch-error>`
- `notify::show: func(title: string, body: string) -> result<_, notify-error>`
- `print::window: func(window: u64, title: string) -> result<print-outcome, ui-error>`
- `print::add-page: func(window: u64) -> result<u32, ui-error>`
- `print::finish: func(window: u64, title: string) -> result<print-outcome, ui-error>`

## `audio`

- `playback::open: func(config: stream-config) -> result<u64, audio-error>`
- `playback::start: func(stream-id: u64) -> result<_, audio-error>`
- `playback::stop: func(stream-id: u64) -> result<_, audio-error>`
- `playback::write: func(stream-id: u64, bytes: list<u8>) -> result<u32, audio-error>`
- `playback::load-sound: func(stream-id: u64, bytes: list<u8>) -> result<u64, audio-error>`
- `playback::play-sound: func(stream-id: u64, sound: u64, gain: f32) -> result<_, audio-error>`
- `playback::stop-sound: func(stream-id: u64, sound: u64) -> result<_, audio-error>`
- `capture::open: func(config: stream-config) -> result<u64, audio-error>`
- `capture::start: func(stream-id: u64) -> result<_, audio-error>`
- `capture::stop: func(stream-id: u64) -> result<_, audio-error>`
- `capture::read: func(stream-id: u64, max-bytes: u32) -> result<list<u8>, audio-error>`

## `camera`

- `capture::devices: func() -> result<list<device-info>, camera-error>`
- `capture::open: func(device: string, config: stream-config) -> result<u64, camera-error>`
- `capture::info: func(stream-id: u64) -> result<frame-info, camera-error>`
- `capture::start: func(stream-id: u64) -> result<_, camera-error>`
- `capture::stop: func(stream-id: u64) -> result<_, camera-error>`
- `capture::read: func(stream-id: u64) -> result<option<frame>, camera-error>`
- `capture::close: func(stream-id: u64) -> result<_, camera-error>`

## `speech`

- `transcription::transcribe: func( model-asset: string, pcm-s16-le: list<u8>, sample-rate: u32, language: option<string>, ) -> result<transcript, speech-error>`
- `transcription::match-line: func( model-asset: string, pcm-s16-le: list<u8>, sample-rate: u32, language: option<string>, expected: string, ) -> result<u8, match-error>`
- `transcription::match-line-stream: func( model-asset: string, pcm-s16-le: list<u8>, sample-rate: u32, language: option<string>, expected: string, finish: bool, ) -> result<option<u8>, match-error>`
- `synthesis::voices: func() -> result<list<voice>, say-error>`
- `synthesis::say: func(text: string, voice: option<string>, rate: option<f32>) -> result<_, say-error>`
- `synthesis::stop: func() -> result<_, say-error>`
- `synthesis::speaking: func() -> result<bool, say-error>`


## The shared packages, exact shapes

The same modules earlier sections describe in prose, but as the exact interfaces the bindings expose -- complete, so there is never a reason to open a bindings file to check a name or a type. Prefer the SDK helpers of section 1; the raw form is `krate::bindings::krate::<package>::<interface>::<fn>`.

## `fs`

- `files::open: func(path: string, mode: open-mode) -> result<file, fs-error>`
- `files::open-chosen: func(token: string, mode: open-mode) -> result<file, fs-error>`
- `files::stat: func(path: string) -> result<file-stat, fs-error>`
- `files::%list: func(path: string) -> result<list<string>, fs-error>`
- `files::remove-file: func(path: string) -> result<_, fs-error>`
- `files::remove-dir: func(path: string) -> result<_, fs-error>`
- `files::mkdir: func(path: string) -> result<_, fs-error>`
- `files::rename: func(%from: string, to: string) -> result<_, fs-error>`
- `watch::start: func(path: string) -> result<u64, fs-error>`
- `watch::changes: func(watch-id: u64) -> result<list<change>, fs-error>`
- `watch::stop: func(watch-id: u64)`

## `io`

- `stdio::stdin: func() -> input-stream`
- `stdio::stdout: func() -> output-stream`
- `stdio::stderr: func() -> output-stream`
- `args::raw: func() -> string`
- `log::emit: func(level: log-level, message: string, fields: list<field>)`

## `locale`

- `info::current: func() -> locale-id`
- `info::timezone: func() -> string`
- `format::format-date: func( millis: u64, tz: string, style: date-style, loc: locale-id, ) -> string`
- `format::format-number: func( value: f64, style: number-style, loc: locale-id, ) -> string`

## `net`

- `http-client::get: func(url: string) -> result<list<u8>, net-error>`
- `http-client::fetch: func(req: request) -> result<response, net-error>`
- `http-client::begin: func(req: request) -> result<u64, net-error>`
- `http-client::poll: func(handle: u64) -> fetch-status`
- `http-client::cancel: func(handle: u64)`
- `ws::open: func(url: string) -> result<u64, net-error>`
- `ws::send: func(handle: u64, message: ws-message) -> result<_, net-error>`
- `ws::poll: func(handle: u64) -> ws-event`
- `ws::close: func(handle: u64)`

## `random`

- `bytes::get: func(count: u32) -> result<list<u8>, random-error>`
- `bytes::next-u64: func() -> result<u64, random-error>`
- `bytes::below: func(bound: u64) -> result<u64, random-error>`

## `store`

- `kv::get: func(key: string) -> result<option<list<u8>>, store-error>`
- `kv::set: func(key: string, value: list<u8>) -> result<_, store-error>`
- `kv::delete: func(key: string) -> result<_, store-error>`
- `kv::keys: func() -> result<list<string>, store-error>`
- `kv::clear: func() -> result<_, store-error>`
- `sql::query: func(statement: string, params: list<value>) -> result<query-result, sql-error>`
- `sql::execute: func(statement: string, params: list<value>) -> result<u64, sql-error>`
- `sql::transaction: func(statements: list<string>) -> result<_, sql-error>`
- `secret::get: func(name: string) -> result<option<list<u8>>, secret-error>`
- `secret::set: func(name: string, secret: list<u8>) -> result<_, secret-error>`
- `secret::delete: func(name: string) -> result<_, secret-error>`
- `secret::names: func() -> result<list<string>, secret-error>`
- `shared::code: func() -> result<option<string>, shared-error>`
- `shared::create: func() -> result<string, shared-error>`
- `shared::join: func(code: string) -> result<_, shared-error>`
- `shared::leave: func() -> result<_, shared-error>`
- `shared::get: func(key: string) -> result<option<list<u8>>, shared-error>`
- `shared::set: func(key: string, value: list<u8>) -> result<_, shared-error>`
- `shared::delete: func(key: string) -> result<_, shared-error>`
- `shared::keys: func() -> result<list<string>, shared-error>`
- `shared::sync: func() -> result<bool, shared-error>`
- `group::get: func(group: string, key: string) -> result<option<list<u8>>, group-error>`
- `group::set: func(group: string, key: string, value: list<u8>) -> result<_, group-error>`
- `group::delete: func(group: string, key: string) -> result<_, group-error>`
- `group::keys: func(group: string) -> result<list<string>, group-error>`

## `time`

- `clock::now-millis: func() -> u64`
- `clock::monotonic-nanos: func() -> u64`
- `sleep::sleep-millis: func(millis: u32)`


## Showing a camera feed

A live preview is a poll in the event loop, not a callback. Open once, start once, then read a frame each time round and draw whatever came back:

    let id = camera::capture::open("", &config)?;  // "" = default camera
    camera::capture::start(id)?;                  // the indicator light comes on here
    // ... each time round the loop:
    if let Ok(Some(frame)) = camera::capture::read(id) {
        last = Some(frame);       // keep it: read() gives each frame once
    }
    if let Some(f) = &last {
        // f.width and f.height, NOT the size you asked for.
        canvas2d::draw_pixels(canvas, area, f.width, f.height, &f.bytes)?;
    }

Five things that decide whether this works:

**`read` returns `None` constantly, and that is normal.** It means no new frame since you last asked, which happens whenever your loop is faster than the camera. Keep the last frame and redraw it; an app that draws only when `read` returns `Some` flickers between the picture and a blank rectangle.

**Wake up often enough to see frames.** Use `events::wait(Some(16))` while the camera is running, the same way a network poll does it. `events::wait(None)` blocks until somebody clicks, so the preview freezes on the first frame.

**Draw at `frame.width` and `frame.height`, never at the size you asked for.** A camera opens at its nearest supported mode: ask a Mac for 640x480 and you get 1920x1080. Every frame carries its own size for exactly this reason, so the numbers cannot disagree with the bytes. Reading a size once at startup and reusing it is the trap -- the true size is not known until the first frame arrives, so an app that did that laid 1080p bytes out as 480p and showed a black window with the camera light on. `info(id)` exists for sizing a viewfinder before frames start; the frame itself is what you draw with.

**The bytes are already the format `draw_pixels` takes**: straight- alpha RGBA, `width * height * 4`, top-left first. No conversion.

**`stop` when the preview is hidden, and `close` on the way out.** The indicator light is wired to the hardware, so a person can see whether an app that says it stopped looking actually did. A photo button is just the last frame you were already holding -- there is no separate capture call, and taking a still does not need a second stream.

Declare `camera.capture` in the manifest, required if the app IS the camera. It is never granted by default: the person is asked, and on macOS the system asks a second time on its own. If `open` returns `system-denied` rather than `permission-denied`, that second wall is the one blocking -- say so plainly, because the fix is in system settings and no amount of clicking inside your app will help.

## Never guess how wide text is -- measure it

`canvas2d::measure_text(canvas, text, font_size)` returns `width`, `height`, `ascent`, and `descent` for the run `draw_text` is about to draw. Use it any time a position depends on the size of text: centring a label, right-aligning a number, placing a caret after what has been typed, sizing a card or a pill around its own text, or stacking lines of a paragraph.

**Do not write a `text_width` helper that multiplies character count by a constant.** It is wrong, not approximate. The face is proportional -- `i` and `W` differ about four times in real width -- so `"iiii"` and `"WWWW"` get the same made-up answer while the drawn pixels differ several times over. That single mistake is why labels are not really centred, captions overflow their cards, and carets sit beside the text instead of after it. The host already knows the true number; ask for it.

    // centre a label in a box
    let m = canvas2d::measure_text(canvas, label, 17.0)?;
    let x = box_x + (box_w - m.width) * 0.5;

    // a caret sitting just after the typed text
    let caret_x = text_x + canvas2d::measure_text(canvas, typed, 16.0)?.width;

`draw_text` takes a **baseline** as its origin, which is what `ascent` is for: to put a run's top edge at `y`, draw it at `y + m.ascent`; to centre it vertically in a box of height `h`, draw at `y + (h - m.height) * 0.5 + m.ascent`. Stack paragraph lines by `m.height`.

The measurement is single-line and unwrapped, because `draw_text` is too. To wrap a paragraph, measure words and break the lines yourself.

Start from the closest example in section 5 rather than these signatures alone -- the samples show the call order (bind a canvas, draw, present) that the signatures do not.

---

# 2g. What the app should look like

## Before any of that: what KIND of app is this?

This is the first design decision and the one most often skipped. Everything below depends on it, and getting it wrong is why a generated app can be technically correct and still look wrong to the person who asked for it.

**A TOOL shows someone their own data.** A hex viewer, a log reader, a spreadsheet, a file browser, a diff, a monitor, a terminal, a database client. The content IS the interface. The person came to read something, and every pixel spent on chrome is a pixel not spent on what they came for.

**A PIECE has no data of its own.** A game, a toy, a visualiser, a clock, a demo. Nothing on screen belongs to the person, so the app is the thing being looked at, and it can spend its whole surface on how it feels.

Most requests are tools. "Show me...", "a viewer for...", "track my...", "a list of..." are all tools. If you are unsure, it is a tool.

### Designing a tool

**A tool is still designed. Restraint is not the same as doing nothing.** Everything under this heading tells you what a tool does NOT spend pixels on, and it is easy to read the whole list as permission to ship the default. It is not. A widget tree with no style block on any node is not a restrained tool -- it is an unfinished one, and it is the single commonest way a generated tool comes out wrong: a text box and two headings at the host's default 13px on the host's default grey, content crammed in the top fifth, the rest of the window empty. That app passes every check Krate runs -- it builds, it imports only `krate:*`, it runs, it paints a frame -- and it looks like nobody opened it. Passing the checks is the floor, not the goal.

So a tool has a MINIMUM. Before you call one done, every item here is true:

1. The root container names a background, so the window is your colour and not the host's grey: `root.style.box_ = Some(BoxStyle { background: Some(page), border: None, border_width: 0.0, corner_radius: 0.0 })`, with `root.style.grow = 1.0` and `root.style.padding` of 20-28 so the content is not against the frame.
2. At least THREE distinct text treatments exist, set through `node.style.text = Some(TextStyle { color, outline: None, outline_width: 0.0, size: Some(px), bold })`. A tool needs a heading, a body/label ink, and a data ink -- sized and weighted apart. The host's default is 13px for everything, which is a form, and one size for the whole window is the flattest thing you can ship.
3. The data region is visually separated from the controls: a background one shade different, or a `border` with `border_width: 1.0`. A 1px border does this completely and costs nothing.
4. The content region grows with the window (`grow`), so a bigger window shows more and does not leave a lake of empty space under the content.

Note the field is `box_`, not `box` -- `box` is a Rust keyword, so the generated binding renames it. Getting that wrong is a compile error, not a silent one.

**Style the widget tree; do not abandon it for a canvas to get a corner.** A tool wants real controls: native text editing, host scrolling, focus, accessibility. You keep all of that AND get colour, weight, borders and corners, because every `WidgetNode` carries a `style` with `text`, `box_` and `place`. Painting the whole interface by hand on a canvas throws the controls away to gain nothing a style block could not do. What keeps this cheap is two helpers, written once and used for every node: an `ink(colour, size, bold) -> TextStyle` and a `boxed(background, border, width, radius) -> BoxStyle`. Build every node through those instead of spelling out a struct each time and a whole app stays consistent for about ten lines. `apps/krate-cards` and `apps/krate-inbox` are the worked examples if you have the repo checked out; if you do not, those two helpers are the whole idea -- do not go hunting the filesystem for them.

A canvas is the right answer for a tool only when the content itself has to be DRAWN and no widget kind can hold it -- a hex grid, a waveform, a chart, a diff gutter. Then draw that region on a canvas and keep the surrounding controls as widgets.

**Use `place` instead of padding to position things.** `node.style.place = Some(Placement::TopRight)` puts a widget where you mean it. Pushing something across the window with a few hundred pixels of padding changes the PARENT'S size, which moves everything else sharing that space.

**The data gets the room; the chrome gets what is left.** Controls go in one compact band, not a hero area. A tool that opens with a large heading and a row of decorative cards above its actual content has put the furniture in front of the window. This is about PROPORTION, not about leaving the chrome unstyled: the compact band is still inked and still separated.

**Name the app in the title bar, not on the screen.** `window::create("Hexview", size)` already names it. A big "Every byte, in view." heading inside the window is marketing copy aimed at somebody who has already opened the app. Kill the tagline, and the subtitle under it.

**Alignment and monospace carry more than colour does.** Columns that line up, one text size for the data, and generous ROW spacing rather than generous padding. A dense grid that aligns reads as professional; the same grid with softer colours and bigger gaps reads as a mock-up of one.

**Two or three greys and one accent.** A tool's colour budget goes on MEANING -- this byte is ASCII, that one is a control character, this line changed. Colour spent decoratively is colour the data needed. If everything is tinted, no tint means anything.

**No shadows, gradients, glows or rounded cards on a tool.** Those are for things that float above other things, and nothing in a data grid floats. A 1px border, or a background one shade different from its neighbour, separates two regions completely -- without softening the edges the eye uses to track a column. This rule REPLACES those effects with a border and a shade; it does not excuse you from separating the regions at all. A small corner radius (4-8) on a control or an input is fine and normal -- what is banned is the rounded card wrapping rectangular data.

**Size text in fixed pixels, never as a fraction of the window.** A tool is resized to see MORE ROWS, not bigger text. A font size computed from the canvas size means resizing zooms the app instead of revealing more of it, which no real tool does and every person notices at once.

### Designing a piece

The section after next applies, and applies fully. A game or a visualiser that is flat and grey has failed in the other direction: it is supposed to be looked at.

## Not looking like it was generated

People can tell, and they do not like it. The tells are consistent, and every one comes from reaching for decoration to signal effort:

- **A marketing headline on a utility.** "Every byte, in view." over a hex editor. Real tools do not advertise themselves to the person already using them.
- **Everything rounded.** Rounded cards inside rounded panels inside a rounded window, wrapping content that is rectangular.
- **A gradient with no reason.** Purple-to-blue behind a settings list.
- **Emoji as iconography.** A rocket beside "Run", a sparkle beside anything.
- **Three sizes of the same grey** where one weight change would have done the job.
- **Padding everywhere, hierarchy nowhere.** Even spacing around every element means nothing is grouped, so the eye has nothing to follow.
- **A card around each thing.** A card is for content that belongs together and is separate from its neighbours. Four cards holding one number each are four boxes, not a design.

The fix is never a different palette. It is asking what the person is looking AT, giving that the space, the contrast and the alignment -- and then stopping. An interface looks designed when every decision was made for a reason you could say out loud.

**Look at what you made.** `krate run <entry.wasm> --shoot frame.png -- quick`, then actually LOOK at the picture before calling it done. Is the content the biggest thing on screen? Could you delete a heading, a card or a colour and lose nothing? Is any text blurry or mis-aligned? That pass catches more than any rule here.

Two failures that picture shows, and they fail in OPPOSITE directions. Ask both:

- **Over-decorated:** could you delete a heading, a card, a gradient or a colour and lose nothing?
- **Under-built:** is most of the window empty, is the text all one size, is the background the host's default grey, is everything piled at the top? If the screenshot would read as "unfinished" to the person who asked, it is not done, and no rule above says to ship it. Go back and give the content room, ink and separation.

An app is finished when neither question has an answer. A blank page is not a restrained design; it is the absence of one.

## Making a PIECE look built, not sketched

This section is for a piece -- a game, a toy, a visualiser, a demo. The DECORATIVE habits below (shadows, gradients, glows, full rounded cards) are the wrong instinct on a tool; see above. But the craft ones apply to everything that draws on a canvas, tool included: always `measure_text`/`measure_text_styled` rather than guessing a width, `stroke_circle` for round outlines, the real `fill_round_rect` rather than a hand-built one, and varied font weight. If a tool draws a region on a canvas, it obeys those four. The difference between a piece that looks like a prototype and one that looks finished is a handful of habits:

**Go full-bleed when the design owns its whole surface.** `window::set-full-bleed(win, true)` right after create extends your content into the title-bar band with the host's window controls overlaid -- the shape every modern editor and terminal has. Always `let _ =` it: a host that cannot do it says unsupported and keeps the standard title bar. Leave the top ~40 pixels free of controls so nothing sits under the overlaid window buttons. `apps/krate-glow` shows it. This one is right for a tool as well: a terminal or an editor wants its whole surface.

**Measure text before you place it.** `canvas2d::measure_text` is the difference between a centred label and a nearly-centred one. Never estimate a width from the character count -- the face is proportional, so `i` and `W` differ about four times. Not optional anywhere: guessed spacing is the most visible defect a generated app ships with, and in a monospace grid it is what makes columns drift.

**Outline round things with `stroke_circle`, never `stroke_rect`.** A rim on a bubble, a ring, a dial, an unfilled dot -- all `stroke_circle(canvas, center, radius, width, colour)`. Reaching for `stroke_rect` instead puts a visible square box around a round shape, which a real generated app shipped with.

**Use the real rounded-rect call, never a hand-built one.** `canvas2d::fill_round_rect(canvas, area, radii, colour)` draws a card or a button in one call, correctly antialiased. Do not fake it by filling a rectangle and four `fill_circle` corners: the seams show, the edges alias differently from the curves, and it costs five calls to look worse. `stroke_round_rect` is the bordered version. A radius of 8-12 reads as a control, 16-20 as a card, and half the height as a pill.

**Put a soft shadow under anything that floats.** `canvas2d::drop_shadow_round_rect(canvas, area, radii, blur, colour)`, drawn *before* the card itself and offset a few pixels down. Use a low-alpha black (alpha 0.15-0.3) and a blur near the corner radius. A card with no shadow on a flat background looks painted on; one with a shadow looks placed. A shadow under something that does not float -- a table row, a column heading -- looks like a mistake, because it is one.

**Reach for gradients, with stops.** `linear_gradient_stops(canvas, area, angle_degrees, stops)` takes an angle and a list, so a backdrop can run diagonally through three colours instead of straight down through two. `radial_gradient` with a transparent outer colour is still the way to put a soft glow behind something important.

**Vary the font weight -- this is what makes text look designed.** `draw_text_styled` takes a weight (400 body, 600-700 headings and big numbers), italic, and letter spacing. Big numbers read best at 600-700 with slightly negative letter spacing. An app whose text is all one weight looks like a form no matter how good the colours are; measure with `measure_text_styled` so the styled width is the one you place against.

**Give things room.** Cramped is a common reason a generated app looks wrong: 16-24px of padding inside a card, 12-16px between rows, and a clear margin around the window edge. On a tool, spend that budget on row spacing rather than on padding around a shrinking grid.

**Pick three colours and stop.** A dark background, one bright accent for the thing you want clicked, and one ink colour for text. Every extra hue makes it look less designed, not more.

---

# Worked examples

Both of these are real shipped Krate apps, copied here from the repository
verbatim. They compile, they pass the import check, and they run. Start from
whichever is closer to what you were asked for and adapt it -- do not write the
`no_std` discipline from a blank page.

## Example 1 -- a CLI app: `krate-clock`

Prints the time, the timezone, and the locale. This is the smallest complete
shape of a CLI app: `#![no_std]`, `use krate::...`, `impl Guest for Component`,
`krate::export!`. Note that it never calls `println!` -- output goes through
`stdio::stdout()`.

`src/lib.rs`:

```rust
// A Krate guest is no_std: the SDK owns the allocator, panic handler, and
// mem intrinsics, so this app cannot pull std's latent wasi:* imports.
#![no_std]
extern crate alloc;

use krate::{
    io::{
        stdio,
        streams::{OutputStream, OutputStreamExt},
    },
    locale::{self, DateStyle},
    time,
    Guest,
};

struct Component;

impl Guest for Component {
    fn run() -> i32 {
        let millis = time::now_millis();
        let locale = locale::current();
        let timezone = locale::timezone();
        let date = locale::format_date(millis, &timezone, DateStyle::Medium, &locale);

        let stdout = stdio::stdout();
        if !write_pair(&stdout, "app", "krate-clock")
            || !write_pair(&stdout, "timezone", &timezone)
            || !write_pair(&stdout, "locale", &locale.bcp47)
            || !write_pair(&stdout, "date", &date)
            || stdout.flush().is_err()
        {
            return 20;
        }

        0
    }
}

fn write_line(stream: &OutputStream, value: &str) -> bool {
    stream.write_line(value).is_ok()
}

fn write_pair(stream: &OutputStream, key: &str, value: &str) -> bool {
    stream.write_text(key).is_ok() && stream.write_text("=").is_ok() && write_line(stream, value)
}

krate::export!(Component);
```

`manifest.toml`:

```toml
[app]
id = "dev.krate.clock"
name = "krate-clock"
version = "0.1.0-dev"
entry = "target/wasm32-wasip1/release/krate_clock.wasm"
world = "krate:app/cli@0.1.0"

[[capabilities]]
cap = "io.stdout"
rationale = "Print the formatted clock output"
required = true

[[capabilities]]
cap = "time.clock"
rationale = "Read the current wall-clock time"
required = true

[[capabilities]]
cap = "locale.info"
rationale = "Read the user's locale and timezone"
required = true

[[capabilities]]
cap = "locale.format"
rationale = "Format the timestamp through the host locale adapter"
required = true
```

## Example 2 -- a GUI app: `krate-checklist`

A checklist that saves. This is the shape to copy for almost any windowed app,
and it shows every part in one file: opening a window, building the widget tree,
binding a canvas, drawing the whole interface by hand, hit-testing clicks against
the rectangles it drew, handling typed text, saving to the key-value store, and
handling the `quick` verification run.

Read how it stays panic-free: fixed-size arrays instead of `Vec`, `[u8; N]` text
buffers, `.get()` everywhere instead of `[i]`, and `push_num` to format numbers
without `format!`.

`src/lib.rs`:

```rust
//! Krate Checklist -- a modern dark checklist drawn entirely on a canvas.
//!
//! The whole UI is painted by the app into one `gfx.canvas2d`: a bold title, a
//! "N of M done" progress line with a filled bar, item rows as rounded cards
//! each with a drawn checkbox that fills with an accent color when checked, a
//! text field to type a new item, and an accent "Add" button. Clicks are
//! hit-tested against the rectangles the app drew, so a drawn checkbox and a
//! drawn button are really clickable; typed characters flow into the draft.
//! Every toggle and add saves to the key-value store, so the list survives a
//! close.
//!
//! `#![no_std]` is the discipline that keeps it `krate:*`-only: the SDK owns the
//! allocator and a trapping panic handler, so no path drags in the `wasi:*`
//! import set. Items live in fixed-capacity arrays; text is fixed byte buffers;
//! numbers are formatted by hand. No `Vec`, `format!`, `unwrap`, or panicking
//! index.

#![no_std]
#![allow(clippy::needless_range_loop)]

extern crate alloc;

// Linked purely for its no_std runtime lang items -- the global allocator, the
// trapping panic handler, and the memory intrinsics a wasm guest needs when std
// is not linked. Not called directly; the underscore keeps the import.

use krate::bindings::krate::io::{args, stdio};
use krate::bindings::krate::store::kv as store_kv;
use krate::gfx::{canvas2d, types as gfx};
use krate::ui::{events, tree, types, window};

const ROOT_ID: u64 = 1;
const CANVAS_ID: u64 = 2;

const WIDTH: f32 = 440.0;
const HEIGHT: f32 = 620.0;

/// How many checklist items the app can ever hold. Fixed so nothing allocates.
const MAX_ITEMS: usize = 32;
/// Bytes of text one item can hold. Fixed for the same no-allocation reason.
const ITEM_TEXT_CAP: usize = 128;
/// The one key this app keeps its items under.
const DATA_KEY: &str = "items";

/// The items seeded on the very first run, so a fresh open is not empty.
const SEED_ITEMS: [&str; 3] = ["Buy milk", "Write the pitch", "Ship the demo"];

/// Interactive runs stay open until the person closes the window; automated
/// runs pass `quick` and exit promptly.
const MAX_WAIT_ROUNDS: u32 = 600_000;
const WAIT_ROUND_MILLIS: u32 = 33;
/// Consecutive quiet rounds before a headless run stops waiting (~10s).
const MAX_IDLE_ROUNDS: u32 = 300;

// ---- layout constants (the rectangles the app draws and hit-tests) ----
const MARGIN: f32 = 28.0;
const CONTENT_W: f32 = WIDTH - MARGIN * 2.0;
const LIST_TOP: f32 = 148.0;
const ROW_H: f32 = 52.0;
const ROW_GAP: f32 = 10.0;
/// How many rows fit in the region before the input strip.
const VISIBLE_ROWS: usize = 6;
const CHECK_SIZE: f32 = 24.0;

const INPUT_H: f32 = 46.0;
const ADD_W: f32 = 92.0;

struct Component;

/// One checklist item: its text (fixed capacity), whether it is done, and
/// whether the slot is in use. Copyable so the list is a plain fixed array.
#[derive(Clone, Copy)]
struct Item {
    text: [u8; ITEM_TEXT_CAP],
    text_len: usize,
    done: bool,
    used: bool,
}

impl Item {
    const EMPTY: Item = Item {
        text: [0; ITEM_TEXT_CAP],
        text_len: 0,
        done: false,
        used: false,
    };

    fn text_str(&self) -> &str {
        let slice = self.text.get(..self.text_len).unwrap_or(&[]);
        core::str::from_utf8(slice).unwrap_or("")
    }

    fn set_text(&mut self, text: &str) {
        self.text_len = 0;
        for byte in text.as_bytes() {
            if let Some(slot) = self.text.get_mut(self.text_len) {
                *slot = *byte;
                self.text_len += 1;
            }
        }
    }
}

/// The whole checklist: a fixed array of items plus how many slots are live.
struct Checklist {
    items: [Item; MAX_ITEMS],
    len: usize,
}

impl Checklist {
    const fn new() -> Self {
        Self {
            items: [Item::EMPTY; MAX_ITEMS],
            len: 0,
        }
    }

    fn push(&mut self, text: &str, done: bool) {
        if let Some(slot) = self.items.get_mut(self.len) {
            slot.set_text(text);
            slot.done = done;
            slot.used = true;
            self.len += 1;
        }
    }

    fn toggle(&mut self, index: usize) {
        if let Some(item) = self.items.get_mut(index) {
            if item.used {
                item.done = !item.done;
            }
        }
    }

    fn done_count(&self) -> usize {
        let mut n = 0usize;
        let mut i = 0usize;
        while i < self.len {
            if let Some(it) = self.items.get(i) {
                if it.used && it.done {
                    n += 1;
                }
            }
            i += 1;
        }
        n
    }
}

/// The text of the new item being typed, before it is added. A fixed buffer so
/// nothing allocates; append and pop only.
struct Draft {
    text: [u8; ITEM_TEXT_CAP],
    len: usize,
}

impl Draft {
    const fn new() -> Self {
        Self {
            text: [0; ITEM_TEXT_CAP],
            len: 0,
        }
    }

    fn as_str(&self) -> &str {
        let slice = self.text.get(..self.len).unwrap_or(&[]);
        core::str::from_utf8(slice).unwrap_or("")
    }

    fn is_empty(&self) -> bool {
        self.len == 0
    }

    fn clear(&mut self) {
        self.len = 0;
    }

    fn push(&mut self, byte: u8) {
        if let Some(slot) = self.text.get_mut(self.len) {
            *slot = byte;
            self.len += 1;
        }
    }

    fn pop(&mut self) {
        self.len = self.len.saturating_sub(1);
    }

    /// Replace the whole draft, used when a native control reports its full
    /// text after any edit.
    fn set(&mut self, text: &str) {
        self.len = 0;
        for byte in text.as_bytes() {
            let printable = byte.is_ascii_graphic() || *byte == b' ';
            if printable {
                self.push(*byte);
            }
        }
    }
}

// ---- persistence: same on-store shape as before, `[x]/[ ] text` lines ----

fn load(list: &mut Checklist) -> bool {
    *list = Checklist::new();
    let Ok(Some(data)) = store_kv::get(DATA_KEY) else {
        return false;
    };
    let mut start = 0usize;
    for i in 0..data.len() {
        if data.get(i).copied() == Some(b'\n') {
            parse_line(data.get(start..i).unwrap_or(&[]), list);
            start = i + 1;
        }
    }
    if start < data.len() {
        parse_line(data.get(start..).unwrap_or(&[]), list);
    }
    true
}

fn parse_line(line: &[u8], list: &mut Checklist) {
    if line.len() < 4 {
        return;
    }
    let done = line.get(1).copied() == Some(b'x');
    let text = line.get(4..).unwrap_or(&[]);
    if let Ok(text) = core::str::from_utf8(text) {
        if list.len < MAX_ITEMS {
            list.push(text, done);
        }
    }
}

fn save(list: &Checklist) -> bool {
    let mut out = [0u8; MAX_ITEMS * (ITEM_TEXT_CAP + 8)];
    let mut len = 0usize;
    let push = |bytes: &[u8], out: &mut [u8], len: &mut usize| {
        for byte in bytes {
            if let Some(slot) = out.get_mut(*len) {
                *slot = *byte;
                *len += 1;
            }
        }
    };
    for i in 0..list.len {
        let Some(item) = list.items.get(i) else {
            continue;
        };
        if !item.used {
            continue;
        }
        push(
            if item.done { b"[x] " } else { b"[ ] " },
            &mut out,
            &mut len,
        );
        push(item.text_str().as_bytes(), &mut out, &mut len);
        push(b"\n", &mut out, &mut len);
    }
    store_kv::set(DATA_KEY, out.get(..len).unwrap_or(&[])).is_ok()
}

// ------------------------------------------------------------------
// Hit testing: which control, if any, contains (x, y)?
// ------------------------------------------------------------------

fn row_y(index: usize) -> f32 {
    LIST_TOP + (index as f32) * (ROW_H + ROW_GAP)
}

fn hit_row(list: &Checklist, x: f32, y: f32) -> Option<usize> {
    if x < MARGIN || x > WIDTH - MARGIN {
        return None;
    }
    let shown = if list.len < VISIBLE_ROWS {
        list.len
    } else {
        VISIBLE_ROWS
    };
    let mut i = 0usize;
    while i < shown {
        let ry = row_y(i);
        if y >= ry && y <= ry + ROW_H {
            return Some(i);
        }
        i += 1;
    }
    None
}

fn input_y() -> f32 {
    HEIGHT - 76.0
}

fn hit_add(x: f32, y: f32) -> bool {
    let ay = input_y();
    let ax = WIDTH - MARGIN - ADD_W;
    x >= ax && x <= ax + ADD_W && y >= ay && y <= ay + INPUT_H
}

fn hit_field(x: f32, y: f32) -> bool {
    let ay = input_y();
    let fx = MARGIN;
    let fw = CONTENT_W - ADD_W - 12.0;
    x >= fx && x <= fx + fw && y >= ay && y <= ay + INPUT_H
}

// ------------------------------------------------------------------
// Palette
// ------------------------------------------------------------------

const BG_TOP: gfx::Color = gfx::Color {
    r: 0.075,
    g: 0.086,
    b: 0.125,
    a: 1.0,
};
const BG_BOT: gfx::Color = gfx::Color {
    r: 0.043,
    g: 0.051,
    b: 0.078,
    a: 1.0,
};
const CARD: gfx::Color = gfx::Color {
    r: 0.129,
    g: 0.145,
    b: 0.196,
    a: 1.0,
};
const CARD_DONE: gfx::Color = gfx::Color {
    r: 0.102,
    g: 0.118,
    b: 0.161,
    a: 1.0,
};
const INK: gfx::Color = gfx::Color {
    r: 0.902,
    g: 0.925,
    b: 0.98,
    a: 1.0,
};
const INK_DIM: gfx::Color = gfx::Color {
    r: 0.478,
    g: 0.525,
    b: 0.627,
    a: 1.0,
};
const INK_DONE: gfx::Color = gfx::Color {
    r: 0.435,
    g: 0.475,
    b: 0.561,
    a: 1.0,
};

// ------------------------------------------------------------------
// Rendering
// ------------------------------------------------------------------

fn draw(
    canvas: u64,
    list: &Checklist,
    draft: &Draft,
    field_focus: bool,
) -> Result<(), gfx::GfxError> {
    // Deep, considered ground -- a soft vertical gradient, not flat black.
    canvas2d::linear_gradient(
        canvas,
        gfx::Rect {
            x: 0.0,
            y: 0.0,
            width: WIDTH,
            height: HEIGHT,
        },
        BG_TOP,
        BG_BOT,
    )?;

    let accent = color(0.42, 0.62, 1.0, 1.0);
    let accent_soft = color(0.42, 0.62, 1.0, 0.16);

    // ---- header: bold title + progress ----
    draw_text(canvas, "Checklist", MARGIN, 58.0, 34.0, INK)?;

    let total = list.len;
    let done = list.done_count();
    let mut buf = [0u8; 32];
    let sub = progress_label(done as u32, total as u32, &mut buf);
    if let Ok(txt) = core::str::from_utf8(sub) {
        draw_text(canvas, txt, MARGIN, 88.0, 15.0, INK_DIM)?;
    }

    // Progress bar track + accent fill.
    let bar_y = 108.0;
    let bar_w = CONTENT_W;
    rounded_rect(
        canvas,
        MARGIN,
        bar_y,
        bar_w,
        8.0,
        4.0,
        color(0.16, 0.18, 0.24, 1.0),
    )?;
    if total > 0 {
        let frac = (done as f32 / total as f32).clamp(0.0, 1.0);
        let fw = (bar_w * frac).max(if done > 0 { 10.0 } else { 0.0 });
        if fw > 0.0 {
            rounded_rect(canvas, MARGIN, bar_y, fw, 8.0, 4.0, accent)?;
        }
    }

    // ---- item rows as cards ----
    let shown = if list.len < VISIBLE_ROWS {
        list.len
    } else {
        VISIBLE_ROWS
    };
    let mut i = 0usize;
    while i < shown {
        if let Some(item) = list.items.get(i) {
            if item.used {
                draw_row(canvas, i, item, accent)?;
            }
        }
        i += 1;
    }
    if list.len > VISIBLE_ROWS {
        let mut mbuf = [0u8; 24];
        let more = more_label((list.len - VISIBLE_ROWS) as u32, &mut mbuf);
        if let Ok(txt) = core::str::from_utf8(more) {
            draw_text(
                canvas,
                txt,
                MARGIN,
                row_y(VISIBLE_ROWS) + 4.0,
                13.0,
                INK_DIM,
            )?;
        }
    }

    // ---- input strip: text field + Add button ----
    let iy = input_y();
    let fw = CONTENT_W - ADD_W - 12.0;
    if field_focus {
        rounded_rect(
            canvas,
            MARGIN - 2.0,
            iy - 2.0,
            fw + 4.0,
            INPUT_H + 4.0,
            14.0,
            accent_soft,
        )?;
    }
    rounded_rect(
        canvas,
        MARGIN,
        iy,
        fw,
        INPUT_H,
        12.0,
        color(0.11, 0.125, 0.17, 1.0),
    )?;
    stroke_rounded(
        canvas,
        MARGIN,
        iy,
        fw,
        INPUT_H,
        12.0,
        color(0.24, 0.27, 0.35, 1.0),
    )?;

    let text_x = MARGIN + 16.0;
    let text_y = iy + INPUT_H * 0.5 + 6.0;
    if draft.is_empty() {
        draw_text(canvas, "Add an item...", text_x, text_y, 16.0, INK_DIM)?;
    } else {
        draw_text(canvas, draft.as_str(), text_x, text_y, 16.0, INK)?;
    }
    if field_focus {
        let cx = text_x + text_width(canvas, draft.as_str(), 16.0) + 2.0;
        fill(canvas, cx, iy + 12.0, 2.0, INPUT_H - 24.0, accent)?;
    }

    // Add button: filled accent rounded rect with a centered label.
    let ax = WIDTH - MARGIN - ADD_W;
    let can_add = !draft.is_empty();
    let btn = if can_add {
        accent
    } else {
        color(0.2, 0.24, 0.33, 1.0)
    };
    rounded_rect(canvas, ax, iy, ADD_W, INPUT_H, 12.0, btn)?;
    let label_ink = if can_add {
        color(0.05, 0.08, 0.16, 1.0)
    } else {
        INK_DIM
    };
    let lw = text_width(canvas, "Add", 17.0);
    draw_text(
        canvas,
        "Add",
        ax + (ADD_W - lw) * 0.5,
        text_y,
        17.0,
        label_ink,
    )?;

    canvas2d::present(canvas)?;
    Ok(())
}

/// One item row: a rounded card, a drawn checkbox that fills with accent when
/// checked (with a drawn tick), and the item text (dimmed + struck when done).
fn draw_row(
    canvas: u64,
    index: usize,
    item: &Item,
    accent: gfx::Color,
) -> Result<(), gfx::GfxError> {
    let y = row_y(index);
    let card = if item.done { CARD_DONE } else { CARD };
    shadow(canvas, MARGIN, y, CONTENT_W, ROW_H, 14.0)?;
    rounded_rect(canvas, MARGIN, y, CONTENT_W, ROW_H, 14.0, card)?;

    let bx = MARGIN + 16.0;
    let by = y + (ROW_H - CHECK_SIZE) * 0.5;
    if item.done {
        rounded_rect(canvas, bx, by, CHECK_SIZE, CHECK_SIZE, 7.0, accent)?;
        draw_tick(canvas, bx, by, CHECK_SIZE)?;
    } else {
        rounded_rect(
            canvas,
            bx,
            by,
            CHECK_SIZE,
            CHECK_SIZE,
            7.0,
            color(0.17, 0.19, 0.26, 1.0),
        )?;
        stroke_rounded(
            canvas,
            bx,
            by,
            CHECK_SIZE,
            CHECK_SIZE,
            7.0,
            color(0.35, 0.39, 0.49, 1.0),
        )?;
    }

    let tx = bx + CHECK_SIZE + 16.0;
    let ty = y + ROW_H * 0.5 + 6.0;
    let ink = if item.done { INK_DONE } else { INK };
    draw_text(canvas, item.text_str(), tx, ty, 17.0, ink)?;
    if item.done {
        let w = text_width(canvas, item.text_str(), 17.0);
        fill(canvas, tx, ty - 6.0, w, 1.5, INK_DONE)?;
    }
    Ok(())
}

/// A white checkmark inside a box at (bx, by) of side `s`.
fn draw_tick(canvas: u64, bx: f32, by: f32, s: f32) -> Result<(), gfx::GfxError> {
    let white = color(0.98, 0.99, 1.0, 1.0);
    let p0 = (bx + s * 0.24, by + s * 0.52);
    let p1 = (bx + s * 0.42, by + s * 0.70);
    let p2 = (bx + s * 0.78, by + s * 0.30);
    thick_line(canvas, p0.0, p0.1, p1.0, p1.1, 1.5, white)?;
    thick_line(canvas, p1.0, p1.1, p2.0, p2.1, 1.5, white)?;
    Ok(())
}

// ------------------------------------------------------------------
// Drawing helpers
// ------------------------------------------------------------------

fn fill(canvas: u64, x: f32, y: f32, w: f32, h: f32, c: gfx::Color) -> Result<(), gfx::GfxError> {
    canvas2d::fill_rect(
        canvas,
        gfx::Rect {
            x,
            y,
            width: w,
            height: h,
        },
        c,
    )
}

/// A filled rounded rectangle. One host call, antialiased on the curve --
/// the cross-of-rects-plus-corner-discs version this replaced showed seams
/// where the bands met the discs.
fn rounded_rect(
    canvas: u64,
    x: f32,
    y: f32,
    w: f32,
    h: f32,
    r: f32,
    c: gfx::Color,
) -> Result<(), gfx::GfxError> {
    canvas2d::fill_round_rect(
        canvas,
        gfx::Rect {
            x,
            y,
            width: w,
            height: h,
        },
        radii(r),
        c,
    )
}

/// A thin rounded-rect outline. The hand-built version drew the corners as
/// 1px dots, so every empty checkbox and text field had four visible gaps.
fn stroke_rounded(
    canvas: u64,
    x: f32,
    y: f32,
    w: f32,
    h: f32,
    r: f32,
    c: gfx::Color,
) -> Result<(), gfx::GfxError> {
    canvas2d::stroke_round_rect(
        canvas,
        gfx::Rect {
            x,
            y,
            width: w,
            height: h,
        },
        radii(r),
        1.5,
        c,
    )
}

/// A soft shadow under a card, drawn before the card itself.
fn shadow(canvas: u64, x: f32, y: f32, w: f32, h: f32, r: f32) -> Result<(), gfx::GfxError> {
    canvas2d::drop_shadow_round_rect(
        canvas,
        gfx::Rect {
            x,
            y: y + 2.0,
            width: w,
            height: h,
        },
        radii(r),
        10.0,
        color(0.0, 0.0, 0.0, 0.28),
    )
}

fn radii(r: f32) -> gfx::CornerRadii {
    gfx::CornerRadii {
        top_left: r,
        top_right: r,
        bottom_right: r,
        bottom_left: r,
    }
}

fn disc(canvas: u64, cx: f32, cy: f32, r: f32, c: gfx::Color) -> Result<(), gfx::GfxError> {
    canvas2d::fill_circle(canvas, gfx::Point { x: cx, y: cy }, r, c)
}

/// A thick line drawn as a chain of small discs so any angle reads smooth.
fn thick_line(
    canvas: u64,
    x0: f32,
    y0: f32,
    x1: f32,
    y1: f32,
    width: f32,
    c: gfx::Color,
) -> Result<(), gfx::GfxError> {
    let dx = x1 - x0;
    let dy = y1 - y0;
    let len = sqrtf(dx * dx + dy * dy).max(0.001);
    let steps = (len / 1.2) as i32 + 1;
    let mut i = 0i32;
    while i <= steps {
        let t = i as f32 / steps as f32;
        disc(canvas, x0 + dx * t, y0 + dy * t, width, c)?;
        i += 1;
    }
    Ok(())
}

fn draw_text(
    canvas: u64,
    text: &str,
    x: f32,
    y: f32,
    size: f32,
    c: gfx::Color,
) -> Result<(), gfx::GfxError> {
    canvas2d::draw_text(canvas, text, gfx::Point { x, y }, size, c)
}

fn color(r: f32, g: f32, b: f32, a: f32) -> gfx::Color {
    gfx::Color { r, g, b, a }
}

/// Rendered width of a string at a given font size, measured by the host with
/// the same font layout `draw_text` draws with.
///
/// This used to be `chars * size * 0.52`, an invented constant on a
/// proportional face where `i` and `W` differ about four times in real width,
/// so a centred label was not centred and a caret sat beside its text rather
/// than after it. `measure_text` is the true answer; the fallback is only
/// reached if the canvas handle is bad, in which case nothing else draws
/// either.
fn text_width(canvas: u64, s: &str, size: f32) -> f32 {
    match canvas2d::measure_text(canvas, s, size) {
        Ok(m) => m.width,
        Err(_) => 0.0,
    }
}

fn sqrtf(x: f32) -> f32 {
    if x <= 0.0 {
        return 0.0;
    }
    let mut g = x;
    let mut i = 0;
    while i < 6 {
        g = 0.5 * (g + x / g);
        i += 1;
    }
    g
}

// ---- number / label formatting into byte buffers, panic-free ----

fn progress_label(done: u32, total: u32, buf: &mut [u8; 32]) -> &[u8] {
    let mut pos = 0usize;
    push_num(buf, &mut pos, done);
    push_bytes(buf, &mut pos, b" of ");
    push_num(buf, &mut pos, total);
    push_bytes(buf, &mut pos, b" done");
    buf.get(..pos).unwrap_or(b"")
}

fn more_label(n: u32, buf: &mut [u8; 24]) -> &[u8] {
    let mut pos = 0usize;
    push_bytes(buf, &mut pos, b"+ ");
    push_num(buf, &mut pos, n);
    push_bytes(buf, &mut pos, b" more");
    buf.get(..pos).unwrap_or(b"")
}

fn push_bytes(buf: &mut [u8], pos: &mut usize, bytes: &[u8]) {
    for b in bytes {
        if let Some(slot) = buf.get_mut(*pos) {
            *slot = *b;
            *pos += 1;
        }
    }
}

fn push_num(buf: &mut [u8], pos: &mut usize, value: u32) {
    if value == 0 {
        if let Some(slot) = buf.get_mut(*pos) {
            *slot = b'0';
            *pos += 1;
        }
        return;
    }
    let mut scratch = [0u8; 10];
    let mut n = value;
    let mut count = 0usize;
    while n > 0 && count < scratch.len() {
        if let Some(slot) = scratch.get_mut(count) {
            *slot = b'0' + (n % 10) as u8;
        }
        n /= 10;
        count += 1;
    }
    let mut i = count;
    while i > 0 {
        i -= 1;
        if let Some(src) = scratch.get(i) {
            if let Some(dst) = buf.get_mut(*pos) {
                *dst = *src;
                *pos += 1;
            }
        }
    }
}

fn number_bytes(value: u32, buf: &mut [u8; 10]) -> &[u8] {
    let mut pos = 0usize;
    push_num(buf, &mut pos, value);
    buf.get(..pos).unwrap_or(b"0")
}

// ------------------------------------------------------------------
// Entry point
// ------------------------------------------------------------------

impl krate::Guest for Component {
    fn run() -> i32 {
        let size = types::WindowSize {
            width: WIDTH as u32,
            height: HEIGHT as u32,
        };
        let Ok(win) = window::create("Checklist", size) else {
            return 30;
        };
        if window::show(win).is_err() {
            return 31;
        }
        if tree::set_root(win, &stack_root()).is_err()
            || tree::upsert_node(win, &canvas_node()).is_err()
        {
            let _ = window::close(win);
            return 32;
        }
        let canvas = match canvas2d::bind(win, CANVAS_ID) {
            Ok(c) => c,
            Err(_) => {
                let _ = window::close(win);
                return 33;
            }
        };
        // The app's own coordinate system: keep drawing in these numbers
        // and the host scales them to any window, centred, never stretched
        // out of proportion (K-096).
        let _ = canvas2d::set_design_size(
            canvas,
            gfx::Size {
                width: WIDTH,
                height: HEIGHT,
            },
        );

        let mut list = Checklist::new();
        if !load(&mut list) || list.len == 0 {
            for seed in SEED_ITEMS {
                list.push(seed, false);
            }
        }
        let mut draft = Draft::new();
        let mut field_focus = false;

        let raw = args::raw();
        let quick = raw
            .as_bytes()
            .split(|byte| *byte == b'\n')
            .next()
            .is_some_and(|first| first == b"quick");

        let commit_draft = |list: &mut Checklist, draft: &mut Draft| -> bool {
            if draft.is_empty() || list.len >= MAX_ITEMS {
                return false;
            }
            list.push(draft.as_str(), false);
            draft.clear();
            save(list)
        };

        let mut saved_any = false;

        if quick {
            // The automated shot / verification run. Start from a clean, fixed
            // list rather than whatever prior CI runs accumulated, so the frame
            // is a believable, half-done checklist and does not grow every run.
            // Then prove the type + add + toggle + save paths on it.
            list = Checklist::new();
            list.push("Buy milk", true);
            list.push("Write the pitch", false);
            list.push("Ship the demo", false);
            list.push("Book the venue", false);
            draft.set("Record the walkthrough");
            if commit_draft(&mut list, &mut draft) {
                saved_any = true;
            }
            list.toggle(1);
            if save(&list) {
                saved_any = true;
            }
            let _ = draw(canvas, &list, &draft, false);
            report(&list, saved_any);
            let _ = window::close(win);
            return 0;
        }

        if draw(canvas, &list, &draft, field_focus).is_err() {
            let _ = window::close(win);
            return 34;
        }

        let mut idle_rounds = 0u32;
        let mut round = 0u32;
        while round < MAX_WAIT_ROUNDS {
            round += 1;
            let event = events::wait(Some(WAIT_ROUND_MILLIS));
            if event.is_none() {
                idle_rounds += 1;
                // Only a headless check gives up on silence. A person who
                // opens this and thinks for a moment must not watch the
                // window close itself.
                if quick && idle_rounds >= MAX_IDLE_ROUNDS {
                    break;
                }
                continue;
            }
            idle_rounds = 0;
            let mut dirty = false;
            let mut done = false;
            match event {
                Some(types::Event::Pointer(p)) if p.pressed => {
                    if let Some(index) = hit_row(&list, p.x, p.y) {
                        list.toggle(index);
                        if save(&list) {
                            saved_any = true;
                        }
                        field_focus = false;
                        dirty = true;
                    } else if hit_add(p.x, p.y) {
                        if commit_draft(&mut list, &mut draft) {
                            saved_any = true;
                        }
                        dirty = true;
                    } else if hit_field(p.x, p.y) {
                        field_focus = true;
                        dirty = true;
                    } else {
                        field_focus = false;
                        dirty = true;
                    }
                }
                Some(types::Event::TextChanged(changed)) => {
                    draft.set(&changed.text);
                    field_focus = true;
                    dirty = true;
                }
                Some(types::Event::TextInput(text)) => {
                    for byte in text.as_bytes() {
                        let printable = byte.is_ascii_graphic() || *byte == b' ';
                        if printable {
                            draft.push(*byte);
                        }
                    }
                    field_focus = true;
                    dirty = true;
                }
                Some(types::Event::Key(key)) if key.pressed => {
                    if key.key.as_bytes() == b"Backspace" {
                        draft.pop();
                        field_focus = true;
                        dirty = true;
                    } else if key.key.as_bytes() == b"Enter" || key.key.as_bytes() == b"Return" {
                        if commit_draft(&mut list, &mut draft) {
                            saved_any = true;
                        }
                        dirty = true;
                    }
                }
                Some(types::Event::CloseRequested(_)) => {
                    done = true;
                }
                _ => {}
            }
            if dirty {
                let _ = draw(canvas, &list, &draft, field_focus);
            }
            if done {
                break;
            }
        }

        report(&list, saved_any);
        let _ = window::close(win);
        0
    }
}

fn report(list: &Checklist, saved_any: bool) {
    let out = stdio::stdout();
    let _ = out.write(b"items:");
    let mut buf = [0u8; 10];
    let _ = out.write(number_bytes(list.len as u32, &mut buf));
    let _ = out.write(b"\n");
    if saved_any {
        let _ = out.write(b"saved:yes\n");
    }
}

// ----- widget builders (one canvas filling the window) -----

fn stack_root() -> types::WidgetNode {
    node(ROOT_ID, None, types::WidgetKind::Stack)
}

fn canvas_node() -> types::WidgetNode {
    node(CANVAS_ID, Some(ROOT_ID), types::WidgetKind::Canvas)
}

fn node(id: u64, parent: Option<u64>, kind: types::WidgetKind) -> types::WidgetNode {
    types::WidgetNode {
        id,
        parent,
        kind,
        label: None,
        role: None,
        style: types::Style {
            width: None,
            height: None,
            grow: 0.0,
            padding: 0.0,
            text: None,
            place: None,
            box_: None,
        },
        checked: None,
        value: None,
        selected: None,
        text_cursor: None,
    }
}

krate::export!(Component);
```

`manifest.toml`:

```toml
[app]
id = "dev.krate.checklist"
name = "Krate Checklist"
version = "0.1.0-dev"
entry = "target/wasm32-wasip1/release/krate_checklist.wasm"
world = "krate:app/gui@0.2.0"

[[capabilities]]
cap = "ui.window:create"
rationale = "Open the checklist window"
required = true

[[capabilities]]
cap = "io.stdout"
rationale = "Report state on exit for automated runs"
required = true

[[capabilities]]
cap = "io.args"
rationale = "Read the quick-run flag used by automated tests"
required = true

[[capabilities]]
cap = "store.kv"
rationale = "Save your checklist items"
required = true
```

---

# The handoff: you cannot build this, and you must say so

**You have no compiler.** You cannot run the app, you cannot see whether it
compiles, and you cannot check its imports. Do not tell the person their app
works, is tested, or is ready. Say what you wrote and hand it to them to build.

After the three files, end with the build instructions, adapted to their name:

> Save these three files into a folder, then run:
>
>     krate check-app .
>
> That builds the app, checks it imports only Krate APIs, and runs it once. If
> it prints `OK`, the app is real. If it fails, it names the stage and the exact
> fix -- paste that back to me and I will correct the code.
>
> If you do not have Krate yet, get it from https://krate.tech and let
> `krate create "<what you want>"` set the folder up for you first, which also
> fills in the paths marked `PREFIX`.

**When they paste back a failure, fix it against this document** -- the failing
stage names what went wrong, and the rules above say why. Common ones:

- *failed at imports, `wasi:*` found* -- a panic path is reachable. Look for
  `format!`, `.unwrap()`, `[i]` indexing, or a missing `krate` dependency.
- *no global memory allocator found* -- the `krate` dependency was removed from
  `Cargo.toml`. Put it back.
- *unresolved import / no function named ...* -- a function was invented. Find
  the real one in the API sections above.
- *failed at run* -- the app probably did not handle the bare `quick` argument,
  or it opened a window and waited forever.

**If the request is something Krate cannot do, say so instead of writing code.**
Krate apps cannot reach another person's device, run in the background, or read
your email or accounts. Producing a plausible-looking app over invented local
data wastes the person's build and is worse than a straight answer.
