1. Design personality
The interface should feel precise, fast, scientific, and composed.
It should not feel chatty, gamified, noisy, or overly decorative.
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 |
| Success | ✓ | OK | Completed successfully |
| Warning | ! | ! | Recoverable issue or degraded mode |
| Error | ✗ | X | Failure |
| 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 text | Ordinary information |
| Dim text | Secondary details, paths, debug-adjacent context |
| Green | Success |
| Yellow / amber | Warning, fallback, degraded mode |
| Red | Error |
| Blue / cyan | Active 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
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
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