terminal-concepts
Comprehensive guide for building CLI and TUI applications - terminal internals, design principles, and battle-tested patterns When building CLI/TUI apps, implementing argument parsing, handling terminal input/output, escape codes, buffering, signals, or asking about terminal development concepts
Best use case
terminal-concepts is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Comprehensive guide for building CLI and TUI applications - terminal internals, design principles, and battle-tested patterns When building CLI/TUI apps, implementing argument parsing, handling terminal input/output, escape codes, buffering, signals, or asking about terminal development concepts
Teams using terminal-concepts should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/terminal-concepts/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How terminal-concepts Compares
| Feature / Agent | terminal-concepts | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Comprehensive guide for building CLI and TUI applications - terminal internals, design principles, and battle-tested patterns When building CLI/TUI apps, implementing argument parsing, handling terminal input/output, escape codes, buffering, signals, or asking about terminal development concepts
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
Related Guides
SKILL.md Source
# Terminal Concepts for Developers
## Overview
This agent provides comprehensive guidance for **building and developing** terminal applications (CLI tools and TUIs). Learn how terminals work, design principles from proven programs, and practical patterns for creating robust, user-friendly command-line applications.
**Philosophy**: Focus on timeless concepts and learn from battle-tested programs like git, vim, tmux, less, ripgrep, and fzf.
**Target Audience**: Developers at all levels building terminal applications in any language.
## What Developers Need to Know
Building terminal applications requires understanding several interconnected concepts:
1. **Terminal Fundamentals**: How TTYs, PTYs, streams, and buffering work
2. **CLI Design**: User-facing interface principles (arguments, output, errors)
3. **TUI Architecture**: Interactive full-screen applications
4. **Common Pitfalls**: Issues developers face and how to avoid them
## Learning from Proven Programs
The best way to understand terminal application design is to study programs that have stood the test of time:
- **git**: Consistent subcommand interface, porcelain vs plumbing separation
- **vim**: Modal editing, robust state management
- **tmux**: Client-server architecture, session management
- **less**: Pager pattern, search and navigation
- **ripgrep**: Sensible defaults, performance-focused design
- **fzf**: Interactive filtering, minimal interface
- **cargo**: Clear progress indicators, helpful error messages
Throughout this guide, we'll reference these programs to illustrate concepts.
---
# Part 1: Terminal Fundamentals for Developers
## Understanding the Terminal Stack
When you build a terminal application, you're working within a layered system:
```
┌─────────────────────────────────┐
│ Terminal Emulator │ ← User sees (renders text, sends input)
│ (iTerm2, Alacritty, etc.) │
└─────────────────────────────────┘
↕ (PTY)
┌─────────────────────────────────┐
│ Shell (bash, zsh, fish) │ ← Interprets commands
└─────────────────────────────────┘
↕ (fork/exec)
┌─────────────────────────────────┐
│ Your Program │ ← Your CLI/TUI application
└─────────────────────────────────┘
```
### TTYs and PTYs
**TTY** (Teletypewriter): Originally physical devices, now refers to the terminal driver in the operating system.
**PTY** (Pseudo-Terminal): A pair of virtual devices that emulate a TTY:
- **Master side**: Terminal emulator reads/writes here
- **Slave side**: Your program sees this as stdin/stdout/stderr
**Why This Matters for Developers**:
- Your program receives input filtered through the TTY driver
- Some control characters are intercepted by the OS (Ctrl-C, Ctrl-Z)
- Buffering behavior differs between TTY and pipes
- You need to detect if you're running in a TTY vs pipe
### Terminal Modes: Cooked vs Raw
**Cooked Mode** (Canonical Mode):
- Default mode for most programs
- OS provides line editing (backspace, Ctrl-U, Ctrl-W)
- Input delivered to your program line-by-line after Enter
- Control characters handled by OS (Ctrl-C sends SIGINT)
**Raw Mode**:
- Character-by-character input (no line buffering)
- Your program receives every keypress including Ctrl-C
- Required for TUIs (vim, less, htop)
- Your program must implement all input handling
**When to Use Each**:
- **Cooked mode**: Normal CLI tools (git, cargo, npm)
- **Raw mode**: Interactive TUIs, line editors
### Standard Streams and File Descriptors
Every process has three standard streams:
| Stream | FD | Purpose | Examples |
| ------ | --- | ------------------------- | ------------------------------- |
| stdin | 0 | Input from user or pipe | Reading commands, file content |
| stdout | 1 | Primary output | Results, listings, JSON |
| stderr | 2 | Errors, logs, diagnostics | Error messages, warnings, debug |
**Critical Design Principle**: Separate stdout and stderr properly.
**Why This Matters**:
```bash
# User wants to pipe your output
your-tool | jq . # Only works if output goes to stdout
# User wants to capture errors
your-tool 2> errors.log # Only works if errors go to stderr
```
**Examples from Proven Programs**:
- **git**: Errors to stderr, porcelain output to stdout
- **ripgrep**: Matches to stdout, warnings to stderr
- **cargo**: Build status to stderr, `--message-format=json` to stdout
### Detecting TTY vs Pipe
Your program must detect its output destination to format appropriately:
**Concept** (language-agnostic):
```
if isatty(stdout):
# Human is watching - use colors, progress bars
enable_colors()
show_progress()
else:
# Piped to another program - plain output
disable_colors()
no_progress()
```
**Real-world examples**:
- `ls --color=auto`: Colors only for TTY
- `git status`: Full output for TTY, shorter for pipes
- `ripgrep`: Colors and summaries for TTY, plain matches for pipes
**C Implementation**:
```c
#include <unistd.h>
if (isatty(STDOUT_FILENO)) {
// stdout is a TTY
}
```
**Rust Implementation**:
```rust
use std::io::IsTerminal;
if std::io::stdout().is_terminal() {
// stdout is a TTY
}
```
**Python Implementation**:
```python
import sys
if sys.stdout.isatty():
# stdout is a TTY
```
**Go Implementation**:
```go
import "golang.org/x/term"
if term.IsTerminal(int(os.Stdout.Fd())) {
// stdout is a TTY
}
```
---
## ASCII Control Characters
### The 33 Control Characters
Control characters are created by holding Ctrl and pressing a key. There are 33 total:
- Ctrl-A through Ctrl-Z (26 characters)
- Plus 7 more: Ctrl-@, Ctrl-[, Ctrl-\, Ctrl-], Ctrl-^, Ctrl-\_, Ctrl-?
### Three Categories
**1. OS-Handled (Terminal Driver Intercepts)**:
| Key | ASCII | Name | Function |
| ------- | ----- | ---- | ----------------------------- |
| Ctrl-C | 3 | ETX | Sends SIGINT (interrupt) |
| Ctrl-D | 4 | EOT | EOF when line is empty |
| Ctrl-Z | 26 | SUB | Sends SIGTSTP (suspend) |
| Ctrl-S | 19 | XOFF | Freezes output (flow control) |
| Ctrl-Q | 17 | XON | Resumes output |
| Ctrl-\ | 28 | FS | Sends SIGQUIT |
**2. Keyboard Literals**:
| Key | ASCII | Name | Usage |
| --------- | ----- | ---- | ------------------------- |
| Enter | 13 | CR | Line terminator |
| Tab | 9 | HT | Tab character |
| Backspace | 127 | DEL | Delete previous character |
| Ctrl-H | 8 | BS | Often same as backspace |
**3. Application-Specific** (Your Program Can Define):
| Key | Common Usage |
| -------- | ------------------------------------ |
| Ctrl-A | Move to line start (readline, emacs) |
| Ctrl-E | Move to line end |
| Ctrl-W | Delete word backwards |
| Ctrl-U | Delete line |
| Ctrl-K | Kill to end of line |
| Ctrl-R | Reverse search (shells) |
| Ctrl-L | Clear screen |
| Ctrl-P/N | Previous/Next (history navigation) |
### What Developers Can and Cannot Intercept
**In Cooked Mode**:
- OS handles: Ctrl-C, Ctrl-D, Ctrl-Z, Ctrl-S, Ctrl-Q
- You see: Ctrl-A, Ctrl-E, Ctrl-W (already processed by line editing)
**In Raw Mode** (TUIs):
- You can intercept almost everything including Ctrl-C
- **Exception**: Ctrl-Z often still suspends (OS-level)
- You must handle all line editing yourself
**Best Practice**: Respect user expectations. Don't redefine Ctrl-C unless you have a very good reason (and document it clearly).
### Limited Modifier Combinations
Unlike GUI applications, terminals have severe limitations:
- **Only 33 Ctrl combinations** total
- Ctrl-Shift-X **doesn't exist** as a distinct character
- Ctrl-[number] combinations limited
- Alt/Meta combinations inconsistent across terminals
**Implication**: Design keyboard shortcuts carefully. You have far fewer options than GUI apps.
---
## Escape Codes and Standards
### What Are Escape Codes?
Escape codes are invisible character sequences that control terminals. They start with ESC (ASCII 27, written as `\x1b`, `\033`, or `\e`).
**Two types**:
1. **Input codes**: Generated by keypresses (arrow keys, function keys)
2. **Output codes**: Your program uses to control display (colors, cursor movement)
### Output Escape Code Standards
#### ECMA-48 Foundation
The base standard defining escape code formats:
**CSI (Control Sequence Introducer)**: `ESC [` followed by parameters
```
ESC[2J # Clear screen
ESC[H # Move cursor to home
ESC[1;31m # Red foreground color
```
**OSC (Operating System Command)**: `ESC ]` followed by parameters
```
ESC]0;Title\x07 # Set window title
ESC]52;c;base64\x07 # Clipboard access (OSC 52)
```
#### XTerm Extensions
XTerm added features beyond ECMA-48:
- Mouse reporting (click, drag, scroll)
- Clipboard access via OSC 52
- Extended color modes (256 colors, RGB)
**These aren't formally standardized** but are widely supported because xterm is so influential.
#### Terminfo Database
A database mapping terminal types to their capabilities:
```bash
echo $TERM # xterm-256color, screen-256color, etc.
infocmp $TERM # Dump terminal capabilities
tput bold # Output "bold" escape sequence for $TERM
```
**Terminfo Approach**:
1. Query terminfo for escape sequences
2. Adapts to different terminals automatically
3. Complex to use, slower
**Hardcoded Approach**:
1. Use common escape sequences directly
2. Test across major terminals
3. Simpler, faster, good enough for most
**Most modern programs use the hardcoded approach** for the subset of widely-supported sequences.
### Practical Compatibility Strategies
**Strategy 1**: Stick to well-supported sequences
- 16 ANSI colors (ESC[30-37m, ESC[40-47m)
- Basic cursor movement (ESC[H, ESC[A/B/C/D)
- Clear screen/line (ESC[2J, ESC[K)
**Strategy 2**: Test on major terminal emulators
- iTerm2, Alacritty (macOS)
- GNOME Terminal, Konsole (Linux)
- Windows Terminal (Windows)
- tmux and screen (multiplexers)
**Strategy 3**: Provide fallbacks
```
if supports_256_colors():
use_256_color_palette()
elif supports_16_colors():
use_basic_colors()
else:
no_colors()
```
**Examples from Proven Programs**:
- **less**: Queries terminfo, falls back to hardcoded
- **vim**: Extensive terminfo support with fallbacks
- **ripgrep**: Hardcoded ANSI, works everywhere
### When to Use Libraries vs Raw Escape Codes
**Use Libraries When**:
- Building complex TUIs (ncurses, crossterm, etc.)
- Need mouse support
- Want automatic capability detection
- Cross-platform support (Windows Console API)
**Use Raw Escape Codes When**:
- Simple color output
- Progress bars
- Cursor positioning for simple UIs
- You want minimal dependencies
---
## Buffering Deep Dive
### Three Buffering Modes
**Unbuffered**: Every write goes directly to the destination
- Slowest (syscall overhead)
- Used for stderr by default
**Line Buffered**: Flush on newlines
- Used for stdout when writing to TTY
- Balance of performance and responsiveness
**Block Buffered**: Flush when buffer full (~8KB)
- Used for stdout when writing to pipe/file
- Most efficient for throughput
### Why Your Program Uses Different Buffering
The standard library (libc, Go runtime, Python runtime) **automatically detects** with `isatty()`:
```
if isatty(stdout):
use_line_buffering() # Interactive user
else:
use_block_buffering() # Pipe or file
```
**This is why pipes get stuck!**
### The Pipe Buffering Problem
```bash
tail -f log.txt | grep ERROR
# Hangs! grep is waiting for 8KB before flushing
```
**Why**:
1. `grep` sees stdout is a pipe (not TTY)
2. Uses block buffering (8KB threshold)
3. Waits to accumulate data
4. Never reaches threshold with sparse matches
5. Appears frozen
### Solutions for Developers Building CLI Tools
**Solution 1**: Add `--line-buffered` flag
**C Implementation**:
```c
#include <stdio.h>
if (line_buffered_flag) {
setvbuf(stdout, NULL, _IOLBF, 0);
}
// Or manually flush:
printf("output\n");
fflush(stdout);
```
**Rust Implementation**:
```rust
use std::io::{self, Write};
fn main() {
let stdout = io::stdout();
let mut handle = stdout.lock();
writeln!(handle, "output").unwrap();
handle.flush().unwrap(); // Manual flush
}
```
**Python Implementation**:
```python
import sys
# Enable line buffering
sys.stdout.reconfigure(line_buffering=True)
# Or manual flush
print("output", flush=True)
# Or environment variable
# PYTHONUNBUFFERED=1 python script.py
```
**Go Implementation**:
```go
import (
"bufio"
"os"
)
writer := bufio.NewWriter(os.Stdout)
writer.WriteString("output\n")
writer.Flush() // Manual flush
```
**Solution 2**: Always flush after important output
**Best Practices**:
- Flush after progress updates
- Flush after each line of JSON in streaming mode
- Flush before long computations
- Provide `--line-buffered` for tools that filter streams
**Examples from Proven Programs**:
- `grep --line-buffered`: Solves pipe buffering
- `sed -u`: Unbuffered mode
- `awk`: Has no built-in flag (common complaint)
### Testing Buffered Output
**In CI/CD**:
```bash
# Force line buffering
stdbuf -oL your-tool | other-tool
# Or use unbuffer (expect package)
unbuffer your-tool | other-tool
```
**In Tests**:
- Mock the TTY with PTY libraries
- Test both TTY and non-TTY paths
- Verify flushing behavior
---
## Input Escape Sequences
### Arrow Keys and Function Keys
When users press special keys, terminals send multi-character escape sequences:
| Key | Sequence | Notes |
| --------- | -------------------- | ------------------ |
| Up | `ESC[A` | CSI sequence |
| Down | `ESC[B` | |
| Right | `ESC[C` | |
| Left | `ESC[D` | |
| Home | `ESC[H` or `ESC[1~` | Varies by terminal |
| End | `ESC[F` or `ESC[4~` | |
| Page Up | `ESC[5~` | |
| Page Down | `ESC[6~` | |
| F1 | `ESC OP` or `ESC[[A` | Highly variable |
| F12 | `ESC[24~` | |
### Distinguishing ESC Key from Escape Sequences
**The Problem**: ESC character can mean:
1. User pressed ESC key (standalone)
2. Start of escape sequence (ESC[A for up arrow)
**Solution**: Timeout-based parsing
```
Read character:
If ESC:
Wait ~50ms for next character:
If timeout: User pressed ESC
Else: Start of sequence, continue reading
```
**Proven Programs**:
- **vim**: Uses 1 second timeout (customizable with `ttimeoutlen`)
- **less**: Uses short timeout
- **readline**: Configurable timeout
### Mouse Events
Modern terminals can send mouse events (clicks, drags, scrolls):
```
ESC[<0;10;5M # Mouse button press at column 10, row 5
```
**Enable mouse reporting**:
```
ESC[?1000h # Send button press/release
ESC[?1002h # Send button press/release/drag
ESC[?1006h # SGR mouse mode (better format)
```
**Disable when exiting**:
```
ESC[?1000l
```
**Used by**: vim, tmux, less, htop
---
# Part 2: CLI Design Guidelines for Developers
_Based on clig.dev with implementation focus_
## Philosophy: The 9 Core Principles
### 1. Human-First Design
**Principle**: CLIs are primarily for humans, not just machines.
**Practical Implications**:
- Show what's happening (don't hang silently)
- Confirm dangerous operations
- Provide helpful errors
- Use conversational language
**Counter to UNIX Tradition**: "Silence is golden" doesn't work for modern tools. Users expect feedback.
**Example from cargo**:
```
$ cargo build
Compiling myapp v0.1.0
Finished dev [unoptimized + debuginfo] target(s) in 2.34s
```
Clear indication of progress and completion.
### 2. Simple, Composable Parts
**Principle**: Standard streams, pipes, and exit codes enable composition.
**Practical Implications**:
- Output primary results to stdout
- Output logs/errors to stderr
- Exit 0 for success, non-zero for failure
- Accept stdin, write to stdout for filters
**Example from ripgrep**:
```bash
rg "pattern" | rg "filter" | wc -l
```
Composes naturally because stdout contains only matches.
### 3. Consistency Across Programs
**Principle**: Follow established conventions.
**Practical Implications**:
- Use standard flag names (-h, --help, -v, --version)
- Follow argument conventions (POSIX or GNU style)
- Ctrl-C should interrupt
- 'q' should quit TUIs
**Example from git**:
Every subcommand uses consistent flags:
```bash
git commit --verbose
git log --verbose
git diff --verbose
```
### 4. Saying Just Enough
**Principle**: Balance information density. Too little confuses, too much overwhelms.
**Guidelines**:
- Default: Show what changed
- Quiet mode (-q): Silence non-errors
- Verbose mode (-v): Show details
- Debug mode (--debug): Everything
**Example from docker**:
```
$ docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
a2abf6c4d29d: Pull complete
Status: Downloaded newer image for nginx:latest
```
Just enough to understand progress.
### 5. Ease of Discovery
**Principle**: Users shouldn't need to memorize everything.
**Practical Implications**:
- Comprehensive help text
- Examples in help
- Suggest corrections for typos
- Show next steps
**Example from git**:
```
$ git pul
git: 'pul' is not a git command. See 'git --help'.
The most similar command is
pull
```
### 6. Conversation as the Norm
**Principle**: Design for iterative use and trial-and-error.
**Practical Implications**:
- Clear error messages that suggest fixes
- Allow undoing or --dry-run
- Preserve state when safe
- Show current state easily
**Example from git**:
```
$ git status
On branch main
Changes not staged for commit:
modified: file.txt
no changes added to commit
```
Always shows current state.
### 7. Robustness
**Principle**: Feel solid, not fragile. Handle errors gracefully.
**Practical Implications**:
- Validate input early
- Meaningful error messages (not stack traces)
- Handle SIGINT gracefully
- Restore terminal state on crash (TUIs)
**Example from cargo**:
```
$ cargo build
error: Could not compile `myapp` due to 2 previous errors
```
Clear, actionable error.
### 8. Empathy
**Principle**: Show you're on the user's side.
**Practical Implications**:
- Assume mistakes are honest
- Explain what went wrong
- Suggest how to fix it
- Be encouraging, not condescending
**Bad**:
```
Error: Invalid argument
```
**Good (from rustc)**:
```
error: unexpected end of file
--> src/main.rs:5:1
|
5 | }
| ^ expected one of 8 possible tokens here
```
### 9. Chaos as Power
**Principle**: Terminal inconsistency enables innovation. Break rules intentionally with purpose.
**When to Break Conventions**:
- Clear usability improvement
- Document the deviation
- Provide escape hatches
**Example**: ripgrep's default behavior (auto-ignore .gitignore) breaks UNIX tradition but is more useful for developers.
---
## Arguments and Flags Implementation
### Terminology
**Arguments** (Positional Parameters):
- Order matters
- No dashes
- Example: `cp source dest`
**Flags** (Named Parameters):
- Order independent
- Start with `-` or `--`
- May have values
- Example: `ls --color=auto -l`
**Options**: Sometimes used interchangeably with flags
### Design Decision: Args vs Flags
**Use Positional Args When**:
- One or two required parameters
- Order is obvious
- Examples: `cat file.txt`, `cd /path`, `rm file1 file2`
**Use Flags When**:
- Optional parameters
- Many parameters
- Order shouldn't matter
- Need extensibility
- Examples: `git commit --message "msg" --amend`
**Prefer flags over args** for anything complex. They're more discoverable and extensible.
**Example from git**: Heavily flag-based for flexibility
```bash
git log --oneline --graph --all --decorate
# Order doesn't matter
git log --all --oneline --decorate --graph
```
### Standard Flag Names
Follow these conventions for consistency:
| Flag | Meaning | Example Usage |
| --------------------- | ------------------------- | ----------------- |
| `-h`, `--help` | Show help (only) | `tool --help` |
| `--version` | Show version (only) | `tool --version` |
| `-v`, `--verbose` | More output | `tool -v` |
| `-q`, `--quiet` | Less output | `tool -q` |
| `-f`, `--force` | Skip confirmations | `rm -f file` |
| `-r`, `--recursive` | Recurse directories | `rm -r dir` |
| `-n`, `--dry-run` | Preview without executing | `git clean -n` |
| `-a`, `--all` | Include all items | `git add -a` |
| `-o`, `--output FILE` | Output file | `gcc -o program` |
| `-i`, `--interactive` | Prompt for decisions | `rm -i file` |
| `--no-input` | No interactive prompts | `tool --no-input` |
| `-d`, `--debug` | Debug output | `tool -d` |
| `--json` | JSON output | `tool --json` |
**Don't Repurpose These**: Users have muscle memory for these flags.
### POSIX vs GNU Style
**POSIX Style**:
```bash
tool -a -b -c # Short flags
tool -abc # Bundled: same as above
tool -o file # Flag with value (space separated)
```
**GNU Style**:
```bash
tool --long-flag # Long flags
tool --output=file # Equals-separated value
tool --output file # Space-separated value
tool -a --long -b # Mixed short and long
```
**Modern Best Practice**: Support both
- Short flags for common options
- Long flags for all options
- Allow bundling short flags (`-la` for `-l -a`)
- Support both `=` and space for values
**Example from git**:
```bash
git commit -m "msg" # POSIX short
git commit --message="msg" # GNU long with =
git commit --message "msg" # GNU long with space
```
### Dangerous Operations: Confirmations
**Three Levels of Danger**:
**1. Low (Reversible)**:
- Delete a single file
- No confirmation needed (or -i flag)
- Example: `rm file.txt` (can restore from trash)
**2. Medium (Significant Impact)**:
- Delete directory, remote changes
- Interactive mode: Prompt for y/n
- Non-interactive: Require --force
- Example: `rm -r dir` (should prompt or need -f)
**3. High (Destructive/Widespread)**:
- Delete entire application, mass operations
- Require typing something non-trivial
- Example: Heroku's pattern:
```
$ heroku apps:destroy myapp
▸ WARNING: This will delete myapp including all add-ons.
▸ To proceed, type myapp or re-run this command with --confirm myapp
> myapp
Destroying myapp... done
```
**For Scripts**: Always provide `--force` or `--confirm=VALUE` to bypass prompts
```bash
tool --force # Bypass all confirmations
tool --confirm="dangerous" # Confirm with specific value
```
### Order Independence
**Principle**: Users shouldn't need to remember flag order.
**Support All These**:
```bash
tool subcommand --flag value arg
tool --flag value subcommand arg
tool --flag value arg subcommand
```
**Example from git** (all equivalent):
```bash
git --no-pager log --oneline
git log --oneline --no-pager
```
**Example from docker** (noun-verb pattern):
```bash
docker container rm --force nginx
docker container rm nginx --force
```
### No Secrets in Flags
**Never**:
```bash
tool --password secret123 # Visible in ps, shell history
```
**Instead**:
```bash
# Option 1: Prompt interactively
tool --prompt-password
# Option 2: Read from file
tool --password-file ~/.secret
# Option 3: Read from stdin
cat ~/.secret | tool --password-stdin
# Option 4: Environment variable (also risky)
PASSWORD=secret123 tool
```
**Why**: `ps aux` shows all flags to all users. Shell history is often world-readable.
---
## Help and Documentation
### Help Display Requirements
**Minimal Help** (`-h` or no arguments):
```
USAGE:
tool [OPTIONS] <FILE>
A brief one-line description of what this tool does.
OPTIONS:
-h, --help Print help information
-v, --version Print version
-o, --output Output file (default: stdout)
EXAMPLES:
tool input.txt Process input.txt
tool -o out.txt in.txt Write to out.txt
For more information, run: tool --help
```
**Full Help** (`--help`):
```
tool 1.2.3
A comprehensive description of what this tool does and why
you might want to use it.
USAGE:
tool [FLAGS] [OPTIONS] <INPUT> [OUTPUT]
ARGS:
<INPUT> Input file to process
[OUTPUT] Output file (default: stdout)
FLAGS:
-h, --help Print help information
-V, --version Print version information
-v, --verbose Verbose output
-q, --quiet Suppress non-error output
-f, --force Overwrite existing files
OPTIONS:
-o, --output <FILE> Write output to FILE
-c, --config <FILE> Use configuration from FILE
EXAMPLES:
# Basic usage
tool input.txt
# Write to file
tool input.txt output.txt
# With options
tool -v --config my.conf input.txt
# Pipe input
cat input.txt | tool > output.txt
ENVIRONMENT:
TOOL_CONFIG Default configuration file path
For bug reports and feature requests:
https://github.com/user/tool/issues
```
### Examples-First Approach
**Principle**: Show examples before describing flags.
**Why**: Users learn faster from examples than from parameter descriptions.
**Learning from git**:
```
$ git help commit
NAME
git-commit - Record changes to the repository
SYNOPSIS
git commit [-a | --interactive | --patch] ...
DESCRIPTION
Create a new commit containing the current contents of the index...
EXAMPLES
Record your own changes
$ git commit -a
Commit with a detailed message
$ git commit -m "Initial commit" -m "More details"
```
Examples section shows common patterns.
### Dynamic Help Generation
Generate help from the same source as argument parsing:
**Benefits**:
- Always stays in sync
- No duplicate maintenance
- Consistent formatting
**Concept** (language-agnostic):
```
define_cli():
add_flag("output", short="o", help="Output file")
add_flag("verbose", short="v", help="Verbose output")
generate_help_from_definitions()
```
Most argument parsing libraries do this automatically.
### Man Pages
**When to Provide**:
- Professional tools
- System-wide installation
- Complex functionality
**Man Page Structure**:
```
NAME
tool - one-line description
SYNOPSIS
tool [OPTIONS] FILES...
DESCRIPTION
Detailed description
OPTIONS
Detailed flag descriptions
EXAMPLES
Usage examples
SEE ALSO
Related commands
BUGS
Bug tracker URL
```
**Generation Tools**:
- `help2man`: Auto-generate from --help output
- `ronn`: Markdown to man page
- `scdoc`: Simple man page format
- `asciidoc`: Comprehensive documentation system
**Example from git**: Extensive man pages for every subcommand
```bash
man git-commit
man git-rebase
```
---
## Output Design for Developers
### TTY Detection Pattern
**Core Pattern**:
```
if is_tty(stdout):
format = HumanReadable(colors=True, progress=True)
else:
format = MachineReadable(colors=False, progress=False)
```
**Implementation** (shown earlier, repeated for context):
- Use `isatty()` system call
- Check file descriptor 1 (stdout)
- Make decision at startup
**Override Flags**:
```bash
tool --color=always # Force colors even in pipe
tool --color=never # No colors even in TTY
tool --color=auto # Default (detect TTY)
```
**Example from ls**:
```bash
ls --color=auto # Default on many systems
```
### Color Implementation
**Respect NO_COLOR Environment Variable**:
```
if getenv("NO_COLOR"):
disable_all_colors()
elif not is_tty(stdout):
disable_all_colors()
elif color_flag == "never":
disable_all_colors()
else:
enable_colors()
```
**16 ANSI Colors (Safest)**:
| Code | Color | Code | Color |
| ----- | ------------- | ------- | ------------------ |
| 30 | Black | 40 | Black background |
| 31 | Red | 41 | Red background |
| 32 | Green | 42 | Green background |
| 33 | Yellow | 43 | Yellow background |
| 34 | Blue | 44 | Blue background |
| 35 | Magenta | 45 | Magenta background |
| 36 | Cyan | 46 | Cyan background |
| 37 | White | 47 | White background |
| 90-97 | Bright colors | 100-107 | Bright backgrounds |
**Usage**:
```
ESC[31m red text ESC[0m # Red foreground
ESC[1;31m bold red ESC[0m # Bold red
ESC[0m # Reset all attributes
```
**Example Code (Rust)**:
```rust
fn print_colored(text: &str, color: u8) {
if atty::is(atty::Stream::Stdout) && std::env::var("NO_COLOR").is_err() {
println!("\x1b[{}m{}\x1b[0m", color, text);
} else {
println!("{}", text);
}
}
```
**Learning from ripgrep**:
- Automatic color detection
- Respects NO_COLOR
- Highlights matches in red by default
- Disables in pipes
### JSON and Machine-Readable Output
**Pattern**: Provide `--json` flag for structured output
**Design**:
```bash
# Human-readable (default for TTY)
$ tool list
Found 3 items:
- Item 1 (active)
- Item 2 (inactive)
- Item 3 (active)
# Machine-readable
$ tool list --json
[{"name":"Item 1","status":"active"},{"name":"Item 2","status":"inactive"}]
```
**Guidelines**:
- One JSON object per line for streaming (JSONL/ndjson)
- Valid JSON even on errors
- Include error information in JSON
**Example (streaming)**:
```bash
$ tool process --json
{"type":"start","count":100}
{"type":"progress","done":50,"total":100}
{"type":"complete","duration":1.5}
```
**Learning from cargo**:
```bash
cargo build --message-format=json
```
Outputs JSON for tooling integration.
### Progress Indicators
**When to Show**:
- TTY output only
- Long-running operations (>1 second)
- User needs feedback
**When to Hide**:
- Piped output
- `--quiet` flag
- CI/CD environments (use `CI` env var)
**Types**:
**Spinner** (indeterminate):
```
⠋ Processing...
⠙ Processing...
⠹ Processing...
```
**Progress Bar** (determinate):
```
[=========> ] 45% (450/1000)
```
**Example Code (Concept)**:
```
if is_tty(stderr) and not quiet_mode:
progress = ProgressBar(total=100)
for item in items:
process(item)
progress.increment()
```
**Learning from cargo**:
```
Updating crates.io index
Downloaded 2 crates (50.3 KB) in 0.38s
Compiling serde v1.0.152
Compiling toml v0.5.11
Finished dev [unoptimized + debuginfo] target(s) in 3.42s
```
Clear progress with meaningful stages.
### Pager Integration
**When to Use Pager**:
- Output longer than terminal height
- User might want to scroll/search
- Examples: git log, man, --help output
**How to Detect**:
```
if is_tty(stdout) and output_lines > terminal_height:
pipe_to_pager()
```
**Respect PAGER Environment Variable**:
```
pager = getenv("PAGER") or "less"
```
**Common Pager Options for less**:
```
LESS="-FIRX"
F: Quit if output fits on screen
I: Case-insensitive search
R: Allow ANSI color codes
X: Don't clear screen on exit
```
**Example from git**:
```bash
git log # Automatically pages long output
```
**Disable When Needed**:
```bash
git --no-pager log # Don't page
```
---
## Error Handling Implementation
### User-Focused Error Messages
**Bad**:
```
Error: FileNotFoundError: [Errno 2] No such file or directory: 'config.toml'
at read_config (tool.py:42)
at main (tool.py:120)
```
**Good**:
```
Error: Could not find configuration file 'config.toml'
Try creating one with: tool init
Or specify a different location: tool --config path/to/config.toml
```
**Principles**:
- Say what went wrong (not code-level details)
- Suggest how to fix it
- No stack traces unless --debug
**Implementation Pattern**:
```
catch FileNotFoundError as e:
if debug_mode:
print_stack_trace(e)
else:
print("Error: Could not find file '{}'".format(e.filename))
print("Try: ...")
```
### Error Message Hierarchy
**Write to stderr**: All errors and warnings
**Structure**:
```
ERROR: Critical failure, operation cannot complete
WARNING: Something's wrong, but continuing
INFO: Notable state change (when verbose)
DEBUG: Detailed diagnostics (when --debug)
```
**Color Coding** (if TTY):
```
ERROR: red
WARNING: yellow
INFO: blue/cyan
DEBUG: gray/dim
```
**End with Critical Info**: Terminal scrolls, last line is most visible
**Example from rustc**:
```
error: aborting due to 2 previous errors
For more information about this error, try `rustc --explain E0425`.
```
Summary and next steps at the end.
### Exit Codes
**POSIX Conventions**:
- `0`: Success
- `1`: General error
- `2`: Misuse (invalid arguments)
- `126`: Command found but not executable
- `127`: Command not found
- `128+N`: Killed by signal N (e.g., 130 for Ctrl-C)
**Design Your Own** for specific errors:
```
0: Success
1: General error
2: Invalid arguments
10: File not found
11: Permission denied
12: Network error
```
**Document them**:
```
EXIT CODES:
0 Success
1 General error
2 Invalid arguments
10 File not found
```
**Why They Matter**: Scripts check exit codes
```bash
if tool process file.txt; then
echo "Success"
else
echo "Failed with code $?"
fi
```
---
## Interactivity Implementation
### Confirmation Prompts
**Simple Yes/No**:
```
$ tool delete-all
Really delete all data? [y/N]: _
```
**Implementation Concept**:
```
if is_tty(stdin) and not no_input_flag:
response = prompt("Really delete all data? [y/N]: ")
if response.lower() != 'y':
exit(0)
elif force_flag:
# Proceed without confirmation
else:
error("Cannot confirm in non-interactive mode. Use --force.")
exit(1)
```
**Type-to-Confirm Pattern** (for dangerous operations):
```
$ heroku apps:destroy myapp
Type the app name to confirm: _
```
### Password Input (No Echo)
**Requirement**: Don't display password as user types
**C Implementation**:
```c
#include <termios.h>
#include <unistd.h>
void disable_echo() {
struct termios tty;
tcgetattr(STDIN_FILENO, &tty);
tty.c_lflag &= ~ECHO;
tcsetattr(STDIN_FILENO, TCSANOW, &tty);
}
void enable_echo() {
struct termios tty;
tcgetattr(STDIN_FILENO, &tty);
tty.c_lflag |= ECHO;
tcsetattr(STDIN_FILENO, TCSANOW, &tty);
}
```
**Python**:
```python
import getpass
password = getpass.getpass("Password: ")
```
**Rust**:
```rust
use rpassword::read_password;
println!("Password: ");
let password = read_password().unwrap();
```
### --no-input Flag
**Critical for CI/CD**: Never hang waiting for input
**Implementation**:
```
if no_input_flag:
# Never prompt
# Use defaults or fail with error
if required_confirmation:
error("Cannot prompt in --no-input mode. Use --force.")
exit(1)
```
**Example**:
```bash
# Interactive (prompts for confirmation)
tool deploy
# CI/CD (fails without --force)
tool deploy --no-input --force
```
---
## Configuration Management
### Configuration Sources and Precedence
**Load Order (highest to lowest priority)**:
1. Command-line flags
2. Environment variables
3. Local config file (./.toolrc)
4. User config file (~/.config/tool/config)
5. System config file (/etc/tool/config)
6. Built-in defaults
**Implementation Pattern**:
```
config = load_defaults()
config.update(load_system_config())
config.update(load_user_config())
config.update(load_local_config())
config.update(load_environment())
config.update(load_flags())
```
### XDG Base Directory Specification
**Standard Paths**:
```
$XDG_CONFIG_HOME/tool/config # User config (default: ~/.config/)
$XDG_DATA_HOME/tool/data # User data (default: ~/.local/share/)
$XDG_CACHE_HOME/tool/cache # Cache (default: ~/.cache/)
```
**Fallbacks**:
```
config_dir = getenv("XDG_CONFIG_HOME") or join(getenv("HOME"), ".config")
config_file = join(config_dir, "tool", "config.toml")
```
**Why**: Reduces dotfile clutter in home directory
**Example from git**:
```
~/.gitconfig # Traditional
~/.config/git/config # XDG (takes precedence)
```
### Environment Variables to Respect
**Standard Variables**:
| Variable | Purpose | Example Usage |
| -------- | ---------------------------------- | ----------------------- |
| NO_COLOR | Disable all colors | Check before colorizing |
| EDITOR | User's preferred editor | `tool edit` opens this |
| VISUAL | Visual editor (prefer over EDITOR) | Same as EDITOR |
| PAGER | Paging program | Use for long output |
| HOME | User's home directory | For config paths |
| TMPDIR | Temporary directory | For temp files |
| TERM | Terminal type | For escape sequences |
| COLUMNS | Terminal width | For formatting |
| LINES | Terminal height | For paging decisions |
| CI | Running in CI environment | Disable progress bars |
| DEBUG | Enable debug mode | Show verbose output |
**Your Own Variables**:
```
TOOL_CONFIG=/path/to/config
TOOL_API_KEY=secret123
TOOL_LOG_LEVEL=debug
```
**Naming Convention**: ALL_CAPS, prefix with tool name
---
# Part 3: Building TUIs - Developer's Guide
## Core Patterns Developers Must Implement
### Terminal Rules for TUIs
**Rule 1**: 'q' quits the program
- **Examples**: less, htop, man
- **Exception**: Text editors where 'q' has other meaning
**Rule 2**: Ctrl-D quits REPLs
- **Examples**: python, irb, node, psql
- **Mimics**: OS-level EOF behavior
**Rule 3**: Ctrl-C should exit or interrupt
- In raw mode, you receive Ctrl-C as character 3
- Either exit immediately or cancel current operation
- **Don't**: Ignore it completely
**Rule 4**: ESC cancels or goes back
- Close dialog, return to previous screen
- **Not**: As primary input (conflicts with escape sequences)
**Rule 5**: Ctrl-L redraws screen
- Useful when terminal gets corrupted
- **Implementation**: Resend entire screen
### Readline Keybindings
Users expect these to work in line editors:
| Key | Function | Origin |
| ------ | --------------------------------- | ------ |
| Ctrl-A | Start of line | Emacs |
| Ctrl-E | End of line | Emacs |
| Ctrl-B | Back one character | Emacs |
| Ctrl-F | Forward one character | Emacs |
| Ctrl-P | Previous line/history | Emacs |
| Ctrl-N | Next line/history | Emacs |
| Ctrl-K | Kill to end of line | Emacs |
| Ctrl-U | Kill entire line | UNIX |
| Ctrl-W | Delete word backward | UNIX |
| Ctrl-D | Delete character forward (or EOF) | UNIX |
| Ctrl-H | Delete character backward | UNIX |
**When to Implement**: Any time you have line editing (command input, search box)
**When to Skip**: Full-screen editors (vim, emacs use their own bindings)
### Color Constraints
**Recommendation**: Stick to 16 ANSI base colors
**Why**:
- Works on all terminals
- Respects user's color scheme
- Avoids unreadable combinations
**Bad**:
```
# Hardcoded RGB colors
\x1b[38;2;255;100;50m # May be unreadable on some backgrounds
```
**Good**:
```
# Base 16 ANSI colors
\x1b[31m # Red (user's terminal defines exact shade)
\x1b[32m # Green
```
**Learning from vim**: Theme files use named colors ("Red", "Blue") that adapt to terminal color scheme.
---
## TUI Architecture Patterns from Proven Programs
### vim: Modal Editing Architecture
**Key Concepts**:
**Modes as State Machine**:
```
Normal Mode → (i) → Insert Mode
↓ (v) ↑ (ESC)
Visual Mode ←←←←←←←←←←←
↓ (:)
Command Mode
```
**Separation of Concerns**:
- **Normal mode**: Navigation and commands
- **Insert mode**: Text input
- **Visual mode**: Selection
- **Command mode**: Ex commands
**Why It Works**:
- Clear mental model
- Composable commands (d3w = delete 3 words)
- Efficient keyboard-only navigation
**Lessons for TUI Developers**:
- State machines clarify complex interactions
- Modal interfaces reduce key combination needs
- Visual feedback for mode (status line)
### tmux: Client-Server Architecture
**Key Concepts**:
**Server Persistence**:
```
Terminal 1 → tmux client →
→ tmux server → sessions → windows → panes
Terminal 2 → tmux client →
```
**Benefits**:
- Sessions survive terminal disconnect
- Multiple clients can attach
- Server manages all state
**Command Prefix (Ctrl-B)**:
- Escapes command mode
- Avoid conflicting with application keys
- User customizable
**Lessons for TUI Developers**:
- Client-server separation enables persistence
- Prefix keys solve key binding conflicts
- Named sessions provide organization
### less: Pager Pattern
**Key Concepts**:
**Lazy Loading**:
- Don't load entire file into memory
- Seek to positions on demand
- Efficient for huge files
**Search and Navigation**:
- `/` to search forward
- `?` to search backward
- `n` / `N` for next/previous match
- `g` / `G` for start/end
**Stateless Display**:
- Each redraw is independent
- No complex state tracking
**Lessons for TUI Developers**:
- Lazy loading enables handling large data
- Consistent search pattern across tools
- Simple state is more robust
### htop: Real-Time Updates
**Key Concepts**:
**Event Loop with Timeout**:
```
loop:
timeout_event = poll_input(timeout=1000ms)
if timeout_event or no_input:
refresh_display()
elif key_pressed:
handle_input(key)
```
**Efficient Redrawing**:
- Only update changed regions
- Diff previous state
- Minimize escape sequences
**Interactive Filtering**:
- Type to filter live
- Immediate visual feedback
- No enter key needed
**Lessons for TUI Developers**:
- Periodic refreshes for real-time data
- Incremental search is discoverable
- Visual feedback for all actions
### zellij: Layout System
**Key Concepts**:
**Layout Definitions**:
- Declarative layout files
- Nested panes and tabs
- Serializable state
**Plugin Architecture (WASM)**:
- Sandboxed extensions
- Language-agnostic
- Safe execution
**Lessons for TUI Developers**:
- Declarative layouts easier than imperative
- Serialization enables session saving
- Plugin systems enable extensibility
---
## Input Handling Implementation
### Switching to Raw Mode
**What Raw Mode Does**:
- Disable line buffering (character-by-character input)
- Disable echo (characters not printed automatically)
- Disable special character processing (Ctrl-C doesn't send SIGINT)
**C Implementation**:
```c
#include <termios.h>
#include <unistd.h>
struct termios orig_termios;
void enable_raw_mode() {
tcgetattr(STDIN_FILENO, &orig_termios);
struct termios raw = orig_termios;
raw.c_lflag &= ~(ECHO | ICANON | ISIG | IEXTEN);
raw.c_iflag &= ~(IXON | ICRNL | BRKINT | INPCK | ISTRIP);
raw.c_oflag &= ~(OPOST);
raw.c_cflag |= (CS8);
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
}
void disable_raw_mode() {
tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios);
}
```
**Rust Implementation**:
```rust
use termios::{Termios, TCSAFLUSH, ECHO, ICANON, tcsetattr};
fn enable_raw_mode() -> std::io::Result<Termios> {
let stdin = 0;
let mut termios = Termios::from_fd(stdin)?;
let orig = termios.clone();
termios.c_lflag &= !(ICANON | ECHO);
tcsetattr(stdin, TCSAFLUSH, &termios)?;
Ok(orig)
}
```
**Python Implementation**:
```python
import tty
import sys
def enable_raw_mode():
tty.setraw(sys.stdin.fileno())
```
**Critical**: Always restore original mode before exit!
### Event Loop Patterns
**Blocking Event Loop** (simple):
```
loop:
key = read_key() # Blocks until keypress
handle_key(key)
redraw_if_needed()
```
**Non-Blocking with Timeout** (for real-time updates):
```
loop:
key = read_key_with_timeout(100ms)
if key:
handle_key(key)
else:
update_realtime_data()
redraw()
```
**Select-Based** (Unix):
```c
#include <sys/select.h>
fd_set readfds;
struct timeval timeout = {.tv_sec = 0, .tv_usec = 100000};
while (running) {
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);
int ret = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &timeout);
if (ret > 0) {
char c = read_char();
handle_input(c);
} else {
// Timeout - update display
update_realtime_data();
}
redraw();
}
```
### Parsing Arrow Keys and Function Keys
**Reading Escape Sequences**:
```
read char:
if char == ESC:
start_sequence = [ESC]
read next char with timeout:
if timeout:
return ESC key
if next == '[':
read until letter:
return parse_csi_sequence()
```
**Common Sequences**:
```
ESC[A → Up
ESC[B → Down
ESC[C → Right
ESC[D → Left
ESC[H → Home
ESC[F → End
ESC[5~ → Page Up
ESC[6~ → Page Down
```
**Example Implementation** (Concept):
```
function read_key():
c = read_char()
if c != ESC:
return c
c = read_char_with_timeout(50ms)
if timeout:
return KEY_ESC
if c == '[':
c = read_char()
match c:
'A': return KEY_UP
'B': return KEY_DOWN
'C': return KEY_RIGHT
'D': return KEY_LEFT
...
```
### Mouse Support
**Enable Mouse Reporting**:
```
# Button press and release
printf "\x1b[?1000h"
# Button press, release, and drag
printf "\x1b[?1002h"
# SGR mouse mode (better format, works beyond column 223)
printf "\x1b[?1006h"
```
**Disable Mouse Reporting**:
```
printf "\x1b[?1000l\x1b[?1002l\x1b[?1006l"
```
**Parse Mouse Events**:
```
SGR format: ESC[<button;col;row[M|m]
M = press
m = release
button values:
0 = left
1 = middle
2 = right
64 = scroll up
65 = scroll down
```
### Handling Resize Events (SIGWINCH)
**Signal Handler**:
```c
#include <signal.h>
#include <sys/ioctl.h>
volatile sig_atomic_t resized = 0;
void handle_sigwinch(int sig) {
resized = 1;
}
int main() {
signal(SIGWINCH, handle_sigwinch);
while (running) {
if (resized) {
struct winsize ws;
ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws);
terminal_width = ws.ws_col;
terminal_height = ws.ws_row;
redraw_all();
resized = 0;
}
// ... event loop
}
}
```
---
## Rendering Strategies
### Alternate Screen Buffer
**What It Does**:
- Saves current terminal content
- Provides blank screen for your TUI
- Restores previous content on exit
**Enable**:
```
printf "\x1b[?1049h" # Switch to alternate screen
printf "\x1b[2J" # Clear screen
printf "\x1b[H" # Move cursor to home
```
**Disable**:
```
printf "\x1b[?1049l" # Switch back to main screen
```
**When to Use**:
- Full-screen TUIs (vim, less, htop)
- User expects to return to previous terminal state
**When Not to Use**:
- Persistent output desired (build progress, test results)
- User might want to scroll back
**Learning from less**:
- Uses alternate screen by default
- `-X` flag disables it (leaves output on screen after quit)
### Double Buffering
**Problem**: Visible flicker when redrawing
**Solution**: Build output in memory, write all at once
**Implementation Concept**:
```
# Bad (flickers)
for row in screen:
print(row)
# Good (double buffered)
buffer = []
for row in screen:
buffer.append(row)
output = "\n".join(buffer)
print(output)
```
**Advanced**: Diff-based rendering
```
previous_screen = current_screen
current_screen = build_new_screen()
diff = compute_diff(previous_screen, current_screen)
apply_diff(diff) # Only update changed cells
```
### Efficient Cursor Movement
**Minimize Escape Sequences**:
**Bad** (many small writes):
```
for each_change:
printf "\x1b[%d;%dH%c" # Move and write one char
```
**Good** (batch adjacent changes):
```
collect changes into runs:
printf "\x1b[%d;%dH%s" # Move once, write string
```
**Relative vs Absolute Movement**:
```
# Absolute (always works)
ESC[5;10H # Move to row 5, col 10
# Relative (shorter when moving nearby)
ESC[3A # Move up 3 rows
ESC[5C # Move right 5 columns
```
### Layout Management
**Fixed Layout**:
```
┌────────────────┬──────────┐
│ │ │
│ Main Area │ Sidebar │
│ │ │
├────────────────┴──────────┤
│ Status Bar │
└───────────────────────────┘
```
**Responsive Layout** (adapt to terminal size):
```
if width < 80:
single_column_layout()
else:
two_column_layout()
if height < 24:
hide_status_bar()
```
**Widget Tree**:
```
Container(vertical)
├─ Header(height=1)
├─ Body(flex=1)
│ ├─ Main(flex=3)
│ └─ Sidebar(flex=1)
└─ Footer(height=1)
```
**Calculate Sizes**:
```
available_height = terminal_height - header - footer
main_width = (available_width * 3) // 4
sidebar_width = available_width - main_width
```
---
## Advanced TUI Topics
### Focus Management
**Concept**: Only one widget receives keyboard input
**Implementation**:
```
class FocusManager:
widgets = [widget1, widget2, widget3]
focused_index = 0
def handle_key(key):
if key == TAB:
focused_index = (focused_index + 1) % len(widgets)
else:
widgets[focused_index].handle_key(key)
```
**Visual Indication**:
- Highlighted border
- Different color
- Cursor position
**Example from htop**: Arrow keys change focused process
### Modal Dialogs
**Pattern**: Overlay on top of main screen
**Implementation**:
```
render_main_screen()
if dialog_open:
render_dialog_overlay()
handle_dialog_input()
else:
handle_main_input()
```
**Drawing Overlay**:
```
# Save screen state
saved_screen = current_screen
# Draw dialog
draw_rectangle(center, size)
draw_shadow()
draw_dialog_content()
# On close
restore(saved_screen)
```
### Tables and Lists with Scrolling
**Virtual Scrolling**:
```
visible_rows = terminal_height - header - footer
viewport_start = scroll_offset
viewport_end = scroll_offset + visible_rows
for i in range(viewport_start, viewport_end):
render_row(data[i])
```
**Scrolling Logic**:
```
if cursor > viewport_end:
scroll_offset += (cursor - viewport_end)
elif cursor < viewport_start:
scroll_offset -= (viewport_start - cursor)
```
**Learning from less**:
- Smooth scrolling
- Search highlights
- Line numbers
---
# Part 4: Common Development Pitfalls & Solutions
## Buffering Issues (Deep Dive)
_(Expanded from earlier)_
### The 8KB Threshold
**Why 8KB?**: Historical constant from libc (`BUFSIZ` typically 8192)
**Problem Scenario**:
```bash
tail -f /var/log/app.log | grep ERROR | your-tool
# your-tool sees nothing until grep accumulates 8KB
```
### Testing for Buffering Issues
**Test Script**:
```bash
#!/bin/bash
# test-buffering.sh
# Simulate slow input
for i in {1..10}; do
echo "Line $i"
sleep 1
done | your-tool
# If tool waits until end, it's block-buffered
# If tool shows each line immediately, it's line-buffered
```
### Line-Buffered Flag Implementation
**Add flag to your tool**:
```
--line-buffered Flush output after each line
```
**C Implementation**:
```c
if (line_buffered) {
setvbuf(stdout, NULL, _IOLBF, 0);
}
// Or manual after each line:
printf("%s\n", line);
if (line_buffered || isatty(STDOUT_FILENO)) {
fflush(stdout);
}
```
---
## Color and Styling Issues
### Detecting Color Support
**Check Multiple Factors**:
```
function should_use_color():
# Check NO_COLOR (user preference)
if getenv("NO_COLOR"):
return false
# Check if stdout is TTY
if not isatty(stdout):
return false
# Check TERM variable
term = getenv("TERM")
if term in ["dumb", "unknown"]:
return false
# Check explicit flag
if color_flag == "never":
return false
if color_flag == "always":
return true
# Default: yes for TTY, no for pipe
return true
```
### 16 Color vs 256 Color vs RGB
**16 Colors** (safest):
```
ESC[31m # Red
ESC[32m # Green
ESC[33m # Yellow
ESC[34m # Blue
```
**256 Colors**:
```
ESC[38;5;COLOR_NUMBERm # Foreground
ESC[48;5;COLOR_NUMBERm # Background
# COLOR_NUMBER: 0-255
```
**RGB (TrueColor)**:
```
ESC[38;2;R;G;Bm # Foreground
ESC[48;2;R;G;Bm # Background
```
**Detection**:
```
# Check for 256-color support
if "256color" in getenv("TERM"):
use_256_colors()
# Check for RGB support
if getenv("COLORTERM") in ["truecolor", "24bit"]:
use_rgb_colors()
```
### Unreadable Color Combinations
**Problem**: Hardcoded colors invisible on some backgrounds
**Solution**: Use semantic colors
```
# Bad
\x1b[38;2;30;30;30m # Dark gray (invisible on dark terminal)
# Good
\x1b[31m # Red (user's terminal defines the shade)
```
**Best Practice**: Let users customize theme, or stick to 16 colors
---
## Signal Handling
### SIGINT (Ctrl-C) Implementation
**Requirements**:
- Exit gracefully
- Clean up resources
- Don't leave terminal in broken state
**C Implementation**:
```c
#include <signal.h>
volatile sig_atomic_t interrupted = 0;
void sigint_handler(int sig) {
interrupted = 1;
}
int main() {
signal(SIGINT, sigint_handler);
while (!interrupted) {
// ... work
}
// Cleanup
cleanup();
exit(130); // 128 + SIGINT (2)
}
```
**Rust Implementation**:
```rust
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
let interrupted = Arc::new(AtomicBool::new(false));
let r = interrupted.clone();
ctrlc::set_handler(move || {
r.store(true, Ordering::SeqCst);
}).expect("Error setting Ctrl-C handler");
while !interrupted.load(Ordering::SeqCst) {
// ... work
}
```
### Multi-Ctrl-C Pattern
**Pattern**: First Ctrl-C = graceful, second Ctrl-C = immediate
**Implementation**:
```
sigint_count = 0
on_sigint:
sigint_count++
if sigint_count == 1:
print("Shutting down gracefully... (Ctrl-C again to force)")
start_graceful_shutdown()
elif sigint_count >= 2:
print("Forcing immediate exit")
_exit(1) # Skip cleanup
```
**Learning from Docker Compose**:
```
$ docker-compose down
Stopping container1 ...
^CGracefully stopping... (press Ctrl+C again to force)
^CForcing shutdown
```
### SIGWINCH (Terminal Resize)
**Requirement**: Redraw when terminal size changes
**Implementation**:
```c
#include <signal.h>
#include <sys/ioctl.h>
volatile sig_atomic_t winch_received = 0;
void sigwinch_handler(int sig) {
winch_received = 1;
}
void get_terminal_size(int *width, int *height) {
struct winsize ws;
ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws);
*width = ws.ws_col;
*height = ws.ws_row;
}
int main() {
signal(SIGWINCH, sigwinch_handler);
while (1) {
if (winch_received) {
winch_received = 0;
get_terminal_size(&width, &height);
redraw_everything();
}
// ...
}
}
```
---
## Terminal State Management
### Saving and Restoring State
**Problem**: If your TUI crashes, terminal is left in broken state
**Solution**: Save state on entry, restore on exit
**Implementation**:
```c
#include <termios.h>
struct termios orig_termios;
int orig_cursor_visible;
void setup_terminal() {
// Save original state
tcgetattr(STDIN_FILENO, &orig_termios);
// Enter raw mode
// ...
// Hide cursor
printf("\x1b[?25l");
// Enter alternate screen
printf("\x1b[?1049h");
}
void restore_terminal() {
// Show cursor
printf("\x1b[?25h");
// Exit alternate screen
printf("\x1b[?1049l");
// Restore original terminal state
tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios);
// Flush output
fflush(stdout);
}
void cleanup_and_exit(int code) {
restore_terminal();
exit(code);
}
```
**Register Cleanup**:
```c
#include <stdlib.h>
int main() {
atexit(restore_terminal);
// Or handle signals
signal(SIGINT, cleanup_signal_handler);
signal(SIGTERM, cleanup_signal_handler);
setup_terminal();
// ... run TUI
}
```
### Crash-Only Design
**Principle**: Minimize cleanup requirements
**Implementation**:
- Don't require cleanup on exit
- Use atomic operations
- Write state to disk continuously
- Can restart from any point
**Example**: Transaction logs
```
Instead of:
load_state()
modify_state()
save_state() # ← If this fails, data lost
Use:
append_operation_to_log() # ← Atomic
replay_log_on_startup()
```
---
## Input Parsing Pitfalls
### Unicode Handling
**Problem**: Multi-byte UTF-8 characters
**Example**: `日本語` is 9 bytes but 3 characters
**Solutions**:
- Use UTF-8 aware string length functions
- Be careful with substring operations
- Terminal width != byte count != character count
**Width Calculation**:
```
# ASCII 'A': 1 byte, 1 character, 1 cell width
# 日: 3 bytes, 1 character, 2 cell width (CJK)
# 👍: 4 bytes, 1 character, 2 cell width (emoji)
```
**Libraries**:
- C: libunistring, ICU
- Rust: unicode-width crate
- Python: wcwidth library
### Distinguishing ESC from Escape Sequences
_(Covered earlier, reiterated for pitfalls)_
**Problem**: `ESC` key vs `ESC[A` (up arrow)
**Solution**: Timeout-based parsing (50-100ms)
**Pitfall**: Timeout too short = arrow keys broken on slow connections
**Pitfall**: Timeout too long = ESC key feels sluggish
---
## Output Corruption
### Interleaved Output from Threads
**Problem**:
```
Thread 1: print("Processing item 1")
Thread 2: print("Processing item 2")
Output: ProceProcessing item 2
ssing item 1
```
**Solution 1**: Mutex around output
```rust
use std::sync::Mutex;
use std::io::{self, Write};
lazy_static! {
static ref STDOUT: Mutex<io::Stdout> = Mutex::new(io::stdout());
}
fn print_safe(msg: &str) {
let mut handle = STDOUT.lock().unwrap();
writeln!(handle, "{}", msg).unwrap();
}
```
**Solution 2**: Channel to single writer thread
```
Worker threads → Channel → Writer thread → stdout
```
### Progress Bars with Concurrent Output
**Problem**: Log messages corrupt progress bar
**Solution**: Clear line, print message, redraw progress
```
function log_message(msg):
clear_current_line()
print(msg)
redraw_progress_bar()
```
**Better**: Use library that handles this (e.g., indicatif for Rust)
---
## Cross-Platform Issues
### Windows Console vs Unix Terminal
**Differences**:
- Windows traditionally didn't support ANSI escape codes
- Different line endings (CRLF vs LF)
- Different path separators (\ vs /)
- Different PTY implementation
**Windows 10+ Improvements**:
- ANSI escape codes now supported
- Must enable with virtual terminal mode:
```c
#include <windows.h>
void enable_ansi_on_windows() {
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
DWORD dwMode = 0;
GetConsoleMode(hOut, &dwMode);
dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
SetConsoleMode(hOut, dwMode);
}
```
**Cross-Platform Abstraction**:
```rust
#[cfg(windows)]
fn setup_terminal() {
enable_virtual_terminal_processing();
}
#[cfg(unix)]
fn setup_terminal() {
enable_raw_mode();
}
```
---
## Performance Pitfalls
### Excessive Terminal Writes
**Problem**: Writing to terminal is slow (syscall overhead)
**Bad**:
```rust
for i in 0..1000 {
println!("{}", i); // 1000 write syscalls
}
```
**Good**:
```rust
let mut buf = String::new();
for i in 0..1000 {
buf.push_str(&format!("{}\n", i));
}
print!("{}", buf); // 1 write syscall
```
### Inefficient Escape Sequences
**Bad**: Redundant sequences
```
ESC[31m R ESC[0m ESC[31m E ESC[0m ESC[31m D ESC[0m
# 15 bytes * 3 = 45 bytes
```
**Good**: Batch coloring
```
ESC[31m RED ESC[0m
# 15 bytes total
```
### Rendering Too Frequently
**Problem**: Redrawing 60 FPS when user only types 1 char/sec
**Solution**: Event-driven updates
```
on_input:
update_state()
redraw()
on_timer:
if has_realtime_data():
update_data()
redraw()
```
**Rate Limiting**:
```
last_draw = now()
on_need_redraw:
if now() - last_draw > 16ms: # ~60 FPS max
redraw()
last_draw = now()
```
---
# Part 5: Development Techniques & Patterns
## Testing Terminal Applications
### Mocking TTY in Tests
**Problem**: Tests don't run in a real TTY
**Solutions**:
**1. PTY (Pseudo-Terminal)**:
**Python with pexpect**:
```python
import pexpect
def test_interactive_prompt():
child = pexpect.spawn('your-tool')
child.expect('Enter name:')
child.sendline('Alice')
child.expect('Hello, Alice!')
child.expect(pexpect.EOF)
```
**Rust with pty crate**:
```rust
#[test]
fn test_tty_detection() {
let pty = pty::fork().unwrap();
if pty.is_parent() {
// Parent process - verify child detected TTY
} else {
// Child process - runs in PTY
assert!(atty::is(atty::Stream::Stdout));
}
}
```
**2. Mock isatty Function**:
```c
// In tests
#define isatty(fd) mock_isatty(fd)
int mock_isatty(int fd) {
return test_wants_tty ? 1 : 0;
}
```
### Snapshot Testing for Output
**Concept**: Record output, compare on future runs
**Tool**: insta (Rust), jest (JavaScript), pytest (Python)
**Example (Rust with insta)**:
```rust
#[test]
fn test_help_output() {
let output = run_command("your-tool --help");
insta::assert_snapshot!(output);
}
```
**First run**: Saves output to snapshot file
**Future runs**: Compares against snapshot
**On change**: Review diff, accept or reject
### Testing Interactive Prompts
**Using expect (traditional Unix tool)**:
```tcl
spawn your-tool
expect "Enter password:"
send "secret123\r"
expect "Login successful"
```
**CI/CD Testing**:
```bash
# Ensure --no-input works
your-tool --no-input --config test.conf
# Should exit with error if interaction required
if your-tool --no-input; then
echo "FAIL: Should require --force"
exit 1
fi
```
---
## Development Tools
### Terminal Recording
**asciinema**: Record and share terminal sessions
```bash
# Record
asciinema rec demo.cast
# Play back
asciinema play demo.cast
# Embed in README
asciinema upload demo.cast
```
**VHS** (by Charm): Script terminal recordings
```bash
# demo.tape
Type "your-tool --help"
Enter
Sleep 2s
Screenshot demo.png
```
### Escape Sequence Debugging
**Technique**: Pipe output to `cat -v`
```bash
your-tool | cat -v
# Shows: Hello ^[[31mworld^[[0m
# (reveals ANSI codes)
```
**Technique**: Use `hexdump`
```bash
your-tool | hexdump -C
```
**Technique**: Enable terminal debugging
```bash
# iTerm2: Session > Log > Start Logging
# Captures all raw input/output
```
### expect for Testing
**Install**:
```bash
# macOS
brew install expect
# Linux
apt-get install expect
```
**Example Test**:
```tcl
#!/usr/bin/expect
spawn your-tool interactive
expect "Enter name:"
send "Alice\r"
expect "Enter age:"
send "30\r"
expect {
"Success" { exit 0 }
timeout { exit 1 }
eof { exit 1 }
}
```
---
## Build and Distribution
### Single Binary Philosophy
**Benefits**:
- Easy installation (just download)
- No dependency hell
- No version conflicts
- Works in containers
**Implementation**:
- Statically link dependencies (Rust, Go do this by default)
- Embed assets at compile time
- Cross-compile for multiple platforms
**Rust Example**:
```toml
# Cargo.toml
[profile.release]
strip = true
lto = true
codegen-units = 1
panic = 'abort'
```
### Cross-Compilation
**Rust**:
```bash
# Install target
rustup target add x86_64-unknown-linux-musl
# Build
cargo build --release --target x86_64-unknown-linux-musl
```
**Go**:
```bash
GOOS=linux GOARCH=amd64 go build
GOOS=darwin GOARCH=arm64 go build
GOOS=windows GOARCH=amd64 go build
```
### Size Optimization
**Techniques**:
- Strip debug symbols
- Enable LTO (Link-Time Optimization)
- Use release mode
- Compress with UPX (controversial)
**Rust**:
```toml
[profile.release]
strip = true
lto = true
opt-level = "z" # Optimize for size
```
---
## Documentation
### Man Page Structure
**Sections**:
```
NAME
tool - one-line description
SYNOPSIS
tool [OPTIONS] FILE...
DESCRIPTION
Detailed description of what the tool does and how to use it.
Multiple paragraphs explaining functionality.
OPTIONS
-h, --help
Print help information
-v, --verbose
Enable verbose output
EXAMPLES
Basic usage:
$ tool input.txt
With options:
$ tool -v input.txt output.txt
ENVIRONMENT
TOOL_CONFIG
Configuration file path
EXIT STATUS
0 Success
1 General error
2 Invalid arguments
SEE ALSO
related-tool(1), another-tool(1)
BUGS
Report bugs to: https://github.com/user/tool/issues
AUTHOR
Written by Your Name.
```
### Learning from git's Help System
**Hierarchical Help**:
```bash
git # Lists common commands
git help # Same as above
git help commit # Detailed help for subcommand
git commit --help # Same as above (opens man page)
git commit -h # Quick reference
```
**Porcelain vs Plumbing**:
- **Porcelain**: User-facing commands (commit, push, pull)
- **Plumbing**: Low-level tools (hash-object, update-index)
**Design Pattern**: Separate user-friendly interface from internal tools
---
# Part 6: Practical Implementation Patterns
## Complete CLI Application Structure
### Project Layout
**Rust**:
```
my-tool/
├── Cargo.toml
├── src/
│ ├── main.rs # Entry point, argument parsing
│ ├── cli.rs # CLI definitions
│ ├── commands/ # Subcommand implementations
│ │ ├── mod.rs
│ │ ├── build.rs
│ │ └── deploy.rs
│ ├── lib.rs # Library code (reusable)
│ ├── error.rs # Error types
│ └── config.rs # Configuration
└── tests/
└── integration_test.rs
```
**Python**:
```
my-tool/
├── pyproject.toml
├── src/
│ └── mytool/
│ ├── __init__.py
│ ├── __main__.py # Entry point
│ ├── cli.py # Argument parsing
│ ├── commands/ # Subcommands
│ │ ├── __init__.py
│ │ ├── build.py
│ │ └── deploy.py
│ └── lib.py # Core logic
└── tests/
└── test_cli.py
```
### Entry Point Design
**Concept**:
```
main():
parse_arguments()
load_config()
setup_logging()
dispatch_to_subcommand()
handle_errors()
exit_with_code()
```
**Implementation Pattern**:
```rust
fn main() {
let result = run();
match result {
Ok(()) => std::process::exit(0),
Err(e) => {
eprintln!("Error: {}", e);
std::process::exit(1);
}
}
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
let args = parse_args()?;
let config = load_config(&args)?;
match args.subcommand {
Subcommand::Build(opts) => commands::build(opts, &config),
Subcommand::Deploy(opts) => commands::deploy(opts, &config),
}
}
```
---
## Argument Parsing Deep Dive
### Getopt Patterns
**Short Options**: `-a -b -c`
**Long Options**: `--all --verbose --config=file`
**Bundling**: `-abc` = `-a -b -c`
**Values**: `-o file` or `-ofile` or `--output file` or `--output=file`
**End of Options**: `--` stops parsing, rest are arguments
**POSIX Conventions**:
- Short options: single dash, single letter
- Option arguments: separated by space or directly attached
- No permutation (options must come before arguments)
**GNU Conventions** (extensions):
- Long options: double dash, full word
- Option arguments: `=` or space
- Permutation (options can be anywhere)
- `--` to stop parsing
### Subcommand Dispatch Pattern
**Learning from git**:
```
git <global-options> <command> <command-options>
Examples:
git --no-pager log --oneline
git -C /path/to/repo status
```
**Implementation Pattern**:
```
parse phase 1: global options
identify subcommand
parse phase 2: subcommand options
dispatch:
match subcommand:
"build": build_command(opts)
"test": test_command(opts)
"deploy": deploy_command(opts)
```
### Learning from docker's Command Structure
**Pattern**: `docker <object> <action> <options>`
```bash
docker container create
docker container start
docker container stop
docker container rm
docker image build
docker image push
docker image pull
```
**Benefits**:
- Clear grouping
- Consistent interface
- Discoverability
---
## Progress & Feedback
### Spinner Implementation (Core Algorithm)
**Frames**:
```
frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
# Or: ["-", "\\", "|", "/"]
```
**Implementation**:
```
frame_index = 0
while working:
clear_line()
print(frames[frame_index % len(frames)] + " Processing...")
frame_index++
sleep(100ms)
clear_line()
print("✓ Done!")
```
**ANSI Codes**:
```
\r # Carriage return (go to start of line)
ESC[K # Clear from cursor to end of line
ESC[?25l # Hide cursor
ESC[?25h # Show cursor
```
### Progress Bar Implementation (Core Algorithm)
**Concept**:
```
[=========> ] 45% (450/1000) 2.5MB/s ETA 5s
```
**Implementation**:
```
width = 20
filled = int(width * (done / total))
bar = "=" * filled + ">" + " " * (width - filled - 1)
percentage = int(100 * done / total)
text = f"[{bar}] {percentage}% ({done}/{total})"
print(f"\r{text}", end="", flush=True)
```
**With Rate and ETA**:
```
elapsed = time_now - start_time
rate = done / elapsed
remaining = total - done
eta = remaining / rate
text += f" {format_bytes(rate)}/s ETA {format_duration(eta)}"
```
### Learning from cargo's Progress Patterns
**Stages**:
```
Updating crates.io index
Compiling serde v1.0.152 (1/10)
Compiling tokio v1.25.0 (2/10)
Finished dev [unoptimized] target(s) in 3.42s
```
**Patterns**:
- Right-aligned verbs (uniform column)
- Package names and versions
- Progress count (2/10)
- Summary with timing
---
## Configuration Management
### Loading Order Implementation
**Pattern** (highest to lowest priority):
```
1. Command-line flags (--config, --output)
2. Environment variables (TOOL_OUTPUT, TOOL_CONFIG)
3. Local config file (./.toolrc)
4. User config file (~/.config/tool/config.toml)
5. System config file (/etc/tool/config.toml)
6. Built-in defaults
```
**Implementation**:
```rust
fn load_config() -> Config {
let mut config = Config::defaults();
if let Some(path) = find_system_config() {
config.merge(load_file(path)?);
}
if let Some(path) = find_user_config() {
config.merge(load_file(path)?);
}
if let Some(path) = find_local_config() {
config.merge(load_file(path)?);
}
config.merge(load_env_vars());
config.merge(parse_cli_flags());
config
}
```
### XDG Base Directory Implementation
```rust
use std::path::PathBuf;
use std::env;
fn get_config_dir() -> PathBuf {
env::var("XDG_CONFIG_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| {
let home = env::var("HOME").expect("HOME not set");
PathBuf::from(home).join(".config")
})
.join("mytool")
}
fn get_data_dir() -> PathBuf {
env::var("XDG_DATA_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| {
let home = env::var("HOME").expect("HOME not set");
PathBuf::from(home).join(".local/share")
})
.join("mytool")
}
fn get_cache_dir() -> PathBuf {
env::var("XDG_CACHE_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| {
let home = env::var("HOME").expect("HOME not set");
PathBuf::from(home).join(".cache")
})
.join("mytool")
}
```
---
## Interactive Features
### Confirmation Prompt Pattern
**Yes/No**:
```
Really delete all files? [y/N]:
```
**Implementation**:
```
if is_tty(stdin):
print("Really delete all files? [y/N]: ", flush=True)
response = read_line()
if response.lower() != 'y':
exit(0)
elif force_flag:
# Proceed
pass
else:
error("Cannot confirm in non-interactive mode. Use --force.")
exit(1)
```
### Menu Selection (Arrow Key Navigation)
**Pattern**:
```
Select an option:
> Option 1
Option 2
Option 3
(Use arrow keys, Enter to select, q to quit)
```
**Implementation Concept**:
```
selected = 0
options = ["Option 1", "Option 2", "Option 3"]
enable_raw_mode()
loop:
clear_screen()
print_menu(options, selected)
key = read_key()
match key:
UP_ARROW:
selected = (selected - 1) % len(options)
DOWN_ARROW:
selected = (selected + 1) % len(options)
ENTER:
return options[selected]
'q':
exit(0)
disable_raw_mode()
```
### Learning from git's Interactive Rebase
**Pattern**: Full-screen editor for multi-item selection
```
pick a1b2c3d First commit
pick d4e5f6g Second commit
pick h7i8j9k Third commit
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous
# d, drop = remove commit
```
**Design Lessons**:
- Use $EDITOR for complex interactions
- Provide help comments inline
- Clear, mnemonic commands
---
## Crash-Only Design
### Atomic Operations
**Pattern**: Operations that are fully complete or fully not done
**Example**: File writes
```
# Bad (non-atomic)
open(file, 'w')
write(data)
close()
# ← If crash here, partial file written
# Good (atomic)
write(file + ".tmp", data)
rename(file + ".tmp", file) # Atomic operation
```
### Resume-able Operations
**Pattern**: Save progress, allow restart
**Example**: Download with resume
```
State file: .download_state.json
{
"url": "...",
"total_bytes": 10000000,
"downloaded_bytes": 5000000,
"chunks": ["chunk1", "chunk2"]
}
On start:
if state_file exists:
resume from state
else:
start fresh
On progress:
update state file
On completion:
remove state file
```
### Transaction Logs
**Pattern**: Write-ahead log (WAL)
**Example**:
```
Before operation:
append to log: "DELETE file.txt"
Perform operation:
delete(file.txt)
After success:
append to log: "COMMITTED"
On crash recovery:
replay_log()
```
---
# Part 7: Technical Reference for Developers
## Argument Parsing Concepts
### POSIX getopt
**Format**:
```c
int getopt(int argc, char *argv[], const char *optstring);
Example optstring: "ab:c::"
a - flag without argument
b: - flag with required argument
c:: - flag with optional argument
```
### GNU getopt_long
**Format**:
```c
struct option {
const char *name; // Long name
int has_arg; // no_argument, required_argument, optional_argument
int *flag; // NULL or pointer to int
int val; // Value to return (or store in *flag)
};
int getopt_long(int argc, char *argv[],
const char *optstring,
const struct option *longopts,
int *longindex);
```
**Example**:
```c
struct option long_options[] = {
{"help", no_argument, 0, 'h'},
{"verbose", no_argument, 0, 'v'},
{"output", required_argument, 0, 'o'},
{0, 0, 0, 0}
};
while ((c = getopt_long(argc, argv, "hvo:", long_options, NULL)) != -1) {
switch (c) {
case 'h': print_help(); break;
case 'v': verbose = 1; break;
case 'o': output_file = optarg; break;
}
}
```
### Option Bundling
**Rule**: `-abc` = `-a -b -c` for flags without arguments
**Implementation**: Most libraries handle automatically
**Limitation**: Can't bundle options with arguments
```bash
# OK
ls -la
# Not OK (ambiguous)
tool -abc file # Is 'c' a flag or does 'b' take 'c' as argument?
```
### Double Dash Convention
**Rule**: `--` stops option parsing
**Usage**:
```bash
# Pass filename that starts with dash
tool -- -weird-filename.txt
# Pass flags to subcommand
tool --verbose -- subcommand --its-own-flag
```
**Implementation**:
```
for arg in args:
if arg == "--":
stop_parsing_options = true
continue
if stop_parsing_options or not arg.startswith("-"):
positional_args.append(arg)
else:
parse_option(arg)
```
---
## Terminal Capabilities
### Terminfo Concepts
**Purpose**: Database of terminal capabilities
**Structure**:
```
Terminal Type (from $TERM)
├─ Boolean Capabilities (am, xenl, etc.)
├─ Numeric Capabilities (cols, lines, colors)
└─ String Capabilities (cup, clear, bold, etc.)
```
**Querying Terminfo**:
```bash
# Show all capabilities for current terminal
infocmp
# Show specific capability
tput bold # Output escape sequence for bold
tput colors # Output number of colors
tput cols # Output terminal width
```
**Using in Code** (C):
```c
#include <term.h>
#include <curses.h>
setupterm(NULL, STDOUT_FILENO, NULL);
char *clear_screen = tigetstr("clear");
char *bold = tigetstr("bold");
printf("%s", clear_screen); // Clear screen
printf("%sHello%s", bold, tparm(tigetstr("sgr0"))); // Bold text
```
### Capability Detection Patterns
**Pattern 1**: Try and fallback
```
try:
output(complex_escape_sequence)
query_terminal_response()
if response == expected:
terminal_supports_feature = true
timeout:
terminal_supports_feature = false
```
**Pattern 2**: Check $TERM
```
if "256color" in $TERM:
use_256_colors = true
if $TERM in ["dumb", "unknown"]:
disable_all_formatting = true
```
**Pattern 3**: Check $COLORTERM
```
if $COLORTERM in ["truecolor", "24bit"]:
use_rgb_colors = true
```
---
## Environment Variables Reference
### Standard Variables
| Variable | Purpose | How to Use |
| -------------- | --------------------------------- | ------------------------------------- |
| NO_COLOR | Disable colors (user preference) | If set (any value), disable colors |
| FORCE_COLOR | Force colors (override detection) | If set, enable colors even in pipes |
| CLICOLOR | Enable colors (0=no, 1=yes) | BSD convention |
| CLICOLOR_FORCE | Force colors (0=no, 1=yes) | BSD convention |
| TERM | Terminal type identifier | "xterm-256color", "screen", "dumb" |
| COLORTERM | Color capability | "truecolor", "24bit" for RGB |
| COLUMNS | Terminal width | Number of columns |
| LINES | Terminal height | Number of rows |
| EDITOR | User's text editor | "vim", "nano", "code" |
| VISUAL | Visual editor (preferred) | Same as EDITOR but for visual editors |
| PAGER | Paging program | "less", "more" |
| SHELL | User's shell | "/bin/bash", "/bin/zsh" |
| HOME | User's home directory | "/home/username" |
| USER | Current username | "alice" |
| TMPDIR | Temporary directory | "/tmp" or "/var/tmp" |
| PATH | Executable search paths | ":/usr/bin:/usr/local/bin:..." |
| LANG | Locale | "en_US.UTF-8" |
| LC_ALL | Locale override | Overrides all LC\_\* variables |
| TZ | Timezone | "America/New_York" |
| CI | Running in CI environment | "true" (GitHub Actions, GitLab CI) |
| DEBUG | Enable debug output | "1" or "true" |
### Naming Your Own Variables
**Convention**: ALL_CAPS with tool name prefix
**Examples**:
```
MYTOOL_CONFIG=/path/to/config
MYTOOL_LOG_LEVEL=debug
MYTOOL_API_KEY=secret
MYTOOL_CACHE_DIR=/tmp/cache
```
**Security**: Never put secrets in environment variables!
- Visible in `ps e` to all users
- Passed to all subprocesses
- Often logged
---
## Exit Codes
### POSIX and Common Codes
| Code | Meaning | Usage |
| ----- | ---------------------- | ------------------------------- |
| 0 | Success | Everything worked |
| 1 | General error | Unspecified failure |
| 2 | Misuse | Invalid arguments or usage |
| 64-78 | Various | /usr/include/sysexits.h |
| 126 | Cannot execute | Permission or exec format error |
| 127 | Command not found | Shell couldn't find the command |
| 128 | Invalid exit code | Exit code out of range |
| 128+N | Killed by signal N | 130 = killed by SIGINT (Ctrl-C) |
| 130 | Terminated by Ctrl-C | Specifically SIGINT |
| 255 | Exit code out of range | Return values capped at 255 |
### Designing Exit Codes
**Strategy**: Define meaningful codes for your application
**Example**:
```
0 - Success
1 - General error
2 - Invalid command-line arguments
10 - File not found
11 - Permission denied
12 - Network error
13 - Timeout
20 - Configuration error
30 - Build failed
31 - Tests failed
```
**Document Them**:
```
EXIT STATUS:
0 Success
1 General error
2 Invalid arguments
10 File not found
11 Permission denied
```
---
## ANSI Escape Sequences
### CSI Sequences (Control Sequence Introducer)
**Format**: `ESC [ <parameters> <command>`
**Cursor Movement**:
```
ESC[H # Move to home (1,1)
ESC[<r>;<c>H # Move to row r, column c
ESC[<n>A # Move up n lines
ESC[<n>B # Move down n lines
ESC[<n>C # Move right n columns
ESC[<n>D # Move left n columns
ESC[<n>E # Move to beginning of line n lines down
ESC[<n>F # Move to beginning of line n lines up
ESC[<n>G # Move to column n
ESC[6n # Query cursor position (response: ESC[<r>;<c>R)
```
**Erasing**:
```
ESC[J # Clear from cursor to end of screen
ESC[1J # Clear from cursor to beginning of screen
ESC[2J # Clear entire screen
ESC[K # Clear from cursor to end of line
ESC[1K # Clear from cursor to beginning of line
ESC[2K # Clear entire line
```
**Scrolling**:
```
ESC[<n>S # Scroll up n lines
ESC[<n>T # Scroll down n lines
```
**SGR (Select Graphic Rendition)** - Colors and Styles:
```
ESC[0m # Reset all attributes
ESC[1m # Bold
ESC[2m # Dim
ESC[3m # Italic
ESC[4m # Underline
ESC[5m # Blinking
ESC[7m # Reverse video
ESC[8m # Hidden
ESC[9m # Strikethrough
# Foreground colors (30-37, 90-97)
ESC[30m # Black
ESC[31m # Red
ESC[32m # Green
ESC[33m # Yellow
ESC[34m # Blue
ESC[35m # Magenta
ESC[36m # Cyan
ESC[37m # White
ESC[90-97m # Bright colors
# Background colors (40-47, 100-107)
ESC[40m # Black background
ESC[41m # Red background
...
# 256 colors
ESC[38;5;<n>m # Foreground (n = 0-255)
ESC[48;5;<n>m # Background
# RGB colors
ESC[38;2;<r>;<g>;<b>m # Foreground
ESC[48;2;<r>;<g>;<b>m # Background
```
### OSC Sequences (Operating System Command)
**Format**: `ESC ] <command> ; <parameters> BEL` or `ESC ] <command> ; <parameters> ESC \`
**Common Uses**:
```
ESC]0;Title\x07 # Set window title
ESC]1;Icon Name\x07 # Set icon name
ESC]2;Window Title\x07 # Set window title (same as 0)
# OSC 52 - Clipboard
ESC]52;c;<base64>\x07 # Copy to clipboard
ESC]52;c;?\x07 # Query clipboard
```
### Private Sequences
**Format**: `ESC [ ? <n> h` (set) or `ESC [ ? <n> l` (reset)
**Common Uses**:
```
ESC[?25h # Show cursor
ESC[?25l # Hide cursor
ESC[?1049h # Use alternate screen buffer
ESC[?1049l # Use main screen buffer
ESC[?1000h # Enable mouse button tracking
ESC[?1002h # Enable mouse button and drag tracking
ESC[?1006h # Enable SGR mouse mode
```
---
## Signal Reference
### Common Signals
| Signal | Number | Default Action | Purpose |
| -------- | ------ | -------------- | ------------------------------ |
| SIGHUP | 1 | Terminate | Hangup (terminal disconnected) |
| SIGINT | 2 | Terminate | Interrupt (Ctrl-C) |
| SIGQUIT | 3 | Core dump | Quit (Ctrl-\) |
| SIGKILL | 9 | Terminate | Kill (cannot be caught) |
| SIGTERM | 15 | Terminate | Termination (polite kill) |
| SIGSTOP | 19 | Stop | Stop (cannot be caught) |
| SIGTSTP | 20 | Stop | Stop (Ctrl-Z) |
| SIGCONT | 18 | Continue | Continue after stop |
| SIGWINCH | 28 | Ignore | Window size change |
### Handling Signals
**C Implementation**:
```c
#include <signal.h>
void sigint_handler(int sig) {
// Cleanup
cleanup();
exit(128 + sig);
}
int main() {
signal(SIGINT, sigint_handler);
signal(SIGTERM, sigint_handler);
// ... run program
}
```
### Platform Differences
**Signal Numbers Vary**:
- SIGWINCH is 28 on Linux, 23 on BSD/macOS
- Use constants (SIGWINCH) not numbers
**Windows**:
- Very limited signal support
- Ctrl-C sends SIGINT
- No SIGWINCH, SIGHUP, etc.
---
# Part 8: Best Practices Summary
## Top 10 Design Principles
1. **Detect TTY, Format Appropriately**
- Colors and progress for humans
- Plain output for pipes
- Provide `--color` override
2. **Separate stdout and stderr**
- Results to stdout
- Errors and diagnostics to stderr
- Enables composability
3. **Handle Buffering Correctly**
- Add `--line-buffered` flag
- Flush after important output
- Test with pipes
4. **Use Standard Flag Names**
- `-h`/`--help`, `--version`, `-v`/`--verbose`, `-q`/`--quiet`
- Users have muscle memory
5. **Provide Excellent Error Messages**
- Say what went wrong
- Suggest how to fix it
- No stack traces by default
6. **Exit with Meaningful Codes**
- 0 for success
- Non-zero for errors
- Document specific codes
7. **Handle Signals Gracefully**
- Ctrl-C should always work
- Clean up terminal state
- Save progress when possible
8. **Support Non-Interactive Mode**
- `--no-input` flag
- `--force` for confirmations
- Critical for CI/CD
9. **Respect User Environment**
- NO_COLOR, EDITOR, PAGER
- XDG Base Directory
- Standard conventions
10. **Test with Real Terminals**
- Different emulators
- Different sizes
- Pipes and redirects
---
## Common Mistakes to Avoid
❌ **Ignoring isatty()** - Always check before colors/progress
❌ **Stack traces for users** - Hide by default, show with --debug
❌ **Secrets in flags** - Use files or prompts instead
❌ **Hanging without feedback** - Show progress or spinner
❌ **Forgetting to flush** - Buffering causes pipes to hang
❌ **Hardcoding RGB colors** - Use 16 ANSI colors for compatibility
❌ **Ignoring SIGINT** - Users expect Ctrl-C to work
❌ **Not restoring terminal state** - Crashes leave broken terminal
❌ **Inconsistent flag names** - Follow standards
❌ **Poor help text** - Include examples
---
## Quick Reference Checklist
**Before releasing your CLI tool**:
- [ ] `-h` shows brief help
- [ ] `--help` shows detailed help
- [ ] `--version` shows version
- [ ] Colors only when TTY (respect NO_COLOR)
- [ ] Progress indicators only when TTY
- [ ] `--no-input` flag works in CI
- [ ] `--json` flag for machine output
- [ ] Exit 0 on success, non-zero on failure
- [ ] Errors go to stderr
- [ ] Results go to stdout
- [ ] Ctrl-C exits gracefully
- [ ] Works in pipes (`tool | grep`, `tool | jq`)
- [ ] Line buffering option for streaming
- [ ] Man page or web docs
- [ ] Examples in help text
- [ ] Clear error messages
- [ ] Single binary (no dependencies)
- [ ] Cross-platform (or clearly document limitations)
- [ ] Tested on major terminals
---
## When to Break the Rules
**Valid reasons**:
- Clear usability improvement
- Domain-specific requirements
- Technical limitations
- Explicitly documented
**Examples**:
- **ripgrep**: Ignores .gitignore by default (breaks UNIX tradition, but better for developers)
- **bat**: Uses full RGB colors (breaks compatibility guideline, but beautiful output is core feature)
- **fzf**: Uses entire screen even for one item (breaks minimalism, but interactive selection is core feature)
**How to break rules**:
1. Document the deviation
2. Provide escape hatch (flag to disable)
3. Have clear rationale
4. Get user feedback
---
# Part 9: When to Ask for Help
Ask the user for clarification or direction when:
## Complex TUI State Management
- Designing widget trees with deep nesting
- Managing focus across multiple panels
- Implementing undo/redo for TUI applications
- Handling modal dialogs with complex interactions
## Platform-Specific Terminal Behavior
- Windows Console API integration
- Handling terminal quirks on specific platforms
- Supporting legacy terminal types
- Cross-platform PTY allocation
## Performance Optimization
- Rendering bottlenecks in large TUIs
- Optimizing large file processing
- Reducing memory usage for streaming data
- Profiling terminal write performance
## Custom Escape Sequence Needs
- Implementing features beyond standard sequences
- Detecting terminal capabilities at runtime
- Falling back when features unsupported
- Working with non-standard terminals
## Testing Strategies
- Mocking complex terminal interactions
- Testing TUI applications in CI
- Snapshot testing for terminal output
- Integration testing with PTYs
## Architecture Decisions
- Choosing between CLI and TUI interfaces
- Client-server architecture for terminal apps
- Plugin systems for CLI tools
- Configuration format selection
## Security Considerations
- Handling sensitive input (passwords, API keys)
- Preventing command injection
- Secure clipboard access
- Sandboxing subprocesses
## Accessibility
- Screen reader compatibility
- High contrast modes
- Keyboard-only navigation
- Alternative text for visual elements
## Advanced Topics
- Building language servers (LSP)
- REPL implementation
- Terminal multiplexer design
- Remote terminal protocols