|
| 1 | +--- |
| 2 | +name: tui-design-system |
| 3 | +description: Use when designing or building any terminal user interface — choosing a layout paradigm, keybindings/interaction model, color system, data visualization, or motion. Framework-agnostic universal patterns that work with Ratatui, Ink, Textual, Bubbletea, or any TUI toolkit. For the repo's own pythinker-code TUI, use write-tui instead. |
| 4 | +--- |
| 5 | + |
| 6 | +# TUI Design System |
| 7 | + |
| 8 | +Universal design patterns for building exceptional terminal user interfaces. Framework-agnostic — works with Ratatui, Ink, Textual, Bubbletea, or any TUI toolkit. |
| 9 | + |
| 10 | +> For editing **this repo's** terminal UI (`apps/pythinker-code/src/tui`), use the **write-tui** skill instead. This skill is the cross-project design vocabulary. |
| 11 | +
|
| 12 | +**Core philosophy:** TUIs earn their power through spatial consistency, keyboard fluency, and information density that respects human attention. Design for the expert's speed without abandoning the beginner's discoverability. |
| 13 | + |
| 14 | +## Design process |
| 15 | + |
| 16 | +```dot |
| 17 | +digraph tui_design { |
| 18 | + rankdir=TB; |
| 19 | + "What are you building?" [shape=diamond]; |
| 20 | + "Select layout paradigm" -> "Design interaction model" -> "Define visual system" -> "Validate against anti-patterns" -> "Ship it"; |
| 21 | + "What are you building?" -> "Select layout paradigm"; |
| 22 | + "Ship it" [shape=doublecircle]; |
| 23 | +} |
| 24 | +``` |
| 25 | + |
| 26 | +1. Pick a **layout paradigm** from what you're building. |
| 27 | +2. Design the **interaction model** (navigation, keybindings, help, dialogs). |
| 28 | +3. Define the **visual system** (color tiers, semantic slots, hierarchy). |
| 29 | +4. Add **data visualization** and **motion** where they earn their place. |
| 30 | +5. **Validate** against the anti-patterns checklist, then ship. |
| 31 | + |
| 32 | +--- |
| 33 | + |
| 34 | +## 1. Layout paradigm selector |
| 35 | + |
| 36 | +| App type | Paradigm | Examples | |
| 37 | +|---|---|---| |
| 38 | +| File manager | Miller Columns | yazi, ranger | |
| 39 | +| Git / DevOps tool | Persistent Multi-Panel | lazygit, lazydocker | |
| 40 | +| System monitor | Widget Dashboard | btop, bottom, oxker | |
| 41 | +| Data browser / K8s | Drill-Down Stack | k9s, diskonaut | |
| 42 | +| SQL / HTTP client | IDE Three-Panel | harlequin, posting | |
| 43 | +| Shell augmentation | Overlay / Popup | atuin, fzf | |
| 44 | +| Log / event viewer | Header + Scrollable List | htop, tig | |
| 45 | + |
| 46 | +### Persistent Multi-Panel |
| 47 | +All panels visible simultaneously; focus shifts between them. Users build spatial memory — "branches are always bottom-left." |
| 48 | + |
| 49 | +``` |
| 50 | +┌─ Status ──┬─────────── Detail ──────────┐ |
| 51 | +├─ Files ───┤ │ |
| 52 | +│ > file.rs │ diff content here... │ |
| 53 | +│ main.rs │ │ |
| 54 | +├─ Branches ┤ │ |
| 55 | +│ * main │ │ |
| 56 | +│ feat/x │ │ |
| 57 | +└───────────┴─────────────────────────────┘ |
| 58 | + [q]uit [c]ommit [p]ush [?]help |
| 59 | +``` |
| 60 | +**Use for:** multi-faceted tools needing simultaneous context (git clients, container managers, monitoring). |
| 61 | +**Key rule:** panels keep fixed positions across sessions. Never rearrange without user action. |
| 62 | + |
| 63 | +### Miller Columns |
| 64 | +Three-pane past/present/future navigation: parent (left), current (center), preview (right). |
| 65 | + |
| 66 | +``` |
| 67 | +┌── Parent ──┬── Current ──┬── Preview ────────┐ |
| 68 | +│ .. │ > config/ │ port: 8080 │ |
| 69 | +│ src/ │ lib/ │ host: localhost │ |
| 70 | +│ > config/ │ main.rs │ log_level: debug │ |
| 71 | +│ tests/ │ mod.rs │ db_url: postgres │ |
| 72 | +└────────────┴─────────────┴───────────────────┘ |
| 73 | +``` |
| 74 | +**Use for:** navigating hierarchical data where context above and below matters. |
| 75 | +**Key rule:** selecting in the center shifts everything left; the preview always reflects the highlighted item. |
| 76 | + |
| 77 | +### Drill-Down Stack |
| 78 | +One level at a time; navigation pushes/pops levels like a stack. Breadcrumb shows depth. |
| 79 | + |
| 80 | +``` |
| 81 | + Context > namespace: prod > pods |
| 82 | +┌─────────────────────────────────────────────┐ |
| 83 | +│ NAME READY STATUS AGE │ |
| 84 | +│ > api-7f9c 2/2 Running 3d │ |
| 85 | +│ worker-1a2b 1/1 Running 3d │ |
| 86 | +└─────────────────────────────────────────────┘ |
| 87 | + :pods :deploy :svc [Enter] drill [Esc] up |
| 88 | +``` |
| 89 | +**Use for:** deep hierarchies where showing all levels at once is impractical (Kubernetes, DB schemas). |
| 90 | +**Key rule:** always show the navigation path as a breadcrumb. Provide a `:resource` command mode for direct jumps. |
| 91 | + |
| 92 | +### Widget Dashboard |
| 93 | +Self-contained widget panels with independent data. All info visible at once; no navigation required. |
| 94 | + |
| 95 | +``` |
| 96 | +┌─── CPU ──────────────┬─── Memory ──────────┐ |
| 97 | +│ ▁▂▃▅▇█▇▅▃▂▁▂▃▅▇ │ ████████░░ 78% │ |
| 98 | +│ core0: 45% core1: 67%│ 12.4G / 16.0G │ |
| 99 | +├─── Network ──────────┼─── Disk ─────────────┤ |
| 100 | +│ ▲ 1.2 MB/s ▼ 340KB/s │ /: 67% /home: 45% │ |
| 101 | +├─── Processes ────────┴──────────────────────┤ |
| 102 | +│ PID USER CPU% MEM% CMD │ |
| 103 | +│ 1234 root 23.4 4.5 postgres │ |
| 104 | +└──────────────────────────────────────────────┘ |
| 105 | +``` |
| 106 | +**Use for:** monitoring, real-time status, dashboards. |
| 107 | +**Key rule:** each widget is self-contained with its own title. Use braille/block characters for density. |
| 108 | + |
| 109 | +### IDE Three-Panel |
| 110 | +Sidebar (left), editor/main (center), detail/output (bottom). Tab bar along top. |
| 111 | +**Use for:** editing-focused tools (SQL clients, HTTP tools, config editors). |
| 112 | +**Key rule:** sidebar toggles with a single key. Center supports tabs. Bottom panel can expand to full height. |
| 113 | + |
| 114 | +### Overlay / Popup |
| 115 | +TUI appears on demand over the shell, disappears after use. |
| 116 | +**Use for:** shell augmentations (history search, file picker, command palette). |
| 117 | +**Key rule:** configurable height; return the selection to the caller; never disrupt scrollback. |
| 118 | + |
| 119 | +### Header + Scrollable List |
| 120 | +Fixed header with meters/stats, scrollable data below, function bar at bottom. |
| 121 | +**Use for:** single-purpose viewers of one stream (process lists, logs, commit history). |
| 122 | +**Key rule:** header and footer stay pinned; only the middle scrolls. |
| 123 | + |
| 124 | +--- |
| 125 | + |
| 126 | +## 2. Responsive layout |
| 127 | + |
| 128 | +Terminals resize constantly. Pick a degradation strategy and test it. |
| 129 | + |
| 130 | +| Strategy | Behavior | |
| 131 | +|---|---| |
| 132 | +| Priority collapse | Less important panels hide first below minimum width | |
| 133 | +| Stacking | Panels collapse to title-only bars; the active one expands (zellij pattern) | |
| 134 | +| Breakpoint modes | Switch layout entirely below a threshold (multi-panel → single panel) | |
| 135 | +| Minimum size gate | Show "terminal too small" below a usable minimum | |
| 136 | + |
| 137 | +**Rules:** |
| 138 | +- Define a minimum size (typically **80×24**). Below it, show a resize message. |
| 139 | +- Never crash on resize. Handle SIGWINCH gracefully. |
| 140 | +- Use constraint-based layouts (percentages, min/max, ratios) — not absolute positions. |
| 141 | +- Test at **80×24, 120×40, 200×60**. |
| 142 | + |
| 143 | +--- |
| 144 | + |
| 145 | +## 3. Interaction model |
| 146 | + |
| 147 | +### Navigation style by complexity |
| 148 | + |
| 149 | +| App complexity | Recommended model | |
| 150 | +|---|---| |
| 151 | +| Single-purpose, <20 actions | Direct keybinding (every key = action) | |
| 152 | +| Multi-view, complex | Vim-style modes + contextual footer | |
| 153 | +| IDE-like, many features | Command palette + tabs + vim motions | |
| 154 | +| Data browser | Drill-down + fuzzy search + `:` command mode | |
| 155 | + |
| 156 | +### Keyboard design layers |
| 157 | + |
| 158 | +| Layer | Keys | Audience | Always shown? | |
| 159 | +|---|---|---|---| |
| 160 | +| L0 Universal | arrows, Enter, Esc, q | Everyone | Yes (footer) | |
| 161 | +| L1 Vim motions | `hjkl / ? : gg G` | Intermediate | Yes (footer) | |
| 162 | +| L2 Actions | single mnemonics: `d`elete, `c`ommit, `p`ush | Regular | On `?` help | |
| 163 | +| L3 Power | composed commands, macros, custom bindings | Power | Docs only | |
| 164 | + |
| 165 | +**Lingua franca (don't deviate):** `j/k` down/up · `h/l` left/right or collapse/expand · `/` search · `?` help · `:` command mode · `q` quit (or Esc back one level) · `Enter` select/confirm/drill · `Tab` switch focus · `Space` toggle selection · `g/G` top/bottom. |
| 166 | + |
| 167 | +**Never bind:** `Ctrl+C` (interrupt), `Ctrl+Z` (suspend), `Ctrl+\` (quit). They belong to the terminal. |
| 168 | + |
| 169 | +### Focus management |
| 170 | +- Only one widget receives input at a time. `Tab`/`Shift+Tab` cycle focus. |
| 171 | +- Focus indicator: highlighted border, color change, or cursor presence. Unfocused panels are dimmed or use thinner borders. |
| 172 | +- Modal dialogs are focus traps — the background receives no events. |
| 173 | +- Nested focus: the outer container routes events to the focused child. |
| 174 | + |
| 175 | +### Search & filtering |
| 176 | +Universal pattern: press `/`, type, results filter live. |
| 177 | +- `n/N` next/previous match · `Esc` dismiss. |
| 178 | +- Fuzzy by default; `'` prefix for exact. Highlight matched characters. Preview updates for the highlighted result. |
| 179 | + |
| 180 | +### Help — three tiers |
| 181 | + |
| 182 | +| Tier | Trigger | Content | Audience | |
| 183 | +|---|---|---|---| |
| 184 | +| Always visible | Footer bar | 3–5 essential shortcuts | Everyone | |
| 185 | +| On demand | `?` | Full keybindings for current context | Regular | |
| 186 | +| Documentation | `--help` / man page | Complete reference | Power | |
| 187 | + |
| 188 | +Footer format: `[q]uit [/]search [?]help [Tab]focus [Enter]select`. Make it context-sensitive — show only what's actionable right now. |
| 189 | + |
| 190 | +### Dialogs & confirmation |
| 191 | + |
| 192 | +| Severity | Pattern | |
| 193 | +|---|---| |
| 194 | +| Reversible | Just do it; brief status-bar confirmation | |
| 195 | +| Moderate (delete file) | Inline "Press y to confirm" | |
| 196 | +| Severe (drop database) | Modal requiring the resource name typed in | |
| 197 | +| Irreversible batch | `--dry-run` flag + explicit confirmation | |
| 198 | + |
| 199 | +Modals render over a dimmed background. Toasts auto-dismiss in 3–5s. Status-bar messages are vim-style one-liners that auto-fade. |
| 200 | + |
| 201 | +--- |
| 202 | + |
| 203 | +## 4. Color design system |
| 204 | + |
| 205 | +### Terminal color tiers — design for graceful degradation |
| 206 | + |
| 207 | +| Tier | Sequence | Colors | Strategy | |
| 208 | +|---|---|---|---| |
| 209 | +| 16 ANSI | `\033[31m` | 16 (relative) | Foundation; terminal theme controls appearance | |
| 210 | +| 256 | `\033[38;5;{n}m` | 256 | Extended; fixed colors may clash with themes | |
| 211 | +| True color | `\033[38;2;{r};{g};{b}m` | 16.7M (absolute) | Full control; needs `COLORTERM=truecolor` | |
| 212 | + |
| 213 | +**Detection order:** `COLORTERM=truecolor|24bit` → true color · `TERM` contains `256color` → 256 · `NO_COLOR` set → no color · else 16 ANSI. |
| 214 | + |
| 215 | +**Golden rule:** the TUI must be usable in 16-color mode. True color *enhances* — it never *creates* the hierarchy. |
| 216 | + |
| 217 | +### Semantic color slots — name by function, not appearance |
| 218 | + |
| 219 | +| Slot | Purpose | Typical dark | |
| 220 | +|---|---|---| |
| 221 | +| `fg.default` | Body text | `#c0caf5` | |
| 222 | +| `fg.muted` | Secondary / metadata | `#565f89` | |
| 223 | +| `fg.emphasis` | Headers, focused | `#e0e0e0` | |
| 224 | +| `bg.base` | Primary background | `#1a1b26` | |
| 225 | +| `bg.surface` | Panel/widget bg | `#24283b` | |
| 226 | +| `bg.overlay` | Popup/dialog bg | `#414868` | |
| 227 | +| `bg.selection` | Selected highlight | `#364a82` | |
| 228 | +| `accent.primary` | Interactive / focus | `#7aa2f7` | |
| 229 | +| `accent.secondary` | Supporting | `#bb9af7` | |
| 230 | +| `status.error` | Errors / deletions | `#f7768e` | |
| 231 | +| `status.warning` | Caution | `#e0af68` | |
| 232 | +| `status.success` | Success / additions | `#9ece6a` | |
| 233 | +| `status.info` | Informational | `#7dcfff` | |
| 234 | + |
| 235 | +**Never hardcode hex in widget code. Always reference a semantic slot.** |
| 236 | + |
| 237 | +### Visual hierarchy techniques |
| 238 | + |
| 239 | +| Technique | Effect | Use for | |
| 240 | +|---|---|---| |
| 241 | +| Bold (SGR 1) | More weight | Headers, labels, active items | |
| 242 | +| Dim (SGR 2) | Less weight | Metadata, timestamps | |
| 243 | +| Italic (SGR 3) | Distinction | Comments, types | |
| 244 | +| Underline (SGR 4) | Actionable | Links, URLs | |
| 245 | +| Reverse (SGR 7) | Swap fg/bg | Selection (always works!) | |
| 246 | +| Strikethrough (SGR 9) | Negation | Deleted/deprecated | |
| 247 | + |
| 248 | +**Recipe:** 80% of content in `fg.default`. Headers `bold + fg.emphasis`. Metadata `dim + fg.muted`. Status in semantic colors. Accents for interactive elements only. |
| 249 | + |
| 250 | +### Background layering |
| 251 | +Create depth without borders by stepping lightness: `bg.base` → `bg.surface` → `bg.overlay`, each ~5–8% lighter in dark themes. The contrast gradient reads as depth and reduces the need for box-drawing. |
| 252 | + |
| 253 | +### Theme architecture & accessibility |
| 254 | +- Base16 pattern: 8 monotones (background↔foreground gradient) + 8 accents. Ship a dark theme by default, at least one light variant, and respect `NO_COLOR`. |
| 255 | +- **WCAG AA:** 4.5:1 for body text, 3:1 for large text / UI elements. |
| 256 | +- **Never use color alone** — pair with symbols (✓ ✗ ▲), text, position, or typography. |
| 257 | +- Color-blind-safe pairs: blue+orange, blue+yellow, black+white. Avoid red vs green as the only signal. |
| 258 | +- Test: monochrome mode, a color-blindness simulator, 3+ terminal emulators, light and dark. |
| 259 | + |
| 260 | +--- |
| 261 | + |
| 262 | +## 5. Data visualization |
| 263 | + |
| 264 | +### Character-resolution building blocks |
| 265 | + |
| 266 | +| Element | Characters | Resolution | Use for | |
| 267 | +|---|---|---|---| |
| 268 | +| Full blocks | `█▉▊▋▌▍▎▏` | 8 steps/cell | Progress bars, bar charts | |
| 269 | +| Shade blocks | `░▒▓█` | 4 densities | Heatmaps, density plots | |
| 270 | +| Braille | `⠁⠂…⣿` (U+2800–28FF) | 2×4 dots/cell | High-res line/scatter | |
| 271 | +| Sparkline | `▁▂▃▄▅▆▇█` | 8 heights | Inline mini-charts | |
| 272 | + |
| 273 | +### Common widgets |
| 274 | + |
| 275 | +| Widget | Pattern | Tips | |
| 276 | +|---|---|---| |
| 277 | +| Progress bar | `[████████░░░░] 67%` | Show % + ETA; gradient green→yellow→red by urgency | |
| 278 | +| Sparkline | `▁▂▃▅▇█▇▅▃▂` | Inline time-series in headers/status bars | |
| 279 | +| Gauge | `CPU [██████████░░] 83%` | Label + bar + value; color by threshold | |
| 280 | +| Table | Sortable, zebra stripes | Numbers right, text left; truncate with `…` | |
| 281 | +| Tree | `├── └── │` guides | Indent 2–4/level; expand/collapse with Enter | |
| 282 | +| Diff | green `+`, red `-` | Word-level highlight within changed lines | |
| 283 | +| Log | colored level + ts + msg | TRACE dim · DEBUG cyan · INFO default · WARN yellow · ERROR red · FATAL red+bold | |
| 284 | + |
| 285 | +### Spinners |
| 286 | + |
| 287 | +| Context | Spinner | Interval | |
| 288 | +|---|---|---| |
| 289 | +| Default / modern | braille `⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏` | 80ms | |
| 290 | +| Minimal | `-\|/` | 130ms | |
| 291 | +| Heavy processing | blocks `▖▘▝▗` | 100ms | |
| 292 | + |
| 293 | +Spinners for indeterminate work, progress bars for determinate. Show spinners only after a ~200ms delay so fast operations don't flash. |
| 294 | + |
| 295 | +--- |
| 296 | + |
| 297 | +## 6. Animation & motion |
| 298 | + |
| 299 | +Flicker-free rendering, three layers — all required: |
| 300 | +1. **Double buffering** — render to an off-screen buffer, then swap. Never paint directly to the visible screen. |
| 301 | +2. **Diff-based updates** — compute the changed cells and emit only those escape sequences; don't repaint the whole screen each frame. |
| 302 | +3. **Frame budget** — cap at the display rate (15–60 fps is plenty for a TUI). Coalesce rapid state changes into one frame; throttle on resize. |
| 303 | + |
| 304 | +Motion guidelines: |
| 305 | +- Animate to communicate state change (loading, transition, focus move), not for decoration. |
| 306 | +- Keep transitions short (<150ms feel). Anything longer needs a cancel path. |
| 307 | +- Respect reduced-motion preferences and `NO_COLOR`-style restraint — offer a static fallback. |
| 308 | +- Never animate on every keystroke; input must always feel instant. |
| 309 | + |
| 310 | +--- |
| 311 | + |
| 312 | +## 7. Validate against anti-patterns |
| 313 | + |
| 314 | +Before shipping, confirm none of these are true: |
| 315 | + |
| 316 | +- [ ] Crashes or corrupts on resize / below minimum size (no size gate). |
| 317 | +- [ ] Hierarchy depends on true color — unusable in 16-color or `NO_COLOR`. |
| 318 | +- [ ] Information conveyed by color alone (no symbol/text/position backup). |
| 319 | +- [ ] Body-text contrast below WCAG AA (4.5:1). |
| 320 | +- [ ] Binds `Ctrl+C`, `Ctrl+Z`, or `Ctrl+\`. |
| 321 | +- [ ] No footer hints and no `?` help — undiscoverable. |
| 322 | +- [ ] Panels rearrange themselves between sessions (broken spatial memory). |
| 323 | +- [ ] Destructive action with no confirmation proportional to severity. |
| 324 | +- [ ] Full-screen repaint every frame (flicker, wasted bandwidth over SSH). |
| 325 | +- [ ] Hardcoded hex/ANSI in widget code instead of semantic slots. |
| 326 | +- [ ] Search doesn't filter live, or `Esc` doesn't dismiss it. |
| 327 | +- [ ] Disrupts shell scrollback (for overlay/popup tools). |
0 commit comments