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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b759a458f409fc1596611c306a59a14e04be1842b6f44fff4e45eebd61f6b673
4
- data.tar.gz: deb8ebbd1e4804f7874f3af677969e333cda8321fb935a287789fd166ab20a29
3
+ metadata.gz: be8d93991312317febcad6a713d4dd58e6e670e620211bae0adc0db0bdd21da1
4
+ data.tar.gz: 5618bc612addd9be3c634ebd4009800ddd6f7da9396ad5d950c8f831727398f8
5
5
  SHA512:
6
- metadata.gz: ecb87f37f0af8ec853a37465aa822b4c547b389ef064579243522429c48e885b56d7fdf9d701cc7e5e08bed1adeb5d0a7a035301351531fe68806b24d15f9923
7
- data.tar.gz: 53fe8549f0a04d5bf403dda60ab3016e7be26f817453693f6fda68288a926bf89bdd6a86dc858e624a5f4e7ed618d1c8adc1b1e545dedf9fdf2d0926bcfa2543
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.0 (initial)
3
+ ## 0.1.3
4
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)
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 help
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
@@ -137,16 +137,16 @@ module TUITD
137
137
  else
138
138
  i += 1
139
139
  end
140
- elsif processed[i] =~ /[[:print:]]/
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] = processed[i]
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 += 1
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 Start MCP server (JSON-RPC over stdio)"
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 " test <file> Run JSON test file"
26
- opts.separator " help Show this help"
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 nil, "help"
78
- puts OptionParser.new { |o| o.banner = "Usage: tui-td <command> [options]" }
79
- exit 0
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TUITD
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.3"
5
5
  end
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.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Haluk Durmus