CLI design specification

Text UI Design Language for Technical Command-Line Tools

A compact visual and behavioral system for spinners, progress bars, status lines, summaries, errors, and non-interactive logs. Designed for high-performance scientific and data-processing CLIs.

1. Design personality

The interface should feel precise, fast, scientific, and composed.

It should not feel chatty, gamified, noisy, or overly decorative.

An observatory control console, not a slot machine.
precise observable calm copyable TTY-aware log-friendly

2. Visual hierarchy

Primary task line

The main operation currently underway.

⠸ Building spatial index   12.4M / 97.2M rows   00:41 elapsed

Progress task line

Use only when the denominator is known.

▰▰▰▰▰▰▱▱▱▱  62%   Crossmatching partitions   18,432 / 29,716

Secondary detail line

Current partition, worker, cache status, or local detail.

  hpix=12481  rows=83,204  cache=warm  worker=07

Completed task line

Compact, aligned, and stable.

✓ Built spatial index        97.2M rows in 02:18
✓ Crossmatched catalogs      14.8M matches, 3.2GB written

3. Status symbols

Use a small, consistent symbol grammar. Do not assign a unique glyph to every concept.

State Unicode ASCII fallback Meaning
Running*Active indeterminate work
Progress complete segment#Completed fraction
Progress remaining segment-Remaining fraction
SuccessOKCompleted successfully
Warning!!Recoverable issue or degraded mode
ErrorXFailure
Transition->Next action, output path, or movement
Detail·-Sub-item or quiet detail

4. Color semantics

Use color semantically, not decoratively. The output must still be understandable in monochrome.

Role Use
Default textOrdinary information
Dim textSecondary details, paths, debug-adjacent context
GreenSuccess
Yellow / amberWarning, fallback, degraded mode
RedError
Blue / cyanActive operation, object names, paths

Good

✓ Wrote results       /data/run42/xmatch.parquet
! Missing metadata    assuming epoch=J2000
✗ Schema mismatch     column "ra" is float32, expected float64

Avoid

🌈✨ DONE!!! ✨🌈

5. Layout and spacing

Use alignment to make repeated output scannable.

✓ Read       objects.parquet        104.2M rows   8.4 GB
✓ Read       sources.parquet         97.8M rows   7.9 GB
✓ Wrote      matches.parquet         14.8M rows   1.2 GB

Recommended column order

[state] [verb] [object] [quantity] [rate/time/detail]

Example

✓ Indexed    gaia-dr3              1.82B rows    11m42s
✓ Cached     margin tiles          19.4M rows    4.1GB
✓ Wrote      xmatch.parquet        87.2M rows    2m08s

6. Spinners

A spinner means: work is happening, but the fraction complete is not known.

Recommended spinner

⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏

ASCII fallback

- \ | /

Good spinner use

⠦ Opening dataset metadata
⠏ Planning query
⠴ Waiting for workers

With details

⠸ Planning query      14 tables, 3 XMATCH joins
⠸ Building index      00:18 elapsed

Rule

Do not show a fake ETA for indeterminate work. If the denominator exists, use a progress bar.

7. Progress bars

Use progress bars only when the denominator is known.

Preferred compact style

▰▰▰▰▰▰▱▱▱▱  62%   Crossmatching partitions   18,432 / 29,716

ASCII fallback

[######----]  62%   Crossmatching partitions   18,432 / 29,716

Wide terminal

Crossmatching partitions  ▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱  62%  18,432/29,716  41k rows/s

Narrow terminal

▰▰▰▰▰▰▱▱▱▱ 62%  Crossmatching

Progress fields

[label] [bar] [percent] [count] [rate] [eta]

8. Multi-level progress

For nested work, show one primary progress indicator and one changing detail region. Avoid one bar per worker.

Crossmatching  ▰▰▰▰▰▰▱▱▱▱  62%  18,432/29,716 partitions

  current: hpix=12481  rows L/R=83,204/71,992  cache=warm  worker=07

Reflowing chip sub-status

When the detail region carries several independent live metrics (a tuning/diagnosis readout), render them as space-separated chips that reflow to the terminal width: a single line when they fit, wrapping to as many lines as needed when the terminal is narrow. Chips are atomic — never split a chip across lines. This is still one logical detail region (it replaces in place on each refresh) and still not a per-worker bar; the line count is a function of width and content, not of the worker pool. Keep chips short and order them by decreasing decision-relevance (the first chips survive on the narrowest terminal); show conditional chips (e.g. a spill readout) only while they apply. Render each chip's value in a fixed-width field (right-aligned to its plausible maximum) so a value gaining or losing a digit redraws in place — neighbouring chips don't slide and the line breaks don't re-wrap. Without this, a fast-changing metric makes the whole block jitter every refresh. A chip that crosses a threshold takes on a severity (§4): caution is yellow with a leading !; danger is red with a leading (ASCII !!) and reverse video. Per §4 the glyph carries the signal so it survives monochrome / NO_COLOR / logs; color and reverse only amplify it. Healthy chips stay dim — colour appears only when a human is wanted, so its mere presence is the at-a-glance signal (don't paint normal chips green).

reading + sharding  ▰▰▰▰▰▰▱▱▱▱  62%  412/5,140 files

  buffer 73%  ·  480k rows/s  ·  6/24 workers spilling
  spilled 1.2 GB · 14 runs · ~88 MB median  ·  RAM 12 GB free  ·  disk 1.4 TB free

The detail region is transient (animated TTY only); its tuning conclusion must also be carried into the retained completion line (§5) so it survives once the bar collapses. The acid import reading+sharding sub-status (_pass_a_chips / Step.detail_chips in acid/io/progress.py) is the reference implementation.

Pipeline form

✓ Read catalogs
✓ Build margin cache
⠸ Crossmatch partitions
  ▰▰▰▰▰▰▱▱▱▱  62%  18,432/29,716
· Write output
· Validate result

Completed form

✓ Read catalogs
✓ Build margin cache
✓ Crossmatch partitions        14.8M matches
✓ Write output                 /data/run42/xmatch.parquet
✓ Validate result              no duplicate left rows

9. Interactive UI versus logs

Interactive TTY mode

Use spinners, progress bars, line updates, color, and compact status.

⠸ Crossmatching partitions   18,432 / 29,716

Non-TTY / log mode

No animation. Emit durable, parseable events.

[2026-06-11 09:41:02] INFO  started crossmatching partitions total=29716
[2026-06-11 09:41:42] INFO  progress crossmatching done=18432 total=29716 rate=128/s
[2026-06-11 09:43:10] INFO  finished crossmatching matches=14802391 elapsed=128.4s
Pretty when interactive. Parseable when logged.

10. Verbosity levels

Quiet

✓ Wrote /data/run42/xmatch.parquet

Normal

✓ Loaded catalogs
Crossmatching  ▰▰▰▰▰▰▱▱▱▱  62%  18,432/29,716
✓ Wrote /data/run42/xmatch.parquet

Verbose

✓ Loaded left catalog        104.2M rows
✓ Loaded right catalog        97.8M rows
✓ Using HEALPix order         12
✓ Match radius                1.0 arcsec
✓ Margin cache                warm, 19.4M rows
Crossmatching  ▰▰▰▰▰▰▱▱▱▱  62%  18,432/29,716  128/s

11. Message style

Use short, direct technical phrases.

Prefer

Building spatial index
Reading Parquet metadata
Crossmatching partitions
Writing result catalog
Validating row counts

Avoid

Please wait while we build your spatial index...
Now doing a crossmatch, this could take a while...
Almost there...

12. Units and formatting

Use compact engineering-style formatting during progress, and exact counts in summaries when needed.

1,204 rows
83,204 rows
14.8M rows
1.82B rows
842 MB
7.3 GB
128/s
41k rows/s
02:18

Scientific parameters

radius=1.0 arcsec
area=9.6 deg²
order=12
hpix=1849231
epoch=J2000

13. Error design

Errors should explain the failed object, reason, and next action.

✗ Crossmatch failed

  reason: schema mismatch
  column: ra
  found:  float32
  expected: float64

  Fix the schema or pass --ra-column explicitly.

14. Recommended ACID-style run

▣ ▣ ▣   ACID
▣ ▢ ▣   Astronomical Catalog Inference Driver
▣ ▣ ▣

✓ Loaded registry             4 catalogs
✓ Planned query               3 joins, 1 XMATCH
✓ Opened left catalog          104.2M rows
✓ Opened right catalog          97.8M rows
✓ Prepared margin cache         19.4M rows

Crossmatching  ▰▰▰▰▰▰▱▱▱▱  62%  18,432/29,716 partitions  128/s  ETA 01:28

  hpix=12481  L=83,204  R=71,992  cache=warm  worker=07

Final output

✓ Crossmatched catalogs       14.8M matches
✓ Wrote result catalog         /data/run42/xmatch.parquet
✓ Validated output             no duplicate left rows

Summary
  elapsed:        02:18
  throughput:     706k rows/s
  output size:    1.2 GB

15. Style guide rules

1
Use spinners only for indeterminate work.
2
Use progress bars only when a denominator exists.
3
Show one primary progress indicator at a time.
4
Prefer stable, aligned summaries over scrolling logs.
5
Use color semantically and sparingly.
6
Every animated UI must degrade cleanly to logs.
7
Never fake progress or ETA.
8
Use exact numbers in summaries, compact numbers during progress.
9
Errors must explain the failed object, reason, and next action.
10
The UI should remain readable when copied into an issue, Slack, or a batch log.

16. Default implementation constants

Symbols:
  running     ⠋
  success     ✓
  warning     !
  error       ✗
  progress    ▰▱

Progress:
  ▰▰▰▰▰▰▱▱▱▱  62%  label  done/total  rate  ETA

Layout:
  [state] [verb/object aligned to ~28 chars] [main metric] [secondary metric]

Tone:
  concise technical verbs, no chatter

Modes:
  --quiet
  default interactive
  --verbose
  --debug
  --no-color
  --plain / auto for non-TTY