Perch
Building modsRecipes

Recipes

The builtin packages as a cookbook — where to find the worked example of every mechanic.

3 min readUpdated Jul 21, 2026

Every builtin widget is written in the public vocabulary, loaded through the public path — which makes the builtin set the best mod documentation that can exist. When you want to build a thing, open the package that already does it.

The recipe map

You wantReadThe mechanic
glyph art from pathsweathera shape icon family composed from shared pieces, picked by bind
a month griddateone grid repeater over 38 cells, each cell’s look chosen by when on a kind field
live clockworkclockLua-generated dial trees, hands via rotate_bind + pivot = "bottom", the pill on a pure ticker
facepilesdiscorda keyed list with overlap, speaking rings via style_when { ring, glow }, motion = "halo" on the ringing avatar
album art + scrubbingmediaimage with fallback, an art-palette viz, a seek bar with {frac}, one card serving both page slots via show
hover docksappshover = "lift" icons, a hover = "scope" tile, hover_show/hover_hide label swap, action = "open:{i}"
a conic timerpomodoroa ring that style_when-recolors by phase, button glyphs swapped by when
transient HUDssystemvolume/brightness/connect/battery — transient contexts, snap bars, timeout_ms
an editable sheetnotepadfield multiline, on_widget_state, storage
an http widgetweather / financeasync fetch → fold → push; on_page_visibility throttling polls
launch tileslauncheropen scope, tile images from package assets

The builtins ship inside the app’s assets; with the source checkout they’re readable under assets/mods/<id>/.

Patterns worth stealing

One payload, many faces

The media package pushes one payload; five contexts bind subsets of it. Don’t maintain per-context state — compose views of one truth.

The waiting state is a state

Every builtin with async data declares its empty look in the tree (when = { field = "ready", ["not"] = true } gating a placeholder). A configured page slot is never a hole — yours shouldn’t be either.

Generated trees

displays.lua is a program that returns data — loops and helpers above the return are free:

local function numerals()
  local out = {}
  for i = 1, 12 do
    local a = i / 12 * 2 * math.pi
    out[#out + 1] = {
      type = "text", value = tostring(i), size = 9, weight = 700,
      at = { 32 + 26 * math.sin(a), 32 - 26 * math.cos(a) }, anchor = "center",
    }
  end
  return out
end

return {
  dial = { type = "frame", w = 64, h = 64, children = numerals() },
}

The output is still pure data — generation happens once at load.

Sticky choices belong in Lua

The Discord chip shows one face: the last speaker, sticky until a different participant speaks. That choice is provider/logic-side — pushed as solo — because the tree can only declare, never decide.


For a complete third-party package built end-to-end, the worldclock walk-through is exactly that.

esc
Type to search
navigate open