tui-td 0.1.1 → 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 +9 -1
- data/lib/tui_td/ansi_parser.rb +48 -7
- data/lib/tui_td/cli.rb +165 -8
- data/lib/tui_td/driver.rb +57 -9
- data/lib/tui_td/version.rb +1 -1
- metadata +2 -2
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
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
# TUI Test Drive
|
|
2
2
|
|
|
3
|
+
*Like Playwright or Puppeteer, but built specifically for Terminal UIs and optimized for AI Coding Agents.*
|
|
4
|
+
|
|
3
5
|
Testing framework for Terminal User Interfaces (TUIs) with MCP support.
|
|
4
6
|
|
|
7
|
+
A Ruby library, but language-agnostic through its JSON test format and MCP server — use it from Python, JavaScript, Go, or any other programming language on Linux and macOS.
|
|
8
|
+
|
|
5
9
|
**tui-td** lets you:
|
|
6
10
|
1. Start a TUI application in a virtual terminal (PTY)
|
|
7
11
|
2. See the output — as structured JSON, plain text, PNG screenshots, or HTML renders
|
|
8
12
|
3. Send input — keystrokes, text, control sequences
|
|
9
13
|
4. Analyze output — find text, check colors, detect cursor position
|
|
10
14
|
5. Loop — adjust and retest without manual intervention
|
|
15
|
+
6. Integrate — works with any language via JSON test files or MCP
|
|
11
16
|
|
|
12
17
|
## Installation
|
|
13
18
|
|
|
@@ -108,9 +113,12 @@ Global options:
|
|
|
108
113
|
--json Output state as compact JSON (includes raw ANSI)
|
|
109
114
|
--pretty Output state as pretty JSON
|
|
110
115
|
--text Output state as plain text table (default)
|
|
111
|
-
-h, --help Show
|
|
116
|
+
-h, --help Show complete reference (commands, options, examples, interactive commands)
|
|
112
117
|
```
|
|
113
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
|
+
|
|
114
122
|
## Ruby API
|
|
115
123
|
|
|
116
124
|
### Driver — Start, send, capture
|
data/lib/tui_td/ansi_parser.rb
CHANGED
|
@@ -92,6 +92,7 @@ module TUITD
|
|
|
92
92
|
attrs = { fg: "default", bg: "default", bold: false, italic: false, underline: false }
|
|
93
93
|
saved_cursor = nil
|
|
94
94
|
scroll_region = nil
|
|
95
|
+
pending_dsr = false
|
|
95
96
|
|
|
96
97
|
# Strip everything before the last full clear (if any)
|
|
97
98
|
# to avoid accumulated garbage
|
|
@@ -102,10 +103,11 @@ module TUITD
|
|
|
102
103
|
if processed[i] == "\e" && processed[i + 1] == "["
|
|
103
104
|
# Find end of CSI sequence
|
|
104
105
|
j = i + 2
|
|
105
|
-
j += 1 while j < processed.length && !processed[j].match?(/[A-HJ-KP-SX@`
|
|
106
|
+
j += 1 while j < processed.length && !processed[j].match?(/[A-HJ-KP-SX@`fmnR]/)
|
|
106
107
|
seq = processed[i..j]
|
|
107
108
|
|
|
108
|
-
_apply_csi(seq, cursor, attrs, grid, rows, cols)
|
|
109
|
+
dsr = _apply_csi(seq, cursor, attrs, grid, rows, cols)
|
|
110
|
+
pending_dsr ||= dsr
|
|
109
111
|
|
|
110
112
|
i = j + 1
|
|
111
113
|
elsif processed[i] == "\n" || processed[i] == "\r\n"
|
|
@@ -135,16 +137,16 @@ module TUITD
|
|
|
135
137
|
else
|
|
136
138
|
i += 1
|
|
137
139
|
end
|
|
138
|
-
elsif processed
|
|
139
|
-
# Printable character
|
|
140
|
+
elsif (char, char_len = _utf8_char_at(processed, i))
|
|
141
|
+
# Printable character (including multi-byte UTF-8)
|
|
140
142
|
if cursor[:row] < rows && cursor[:col] < cols
|
|
141
143
|
cell = grid[cursor[:row]][cursor[:col]]
|
|
142
|
-
cell[:char] =
|
|
144
|
+
cell[:char] = char
|
|
143
145
|
cell.merge!(attrs)
|
|
144
146
|
cursor[:col] += 1
|
|
145
147
|
cursor[:col] = cols - 1 if cursor[:col] >= cols
|
|
146
148
|
end
|
|
147
|
-
i +=
|
|
149
|
+
i += char_len
|
|
148
150
|
else
|
|
149
151
|
i += 1
|
|
150
152
|
end
|
|
@@ -164,6 +166,7 @@ module TUITD
|
|
|
164
166
|
size: { rows: rows, cols: cols },
|
|
165
167
|
cursor: cursor,
|
|
166
168
|
rows: grid,
|
|
169
|
+
pending_dsr: pending_dsr,
|
|
167
170
|
}
|
|
168
171
|
end
|
|
169
172
|
|
|
@@ -211,7 +214,7 @@ module TUITD
|
|
|
211
214
|
def self._apply_csi(seq, cursor, attrs, grid, rows, cols)
|
|
212
215
|
# Strip leading escape char if present
|
|
213
216
|
cleaned = seq.sub(/^\e/, "")
|
|
214
|
-
match = cleaned.match(/^\[([\d;]*)([A-HJ-KP-SX@`
|
|
217
|
+
match = cleaned.match(/^\[([\d;]*)([A-HJ-KP-SX@`fhmnR])$/)
|
|
215
218
|
return unless match
|
|
216
219
|
|
|
217
220
|
params = match[1].split(";").map(&:to_i)
|
|
@@ -267,6 +270,11 @@ module TUITD
|
|
|
267
270
|
next unless cursor[:row] < rows && cursor[:col] + i < cols
|
|
268
271
|
grid[cursor[:row]][cursor[:col] + i][:char] = " "
|
|
269
272
|
end
|
|
273
|
+
when "n" # DSR — Device Status Report request
|
|
274
|
+
# \e[6n = request cursor position → caller must respond with \e[row;colR
|
|
275
|
+
return params[0] == 6
|
|
276
|
+
when "R" # DSR response (from terminal side) or CPR — ignore
|
|
277
|
+
nil
|
|
270
278
|
end
|
|
271
279
|
end
|
|
272
280
|
|
|
@@ -401,5 +409,38 @@ module TUITD
|
|
|
401
409
|
nil
|
|
402
410
|
end
|
|
403
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
|
|
404
445
|
end
|
|
405
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/driver.rb
CHANGED
|
@@ -30,6 +30,9 @@ module TUITD
|
|
|
30
30
|
@stdout = nil
|
|
31
31
|
@wait_thr = nil
|
|
32
32
|
@output_buffer = +""
|
|
33
|
+
@output_mutex = Mutex.new
|
|
34
|
+
@reader_thread = nil
|
|
35
|
+
@reader_running = false
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
# Start the TUI application in a PTY
|
|
@@ -46,6 +49,8 @@ module TUITD
|
|
|
46
49
|
wait_for_stable
|
|
47
50
|
refresh_state!
|
|
48
51
|
|
|
52
|
+
_start_reader_thread
|
|
53
|
+
|
|
49
54
|
true
|
|
50
55
|
end
|
|
51
56
|
|
|
@@ -81,7 +86,8 @@ module TUITD
|
|
|
81
86
|
loop do
|
|
82
87
|
raise TimeoutError, "Timeout waiting for: #{text.inspect}" if monotonic > deadline
|
|
83
88
|
read_available!
|
|
84
|
-
|
|
89
|
+
found = @output_mutex.synchronize { @output_buffer.include?(text) }
|
|
90
|
+
break if found
|
|
85
91
|
sleep 0.05
|
|
86
92
|
end
|
|
87
93
|
refresh_state!
|
|
@@ -114,7 +120,7 @@ module TUITD
|
|
|
114
120
|
# Get the terminal output (raw ANSI + text)
|
|
115
121
|
def raw_output
|
|
116
122
|
read_available!
|
|
117
|
-
@output_buffer
|
|
123
|
+
@output_mutex.synchronize { @output_buffer.dup }
|
|
118
124
|
end
|
|
119
125
|
|
|
120
126
|
# Get structured terminal state as a Hash
|
|
@@ -137,6 +143,8 @@ module TUITD
|
|
|
137
143
|
|
|
138
144
|
# Close the driver and clean up
|
|
139
145
|
def close
|
|
146
|
+
_stop_reader_thread
|
|
147
|
+
|
|
140
148
|
# Kill the process if still running
|
|
141
149
|
if @pid
|
|
142
150
|
begin
|
|
@@ -156,6 +164,30 @@ module TUITD
|
|
|
156
164
|
|
|
157
165
|
private
|
|
158
166
|
|
|
167
|
+
def _start_reader_thread
|
|
168
|
+
@reader_running = true
|
|
169
|
+
@reader_thread = Thread.new do
|
|
170
|
+
loop do
|
|
171
|
+
break unless @reader_running
|
|
172
|
+
begin
|
|
173
|
+
read_available!
|
|
174
|
+
rescue IOError, Errno::EIO
|
|
175
|
+
break
|
|
176
|
+
end
|
|
177
|
+
sleep 0.05
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def _stop_reader_thread
|
|
183
|
+
@reader_running = false
|
|
184
|
+
if @reader_thread
|
|
185
|
+
@reader_thread.join(1)
|
|
186
|
+
@reader_thread.kill rescue nil
|
|
187
|
+
@reader_thread = nil
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
159
191
|
def ensure_running!
|
|
160
192
|
raise Error, "Driver not started. Call #start first." if @stdin.nil?
|
|
161
193
|
raise Error, "Process exited (status: #{@wait_thr&.value&.exitstatus})" unless @wait_thr&.alive?
|
|
@@ -164,19 +196,35 @@ module TUITD
|
|
|
164
196
|
def read_available!
|
|
165
197
|
return false unless @stdout
|
|
166
198
|
|
|
167
|
-
|
|
168
|
-
|
|
199
|
+
data = @stdout.read_nonblock(4096)
|
|
200
|
+
|
|
201
|
+
@output_mutex.synchronize { @output_buffer << data }
|
|
202
|
+
|
|
203
|
+
respond_to_dsr if data.include?("\e[6n")
|
|
169
204
|
|
|
170
|
-
data = @stdout.readpartial(4096)
|
|
171
|
-
@output_buffer << data
|
|
172
205
|
true
|
|
173
|
-
rescue EOFError
|
|
206
|
+
rescue IO::WaitReadable, EOFError
|
|
174
207
|
false
|
|
175
208
|
end
|
|
176
209
|
|
|
210
|
+
def respond_to_dsr
|
|
211
|
+
@output_mutex.synchronize do
|
|
212
|
+
@state = ANSIParser.parse(@output_buffer, @rows, @cols)
|
|
213
|
+
@state[:raw] = @output_buffer.dup
|
|
214
|
+
@output_buffer.gsub!("\e[6n", "")
|
|
215
|
+
|
|
216
|
+
cursor = @state[:cursor]
|
|
217
|
+
response = "\e[#{cursor[:row] + 1};#{cursor[:col] + 1}R"
|
|
218
|
+
@stdin&.print(response)
|
|
219
|
+
@stdin&.flush
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
177
223
|
def refresh_state!
|
|
178
|
-
@
|
|
179
|
-
|
|
224
|
+
@output_mutex.synchronize do
|
|
225
|
+
@state = ANSIParser.parse(@output_buffer, @rows, @cols)
|
|
226
|
+
@state[:raw] = @output_buffer.dup
|
|
227
|
+
end
|
|
180
228
|
end
|
|
181
229
|
|
|
182
230
|
def monotonic
|
data/lib/tui_td/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tui-td
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Haluk Durmus
|
|
@@ -155,5 +155,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
155
155
|
requirements: []
|
|
156
156
|
rubygems_version: 4.0.6
|
|
157
157
|
specification_version: 4
|
|
158
|
-
summary: TUI
|
|
158
|
+
summary: TUI testing framework — language-agnostic via JSON tests and MCP
|
|
159
159
|
test_files: []
|