{"id":49896578,"url":"https://github.com/vurte/tui-td","last_synced_at":"2026-05-21T19:01:21.643Z","repository":{"id":358118647,"uuid":"1240088443","full_name":"vurte/tui-td","owner":"vurte","description":"Testing framework for Terminal User Interfaces — capture, drive, screenshot, and assert on TUI applications","archived":false,"fork":false,"pushed_at":"2026-05-20T18:32:26.000Z","size":717,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-20T22:16:48.028Z","etag":null,"topics":["cli","json","mcp","rspec","ruby","tdd","terminal","testing","tui"],"latest_commit_sha":null,"homepage":"https://rubygems.org/gems/tui-td","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/vurte.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-15T18:54:02.000Z","updated_at":"2026-05-20T18:32:31.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/vurte/tui-td","commit_stats":null,"previous_names":["vurte/tui-td"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/vurte/tui-td","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vurte%2Ftui-td","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vurte%2Ftui-td/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vurte%2Ftui-td/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vurte%2Ftui-td/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vurte","download_url":"https://codeload.github.com/vurte/tui-td/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vurte%2Ftui-td/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33311388,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-21T12:23:38.849Z","status":"ssl_error","status_checked_at":"2026-05-21T12:22:11.673Z","response_time":62,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","json","mcp","rspec","ruby","tdd","terminal","testing","tui"],"created_at":"2026-05-16T00:20:47.772Z","updated_at":"2026-05-21T19:01:21.442Z","avatar_url":"https://github.com/vurte.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# TUI Test Drive\n\nTesting framework for Terminal User Interfaces (TUIs) with MCP support.\n\n**tui-td** lets you:\n1. Start a TUI application in a virtual terminal (PTY)\n2. See the output — as structured JSON, plain text, PNG screenshots, or HTML renders\n3. Send input — keystrokes, text, control sequences\n4. Analyze output — find text, check colors, detect cursor position\n5. Loop — adjust and retest without manual intervention\n\n## Installation\n\n### 1. Install Ruby\n\n**rbenv (recommended):**\n\n```bash\n# macOS\nbrew install rbenv ruby-build\necho 'eval \"$(rbenv init - zsh)\"' \u003e\u003e ~/.zshrc\n\n# Linux\ngit clone https://github.com/rbenv/rbenv.git ~/.rbenv\ngit clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build\necho 'eval \"$(~/.rbenv/bin/rbenv init - bash)\"' \u003e\u003e ~/.bashrc\n```\n\nThen install Ruby 3 and activate it:\n\n```bash\nrbenv install 3.4.1\nrbenv global 3.4.1\nruby --version  # must show 3.0+\n```\n\n**Alternative — Homebrew (macOS):**\n\n```bash\nbrew install ruby\n```\n\n### 2. Install tui-td\n\n```bash\ncurl -sL https://github.com/vurte/tui-td/releases/download/v0.1.0/tui-td-0.1.0.gem -o tui-td.gem\ngem install tui-td.gem \u0026\u0026 rm tui-td.gem\n```\n\n### 3. Test\n\n```bash\ntui-td capture \"echo Hello World\"\n```\n\n## CLI Usage\n\n```bash\n# Capture output of any terminal command\ntui-td capture \"ls -la\"\n\n# Capture with custom terminal size\ntui-td -r 24 -c 80 capture \"vim --version\"\n\n# Run a command from a specific directory\ntui-td -C /path/to/project capture \"make test\"\n\n# Output as JSON for scripts\ntui-td --json capture \"ls -la\"\n\n# Save as HTML for browser visualization\ntui-td --html output.html capture \"htop\"\n\n# Save as a PNG screenshot\ntui-td --screenshot output.png capture \"htop\"\n\n# Drive a TUI interactively\ntui-td drive \"htop\"\n# At the \u003e prompt:\n#   state       Show current terminal state as pretty JSON\n#   raw         Show raw ANSI output (first 2000 chars)\n#   key \u003cname\u003e  Send a special key (enter, up, down, tab, escape, ctrl_c, ...)\n#   exit        Quit\n#   \u003canything\u003e  Sent as text + Enter to the TUI\n\n# Start MCP server for AI integration\ntui-td serve\n```\n\n## CLI Reference\n\n```\nUsage: tui-td \u003ccommand\u003e [options]\n\nCommands:\n  serve              Start MCP server (JSON-RPC over stdio)\n  test \u003cfile\u003e        Run JSON test file\n  run \u003ccommand\u003e      Run a TUI app and show live output\n  drive \u003ccommand\u003e    Drive a TUI with structured state output\n  capture \u003ccommand\u003e  Run once, capture and display state\n\nGlobal options:\n  -r, --rows N          Terminal rows (default: 40)\n  -c, --cols N          Terminal cols (default: 120)\n  -C, --chdir PATH      Working directory for the command\n  -t, --timeout N       Timeout in seconds (default: 30)\n  --screenshot PATH     Save PNG screenshot\n  --html PATH           Save HTML render for browser viewing\n  --json                Output state as compact JSON (includes raw ANSI)\n  --pretty              Output state as pretty JSON\n  --text                Output state as plain text table (default)\n  -h, --help            Show help\n```\n\n## Ruby API\n\n### Driver — Start, send, capture\n\n```ruby\nrequire \"tui_td\"\n\ndriver = TUITD::Driver.new(\"htop\", rows: 40, cols: 120, timeout: 30)\ndriver.start\n\n# Send input\ndriver.send(\"hello world\\n\")\ndriver.send_keys(:enter)     # :enter, :tab, :escape\ndriver.send_keys(:up)        # :up, :down, :left, :right\ndriver.send_keys(:ctrl_c)    # :ctrl_c, :ctrl_d, :backspace\n\n# Wait for expected output\ndriver.wait_for_text(\"\u003e \")\ndriver.wait_for_stable          # Wait until 300ms of silence\ndriver.wait_for_exit            # Wait until process ends\n\n# Read output\ndriver.raw_output               # Raw ANSI string\ndriver.state_data               # Structured Hash with :raw, :rows, :cursor, :size\ndriver.state_json                # JSON string (includes raw ANSI)\ndriver.state_json(pretty: true)  # Pretty JSON\n\n# Visual capture\ndriver.screenshot(\"screenshot.png\")  # PNG renderer\nTUITD::HtmlRenderer.new(driver.state_data).render(\"output.html\")  # HTML renderer\nhtml_string = TUITD::HtmlRenderer.new(driver.state_data).to_html   # HTML string\n\ndriver.close\n```\n\n### State — Analyze terminal content\n\n```ruby\nstate = TUITD::State.new(driver.state_data)\n\n# Read text\nstate.plain_text                # \"Hello\\n\u003e prompt\\n\"\nstate.text_at(row, col, length) # Extract substring at position\nstate.find_text(\"error\")        # [{row: 2, col: 10, text: \"error\", full_line: \"...\"}]\n\n# Inspect cells\nstate.foreground_at(0, 5)       # \"cyan\"\nstate.background_at(0, 5)       # \"bright_black\"\nstate.style_at(0, 5)            # {bold: true, italic: false, underline: false}\n\n# AI-optimized compact output\nstate.to_ai_json\n# =\u003e {\n#   size:    {rows: 40, cols: 120},\n#   cursor:  {row: 5, col: 12},\n#   text:    \"\u003e Hello\\n...\",\n#   highlights: [\n#     {row: 0, text: \"MyApp v1.0.0\", bold: true, fg: \"cyan\"}\n#   ],\n#   summary: \"Cursor at [5,12]. 1 styled row, colors: fg=cyan.\"\n# }\n```\n\n### Full example — Test a TUI programmatically\n\n```ruby\nrequire \"tui_td\"\n\ndriver = TUITD::Driver.new(\"my_tui_app\", rows: 24, cols: 80)\ndriver.start\n\n# Wait for the initial prompt\ndriver.wait_for_text(\"\u003e \", timeout: 10)\n\n# Send a command\ndriver.send(\"list files\\n\")\ndriver.wait_for_stable\n\n# Analyze output\nstate = TUITD::State.new(driver.state_data)\n\nif state.find_text(\"ERROR\").any?\n  puts \"Bug found!\"\n  driver.screenshot(\"error_proof.png\")\nend\n\n# Check colors\nwelcome_fg = state.foreground_at(0, 0)\nraise \"Expected cyan header\" unless welcome_fg == \"cyan\"\n\n# Send more commands, inspect, loop...\ndriver.send(\"/quit\\n\")\ndriver.wait_for_exit\ndriver.close\n```\n\n## Testing\n\ntui-td supports two test formats: **JSON** for declarative, framework-agnostic tests, and **RSpec** for Ruby-native tests with custom matchers.\n\n### JSON Test Format\n\nTests are defined as JSON with a sequence of action steps. Each step maps to a tui-td operation.\n\nRun with the `tui-td test` command:\n\n```bash\ntui-td test examples/echo_test.json\n```\n\n**Format:**\n\n```json\n{\n  \"name\": \"My test\",\n  \"rows\": 24,\n  \"cols\": 80,\n  \"timeout\": 10,\n  \"steps\": [\n    { \"start\": \"my_tui_app\" },\n    { \"wait_for_text\": \"\u003e \" },\n    { \"send\": \"hello\\n\" },\n    { \"assert_text\": \"hello\" },\n    { \"assert_fg\": [0, 0], \"is\": \"cyan\" },\n    { \"close\": true }\n  ]\n}\n```\n\n**Available steps:**\n\n| Step | Key | Description |\n|------|-----|-------------|\n| `start` | `\"command\"` | Start a TUI application |\n| `send` | `\"text\"` | Send text (use `\\n` for Enter) |\n| `send_key` | `\"key\"` | Send a special key (`enter`, `up`, `ctrl_c`, ...) |\n| `wait_for_text` | `\"text\"` | Wait until text appears |\n| `wait_for_stable` | — | Wait until output is stable |\n| `assert_text` | `\"text\"` | Assert that text exists on screen |\n| `assert_fg` | `[row, col], \"is\": \"color\"` | Assert foreground color |\n| `assert_bg` | `[row, col], \"is\": \"color\"` | Assert background color |\n| `assert_style` | `[row, col], \"bold\": true` | Assert cell style (bold, italic, underline) |\n| `screenshot` | `\"path\"` | Save PNG screenshot |\n| `html` | `\"path\"` | Save HTML render for browser viewing |\n| `close` | — | Close the TUI |\n\nExample with `html` step for before/after snapshots:\n\n```json\n{\n  \"name\": \"Visual diff test\",\n  \"rows\": 40, \"cols\": 120,\n  \"steps\": [\n    { \"start\": \"my_tui_app\" },\n    { \"wait_for_stable\": true },\n    { \"html\": \"/tmp/before.html\" },\n    { \"send\": \"Help\\n\" },\n    { \"wait_for_stable\": true },\n    { \"html\": \"/tmp/after.html\" },\n    { \"close\": true }\n  ]\n}\n```\n\n**Ruby API:**\n\n```ruby\nplan = File.read(\"test/example.json\")\nresult = TUITD::TestRunner.new(plan).run\nputs result[:passed]  # =\u003e true\nresult[:results].each { |r| puts \"#{r[:step]}: #{r[:passed]} - #{r[:message]}\" }\n```\n\n### RSpec Tests\n\nUse custom matchers for expressive, Ruby-native TUI tests:\n\n```ruby\nrequire \"tui_td\"\nrequire \"tui_td/matchers\"\n\nRSpec.describe \"My TUI\" do\n  before(:all) do\n    @driver = TUITD::Driver.new(\"my_tui_app\", rows: 24, cols: 80)\n    @driver.start\n  end\n\n  after(:all) { @driver\u0026.close }\n\n  let(:state) { TUITD::State.new(@driver.state_data) }\n\n  it \"shows welcome message\" do\n    expect(state).to have_text(\"Welcome\")\n  end\n\n  it \"has a cyan header\" do\n    expect(state).to have_fg(\"cyan\").at(0, 0)\n  end\n\n  it \"has a blue background on row 3\" do\n    expect(state).to have_bg(\"blue\").at(3, 0)\n  end\n\n  it \"has bold text on the first line\" do\n    expect(state).to have_style.at(0, 0).with(bold: true)\n  end\nend\n```\n\n**Matchers:**\n\n| Matcher | Usage |\n|---------|-------|\n| `have_text(\"...\")` | Assert text is present on screen |\n| `have_fg(\"color\").at(row, col)` | Assert foreground color at position |\n| `have_bg(\"color\").at(row, col)` | Assert background color at position |\n| `have_style.at(row, col).with(bold: true, ...)` | Assert cell style |\n\n## MCP Server — AI Integration\n\nStart the MCP server to let any MCP client control TUIs:\n\n```bash\ntui-td serve\n```\n\n### Available tools\n\n| Tool | Description |\n|------|-------------|\n| `tui_start` | Start a TUI application. Call first. |\n| `tui_send` | Send text input. Use `\\n` for Enter. |\n| `tui_send_key` | Send special keys: `enter`, `tab`, `up`, `down`, `left`, `right`, `escape`, `ctrl_c`, `ctrl_d` |\n| `tui_wait_for_text` | Wait until specified text appears in output (with timeout). |\n| `tui_wait_for_stable` | Wait until terminal output stabilizes (300ms of silence). |\n| `tui_state` | Get terminal state: AI-friendly compact mode (default), `full` grid, or `text` only. |\n| `tui_plain_text` | Get plain text content, ANSI stripped. |\n| `tui_screenshot` | Capture a PNG screenshot of the current terminal. |\n| `tui_close` | Close the TUI and clean up. |\n\n### MCP configuration\n\nAdd to your MCP client configuration:\n\n```json\n{\n  \"mcpServers\": {\n    \"tui-td\": {\n      \"command\": \"tui-td\",\n      \"args\": [\"serve\"]\n    }\n  }\n}\n```\n\n### Example MCP session\n\n```json\n// 1. Start the TUI\n{\"method\": \"tools/call\", \"params\": {\"name\": \"tui_start\", \"arguments\": {\"command\": \"htop\"}}}\n\n// 2. Wait for prompt\n{\"method\": \"tools/call\", \"params\": {\"name\": \"tui_wait_for_text\", \"arguments\": {\"text\": \"\u003e \"}}}\n\n// 3. Send a command\n{\"method\": \"tools/call\", \"params\": {\"name\": \"tui_send\", \"arguments\": {\"text\": \"Write hello.rb\\n\"}}}\n\n// 4. Wait for output\n{\"method\": \"tools/call\", \"params\": {\"name\": \"tui_wait_for_stable\", \"arguments\": {}}}\n\n// 5. Get AI-friendly state\n{\"method\": \"tools/call\", \"params\": {\"name\": \"tui_state\", \"arguments\": {\"format\": \"ai\"}}}\n\n// 6. Take screenshot if needed\n{\"method\": \"tools/call\", \"params\": {\"name\": \"tui_screenshot\", \"arguments\": {\"path\": \"/tmp/proof.png\"}}}\n\n// 7. Clean up\n{\"method\": \"tools/call\", \"params\": {\"name\": \"tui_close\", \"arguments\": {}}}\n```\n\n## State Format\n\nTop-level structure returned by `state_data` / `--json`:\n\n```json\n{\n  \"size\":   {\"rows\": 40, \"cols\": 120},\n  \"cursor\": {\"row\": 5, \"col\": 12},\n  \"rows\":   [[{\"char\": \"A\", \"fg\": \"cyan\", ...}]],\n  \"raw\":    \"\\e[31mred\\e[0m\\n...\"\n}\n```\n\nEach cell in the `rows` grid:\n\n```json\n{\n  \"char\": \"A\",\n  \"fg\": \"cyan\",\n  \"bg\": \"default\",\n  \"bold\": true,\n  \"italic\": false,\n  \"underline\": false\n}\n```\n\n`raw` is the original ANSI output with all escape sequences preserved.\n\n### Color value formats\n\n| Format | Example | Description |\n|--------|---------|-------------|\n| `\"default\"` | — | Terminal default color |\n| Named | `\"red\"`, `\"cyan\"` | Standard 16 ANSI colors |\n| Bright | `\"bright_red\"`, `\"bright_green\"` | Bright 16 ANSI colors |\n| 256-color | `\"color82\"` | XTerm 256-color palette |\n| TrueColor | `\"#ff6432\"` | 24-bit hex RGB |\n\n## Screenshot\n\nScreenshots 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).\n\n```ruby\n# Via CLI\ntui-td --screenshot output.png capture \"echo 'Hello World'\"\n\n# Via Ruby API\ndriver.screenshot(\"output.png\")\n```\n\n## HTML Renderer\n\nRenders 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.\n\nFeatures:\n- Dark theme matching terminal appearance\n- Run-length encoding of adjacent identically-styled cells (compact HTML)\n- Cursor indicator (yellow outline)\n- HTML entity escaping (`\u003c`, `\u003e`, `\u0026`, `\"`)\n- All ANSI color formats: 16 named, bright, 256-color, TrueColor\n\n```bash\n# CLI — capture once\ntui-td --html output.html capture \"htop\"\n\n# CLI — run with custom terminal size\ntui-td --html /tmp/demo.html run \"htop\" --rows 40 --cols 120\n```\n\n```ruby\n# Ruby API — render to file\nTUITD::HtmlRenderer.new(driver.state_data).render(\"output.html\")\n\n# Ruby API — get HTML string (e.g. for API responses)\nhtml = TUITD::HtmlRenderer.new(driver.state_data).to_html\n```\n\n```json\n// Test-Runner — before/after snapshots\n{\"html\": \"/tmp/snapshot.html\"}\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvurte%2Ftui-td","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvurte%2Ftui-td","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvurte%2Ftui-td/lists"}