The examples

Everything runnable lives under examples/glitter/; 32 namespaces, in two kinds:

  • Six interactive demos you open and click. Four of them are 7GUIs tasks, so they can be compared against implementations in other toolkits.
  • Four widget galleries — reference pages you can run. Between them they use all 43 registered tags, and each is written to be copied from; widgets.md quotes them. Run bb gallery-inputs, bb gallery-layout, bb gallery-display, bb gallery-chrome.
  • Twenty-seven live-GTK smokes that mount a real window, assert against real GTK state, and exit non-zero on failure. These are the project's actual regression suite for the GTK layer; the headless unit suite can't reach it.

Run any of them with bb <name>, or jolt -M:<name> without babashka. bb info prints the same grouping live.

Interactive demos

previewbb nameTaskWhat it demonstrates
counterThe canonical demo. One state atom, a pure state -> hiccup view, handlers as data. The 20-line version of the whole model.
todoA task board: derived counts computed inline (glitter has no reactive-derivation primitive, the view just re-runs), an entry with :change/:activate, checkbutton toggles, list rendering in a frame.
crudCRUDLive prefix filter, single-selection list box, name/surname fields, Create/Update/Delete. The spec's "separation of domain and presentation logic" is the pure get-people fn, shared by the view and the select-row expansion.
flightsFlight BookerConstraints between widgets and within one. The first glitter.nexus consumer, and the only demo that needs no action expansions at all: every interaction is at most two effects.
temperatureTemperature ConverterTwo linked numeric fields, each updating the other. Needs exactly one action expansion, because which field is the source depends on which one you just edited.
timerTimerThe first demo whose state advances on its own: a background tick via a demo-local :effect/schedule, plus a Reset button.

Every preview is a real recording of the demo running, not a mockup. They are committed under docs/demos/, and each thumbnail links to the full-size recording.

Widget galleries

Runnable reference pages rather than tasks. Between them they use all 43 registered tags, and each is written to be copied from — widgets.md quotes them per tag.

previewgallery
bb gallery-inputs — every value-bearing widget, all writing into one state atom, with a readout rendering that atom, so the signal → action → effect → swap! → re-render round trip is the visible subject.
bb gallery-layout — the containers, grouped by how each decides where a child goes: ordered lists, fixed named slots, child-driven placement, single-child wrappers.
bb gallery-display — the read-only widgets, with no signal of their own. One :scale drives all nine, so a single value flows into nine presentations of itself.
bb gallery-chrome — app chrome and navigation as controlled components: header/action bars, :menu-button + :popover, :notebook, :stack, :search-bar.

gallery_smoke.clj mounts all four against live GTK on every bb smokes run, so nothing quoted in the reference can quietly stop compiling.

Why the demos are worth reading, not just running

Each one is a deliberate contrast with how the same UI would be written in glimmer, the Reagent-style sibling. counter.clj says it directly: in glimmer, local state lives in a component-scoped ratom and a click closure calls swap! itself. Here all state is in one top-level atom, the view is a pure function of it, and click handlers are data dispatched through one global fn, never closures. That difference is the whole point of the project, and it is easier to see in 20 lines of counter.clj than in any prose.

The four 7GUIs demos add a second axis: each names the specific challenge its task is designed to expose, and shows what that challenge looks like under this model. flights.clj and crud.clj are the interesting pair: same engine, and one needs no action expansions while the other's interactions can't be expressed without them.

Two findings worth knowing, both from flights.clj

Lenient date parsing

flights.clj needed its own parse-date. t/parse-date from jolt-lang/time is lenient, not strict: verified live that "27.03.2014x", "not-a-date" and "31.02.2014" all parse without throwing. The demo wraps it in a round-trip check (parse, reformat with the same formatter, reject unless the result matches the trimmed input), which is the only way the spec's "T is coloured red when ill-formatted" requirement actually works.

This one is structural, not a version to wait out: jolt/time/fmt.clj's parse-with-pattern is a hand-rolled field scanner, and the library has no ResolverStyle/withResolverStyle at all — DateTimeFormatterBuilder's parseLenient and parseCaseInsensitive are (fn [b] b) no-ops.

(t/today) answered the UTC date — found here, fixed upstream

flights.clj defaults its departure field to today, and opened on yesterday for the first 10 hours of every AEST day. (t/today) answered the UTC date on every machine and ignored TZ even when it was set explicitly.

Two independent jolt-lang/time defects caused it — ZoneId/systemDefault hardcoded to UTC, and the now family ignoring a zone even when handed one — and fixing either alone was not enough. Both are fixed in jolt-lang/time#10, released as v0.0.7, which is the SHA deps.edn pins. The demo calls plain (t/today).

The pin is load-bearing, and so is the toolchain: a correct local date also needs jolt v0.7.24 or newer, because zone discovery reads TZ and an older jolt leaks TZ=UTC from its own boot probe (#712). v0.7.24 also carries #716, which takes (t/today) from ~1.15ms to ~13.6us. Full write-up, including why reaching for GLib instead does not dodge any of it: nexus.md.

Live-GTK smokes

bb smokes runs all twenty-seven in sequence and stops at the first failure. Each is also a standalone bb <name>.

These exist because GTK4 is a live, stateful system with a blocking main loop, and several of this project's fixed bugs were "obviously correct" on paper and wrong when actually run. Each smoke pins the specific behaviour that was once broken.

Smokes: reconciler behaviour

bb namePins
smokeMount a tree and run the loop without an exception escaping
keyedA keyed reorder lands in the right GTK order
replace-childA replaced child stays at its position, not the end
aliasedAliases expand through the real renderer, on mount and update
main-thread-smokeAn off-main-thread swap! renders on the GTK main thread
list-box-reorder-smokeThe g_object_ref_sink fix for the keyed-reorder use-after-dispose bug
gallery-smokeAll four widget galleries mount and round-trip; the :placeholder fix; :grid placement; the notebook/stack mount-time dispatch
ctor-apply-regression-smokeFour real ctor/apply bugs found in the round-11 audit stay fixed

Smokes: signals and value delivery

bb namePins
scale-smokevalue-changed delivers the right double, with no spurious dispatch
switch-smokestate-set (3-arg, non-void return) via a generalized callable
toggle-level-smoke:toggle-button's toggled, plus :level-bar re-render
link-button-smoke:link-button reuses :on-click; a real click via gtk_widget_activate
spin-button-list-box-smoke:spin-button's tag-safe value-fn; :list-box's row-selected
password-search-entry-smokeBoth reuse :entry's GtkEditable-delegate changed signal
expander-paned-smokenotify::* signals reuse the 3-arg-void callable shape
notebook-scale-button-smokeswitch-page's raw-arg read and mount-time auto-select; :scale-button's tag-aware value-changed

Smokes: containers and layout

bb namePins
revealer-center-box-smoke:revealer's props-driven reveal; :center-box's three named slots
overlay-flow-box-smoke:overlay's one-main-plus-N-overlay shape; :flow-box's verified difference from :list-box
aspect-frame-calendar-smokeSingle-child container reuse; :calendar's refcounted GDateTime round-trip
header-bar-action-bar-smokeThe hybrid title-plus-pack-start shape, including the show-title-buttons prepend
menu-button-popover-smoke:menu-button's popover-as-child relationship; :popover's guarded :visible
window-handle-stack-smoke:window-handle's single-child wrap; :stack's name-addressed pages
drop-down-grid-smoke:drop-down's GtkStringList selection round-trip; :grid's structural-props cell placement

Smokes: props and display-only widgets

bb namePins
class-smoke:class reaches real GTK CSS classes (add, coexist, remove on diff
leaf-widgets-smoke:spinner/:progress-bar/:image construction and re-render
picture-editable-label-smoke:picture re-render; :editable-label as a third GtkEditable reuse
inscription-search-bar-smokeDisplay and props only) the round where zero glitter.gtk changes were needed

Adding an example

Four touchpoints, and skipping any one of them leaves the example invisible to something:

  1. The namespace under examples/glitter/.
  2. A deps.edn alias, so jolt -M:<name> works without babashka.
  3. A bb.edn task, so bb <name> works and it shows up in bb info.
  4. If it's a smoke, add it to bb.edn's smokes list: otherwise bb smokes won't run it and CI-by-hand won't catch a regression in it.

A smoke must exit non-zero on failure. Do not gate it on jolt <task>: a deps.edn :tasks entry doesn't propagate its child's exit status, so jolt smoke prints failures and still exits 0. Use jolt -M:<alias> or a bb.edn task. See testing-and-tasks.md for the full rationale and CONTRIBUTING.md for the invariant list.