Documenting
How to write and check a page in this book. The book source is docs/, built by mdBook. The code
is the only source of truth: verify every behavioural claim in renderer/src, supervisor/src or
shared/src before writing it. DECISIONS.md records why things were built;
it is history and may be stale, so cite an ADR only as a “why” pointer after the code confirms it.
Commands
| Command | Does |
|---|---|
just docs | Serves the book at http://localhost:3000, rebuilt on save |
just book | Builds the book as CI publishes it, then checks every link and anchor |
just stubs | Regenerates every lua-meta/*.lua, every docs/capabilities/<name>.md and the generated property tables |
cargo test -p renderer every_lua_block_in_the_docs | Runs every Lua block in docs/ and checks every screenshot (part of just check) |
just shots | Re-renders the screenshots that changed, deletes orphans, lists what moved |
just rustdoc | Rustdoc for the crates, warnings as errors. Not this book |
Lua examples are tests
Every fenced block whose info string is lua or starts lua, runs in cargo test. The test is
every_lua_block_in_the_docs_evaluates_and_lays_out in renderer/src/check.rs. Each block goes
through the same evaluation and four layout passes as mantle check: no Wayland, no subprocesses,
every capability nil, then one push each of the first, alternate and empty sample sets, one
704x396 output named DP-1. The test
fails on a Lua error, a check error or a layout error, and names the block by file:line. Blocks
inside > quotes count too.
| Info string | The block |
|---|---|
lua | Runs and lays out. It returns surfaces as shell.lua does, or returns one node, which the test mounts in a panel with 24 px of padding |
lua,shot | A lua block the book shows a screenshot of (screenshots) |
lua,fragment | Only parses. For a snippet that needs context the page has not given, like a require of another file |
lua,must-fail | Must fail to evaluate or lay out. For showing a mistake |
lua,no-check | Skipped. Put the reason in an HTML comment on the line above |
Prefer lua over fragment, and fragment over no-check. mdBook turns the comma into a second
CSS class, so every tag still highlights as Lua.
A widget example needs no surface around it:
rect {
padding = 6,
on_click = function() print("clicked") end,
children = { text { content = "Click" } },
}
Evaluation accepts this width, but layout refuses it:
rect { width = "Wide", height = 10 }
A module file that only makes sense beside another one:
local clock = require("widgets.clock")
return panel { id = "bar", layer = "top", child = clock }
return { require("bar") } -- fails
local bar = require("bar")
return { bar } -- works
Screenshots
A lua,shot block also renders, headless over EGL, and must match its committed image,
docs/images/<section>/<page>-<n>.png, the page’s n-th shot. tools/book_links.py puts the image
under the block on the site, so a page never links it. Tag the example a reader wants to see; a
block has to return a node or surfaces.
| Part | Rule |
|---|---|
| Image | Every visible surface stacked top to bottom 8 px apart, each popup where its anchor_rect, anchor, gravity, offset and "slide_x" put it on its parent, at scale 1 over Catppuccin Mocha crust #11111b. Cropped to the painted pixels plus 16 px; a shot that paints nothing fails |
| Still | Drawn with every tween finished |
<!-- shot: frames=0..400/20 --> on the line above | An animated PNG: one frame per time, in ms after the last tween started. delay, pulse, timer and interval advance with the frame time and fire in order, so a staged sequence plays out. frames=0,50,120 lists them. Each frame shows until the next time and the last for a second; 0@900 holds one for 900 ms, so a loop can rest on the state worth seeing; 0..400/20@20 holds a range’s last frame, so a loop runs on without a pause. Viewers without APNG show the frame held longest |
<!-- shot-alt: ... --> immediately above a shot or its frame directive | Describes the visible result for readers who cannot see the image. Name the state or motion the example demonstrates, not the code syntax |
docs/images/<section>/<page>.fakes.lua | Runs before each shot on the page. fakes = { battery = {...} } is pushed as each capability’s first push, on_change included. __pointer = { surface = "bar", x = 40, y = 12 } rests the pointer there after the first layout, in that surface’s logical px (surface defaults to the first), so hover(), its rect and on_hover answer as for a real pointer. A __after function runs after the first layout, then the shot lays out again: that is how an OSD shows or a card leaves. A table it returns, shaped like fakes, is the next push, so a capability can change mid-shot. A popup a click opens needs its anchor state set to the rect that click would pass |
<!-- file: shaders/glow.frag --> on the line above any fenced block | That block is written to that path in each shot’s config directory, mantle.config_dir, so a page’s shots load the file the page shows |
| Pinned | Fonts, icons and images come from renderer/fixtures/shots, os.time() is 2026-09-24 12:45 UTC and os.date reads UTC, $USER is user |
A quoted absolute path whose file name is in renderer/fixtures/shots/images points at that file.
An icon missing from fixtures/shots/icons fails the test with its name; copy it in from Adwaita
and note it in fixtures/shots/NOTICE.
| Failure | Fix |
|---|---|
<name>.png differs by up to N per channel | The render moved. Compare <name>.new.png beside it. Intended: just shots. Not: fix the regression |
is missing or a different size | A new shot, or its size changed: just shots, then look at the image |
no lua,shot block draws this image | A shot was removed or renumbered: just shots deletes it |
| An APNG passes on one GPU and differs by 100+ on another | A moving edge lands on exactly half a pixel in some frame, and drivers round that differently. Pick frame times that miss it, such as 0..210/30 over 0..200/20 |
A render within 2 per channel of the committed image passes and is not rewritten, so another GPU driver never shows up in git. EGL is required: without it the test fails.
Capability pages
docs/capabilities/<name>.md is generated by supervisor/src/stubs.rs from the payload types in
shared/src/state/ and the action types in shared/src/action/.
Never edit it: write prose in docs/capabilities/intro/<name>.md and run just stubs.
| Part of the intro file | Lands on the page |
|---|---|
Above <!-- reference --> | After the one-line blurb, before the generated State and Actions tables |
Below <!-- reference --> | After the tables: How do I…, Gotchas, See also |
| No marker | All of it above the tables |
cargo test fails while a generated page differs from what just stubs would write. intro/ stays
out of SUMMARY.md.
Property tables
The property table on each docs/nodes/<kind>.md and docs/surfaces/<role>.md, the common one on
nodes/index.md and the box one on guide/paint.md are generated by renderer/src/lua/nodes/stubs.rs
from the typed fields in renderer/src/layout/node/properties.rs, the same fields lua-meta/nodes.lua
and surfaces.lua come from and the engine parses through. just stubs rewrites what sits between
two markers inside the page and leaves the rest alone:
<!-- Generated from renderer/src/layout/node/properties.rs by `just stubs`: edit the table there. -->
<!-- End of the generated table. -->
| Column | From the field |
|---|---|
| Type | Its Rust type’s LuaCATS, as the stub declares it; range after it |
| Default | absent |
| Behaviour | The Book: paragraph of its /// block, which may link relative to the page, else the rest of that block without (ADR-NNNN) |
A page links to the table’s anchors, not to rows. Prose about a property goes around the table, or into its row if every page showing it should say it.
A table a property takes, such as Edges or Transition, is declared with lua_shape! on the
struct its parser reads it as (renderer/src/lua/luacats.rs): its /// blocks are what lua-meta
says about the shape and each key. The struct fixes the key names and field types; which keys are
optional and how Lua spells one (as Option<T>, as S) are marked by hand beside them. One struct
has one shape, so anchor_rect, whose x and y may be left out, is declared as region’s Rect
with every key required. The book describes a shape in prose on the page.
Page shape
Start with the action or example a reader came for. Put prerequisites and implementation detail
after it. A reference page can then give property tables; a tutorial can explain the example in
small steps. Add How do I… or Gotchas only when they answer questions the page owns. The
introduction, sidebar and FAQ already route readers to topics and symptoms, so do not repeat their
indexes on every page.
Keep each behavioral fact on one page and link to it elsewhere. End technical pages with a
Source: line of code links. Links to code go out of docs/ as relative paths
(../../renderer/src/... from docs/<dir>/x.md); the book turns them into GitHub links and fails
the build on a missing target.