Build a click counter end to end: scaffold it, read the three files, add a theme, and watch hot reload swap markup and script while keeping the running state.
The counter template writes three files into a new directory and opens a live window.
lumenc new counter my-counter lumenc run my-counter
A Lumen app is a single directory. Only main.lmn is required; everything else is optional and resolves next to it.
my-counter/ ├── main.lmn # markup tree (required) ├── main.css # styling (optional) ├── main.rhai # script logic (optional) └── lumen.toml # per-app config (optional)
If main.css sits next to the entry file, it's applied automatically. Config lives in lumen.toml — window title and size, asset roots, and more — and unknown keys are rejected so typos surface as errors.
Every .lmn file has exactly one <root>. Unknown tags fail fast at parse time with a byte offset, so a typo can't slip through.
<root> <column padding="24" gap="12"> <label id="counter-label" text="Lumen — clicks: 0" /> <button id="bump">Click me</button> </column> </root>
<column> stacks children top to bottom; padding and gap take logical pixels. An id is the handle your script binds to.
An optional main.rhai holds view logic. on_start() runs once after the tree spawns — register handlers and seed signals there.
fn on_start() { let clicks = signal("clicks", 0); derive("counter-label", [clicks], |n| "Lumen — clicks: " + n); on("click", "bump", "handle_click"); } fn handle_click(_id) { let c = signal("clicks", 0); c.set(c.get() + 1); }
signal(name, default) returns a handle into the reactive store. derive(name, deps, fn) registers a computed value that re-runs whenever a dependency changes and writes the result back — and the label, whose id matches, re-renders each time.
Create main.css. Declare tokens on :root, reference them with var(--name), and remember: inline markup attributes always win over CSS.
:root { --bg: #0d0d14; --fg: #e6e6e6; --accent: #7aa2f7; } root { bg: var(--bg); text-color: var(--fg); } label { font-size: 18; text-align: center; } button { bg: var(--accent); text-color: #0d0d14; radius: 8; hover-bg: #9bb8ff; press-bg: #5f86d0; }
Save it. The running app re-parses the stylesheet and re-applies styling — no restart, no flicker.
With the app still running, edit any of the three files and save. lumenc run watches the directory and reloads only what changed — and each path preserves the state that matters.
| File | On save |
|---|---|
main.lmn | Re-parse and re-spawn, restoring stateful components by id — the counter value, input cursor, toggle state, and scroll position survive. |
main.css | Re-apply styling in place. Focus and hover state are kept; a class flip that changes nothing is a no-op. |
main.rhai | Swap the script while keeping the live scope, so signals and handler registrations don't blink back to their defaults. |
Change the button text from Click me to Bump it and save — the label flips instantly while the running count stays put. That state-preserving swap is the core of the Lumen workflow.
.get() reads it, .set(v) writes it and marks it dirty, and derive(...) recomputes downstream values. The same primitive drives text content, checkbox state, and slider values — bind-text, bind-checked, and bind-value close the loop in both directions.