tui-td 0.1.2 → 0.1.3
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 +4 -4
- data/CHANGELOG.md +34 -6
- data/README.md +4 -1
- data/lib/tui_td/ansi_parser.rb +37 -4
- data/lib/tui_td/cli.rb +165 -8
- data/lib/tui_td/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: be8d93991312317febcad6a713d4dd58e6e670e620211bae0adc0db0bdd21da1
|
|
4
|
+
data.tar.gz: 5618bc612addd9be3c634ebd4009800ddd6f7da9396ad5d950c8f831727398f8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ea0199d84e4f2a038141178e6b0adc539c1547184d99b13ee270e971e7076e37d079f315b6d6fde4b880231a032f8463677ebc359e93ad5084974ca0bc6ad30d
|
|
7
|
+
data.tar.gz: 99e5a3c8d7bdf39ec903a7b44fb9a7d2e9c792cc405ec25b409ed8eca6f504b830b75cdf24a29271612931b79b4719f547580f61ab6f5f67aa829e74ab95bd10
|
data/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,37 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.3
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
5
|
+
- UTF-8 multi-byte character support in ANSI parser (`_utf8_char_at`)
|
|
6
|
+
- `--help` is now a complete CLI reference: Examples section, interactive drive commands
|
|
7
|
+
- `tui-td help test` — JSON test step reference with CLI and Ruby code workflow
|
|
8
|
+
- `tui-td help rspec` — RSpec matchers reference with Driver/State setup workflow
|
|
9
|
+
- `--version` flag
|
|
10
|
+
|
|
11
|
+
## 0.1.2
|
|
12
|
+
|
|
13
|
+
- Background reader thread — continuous PTY reads prevent buffer overflow
|
|
14
|
+
- Thread-safe output buffer with Mutex protection
|
|
15
|
+
- DSR (Device Status Report) support — respond to `\e[6n` cursor position requests
|
|
16
|
+
- Replace `IO.select`/`readpartial` with `read_nonblock`
|
|
17
|
+
|
|
18
|
+
## 0.1.1
|
|
19
|
+
|
|
20
|
+
- Add `--chdir` / `-C` CLI flag for running commands in a specific directory
|
|
21
|
+
- Default `capture` output to text instead of full JSON grid
|
|
22
|
+
- Use `permute!` for CLI option parsing so global flags work after the command
|
|
23
|
+
- Fix: skip ISO 2022 charset sequences (`ESC(B`) instead of leaking `(B` into output
|
|
24
|
+
- Add `raw` field to state JSON (original ANSI with all escape sequences preserved)
|
|
25
|
+
- Publish to rubygems.org
|
|
26
|
+
|
|
27
|
+
## 0.1.0
|
|
28
|
+
|
|
29
|
+
- PTY-based TUI driver with `start`, `send`, `send_keys`, `wait_for_text`, `wait_for_stable`
|
|
30
|
+
- ANSI parser: SGR colors (16, 256, TrueColor), cursor movement, erase, scroll
|
|
31
|
+
- Structured state output: `{size, cursor, rows, raw}`
|
|
32
|
+
- CLI: `capture`, `run`, `drive`, `test`, `serve`
|
|
33
|
+
- Pure Ruby PNG screenshots via `chunky_png` with embedded Spleen 8×16 font
|
|
34
|
+
- HTML renderer with run-length encoding
|
|
35
|
+
- JSON test runner with 12 step types
|
|
36
|
+
- RSpec matchers: `have_text`, `have_fg`, `have_bg`, `have_style`
|
|
37
|
+
- MCP server (JSON-RPC over stdio) for AI-driven TUI testing
|
data/README.md
CHANGED
|
@@ -113,9 +113,12 @@ Global options:
|
|
|
113
113
|
--json Output state as compact JSON (includes raw ANSI)
|
|
114
114
|
--pretty Output state as pretty JSON
|
|
115
115
|
--text Output state as plain text table (default)
|
|
116
|
-
-h, --help Show
|
|
116
|
+
-h, --help Show complete reference (commands, options, examples, interactive commands)
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
`tui-td --help` serves as the full CLI reference. `tui-td help test` shows all JSON test
|
|
120
|
+
step types and `tui-td help rspec` shows all RSpec matchers — no need to consult the docs.
|
|
121
|
+
|
|
119
122
|
## Ruby API
|
|
120
123
|
|
|
121
124
|
### Driver — Start, send, capture
|
data/lib/tui_td/ansi_parser.rb
CHANGED
|
@@ -137,16 +137,16 @@ module TUITD
|
|
|
137
137
|
else
|
|
138
138
|
i += 1
|
|
139
139
|
end
|
|
140
|
-
elsif processed
|
|
141
|
-
# Printable character
|
|
140
|
+
elsif (char, char_len = _utf8_char_at(processed, i))
|
|
141
|
+
# Printable character (including multi-byte UTF-8)
|
|
142
142
|
if cursor[:row] < rows && cursor[:col] < cols
|
|
143
143
|
cell = grid[cursor[:row]][cursor[:col]]
|
|
144
|
-
cell[:char] =
|
|
144
|
+
cell[:char] = char
|
|
145
145
|
cell.merge!(attrs)
|
|
146
146
|
cursor[:col] += 1
|
|
147
147
|
cursor[:col] = cols - 1 if cursor[:col] >= cols
|
|
148
148
|
end
|
|
149
|
-
i +=
|
|
149
|
+
i += char_len
|
|
150
150
|
else
|
|
151
151
|
i += 1
|
|
152
152
|
end
|
|
@@ -409,5 +409,38 @@ module TUITD
|
|
|
409
409
|
nil
|
|
410
410
|
end
|
|
411
411
|
end
|
|
412
|
+
|
|
413
|
+
# Extract a single UTF-8 character at position i in a binary string.
|
|
414
|
+
# Returns [char_string, byte_length] or nil if the byte is not printable/valid.
|
|
415
|
+
def self._utf8_char_at(str, i)
|
|
416
|
+
byte = str.getbyte(i)
|
|
417
|
+
return nil unless byte
|
|
418
|
+
|
|
419
|
+
if byte < 0x80
|
|
420
|
+
# Single-byte ASCII
|
|
421
|
+
return nil unless byte >= 0x20 # only printable, skip control chars
|
|
422
|
+
return [byte.chr, 1]
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# Multi-byte UTF-8
|
|
426
|
+
len = if byte & 0xE0 == 0xC0
|
|
427
|
+
2
|
|
428
|
+
elsif byte & 0xF0 == 0xE0
|
|
429
|
+
3
|
|
430
|
+
elsif byte & 0xF8 == 0xF0
|
|
431
|
+
4
|
|
432
|
+
else
|
|
433
|
+
return nil # continuation byte or invalid — let main loop advance
|
|
434
|
+
end
|
|
435
|
+
return nil if i + len > str.bytesize
|
|
436
|
+
|
|
437
|
+
bytes = str.byteslice(i, len)
|
|
438
|
+
char = bytes.dup.force_encoding("UTF-8")
|
|
439
|
+
return nil unless char.valid_encoding?
|
|
440
|
+
|
|
441
|
+
[char, len]
|
|
442
|
+
rescue StandardError
|
|
443
|
+
nil
|
|
444
|
+
end
|
|
412
445
|
end
|
|
413
446
|
end
|
data/lib/tui_td/cli.rb
CHANGED
|
@@ -18,12 +18,29 @@ module TUITD
|
|
|
18
18
|
opts.banner = "Usage: tui-td <command> [options]"
|
|
19
19
|
opts.separator ""
|
|
20
20
|
opts.separator "Commands:"
|
|
21
|
-
opts.separator " serve
|
|
22
|
-
opts.separator " run <command> Run a TUI app and show live output"
|
|
23
|
-
opts.separator " drive <command> Drive a TUI with structured state output"
|
|
21
|
+
opts.separator " serve Start MCP server (JSON-RPC over stdio)"
|
|
24
22
|
opts.separator " capture <command> Run once, capture and display state"
|
|
25
|
-
opts.separator "
|
|
26
|
-
opts.separator "
|
|
23
|
+
opts.separator " drive <command> Drive a TUI interactively"
|
|
24
|
+
opts.separator " run <command> Run a TUI app and show live output"
|
|
25
|
+
opts.separator " test <file.json> Run JSON test file"
|
|
26
|
+
opts.separator " help [topic] Show this help, or help test / help rspec"
|
|
27
|
+
opts.separator ""
|
|
28
|
+
opts.separator "Examples:"
|
|
29
|
+
opts.separator " tui-td capture \"ls -la\""
|
|
30
|
+
opts.separator " tui-td --screenshot out.png capture \"htop\" --timeout 5"
|
|
31
|
+
opts.separator " tui-td --html out.html capture \"glow README.md\""
|
|
32
|
+
opts.separator " tui-td -C /my/project capture \"make test\""
|
|
33
|
+
opts.separator " tui-td drive \"vim file.txt\" --rows 24 --cols 80"
|
|
34
|
+
opts.separator " tui-td test examples/echo_test.json"
|
|
35
|
+
opts.separator " tui-td serve"
|
|
36
|
+
opts.separator ""
|
|
37
|
+
opts.separator "Interactive commands (drive mode):"
|
|
38
|
+
opts.separator " state Show terminal state as pretty JSON"
|
|
39
|
+
opts.separator " raw Show raw ANSI output"
|
|
40
|
+
opts.separator " key <name> Send keystroke (enter, tab, escape, up, down, left, right,"
|
|
41
|
+
opts.separator " backspace, ctrl_c, ctrl_d)"
|
|
42
|
+
opts.separator " <text> Send text to the TUI"
|
|
43
|
+
opts.separator " exit Quit drive mode"
|
|
27
44
|
opts.separator ""
|
|
28
45
|
opts.separator "Global options:"
|
|
29
46
|
|
|
@@ -54,6 +71,10 @@ module TUITD
|
|
|
54
71
|
opts.on("--text", "Output state as plain text table") do |_|
|
|
55
72
|
global_opts[:format] = :text
|
|
56
73
|
end
|
|
74
|
+
opts.on("--version", "Show version") do
|
|
75
|
+
puts "tui-td #{TUITD::VERSION}"
|
|
76
|
+
exit 0
|
|
77
|
+
end
|
|
57
78
|
opts.on("-h", "--help", "Show help") do
|
|
58
79
|
puts opts
|
|
59
80
|
exit 0
|
|
@@ -74,9 +95,20 @@ module TUITD
|
|
|
74
95
|
cmd_capture(command_opts, global_opts)
|
|
75
96
|
when "test"
|
|
76
97
|
cmd_test(command_opts, global_opts)
|
|
77
|
-
when
|
|
78
|
-
|
|
79
|
-
|
|
98
|
+
when "help"
|
|
99
|
+
topic = argv.shift
|
|
100
|
+
case topic
|
|
101
|
+
when "test"
|
|
102
|
+
_help_test
|
|
103
|
+
when "rspec"
|
|
104
|
+
_help_rspec
|
|
105
|
+
when nil
|
|
106
|
+
_help_main
|
|
107
|
+
else
|
|
108
|
+
abort "Unknown help topic: #{topic.inspect}\nTry: tui-td help test, tui-td help rspec"
|
|
109
|
+
end
|
|
110
|
+
when nil
|
|
111
|
+
_help_main
|
|
80
112
|
else
|
|
81
113
|
abort "Unknown command: #{command.inspect}\nUse tui-td --help for usage"
|
|
82
114
|
end
|
|
@@ -228,5 +260,130 @@ module TUITD
|
|
|
228
260
|
puts line.empty? ? "~" : line
|
|
229
261
|
end
|
|
230
262
|
end
|
|
263
|
+
|
|
264
|
+
def _help_main
|
|
265
|
+
puts OptionParser.new { |o| o.banner = "Usage: tui-td <command> [options]" }
|
|
266
|
+
puts
|
|
267
|
+
puts "For more: tui-td help test (JSON test step types)"
|
|
268
|
+
puts " tui-td help rspec (RSpec matchers)"
|
|
269
|
+
exit 0
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def _help_test
|
|
273
|
+
puts <<~HELP
|
|
274
|
+
JSON Test Format
|
|
275
|
+
================
|
|
276
|
+
|
|
277
|
+
Run from CLI:
|
|
278
|
+
|
|
279
|
+
tui-td test examples/echo_test.json
|
|
280
|
+
|
|
281
|
+
Or from Ruby code:
|
|
282
|
+
|
|
283
|
+
require "tui_td/test_runner"
|
|
284
|
+
runner = TUITD::TestRunner.new(name: "my test", steps: [...])
|
|
285
|
+
result = runner.run # => { passed: true, results: [...] }
|
|
286
|
+
|
|
287
|
+
A test is a Hash or JSON string: {"name": "...", "steps": [...]}
|
|
288
|
+
|
|
289
|
+
Top-level keys: name, steps, rows (default 40), cols (default 120),
|
|
290
|
+
timeout (default 30), chdir
|
|
291
|
+
|
|
292
|
+
Each step is an object with a single action key:
|
|
293
|
+
|
|
294
|
+
{"start": "<command>"}
|
|
295
|
+
Start a TUI process in a PTY.
|
|
296
|
+
|
|
297
|
+
{"send": "<text>"}
|
|
298
|
+
Send text to the TUI. Use "\\n" for Enter.
|
|
299
|
+
|
|
300
|
+
{"send_key": "<name>"}
|
|
301
|
+
Send a keystroke. Names: enter, tab, escape, up, down,
|
|
302
|
+
left, right, backspace, ctrl_c, ctrl_d.
|
|
303
|
+
|
|
304
|
+
{"wait_for_text": "<substring>"}
|
|
305
|
+
Wait until the given text appears in the output.
|
|
306
|
+
|
|
307
|
+
{"wait_for_stable": true}
|
|
308
|
+
Wait until the output stops changing (default 300ms quiet).
|
|
309
|
+
|
|
310
|
+
{"assert_text": "<substring>"}
|
|
311
|
+
Fail if the text is not found in the current state.
|
|
312
|
+
|
|
313
|
+
{"assert_fg": [row, col], "is": "<color>"}
|
|
314
|
+
Assert foreground color at cell. Colors: "default",
|
|
315
|
+
named ANSI (red, green, blue, cyan, ...), "bright_*",
|
|
316
|
+
"color<N>" (256-color), "#rrggbb" (TrueColor).
|
|
317
|
+
|
|
318
|
+
{"assert_bg": [row, col], "is": "<color>"}
|
|
319
|
+
Assert background color at cell. Same color format.
|
|
320
|
+
|
|
321
|
+
{"assert_style": [row, col], "bold": true, "italic": false, ...}
|
|
322
|
+
Assert style attributes at cell. Checks only the keys provided.
|
|
323
|
+
|
|
324
|
+
{"screenshot": "<path>"}
|
|
325
|
+
Save a PNG screenshot. Path defaults to /tmp/tui_td_<ts>.png.
|
|
326
|
+
|
|
327
|
+
{"html": "<path>"}
|
|
328
|
+
Save an HTML render. Path defaults to /tmp/tui_td_<ts>.html.
|
|
329
|
+
|
|
330
|
+
{"close": true}
|
|
331
|
+
Close the driver session.
|
|
332
|
+
|
|
333
|
+
Example test file: examples/echo_test.json
|
|
334
|
+
HELP
|
|
335
|
+
exit 0
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def _help_rspec
|
|
339
|
+
puts <<~HELP
|
|
340
|
+
RSpec Matchers
|
|
341
|
+
==============
|
|
342
|
+
|
|
343
|
+
Matchers work on TUITD::State objects, not raw output.
|
|
344
|
+
Get a State from the Driver:
|
|
345
|
+
|
|
346
|
+
require "tui_td"
|
|
347
|
+
require "tui_td/matchers"
|
|
348
|
+
|
|
349
|
+
driver = TUITD::Driver.new("my_tui", rows: 24, cols: 80)
|
|
350
|
+
driver.start
|
|
351
|
+
state = TUITD::State.new(driver.state_data)
|
|
352
|
+
|
|
353
|
+
expect(state).to have_text("Hello")
|
|
354
|
+
expect(state).to have_fg("red").at(0, 5)
|
|
355
|
+
|
|
356
|
+
driver.close
|
|
357
|
+
|
|
358
|
+
Or build a State manually for unit tests:
|
|
359
|
+
|
|
360
|
+
state = TUITD::State.new(
|
|
361
|
+
size: { rows: 5, cols: 20 },
|
|
362
|
+
cursor: { row: 0, col: 0 },
|
|
363
|
+
rows: [[{ char: "H", fg: "default", bg: "default",
|
|
364
|
+
bold: false, italic: false, underline: false }]]
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
Matchers
|
|
368
|
+
--------
|
|
369
|
+
|
|
370
|
+
have_text(expected)
|
|
371
|
+
Passes if expected text appears anywhere in the terminal state.
|
|
372
|
+
Usage: expect(state).to have_text("Hello")
|
|
373
|
+
|
|
374
|
+
have_fg(expected).at(row, col)
|
|
375
|
+
Assert foreground color at [row, col] matches expected.
|
|
376
|
+
Usage: expect(state).to have_fg("red").at(0, 5)
|
|
377
|
+
|
|
378
|
+
have_bg(expected).at(row, col)
|
|
379
|
+
Assert background color at [row, col] matches expected.
|
|
380
|
+
Usage: expect(state).to have_bg("default").at(0, 0)
|
|
381
|
+
|
|
382
|
+
have_style.at(row, col).with(bold: true, italic: false, ...)
|
|
383
|
+
Assert style attributes at [row, col] match the given hash.
|
|
384
|
+
Usage: expect(state).to have_style.at(0, 0).with(bold: true)
|
|
385
|
+
HELP
|
|
386
|
+
exit 0
|
|
387
|
+
end
|
|
231
388
|
end
|
|
232
389
|
end
|
data/lib/tui_td/version.rb
CHANGED