DESIGN.md 9.6 KB

Terminal Output Design Language

Status: experimental. The first skill on this language is fleet-ops. New output-heavy skills should follow this guide and source skills/_lib/term.sh.

claude-mods ships ~70 skills, many of which write to a TTY (fleet-ops, git-ops, push-gate, sync, ...). When you run five of them in one session and each rolled its own glyphs and dividers, the toolkit feels like five toolkits. This document is the forcing function: one palette, one helper library, one shape.

Principles

  1. Readable first, structured second, decorative last. A pipe-friendly plaintext line beats a beautiful one nobody can grep.
  2. ASCII fallback is mandatory. Every Unicode glyph has an ASCII twin. Honor TERM_ASCII=1, LANG without UTF-8, and TERM=dumb.
  3. Respect the pipe. No color into non-TTY stdout. Honor NO_COLOR. FORCE_COLOR=1 overrides for CI tooling that wants ANSI in logs.
  4. One screen of output preferred. A status command should fit in 24 lines on a default terminal. Long output earns its length.
  5. 80 columns is the ceiling. Some users still split panes. Tables that exceed it must wrap or truncate, not scroll horizontally.
  6. Color is signal, not skin. Never use color as the only differentiator. Glyphs and labels carry the meaning; color amplifies.

Glyph Palette

State icons. Use through term_state_icon when possible; the literals are listed for cross-reference.

Meaning Unicode ASCII Color Use for
pending โณ [.] yellow running, queued, in-flight
ready โœ… [+] green passed, ready to land
done ๐Ÿš€ [*] green merged, shipped, terminal good
failed โŒ [x] red tests failed, refused, blocked
warning โš ๏ธ [!] yellow conflict, hygiene flag
hint ๐Ÿ’ก [i] cyan suggestion, next-step pointer

Don't introduce new state glyphs without adding them here and to term_state_icon. Improvising glyphs is what got us here.

Box Drawing

Use sparingly โ€” borders that wrap nothing waste lines.

Role Unicode ASCII
horizontal โ”€ -
vertical โ”‚ \|
corners โ”Œ โ” โ”” โ”˜ +
connectors โ”œ โ”ค โ”ฌ โ”ด โ”ผ +
tree branch โ”œโ”€ โ””โ”€ โ”‚ +- \- |`

term_header and term_divider already pick the right glyph based on TERM_ASCII_MODE. Reach for them before drawing your own boxes.

Layouts

The default layout is rule + grouped tree: a horizontal-line "app header" on top, then items grouped by state with tree connectors. Flat tables are reserved for one-row-per-thing data where grouping would just add noise.

Header rule (the "app header")

Always present. Title in cyan, trailing meta in dim. The rule extends to terminal width so the header reads as the section's banner.

โ”€โ”€ fleet โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€  4 lanes ยท 3 active

Grouped tree (default body)

State icon + group label + count, then children under tree connectors. Empty groups are omitted โ€” never render a group with (0).

โ”€โ”€ fleet โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€  4 lanes ยท 3 active

  โณ RUNNING   (2)
    โ”œโ”€ feat/auth-rewrite             12m
    โ””โ”€ spike/wasm-eval               34m

  โœ… READY     (1)
    โ””โ”€ fix/cache-bust                2m

  ๐Ÿš€ LANDED    (1)
    โ””โ”€ chore/bump-deps               1h

Why grouped instead of flat: when ten lanes are in flight, scanning a flat table for "what's actually ready to land?" forces your eyes to do the filtering. Grouping does it for you, and the count tells you at a glance whether the answer is none, one, or twelve.

Flat status table (escape hatch)

When the data is genuinely flat โ€” git status-style fields, a single PR's checks โ€” drop the tree. Glyph-first, no nested tables.

  โœ…  secret scan        clean
  โœ…  forbidden files    none
  โŒ  divergence         3 ahead, 1 behind

Plain tree (filesystem-style, no state grouping)

For hierarchies that aren't keyed on state โ€” a directory tree, an include graph. Use sparingly; the grouped-tree above is preferred for state.

repo/
โ”œโ”€ main                           clean
โ”œโ”€ feat/auth-rewrite              ahead 3, dirty
โ””โ”€ fix/cache-bust                 behind 1

Section divider

Plain rule between blocks. No title.

โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

Empty state

Dim, parenthesised, single line โ€” never a multi-line "nothing here" banner.

  (no lanes โ€” run: fleet init <name>...)

Colors

Color is signal layered on top of glyph and label. Strip color and the output must still be readable.

Color Meaning
green success, terminal-good (READY, LANDED)
yellow pending or warning (RUNNING, CONFLICT)
red failure (FAILED, refused)
cyan section headers, hints
dim metadata: timestamps, counts, hint text

Disabled when stdout isn't a TTY, or NO_COLOR is set. Forced on with FORCE_COLOR=1.

Examples (rendered)

Before โ€” fleet-ops rolling its own (flat table, double rules)

โ”€โ”€ Fleet โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        BRANCH                           STATUS     AGE
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  โณ   feat/auth-rewrite                 RUNNING    12m
  โœ…   fix/cache-bust                    READY      2m
  ๐Ÿš€   chore/bump-deps                   LANDED     1h
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

After โ€” rule on top, grouped tree as default

โ”€โ”€ fleet โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€  3 lanes ยท 2 active

  โณ RUNNING   (1)
    โ””โ”€ feat/auth-rewrite             12m

  โœ… READY     (1)
    โ””โ”€ fix/cache-bust                2m

  ๐Ÿš€ LANDED    (1)
    โ””โ”€ chore/bump-deps               1h

The header rule survives โ€” it's the strongest visual cue that you're inside a skill's output. The flat table gives way to grouped state, so "what's ready" and "what's still running" answer themselves before you read a single branch name.

git-ops/status reformatted in the same language

โ”€โ”€ Repo โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€  X:/Forge/claude-mods
  branch    claude/sleepy-johnson-74f19d
  HEAD      367b062 fix(skills/fleet-ops): consistent .claude/ path (2h ago)
  sync      0 ahead / 0 behind
  tree      0 staged / 2 unstaged / 1 untracked

  โš ๏ธ   HYGIENE  main checkout on 'claude/...' โ€” feature work belongs in worktrees

push-gate refusal

โ”€โ”€ push-gate โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€  refusing
  โŒ   secret scan        2 hits in src/config/keys.ts
  โœ…   forbidden files    none
  โœ…   divergence         clean

  ๐Ÿ’ก   run: gitleaks detect --source . --no-git

Anti-patterns

  • Decorative emoji. โœจ๐Ÿ“ฆ๐ŸŽ‰ carry no state. Keep the glyph budget for the six in the palette.
  • Nested tables or boxes. A table inside a bordered box is two layouts fighting for the same line. Pick one.
  • Color as the only difference. "Red row vs green row" fails for CI logs, screen readers, and color-blind users. Always pair with a glyph.
  • Lines past 80 columns by default. If you genuinely need 120, gate it behind --wide or auto-detect via tput cols.
  • Assuming color in CI. GitHub Actions sets TERM=dumb. Check.
  • Multi-line empty states. (no lanes) beats a 4-line ASCII shrug.
  • New glyphs. If your state doesn't fit pending/ready/done/failed/warn/hint, the state probably collapses into one of them. If it really doesn't, amend this document first.

The library

skills/_lib/term.sh is the single source of truth for glyphs, colors, and layout helpers. Source it, call term_init, then use:

LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../_lib" && pwd)"
. "$LIB/term.sh"
term_init

term_header "Fleet" "$count lanes"
term_table_row "$(term_state_icon READY)" "$branch" "READY" "$age"
term_empty "no lanes โ€” run: fleet init <name>..."

The helpers no-op gracefully under NO_COLOR, non-TTY, and TERM_ASCII=1. That's the whole contract โ€” if you're reaching for raw \033[ codes in a skill, you're off the path.