Reference

The CLI

run, check, fmt, test, repl, and why they are shaped that way.

Edit this page on GitHub

The twill command line

This records the palette, the rules, and the degradation ladder for src/term/ and src/cli/. It exists so that the next person to add a command does not have to reverse-engineer the taste from the code.

The whole thing is written in twill, in mode systems. It does not run yet. Gaps in the language are recorded in docs/needs.md.

1. The palette

From the mark: a glowing twisted mint ribbon.

Name Hex Where it is used
pale #D2F0E4 the ribbon's lit edge; emphasis, quoted source, the shimmer's head
mint #A8DCCB its body; success, the spinner's label at rest
accent #7FE3C4 the glow at the twist; headings, the REPL prompt, the spinner glyph
teal #4FB79B its shaded face; rules, brackets, the progress track, agreeing axes
deep #12332C reserved for backgrounds, currently unused
ink #0B1512 reserved for backgrounds, currently unused

Spool, the sibling package manager, is warm. These appear in twill only where twill is talking about packages, and are borrowed for warnings, so that a package-shaped concern looks like one across both tools.

Name Hex
spool warm #E3A76F
spool pale #F2DCC6
spool deep #33231A

One colour is not in the brand: #D66A60, a desaturated red, used only for real errors. An error has to leave the palette or it stops reading as an error, and a mint error is a design that has stopped meaning anything. It is desaturated so that it sits beside mint without shouting.

All of it lives in src/cli/theme.tw. No other file names a colour.

2. The rules

Colour carries meaning, never decoration. Mint is success and structure. Warm is warning. Red is a real error, at most once per message. If a colour cannot be given a sentence explaining what it means, it does not go in.

Colour marks structure, not content. In the help screen the group titles and command names are coloured and the descriptions are not. Colouring the descriptions makes a page where every line is loud and the shape of the page is invisible.

Four levels of emphasis, no more. heading, body, muted, emphasis. Body is deliberately unstyled so it inherits the user's own foreground and therefore works on a light theme, which no hardcoded colour can do.

Animation is for things that take time. The spinner does not appear until 90ms have passed, so a fast path through a command produces no animation and no cursor games. An indicator that appears and vanishes inside one frame is worse than none: the eye catches the flash and has nothing to attach it to.

Nothing blinks. No SGR 5 anywhere, and no effect that takes a character from visible to invisible. The spinner turns; the shimmer moves hue along a label without any character going dark. Liveness comes from motion, not from flashing.

Slow is calmer than fast. The spinner is 120ms a frame, a half turn a second. Repaints are capped at 30 a second. Faster is invisible to the eye and costs bytes on every link between here and it, which is what makes a remote session feel sluggish.

Meaning never lives only in colour. Every status has a glyph as well as a colour. The failing axis in a shape error has a caret under it as well as being red. Turn colour off and nothing is lost but the speed of finding it.

Never repaint identical bytes. frame.tw compares against the last body and sends nothing when they match, because repainting the same content is pure flicker on terminals that do not double-buffer.

Truncate every line in a frame. A line that wraps breaks the repaint arithmetic permanently: the frame counts logical lines and the terminal counts visual ones. This is not a nicety.

The banner appears twice. Bare twill, and twill --help. Nowhere else.

Exit codes are the API. Anything that renders an error exits non-zero. A rendered diagnostic with exit 0 is a broken build that CI calls green.

3. The degradation ladder

Capability is detected once in src/term/caps.tw and every other file obeys it. The bias is one-directional: when a signal is ambiguous, assume less. A plain run in a capable terminal is a missed flourish. An escape sequence in a log file is a bug that survives for years, because the person who caused it never sees it.

Colour tiers

Tier Condition What changes
truecolor COLORTERM=truecolor/24bit, or a known terminal full palette, gradients, the shimmer
256 TERM contains 256color, or a tty that claimed nothing palette quantised per channel to the 6x6x6 cube plus the 24-step grey ramp; the shimmer is dropped and the label is flat mint, because a quantised gradient reads as a defect
plain NO_COLOR set, TERM=dumb, TERM empty, --no-color, or stdout is not a tty every styling function returns the empty string; the output is exactly the text it would have been

NO_COLOR is honoured on presence, whatever the value. FORCE_COLOR is the only way to raise the tier, and it exists for logs that are replayed through a renderer.

Everything else

Signal On Off
tty cursor control, in-place repainting, prompts no cursor sequences at all; spinner emits nothing and prints one outcome line; progress bar prints one line per ten percent; REPL prints no prompt
unicode braille mark, block-fill progress, ✓ ▲ ✕ ❯ ━ ⋮ … ok ! x > ^ : ..., # and . for the bar
OSC 8 hyperlinks file:line:col in a diagnostic is clickable the same text, unlinked; the label already contains the path so nothing is lost
width layouts fill the terminal, tensors show as many columns as fit 80 columns assumed; below the minimum the progress bar drops rather than squeezing

Widget by widget

  • banner needs braille and colour and a tty and 40 columns. Missing any of them it degrades to one line: twill 0.28.0. There is no middle rendering, because a monochrome braille blob is not a logo.
  • spinner degrades: truecolor shimmer, then flat mint with a turning glyph, then an ASCII glyph, then (no tty) silence plus one line when the work ends.
  • progress bar degrades: gradient eighth-blocks, then flat #, then one milestone line per ten percent. Not one line per step, which buries a build log, and not nothing, because a CI job silent for six minutes is indistinguishable from a hung one.
  • diagnostics degrade only in colour. The gutter, the caret span and the layout are drawn from ASCII-safe characters where unicode is missing, and the information content is identical at every tier.

4. The shape mismatch

This is the error the tool is judged on, because shape errors are what twill is for. src/cli/shape.tw.

Both shapes are laid out as a table, one axis per column, aligned from the right. Right, because that is how broadcasting aligns them; a rendering that aligned them left would be drawing a different relation from the one being checked. Column widths are computed across both shapes so the rows line up character for character, which is the entire reason it works.

Then:

  • agreeing axes in quiet teal;
  • an axis of length 1 that will stretch in warm, because that is a thing worth noticing and not an error;
  • a missing leading axis as · in muted, distinct from a length-1 axis, because they are different mistakes and merging them makes the user fix the wrong one;
  • the first disagreeing axis in bold red, with a caret under it and its index spelled out. First, not all, because one transposition usually produces several and reporting five reads as five problems.

The help line names the change to make. It detects the transposition case, where the two disagreeing dimensions each appear on the other side one axis over, and says so, which turns a five-minute hunt into a one-line fix.

5. Layout of the code

src/term/     primitives, no twill-specific knowledge
  caps.tw     capability detection, the one place that decides
  color.tw    Rgb, the 256 approximation, interpolation and ramps
  ansi.tw     escapes, Style, cursor control, OSC 8
  width.tw    escape-aware measuring, truncation, wrapping, tab expansion
  frame.tw    in-place repainting

src/cli/      the experience
  theme.tw    the palette and the role each colour plays
  banner.tw   the ribbon mark, computed from the twist
  spinner.tw  indeterminate work
  progress.tw determinate work
  diagnostic.tw  file:line:col, source, caret, explanation
  shape.tw    the shape mismatch
  help.tw     grouped commands, one aligned description column
  tensor.tw   aligned columns, elided middles
  repl.tw     the prompt and the session
  main.tw     dispatch, and the three policies

Nothing in src/term/ writes to stdout. Every function returns a string. That is what lets the whole UI be rendered into a buffer and diffed in a test, and it is why the plain path costs nothing: at the plain tier the styling functions return "" and the caller's concatenation collapses to the text it would have printed anyway.

6. Things deliberately not done

  • No alternate screen. A tool that clears your scrollback to show a progress bar and hands it back afterwards is a tool whose output you cannot read later.
  • No out[3] = result labels in the REPL. Nobody refers back by index in a terminal, and the label costs a column of every line forever.
  • No box drawing around anything. Borders cost two columns and four lines and add no information. Alignment and whitespace do the same job.
  • No spinner on fast commands, and no banner on working commands. These are the two rules most likely to be broken by the next person in a hurry, which is why main.tw makes both an explicit decision rather than a default.