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…
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…
$ npx skills add shepherdjerred/monorepo --skill terminal-concepts -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install shepherdjerred/monorepo terminal-concepts --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ 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-srcUse ~/.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/
Install the "terminal-concepts" agent skill from https://github.com/shepherdjerred/monorepo/tree/main/packages/dotfiles/dot_agents/skills/terminal-concepts into .claude/skills/terminal-concepts/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "terminal-concepts", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/shepherdjerred/monorepo/tree/main/packages/dotfiles/dot_agents/skills/terminal-conceptsType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add shepherdjerred/monorepo --skill terminal-concepts -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install shepherdjerred/monorepo terminal-concepts --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/shepherdjerred/monorepo.git skills-src && mkdir -p .agents/skills && cp -r skills-src/packages/dotfiles/dot_agents/skills/terminal-concepts .agents/skills/terminal-concepts && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "terminal-concepts" agent skill from https://github.com/shepherdjerred/monorepo/tree/main/packages/dotfiles/dot_agents/skills/terminal-concepts into .agents/skills/terminal-concepts/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "terminal-concepts", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add shepherdjerred/monorepo --skill terminal-concepts -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install shepherdjerred/monorepo terminal-concepts --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/shepherdjerred/monorepo.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/packages/dotfiles/dot_agents/skills/terminal-concepts .cursor/skills/terminal-concepts && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "terminal-concepts" agent skill from https://github.com/shepherdjerred/monorepo/tree/main/packages/dotfiles/dot_agents/skills/terminal-concepts into .cursor/skills/terminal-concepts/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "terminal-concepts", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/shepherdjerred/monorepo.git --path packages/dotfiles/dot_agents/skills/terminal-concepts--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add shepherdjerred/monorepo --skill terminal-concepts -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install shepherdjerred/monorepo terminal-concepts --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/shepherdjerred/monorepo.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/packages/dotfiles/dot_agents/skills/terminal-concepts .gemini/skills/terminal-concepts && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "terminal-concepts" agent skill from https://github.com/shepherdjerred/monorepo/tree/main/packages/dotfiles/dot_agents/skills/terminal-concepts into .gemini/skills/terminal-concepts/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "terminal-concepts", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install shepherdjerred/monorepo terminal-conceptsInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add shepherdjerred/monorepo --skill terminal-concepts -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/shepherdjerred/monorepo.git skills-src && mkdir -p .github/skills && cp -r skills-src/packages/dotfiles/dot_agents/skills/terminal-concepts .github/skills/terminal-concepts && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "terminal-concepts" agent skill from https://github.com/shepherdjerred/monorepo/tree/main/packages/dotfiles/dot_agents/skills/terminal-concepts into .github/skills/terminal-concepts/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "terminal-concepts", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add shepherdjerred/monorepo --skill terminal-concepts -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install shepherdjerred/monorepo terminal-concepts --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/shepherdjerred/monorepo.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/packages/dotfiles/dot_agents/skills/terminal-concepts .opencode/skills/terminal-concepts && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "terminal-concepts" agent skill from https://github.com/shepherdjerred/monorepo/tree/main/packages/dotfiles/dot_agents/skills/terminal-concepts into .opencode/skills/terminal-concepts/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "terminal-concepts", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
terminal-conceptsComprehensive 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. 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.
9 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit bc57ca5. It shows what the files ask for, not the result of running them.
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.
Shell commands in SKILL.md call:
gitdockercargogojqrgherokudocker-composebrewapt-getFrom the folder's file list and the shell code blocks in SKILL.md.
Hosts in commands or code, which the agent is likely to contact:
github.comFrom URLs in SKILL.md, links to its own repository left out.
Names these keys or tokens, usually read from environment variables:
TOOL_API_KEYMYTOOL_API_KEYFrom names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
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.
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.
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.
The full file from shepherdjerred/monorepo at commit bc57ca5, republished under its GPL-3.0 licence (© shepherdjerred). 5,623 words, ~22,867 tokens.
.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.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.
Building terminal applications requires understanding several interconnected concepts:
The best way to understand terminal application design is to study programs that have stood the test of time:
Throughout this guide, we'll reference these programs to illustrate concepts.
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
└─────────────────────────────────┘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:
Why This Matters for Developers:
Cooked Mode (Canonical Mode):
Raw Mode:
When to Use Each:
Every process has three standard streams:
| Stream | FD | Purpose | Examples |
|---|---|---|---|
| stdin | 0 | Input from user or pipe | Reading commands, file content |
| stdout | 1 | Primary output | Results, listings, JSON |
| stderr | 2 | Errors, logs, diagnostics | Error messages, warnings, debug |
Critical Design Principle: Separate stdout and stderr properly.
Why This Matters:
# 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 stderrExamples from Proven Programs:
--message-format=json to stdoutYour 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 TTYgit status: Full output for TTY, shorter for pipesripgrep: Colors and summaries for TTY, plain matches for pipesC Implementation:
#include <unistd.h>
if (isatty(STDOUT_FILENO)) {
// stdout is a TTY
}Rust Implementation:
use std::io::IsTerminal;
if std::io::stdout().is_terminal() {
// stdout is a TTY
}Python Implementation:
import sys
if sys.stdout.isatty():
# stdout is a TTYGo Implementation:
import "golang.org/x/term"
if term.IsTerminal(int(os.Stdout.Fd())) {
// stdout is a TTY
}Control characters are created by holding Ctrl and pressing a key. There are 33 total:
1. OS-Handled (Terminal Driver Intercepts):
| Key | ASCII | Name | Function |
|---|---|---|---|
| Ctrl-C | 3 | ETX | Sends SIGINT (interrupt) |
| Ctrl-D | 4 | EOT | EOF when line is empty |
| Ctrl-Z | 26 | SUB | Sends SIGTSTP (suspend) |
| Ctrl-S | 19 | XOFF | Freezes output (flow control) |
| Ctrl-Q | 17 | XON | Resumes output |
| Ctrl-\ | 28 | FS | Sends SIGQUIT |
2. Keyboard Literals:
| Key | ASCII | Name | Usage |
|---|---|---|---|
| Enter | 13 | CR | Line terminator |
| Tab | 9 | HT | Tab character |
| Backspace | 127 | DEL | Delete previous character |
| Ctrl-H | 8 | BS | Often same as backspace |
3. Application-Specific (Your Program Can Define):
| Key | Common Usage |
|---|---|
| Ctrl-A | Move to line start (readline, emacs) |
| Ctrl-E | Move to line end |
| Ctrl-W | Delete word backwards |
| Ctrl-U | Delete line |
| Ctrl-K | Kill to end of line |
| Ctrl-R | Reverse search (shells) |
| Ctrl-L | Clear screen |
| Ctrl-P/N | Previous/Next (history navigation) |
In Cooked Mode:
In Raw Mode (TUIs):
Best Practice: Respect user expectations. Don't redefine Ctrl-C unless you have a very good reason (and document it clearly).
Unlike GUI applications, terminals have severe limitations:
Implication: Design keyboard shortcuts carefully. You have far fewer options than GUI apps.
Escape codes are invisible character sequences that control terminals. They start with ESC (ASCII 27, written as \x1b, \033, or \e).
Two types:
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 colorOSC (Operating System Command): ESC ] followed by parameters
ESC]0;Title\x07 # Set window title
ESC]52;c;base64\x07 # Clipboard access (OSC 52)XTerm added features beyond ECMA-48:
These aren't formally standardized but are widely supported because xterm is so influential.
A database mapping terminal types to their capabilities:
echo $TERM # xterm-256color, screen-256color, etc.
infocmp $TERM # Dump terminal capabilities
tput bold # Output "bold" escape sequence for $TERMTerminfo Approach:
Hardcoded Approach:
Most modern programs use the hardcoded approach for the subset of widely-supported sequences.
Strategy 1: Stick to well-supported sequences
Strategy 2: Test on major terminal emulators
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:
Use Libraries When:
Use Raw Escape Codes When:
Unbuffered: Every write goes directly to the destination
Line Buffered: Flush on newlines
Block Buffered: Flush when buffer full (~8KB)
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 fileThis is why pipes get stuck!
tail -f log.txt | grep ERROR
# Hangs! grep is waiting for 8KB before flushingWhy:
grep sees stdout is a pipe (not TTY)Solution 1: Add --line-buffered flag
C Implementation:
#include <stdio.h>
if (line_buffered_flag) {
setvbuf(stdout, NULL, _IOLBF, 0);
}
// Or manually flush:
printf("output\n");
fflush(stdout);Rust Implementation:
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:
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.pyGo Implementation:
import (
"bufio"
"os"
)
writer := bufio.NewWriter(os.Stdout)
writer.WriteString("output\n")
writer.Flush() // Manual flushSolution 2: Always flush after important output
Best Practices:
--line-buffered for tools that filter streamsExamples from Proven Programs:
grep --line-buffered: Solves pipe bufferingsed -u: Unbuffered modeawk: Has no built-in flag (common complaint)In CI/CD:
# Force line buffering
stdbuf -oL your-tool | other-tool
# Or use unbuffer (expect package)
unbuffer your-tool | other-toolIn Tests:
When users press special keys, terminals send multi-character escape sequences:
| Key | Sequence | Notes |
|---|---|---|
| Up | ESC[A | CSI sequence |
| Down | ESC[B | |
| Right | ESC[C | |
| Left | ESC[D | |
| Home | ESC[H or ESC[1~ | Varies by terminal |
| End | ESC[F or ESC[4~ | |
| Page Up | ESC[5~ | |
| Page Down | ESC[6~ | |
| F1 | ESC OP or ESC[[A | Highly variable |
| F12 | ESC[24~ |
The Problem: ESC character can mean:
Solution: Timeout-based parsing
Read character:
If ESC:
Wait ~50ms for next character:
If timeout: User pressed ESC
Else: Start of sequence, continue readingProven Programs:
ttimeoutlen)Modern terminals can send mouse events (clicks, drags, scrolls):
ESC[<0;10;5M # Mouse button press at column 10, row 5Enable 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[?1000lUsed by: vim, tmux, less, htop
Based on clig.dev with implementation focus
Principle: CLIs are primarily for humans, not just machines.
Practical Implications:
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.34sClear indication of progress and completion.
Principle: Standard streams, pipes, and exit codes enable composition.
Practical Implications:
Example from ripgrep:
rg "pattern" | rg "filter" | wc -lComposes naturally because stdout contains only matches.
Principle: Follow established conventions.
Practical Implications:
Example from git: Every subcommand uses consistent flags:
git commit --verbose
git log --verbose
git diff --verbosePrinciple: Balance information density. Too little confuses, too much overwhelms.
Guidelines:
Example from docker:
$ docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
a2abf6c4d29d: Pull complete
Status: Downloaded newer image for nginx:latestJust enough to understand progress.
Principle: Users shouldn't need to memorize everything.
Practical Implications:
Example from git:
$ git pul
git: 'pul' is not a git command. See 'git --help'.
The most similar command is
pullPrinciple: Design for iterative use and trial-and-error.
Practical Implications:
Example from git:
$ git status
On branch main
Changes not staged for commit:
modified: file.txt
no changes added to commitAlways shows current state.
Principle: Feel solid, not fragile. Handle errors gracefully.
Practical Implications:
Example from cargo:
$ cargo build
error: Could not compile `myapp` due to 2 previous errorsClear, actionable error.
Principle: Show you're on the user's side.
Practical Implications:
Bad:
Error: Invalid argumentGood (from rustc):
error: unexpected end of file
--> src/main.rs:5:1
|
5 | }
| ^ expected one of 8 possible tokens herePrinciple: Terminal inconsistency enables innovation. Break rules intentionally with purpose.
When to Break Conventions:
Example: ripgrep's default behavior (auto-ignore .gitignore) breaks UNIX tradition but is more useful for developers.
Arguments (Positional Parameters):
cp source destFlags (Named Parameters):
- or --ls --color=auto -lOptions: Sometimes used interchangeably with flags
Use Positional Args When:
cat file.txt, cd /path, rm file1 file2Use Flags When:
git commit --message "msg" --amendPrefer flags over args for anything complex. They're more discoverable and extensible.
Example from git: Heavily flag-based for flexibility
git log --oneline --graph --all --decorate
# Order doesn't matter
git log --all --oneline --decorate --graphFollow these conventions for consistency:
| Flag | Meaning | Example Usage |
|---|---|---|
-h, --help | Show help (only) | tool --help |
--version | Show version (only) | tool --version |
-v, --verbose | More output | tool -v |
-q, --quiet | Less output | tool -q |
-f, --force | Skip confirmations | rm -f file |
-r, --recursive | Recurse directories | rm -r dir |
-n, --dry-run | Preview without executing | git clean -n |
-a, --all | Include all items | git add -a |
-o, --output FILE | Output file | gcc -o program |
-i, --interactive | Prompt for decisions | rm -i file |
--no-input | No interactive prompts | tool --no-input |
-d, --debug | Debug output | tool -d |
--json | JSON output | tool --json |
Don't Repurpose These: Users have muscle memory for these flags.
POSIX Style:
tool -a -b -c # Short flags
tool -abc # Bundled: same as above
tool -o file # Flag with value (space separated)GNU Style:
tool --long-flag # Long flags
tool --output=file # Equals-separated value
tool --output file # Space-separated value
tool -a --long -b # Mixed short and longModern Best Practice: Support both
-la for -l -a)= and space for valuesExample from git:
git commit -m "msg" # POSIX short
git commit --message="msg" # GNU long with =
git commit --message "msg" # GNU long with spaceThree Levels of Danger:
1. Low (Reversible):
rm file.txt (can restore from trash)2. Medium (Significant Impact):
rm -r dir (should prompt or need -f)3. High (Destructive/Widespread):
$ 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... doneFor Scripts: Always provide --force or --confirm=VALUE to bypass prompts
tool --force # Bypass all confirmations
tool --confirm="dangerous" # Confirm with specific valuePrinciple: Users shouldn't need to remember flag order.
Support All These:
tool subcommand --flag value arg
tool --flag value subcommand arg
tool --flag value arg subcommandExample from git (all equivalent):
git --no-pager log --oneline
git log --oneline --no-pagerExample from docker (noun-verb pattern):
docker container rm --force nginx
docker container rm nginx --forceNever:
tool --password secret123 # Visible in ps, shell historyInstead:
# 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 toolWhy: ps aux shows all flags to all users. Shell history is often world-readable.
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 --helpFull 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/issuesPrinciple: 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.
Generate help from the same source as argument parsing:
Benefits:
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.
When to Provide:
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 URLGeneration Tools:
help2man: Auto-generate from --help outputronn: Markdown to man pagescdoc: Simple man page formatasciidoc: Comprehensive documentation systemExample from git: Extensive man pages for every subcommand
man git-commit
man git-rebaseCore Pattern:
if is_tty(stdout):
format = HumanReadable(colors=True, progress=True)
else:
format = MachineReadable(colors=False, progress=False)Implementation (shown earlier, repeated for context):
isatty() system callOverride Flags:
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:
ls --color=auto # Default on many systemsRespect NO_COLOR Environment Variable:
if getenv("NO_COLOR"):
disable_all_colors()
elif not is_tty(stdout):
disable_all_colors()
elif color_flag == "never":
disable_all_colors()
else:
enable_colors()16 ANSI Colors (Safest):
| Code | Color | Code | Color |
|---|---|---|---|
| 30 | Black | 40 | Black background |
| 31 | Red | 41 | Red background |
| 32 | Green | 42 | Green background |
| 33 | Yellow | 43 | Yellow background |
| 34 | Blue | 44 | Blue background |
| 35 | Magenta | 45 | Magenta background |
| 36 | Cyan | 46 | Cyan background |
| 37 | White | 47 | White background |
| 90-97 | Bright colors | 100-107 | Bright backgrounds |
Usage:
ESC[31m red text ESC[0m # Red foreground
ESC[1;31m bold red ESC[0m # Bold red
ESC[0m # Reset all attributesExample Code (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:
Pattern: Provide --json flag for structured output
Design:
# 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:
Example (streaming):
$ tool process --json
{"type":"start","count":100}
{"type":"progress","done":50,"total":100}
{"type":"complete","duration":1.5}Learning from cargo:
cargo build --message-format=jsonOutputs JSON for tooling integration.
When to Show:
When to Hide:
--quiet flagCI 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.42sClear progress with meaningful stages.
When to Use Pager:
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 exitExample from git:
git log # Automatically pages long outputDisable When Needed:
git --no-pager log # Don't pageBad:
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.tomlPrinciples:
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: ...")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/dimEnd 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.
POSIX Conventions:
0: Success1: General error2: Misuse (invalid arguments)126: Command found but not executable127: Command not found128+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 errorDocument them:
EXIT CODES:
0 Success
1 General error
2 Invalid arguments
10 File not foundWhy They Matter: Scripts check exit codes
if tool process file.txt; then
echo "Success"
else
echo "Failed with code $?"
fiSimple 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: _Requirement: Don't display password as user types
C Implementation:
#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:
import getpass
password = getpass.getpass("Password: ")Rust:
use rpassword::read_password;
println!("Password: ");
let password = read_password().unwrap();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:
# Interactive (prompts for confirmation)
tool deploy
# CI/CD (fails without --force)
tool deploy --no-input --forceLoad Order (highest to lowest priority):
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())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)Standard Variables:
| Variable | Purpose | Example Usage |
|---|---|---|
| NO_COLOR | Disable all colors | Check before colorizing |
| EDITOR | User's preferred editor | tool edit opens this |
| VISUAL | Visual editor (prefer over EDITOR) | Same as EDITOR |
| PAGER | Paging program | Use for long output |
| HOME | User's home directory | For config paths |
| TMPDIR | Temporary directory | For temp files |
| TERM | Terminal type | For escape sequences |
| COLUMNS | Terminal width | For formatting |
| LINES | Terminal height | For paging decisions |
| CI | Running in CI environment | Disable progress bars |
| DEBUG | Enable debug mode | Show verbose output |
Your Own Variables:
TOOL_CONFIG=/path/to/config
TOOL_API_KEY=secret123
TOOL_LOG_LEVEL=debugNaming Convention: ALL_CAPS, prefix with tool name
Rule 1: 'q' quits the program
Rule 2: Ctrl-D quits REPLs
Rule 3: Ctrl-C should exit or interrupt
Rule 4: ESC cancels or goes back
Rule 5: Ctrl-L redraws screen
Users expect these to work in line editors:
| Key | Function | Origin |
|---|---|---|
| Ctrl-A | Start of line | Emacs |
| Ctrl-E | End of line | Emacs |
| Ctrl-B | Back one character | Emacs |
| Ctrl-F | Forward one character | Emacs |
| Ctrl-P | Previous line/history | Emacs |
| Ctrl-N | Next line/history | Emacs |
| Ctrl-K | Kill to end of line | Emacs |
| Ctrl-U | Kill entire line | UNIX |
| Ctrl-W | Delete word backward | UNIX |
| Ctrl-D | Delete character forward (or EOF) | UNIX |
| Ctrl-H | Delete character backward | UNIX |
When to Implement: Any time you have line editing (command input, search box)
When to Skip: Full-screen editors (vim, emacs use their own bindings)
Recommendation: Stick to 16 ANSI base colors
Why:
Bad:
# Hardcoded RGB colors
\x1b[38;2;255;100;50m # May be unreadable on some backgroundsGood:
# Base 16 ANSI colors
\x1b[31m # Red (user's terminal defines exact shade)
\x1b[32m # GreenLearning from vim: Theme files use named colors ("Red", "Blue") that adapt to terminal color scheme.
Key Concepts:
Modes as State Machine:
Normal Mode → (i) → Insert Mode
↓ (v) ↑ (ESC)
Visual Mode ←←←←←←←←←←←
↓ (:)
Command ModeSeparation of Concerns:
Why It Works:
Lessons for TUI Developers:
Key Concepts:
Server Persistence:
Terminal 1 → tmux client →
→ tmux server → sessions → windows → panes
Terminal 2 → tmux client →Benefits:
Command Prefix (Ctrl-B):
Lessons for TUI Developers:
Key Concepts:
Lazy Loading:
Search and Navigation:
/ to search forward? to search backwardn / N for next/previous matchg / G for start/endStateless Display:
Lessons for TUI Developers:
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:
Interactive Filtering:
Lessons for TUI Developers:
Key Concepts:
Layout Definitions:
Plugin Architecture (WASM):
Lessons for TUI Developers:
What Raw Mode Does:
C Implementation:
#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:
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:
import tty
import sys
def enable_raw_mode():
tty.setraw(sys.stdin.fileno())Critical: Always restore original mode before exit!
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):
#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();
}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 DownExample 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
...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 downSignal Handler:
#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
}
}What It Does:
Enable:
printf "\x1b[?1049h" # Switch to alternate screen
printf "\x1b[2J" # Clear screen
printf "\x1b[H" # Move cursor to homeDisable:
printf "\x1b[?1049l" # Switch back to main screenWhen to Use:
When Not to Use:
Learning from less:
-X flag disables it (leaves output on screen after quit)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 cellsMinimize Escape Sequences:
Bad (many small writes):
for each_change:
printf "\x1b[%d;%dH%c" # Move and write one charGood (batch adjacent changes):
collect changes into runs:
printf "\x1b[%d;%dH%s" # Move once, write stringRelative 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 columnsFixed 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_widthConcept: 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:
Example from htop: Arrow keys change focused process
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)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:
(Expanded from earlier)
Why 8KB?: Historical constant from libc (BUFSIZ typically 8192)
Problem Scenario:
tail -f /var/log/app.log | grep ERROR | your-tool
# your-tool sees nothing until grep accumulates 8KBTest Script:
#!/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-bufferedAdd flag to your tool:
--line-buffered Flush output after each lineC Implementation:
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);
}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 true16 Colors (safest):
ESC[31m # Red
ESC[32m # Green
ESC[33m # Yellow
ESC[34m # Blue256 Colors:
ESC[38;5;COLOR_NUMBERm # Foreground
ESC[48;5;COLOR_NUMBERm # Background
# COLOR_NUMBER: 0-255RGB (TrueColor):
ESC[38;2;R;G;Bm # Foreground
ESC[48;2;R;G;Bm # BackgroundDetection:
# 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()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
Requirements:
C Implementation:
#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:
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
}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 cleanupLearning from Docker Compose:
$ docker-compose down
Stopping container1 ...
^CGracefully stopping... (press Ctrl+C again to force)
^CForcing shutdownRequirement: Redraw when terminal size changes
Implementation:
#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();
}
// ...
}
}Problem: If your TUI crashes, terminal is left in broken state
Solution: Save state on entry, restore on exit
Implementation:
#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:
#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
}Principle: Minimize cleanup requirements
Implementation:
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()Problem: Multi-byte UTF-8 characters
Example: 日本語 is 9 bytes but 3 characters
Solutions:
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:
(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
Problem:
Thread 1: print("Processing item 1")
Thread 2: print("Processing item 2")
Output: ProceProcessing item 2
ssing item 1Solution 1: Mutex around output
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 → stdoutProblem: 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)
Differences:
Windows 10+ Improvements:
#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:
#[cfg(windows)]
fn setup_terminal() {
enable_virtual_terminal_processing();
}
#[cfg(unix)]
fn setup_terminal() {
enable_raw_mode();
}Problem: Writing to terminal is slow (syscall overhead)
Bad:
for i in 0..1000 {
println!("{}", i); // 1000 write syscalls
}Good:
let mut buf = String::new();
for i in 0..1000 {
buf.push_str(&format!("{}\n", i));
}
print!("{}", buf); // 1 write syscallBad: Redundant sequences
ESC[31m R ESC[0m ESC[31m E ESC[0m ESC[31m D ESC[0m
# 15 bytes * 3 = 45 bytesGood: Batch coloring
ESC[31m RED ESC[0m
# 15 bytes totalProblem: 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()Problem: Tests don't run in a real TTY
Solutions:
1. PTY (Pseudo-Terminal):
Python with pexpect:
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:
#[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:
// In tests
#define isatty(fd) mock_isatty(fd)
int mock_isatty(int fd) {
return test_wants_tty ? 1 : 0;
}Concept: Record output, compare on future runs
Tool: insta (Rust), jest (JavaScript), pytest (Python)
Example (Rust with insta):
#[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
Using expect (traditional Unix tool):
spawn your-tool
expect "Enter password:"
send "secret123\r"
expect "Login successful"CI/CD Testing:
# 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
fiasciinema: Record and share terminal sessions
# Record
asciinema rec demo.cast
# Play back
asciinema play demo.cast
# Embed in README
asciinema upload demo.castVHS (by Charm): Script terminal recordings
# demo.tape
Type "your-tool --help"
Enter
Sleep 2s
Screenshot demo.pngTechnique: Pipe output to cat -v
your-tool | cat -v
# Shows: Hello ^[[31mworld^[[0m
# (reveals ANSI codes)Technique: Use hexdump
your-tool | hexdump -CTechnique: Enable terminal debugging
# iTerm2: Session > Log > Start Logging
# Captures all raw input/outputInstall:
# macOS
brew install expect
# Linux
apt-get install expectExample Test:
#!/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 }
}Benefits:
Implementation:
Rust Example:
# Cargo.toml
[profile.release]
strip = true
lto = true
codegen-units = 1
panic = 'abort'Rust:
# Install target
rustup target add x86_64-unknown-linux-musl
# Build
cargo build --release --target x86_64-unknown-linux-muslGo:
GOOS=linux GOARCH=amd64 go build
GOOS=darwin GOARCH=arm64 go build
GOOS=windows GOARCH=amd64 go buildTechniques:
Rust:
[profile.release]
strip = true
lto = true
opt-level = "z" # Optimize for sizeSections:
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.Hierarchical Help:
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 referencePorcelain vs Plumbing:
Design Pattern: Separate user-friendly interface from internal tools
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.rsPython:
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.pyConcept:
main():
parse_arguments()
load_config()
setup_logging()
dispatch_to_subcommand()
handle_errors()
exit_with_code()Implementation Pattern:
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),
}
}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:
GNU Conventions (extensions):
= or space-- to stop parsingLearning from git:
git <global-options> <command> <command-options>
Examples:
git --no-pager log --oneline
git -C /path/to/repo statusImplementation 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)Pattern: docker <object> <action> <options>
docker container create
docker container start
docker container stop
docker container rm
docker image build
docker image push
docker image pullBenefits:
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 cursorConcept:
[=========> ] 45% (450/1000) 2.5MB/s ETA 5sImplementation:
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)}"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.42sPatterns:
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 defaultsImplementation:
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
}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")
}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)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()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 commitDesign Lessons:
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 operationPattern: 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 filePattern: 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()Format:
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 argumentFormat:
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:
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;
}
}Rule: -abc = -a -b -c for flags without arguments
Implementation: Most libraries handle automatically
Limitation: Can't bundle options with arguments
# OK
ls -la
# Not OK (ambiguous)
tool -abc file # Is 'c' a flag or does 'b' take 'c' as argument?Rule: -- stops option parsing
Usage:
# Pass filename that starts with dash
tool -- -weird-filename.txt
# Pass flags to subcommand
tool --verbose -- subcommand --its-own-flagImplementation:
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)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:
# 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 widthUsing in Code (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 textPattern 1: Try and fallback
try:
output(complex_escape_sequence)
query_terminal_response()
if response == expected:
terminal_supports_feature = true
timeout:
terminal_supports_feature = falsePattern 2: Check $TERM
if "256color" in $TERM:
use_256_colors = true
if $TERM in ["dumb", "unknown"]:
disable_all_formatting = truePattern 3: Check $COLORTERM
if $COLORTERM in ["truecolor", "24bit"]:
use_rgb_colors = true| Variable | Purpose | How to Use |
|---|---|---|
| NO_COLOR | Disable colors (user preference) | If set (any value), disable colors |
| FORCE_COLOR | Force colors (override detection) | If set, enable colors even in pipes |
| CLICOLOR | Enable colors (0=no, 1=yes) | BSD convention |
| CLICOLOR_FORCE | Force colors (0=no, 1=yes) | BSD convention |
| TERM | Terminal type identifier | "xterm-256color", "screen", "dumb" |
| COLORTERM | Color capability | "truecolor", "24bit" for RGB |
| COLUMNS | Terminal width | Number of columns |
| LINES | Terminal height | Number of rows |
| EDITOR | User's text editor | "vim", "nano", "code" |
| VISUAL | Visual editor (preferred) | Same as EDITOR but for visual editors |
| PAGER | Paging program | "less", "more" |
| SHELL | User's shell | "/bin/bash", "/bin/zsh" |
| HOME | User's home directory | "/home/username" |
| USER | Current username | "alice" |
| TMPDIR | Temporary directory | "/tmp" or "/var/tmp" |
| PATH | Executable search paths | ":/usr/bin:/usr/local/bin:..." |
| LANG | Locale | "en_US.UTF-8" |
| LC_ALL | Locale override | Overrides all LC_* variables |
| TZ | Timezone | "America/New_York" |
| CI | Running in CI environment | "true" (GitHub Actions, GitLab CI) |
| DEBUG | Enable debug output | "1" or "true" |
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/cacheSecurity: Never put secrets in environment variables!
ps e to all users| Code | Meaning | Usage |
|---|---|---|
| 0 | Success | Everything worked |
| 1 | General error | Unspecified failure |
| 2 | Misuse | Invalid arguments or usage |
| 64-78 | Various | /usr/include/sysexits.h |
| 126 | Cannot execute | Permission or exec format error |
| 127 | Command not found | Shell couldn't find the command |
| 128 | Invalid exit code | Exit code out of range |
| 128+N | Killed by signal N | 130 = killed by SIGINT (Ctrl-C) |
| 130 | Terminated by Ctrl-C | Specifically SIGINT |
| 255 | Exit code out of range | Return values capped at 255 |
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 failedDocument Them:
EXIT STATUS:
0 Success
1 General error
2 Invalid arguments
10 File not found
11 Permission deniedFormat: 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 lineScrolling:
ESC[<n>S # Scroll up n lines
ESC[<n>T # Scroll down n linesSGR (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 # BackgroundFormat: 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 clipboardFormat: 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 | Number | Default Action | Purpose |
|---|---|---|---|
| SIGHUP | 1 | Terminate | Hangup (terminal disconnected) |
| SIGINT | 2 | Terminate | Interrupt (Ctrl-C) |
| SIGQUIT | 3 | Core dump | Quit (Ctrl-) |
| SIGKILL | 9 | Terminate | Kill (cannot be caught) |
| SIGTERM | 15 | Terminate | Termination (polite kill) |
| SIGSTOP | 19 | Stop | Stop (cannot be caught) |
| SIGTSTP | 20 | Stop | Stop (Ctrl-Z) |
| SIGCONT | 18 | Continue | Continue after stop |
| SIGWINCH | 28 | Ignore | Window size change |
C Implementation:
#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
}Signal Numbers Vary:
Windows:
Detect TTY, Format Appropriately
--color overrideSeparate stdout and stderr
Handle Buffering Correctly
--line-buffered flagUse Standard Flag Names
-h/--help, --version, -v/--verbose, -q/--quietProvide Excellent Error Messages
Exit with Meaningful Codes
Handle Signals Gracefully
Support Non-Interactive Mode
--no-input flag--force for confirmationsRespect User Environment
Test with Real Terminals
❌ 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
Before releasing your CLI tool:
-h shows brief help--help shows detailed help--version shows version--no-input flag works in CI--json flag for machine outputtool | grep, tool | jq)Valid reasons:
Examples:
How to break rules:
Ask the user for clarification or direction when:
© 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
SKILL.md and 7 other files (references) in packages/dotfiles/dot_agents/skills/terminal-concepts of shepherdjerred/monorepo.
Open the folder on GitHubat commit bc57ca5
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.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Terminal Concepts this skillshepherdjerred/monorepo | 112 | — | ~23k | Automated safety check: Pass | GPL-3.0 | |
| Internal Commsalirezarezvani/claude-skills | 28k | — | ~3.4k | Automated safety check: Pass | MIT | |
| Internal Communicationsickn33/agentic-awesome-skills | 47k | 1 repos | ~3.4k | Automated safety check: Pass | MIT | |
| Terminal Openeraffaan-m/ECC | 276k | — | ~635 | Automated safety check: Pass | MIT | |
| Internal Communications Writeranthropics/skills | 180k | 38 repos | ~378 | Automated safety check: Pass | Apache-2.0 | |
| Internal Comms Anthropicsickn33/agentic-awesome-skills | 47k | 1 repos | ~702 | Automated safety check: Pass | Apache-2.0 |
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…
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.
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…
anthropics/skills
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…
sickn33/agentic-awesome-skills
Compatibility alias for internal-comms: draft status updates, newsletters and FAQs from approved sources.
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.
shepherdjerred/monorepo
Bun runtime APIs and current operational patterns for files, processes, modules, networking, databases, tests, and deployment.
shepherdjerred/monorepo
Current Bun test runner guidance for discovery, isolation, parallelism, sharding, changed tests, mocks, timers, snapshots, coverage, DOM Testing Library, and integration teardown.
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…
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…
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"…
shepherdjerred/monorepo
Current Fish shell scripting, functions, abbreviations, completions, variables, events, configuration, plugins, testing, and safety guidance.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.