tui-td 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 45c5b163ff48383823bccd9c0b9efe73f5a37b009c2e0968fb2619dea4c87d51
4
+ data.tar.gz: '01885208a25d500718f786fb66bc2b0753b2b0ff720cf6ed39c9380c9f1d13bf'
5
+ SHA512:
6
+ metadata.gz: d4887b9af284ee25b16236b8e4a0aad6eadbb1c8583d14119630250749d3d9fce45a138321f50eb2b59e7de0ea107b34d33461da62113d6b1a76e4f8d789b698
7
+ data.tar.gz: c435e9556f67254b627c2b7ff21199489b9991bd37e06eb5f1e609a4b99414917a6d731776434a0c42af53e7d362103caf203020200feb79328e310077e53903
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # CHANGELOG
2
+
3
+ ## 0.1.0 (initial)
4
+
5
+ - PTY-basierter TUI-Driver
6
+ - ANSI-Parser (SGR Farben, Cursor, Erase, 256/Truecolor)
7
+ - Strukturierter State als JSON
8
+ - CLI: run, drive, capture
9
+ - Screenshot-Funktion (ANSI → PNG via terminal-screenshot)
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Haluk Durmus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,479 @@
1
+ # TUI Test Drive
2
+
3
+ Testing framework for Terminal User Interfaces (TUIs) with MCP support.
4
+
5
+ **tui-td** lets you:
6
+ 1. Start a TUI application in a virtual terminal (PTY)
7
+ 2. See the output — as structured JSON, plain text, PNG screenshots, or HTML renders
8
+ 3. Send input — keystrokes, text, control sequences
9
+ 4. Analyze output — find text, check colors, detect cursor position
10
+ 5. Loop — adjust and retest without manual intervention
11
+
12
+ ## Installation
13
+
14
+ ### 1. Install Ruby
15
+
16
+ **rbenv (recommended):**
17
+
18
+ ```bash
19
+ # macOS
20
+ brew install rbenv ruby-build
21
+ echo 'eval "$(rbenv init - zsh)"' >> ~/.zshrc
22
+
23
+ # Linux
24
+ git clone https://github.com/rbenv/rbenv.git ~/.rbenv
25
+ git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
26
+ echo 'eval "$(~/.rbenv/bin/rbenv init - bash)"' >> ~/.bashrc
27
+ ```
28
+
29
+ Then install Ruby 3 and activate it:
30
+
31
+ ```bash
32
+ rbenv install 3.4.1
33
+ rbenv global 3.4.1
34
+ ruby --version # must show 3.0+
35
+ ```
36
+
37
+ **Alternative — Homebrew (macOS):**
38
+
39
+ ```bash
40
+ brew install ruby
41
+ ```
42
+
43
+ ### 2. Install tui-td
44
+
45
+ ```bash
46
+ curl -sL https://github.com/vurte/tui-td/releases/download/v0.1.0/tui-td-0.1.0.gem -o tui-td.gem
47
+ gem install tui-td.gem && rm tui-td.gem
48
+ ```
49
+
50
+ ### 3. Test
51
+
52
+ ```bash
53
+ tui-td capture "echo Hello World"
54
+ ```
55
+
56
+ ## CLI Usage
57
+
58
+ ```bash
59
+ # Capture output of any terminal command
60
+ tui-td capture "ls -la"
61
+
62
+ # Capture with custom terminal size
63
+ tui-td -r 24 -c 80 capture "vim --version"
64
+
65
+ # Run a command from a specific directory
66
+ tui-td -C /path/to/project capture "make test"
67
+
68
+ # Output as JSON for scripts
69
+ tui-td --json capture "ls -la"
70
+
71
+ # Save as HTML for browser visualization
72
+ tui-td --html output.html capture "htop"
73
+
74
+ # Save as a PNG screenshot
75
+ tui-td --screenshot output.png capture "htop"
76
+
77
+ # Drive a TUI interactively
78
+ tui-td drive "htop"
79
+ # At the > prompt:
80
+ # state Show current terminal state as pretty JSON
81
+ # raw Show raw ANSI output (first 2000 chars)
82
+ # key <name> Send a special key (enter, up, down, tab, escape, ctrl_c, ...)
83
+ # exit Quit
84
+ # <anything> Sent as text + Enter to the TUI
85
+
86
+ # Start MCP server for AI integration
87
+ tui-td serve
88
+ ```
89
+
90
+ ## CLI Reference
91
+
92
+ ```
93
+ Usage: tui-td <command> [options]
94
+
95
+ Commands:
96
+ serve Start MCP server (JSON-RPC over stdio)
97
+ test <file> Run JSON test file
98
+ run <command> Run a TUI app and show live output
99
+ drive <command> Drive a TUI with structured state output
100
+ capture <command> Run once, capture and display state
101
+
102
+ Global options:
103
+ -r, --rows N Terminal rows (default: 40)
104
+ -c, --cols N Terminal cols (default: 120)
105
+ -C, --chdir PATH Working directory for the command
106
+ -t, --timeout N Timeout in seconds (default: 30)
107
+ --screenshot PATH Save PNG screenshot
108
+ --html PATH Save HTML render for browser viewing
109
+ --json Output state as compact JSON (includes raw ANSI)
110
+ --pretty Output state as pretty JSON
111
+ --text Output state as plain text table (default)
112
+ -h, --help Show help
113
+ ```
114
+
115
+ ## Ruby API
116
+
117
+ ### Driver — Start, send, capture
118
+
119
+ ```ruby
120
+ require "tui_td"
121
+
122
+ driver = TUITD::Driver.new("htop", rows: 40, cols: 120, timeout: 30)
123
+ driver.start
124
+
125
+ # Send input
126
+ driver.send("hello world\n")
127
+ driver.send_keys(:enter) # :enter, :tab, :escape
128
+ driver.send_keys(:up) # :up, :down, :left, :right
129
+ driver.send_keys(:ctrl_c) # :ctrl_c, :ctrl_d, :backspace
130
+
131
+ # Wait for expected output
132
+ driver.wait_for_text("> ")
133
+ driver.wait_for_stable # Wait until 300ms of silence
134
+ driver.wait_for_exit # Wait until process ends
135
+
136
+ # Read output
137
+ driver.raw_output # Raw ANSI string
138
+ driver.state_data # Structured Hash with :raw, :rows, :cursor, :size
139
+ driver.state_json # JSON string (includes raw ANSI)
140
+ driver.state_json(pretty: true) # Pretty JSON
141
+
142
+ # Visual capture
143
+ driver.screenshot("screenshot.png") # PNG renderer
144
+ TUITD::HtmlRenderer.new(driver.state_data).render("output.html") # HTML renderer
145
+ html_string = TUITD::HtmlRenderer.new(driver.state_data).to_html # HTML string
146
+
147
+ driver.close
148
+ ```
149
+
150
+ ### State — Analyze terminal content
151
+
152
+ ```ruby
153
+ state = TUITD::State.new(driver.state_data)
154
+
155
+ # Read text
156
+ state.plain_text # "Hello\n> prompt\n"
157
+ state.text_at(row, col, length) # Extract substring at position
158
+ state.find_text("error") # [{row: 2, col: 10, text: "error", full_line: "..."}]
159
+
160
+ # Inspect cells
161
+ state.foreground_at(0, 5) # "cyan"
162
+ state.background_at(0, 5) # "bright_black"
163
+ state.style_at(0, 5) # {bold: true, italic: false, underline: false}
164
+
165
+ # AI-optimized compact output
166
+ state.to_ai_json
167
+ # => {
168
+ # size: {rows: 40, cols: 120},
169
+ # cursor: {row: 5, col: 12},
170
+ # text: "> Hello\n...",
171
+ # highlights: [
172
+ # {row: 0, text: "MyApp v1.0.0", bold: true, fg: "cyan"}
173
+ # ],
174
+ # summary: "Cursor at [5,12]. 1 styled row, colors: fg=cyan."
175
+ # }
176
+ ```
177
+
178
+ ### Full example — Test a TUI programmatically
179
+
180
+ ```ruby
181
+ require "tui_td"
182
+
183
+ driver = TUITD::Driver.new("my_tui_app", rows: 24, cols: 80)
184
+ driver.start
185
+
186
+ # Wait for the initial prompt
187
+ driver.wait_for_text("> ", timeout: 10)
188
+
189
+ # Send a command
190
+ driver.send("list files\n")
191
+ driver.wait_for_stable
192
+
193
+ # Analyze output
194
+ state = TUITD::State.new(driver.state_data)
195
+
196
+ if state.find_text("ERROR").any?
197
+ puts "Bug found!"
198
+ driver.screenshot("error_proof.png")
199
+ end
200
+
201
+ # Check colors
202
+ welcome_fg = state.foreground_at(0, 0)
203
+ raise "Expected cyan header" unless welcome_fg == "cyan"
204
+
205
+ # Send more commands, inspect, loop...
206
+ driver.send("/quit\n")
207
+ driver.wait_for_exit
208
+ driver.close
209
+ ```
210
+
211
+ ## Testing
212
+
213
+ tui-td supports two test formats: **JSON** for declarative, framework-agnostic tests, and **RSpec** for Ruby-native tests with custom matchers.
214
+
215
+ ### JSON Test Format
216
+
217
+ Tests are defined as JSON with a sequence of action steps. Each step maps to a tui-td operation.
218
+
219
+ Run with the `tui-td test` command:
220
+
221
+ ```bash
222
+ tui-td test examples/echo_test.json
223
+ ```
224
+
225
+ **Format:**
226
+
227
+ ```json
228
+ {
229
+ "name": "My test",
230
+ "rows": 24,
231
+ "cols": 80,
232
+ "timeout": 10,
233
+ "steps": [
234
+ { "start": "my_tui_app" },
235
+ { "wait_for_text": "> " },
236
+ { "send": "hello\n" },
237
+ { "assert_text": "hello" },
238
+ { "assert_fg": [0, 0], "is": "cyan" },
239
+ { "close": true }
240
+ ]
241
+ }
242
+ ```
243
+
244
+ **Available steps:**
245
+
246
+ | Step | Key | Description |
247
+ |------|-----|-------------|
248
+ | `start` | `"command"` | Start a TUI application |
249
+ | `send` | `"text"` | Send text (use `\n` for Enter) |
250
+ | `send_key` | `"key"` | Send a special key (`enter`, `up`, `ctrl_c`, ...) |
251
+ | `wait_for_text` | `"text"` | Wait until text appears |
252
+ | `wait_for_stable` | — | Wait until output is stable |
253
+ | `assert_text` | `"text"` | Assert that text exists on screen |
254
+ | `assert_fg` | `[row, col], "is": "color"` | Assert foreground color |
255
+ | `assert_bg` | `[row, col], "is": "color"` | Assert background color |
256
+ | `assert_style` | `[row, col], "bold": true` | Assert cell style (bold, italic, underline) |
257
+ | `screenshot` | `"path"` | Save PNG screenshot |
258
+ | `html` | `"path"` | Save HTML render for browser viewing |
259
+ | `close` | — | Close the TUI |
260
+
261
+ Example with `html` step for before/after snapshots:
262
+
263
+ ```json
264
+ {
265
+ "name": "Visual diff test",
266
+ "rows": 40, "cols": 120,
267
+ "steps": [
268
+ { "start": "my_tui_app" },
269
+ { "wait_for_stable": true },
270
+ { "html": "/tmp/before.html" },
271
+ { "send": "Help\n" },
272
+ { "wait_for_stable": true },
273
+ { "html": "/tmp/after.html" },
274
+ { "close": true }
275
+ ]
276
+ }
277
+ ```
278
+
279
+ **Ruby API:**
280
+
281
+ ```ruby
282
+ plan = File.read("test/example.json")
283
+ result = TUITD::TestRunner.new(plan).run
284
+ puts result[:passed] # => true
285
+ result[:results].each { |r| puts "#{r[:step]}: #{r[:passed]} - #{r[:message]}" }
286
+ ```
287
+
288
+ ### RSpec Tests
289
+
290
+ Use custom matchers for expressive, Ruby-native TUI tests:
291
+
292
+ ```ruby
293
+ require "tui_td"
294
+ require "tui_td/matchers"
295
+
296
+ RSpec.describe "My TUI" do
297
+ before(:all) do
298
+ @driver = TUITD::Driver.new("my_tui_app", rows: 24, cols: 80)
299
+ @driver.start
300
+ end
301
+
302
+ after(:all) { @driver&.close }
303
+
304
+ let(:state) { TUITD::State.new(@driver.state_data) }
305
+
306
+ it "shows welcome message" do
307
+ expect(state).to have_text("Welcome")
308
+ end
309
+
310
+ it "has a cyan header" do
311
+ expect(state).to have_fg("cyan").at(0, 0)
312
+ end
313
+
314
+ it "has a blue background on row 3" do
315
+ expect(state).to have_bg("blue").at(3, 0)
316
+ end
317
+
318
+ it "has bold text on the first line" do
319
+ expect(state).to have_style.at(0, 0).with(bold: true)
320
+ end
321
+ end
322
+ ```
323
+
324
+ **Matchers:**
325
+
326
+ | Matcher | Usage |
327
+ |---------|-------|
328
+ | `have_text("...")` | Assert text is present on screen |
329
+ | `have_fg("color").at(row, col)` | Assert foreground color at position |
330
+ | `have_bg("color").at(row, col)` | Assert background color at position |
331
+ | `have_style.at(row, col).with(bold: true, ...)` | Assert cell style |
332
+
333
+ ## MCP Server — AI Integration
334
+
335
+ Start the MCP server to let any MCP client control TUIs:
336
+
337
+ ```bash
338
+ tui-td serve
339
+ ```
340
+
341
+ ### Available tools
342
+
343
+ | Tool | Description |
344
+ |------|-------------|
345
+ | `tui_start` | Start a TUI application. Call first. |
346
+ | `tui_send` | Send text input. Use `\n` for Enter. |
347
+ | `tui_send_key` | Send special keys: `enter`, `tab`, `up`, `down`, `left`, `right`, `escape`, `ctrl_c`, `ctrl_d` |
348
+ | `tui_wait_for_text` | Wait until specified text appears in output (with timeout). |
349
+ | `tui_wait_for_stable` | Wait until terminal output stabilizes (300ms of silence). |
350
+ | `tui_state` | Get terminal state: AI-friendly compact mode (default), `full` grid, or `text` only. |
351
+ | `tui_plain_text` | Get plain text content, ANSI stripped. |
352
+ | `tui_screenshot` | Capture a PNG screenshot of the current terminal. |
353
+ | `tui_close` | Close the TUI and clean up. |
354
+
355
+ ### MCP configuration
356
+
357
+ Add to your MCP client configuration:
358
+
359
+ ```json
360
+ {
361
+ "mcpServers": {
362
+ "tui-td": {
363
+ "command": "tui-td",
364
+ "args": ["serve"]
365
+ }
366
+ }
367
+ }
368
+ ```
369
+
370
+ ### Example MCP session
371
+
372
+ ```json
373
+ // 1. Start the TUI
374
+ {"method": "tools/call", "params": {"name": "tui_start", "arguments": {"command": "htop"}}}
375
+
376
+ // 2. Wait for prompt
377
+ {"method": "tools/call", "params": {"name": "tui_wait_for_text", "arguments": {"text": "> "}}}
378
+
379
+ // 3. Send a command
380
+ {"method": "tools/call", "params": {"name": "tui_send", "arguments": {"text": "Write hello.rb\n"}}}
381
+
382
+ // 4. Wait for output
383
+ {"method": "tools/call", "params": {"name": "tui_wait_for_stable", "arguments": {}}}
384
+
385
+ // 5. Get AI-friendly state
386
+ {"method": "tools/call", "params": {"name": "tui_state", "arguments": {"format": "ai"}}}
387
+
388
+ // 6. Take screenshot if needed
389
+ {"method": "tools/call", "params": {"name": "tui_screenshot", "arguments": {"path": "/tmp/proof.png"}}}
390
+
391
+ // 7. Clean up
392
+ {"method": "tools/call", "params": {"name": "tui_close", "arguments": {}}}
393
+ ```
394
+
395
+ ## State Format
396
+
397
+ Top-level structure returned by `state_data` / `--json`:
398
+
399
+ ```json
400
+ {
401
+ "size": {"rows": 40, "cols": 120},
402
+ "cursor": {"row": 5, "col": 12},
403
+ "rows": [[{"char": "A", "fg": "cyan", ...}]],
404
+ "raw": "\e[31mred\e[0m\n..."
405
+ }
406
+ ```
407
+
408
+ Each cell in the `rows` grid:
409
+
410
+ ```json
411
+ {
412
+ "char": "A",
413
+ "fg": "cyan",
414
+ "bg": "default",
415
+ "bold": true,
416
+ "italic": false,
417
+ "underline": false
418
+ }
419
+ ```
420
+
421
+ `raw` is the original ANSI output with all escape sequences preserved.
422
+
423
+ ### Color value formats
424
+
425
+ | Format | Example | Description |
426
+ |--------|---------|-------------|
427
+ | `"default"` | — | Terminal default color |
428
+ | Named | `"red"`, `"cyan"` | Standard 16 ANSI colors |
429
+ | Bright | `"bright_red"`, `"bright_green"` | Bright 16 ANSI colors |
430
+ | 256-color | `"color82"` | XTerm 256-color palette |
431
+ | TrueColor | `"#ff6432"` | 24-bit hex RGB |
432
+
433
+ ## Screenshot
434
+
435
+ 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).
436
+
437
+ ```ruby
438
+ # Via CLI
439
+ tui-td --screenshot output.png capture "echo 'Hello World'"
440
+
441
+ # Via Ruby API
442
+ driver.screenshot("output.png")
443
+ ```
444
+
445
+ ## HTML Renderer
446
+
447
+ 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.
448
+
449
+ Features:
450
+ - Dark theme matching terminal appearance
451
+ - Run-length encoding of adjacent identically-styled cells (compact HTML)
452
+ - Cursor indicator (yellow outline)
453
+ - HTML entity escaping (`<`, `>`, `&`, `"`)
454
+ - All ANSI color formats: 16 named, bright, 256-color, TrueColor
455
+
456
+ ```bash
457
+ # CLI — capture once
458
+ tui-td --html output.html capture "htop"
459
+
460
+ # CLI — run with custom terminal size
461
+ tui-td --html /tmp/demo.html run "htop" --rows 40 --cols 120
462
+ ```
463
+
464
+ ```ruby
465
+ # Ruby API — render to file
466
+ TUITD::HtmlRenderer.new(driver.state_data).render("output.html")
467
+
468
+ # Ruby API — get HTML string (e.g. for API responses)
469
+ html = TUITD::HtmlRenderer.new(driver.state_data).to_html
470
+ ```
471
+
472
+ ```json
473
+ // Test-Runner — before/after snapshots
474
+ {"html": "/tmp/snapshot.html"}
475
+ ```
476
+
477
+ ## License
478
+
479
+ MIT
data/bin/tui-td ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
5
+
6
+ require "tui_td"
7
+ require "tui_td/cli"
8
+
9
+ TUITD::CLI.run(ARGV)