TUI Test Drive

Testing framework for Terminal User Interfaces (TUIs) with MCP support.

tui-td lets you:

  1. Start a TUI application in a virtual terminal (PTY)
  2. See the output — as structured JSON, plain text, PNG screenshots, or HTML renders
  3. Send input — keystrokes, text, control sequences
  4. Analyze output — find text, check colors, detect cursor position
  5. Loop — adjust and retest without manual intervention

Installation

Ruby 3.0+ is required. Install via rbenv or brew install ruby.

gem install tui-td

Quick 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)
  capture <command>  Run once, capture and display state
  drive <command>    Drive a TUI interactively
  run <command>      Run a TUI app and show live output
  test <file.json>   Run JSON test file
  help [topic]       Show this help, or help test / help rspec

Examples:
  tui-td capture "ls -la"
  tui-td --screenshot out.png capture "htop" --timeout 5
  tui-td --html out.html capture "glow README.md"
  tui-td -C /my/project capture "make test"
  tui-td drive "vim file.txt" --rows 24 --cols 80
  tui-td test examples/echo_test.json
  tui-td -vl test examples/vim_hello_world.json
  tui-td serve

Interactive commands (drive mode):
  state              Show terminal state as pretty JSON
  raw                Show raw ANSI output
  key <name>         Send keystroke (enter, tab, escape, up, down, left, right,
                     backspace, ctrl_c, ctrl_d)
  <text>             Send text to the TUI
  exitstatus         Show process exit status (nil if running)
  exit               Quit drive mode

Global options:
    -r, --rows N                     Terminal rows (default: 40)
    -c, --cols N                     Terminal cols (default: 120)
    -t, --timeout SECONDS            Timeout in seconds (default: 30)
    -C, --chdir PATH                 Working directory for the command
        --screenshot PATH            Save screenshot (e.g., output.png)
        --html PATH                  Save HTML render (e.g., output.html)
        --json                       Output state as compact JSON
        --pretty                     Output state as pretty JSON
        --text                       Output state as plain text table
    -v, --verbose                    Show each test step as it runs
    -l, --live                       Show terminal state after each step (screen-refresh)
    -s, --step                       Pause after each test step for confirmation
        --version                    Show version
    -h, --help                       Show help

tui-td --help serves as the full CLI reference. tui-td help test shows all JSON test step types and tui-td help rspec shows all RSpec matchers — no need to consult the docs.

Demo

Step-by-step live view of a vim test — type "Hello World", copy the line, replace "World" with "Sun":

vim test demo

tui-td -vls test examples/vim_hello_world.json

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,
  "chdir": "/path/to/project",
  "before_all": [
    { "start": "my_tui_app", "env": { "DATABASE_URL": "test://" } }
  ],
  "steps": [
    { "wait_for_text": "> " },
    { "send": "hello\n" },
    { "assert_text": "hello" },
    { "assert_regex": "hello|world" },
    { "assert_fg": [0, 0], "is": "cyan" }
  ],
  "after_all": [
    { "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_not_text "text" Assert that text does NOT exist on screen
assert_regex "pattern" Assert that regex pattern matches (e.g. `"error\
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)
wait_for_exit Wait until the process exits
assert_exit N Assert the process exit code equals N
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_regex(/pattern/) Assert regex pattern matches anywhere
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
have_exit_status(N) Assert the driver process exit status equals N

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