tmux-tui-testing
Test TUI (Text User Interface) applications using tmux. Use this skill when you need to automate testing of terminal-based applications by sending keystrokes and capturing pane output.
SKILL.md
| Name | tmux-tui-testing |
| Description | Test TUI (Text User Interface) applications using tmux. Use this skill when you need to automate testing of terminal-based applications by sending keystrokes and capturing pane output. |
name: tmux-tui-testing description: Test TUI (Text User Interface) applications using tmux. Use this skill when you need to automate testing of terminal-based applications by sending keystrokes and capturing pane output.
<objective> Automate testing of TUI applications by controlling them through tmux sessions. This enables programmatic interaction with terminal apps that render visual interfaces, allowing you to send input, wait for rendering, and assert on captured output. </objective><essential_principles>
Why tmux for TUI Testing
TUI applications render to a terminal rather than producing structured output. tmux provides:
- Detached sessions for headless testing
- Precise window/pane sizing for consistent layouts
- Keystroke injection via
send-keys - Pane content capture with
capture-pane - ANSI sequence preservation for color/style assertions
Critical Timing Considerations
TUI apps need time to:
- Start up and initialize
- Render after receiving input
- Complete async operations
Always add appropriate waits between actions. Start with 0.5-1s delays and adjust based on app behavior. </essential_principles>
<core_commands>
Session Management
# Create detached session
tmux new-session -d -s "$SESSION_NAME"
# Create named window in session
tmux new-window -t "$SESSION_NAME" -n "$WINDOW_NAME"
# Force specific dimensions (critical for consistent TUI rendering)
tmux resize-window -t "$SESSION_NAME:$WINDOW_NAME" -x $WIDTH -y $HEIGHT
# Kill session when done
tmux kill-session -t "$SESSION_NAME"
Sending Input
# Send command + Enter
tmux send-keys -t "$SESSION_NAME:$WINDOW_NAME" "your-command" Enter
# Send special keys
tmux send-keys -t "$SESSION_NAME:$WINDOW_NAME" Up # Arrow up
tmux send-keys -t "$SESSION_NAME:$WINDOW_NAME" Down # Arrow down
tmux send-keys -t "$SESSION_NAME:$WINDOW_NAME" Tab # Tab
tmux send-keys -t "$SESSION_NAME:$WINDOW_NAME" Escape # Escape
tmux send-keys -t "$SESSION_NAME:$WINDOW_NAME" C-c # Ctrl+C
tmux send-keys -t "$SESSION_NAME:$WINDOW_NAME" C-d # Ctrl+D
tmux send-keys -t "$SESSION_NAME:$WINDOW_NAME" Space # Space
tmux send-keys -t "$SESSION_NAME:$WINDOW_NAME" BSpace # Backspace
# Send literal text (no special key interpretation)
tmux send-keys -t "$SESSION_NAME:$WINDOW_NAME" -l "literal text"
Capturing Output
# Capture visible pane content (plain text)
tmux capture-pane -t "$SESSION_NAME:$WINDOW_NAME" -p
# Capture with ANSI escape sequences (colors, styles)
tmux capture-pane -t "$SESSION_NAME:$WINDOW_NAME" -p -e
# Capture including scrollback history
tmux capture-pane -t "$SESSION_NAME:$WINDOW_NAME" -p -S -1000
</core_commands>
<process> ## Testing Workflow- Setup: Create detached tmux session with fixed dimensions
- Launch: Start the TUI application in the session
- Wait: Allow time for app to initialize and render
- Interact: Send keystrokes to navigate/interact
- Wait: Allow time for UI to update after each interaction
- Capture: Get pane contents
- Assert: Check captured output for expected content
- Cleanup: Kill the tmux session
Example Test Sequence
SESSION="tui-test-$$"
WIDTH=80
HEIGHT=24
# Setup
tmux new-session -d -s "$SESSION"
tmux resize-window -t "$SESSION" -x $WIDTH -y $HEIGHT
# Launch app
tmux send-keys -t "$SESSION" "./my-tui-app" Enter
sleep 1 # Wait for startup
# Interact
tmux send-keys -t "$SESSION" Down Down Enter
sleep 0.5 # Wait for render
# Capture and check
OUTPUT=$(tmux capture-pane -t "$SESSION" -p)
if echo "$OUTPUT" | grep -q "Expected Text"; then
echo "PASS: Found expected content"
else
echo "FAIL: Expected content not found"
echo "Captured output:"
echo "$OUTPUT"
fi
# Cleanup
tmux kill-session -t "$SESSION"
</process>
<common_patterns>
Pattern: Wait for Specific Text
wait_for_text() {
local session="$1"
local text="$2"
local timeout="${3:-10}"
local elapsed=0
while [ $elapsed -lt $timeout ]; do
if tmux capture-pane -t "$session" -p | grep -q "$text"; then
return 0
fi
sleep 0.2
elapsed=$((elapsed + 1))
done
return 1
}
# Usage
if wait_for_text "$SESSION" "Ready"; then
echo "App is ready"
fi
Pattern: Snapshot Comparison
# Capture baseline
tmux capture-pane -t "$SESSION" -p > expected.txt
# Later, compare
tmux capture-pane -t "$SESSION" -p > actual.txt
diff expected.txt actual.txt
Pattern: Test Multiple Scenarios
run_test() {
local name="$1"
local keys="$2"
local expected="$3"
# Reset to known state
tmux send-keys -t "$SESSION" C-c
sleep 0.3
tmux send-keys -t "$SESSION" "./app" Enter
sleep 1
# Execute test
tmux send-keys -t "$SESSION" $keys
sleep 0.5
# Verify
if tmux capture-pane -t "$SESSION" -p | grep -q "$expected"; then
echo "PASS: $name"
else
echo "FAIL: $name"
fi
}
run_test "Navigate down" "Down Down" "Item 3"
run_test "Select item" "Enter" "Selected:"
</common_patterns>
<advanced_patterns>
Mouse Events
# Enable mouse support in tmux (add to session or tmux.conf)
tmux set-option -t "$SESSION" mouse on
# Click at specific coordinates (x, y from top-left)
# Uses escape sequence: \e[<button;x;y;M for press, m for release
send_mouse_click() {
local session="$1"
local x="$2"
local y="$3"
# Send mouse press at (x, y) - button 0 = left click
printf '\e[<0;%d;%dM' "$x" "$y" | tmux load-buffer -
tmux paste-buffer -t "$session"
# Send mouse release
printf '\e[<0;%d;%dm' "$x" "$y" | tmux load-buffer -
tmux paste-buffer -t "$session"
}
# Click at row 5, column 10
send_mouse_click "$SESSION" 10 5
# Alternative: Use send-keys with MouseDown/MouseUp (tmux 3.0+)
tmux send-keys -t "$SESSION" -M MouseDown1Pane 10 5
tmux send-keys -t "$SESSION" -M MouseUp1Pane 10 5
Clipboard Operations
# Copy text TO tmux buffer (for pasting into app)
echo "text to paste" | tmux load-buffer -
# Paste from tmux buffer into pane
tmux paste-buffer -t "$SESSION"
# Set buffer directly
tmux set-buffer "content to paste"
# Capture pane to buffer, then retrieve
tmux capture-pane -t "$SESSION" -b capture-buffer
tmux show-buffer -b capture-buffer
# For apps that use system clipboard, set DISPLAY for xclip/xsel
# or use pbcopy/pbpaste on macOS
tmux send-keys -t "$SESSION" "echo 'copied' | pbcopy" Enter
Multiple Panes
# Split window horizontally (top/bottom)
tmux split-window -t "$SESSION" -v
# Split window vertically (left/right)
tmux split-window -t "$SESSION" -h
# Address specific panes (0-indexed)
tmux send-keys -t "$SESSION:0.0" "left pane" Enter # First pane
tmux send-keys -t "$SESSION:0.1" "right pane" Enter # Second pane
# Resize panes
tmux resize-pane -t "$SESSION:0.0" -x 40 # Set width
tmux resize-pane -t "$SESSION:0.1" -y 10 # Set height
# Capture specific pane
tmux capture-pane -t "$SESSION:0.1" -p
# Select/focus a pane
tmux select-pane -t "$SESSION:0.1"
# List panes with their indices and dimensions
tmux list-panes -t "$SESSION" -F "#{pane_index}: #{pane_width}x#{pane_height}"
Pattern: Testing Split-Pane Apps
# Some TUIs use split layouts - test each pane independently
setup_split_test() {
tmux new-session -d -s "$SESSION"
tmux resize-window -t "$SESSION" -x 120 -y 40
# Launch app that creates splits
tmux send-keys -t "$SESSION" "./split-app" Enter
sleep 1
}
verify_pane_content() {
local pane="$1"
local expected="$2"
local content
content=$(tmux capture-pane -t "$SESSION:0.$pane" -p)
if echo "$content" | grep -q "$expected"; then
echo "PASS: Pane $pane contains '$expected'"
return 0
else
echo "FAIL: Pane $pane missing '$expected'"
return 1
fi
}
# Test left pane shows file list, right pane shows preview
verify_pane_content 0 "file1.txt"
verify_pane_content 1 "Preview:"
Pattern: Scrolling and Viewport
# Scroll up/down in pane (for apps with scrolling content)
tmux send-keys -t "$SESSION" PageUp
tmux send-keys -t "$SESSION" PageDown
# Enter copy mode to scroll (then exit)
tmux copy-mode -t "$SESSION"
tmux send-keys -t "$SESSION" -X scroll-up
tmux send-keys -t "$SESSION" -X scroll-down
tmux send-keys -t "$SESSION" q # Exit copy mode
# Capture with scrollback (negative = lines above visible)
tmux capture-pane -t "$SESSION" -p -S -100 -E -1 # Last 100 lines of scrollback
# Capture everything (visible + all scrollback)
tmux capture-pane -t "$SESSION" -p -S - -E -
Pattern: Environment and Shell Setup
# Set environment variables before launching app
tmux send-keys -t "$SESSION" "export TERM=xterm-256color" Enter
tmux send-keys -t "$SESSION" "export NO_COLOR=1" Enter # Disable colors for easier parsing
tmux send-keys -t "$SESSION" "export LC_ALL=en_US.UTF-8" Enter
# Start with clean shell
tmux send-keys -t "$SESSION" "env -i bash --norc --noprofile" Enter
tmux send-keys -t "$SESSION" "export TERM=xterm-256color" Enter
# Change directory
tmux send-keys -t "$SESSION" "cd /path/to/project" Enter
Pattern: Waiting for App Exit
wait_for_exit() {
local session="$1"
local timeout="${2:-30}"
local elapsed=0
while [ $elapsed -lt $timeout ]; do
# Check if pane is showing shell prompt (app exited)
if tmux capture-pane -t "$session" -p | grep -qE '^\$|^>|^%'; then
return 0
fi
sleep 0.5
elapsed=$((elapsed + 1))
done
return 1
}
# Run app and wait for it to finish
tmux send-keys -t "$SESSION" "./app --batch-mode" Enter
if wait_for_exit "$SESSION" 60; then
echo "App completed"
else
echo "App timed out"
tmux send-keys -t "$SESSION" C-c # Force quit
fi
Pattern: Capture with Line Numbers
# Useful for debugging - see exactly which line contains what
capture_with_lines() {
local session="$1"
tmux capture-pane -t "$session" -p | nl -ba
}
# Assert content at specific line
assert_line() {
local session="$1"
local line_num="$2"
local expected="$3"
local actual
actual=$(tmux capture-pane -t "$session" -p | sed -n "${line_num}p")
if [ "$actual" = "$expected" ]; then
echo "PASS: Line $line_num matches"
return 0
else
echo "FAIL: Line $line_num"
echo " Expected: '$expected'"
echo " Actual: '$actual'"
return 1
fi
}
# Check that line 3 shows the header
assert_line "$SESSION" 3 "=== My App v1.0 ==="
</advanced_patterns>
<troubleshooting> ## Common IssuesProblem: Captured output is empty or incomplete
- Cause: App hasn't rendered yet
- Fix: Increase sleep duration after send-keys
Problem: Layout looks wrong in captures
- Cause: Window dimensions don't match app expectations
- Fix: Use
resize-window -x WIDTH -y HEIGHTbefore launching app
Problem: Special characters not sent correctly
- Cause: Using wrong key names
- Fix: Check
man tmuxfor correct key names (e.g.,BSpacenotBackspace)
Problem: ANSI codes making assertions difficult
- Cause: Using
-eflag when not needed - Fix: Omit
-efor plain text, or strip ANSI withsed 's/\x1b\[[0-9;]*m//g'
Problem: Tests flaky/inconsistent
- Cause: Race conditions with async rendering
- Fix: Use
wait_for_textpattern instead of fixed sleeps </troubleshooting>
<success_criteria> A successful TUI test:
- Runs in a detached tmux session (no display required)
- Uses fixed dimensions for reproducible layouts
- Waits appropriately for rendering
- Captures pane content and asserts on expected text
- Cleans up the session after completion
- Handles both success and failure cases gracefully </success_criteria>