A small D library for CLI applications
Base utilities, terminal styling, pretty-printing, UI components, and @nogc support
Early stage (v0.0.1) -- API may change
sparkles is a D monorepo of utilities for building command-line applications
and supporting libraries. sparkles:base provides allocation-conscious
foundation modules with a focus on @safe, @nogc, pure, and nothrow
compatibility; sparkles:core-cli builds on it with higher-level CLI tools.
- Base --
SmallBuffer, lifetime helpers, text readers/writers, terminal styling, styled templates, terminal control sequences, and logging - Styled Templates -- Apply ANSI styles using D's Interpolated Expression Sequences (IES) with a concise
{style text}syntax - Pretty Printing -- Colorized, type-aware formatting for any D type via compile-time introspection
- UI Components -- Tables (spans, alignment, titles, streaming), boxes, headers, trees, meters/progress bars, key-value lists, horizontal layout, and OSC 8 hyperlinks
- Live Rendering -- Repaint-in-place live regions and task-list checklists with bounded output tails (nix/bazel-style), degrading to a plain transition log when piped
- Interactive Prompts --
select/confirm/textInputwith a uniform non-interactive policy for--autoflags and piped stdin - Terminal Capabilities -- One-shot tty/color/unicode/size detection (
detectTermCaps) and a theme layer with ASCII fallbacks - Semantic Versioning -- SemVer parsing, normalization, and precedence comparison
- Test Runner -- Parallel
unittestrunner with compile-time (@ctfe),-betterC(@betterC), WebAssembly (@wasm), and benchmark (@benchmark) modes
Add the package you need to your dub.sdl:
dependency "sparkles:base" version="~>0.0.1"
dependency "sparkles:core-cli" version="~>0.0.1"
Or dub.json:
"dependencies": {
"sparkles:base": "~>0.0.1",
"sparkles:core-cli": "~>0.0.1"
}sparkles:base contains the shared low-level modules used by the rest of the
monorepo: SmallBuffer, recycledInstance, recycledErrorInstance, @nogc
text parsing/formatting, terminal styling, styled IES rendering, and the
CoreLogger logging interface. See the base documentation
for the tutorial, how-to guides, and API index.
sparkles:versions is an ecosystem-aware version library: it parses,
compares, and constrains the version strings of many package ecosystems
(SemVer, PEP 440/PyPI, Maven, Debian, CalVer, …) and interoperates with
pURL and VERS. Each ecosystem is a hand-written struct conforming to a
small compile-time concept; cross-scheme comparison does not compile.
#!/usr/bin/env dub
/+ dub.sdl:
name "readme_versions"
dependency "sparkles:versions" version="*"
+/
import std.stdio : writeln;
import sparkles.versions.schemes.semver : SemVer;
import sparkles.versions.operations : satisfies;
void main()
{
auto current = SemVer.parse("1.2.3").value;
auto next = SemVer.parse("1.3.0-beta.1").value;
writeln(current);
writeln(next > current);
// Loose parsing accepts a leading `v` and partial versions.
writeln(SemVer.parseLoose("v1.2").value);
// Range membership.
auto range = SemVer.parseNativeRange("^1.2.0").value;
writeln(current.satisfies(range));
}1.2.3
true
1.2.0
true
For the full tour — comparing and sorting, ranges, VERS/pURL interop, the eleven shipped schemes, and adding your own — see the versions documentation.
Apply terminal styles using D's Interpolated Expression Sequences.
#!/usr/bin/env dub
/+ dub.sdl:
name "readme_styled_templates"
dependency "sparkles:base" version="*"
+/
import sparkles.base.styled_template;
void main()
{
int cpu = 75;
styledWriteln(i"CPU: {red $(cpu)%} Status: {green OK}");
styledWriteln(i"{bold.red ERROR:} Connection refused");
styledWriteln(i"{cyan Outer {bold.underline inner} just cyan}");
styledWriteln(i"Press {bold.cyan q} to quit, {bold.cyan h} for help");
}CPU: 75% Status: OK
ERROR: Connection refused
Outer inner just cyan
Press q to quit, h for help
Syntax at a glance:
| Syntax | Description |
|---|---|
{red text} |
Single style |
{bold.red text} |
Chained styles |
{bold outer {red nested}} |
Nested blocks with inheritance |
{red text {~red normal}} |
Style negation with ~ |
#{ / #} |
Escaped literal braces |
Format D values with syntax highlighting and structural indentation. Supports enums, booleans, strings, numerics, pointers, tuples, associative arrays, arrays, ranges, structs, and classes.
#!/usr/bin/env dub
/+ dub.sdl:
name "readme_pretty_printing"
dependency "sparkles:base" version="*"
+/
import std.stdio : writeln;
import sparkles.base.prettyprint;
struct Server
{
string name;
string ip;
int port;
}
struct Cluster
{
string name;
Server[] servers;
bool active;
}
void main()
{
auto cluster = Cluster(
name: "Production",
servers: [
Server("web-01", "192.168.1.10", 80),
Server("web-02", "192.168.1.11", 80),
Server("db-01", "192.168.1.20", 5432),
],
active: true,
);
writeln(prettyPrint(cluster, PrettyPrintOptions!void(colored: false)));
}Cluster(
name: "Production",
servers: [
Server(name: "web-01", ip: "192.168.1.10", port: 80),
Server(name: "web-02", ip: "192.168.1.11", port: 80),
Server(name: "db-01", ip: "192.168.1.20", port: 5432)
],
active: true
)
Options via PrettyPrintOptions:
prettyPrint(value, PrettyPrintOptions!void(
indentStep: 2, // spaces per indent level
maxDepth: 8, // recursion limit
maxItems: 32, // max array/AA items shown
softMaxWidth: 80, // single-line threshold
colored: true, // ANSI color output
useOscLinks: false, // OSC 8 hyperlinks on type names
));The SourceUriHook template parameter controls the URI scheme for OSC 8 hyperlinks. Use SchemeHook!"code" for VS Code, EditorDetectHook for auto-detection from $EDITOR/$VISUAL, or implement a custom hook via Design by Introspection.
ANSI colors and text attributes via stylize and a fluent stylizedTextBuilder. Both work at runtime and at compile time (CTFE).
#!/usr/bin/env dub
/+ dub.sdl:
name "readme_terminal_styling"
dependency "sparkles:base" version="*"
+/
import std.stdio : writeln;
import sparkles.base.term_style;
void main()
{
// Runtime styling
writeln("Error: ".stylize(Style.red) ~ "something went wrong");
// Compile-time styling via fluent builder
enum title = "Important".stylizedTextBuilder(true).bold.underline.red;
writeln(title);
}Error: something went wrong
Important
#!/usr/bin/env dub
/+ dub.sdl:
name "readme_tables"
dependency "sparkles:core-cli" version="*"
+/
import std.stdio : writeln;
import sparkles.ui.components.table;
void main()
{
drawTable([
["Name", "Status", "Load"],
["web-01", "UP", "23%"],
["web-02", "UP", "45%"],
["db-01", "DOWN", "0%" ],
]).writeln;
}╭────────┬────────┬──────╮
│ Name │ Status │ Load │
│ web-01 │ UP │ 23% │
│ web-02 │ UP │ 45% │
│ db-01 │ DOWN │ 0% │
╰────────┴────────┴──────╯
Cells can span columns and rows, columns can be aligned (including
Align.decimal, which lines a numeric column up on its dot) with per-cell
overrides, frames can carry a title/footer like drawBox's, and separators and
glyphs are configurable (TableProps / the stylePresets registry) — including
headerRows / headerCols for a distinct rule setting off the header rows and
the stub (row-header) column. Both the dense Cell[][] form and a sparse
Placement[] form are accepted, and drawTableLines / drawTableChunks /
the writer overload emit the same bytes lazily for live regions and paced
output:
#!/usr/bin/env dub
/+ dub.sdl:
name "readme_table_spans"
dependency "sparkles:core-cli" version="*"
+/
import std.stdio : write;
import sparkles.ui.components.table;
import sparkles.base.text.width : Align;
void main()
{
drawTable([
[Cell("Quarterly Sales", colSpan: 3)],
[Cell("Region"), Cell("Q1"), Cell("Q2")],
[Cell("North"), Cell("1200"), Cell("1350")],
[Cell("South"), Cell("98"), Cell("110")],
], TableProps(
columnAligns: [Align.left, Align.right, Align.right],
headerRows: 2, // banner + column-label row
headerCols: 1, // the Region stub column
)).write;
}╭──────────────────────╮
│ Quarterly Sales │
│ Region ┃ Q1 │ Q2 │
┝━━━━━━━━╋━━━━━━┿━━━━━━┥
│ North ┃ 1200 │ 1350 │
│ South ┃ 98 │ 110 │
╰────────┸──────┴──────╯
#!/usr/bin/env dub
/+ dub.sdl:
name "readme_boxes"
dependency "sparkles:core-cli" version="*"
+/
import std.stdio : writeln;
import sparkles.ui.components.box;
void main()
{
drawBox(
["Build started at 14:32:01",
"Compiling 42 modules...",
"Linking executable...",
"Build completed in 3.2s"],
"Build Log",
BoxProps(footer: "Success"),
).writeln;
}╭──╼ Build Log ╾────────────╮
│ Build started at 14:32:01 │
│ Compiling 42 modules... │
│ Linking executable... │
│ Build completed in 3.2s │
╰──╼ Success ╾──────────────╯
#!/usr/bin/env dub
/+ dub.sdl:
name "readme_headers"
dependency "sparkles:core-cli" version="*"
+/
import std.stdio : writeln;
import sparkles.ui.components.header;
void main()
{
// Divider style: ── Section Title ──
"Section Title".drawHeader.writeln;
// Banner style
"Main Title".drawHeader(HeaderProps(
style: HeaderStyle.banner,
lineChar: '═',
width: 40,
)).writeln;
}── Section Title ──
════════════════════════════════════════
Main Title
════════════════════════════════════════
Make text clickable in terminal emulators that support OSC 8.
#!/usr/bin/env dub
/+ dub.sdl:
name "readme_osc_link"
dependency "sparkles:core-cli" version="*"
+/
import std.stdio : writeln;
import sparkles.ui.components.osc_link;
import sparkles.base.term_style : Style;
void main()
{
// Plain clickable link
writeln(oscLink(text: "Example", uri: "https://example.com"));
// Styled clickable link (blue text)
writeln(oscLink(text: "D Language", uri: "https://dlang.org", style: Style.blue));
}Example
D Language
Proportional bars with eighth-cell precision, plus composed done/total
progress lines:
#!/usr/bin/env dub
/+ dub.sdl:
name "readme_meters"
dependency "sparkles:core-cli" version="*"
+/
import std.stdio : writeln;
import sparkles.ui.components.meter : meter, ProgressBar;
void main()
{
writeln("|", meter(0.33, 16), "|");
writeln("|", meter(7, 9, 16), "|");
writeln(ProgressBar(done: 5, total: 40, barWidth: 16));
}|█████▎ |
|████████████▌ |
██ 5/40
Trees render from flat, pre-ordered (label, depth) nodes — no recursive node
objects, so any depth-first walk displays directly (and the guides compose as a
table's first column):
#!/usr/bin/env dub
/+ dub.sdl:
name "readme_tree"
dependency "sparkles:core-cli" version="*"
+/
import std.stdio : writeln;
import sparkles.ui.components.tree : renderTree, TreeNode;
void main()
{
foreach (line; renderTree([
TreeNode("apps", 0),
TreeNode("ci", 1),
TreeNode("release", 1),
TreeNode("src", 2),
TreeNode("libs", 0),
]))
writeln(line);
}apps
├─ ci
└─ release
└─ src
libs
LiveRegion repaints a block of lines in place at the bottom of normal
scrollback (frames wrapped in DEC 2026 synchronized-output markers, completed
lines graduating into the scrollback above); TaskReporter drives a checklist
through it, with each running task's child-process output streaming into a
bounded tail pane (runStreaming). On piped output only the transition log
remains — no escape codes. The row renderers are pure and theme-driven:
#!/usr/bin/env dub
/+ dub.sdl:
name "readme_tasklist"
dependency "sparkles:core-cli" version="*"
+/
import std.stdio : writeln;
import sparkles.ui.components.tasklist : renderTaskList, TaskItem, TaskStatus;
import sparkles.ui.components.theme : Theme;
void main()
{
// The pure renderer (a real app drives TaskReporter over a LiveRegion —
// see libs/core-cli/examples/live-tasklist.d for the animated version).
auto items = [
TaskItem(label: "fetch dependencies", status: TaskStatus.ok),
TaskItem(label: "build", status: TaskStatus.running,
tail: ["compiling module 11", "compiling module 12"]),
TaskItem(label: "publish", status: TaskStatus.pending),
];
foreach (line; renderTaskList(items, Theme(colors: false)))
writeln(line);
}⠋ build
compiling module 11
compiling module 12
○ publish
kvList renders aligned label/value lines; hjoin zips pre-rendered blocks
side by side (padded by visible width, so styled/CJK content lines up):
#!/usr/bin/env dub
/+ dub.sdl:
name "readme_layout"
dependency "sparkles:core-cli" version="*"
+/
import std.stdio : writeln;
import sparkles.ui.components.box : BoxProps, drawBox;
import sparkles.ui.components.layout : hjoin, kvList;
void main()
{
auto receipt = kvList([
["tag", "v0.6.0 (annotated)"],
["pushed", "origin ✔"],
]);
writeln(hjoin([
drawBox(receipt, "released", BoxProps(footer: "next: publish")),
"notes:\n2 feats\n1 fix",
]));
}╭──╼ released ╾──────────────╮ notes:
│ tag v0.6.0 (annotated) │ 2 feats
│ pushed origin ✔ │ 1 fix
╰──╼ next: publish ╾─────────╯
Line-based select / confirm / textInput, each with a PromptPolicy so
--auto runs and piped stdin resolve to defaults (or fail) uniformly. EOF is
an error, never an accidental default:
#!/usr/bin/env dub
/+ dub.sdl:
name "readme_prompts"
dependency "sparkles:core-cli" version="*"
+/
import std.stdio : writefln;
import sparkles.core_cli.prompts;
import sparkles.base.term_caps : isTerminal, StdStream;
void main()
{
// Interactive on a terminal; silently takes the defaults when piped
// (which is how this example runs under CI).
const policy = isTerminal(StdStream.stdin)
? PromptPolicy.interactive : PromptPolicy.takeDefault;
auto io = stdioPromptIo();
auto bump = select("Version bump:", [
SelectOption("patch", "v0.5.0 → v0.5.1"),
SelectOption("minor", "v0.5.0 → v0.6.0 (suggested)"),
SelectOption("major", "v0.5.0 → v1.0.0"),
], 1, policy, io);
auto go = confirm("Push to origin?", defaultYes: true, policy, io);
writefln!"bump=%s push=%s"(bump.value + 1, go.value);
}bump=2 push=true
Delta-time-prefixed logging via DeltaTimeLogger, a std.logger.Logger subclass. Each log line shows wall-clock time, elapsed time since start, and delta since the previous entry.
#!/usr/bin/env dub
/+ dub.sdl:
name "readme_logger"
dependency "sparkles:base" version="*"
+/
import std.logger : log, LogLevel;
import sparkles.base.logger : initLogger;
void main()
{
initLogger(LogLevel.trace);
log(LogLevel.info, "Listening on port 8080");
log(LogLevel.warning, "Disk usage above 80%");
log(LogLevel.error, "Connection to database lost");
}[ 12:42:19 | Δt 128.5µs | Δtᵢ 128.5µs | INF | readme_logger.d:15 ]: Listening on port 8080
[ 12:42:19 | Δt 240.7µs | Δtᵢ 112.1µs | WRN | readme_logger.d:16 ]: Disk usage above 80%
[ 12:42:19 | Δt 280.7µs | Δtᵢ 40.0µs | ERR | readme_logger.d:17 ]: Connection to database lost
The colored output uses writeStyled IES for ANSI styling -- log levels are color-coded (green for info, yellow for warnings, red for errors, bold+red for critical/fatal), durations are highlighted, and file locations are dimmed.
A @nogc dynamic array with small buffer optimization. Stores data inline up to a configurable threshold, then falls back to the heap via pureMalloc.
import sparkles.base.smallbuffer;
@safe pure nothrow @nogc
unittest {
SmallBuffer!(char, 64) buf;
buf ~= "Hello";
buf ~= ' ';
buf ~= "World";
assert(buf[] == "Hello World");
assert(!buf.onHeap); // still using inline storage
}Works as an output range, so it composes with std.algorithm, prettyPrint, styled templates, and the rest of the library.
recycledInstance -- Reuse thread-local static instances for throwing errors in @nogc code:
import sparkles.base.lifetime;
@nogc void validate(int x) {
if (x < 0)
throw recycledInstance!Error("value must be non-negative");
}text_writers -- Write integers, floats, escaped characters, and ANSI codes without GC allocation. Includes writeValue for best-effort @nogc conversion of any type, and writeStyledValue for hook-controlled styled output.
term_unstyle -- Strip ANSI escape sequences from styled text. This lives in
sparkles:core-cli.
term_caps -- Query the terminal size (terminalSize) and detect window resizes via SIGWINCH. This lives in
sparkles:core-cli.
sparkles:test-runner runs a package's unittests in parallel (add it to
configuration "unittest" and use dub test as usual), with marker
attributes that opt individual tests into extra environments: @ctfe tests
run while the build compiles (a failure is a compile error), @betterC and
@wasm tests are additionally extracted and executed without druntime /
on wasm32 (--better-c / --wasm), and @benchmark tests are measured
with auto-scaling iteration counts (--bench). See the
test-runner documentation.
#!/usr/bin/env dub
/+ dub.sdl:
name "readme_test_runner"
dependency "sparkles:test-runner" version="*"
+/
import sparkles.test_runner.attributes : benchmark, betterC, ctfe;
import sparkles.test_runner.bench : blackBox, computeStats;
@("digits.parity")
@betterC @safe pure nothrow @nogc
unittest // runs under `dub test` — and without druntime via `--better-c`
{
int parity;
foreach (c; "12345")
parity ^= c - '0';
assert(parity == 1);
}
@("digits.parity.ct")
@ctfe @safe pure nothrow @nogc
unittest // runs while the test build compiles; never at runtime
{
assert((1 ^ 2 ^ 3 ^ 4 ^ 5) == 1);
}
void main()
{
import std.stdio : writefln;
// The statistics `--bench` reports, over hand-made ns/iter samples so
// this example's output is deterministic:
const stats = computeStats("demo", 1000, [22.0, 18.0, 20.0]);
writefln!"median=%.0fns/iter min=%.0f max=%.0f over %s samples"(
stats.nsPerIterMedian, stats.nsPerIterMin, stats.nsPerIterMax,
stats.samples);
// blackBox is the optimizer barrier used inside @benchmark tests.
assert(blackBox(21) * 2 == 42);
}median=20ns/iter min=18 max=22 over 3 samples
Runnable examples are in libs/base/examples/,
libs/build-primitives/examples/, and
libs/core-cli/examples/:
dub run --single libs/base/examples/logger.d
dub run --single libs/base/examples/prettyprint.d
dub run --single libs/base/examples/text-fields.d
dub run --single libs/base/examples/term-control.d
dub run --single libs/build-primitives/examples/gitignore_listing.d
dub run --single libs/core-cli/examples/styled-template.d
dub run --single libs/core-cli/examples/table.d
dub run --single libs/core-cli/examples/streaming-table.d # animated; --mode cell|line|race
dub run --single libs/core-cli/examples/table-leaderboard.d # animated, re-sorting live table
dub run --single libs/core-cli/examples/table-bench-ticker.d # animated benchmark results
dub run --single libs/core-cli/examples/box.d
dub run --single libs/core-cli/examples/streaming-box.d # animated
dub run --single libs/core-cli/examples/header.d
dub run --single libs/core-cli/examples/osc-link.d
dub run --single libs/core-cli/examples/color.d
dub run --single libs/core-cli/examples/theme.d
dub run --single libs/core-cli/examples/meter.d
dub run --single libs/core-cli/examples/tree.d -- [path] # gitignore-aware tree(1) clone
dub run --single libs/core-cli/examples/layout.d
dub run --single libs/core-cli/examples/prompts.d # interactive
dub run --single libs/core-cli/examples/live-tasklist.d # animated
dub run --single libs/base/examples/term-caps.d# Build
dub build :base
dub build :core-cli
# Run all tests
dub run :ci -- --test
# Test a specific sub-package
dub test :base
dub test :core-cli
# Run tests matching a pattern
dub test :base -- -i "SmallBuffer"
# Verbose output with stack traces
dub test :core-cli -- -v
# Special test-runner modes (see docs/libs/test-runner/)
dub test :base -- --bench # measure @benchmark tests
dub test :base -- --better-c # run @betterC tests without druntime
dub test :base -- --wasm # run @wasm tests on wasm32The project uses a Nix development shell for reproducible builds:
nix develop -c dub build :core-cli
nix run .#ci -- --testDocumentation (work in progress) is available at sparkles-docs.pages.dev.