Skip to content

sparkles:ui-app — Feature Requirements

Status: proposed — every row is not started or decided · Date: 2026-08-06 · Scope: libs/ui-app — the application host: backend selection (BKD), the shared window/font CLI (CLI), the frame/event loop (HST), the package graph (APP) and the testability obligations (TST).

Architecture (APP)

IDRequirementStatusTraces to
APP1The host must be a sibling package, not a layer inside sparkles:ui. The toolkit gains no dependency and no knowledge of window systems, terminals or the host itself (PKG1).not startedlibs/ui-app/dub.sdl
APP2An application depending on the host must be able to build without naming any backend: no sparkles:ui-tui, sparkles:ui-raylib, raylib or sparkles.tui import, and no such dependency in its dub.sdl.not startedlibs/ui-app/dub.sdl; consumer manifests
APP3The host must ship three configurationstui (default), gui, full — where gui keeps sparkles:tui out of the dependency closure entirely, because an Android build has no terminal and must not link one.not startedlibs/ui-app/dub.sdl
APP4Each backend arm must additionally be conditionally compiled: the terminal arm behind version (Posix) (its session type exists only there) and the GPU arm behind version (UiAppGui), so every configuration type-checks on every platform.not startedtui_loop.d; gui_loop.d
APP5Where the host's public API is a template (run, Host), the version-gated arms must be proven to resolve in the consumer's compilation, since a version identifier set by a dependency's configuration is what makes the arm visible there.decided — proven by the P1.0 spike; a dependency's configuration versions do reach the dependent, so APP3's configurations stand (UIAPP-O2)run.d

IMPORTANT

APP4 is not defensive. sparkles.ui_tui.session imports Terminal and PosixEvents unconditionally, and both live behind version (Posix): in libs/tui/src/sparkles/tui/terminal.d and input.d. Without the gate, the tui and full configurations fail to type-check on Windows and Android — which is exactly the platform hue's APK build targets.

Backend selection (BKD)

IDRequirementStatusTraces to
BKD1The host owns one backend vocabulary: Backend { gui, tui, html, ansi }. html and ansi are members because the toolkit already has an HTML target (TGT4) and a non-interactive ANSI sink; an application must not re-derive them.not startedbackend.d
BKD2The decision must be a pure function over an injected policy — CLI flags, whether the GPU backend was compiled in, stdin/stdout tty-ness, display presence — so the full matrix is testable with no tty, no display and no window.not startedbackend.d BackendPolicy, pickBackend
BKD3Environment probing ($DISPLAY / $WAYLAND_DISPLAY, $SSH_CONNECTION on macOS/Windows) must be a separate, callable function, never folded into the decision — a probe is impure, a decision is not.not startedbackend.d displayAvailable
BKD4The Android fact belongs here: on Android the surface is the application, so the answer is gui unconditionally. It is a statement about the process model, not a display heuristic — isTerminal and displayAvailable are both false there and would answer ansi.not startedbackend.d, version (Android)
BKD5run must accept auto and fall back in both directions: to the terminal when the GPU arm is not compiled in, and to the GPU when the platform has no terminal arm. With neither available it must report a typed failure rather than open nothing.not startedrun.d

Behavioral rules BKD2 preserves verbatim from today's apps/hue:

  • an explicit --gui wins even without GPU support — the sink reports the problem itself rather than the picker silently choosing something else;
  • --html selects html; --no-gui/--tui force the terminal;
  • otherwise: GUI when compiled in and stdout is a tty and a display is present; else the interactive TUI when stdin and stdout are both ttys; else ansi.

Window and font CLI (CLI)

IDRequirementStatusTraces to
CLI1The window/font flags must be declared once: a mixin template supplies the fields, and the standalone options struct is defined by that mixin. Two parallel declarations of the same vocabulary are the defect being removed.not startedgui_options.d GuiCliFields, GuiOptions
CLI2An application must be able to either embed the options struct or mix the fields into its own parameter struct, so an app whose flags are flat stays flat.not startedgui_options.d
CLI3There is one set of defaults across every application — including the default point size and the default font preference list. A per-application default is not offered.decidedgui_options.d; UIAPP-O1
CLI4Font resolution must be one implementation covering both routes: a fontconfig preference list, or a directory scan when --font-dir is given (which also disables fontconfig, so a build's font selection is deterministic).not startedgui_setup.d resolveFontPath
CLI5The setup order is part of the contract, not the caller's problem: open the window, load the font set, resolve the point size against the real display, then size the window to the loaded cell metrics.not startedgui_setup.d openGuiSession
CLI6Deterministic-capture and platform hooks (a pixel-size override that suppresses DPI scaling, extra font sources, a trace-log sink) must be parameters supplied by the caller, never environment reads inside the library.not startedgui_setup.d GuiRequest

IMPORTANT

CLI5 is a genuine ordering constraint, not a style preference: cell metrics do not exist until the font set loads, the font set cannot load until a GL context exists, and the requested window size is expressed in cells. Any host that reorders these silently produces a window of the wrong size.

CLI6 matters because the pixel-size override is what makes hue's golden-frame screenshot captures reproducible. A capture whose font size quietly follows the panel's DPI is a broken oracle, not a cosmetic difference.

The host contract (HST)

IDRequirementStatusTraces to
HST1The loop is entered through one call taking a configuration, a present callback and a handle callback; both callbacks receive ref Host. The application never names a canvas, a window, a terminal or an event source.not startedrun.d; host.d
HST2Host must be a per-backend template instantiation, not an interface — no vtable in the frame path, and @safe/@nogc/nothrow inferred from the concrete backend, matching the isCanvas discipline.not startedhost.d
HST3The host must offer all three render levels: a widget tree it lays out and paints, an appendable display-list buffer, and direct access to the concrete canvas for an application with its own renderer.not startedhost.d
HST4The per-frame display-list buffer is owned and reused by the host. An application never sizes, allocates or clears one.not startedhost.d ops()
HST5The application must be able to end the loop (quit()) and to request another frame (requestFrame()) — the latter is what an animation or an eased transition needs on a target that otherwise blocks on input.not startedhost.d
HST6The application must be able to decline to draw a frame (skipFrame()), and the host must honour it by presenting nothing: no cell diff on the terminal, and no buffer swap on the GPU target.not startedhost.d; gui_loop.d; tui_loop.d
HST7Resize must be normalized: the event handed to the application always carries the real surface size, on every backend, regardless of what the underlying producer reports.not startedrun.d
HST8The platform errands an interactive application actually performs must be on the host: pointer shape, clipboard, window title, out-of-band terminal writes, fullscreen toggle and its capability. An application must not reach past the host for them.not startedhost.d
HST9The loop must have a declared repaint policy per backend: the terminal blocks on input unless a frame was requested or an idle interval is configured; the GPU target paces to its frame rate. Background work must be expressible without dropping input.not startedrun.d; tui_loop.d

IMPORTANT

HST6 exists because of a measured behavior in apps/terminal: when nothing is dirty it polls input and paces the frame without swapping buffers, keeping the last frame on screen and idle CPU near zero. A host that unconditionally begins and ends a frame would erase that, so the ability to decline is part of the contract rather than an optimization a backend may or may not honour.

HST7 exists because the GPU event synthesizer emits a resize event with a zero size by design (the caller is expected to re-query). That is a reasonable producer contract and a trap for every consumer; the host absorbs it once.

Testability (TST)

IDRequirementStatusTraces to
TST1The host must ship a recording target as a supported third backend: a scripted event list in, and the frames, draw operations and platform calls the application asked for out. It requires no window and no tty.not startedrecord.d RecordingHost
TST2Every element of the host contract must be assertable through that target — quit, requested frames, skipped frames, all three render levels, resize normalization and end-of-input.not startedrecord.d
TST3The same scripted session driven through the recording target and through a live backend must produce the same draw-operation stream, so target parity is a test rather than a claim (TGT10, at session scope).not startedrecord.d; tui_loop.d
TST4Consumers of the host must move application decision logic out of modules excluded from their unittest builds. The excluded surface is tracked as a number, per application, and must fall.not startedPLAN

Non-goals

Not this package's jobWhere it belongs
Widget composition, layout, themingsparkles:ui
Drawing primitives, atlases, cell gridssparkles:ui-raylib, sparkles:ui-tui
The event vocabularysparkles:input (INP)
Argument parsing machinerysparkles:core-cli — the host contributes a vocabulary, not a parser
Document/content decisions (what to render)the application

Module coverage

Every planned source file is covered by at least one requirement; see the traceability table on the overview page.