<< All versions

Skill v1.0.1

currentAutomated scan100/100
caarlos0/dotfiles/tui-design

+2 new

──Details
PublishedOctober 30, 2026 at 12:17 AM
Content Hashsha256:34f09c5a6a7af8de...
Git SHAc0dfefe95eea
Bump Typepatch
Compare with v1.0.0
──Files
Files (1 file, 11.7 KB)
SKILL.md11.7 KBactive
SKILL.md · 233 lines · 11.7 KB

name: tui-design description: Design terminal user interfaces and interactive CLIs that stay usable, accessible, and scriptable. Use when building or reviewing a TUI, a full-screen terminal app, or an interactive command-line tool. user_invocable: true metadata: version: "1.0.1"


TUI Design

Design the command-line interface first. Add a full-screen TUI only when the task needs persistent context, fast navigation, selection, or live updates. A TUI owns the screen, the input, the scrollback, and the terminal state; do not take that cost without a reason.

Decide if a TUI is correct

  • Use a plain command when the operation is one-shot, batchable, or scripted.
  • Use the user's $PAGER when the task is reading or searching long output.

Page only when stdout is a terminal, and provide --no-pager.

  • Use a TUI when users repeatedly navigate, compare, filter, select, or watch

changing data. Process monitors, resource browsers, and Git interfaces are good examples.

  • Do not build a TUI only to get color or animation. Styled output, a prompt,

or a single selector is usually enough.

  • Never make the TUI the only way to do an important operation. Ship

equivalent non-interactive subcommands and structured output:

``bash app # optional interactive interface app list --json app delete ID --yes ``

  • Share the domain logic below both interfaces. The TUI is an adapter.

Keep the two interfaces separate

  • Write results to stdout and write progress, warnings, and errors to stderr.
  • Test each stream for a terminal independently. stdout can be a pipe while

stderr is still a terminal.

  • When output is not a terminal, change presentation only, never meaning.

Remove color, cursor motion, progress animation, and the alternate screen.

  • Provide --json for structured data and one JSON document per line for

streaming events. Treat those schemas as a public interface.

  • Prompt only when stdin is a terminal. Provide a flag for every prompt so a

script can supply the answer. Never block a pipeline on input.

  • Support -q/--quiet and --verbose, with the same meaning everywhere.

Lay out for a character grid

  • Make 80x24 the minimum target. Essential navigation, current state, primary

actions, and errors must stay usable there.

  • Recompute the layout from the current size on every resize. Handle

SIGWINCH, then query the real dimensions; do not cache the launch size.

  • Use explicit breakpoints. Drop decoration and secondary metadata before you

hide primary content. Below the minimum, print a plain "terminal too small".

  • Clamp sizes before subtracting. Test 1x1, 20x5, and zero-size reports.
  • Give headers, status bars, and key hints fixed heights; give the main view

the rest; set minimum sizes on interactive panes.

  • Wrap prose. Truncate scan-oriented rows with an ellipsis and offer a detail

view. An ellipsis signals hidden content; silent clipping does not.

  • Use the alternate screen for sustained applications, and inline rendering

for command-shaped work that should leave output in scrollback.

  • Spend borders carefully: two bordered panes cost at least four columns at
  1. Prefer whitespace, alignment, and one shared divider.

Handle text as the terminal sees it

  • Measure rendered cells, not bytes, code points, or string length. CJK is

usually two cells, combining marks are zero, and one emoji may be many code points.

  • Cut and edit at grapheme-cluster boundaries, not code points.
  • Do not treat East Asian Width as a complete width algorithm; ambiguous

characters vary by terminal, locale, and font.

  • Keep emoji out of borders, fixed columns, progress tracks, and cursors.

Reported widths disagree across terminals and multiplexers.

  • Treat Nerd Font and Powerline glyphs as optional. Keep an ASCII fallback and

never let an unlabeled icon be the only meaning.

  • Normalize tabs to a fixed width before measuring.

Use color as an enhancement

  • Design a readable monochrome hierarchy first, then add semantic color.
  • Support the ladder: no color, 16 colors, 256 colors, true color. Detect the

capability; do not assume true color, and remember COLORTERM is lost through ssh and sudo.

  • Define roles such as muted, surface, selected, success, warning, and error.

Map roles through themes; do not scatter literal colors in the code.

  • Detect the terminal background before you choose fixed colors, and keep an

unknown state for terminals that do not answer.

  • Never encode meaning in color alone. Add a label, symbol, border, or cursor.
  • Honor NO_COLOR when it is present and not empty. An explicit flag or the

user config may still override it.

  • Target 4.5:1 contrast for ordinary text. You do not control the user's font

size, theme, or rendering.

Bind keys that every terminal can send

  • Support arrows and hjkl. Show arrows in the footer and the vim aliases in

the full help.

  • Make Ctrl+C a global, highest-priority exit that no focused widget can

shadow. It must restore the terminal before it exits.

  • Bind q to quit at the root, ? to full help, / to filter, : to a

command palette, Enter to activate, Tab/Shift+Tab to move focus.

  • Give Esc one consistent ladder: close a menu, cancel an edit or filter,

close the overlay, go back one level. At the root it does nothing.

  • Leave Ctrl+S, Ctrl+Q, Ctrl+D, and Ctrl+Z alone. They belong to the

terminal and the shell.

  • Do not require Ctrl+Shift, Ctrl+Enter, or a Tab and Ctrl+I

distinction. Legacy terminals cannot encode them. Use the Kitty keyboard protocol only for optional accelerators.

  • Keep mouse support off by default unless direct manipulation is central.

Mouse reporting breaks ordinary text selection; document the Shift or Option override, and keep full keyboard equivalence.

Make the interface discoverable

  • Keep a one-line footer with three to six actions for the current focus, for

example up/down move Enter open / filter ? help q quit.

  • Derive the footer from the active keymap so it cannot drift from reality.
  • ? opens a full help overlay grouped by global, navigation, current pane,

actions, and search.

  • List aliases together, and never advertise a key that does nothing. Dim

disabled actions and give the reason.

  • Show the active sort, filter, mode, and match count. Do not overload plain

typing silently.

  • Treat empty states as content. Distinguish "nothing exists", "no match",

"loading", "failed", and "no access", and give the next action: No matches for "prod". Press Esc to clear the filter.

Report state and errors honestly

  • Print something within 100ms. Silence during slow work looks like a hang.
  • Use a spinner only for unknown durations; switch to counts or a progress

measure when totals are known. Always label what is happening.

  • Stop animation on success, failure, cancellation, or non-interactive output,

and keep the frame rate low enough for ssh and slow terminals.

  • Show loading inside the affected pane and keep the surrounding context.
  • Every error answers three questions: what failed, why, and what to do next.

Rewrite low-level errors into domain language. Put the action last.

  • Group repeated failures under one explanation instead of one paragraph each.
  • Keep recoverable errors inside the TUI, preserve the user's input and

selection, and offer the retry key. Restore the terminal before a fatal error prints.

  • Confirm irreversible, broad, or externally visible actions, and name the

target: Delete 12 pods in production? Default the focus to cancel.

  • Do not confirm actions with reliable undo. Offer undo instead, and only

advertise it when recovery is guaranteed.

Structure the application

  • Keep one source of truth for state, and one serialized path for every

visible state transition.

  • Make the update step a deterministic transition: no clock, no filesystem, no

network, no globals inside it.

  • Turn every occurrence into a typed event: keys, resize, ticks, completion,

failure, cancellation.

  • Return descriptions of effects instead of performing them in the update

step. Keep rendering pure and cheap.

  • Keep the event loop free. Anything slower than a few milliseconds is worker

work that sends its result back as an event.

  • Cancel stale work and also reject stale results with a generation counter;

an old response must never overwrite a newer one.

  • Tie worker lifetime to the screen or widget that started it.
  • Keep the domain packages free of the TUI framework, of styling, and of

terminal APIs, so the same code serves --json, tests, and scripts.

  • Reuse existing primitives for lists, tables, viewports, inputs, and help

before writing your own; they already solved Unicode, scrolling, and paste.

  • Never log to the stream the TUI owns. Log to a file and tail -f it from a

second terminal. Debug from a second process.

Own the terminal state

  • Centralize acquisition and restoration. Track raw mode, alternate screen,

hidden cursor, mouse capture, focus reporting, and bracketed paste, and undo them in reverse order.

  • Restore on normal exit, on error, on panic, on SIGINT, and on SIGTERM.

Make cleanup idempotent and best effort.

  • Treat pasted text as data, not as keybindings, and bound its size.
  • Accept that SIGKILL cannot be handled. Document reset or stty sane.
  • Render on state change, not in a free loop. Build the next frame, diff it,

and write only the changed cells. Do not clear the whole screen every frame.

  • Virtualize long lists, key the selection by item identity rather than row

position, and bound streaming queues so producers apply backpressure.

Configure with a clear precedence

  • Order: flags, environment variables, project config, user config, system

config, built-in defaults. Provide a command that shows effective values and their source.

  • Match the mechanism to the lifetime: flags vary per run, environment varies

per machine, project files are shared, user config is personal.

  • Follow XDG: XDG_CONFIG_HOME, XDG_STATE_HOME, XDG_DATA_HOME,

XDG_CACHE_HOME, XDG_RUNTIME_DIR.

  • Ship a complete default keymap and theme. High configurability without a

good baseline is onboarding debt. Make reset easy.

  • Map keys to semantic actions such as item.open, not to functions, and

detect conflicts.

Design for screen readers

  • Assume a screen reader sees a text buffer, not your widgets. Borders and

coordinates carry no meaning to it.

  • Provide a first-class linear mode: --plain, --no-interactive, or an

accessible prompt mode that echoes context and asks one question at a time.

  • Do not assume a framework is accessible because it is popular. Verify with

NVDA, JAWS, and VoiceOver separately; snapshot tests prove nothing here.

  • Keep a real cursor where the user's attention is.
  • There is no portable accessibility environment variable. NO_COLOR is the

only established convention; anything else must be documented and paired with a flag.

Verify it

  • Test state transitions without a terminal: send an event, assert the new

state and the requested effect.

  • Snapshot the rendered buffer at fixed sizes. Cover narrow, wide, empty,

Unicode, selected, error, and loading states.

  • Golden-test the plain contract with redirected streams, TERM=dumb, and

NO_COLOR=1. Assert that no escape sequence, prompt, or timestamp appears.

  • Add pseudo-terminal tests for lifecycle: raw mode, resize, paste, Ctrl+C,

SIGTERM, and panic. After each failure path, assert the terminal was restored.

  • Exercise the real thing through ssh and tmux, and on more than one emulator.
  • Use recorded tapes for documentation, not as the regression gate.

Related skills

  • writing-tests for deterministic tests, including the TUI cases above.
  • my-code-review when reviewing someone else's terminal interface.
  • go-conventions when the implementation is Go.
← v1.0.0All versions