Skip to content

Instantly share code, notes, and snippets.

@subtleGradient
Last active December 26, 2025 10:51
Show Gist options
  • Select an option

  • Save subtleGradient/fd3d1ef74053d9ed3481b84a2c198751 to your computer and use it in GitHub Desktop.

Select an option

Save subtleGradient/fd3d1ef74053d9ed3481b84a2c198751 to your computer and use it in GitHub Desktop.
VT-HIG Cheat Sheet -- Virtual Terminal Human Interface Guidelines
Display the source blob
Display the rendered blob
Raw
import React from "react"
// VT-HIG Cheat Sheet — v0.3
// Goal: a compact, highly organized reference for CLI/TUI UX contracts.
// This is not about rich text editing; focus is navigation + viewing + selection.
export default function VTHIGCheatSheet() {
return (
<div style={s.page}>
{/* Sticky header keeps the “what is this?” context while scanning sections */}
<Header />
<main style={s.main}>
<Section title="00 · The Web of Lies (Capability Stack)">
<StackCard />
</Section>
<Section title="01 · Contracts">
<Grid columns={2}>
<Card title="CLI contract" subtitle="streams + exit codes + composability">
<Bullets
items={[
"stdout = data",
"stderr = diagnostics",
"exit code = success/failure",
"--help always works",
"--dry-run / --verbose build trust",
]}
/>
</Card>
<Card title="TUI contract" subtitle="state → render loop + keyboard-first">
<Bullets
items={[
"single full-screen surface",
"byte-stream substrate (TTY/PTY)",
"status line = toast",
"mode indicator always visible",
"fast path = memorized keys",
]}
/>
</Card>
</Grid>
</Section>
<Section title="02 · Patterns (Binary)">
<Grid columns={2}>
<Binary
leftTitle="Navigation"
leftItems={[
["Drill-in", "Enter / open detail"],
["Back", "Esc / q (consistent)"],
["Focus", "highlight selected row"],
["Panes", "Tab cycles focus"],
]}
rightTitle="Input"
rightItems={[
["Modes", "Normal vs Insert"],
["Cancel", "Esc cancels; Ctrl+C interrupts"],
["Filter", "type-to-filter"],
["Search", "/ then n/N"],
]}
/>
<Binary
leftTitle="Feedback"
leftItems={[
["Status line", "non-modal confirmations"],
["Errors", "persist until replaced"],
["Progress", "spinner/bar on stderr"],
["Explain", "--verbose / logs"],
]}
rightTitle="Safety"
rightItems={[
["Dry run", "preview the mutation"],
["Confirm", "only destructive"],
["Undo", "when feasible"],
["Idempotent", "safe under interrupts"],
]}
/>
</Grid>
</Section>
<Section title="03 · Keys (Expected Defaults)">
<Grid columns={2}>
<Card title="Universal" subtitle="muscle-memory contracts">
<KeyList
items={[
["Esc", "cancel / back / exit overlay"],
["Ctrl+C", "interrupt current op"],
["?", "help"],
["/", "search"],
["n / N", "next / prev match"],
]}
/>
</Card>
<Card title="Lists" subtitle="selection + narrowing">
<KeyList
items={[
["↑↓ / j k", "move selection"],
["Enter", "open / confirm"],
["Type", "filter query"],
["Backspace", "edit query"],
["Tab", "next pane"],
]}
/>
</Card>
</Grid>
</Section>
<Section title="04 · Accessibility">
<Grid columns={2}>
<Binary
leftTitle="Screen reader mode"
leftItems={[
["Prefer", "stable prompts + linear text"],
["Avoid", "rapid full-screen animation"],
["Provide", "--no-tui / --plain / --json"],
["Announce", "mode + focus changes"],
]}
rightTitle="Low-vision mode"
rightItems={[
["Prefer", "high-contrast themes"],
["Avoid", "color-only meaning"],
["Provide", "fewer panes (zoom by simplification)"],
["Support", "monochrome fallback"],
]}
/>
<Binary
leftTitle="Braille / tactile"
leftItems={[
["Prefer", "predictable reading order"],
["Avoid", "cursor teleport noise"],
["Provide", "single-pane detail view"],
["Expose", "copyable summaries"],
]}
rightTitle="Motor / fatigue"
rightItems={[
["Prefer", "few essential keys"],
["Avoid", "tight chords + timing combos"],
["Provide", "command palette / verbs"],
["Support", "keybind remapping"],
]}
/>
</Grid>
<Grid columns={2}>
<Card title="How blind users interact" subtitle="what viewing TUIs must respect">
<Bullets
items={[
"Often via a screen reader that reads the terminal buffer (or a Braille display)",
"They depend on linear text, not spatial layouts",
"Frequent redraws can sound like ‘everything changed’ repeatedly",
"Provide a plain/CLI view for the same data whenever possible",
]}
/>
</Card>
<Card title="Concrete accessibility affordances" subtitle="terminal-friendly (no ARIA)">
<Bullets
items={[
"Add --accessibility (reduced motion + fewer panes)",
"Keep a stable status line summary (mode, focus, selection)",
"Add “describe current selection” as a command",
"Never encode meaning only by color (use symbols + words)",
]}
/>
</Card>
</Grid>
</Section>
<Section title="05 · Non-keyboard Events → Typical Reactions">
<Grid columns={2}>
<Binary
leftTitle="Inside the TTY"
leftItems={[
["SIGWINCH", "reflow layout; redraw"],
["SIGINT", "cancel op; keep state"],
["SIGTERM", "flush/cleanup; exit"],
["SIGHUP", "treat as disconnect"],
]}
rightTitle="Outside the TTY"
rightItems={[
["Focus lost", "pause hotkeys; degrade"],
["VT switch", "stop drawing; resume"],
["SSH lag", "avoid rapid redraw"],
["Theme change", "repaint palette"],
]}
/>
<Binary
leftTitle="I/O & Time"
leftItems={[
["Timer tick", "animate spinner; minimal"],
["Async update", "patch region"],
["Child exit", "show status; refresh"],
["FS change", "invalidate cache"],
]}
rightTitle="Pointer & Paste"
rightItems={[
["Mouse", "optional; never required"],
["Wheel", "scroll; keep selection"],
["Paste", "treat as text; rate-limit"],
["Bracketed paste", "avoid triggering keybinds"],
]}
/>
</Grid>
</Section>
<Section title="06 · Redraw Strategy (Full vs Surgical)">
<Grid columns={2}>
<Card title="Full redraw triggers" subtitle="cheap correctness beats cleverness">
<Bullets
items={[
"terminal resize (SIGWINCH)",
"theme/palette change",
"layout mode change (split/merge panes)",
"screen corruption / unknown state",
"unicode width assumptions changed",
]}
/>
</Card>
<Card title="Surgical update triggers" subtitle="small change, known region">
<Bullets
items={[
"cursor move + highlight change",
"status line message update",
"spinner/progress tick",
"one row in a list changes",
"append-only log tail in a pane",
]}
/>
</Card>
</Grid>
<Grid columns={2}>
<Binary
leftTitle="Full redraw"
leftItems={[
["Guarantee", "screen = render(state)"],
["Cost", "O(rows×cols)"],
["Best for", "fragile/remote terminals"],
["UX", "stable + predictable"],
]}
rightTitle="Surgical redraw"
rightItems={[
["Guarantee", "patch screen to match state"],
["Cost", "O(changed cells/rects)"],
["Best for", "high-frequency dashboards"],
["UX", "snappy + less flicker"],
]}
/>
<Card title="Heuristic" subtitle="choose based on uncertainty">
<pre style={s.pre}>{`if (screenStateUnknown) fullRedraw()
else if (layoutChanged) fullRedraw()
else patch(changedRects)`}</pre>
<div style={s.note}>
If you can’t prove which cells are correct, you can’t patch safely.
</div>
</Card>
</Grid>
</Section>
<Section title="07 · Text Selection (Viewing / Navigation)">
<Grid columns={2}>
<Binary
leftTitle="Primary models"
leftItems={[
["Range", "start/end positions in a buffer"],
["Rect", "columnar selection (rare)"],
["Line", "whole-line selection"],
["Token", "word/path selection"],
]}
rightTitle="Primary intents"
rightItems={[
["Copy", "send to clipboard or stdout"],
["Quote", "copy with context/line numbers"],
["Search", "promote selection → / query"],
["Navigate", "jump to selection target"],
]}
/>
<Binary
leftTitle="Selection lifecycle"
leftItems={[
["Enter", "start selecting"],
["Adjust", "expand/shrink with keys"],
["Confirm", "copy/action"],
["Exit", "Esc cancels; clears highlight"],
]}
rightTitle="Non-goals"
rightItems={[
["No rich edit", "editing UX is out of scope"],
["No layout", "selection is on text, not pixels"],
["No mouse", "mouse optional only"],
["No surprises", "never steal focus"],
]}
/>
</Grid>
<Grid columns={2}>
<Card title="Expected patterns" subtitle="document viewing (less, man, logs)">
<Bullets
items={[
"Two modes: browse vs select (selection is explicit)",
"Selection is visible: inverse/highlight band over text range",
"Copy is a verb: y / c / Enter in a copy dialog",
"Search-from-selection: promote selection into / prompt",
"Optional: add line numbers for stable quoting",
]}
/>
</Card>
<Card title="Clipboard reality" subtitle="multiple viable paths">
<Bullets
items={[
"Minimum: copy prints to stdout (user pipes it)",
"Enhanced: OSC-52 clipboard (works over SSH in many emulators)",
"Enhanced: selection to a temp file path",
"Never: require mouse for copying",
]}
/>
</Card>
</Grid>
</Section>
<Section title="08 · Minimum vs Enhanced (Portability)">
<Grid columns={2}>
<Card title="Minimum substrate" subtitle="works everywhere">
<Bullets
items={[
"ASCII first; unicode optional",
"no mouse required",
"no color dependency",
"no hover assumptions",
"stateless redraw tolerated",
]}
/>
</Card>
<Card title="Enhanced substrate" subtitle="nice-to-have">
<Bullets
items={[
"truecolor themes",
"mouse support (optional)",
"inline images (kitty/iterm)",
"OSC-52 clipboard",
"hyperlinks (OSC-8)",
]}
/>
</Card>
</Grid>
</Section>
<Section title="09 · Anti-patterns">
<Grid columns={2}>
<Card title="Surprise" subtitle="breaks trust">
<Bullets
items={[
"mode switches without indicator",
"destructive defaults without preview",
"progress mixed into stdout",
"key chords that depend on timing",
]}
/>
</Card>
<Card title="Fragility" subtitle="breaks portability">
<Bullets
items={[
"requires unicode for core affordances",
"requires mouse to navigate",
"assumes fixed terminal size",
"breaks under tmux/ssh/resizes",
]}
/>
</Card>
</Grid>
</Section>
<Footer />
</main>
</div>
)
}
function Header() {
return (
<header style={s.header}>
<div style={s.hRow}>
<div>
{/* Keep the subtitle short; this is a cheat sheet, not a manifesto */}
<div style={s.hTitle}>VT-HIG — Cheat Sheet</div>
<div style={s.hSub}>CLI/TUI Human Interface Guidelines (draft) 😎</div>
</div>
{/* Version pill is a simple visual anchor for iteration */}
<div style={s.pill}>v0.3</div>
</div>
</header>
)
}
function Footer() {
return (
<div style={s.footer}>
<div style={s.footerLeft}>
<span style={s.mono}>Principle:</span> ship a reliable contract on a dumb byte
stream.
</div>
<div style={s.footerRight}>
<span style={s.mono}>Pattern format:</span> intent → contract → keys → failure
modes.
</div>
</div>
)
}
function Section({
title,
children,
}: {
title: string
children: React.ReactNode
}) {
return (
<section style={s.section}>
{/* Sections are the “table of contents” without needing an actual ToC */}
<div style={s.sectionTitle}>{title}</div>
<div style={s.sectionBody}>{children}</div>
</section>
)
}
function Grid({
columns,
children,
}: {
columns: 1 | 2 | 3
children: React.ReactNode
}) {
return (
<div
style={{
display: "grid",
// Use equal-width columns for scanability (cheat sheets are for scanning).
gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
gap: 12,
}}
>
{children}
</div>
)
}
function Card({
title,
subtitle,
children,
}: {
title: string
subtitle?: string
children: React.ReactNode
}) {
return (
<div style={s.card}>
{/* Card head is intentionally compact to keep “less is more” */}
<div style={s.cardHead}>
<div style={s.cardTitle}>{title}</div>
{subtitle ? <div style={s.cardSub}>{subtitle}</div> : null}
</div>
<div style={s.cardBody}>{children}</div>
</div>
)
}
function Bullets({ items }: { items: string[] }) {
return (
<ul style={s.ul}>
{items.map((t) => (
// Using the bullet string as key is fine here: static, short lists.
<li key={t} style={s.li}>
{t}
</li>
))}
</ul>
)
}
function KeyList({ items }: { items: Array<[string, string]> }) {
return (
<div style={{ display: "grid", gap: 8 }}>
{items.map(([k, v]) => (
// Key labels are unique enough in this list; good keys keep React diffing cheap.
<div key={k} style={s.kRow}>
<kbd style={s.kbd}>{k}</kbd>
<div style={s.kText}>{v}</div>
</div>
))}
</div>
)
}
function Binary({
leftTitle,
leftItems,
rightTitle,
rightItems,
}: {
leftTitle: string
leftItems: Array<[string, string]>
rightTitle: string
rightItems: Array<[string, string]>
}) {
return (
<div style={s.binary}>
{/* Binary blocks enforce a “two buckets” mental model (your preference). */}
<div style={s.binaryCol}>
<div style={s.binaryTitle}>{leftTitle}</div>
<Pairs items={leftItems} />
</div>
<div style={s.binaryDivider} />
<div style={s.binaryCol}>
<div style={s.binaryTitle}>{rightTitle}</div>
<Pairs items={rightItems} />
</div>
</div>
)
}
function Pairs({ items }: { items: Array<[string, string]> }) {
return (
<div style={{ display: "grid", gap: 8 }}>
{items.map(([k, v]) => (
// Pair key is used as React key because the left column is a label.
<div key={k} style={s.pair}>
<div style={s.pairKey}>{k}</div>
<div style={s.pairVal}>{v}</div>
</div>
))}
</div>
)
}
function StackCard() {
return (
<Card title="Capability stack" subtitle="where the UX contract actually sits">
{/* Preformatted so the arrows align as a visual ladder */}
<pre style={s.pre}>
{`[User Intent]
[App UX contract]
[Library fiction]
[TTY/PTY contract]
[Terminal emulator fiction]
[OS + kernel]
[Hardware]`}</pre>
<div style={s.note}>
Design patterns live at <span style={s.mono}>App UX contract</span>,
constrained by everything below.
</div>
</Card>
)
}
// Styling: inline only (single-file cheat sheet; easy to paste into any sandbox).
// Keep the palette minimal to avoid turning this into a theme project.
const s: Record<string, React.CSSProperties> = {
page: {
minHeight: "100vh",
background: "#0b0d10",
color: "#e9edf2",
fontFamily:
"ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial",
},
header: {
position: "sticky",
top: 0,
zIndex: 5,
background: "rgba(11, 13, 16, 0.92)",
backdropFilter: "blur(10px)",
borderBottom: "1px solid rgba(233, 237, 242, 0.10)",
},
hRow: {
maxWidth: 980,
margin: "0 auto",
padding: "18px 16px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 12,
},
hTitle: {
fontSize: 18,
fontWeight: 700,
letterSpacing: 0.2,
},
hSub: {
marginTop: 4,
fontSize: 12,
color: "rgba(233, 237, 242, 0.70)",
},
pill: {
fontSize: 12,
padding: "6px 10px",
borderRadius: 999,
border: "1px solid rgba(233, 237, 242, 0.16)",
color: "rgba(233, 237, 242, 0.85)",
},
main: {
maxWidth: 980,
margin: "0 auto",
padding: "14px 16px 32px",
display: "grid",
gap: 14,
},
section: {
display: "grid",
gap: 10,
},
sectionTitle: {
fontSize: 13,
fontWeight: 700,
color: "rgba(233, 237, 242, 0.90)",
letterSpacing: 0.2,
},
sectionBody: {},
card: {
borderRadius: 16,
border: "1px solid rgba(233, 237, 242, 0.12)",
background: "rgba(255, 255, 255, 0.03)",
overflow: "hidden",
},
cardHead: {
padding: "12px 12px 0",
},
cardTitle: {
fontSize: 13,
fontWeight: 700,
},
cardSub: {
marginTop: 4,
fontSize: 12,
color: "rgba(233, 237, 242, 0.70)",
},
cardBody: {
padding: 12,
},
ul: {
margin: 0,
paddingLeft: 18,
display: "grid",
gap: 6,
},
li: {
color: "rgba(233, 237, 242, 0.88)",
fontSize: 12,
lineHeight: 1.35,
},
kRow: {
display: "grid",
gridTemplateColumns: "auto 1fr",
gap: 10,
alignItems: "center",
},
kbd: {
fontFamily:
"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New'",
fontSize: 12,
padding: "4px 8px",
borderRadius: 10,
border: "1px solid rgba(233, 237, 242, 0.14)",
background: "rgba(0,0,0,0.25)",
color: "rgba(233, 237, 242, 0.92)",
justifySelf: "start",
},
kText: {
fontSize: 12,
color: "rgba(233, 237, 242, 0.82)",
lineHeight: 1.35,
},
binary: {
display: "grid",
gridTemplateColumns: "1fr 1px 1fr",
borderRadius: 16,
border: "1px solid rgba(233, 237, 242, 0.12)",
background: "rgba(255, 255, 255, 0.03)",
overflow: "hidden",
},
binaryCol: {
padding: 12,
display: "grid",
gap: 10,
},
binaryDivider: {
background: "rgba(233, 237, 242, 0.10)",
},
binaryTitle: {
fontSize: 12,
fontWeight: 700,
color: "rgba(233, 237, 242, 0.88)",
},
pair: {
display: "grid",
gridTemplateColumns: "110px 1fr",
gap: 10,
alignItems: "baseline",
},
pairKey: {
fontSize: 12,
fontWeight: 700,
color: "rgba(233, 237, 242, 0.92)",
},
pairVal: {
fontSize: 12,
color: "rgba(233, 237, 242, 0.78)",
lineHeight: 1.35,
},
pre: {
margin: 0,
padding: 12,
borderRadius: 14,
background: "rgba(0,0,0,0.28)",
border: "1px solid rgba(233, 237, 242, 0.10)",
fontSize: 12,
lineHeight: 1.35,
overflowX: "auto",
fontFamily:
"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New'",
color: "rgba(233, 237, 242, 0.92)",
},
note: {
marginTop: 10,
fontSize: 12,
color: "rgba(233, 237, 242, 0.75)",
lineHeight: 1.35,
},
mono: {
fontFamily:
"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New'",
color: "rgba(233, 237, 242, 0.92)",
},
footer: {
marginTop: 8,
paddingTop: 14,
borderTop: "1px solid rgba(233, 237, 242, 0.10)",
display: "flex",
flexWrap: "wrap",
gap: 10,
justifyContent: "space-between",
color: "rgba(233, 237, 242, 0.70)",
fontSize: 12,
},
footerLeft: {
flex: "1 1 360px",
},
footerRight: {
flex: "1 1 360px",
textAlign: "right",
},
}
@subtleGradient
Copy link
Author

@subtleGradient
Copy link
Author

lazygit uses [ / ] to switch between tabs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment