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.
  • Twenty-six 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
counterโ€”The canonical demo. One state atom, a pure state -> hiccup view, handlers as data. The 20-line version of the whole model.
todoโ€”A 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.

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.

One finding worth knowing: 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.

Live-GTK smokes

bb smokes runs all twenty-six 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
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.