Agent skill

Terminal Concepts

by shepherdjerred in shepherdjerred/monorepo

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…

GPL-3.0Auto-check passed

Install Terminal Concepts

skills CLI
$ npx skills add shepherdjerred/monorepo --skill terminal-concepts -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install shepherdjerred/monorepo terminal-concepts --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/shepherdjerred/monorepo.git skills-src && mkdir -p .claude/skills && cp -r skills-src/packages/dotfiles/dot_agents/skills/terminal-concepts .claude/skills/terminal-concepts && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
terminal-concepts
GitHub stars
112
Token cost
~23k tokens
SKILL.md length
5,623 words
Files
8 (incl. references)
Skills in repo
63
Repo updated
First seen
Licence
GPL-3.0

At a glance

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…

  • Works in 9 steps: Human-First Design → Simple, Composable Parts → Consistency Across Programs → …
  • SKILL.md covers Overview, What Developers Need to Know, Learning from Proven Programs and Understanding the Terminal Stack, plus 4 more sections
  • Calls git, docker and cargo; reaches github.com; needs TOOL_API_KEY and MYTOOL_API_KEY

What it does

Terminal Concepts is an agent skill from shepherdjerred/monorepo. 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

Its SKILL.md is about 23k tokens, which your agent loads only when the skill is triggered. The skill folder holds 8 other files, including reference files (for example `references/buffering.md`, `references/cli-design.md` and `references/control-characters.md`).

The repository describes itself as: Monorepo for all of my projects. The licence is GPL-3.0.

Example prompts

  • “/terminal-concepts”

Requirements

  • Python 3

Workflow steps

9 steps, taken from the step headings in SKILL.md.

  1. Human-First Design
  2. Simple, Composable Parts
  3. Consistency Across Programs
  4. Saying Just Enough
  5. Ease of Discovery
  6. Conversation as the Norm
  7. Robustness
  8. Empathy
  9. Chaos as Power

What it can do on your machine

Read from SKILL.md and the folder at commit bc57ca5. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • git
    • docker
    • cargo
    • go
    • jq
    • rg
    • heroku
    • docker-compose
    • brew
    • apt-get

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    Hosts in commands or code, which the agent is likely to contact:

    • github.com

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names these keys or tokens, usually read from environment variables:

    • TOOL_API_KEY
    • MYTOOL_API_KEY

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Terminal Concepts loads about 23k tokens when it runs, and up to ~44k if it reads all its reference files. Until then it costs about 79 tokens; SKILL.md has 5,623 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~79
When it runs · the whole SKILL.md, loaded when a task matches
~23k
With references · SKILL.md plus every file in references/, read only if the agent opens them
~44k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from shepherdjerred/monorepo at commit bc57ca5, republished under its GPL-3.0 licence (© shepherdjerred). 5,623 words, ~22,867 tokens.

Download SKILL.mdSave it as .claude/skills/terminal-concepts/SKILL.md (or your agent's skills folder). This skill also uses 7 other files; get the full folder from GitHub.
name
terminal-concepts
description
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

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:

StreamFDPurposeExamples
stdin0Input from user or pipeReading commands, file content
stdout1Primary outputResults, listings, JSON
stderr2Errors, logs, diagnosticsError 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):

KeyASCIINameFunction
Ctrl-C3ETXSends SIGINT (interrupt)
Ctrl-D4EOTEOF when line is empty
Ctrl-Z26SUBSends SIGTSTP (suspend)
Ctrl-S19XOFFFreezes output (flow control)
Ctrl-Q17XONResumes output
Ctrl-\28FSSends SIGQUIT

2. Keyboard Literals:

KeyASCIINameUsage
Enter13CRLine terminator
Tab9HTTab character
Backspace127DELDelete previous character
Ctrl-H8BSOften same as backspace

3. Application-Specific (Your Program Can Define):

KeyCommon Usage
Ctrl-AMove to line start (readline, emacs)
Ctrl-EMove to line end
Ctrl-WDelete word backwards
Ctrl-UDelete line
Ctrl-KKill to end of line
Ctrl-RReverse search (shells)
Ctrl-LClear screen
Ctrl-P/NPrevious/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:

KeySequenceNotes
UpESC[ACSI sequence
DownESC[B
RightESC[C
LeftESC[D
HomeESC[H or ESC[1~Varies by terminal
EndESC[F or ESC[4~
Page UpESC[5~
Page DownESC[6~
F1ESC OP or ESC[[AHighly variable
F12ESC[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:

FlagMeaningExample Usage
-h, --helpShow help (only)tool --help
--versionShow version (only)tool --version
-v, --verboseMore outputtool -v
-q, --quietLess outputtool -q
-f, --forceSkip confirmationsrm -f file
-r, --recursiveRecurse directoriesrm -r dir
-n, --dry-runPreview without executinggit clean -n
-a, --allInclude all itemsgit add -a
-o, --output FILEOutput filegcc -o program
-i, --interactivePrompt for decisionsrm -i file
--no-inputNo interactive promptstool --no-input
-d, --debugDebug outputtool -d
--jsonJSON outputtool --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):

CodeColorCodeColor
30Black40Black background
31Red41Red background
32Green42Green background
33Yellow43Yellow background
34Blue44Blue background
35Magenta45Magenta background
36Cyan46Cyan background
37White47White background
90-97Bright colors100-107Bright 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:

VariablePurposeExample Usage
NO_COLORDisable all colorsCheck before colorizing
EDITORUser's preferred editortool edit opens this
VISUALVisual editor (prefer over EDITOR)Same as EDITOR
PAGERPaging programUse for long output
HOMEUser's home directoryFor config paths
TMPDIRTemporary directoryFor temp files
TERMTerminal typeFor escape sequences
COLUMNSTerminal widthFor formatting
LINESTerminal heightFor paging decisions
CIRunning in CI environmentDisable progress bars
DEBUGEnable debug modeShow 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:

KeyFunctionOrigin
Ctrl-AStart of lineEmacs
Ctrl-EEnd of lineEmacs
Ctrl-BBack one characterEmacs
Ctrl-FForward one characterEmacs
Ctrl-PPrevious line/historyEmacs
Ctrl-NNext line/historyEmacs
Ctrl-KKill to end of lineEmacs
Ctrl-UKill entire lineUNIX
Ctrl-WDelete word backwardUNIX
Ctrl-DDelete character forward (or EOF)UNIX
Ctrl-HDelete character backwardUNIX

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

Show full SKILL.md (2,268 more words)Show less
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
VariablePurposeHow to Use
NO_COLORDisable colors (user preference)If set (any value), disable colors
FORCE_COLORForce colors (override detection)If set, enable colors even in pipes
CLICOLOREnable colors (0=no, 1=yes)BSD convention
CLICOLOR_FORCEForce colors (0=no, 1=yes)BSD convention
TERMTerminal type identifier"xterm-256color", "screen", "dumb"
COLORTERMColor capability"truecolor", "24bit" for RGB
COLUMNSTerminal widthNumber of columns
LINESTerminal heightNumber of rows
EDITORUser's text editor"vim", "nano", "code"
VISUALVisual editor (preferred)Same as EDITOR but for visual editors
PAGERPaging program"less", "more"
SHELLUser's shell"/bin/bash", "/bin/zsh"
HOMEUser's home directory"/home/username"
USERCurrent username"alice"
TMPDIRTemporary directory"/tmp" or "/var/tmp"
PATHExecutable search paths":/usr/bin:/usr/local/bin:..."
LANGLocale"en_US.UTF-8"
LC_ALLLocale overrideOverrides all LC_* variables
TZTimezone"America/New_York"
CIRunning in CI environment"true" (GitHub Actions, GitLab CI)
DEBUGEnable 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
CodeMeaningUsage
0SuccessEverything worked
1General errorUnspecified failure
2MisuseInvalid arguments or usage
64-78Various/usr/include/sysexits.h
126Cannot executePermission or exec format error
127Command not foundShell couldn't find the command
128Invalid exit codeExit code out of range
128+NKilled by signal N130 = killed by SIGINT (Ctrl-C)
130Terminated by Ctrl-CSpecifically SIGINT
255Exit code out of rangeReturn 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
SignalNumberDefault ActionPurpose
SIGHUP1TerminateHangup (terminal disconnected)
SIGINT2TerminateInterrupt (Ctrl-C)
SIGQUIT3Core dumpQuit (Ctrl-)
SIGKILL9TerminateKill (cannot be caught)
SIGTERM15TerminateTermination (polite kill)
SIGSTOP19StopStop (cannot be caught)
SIGTSTP20StopStop (Ctrl-Z)
SIGCONT18ContinueContinue after stop
SIGWINCH28IgnoreWindow 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

© shepherdjerred, GPL-3.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

SKILL.md and 7 other files (references) in packages/dotfiles/dot_agents/skills/terminal-concepts of shepherdjerred/monorepo.

  • SKILL.md
  • references/buffering.md
  • references/cli-design.md
  • references/control-characters.md
  • references/escape-codes.md
  • references/output-and-errors.md
  • references/terminal-fundamentals.md
  • references/tui-development.md

Open the folder on GitHubat commit bc57ca5

Compare with similar skills

Terminal Concepts next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Terminal Concepts compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Terminal Concepts this skillshepherdjerred/monorepo112—~23kAutomated safety check: PassGPL-3.0
Internal Commsalirezarezvani/claude-skills28k—~3.4kAutomated safety check: PassMIT
Internal Communicationsickn33/agentic-awesome-skills47k1 repos~3.4kAutomated safety check: PassMIT
Terminal Openeraffaan-m/ECC276k—~635Automated safety check: PassMIT
Internal Communications Writeranthropics/skills180k38 repos~378Automated safety check: PassApache-2.0
Internal Comms Anthropicsickn33/agentic-awesome-skills47k1 repos~702Automated safety check: PassApache-2.0

Similar skills

  • Internal Comms

    alirezarezvani/claude-skills

    A skill your agent uses when a Head of People Ops, BizOps lead, or Internal Communications owner needs to draft and sequence an internal-only change-management communication — a re-org announcement…

    28k GitHub stars~3.4k tokensUpdated 1 mo ago
    Writing & ContentAuto-check passed
  • Internal Communication

    sickn33/agentic-awesome-skills

    Internal communication log: title, type, date, department, host and attendees, agenda, action items, follow-up date, meeting link and delivery status.

    47k GitHub starsUsed in 1 repo~3.4k tokens
    Writing & ContentAuto-check passed
  • Terminal Opener

    affaan-m/ECC

    Open an executable and its argument array in a visible terminal window through a reusable, shell-free launch plan with dry-run, JSON, capability detection, detached fallback, and standalone recovery…

    276k GitHub stars~635 tokensUpdated 4 days ago
    Marketing & SEOAuto-check passed
  • Official

    A set of resources to help me write all kinds of internal communications, using the formats that my company likes to use. Claude should use this skill…

    180k GitHub starsUsed in 38 repos~378 tokens
    Writing & ContentAuto-check passed
  • Internal Comms Anthropic

    sickn33/agentic-awesome-skills

    Compatibility alias for internal-comms: draft status updates, newsletters and FAQs from approved sources.

    47k GitHub starsUsed in 1 repo~702 tokens
    Writing & ContentAuto-check passed
  • Prototype Openclaw Tui

    openclaw/openclaw

    Build throwaway, fixture-driven OpenClaw Clack or Pi TUI prototypes and compare multiple interactive variants side by side in tmux without running the full application or touching live state.

    392k GitHub stars~1.3k tokensUpdated today
    DevelopmentAuto-check passed

More from shepherdjerred/monorepo

All 63 skills in this repo
  • Bun Runtime Best Practices

    shepherdjerred/monorepo

    Bun runtime APIs and current operational patterns for files, processes, modules, networking, databases, tests, and deployment.

    112 GitHub stars~2k tokensUpdated today
    Auto-check passed
  • Bun Test Patterns

    shepherdjerred/monorepo

    Current Bun test runner guidance for discovery, isolation, parallelism, sharding, changed tests, mocks, timers, snapshots, coverage, DOM Testing Library, and integration teardown.

    112 GitHub stars~2k tokensUpdated today
    Auto-check passed
  • Bun Workspaces

    shepherdjerred/monorepo

    Current Bun workspace guidance for isolated and hoisted linkers, catalogs, filters, scripts, dependency classes, lockfiles, lifecycle trust, caches, publishing, TypeScript package exports, and…

    112 GitHub stars~2.2k tokensUpdated today
    Auto-check passed
  • Deep Research

    shepherdjerred/monorepo

    This skill should be used when the user asks to "deep research", "research this topic", "investigate thoroughly", "do a deep dive on", "comprehensive research on", "find everything about", "survey…

    112 GitHub stars~4.7k tokensUpdated today
    Auto-check: notes
  • Figma Use

    shepherdjerred/monorepo

    This skill should be used when the user asks to "create a Figma design", "design in Figma", "make a Figma mockup", "create an app icon", "design UI", "render JSX to Figma", "export from Figma"…

    112 GitHub stars~946 tokensUpdated today
    Auto-check passed
  • Fish Helper

    shepherdjerred/monorepo

    Current Fish shell scripting, functions, abbreviations, completions, variables, events, configuration, plugins, testing, and safety guidance.

    112 GitHub stars~2k tokensUpdated today
    Auto-check passed

Questions about Terminal Concepts

What does Terminal Concepts 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 Concepts is an agent skill from shepherdjerred/monorepo.

How do I install Terminal Concepts in Claude Code?

Run `npx skills add shepherdjerred/monorepo --skill terminal-concepts -a claude-code`. Or copy the skill folder (packages/dotfiles/dot_agents/skills/terminal-concepts in shepherdjerred/monorepo) into .claude/skills/terminal-concepts in your project. Claude Code loads it when a task matches its description.

How do I install Terminal Concepts in Codex?

Run `npx skills add shepherdjerred/monorepo --skill terminal-concepts -a codex`. Or copy the skill folder (packages/dotfiles/dot_agents/skills/terminal-concepts in shepherdjerred/monorepo) into .agents/skills/terminal-concepts in your project. Codex loads it when a task matches its description.

Can I use Terminal Concepts in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add shepherdjerred/monorepo --skill terminal-concepts -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/terminal-concepts, .gemini/skills/terminal-concepts, .github/skills/terminal-concepts and .opencode/skills/terminal-concepts in your project.

What does Terminal Concepts need to run?

Going by SKILL.md and its folder, Terminal Concepts needs the command-line tools its instructions call (git, docker, cargo, go, jq and rg) and credentials named TOOL_API_KEY and MYTOOL_API_KEY. Our summary lists: Python 3.

Does Terminal Concepts access the network?

SKILL.md names 1 domain. In commands or code: github.com; the agent is likely to contact it when it follows the instructions. This is read from the text; nothing was executed.

Is Terminal Concepts safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Terminal Concepts use?

Terminal Concepts is published under the GPL-3.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Terminal Concepts use?

About 23k tokens (SKILL.md is roughly 91k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full. Its references folder adds about 21k tokens, read only when the agent opens those files.

What are the alternatives to Terminal Concepts?

Skills that share tags, products or a category with Terminal Concepts: Internal Comms (alirezarezvani/claude-skills, 28k stars), Internal Communication (sickn33/agentic-awesome-skills, 47k stars), Terminal Opener (affaan-m/ECC, 276k stars) and Internal Communications Writer (anthropics/skills, 180k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Terminal Concepts?

shepherdjerred (a GitHub user) maintains it in shepherdjerred/monorepo, which has 112 GitHub stars. The repository holds 63 skills in this directory. The repository was last updated on October 9, 2026.

Source: shepherdjerred/monorepo on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.