TUI Test Drive
Testing framework for Terminal User Interfaces (TUIs) with MCP support.
tui-td lets you:
- Start a TUI application in a virtual terminal (PTY)
- See the output — as structured JSON, plain text, PNG screenshots, or HTML renders
- Send input — keystrokes, text, control sequences
- Analyze output — find text, check colors, detect cursor position
- Loop — adjust and retest without manual intervention
Installation
1. Install Ruby
rbenv (recommended):
# macOS
brew install rbenv ruby-build
echo 'eval "$(rbenv init - zsh)"' >> ~/.zshrc
# Linux
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
echo 'eval "$(~/.rbenv/bin/rbenv init - bash)"' >> ~/.bashrc
Then install Ruby 3 and activate it:
rbenv install 3.4.1
rbenv global 3.4.1
ruby --version # must show 3.0+
Alternative — Homebrew (macOS):
brew install ruby
2. Install tui-td
gem install tui-td
3. Test
tui-td capture "echo Hello World"
CLI Usage
# Capture output of any terminal command
tui-td capture "ls -la"
# Capture with custom terminal size
tui-td -r 24 -c 80 capture "vim --version"
# Run a command from a specific directory
tui-td -C /path/to/project capture "make test"
# Output as JSON for scripts
tui-td --json capture "ls -la"
# Save as HTML for browser visualization
tui-td --html output.html capture "htop"
# Save as a PNG screenshot
tui-td --screenshot output.png capture "htop"
# Drive a TUI interactively
tui-td drive "htop"
# At the > prompt:
# state Show current terminal state as pretty JSON
# raw Show raw ANSI output (first 2000 chars)
# key <name> Send a special key (enter, up, down, tab, escape, ctrl_c, ...)
# exit Quit
# <anything> Sent as text + Enter to the TUI
# Start MCP server for AI integration
tui-td serve
CLI Reference
Usage: tui-td <command> [options]
Commands:
serve Start MCP server (JSON-RPC over stdio)
test <file> Run JSON test file
run <command> Run a TUI app and show live output
drive <command> Drive a TUI with structured state output
capture <command> Run once, capture and display state
Global options:
-r, --rows N Terminal rows (default: 40)
-c, --cols N Terminal cols (default: 120)
-C, --chdir PATH Working directory for the command
-t, --timeout N Timeout in seconds (default: 30)
--screenshot PATH Save PNG screenshot
--html PATH Save HTML render for browser viewing
--json Output state as compact JSON (includes raw ANSI)
--pretty Output state as pretty JSON
--text Output state as plain text table (default)
-h, --help Show help
Ruby API
Driver — Start, send, capture
require "tui_td"
driver = TUITD::Driver.new("htop", rows: 40, cols: 120, timeout: 30)
driver.start
# Send input
driver.send("hello world\n")
driver.send_keys(:enter) # :enter, :tab, :escape
driver.send_keys(:up) # :up, :down, :left, :right
driver.send_keys(:ctrl_c) # :ctrl_c, :ctrl_d, :backspace
# Wait for expected output
driver.wait_for_text("> ")
driver.wait_for_stable # Wait until 300ms of silence
driver.wait_for_exit # Wait until process ends
# Read output
driver.raw_output # Raw ANSI string
driver.state_data # Structured Hash with :raw, :rows, :cursor, :size
driver.state_json # JSON string (includes raw ANSI)
driver.state_json(pretty: true) # Pretty JSON
# Visual capture
driver.screenshot("screenshot.png") # PNG renderer
TUITD::HtmlRenderer.new(driver.state_data).render("output.html") # HTML renderer
html_string = TUITD::HtmlRenderer.new(driver.state_data).to_html # HTML string
driver.close
State — Analyze terminal content
state = TUITD::State.new(driver.state_data)
# Read text
state.plain_text # "Hello\n> prompt\n"
state.text_at(row, col, length) # Extract substring at position
state.find_text("error") # [{row: 2, col: 10, text: "error", full_line: "..."}]
# Inspect cells
state.foreground_at(0, 5) # "cyan"
state.background_at(0, 5) # "bright_black"
state.style_at(0, 5) # {bold: true, italic: false, underline: false}
# AI-optimized compact output
state.to_ai_json
# => {
# size: {rows: 40, cols: 120},
# cursor: {row: 5, col: 12},
# text: "> Hello\n...",
# highlights: [
# {row: 0, text: "MyApp v1.0.0", bold: true, fg: "cyan"}
# ],
# summary: "Cursor at [5,12]. 1 styled row, colors: fg=cyan."
# }
Full example — Test a TUI programmatically
require "tui_td"
driver = TUITD::Driver.new("my_tui_app", rows: 24, cols: 80)
driver.start
# Wait for the initial prompt
driver.wait_for_text("> ", timeout: 10)
# Send a command
driver.send("list files\n")
driver.wait_for_stable
# Analyze output
state = TUITD::State.new(driver.state_data)
if state.find_text("ERROR").any?
puts "Bug found!"
driver.screenshot("error_proof.png")
end
# Check colors
welcome_fg = state.foreground_at(0, 0)
raise "Expected cyan header" unless welcome_fg == "cyan"
# Send more commands, inspect, loop...
driver.send("/quit\n")
driver.wait_for_exit
driver.close
Testing
tui-td supports two test formats: JSON for declarative, framework-agnostic tests, and RSpec for Ruby-native tests with custom matchers.
JSON Test Format
Tests are defined as JSON with a sequence of action steps. Each step maps to a tui-td operation.
Run with the tui-td test command:
tui-td test examples/echo_test.json
Format:
{
"name": "My test",
"rows": 24,
"cols": 80,
"timeout": 10,
"steps": [
{ "start": "my_tui_app" },
{ "wait_for_text": "> " },
{ "send": "hello\n" },
{ "assert_text": "hello" },
{ "assert_fg": [0, 0], "is": "cyan" },
{ "close": true }
]
}
Available steps:
| Step | Key | Description |
|---|---|---|
start |
"command" |
Start a TUI application |
send |
"text" |
Send text (use \n for Enter) |
send_key |
"key" |
Send a special key (enter, up, ctrl_c, ...) |
wait_for_text |
"text" |
Wait until text appears |
wait_for_stable |
— | Wait until output is stable |
assert_text |
"text" |
Assert that text exists on screen |
assert_fg |
[row, col], "is": "color" |
Assert foreground color |
assert_bg |
[row, col], "is": "color" |
Assert background color |
assert_style |
[row, col], "bold": true |
Assert cell style (bold, italic, underline) |
screenshot |
"path" |
Save PNG screenshot |
html |
"path" |
Save HTML render for browser viewing |
close |
— | Close the TUI |
Example with html step for before/after snapshots:
{
"name": "Visual diff test",
"rows": 40, "cols": 120,
"steps": [
{ "start": "my_tui_app" },
{ "wait_for_stable": true },
{ "html": "/tmp/before.html" },
{ "send": "Help\n" },
{ "wait_for_stable": true },
{ "html": "/tmp/after.html" },
{ "close": true }
]
}
Ruby API:
plan = File.read("test/example.json")
result = TUITD::TestRunner.new(plan).run
puts result[:passed] # => true
result[:results].each { |r| puts "#{r[:step]}: #{r[:passed]} - #{r[:message]}" }
RSpec Tests
Use custom matchers for expressive, Ruby-native TUI tests:
require "tui_td"
require "tui_td/matchers"
RSpec.describe "My TUI" do
before(:all) do
@driver = TUITD::Driver.new("my_tui_app", rows: 24, cols: 80)
@driver.start
end
after(:all) { @driver&.close }
let(:state) { TUITD::State.new(@driver.state_data) }
it "shows welcome message" do
expect(state).to have_text("Welcome")
end
it "has a cyan header" do
expect(state).to have_fg("cyan").at(0, 0)
end
it "has a blue background on row 3" do
expect(state).to have_bg("blue").at(3, 0)
end
it "has bold text on the first line" do
expect(state).to have_style.at(0, 0).with(bold: true)
end
end
Matchers:
| Matcher | Usage |
|---|---|
have_text("...") |
Assert text is present on screen |
have_fg("color").at(row, col) |
Assert foreground color at position |
have_bg("color").at(row, col) |
Assert background color at position |
have_style.at(row, col).with(bold: true, ...) |
Assert cell style |
MCP Server — AI Integration
Start the MCP server to let any MCP client control TUIs:
tui-td serve
Available tools
| Tool | Description |
|---|---|
tui_start |
Start a TUI application. Call first. |
tui_send |
Send text input. Use \n for Enter. |
tui_send_key |
Send special keys: enter, tab, up, down, left, right, escape, ctrl_c, ctrl_d |
tui_wait_for_text |
Wait until specified text appears in output (with timeout). |
tui_wait_for_stable |
Wait until terminal output stabilizes (300ms of silence). |
tui_state |
Get terminal state: AI-friendly compact mode (default), full grid, or text only. |
tui_plain_text |
Get plain text content, ANSI stripped. |
tui_screenshot |
Capture a PNG screenshot of the current terminal. |
tui_close |
Close the TUI and clean up. |
MCP configuration
Add to your MCP client configuration:
{
"mcpServers": {
"tui-td": {
"command": "tui-td",
"args": ["serve"]
}
}
}
Example MCP session
// 1. Start the TUI
{"method": "tools/call", "params": {"name": "tui_start", "arguments": {"command": "htop"}}}
// 2. Wait for prompt
{"method": "tools/call", "params": {"name": "tui_wait_for_text", "arguments": {"text": "> "}}}
// 3. Send a command
{"method": "tools/call", "params": {"name": "tui_send", "arguments": {"text": "Write hello.rb\n"}}}
// 4. Wait for output
{"method": "tools/call", "params": {"name": "tui_wait_for_stable", "arguments": {}}}
// 5. Get AI-friendly state
{"method": "tools/call", "params": {"name": "tui_state", "arguments": {"format": "ai"}}}
// 6. Take screenshot if needed
{"method": "tools/call", "params": {"name": "tui_screenshot", "arguments": {"path": "/tmp/proof.png"}}}
// 7. Clean up
{"method": "tools/call", "params": {"name": "tui_close", "arguments": {}}}
State Format
Top-level structure returned by state_data / --json:
{
"size": {"rows": 40, "cols": 120},
"cursor": {"row": 5, "col": 12},
"rows": [[{"char": "A", "fg": "cyan", ...}]],
"raw": "\e[31mred\e[0m\n..."
}
Each cell in the rows grid:
{
"char": "A",
"fg": "cyan",
"bg": "default",
"bold": true,
"italic": false,
"underline": false
}
raw is the original ANSI output with all escape sequences preserved.
Color value formats
| Format | Example | Description |
|---|---|---|
"default" |
— | Terminal default color |
| Named | "red", "cyan" |
Standard 16 ANSI colors |
| Bright | "bright_red", "bright_green" |
Bright 16 ANSI colors |
| 256-color | "color82" |
XTerm 256-color palette |
| TrueColor | "#ff6432" |
24-bit hex RGB |
Screenshot
Screenshots are rendered using the embedded Spleen 8×16 bitmap font via chunky_png. No external tools required (no npm, no ImageMagick). Handles all color formats and styles (bold, italic, underline).
# Via CLI
tui-td --screenshot output.png capture "echo 'Hello World'"
# Via Ruby API
driver.screenshot("output.png")
HTML Renderer
Renders terminal state as a self-contained HTML document with inline CSS. Faithfully reproduces colors, bold, italic, underline, and cursor position — so an LLM or human can "see" the TUI in any browser without external dependencies.
Features:
- Dark theme matching terminal appearance
- Run-length encoding of adjacent identically-styled cells (compact HTML)
- Cursor indicator (yellow outline)
- HTML entity escaping (
<,>,&,") - All ANSI color formats: 16 named, bright, 256-color, TrueColor
# CLI — capture once
tui-td --html output.html capture "htop"
# CLI — run with custom terminal size
tui-td --html /tmp/demo.html run "htop" --rows 40 --cols 120
# Ruby API — render to file
TUITD::HtmlRenderer.new(driver.state_data).render("output.html")
# Ruby API — get HTML string (e.g. for API responses)
html = TUITD::HtmlRenderer.new(driver.state_data).to_html
// Test-Runner — before/after snapshots
{"html": "/tmp/snapshot.html"}
License
MIT