Install
$ agentstack add mcp-vurte-tui-td ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ● Filesystem access Used
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
TUI Test Drive
[](https://rubygems.org/gems/tui-td) [](LICENSE.txt)
Testing framework and general-purpose TUI driver. Start a TUI in a PTY, send input, analyze output — as structured JSON, plain text, PNG screenshots, or HTML renders. Use it for testing, automation, or as a bridge between AI agents and terminal apps.
Language-agnostic: JSON tests + CLI + MCP let you drive TUIs from any language (Python, JavaScript, Rust, Go, …). Write a .json test plan, script via tui-td drive, or let an AI agent control a TUI through tui-td serve.
Ruby-native: In the Ruby ecosystem, tui-td integrates with RSpec and Minitest — auto-wait matchers and assertions included.
> New to tui-td? Jump to [Quick Start](docs/quick_start.md).
What tui-td gives you:
- Start any TUI in a virtual terminal (PTY) with
Driveror JSON test plans - Auto-wait assertions — matchers automatically retry until the condition is met or timeout
- Semantic selectors —
get_by_role(:button),get_by_role(:dialog),within { }scoping - Multiple output formats — structured JSON, plain text, PNG screenshots, HTML renders
- JSON test runner — language-agnostic, 23+ step types, CI-friendly
- RSpec matchers —
have_text,have_fg,have_button,have_dialog, and more - Minitest assertions —
assert_text,assert_button,assert_snapshot, and more - MCP server — AI agents can drive TUIs via JSON-RPC over stdio
- Pure Ruby rendering — embedded Spleen font + 2766 Unifont glyphs, no native deps required
- Video recording — record TUI sessions as MP4 via ffmpeg for demos, debugging, and AI agent playback
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"
# Record a session as video (requires ffmpeg)
tui-td --record demo.mp4 capture "htop" --timeout 10
# 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 Send a special key (enter, up, down, tab, escape, ctrl_c, ...)
# exit Quit
# Sent as text + Enter to the TUI
# Start MCP server for AI integration
tui-td serve
CLI Reference
Usage: tui-td [options]
Commands:
serve Start MCP server (JSON-RPC over stdio)
capture Run once, capture and display state
drive Drive a TUI interactively
run Run a TUI app and show live output
test 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 test examples/login_form.json # interactive form test (JSON)
rspec examples/rspec_interactive_spec.rb # interactive form test (RSpec)
ruby examples/minitest_interactive_test.rb # interactive form test (Minitest)
tui-td serve
Interactive commands (drive mode):
state Show terminal state as pretty JSON
raw Show raw ANSI output
elements Show detected UI elements (buttons, dialogs, etc.)
key Send keystroke (enter, tab, escape, up, down, left, right,
backspace, ctrl_c, ctrl_d)
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)
--record PATH Record session as video (MP4/WebM, requires ffmpeg)
--framerate N Recording framerate (default: 30)
--codec NAME Video codec: libx264, libx265, libvpx-vp9 (default: libx264)
--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, tui-td help rspec shows all RSpec matchers, and tui-td help minitest shows all Minitest assertions — 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":
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
# Video recording (requires ffmpeg)
driver.start_recording("session.mp4", framerate: 30, codec: "libx264")
driver.recording? # => true
driver.stop_recording # => "session.mp4"
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\|fail") | | 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 | | assert_button | "text" | Assert a button with given text is visible ([ OK ], (Cancel), `) | | assertdialog | — | Assert a dialog (box-drawing region) is visible | | assertcheckbox | "label", "checked": true | Assert a checkbox with given label (and optionally checked state) | | assertrole | ":button", "text": "OK" | Generic role assertion (:button, :checkbox, :dialog, :statusbar, :progress, :input, :label, :menu, :tab) | | assertinput | "text" (optional) | Assert an input field ([____]) is visible | | assertlabel | "text" | Assert a label (text ending with colon) is visible | | assertmenu | "text" (optional) | Assert a menu bar or dropdown item is visible | | asserttab | "text" | Assert a tab ([Tab1]) is visible | | assertstatusbar | — | Assert a status bar (bottom row with background) is visible | | assertprogressbar | "text" (optional) | Assert a progress bar ([####]) is visible | | startrecording | "path", "framerate": 30 | Start video recording (requires ffmpeg) | | stoprecording | — | Stop recording and finalize video | | assert_recording | true / false | Assert recording is active / not active | | 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_button("OK") | Assert a button with given text is visible | | have_dialog | Assert a dialog (box-drawing region) is visible | | have_checkbox("Label").checked | Assert a checkbox with given label (chain .checked) | | have_role(:button, text: "OK", checked: true, disabled: false) | Generic role assertion with optional text, checked, disabled filters | | have_input | Assert an input field ([____]) is visible | | have_label("Name") | Assert a label (text ending with colon) is visible | | have_menu | Assert a menu bar or dropdown item is visible | | have_tab("File") | Assert a tab is visible | | have_statusbar | Assert a status bar (bottom row with background) is visible | | have_progress_bar("50%") | Assert a progress bar ([####]) is visible | | have_exit_status(N) | Assert the driver process exit status equals N | | be_recording | Assert that the driver is currently recording video | | have_recorded_video("path.mp4") | Assert that a video file was created and has content |
Minitest Assertions
Include the assertions module for native Minitest support (auto-wait included):
require "tui_td/minitest/assertions"
class MyTUITest "}}}
// 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. Render as HTML (save to file or get inline)
{"method": "tools/call", "params": {"name": "tui_html_render", "arguments": {"path": "/tmp/proof.html"}}}
// Or without path to get HTML inline:
// {"method": "tools/call", "params": {"name": "tui_html_render", "arguments": {}}}
// 8. Search for text in the terminal
{"method": "tools/call", "params": {"name": "tui_find_text", "arguments": {"pattern": "error|fail"}}}
// 9. Find UI elements by role
{"method": "tools/call", "params": {"name": "tui_find_elements", "arguments": {"role": "button"}}}
// 10. Get actions for an element
{"method": "tools/call", "params": {"name": "tui_element_actions", "arguments": {"role": "button", "text": "OK"}}}
// 11. Check exit status (or wait for exit)
{"method": "tools/call", "params": {"name": "tui_exit_status", "arguments": {}}}
// 12. Clean up
{"method": "tools/call", "params": {
…
## Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [vurte](https://github.com/vurte)
- **Source:** [vurte/tui-td](https://github.com/vurte/tui-td)
- **License:** MIT
- **Homepage:** https://rubygems.org/gems/tui-td
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.