#!/usr/bin/env dub
/+ dub.sdl:
name "live-tasklist"
dependency "sparkles:core-cli" path="../../.."
targetPath "build"
// Optimised, assertions live, `debug {}` blocks out — the build every nix
// artifact uses. Neither `debug` (which compiles those blocks in) nor
// `release` (which deletes assert *expressions*, side effects included).
buildType "checked" {
buildOptions "optimize" "inline" "debugInfo"
}
+/
// ci: build-only
// The live-rendering stack (`ui/live` + `ui/tasklist` + `runStreaming`): a
// `LiveRegion` repaints a block in place (DEC 2026 synchronized frames, static
// channel for lines graduating into scrollback, non-tty degradation to a plain
// transition log); a `TaskReporter` drives a checklist through it, streaming a
// child process's output into the running task's bounded tail pane. Animated,
// so `ci` only builds this; run it in a terminal — and pipe it through `cat`
// to see the escape-free non-tty transition log.
module (module) live_tasklist_examplelive_tasklist_example;
import (package) corecore.(module) core.threadThe thread module provides support for thread creation and management.
Source
core/thread/package.d
thread : (class) core.thread.osthread.ThreadThis class encapsulates all threading functionality for the D
programming language. As thread manipulation is a required facility
for garbage collection, all user threads should derive from this
class, and instances of this class should never be explicitly deleted.
A new thread may be created using either derivation or composition, as
in the following example.
Thread;
import (package) corecore.(module) core.timeModule containing core time functionality, such as Duration (which
represents a duration of time) or MonoTime (which represents a
timestamp of the system's monotonic clock).
Various functions take a string (or strings) to represent a unit of time
(e.g. convert!("days", "hours")(numDays)). The valid strings to use
with such functions are "years", "months", "weeks", "days", "hours",
"minutes", "seconds", "msecs" (milliseconds), "usecs" (microseconds),
"hnsecs" (hecto-nanoseconds - i.e. 100 ns) or some subset thereof. There
are a few functions that also allow "nsecs", but very little actually
has precision greater than hnsecs.
Symbol Description Types Duration Represents a duration of time of weeks or less (kept internally as hnsecs). (e.g. 22 days or 700 seconds). TickDuration DEPRECATED Represents a duration of time in system clock ticks, using the highest precision that the system provides. MonoTime Represents a monotonic timestamp in system clock ticks, using the highest precision that the system provides. Functions convert Generic way of converting between two time units. dur Allows constructing a Duration from the given time units with the given length. weeks days hours
minutes seconds msecs
usecs hnsecs nsecs |
Convenience aliases for dur. |
| abs | Returns the absolute value of a duration. |
From Duration
From TickDuration
From units
To Duration tickDuration.to, std,conv!Duration() dur!"msecs"(5) or 5.msecs()
| To TickDuration |
duration.to, std,conv!TickDuration() |
|
TickDuration.from!"msecs"(msecs) |
| To units |
duration.total!"days" |
tickDuration.msecs |
convert!("days", "msecs")(msecs) |
Source
core/time.d
time : msecs;
import (package) sparklessparkles.(package) sparkles.core_clicore_cli.(module) sparkles.core_cli.process_utilsprocess_utils : (alias template) live_tasklist_example.runStreaming = sparkles.core_cli.process_utils.runStreaming(Sink)(const(string)[] args, scope Sink sink, string workDir = null, const string[string] env = null)Runs args (an argv array — no shell) streaming its output line by line into
sink as the child produces it, for live progress displays (a task list's
subprocess tail). stdout and stderr are merged into one stream so lines arrive
in output order; the full combined output is also returned in
CapturedResult.stdout (stderr stays empty).
Like runCaptured this never throws: spawn failures yield status == 127
with the error text in stderr. The single merged pipe is drained continuously,
so a chatty child cannot deadlock. Attributes infer from Sink (a callable
taking scope const(char)[]).
runStreaming;
import (package) sparklessparkles.(package) sparkles.basebase.(module) sparkles.base.term_capsTerminal capability probing: the synchronous size query (terminalSize),
tty and color detection (detectTermCaps), and resize notifications
(setTermWindowSizeHandler).
This is the single place the "what can this terminal do" decision is made;
renderers stay pure producers that take explicit widths/flags. It lives in
sparkles:base rather than a UI package because it is an environment query,
not a presentation concern — a logger, a CLI tool and a full-screen UI all need
it, and none of them should pull in a UI stack to ask.
TermSize is deliberately a plain POD rather than a
Vector specialization: base sits below
sparkles:math, and a capability snapshot never does vector arithmetic. The
terminal's geometry types — positions you add offsets to — live in
sparkles:tui (TermPosition), which is free to specialize Vector.
term_caps : (alias) live_tasklist_example.detectTermCaps = sparkles.base.term_caps.TermCaps sparkles.base.term_caps.detectTermCaps(bool noColors = false) @safeDetects capabilities and prepares the console.
Colors are on only when stdout is a terminal and neither noColors,
$NO_COLOR, nor TERM=dumb disables them; a non-empty, non-"0"
$CLICOLOR_FORCE forces them on for a non-tty (but never overrides an
explicit disable). colorDepth is the tier
(classifyColorDepth over $COLORTERM/$TERM)
when colors are on, else none. On Windows this additionally sets the output code page to
UTF-8 (so ✓ ✗ ⚙ render even without colors) and enables virtual-terminal
processing (so ANSI escapes are interpreted rather than printed literally);
colors stay off when stdout is redirected or VT can't be enabled.
detectTermCaps;
import (package) sparklessparkles.(package) sparkles.uiui.(package) sparkles.ui.componentscomponents.(module) sparkles.ui.components.liveA live region: a block of lines at the bottom of the normal scrollback flow that
is repainted in place (no alternate screen), with a static channel for lines
that graduate into scrollback above it — the log-update pattern (Ink/Bubble
Tea/Mosaic) per docs/specs/core-cli/tui-components §5.
Every repaint is framed in DEC 2026 synchronized-output markers (no tearing),
each frame line is truncated to the terminal width (a wrapped line would break
the cursor-up arithmetic), and the region re-reads the width every update so
resizes are picked up. On a non-interactive sink (piped output) update is a
no-op and printAbove degrades to plain appended lines, so redirected runs see
only the permanent output.
The region writes through an injected sink (testable without a terminal); use
stdoutLiveRegion for the real thing, and scope (exit) region.finish();
so a thrown exception never leaves the cursor hidden.
live : (alias) live_tasklist_example.stdoutLiveRegion = sparkles.ui.components.live.LiveRegion sparkles.ui.components.live.stdoutLiveRegion()A LiveRegion on stdout: interactive iff stdout is a terminal (each write is
flushed so frames appear atomically), width from terminalSize() per repaint.
stdoutLiveRegion;
import (package) sparklessparkles.(package) sparkles.uiui.(package) sparkles.ui.componentscomponents.(module) sparkles.ui.components.tasklistA checklist of tasks driven through a LiveRegion:
pending/running items are repainted in place, and each task that reaches a
terminal state (ok / failed / skipped) graduates as a permanent line into the
scrollback above — so on a tty the block shrinks as work completes, and on
piped output only the completed-task transitions appear, one line each.
renderTaskLine/renderTaskList are pure producers (unit-testable, theme-
driven); TaskReporter is the stateful driver the apps use.
tasklist : (struct) sparkles.ui.components.tasklist.TaskReporterThe stateful checklist driver. Owns the item states; each transition
repaints the live block and graduates finished tasks into the scrollback.
The LiveRegion stays owned by the caller (scope (exit) region.finish();).
TaskReporter;
import (package) sparklessparkles.(package) sparkles.uiui.(package) sparkles.ui.componentscomponents.(module) sparkles.ui.components.themeTheme layer: one border-charset selector shared by every framed component
(drawBox / drawHeader / drawTable), the status-glyph vocabulary with ASCII
fallbacks, semantic styles, and a Theme snapshot derived from
TermCaps.
Components stay pure producers — the theme only selects glyphs/styles once at
the edge (typically makeTheme(detectTermCaps()) at app startup); callers thread
the resulting values through the existing BoxProps/TableProps/colored
parameters.
theme : (alias) live_tasklist_example.makeTheme = sparkles.ui.components.theme.Theme sparkles.ui.components.theme.makeTheme(in sparkles.base.term_caps.TermCaps caps) pure nothrow @nogc @safeThe theme for a capability snapshot: ASCII borders + fallback glyphs on a
non-UTF-8 terminal, colors per the caps decision.
makeTheme;
void void D main()main()
{
const (local variable) const(sparkles.ui.components.theme.Theme) themetheme = sparkles.ui.components.theme.Theme sparkles.ui.components.theme.makeTheme(in sparkles.base.term_caps.TermCaps caps) pure nothrow @nogc @safeThe theme for a capability snapshot: ASCII borders + fallback glyphs on a
non-UTF-8 terminal, colors per the caps decision.
makeTheme(sparkles.base.term_caps.TermCaps sparkles.base.term_caps.detectTermCaps(bool noColors = false) @safeDetects capabilities and prepares the console.
Colors are on only when stdout is a terminal and neither noColors,
$NO_COLOR, nor TERM=dumb disables them; a non-empty, non-"0"
$CLICOLOR_FORCE forces them on for a non-tty (but never overrides an
explicit disable). colorDepth is the tier
(classifyColorDepth over $COLORTERM/$TERM)
when colors are on, else none. On Windows this additionally sets the output code page to
UTF-8 (so ✓ ✗ ⚙ render even without colors) and enables virtual-terminal
processing (so ANSI escapes are interpreted rather than printed literally);
colors stay off when stdout is redirected or VT can't be enabled.
detectTermCaps());
auto (local variable) sparkles.ui.components.live.LiveRegion regionregion = sparkles.ui.components.live.LiveRegion sparkles.ui.components.live.stdoutLiveRegion()A LiveRegion on stdout: interactive iff stdout is a terminal (each write is
flushed so frames appear atomically), width from terminalSize() per repaint.
stdoutLiveRegion();
scope (exit)
(local variable) sparkles.ui.components.live.LiveRegion regionregion.void sparkles.ui.components.live.LiveRegion.finish(bool keepFrame = true)End the region: restore the cursor and either keep the last frame as
permanent output (default) or erase it. Idempotent; call it from a
scope (exit) so exceptions can't leave the cursor hidden.
finish();
auto (local variable) sparkles.ui.components.tasklist.TaskReporter taskstasks = (struct) sparkles.ui.components.tasklist.TaskReporterThe stateful checklist driver. Owns the item states; each transition
repaints the live block and graduates finished tasks into the scrollback.
The LiveRegion stays owned by the caller (scope (exit) region.finish();).
TaskReporter(&sparkles.ui.components.tasklist.TaskReporter sparkles.ui.components.tasklist.TaskReporter.this(sparkles.ui.components.live.LiveRegion* region, sparkles.ui.components.theme.Theme theme) refregion, (local variable) const(sparkles.ui.components.theme.Theme) themetheme);
// Register everything up front so the pending rows show the plan.
const (local variable) const(ulong) fetchfetch = (local variable) sparkles.ui.components.tasklist.TaskReporter taskstasks.ulong sparkles.ui.components.tasklist.TaskReporter.add(string label, ulong indent = 0LU)Register a task (pending) and return its handle.
add("fetch dependencies");
const (local variable) const(ulong) buildbuild = (local variable) sparkles.ui.components.tasklist.TaskReporter taskstasks.ulong sparkles.ui.components.tasklist.TaskReporter.add(string label, ulong indent = 0LU)Register a task (pending) and return its handle.
add("build (streams output into the tail pane)");
const (local variable) const(ulong) lintlint = (local variable) sparkles.ui.components.tasklist.TaskReporter taskstasks.ulong sparkles.ui.components.tasklist.TaskReporter.add(string label, ulong indent = 0LU)Register a task (pending) and return its handle.
add("lint");
const (local variable) const(ulong) publishpublish = (local variable) sparkles.ui.components.tasklist.TaskReporter taskstasks.ulong sparkles.ui.components.tasklist.TaskReporter.add(string label, ulong indent = 0LU)Register a task (pending) and return its handle.
add("publish");
(local variable) sparkles.ui.components.tasklist.TaskReporter taskstasks.void sparkles.ui.components.tasklist.TaskReporter.start(ulong id)Mark id running (starts its clock) and repaint.
start((local variable) const(ulong) fetchfetch);
foreach ((local variable) int ii; 0 .. 8)
{
(local variable) sparkles.ui.components.tasklist.TaskReporter taskstasks.void sparkles.ui.components.tasklist.TaskReporter.tick()Advance the spinner (call from a periodic tick when one is available).
tick(); // spinner animation between events
(class) core.thread.osthread.ThreadThis class encapsulates all threading functionality for the D
programming language. As thread manipulation is a required facility
for garbage collection, all user threads should derive from this
class, and instances of this class should never be explicitly deleted.
A new thread may be created using either derivation or composition, as
in the following example.
Thread.void core.thread.osthread.Thread.sleep(core.time.Duration val) nothrow @nogc @trustedSuspends the calling thread for at least the supplied period. This may
result in multiple OS calls if period is greater than the maximum sleep
duration supported by the operating system.
In
period must be non-negative.
Example
Thread.sleep( dur!("msecs")( 50 ) ); // sleep for 50 milliseconds
Thread.sleep( dur!("seconds")( 5 ) ); // sleep for 5 seconds
sleep(80.msecs);
}
(local variable) sparkles.ui.components.tasklist.TaskReporter taskstasks.void sparkles.ui.components.tasklist.TaskReporter.succeed(ulong id, string detail = null)Terminal transitions: the task's line graduates into the scrollback.
A multi-line detail puts the first line on the task row and the rest
as indented follow-up lines (e.g. a failure's output tail).
succeed((local variable) const(ulong) fetchfetch);
// A real child process: each output line lands in the bounded tail pane
// (last 4 lines) under the running row, and pulses the spinner.
(local variable) sparkles.ui.components.tasklist.TaskReporter taskstasks.void sparkles.ui.components.tasklist.TaskReporter.start(ulong id)Mark id running (starts its clock) and repaint.
start((local variable) const(ulong) buildbuild);
sparkles.core_cli.process_utils.CapturedResult sparkles.core_cli.process_utils.runStreaming!(void delegate(scope const(char)[] line) @system)(const(string)[] args, scope void delegate(scope const(char)[] line) @system sink, string workDir = null, const(string[string]) env = cast(const(string[string]))null) nothrow @systemRuns args (an argv array — no shell) streaming its output line by line into
sink as the child produces it, for live progress displays (a task list's
subprocess tail). stdout and stderr are merged into one stream so lines arrive
in output order; the full combined output is also returned in
CapturedResult.stdout (stderr stays empty).
Like runCaptured this never throws: spawn failures yield status == 127
with the error text in stderr. The single merged pipe is drained continuously,
so a chatty child cannot deadlock. Attributes infer from Sink (a callable
taking scope const(char)[]).
runStreaming(["sh", "-c",
"for i in $(seq 1 12); do echo compiling module $i; sleep 0.15; done"],
(scope const(char)[] (parameter) const(char)[] lineline) { (local variable) sparkles.ui.components.tasklist.TaskReporter taskstasks.void sparkles.ui.components.tasklist.TaskReporter.output(ulong id, scope const(char)[] line)Feed one live-output line into the running task id's bounded tail
pane (keeps the last tailLines), advancing the spinner and repainting.
No-op for tasks that are not running (late output after completion is
dropped — the frame no longer shows the row).
output((local variable) const(ulong) buildbuild, (parameter) const(char)[] lineline); });
(local variable) sparkles.ui.components.tasklist.TaskReporter taskstasks.void sparkles.ui.components.tasklist.TaskReporter.succeed(ulong id, string detail = null)Terminal transitions: the task's line graduates into the scrollback.
A multi-line detail puts the first line on the task row and the rest
as indented follow-up lines (e.g. a failure's output tail).
succeed((local variable) const(ulong) buildbuild);
(local variable) sparkles.ui.components.tasklist.TaskReporter taskstasks.void sparkles.ui.components.tasklist.TaskReporter.start(ulong id)Mark id running (starts its clock) and repaint.
start((local variable) const(ulong) lintlint);
(class) core.thread.osthread.ThreadThis class encapsulates all threading functionality for the D
programming language. As thread manipulation is a required facility
for garbage collection, all user threads should derive from this
class, and instances of this class should never be explicitly deleted.
A new thread may be created using either derivation or composition, as
in the following example.
Thread.void core.thread.osthread.Thread.sleep(core.time.Duration val) nothrow @nogc @trustedSuspends the calling thread for at least the supplied period. This may
result in multiple OS calls if period is greater than the maximum sleep
duration supported by the operating system.
In
period must be non-negative.
Example
Thread.sleep( dur!("msecs")( 50 ) ); // sleep for 50 milliseconds
Thread.sleep( dur!("seconds")( 5 ) ); // sleep for 5 seconds
sleep(300.msecs);
(local variable) sparkles.ui.components.tasklist.TaskReporter taskstasks.void sparkles.ui.components.tasklist.TaskReporter.fail(ulong id, string detail = null)Terminal transitions: the task's line graduates into the scrollback.
A multi-line detail puts the first line on the task row and the rest
as indented follow-up lines (e.g. a failure's output tail).
fail((local variable) const(ulong) lintlint, "3 warnings\nunused import in app.d\nshadowed variable in io.d");
(local variable) sparkles.ui.components.tasklist.TaskReporter taskstasks.void sparkles.ui.components.tasklist.TaskReporter.skip(ulong id, string detail = null)Terminal transitions: the task's line graduates into the scrollback.
A multi-line detail puts the first line on the task row and the rest
as indented follow-up lines (e.g. a failure's output tail).
skip((local variable) const(ulong) publishpublish, "blocked by lint");
}