Skip to content

sparkles:ui-app — Overview

Status: proposed — no implementation yet · Date: 2026-08-06 · Scope: the planned libs/ui-app package: the application host that owns backend selection, the shared window/font CLI, and the frame/event loop, so an application never names a canvas.

sparkles:ui is backend-free by construction (PKG1, TGT6), and the concrete canvases live in sibling packages. Nothing owns the layer above them: opening the right backend, resolving fonts, sizing a window, draining input, and driving a frame. Today apps/hue and apps/terminal each implement that layer privately, in mutually incompatible ways.

sparkles:ui-app is that layer, as one more sibling package — not a change to the toolkit.

Why

Three concrete duplications, each currently a source of drift:

TodayConsequence
hue and terminal each declare --font, --font-size, --window-width/height, --font-dir, …different spellings, different defaults (13 pt vs 14 pt), different resolution order for the same job
hue owns the backend decision (--gui/--no-gui, $DISPLAY, tty, Android); terminal has nonethe Android "the surface is the app" rule lives in one app's private comment
Each app hand-writes a frame loop against Window/RaylibEvents or TerminalSessiontwo loops with divergent resize, quit, pointer-shape and repaint policy; neither is unit-testable

The last row is the expensive one. apps/hue/src/gui.d (2536 lines), apps/hue/src/app.d (934) and apps/terminal/src/app.d (1340) are all excluded from their unittest builds, so 4810 lines of application behavior is verified only by manual passes and the screenshot oracle — not because the logic is untestable, but because it sits next to a window.

What it owns

ConcernModuleRequirements
Backend selectionbackend.dBKD
Window/font CLIgui_options.dCLI
Window/font setupgui_setup.dCLI
Frame/event looprun.d, host.dHST
Backend armstui_loop.d, gui_loop.dAPP
Headless test targetrecord.dTST

Render targets

The host instantiates one Host per target. All three satisfy the same shape, so an application's present/handle pair is written once:

TargetCanvasInputNotes
GUIRaylibCanvasRaylibEvents.pollpolls once per frame; Host.skipFrame() suppresses the buffer swap
TUIGridCanvasTerminalSessionblocks on input unless a frame was requested or an idle tick is set
recordingRecordingCanvasa scripted Event[]no window, no tty — the seam that makes an app's loop testable

The recording target is the loop-level analogue of the toolkit's RecordingCanvas (TGT10 asks that a widget tree be renderable through every target in a test; this extends that to a whole session).

The three render levels

An application defers as much of the pipeline as it wants, mirroring the toolkit's own three levels (UIA2):

LevelCallThe host doesConsumer
widgetshost.paint(tree, theme)layoutbuildDisplayListpaint(canvas)hue's chrome, diagram's menus
display listhost.ops() ~= opreplays the host-owned buffer into the canvasdiagram's board
direct canvashost.canvasnothing — the app drives isCanvas primitives itselfterminal's per-cell VT renderer

The third level is why apps/terminal can migrate at all: its renderer is a per-cell drawSolid/drawBox/drawGrapheme walk over a libghostty screen, and routing it through a DrawOp stream would be a rewrite of a benchmarked hot path.

Package graph

sparkles:ui-app  → ui, input, core-cli        config "tui":  + ui-tui
                                              config "gui":  + ui-raylib, version UiAppGui
                                              config "full": + both

sparkles:ui gains no dependency and no knowledge of the host — the direction of PKG1 is preserved.

Documentation map

PageWhat it covers
Overview (this page)what the host is · why it exists · targets · render levels · the status/ID scheme
Feature requirementsthe requirement tree: architecture (APP), backend selection (BKD), the CLI (CLI), the host contract (HST), testability (TST)
Delivery planexecution: the four phases, their dependencies, the acceptance gates, and what each phase makes testable
Open issuesdeferred decisions and the constraints behind them

Status scheme

Identical to the sparkles:ui schemenot started · researched · partial · full (<sha>) · decided — so the trees cross-reference without translation. Every row on this tree is currently not started or decided; the package does not exist yet.

ID scheme

<AREA><n>, unique within a document:

AreaMeaning
APParchitecture and package graph
BKDbackend selection — the flags, probes and platform facts behind the pick
CLIthe shared window/font command-line vocabulary and setup order
HSTthe host contract — the loop, the frame, and the platform errands
TSTtestability: the recording target and the coverage obligations

Traceability

Planned files, each owned by at least one requirement. The table is the code → requirement direction; the "Traces to" column of each row is the reverse.

Planned source fileAreas
libs/ui-app/src/sparkles/ui_app/backend.dBKD1BKD5
libs/ui-app/src/sparkles/ui_app/gui_options.dCLI1CLI3
libs/ui-app/src/sparkles/ui_app/gui_setup.dCLI4CLI6
libs/ui-app/src/sparkles/ui_app/host.dHST1HST8
libs/ui-app/src/sparkles/ui_app/run.dHST1, HST9, BKD5
libs/ui-app/src/sparkles/ui_app/tui_loop.dAPP4, HST6, HST7
libs/ui-app/src/sparkles/ui_app/gui_loop.dAPP4, HST6, HST7
libs/ui-app/src/sparkles/ui_app/record.dTST1TST3
libs/ui-app/dub.sdlAPP2APP4

Relationship to existing specs

SpecRelationship
sparkles:uithe toolkit this hosts; PKG1/TGT6 are the constraints that make the host a sibling rather than a layer inside it
sparkles:ui backendsthe isCanvas targets the host instantiates; TGT5 capability declaration is what the host forwards to the app
sparkles:inputthe event vocabulary the host drains; phase 0 extends it with key levels and the frame fold
hue UI architectureUIA7/UIA8 named the window and terminal seams; this spec is the layer above them
hue GUI, hue TUIthe two hosts being migrated onto this contract

Feature requirements · Delivery plan · Open issues