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 +7 -0
- data/CHANGELOG.md +9 -0
- data/LICENSE.txt +21 -0
- data/README.md +479 -0
- data/bin/tui-td +9 -0
- data/lib/tui_td/ansi_parser.rb +405 -0
- data/lib/tui_td/cli.rb +232 -0
- data/lib/tui_td/driver.rb +188 -0
- data/lib/tui_td/html_renderer.rb +228 -0
- data/lib/tui_td/matchers.rb +72 -0
- data/lib/tui_td/mcp/server.rb +463 -0
- data/lib/tui_td/screenshot.rb +271 -0
- data/lib/tui_td/state.rb +111 -0
- data/lib/tui_td/test_runner.rb +178 -0
- data/lib/tui_td/version.rb +5 -0
- data/lib/tui_td.rb +25 -0
- metadata +159 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "chunky_png"
|
|
4
|
+
|
|
5
|
+
module TUITD
|
|
6
|
+
class Screenshot
|
|
7
|
+
CELL_W = 8
|
|
8
|
+
CELL_H = 16
|
|
9
|
+
|
|
10
|
+
FONT = [
|
|
11
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # space (32)
|
|
12
|
+
0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, # ! (33)
|
|
13
|
+
0x00, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # " (34)
|
|
14
|
+
0x00, 0x00, 0x6c, 0x6c, 0xfe, 0x6c, 0x6c, 0x6c, 0x6c, 0xfe, 0x6c, 0x6c, 0x00, 0x00, 0x00, 0x00, # # (35)
|
|
15
|
+
0x00, 0x10, 0x7e, 0xd0, 0xd0, 0xd0, 0x7c, 0x16, 0x16, 0x16, 0x16, 0xfc, 0x10, 0x00, 0x00, 0x00, # $ (36)
|
|
16
|
+
0x00, 0x00, 0x06, 0x66, 0x6c, 0x0c, 0x18, 0x18, 0x30, 0x36, 0x66, 0x60, 0x00, 0x00, 0x00, 0x00, # % (37)
|
|
17
|
+
0x00, 0x00, 0x38, 0x6c, 0x6c, 0x6c, 0x38, 0x70, 0xda, 0xcc, 0xcc, 0x7a, 0x00, 0x00, 0x00, 0x00, # & (38)
|
|
18
|
+
0x00, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # ' (39)
|
|
19
|
+
0x00, 0x0e, 0x18, 0x30, 0x30, 0x60, 0x60, 0x60, 0x60, 0x30, 0x30, 0x18, 0x0e, 0x00, 0x00, 0x00, # ( (40)
|
|
20
|
+
0x00, 0x70, 0x18, 0x0c, 0x0c, 0x06, 0x06, 0x06, 0x06, 0x0c, 0x0c, 0x18, 0x70, 0x00, 0x00, 0x00, # ) (41)
|
|
21
|
+
0x00, 0x00, 0x00, 0x00, 0x66, 0x3c, 0x18, 0xff, 0x18, 0x3c, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, # * (42)
|
|
22
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x7e, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # + (43)
|
|
23
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30, 0x00, 0x00, 0x00, # , (44)
|
|
24
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # - (45)
|
|
25
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, # . (46)
|
|
26
|
+
0x00, 0x06, 0x06, 0x0c, 0x0c, 0x18, 0x18, 0x30, 0x30, 0x60, 0x60, 0xc0, 0xc0, 0x00, 0x00, 0x00, # / (47)
|
|
27
|
+
0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xce, 0xde, 0xf6, 0xe6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00, # 0 (48)
|
|
28
|
+
0x00, 0x00, 0x18, 0x38, 0x78, 0x58, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7e, 0x00, 0x00, 0x00, 0x00, # 1 (49)
|
|
29
|
+
0x00, 0x00, 0x7c, 0xc6, 0x06, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc6, 0xfe, 0x00, 0x00, 0x00, 0x00, # 2 (50)
|
|
30
|
+
0x00, 0x00, 0x7c, 0xc6, 0x06, 0x06, 0x3c, 0x06, 0x06, 0x06, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00, # 3 (51)
|
|
31
|
+
0x00, 0x00, 0xc0, 0xc0, 0xcc, 0xcc, 0xcc, 0xcc, 0xfe, 0x0c, 0x0c, 0x0c, 0x00, 0x00, 0x00, 0x00, # 4 (52)
|
|
32
|
+
0x00, 0x00, 0xfe, 0xc6, 0xc0, 0xc0, 0xfc, 0x06, 0x06, 0x06, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00, # 5 (53)
|
|
33
|
+
0x00, 0x00, 0x7c, 0xc6, 0xc0, 0xc0, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00, # 6 (54)
|
|
34
|
+
0x00, 0x00, 0xfe, 0xc6, 0x06, 0x06, 0x0c, 0x18, 0x30, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, # 7 (55)
|
|
35
|
+
0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00, # 8 (56)
|
|
36
|
+
0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x06, 0x06, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00, # 9 (57)
|
|
37
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, # : (58)
|
|
38
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30, 0x00, 0x00, 0x00, # ; (59)
|
|
39
|
+
0x00, 0x00, 0x06, 0x0c, 0x18, 0x30, 0x60, 0x60, 0x30, 0x18, 0x0c, 0x06, 0x00, 0x00, 0x00, 0x00, # < (60)
|
|
40
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # = (61)
|
|
41
|
+
0x00, 0x00, 0x60, 0x30, 0x18, 0x0c, 0x06, 0x06, 0x0c, 0x18, 0x30, 0x60, 0x00, 0x00, 0x00, 0x00, # > (62)
|
|
42
|
+
0x00, 0x00, 0x7c, 0xc6, 0x06, 0x0c, 0x18, 0x30, 0x30, 0x00, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, # ? (63)
|
|
43
|
+
0x00, 0x00, 0x00, 0x7c, 0xc2, 0xda, 0xda, 0xda, 0xda, 0xde, 0xc0, 0x7c, 0x00, 0x00, 0x00, 0x00, # @ (64)
|
|
44
|
+
0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xfe, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00, # A (65)
|
|
45
|
+
0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xfc, 0x00, 0x00, 0x00, 0x00, # B (66)
|
|
46
|
+
0x00, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00, # C (67)
|
|
47
|
+
0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xfc, 0x00, 0x00, 0x00, 0x00, # D (68)
|
|
48
|
+
0x00, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0xf8, 0xc0, 0xc0, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00, # E (69)
|
|
49
|
+
0x00, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0xf8, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x00, 0x00, 0x00, 0x00, # F (70)
|
|
50
|
+
0x00, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0xde, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00, # G (71)
|
|
51
|
+
0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xfe, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00, # H (72)
|
|
52
|
+
0x00, 0x00, 0x7e, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7e, 0x00, 0x00, 0x00, 0x00, # I (73)
|
|
53
|
+
0x00, 0x00, 0x7e, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xf0, 0x00, 0x00, 0x00, 0x00, # J (74)
|
|
54
|
+
0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xcc, 0xf8, 0xcc, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00, # K (75)
|
|
55
|
+
0x00, 0x00, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00, # L (76)
|
|
56
|
+
0x00, 0x00, 0xc6, 0xee, 0xfe, 0xd6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00, # M (77)
|
|
57
|
+
0x00, 0x00, 0xc6, 0xc6, 0xe6, 0xe6, 0xd6, 0xd6, 0xce, 0xce, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00, # N (78)
|
|
58
|
+
0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00, # O (79)
|
|
59
|
+
0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xfc, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x00, 0x00, 0x00, 0x00, # P (80)
|
|
60
|
+
0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xd6, 0xd6, 0x7c, 0x18, 0x0c, 0x00, 0x00, # Q (81)
|
|
61
|
+
0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00, # R (82)
|
|
62
|
+
0x00, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0x7c, 0x06, 0x06, 0x06, 0x06, 0xfc, 0x00, 0x00, 0x00, 0x00, # S (83)
|
|
63
|
+
0x00, 0x00, 0xff, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, # T (84)
|
|
64
|
+
0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00, # U (85)
|
|
65
|
+
0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x6c, 0x38, 0x10, 0x00, 0x00, 0x00, 0x00, # V (86)
|
|
66
|
+
0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xd6, 0xfe, 0xee, 0xc6, 0x00, 0x00, 0x00, 0x00, # W (87)
|
|
67
|
+
0x00, 0x00, 0xc6, 0xc6, 0xc6, 0x6c, 0x38, 0x6c, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00, # X (88)
|
|
68
|
+
0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x06, 0x06, 0x06, 0x06, 0xfc, 0x00, 0x00, 0x00, 0x00, # Y (89)
|
|
69
|
+
0x00, 0x00, 0xfe, 0x06, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0, 0xc0, 0xfe, 0x00, 0x00, 0x00, 0x00, # Z (90)
|
|
70
|
+
0x00, 0x3e, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3e, 0x00, 0x00, 0x00, # [ (91)
|
|
71
|
+
0x00, 0xc0, 0xc0, 0x60, 0x60, 0x30, 0x30, 0x18, 0x18, 0x0c, 0x0c, 0x06, 0x06, 0x00, 0x00, 0x00, # \ (92)
|
|
72
|
+
0x00, 0x7c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x7c, 0x00, 0x00, 0x00, # ] (93)
|
|
73
|
+
0x00, 0x10, 0x38, 0x6c, 0xc6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # ^ (94)
|
|
74
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x00, # _ (95)
|
|
75
|
+
0x00, 0x30, 0x18, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # ` (96)
|
|
76
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x06, 0x7e, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00, # a (97)
|
|
77
|
+
0x00, 0x00, 0xc0, 0xc0, 0xc0, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xfc, 0x00, 0x00, 0x00, 0x00, # b (98)
|
|
78
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00, # c (99)
|
|
79
|
+
0x00, 0x00, 0x06, 0x06, 0x06, 0x7e, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00, # d (100)
|
|
80
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc6, 0xc6, 0xfe, 0xc0, 0xc0, 0x7e, 0x00, 0x00, 0x00, 0x00, # e (101)
|
|
81
|
+
0x00, 0x00, 0x1e, 0x30, 0x30, 0x30, 0x7c, 0x30, 0x30, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, # f (102)
|
|
82
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x06, 0x06, 0xfc, 0x00, # g (103)
|
|
83
|
+
0x00, 0x00, 0xc0, 0xc0, 0xc0, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00, # h (104)
|
|
84
|
+
0x00, 0x00, 0x18, 0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1c, 0x00, 0x00, 0x00, 0x00, # i (105)
|
|
85
|
+
0x00, 0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x70, 0x00, # j (106)
|
|
86
|
+
0x00, 0x00, 0xc0, 0xc0, 0xc0, 0xcc, 0xd8, 0xf0, 0xf0, 0xd8, 0xcc, 0xc6, 0x00, 0x00, 0x00, 0x00, # k (107)
|
|
87
|
+
0x00, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x1c, 0x00, 0x00, 0x00, 0x00, # l (108)
|
|
88
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0xec, 0xd6, 0xd6, 0xd6, 0xd6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00, # m (109)
|
|
89
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00, # n (110)
|
|
90
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x00, 0x00, # o (111)
|
|
91
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xfc, 0xc0, 0xc0, 0xc0, 0x00, # p (112)
|
|
92
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x06, 0x06, 0x06, 0x00, # q (113)
|
|
93
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc6, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x00, 0x00, 0x00, 0x00, # r (114)
|
|
94
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc0, 0xc0, 0x7c, 0x06, 0x06, 0xfc, 0x00, 0x00, 0x00, 0x00, # s (115)
|
|
95
|
+
0x00, 0x00, 0x30, 0x30, 0x30, 0x7c, 0x30, 0x30, 0x30, 0x30, 0x30, 0x1e, 0x00, 0x00, 0x00, 0x00, # t (116)
|
|
96
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00, 0x00, 0x00, 0x00, # u (117)
|
|
97
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0x6c, 0x38, 0x10, 0x00, 0x00, 0x00, 0x00, # v (118)
|
|
98
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0xc6, 0xc6, 0xd6, 0xd6, 0xd6, 0xd6, 0x6e, 0x00, 0x00, 0x00, 0x00, # w (119)
|
|
99
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0xc6, 0x6c, 0x38, 0x38, 0x6c, 0xc6, 0xc6, 0x00, 0x00, 0x00, 0x00, # x (120)
|
|
100
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x06, 0x06, 0xfc, 0x00, # y (121)
|
|
101
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xfe, 0x00, 0x00, 0x00, 0x00, # z (122)
|
|
102
|
+
0x00, 0x0e, 0x18, 0x18, 0x18, 0x18, 0x70, 0x70, 0x18, 0x18, 0x18, 0x18, 0x0e, 0x00, 0x00, 0x00, # { (123)
|
|
103
|
+
0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, # | (124)
|
|
104
|
+
0x00, 0x70, 0x18, 0x18, 0x18, 0x18, 0x0e, 0x0e, 0x18, 0x18, 0x18, 0x18, 0x70, 0x00, 0x00, 0x00, # } (125)
|
|
105
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x7e, 0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # ~ (126)
|
|
106
|
+
].freeze
|
|
107
|
+
private_constant :FONT
|
|
108
|
+
|
|
109
|
+
ANSI_RGB = {
|
|
110
|
+
"black" => [0x00, 0x00, 0x00],
|
|
111
|
+
"red" => [0xAA, 0x00, 0x00],
|
|
112
|
+
"green" => [0x00, 0xAA, 0x00],
|
|
113
|
+
"yellow" => [0xAA, 0x55, 0x00],
|
|
114
|
+
"blue" => [0x00, 0x00, 0xAA],
|
|
115
|
+
"magenta" => [0xAA, 0x00, 0xAA],
|
|
116
|
+
"cyan" => [0x00, 0xAA, 0xAA],
|
|
117
|
+
"white" => [0xAA, 0xAA, 0xAA],
|
|
118
|
+
"bright_black" => [0x55, 0x55, 0x55],
|
|
119
|
+
"bright_red" => [0xFF, 0x55, 0x55],
|
|
120
|
+
"bright_green" => [0x55, 0xFF, 0x55],
|
|
121
|
+
"bright_yellow" => [0xFF, 0xFF, 0x55],
|
|
122
|
+
"bright_blue" => [0x55, 0x55, 0xFF],
|
|
123
|
+
"bright_magenta"=> [0xFF, 0x55, 0xFF],
|
|
124
|
+
"bright_cyan" => [0x55, 0xFF, 0xFF],
|
|
125
|
+
"bright_white" => [0xFF, 0xFF, 0xFF],
|
|
126
|
+
}.freeze
|
|
127
|
+
private_constant :ANSI_RGB
|
|
128
|
+
|
|
129
|
+
CUBE = [0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF].freeze
|
|
130
|
+
private_constant :CUBE
|
|
131
|
+
|
|
132
|
+
ANSI_INDEX = %w[
|
|
133
|
+
black red green yellow blue magenta cyan white
|
|
134
|
+
bright_black bright_red bright_green bright_yellow
|
|
135
|
+
bright_blue bright_magenta bright_cyan bright_white
|
|
136
|
+
].freeze
|
|
137
|
+
private_constant :ANSI_INDEX
|
|
138
|
+
|
|
139
|
+
DEFAULT_FG = [0xC0, 0xC0, 0xC0].freeze
|
|
140
|
+
DEFAULT_BG = [0x00, 0x00, 0x00].freeze
|
|
141
|
+
|
|
142
|
+
def initialize(state)
|
|
143
|
+
@state = state
|
|
144
|
+
@rows = _dig(state, :size, :rows) || 40
|
|
145
|
+
@cols = _dig(state, :size, :cols) || 120
|
|
146
|
+
@grid = state[:rows] || state["rows"] || []
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def render(output_path)
|
|
150
|
+
width = @cols * CELL_W
|
|
151
|
+
height = @rows * CELL_H
|
|
152
|
+
image = ChunkyPNG::Image.new(width, height, ChunkyPNG::Color::BLACK)
|
|
153
|
+
|
|
154
|
+
@grid.each_with_index do |row, ri|
|
|
155
|
+
next unless row
|
|
156
|
+
row.each_with_index do |cell, ci|
|
|
157
|
+
next unless cell
|
|
158
|
+
render_cell(image, ri, ci, cell)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
image.save(output_path)
|
|
163
|
+
output_path
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private
|
|
167
|
+
|
|
168
|
+
def render_cell(image, ri, ci, cell)
|
|
169
|
+
char = cell[:char] || cell["char"] || " "
|
|
170
|
+
fg = cell[:fg] || cell["fg"] || "default"
|
|
171
|
+
bg = cell[:bg] || cell["bg"] || "default"
|
|
172
|
+
bold = cell[:bold] || cell["bold"] || false
|
|
173
|
+
italic = cell[:italic] || cell["italic"] || false
|
|
174
|
+
underline = cell[:underline] || cell["underline"] || false
|
|
175
|
+
|
|
176
|
+
fg_rgb = resolve_color(fg, DEFAULT_FG)
|
|
177
|
+
bg_rgb = resolve_color(bg, DEFAULT_BG)
|
|
178
|
+
|
|
179
|
+
px = ci * CELL_W
|
|
180
|
+
py = ri * CELL_H
|
|
181
|
+
|
|
182
|
+
fill_rect(image, px, py, CELL_W, CELL_H, bg_rgb)
|
|
183
|
+
|
|
184
|
+
return if char == " " || char.ord < 32 || char.ord > 126
|
|
185
|
+
|
|
186
|
+
rows_data = glyph_rows(char)
|
|
187
|
+
return unless rows_data
|
|
188
|
+
|
|
189
|
+
draw_glyph(image, px, py, rows_data, fg_rgb, bold: bold, italic: italic)
|
|
190
|
+
|
|
191
|
+
draw_underline(image, px, py, CELL_W, fg_rgb) if underline
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def resolve_color(name, fallback)
|
|
195
|
+
case name
|
|
196
|
+
when "default"
|
|
197
|
+
fallback
|
|
198
|
+
when /^#([0-9a-fA-F]{6})$/
|
|
199
|
+
[$1[0..1].to_i(16), $1[2..3].to_i(16), $1[4..5].to_i(16)]
|
|
200
|
+
when /\Acolor(\d+)\z/
|
|
201
|
+
xterm_256($1.to_i)
|
|
202
|
+
when /\Abright_(.+)\z/
|
|
203
|
+
ANSI_RGB[name] || fallback
|
|
204
|
+
else
|
|
205
|
+
ANSI_RGB[name] || fallback
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def xterm_256(index)
|
|
210
|
+
if index < 16
|
|
211
|
+
name = ANSI_INDEX[index]
|
|
212
|
+
ANSI_RGB[name] || DEFAULT_FG
|
|
213
|
+
elsif index < 232
|
|
214
|
+
r = CUBE[((index - 16) / 36) % 6]
|
|
215
|
+
g = CUBE[((index - 16) / 6) % 6]
|
|
216
|
+
b = CUBE[(index - 16) % 6]
|
|
217
|
+
[r, g, b]
|
|
218
|
+
else
|
|
219
|
+
v = 8 + (index - 232) * 10
|
|
220
|
+
[v, v, v]
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def fill_rect(image, x, y, w, h, rgb)
|
|
225
|
+
color = ChunkyPNG::Color.rgb(*rgb)
|
|
226
|
+
h.times do |dy|
|
|
227
|
+
w.times do |dx|
|
|
228
|
+
image[x + dx, y + dy] = color
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def glyph_rows(char)
|
|
234
|
+
idx = (char.ord - 32) * 16
|
|
235
|
+
return nil if idx < 0 || idx + 15 >= FONT.length
|
|
236
|
+
|
|
237
|
+
FONT[idx, 16]
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def draw_glyph(image, px, py, rows, fg_rgb, bold:, italic:)
|
|
241
|
+
color = ChunkyPNG::Color.rgb(*fg_rgb)
|
|
242
|
+
|
|
243
|
+
rows.each_with_index do |byte, dy|
|
|
244
|
+
next if byte == 0
|
|
245
|
+
|
|
246
|
+
slant = italic ? dy / 8 : 0
|
|
247
|
+
|
|
248
|
+
8.times do |dx|
|
|
249
|
+
next unless (byte >> (7 - dx)) & 1 == 1
|
|
250
|
+
|
|
251
|
+
image[px + dx + slant, py + dy] = color
|
|
252
|
+
image[px + dx + slant + 1, py + dy] = color if bold
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def draw_underline(image, px, py, w, fg_rgb)
|
|
258
|
+
color = ChunkyPNG::Color.rgb(*fg_rgb)
|
|
259
|
+
y = py + CELL_H - 2
|
|
260
|
+
w.times { |dx| image[px + dx, y] = color }
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def _dig(hash, *keys)
|
|
264
|
+
keys.each do |k|
|
|
265
|
+
return nil unless hash
|
|
266
|
+
hash = hash[k] || hash[k.to_s]
|
|
267
|
+
end
|
|
268
|
+
hash
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
data/lib/tui_td/state.rb
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TUITD
|
|
4
|
+
# Represents the parsed state of a terminal screen.
|
|
5
|
+
# Provides high-level query methods for AI consumption.
|
|
6
|
+
class State
|
|
7
|
+
attr_reader :rows, :cols, :grid, :cursor
|
|
8
|
+
|
|
9
|
+
def initialize(data)
|
|
10
|
+
@rows = data[:size][:rows]
|
|
11
|
+
@cols = data[:size][:cols]
|
|
12
|
+
@grid = data[:rows]
|
|
13
|
+
@cursor = data[:cursor]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Get plain text of the entire terminal (no ANSI)
|
|
17
|
+
def plain_text
|
|
18
|
+
@grid.map { |row| row.map { |c| c[:char] }.join.rstrip }.join("\n")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Get text at a specific position
|
|
22
|
+
def text_at(row, col, length = @cols - col)
|
|
23
|
+
return "" if row >= @rows || col >= @cols
|
|
24
|
+
@grid[row][col, length].map { |c| c[:char] }.join
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Search for text across the entire terminal
|
|
28
|
+
def find_text(pattern)
|
|
29
|
+
results = []
|
|
30
|
+
@grid.each_with_index do |row, ri|
|
|
31
|
+
text = row.map { |c| c[:char] }.join
|
|
32
|
+
pos = 0
|
|
33
|
+
while (match = text.index(pattern, pos))
|
|
34
|
+
results << { row: ri, col: match, text: pattern, full_line: text }
|
|
35
|
+
pos = match + 1
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
results
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Get the color at a specific cell
|
|
42
|
+
def foreground_at(row, col)
|
|
43
|
+
return nil if row >= @rows || col >= @cols
|
|
44
|
+
@grid[row][col][:fg]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def background_at(row, col)
|
|
48
|
+
return nil if row >= @rows || col >= @cols
|
|
49
|
+
@grid[row][col][:bg]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def style_at(row, col)
|
|
53
|
+
return nil if row >= @rows || col >= @cols
|
|
54
|
+
cell = @grid[row][col]
|
|
55
|
+
{ bold: cell[:bold], italic: cell[:italic], underline: cell[:underline] }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def to_ai_json
|
|
59
|
+
h = extract_highlights
|
|
60
|
+
cursor_info = @cursor.is_a?(Hash) ? @cursor : {}
|
|
61
|
+
r = cursor_info[:row] || cursor_info["row"] || 0
|
|
62
|
+
c = cursor_info[:col] || cursor_info["col"] || 0
|
|
63
|
+
styled_count = h.count { |hl| hl[:bold] || hl[:italic] || hl[:underline] || hl[:fg] || hl[:bg] }
|
|
64
|
+
|
|
65
|
+
summary = +"Cursor at [#{r},#{c}]. "
|
|
66
|
+
summary << "#{styled_count} styled row#{styled_count == 1 ? '' : 's'}"
|
|
67
|
+
fgs = h.flat_map { |hl| hl[:fg] }.compact.uniq
|
|
68
|
+
bgs = h.flat_map { |hl| hl[:bg] }.compact.uniq
|
|
69
|
+
summary << ", colors: fg=#{fgs.sort.join(',')}" unless fgs.empty?
|
|
70
|
+
summary << ", bg=#{bgs.sort.join(',')}" unless bgs.empty?
|
|
71
|
+
summary << "."
|
|
72
|
+
|
|
73
|
+
{
|
|
74
|
+
size: { rows: @rows, cols: @cols },
|
|
75
|
+
cursor: cursor_info,
|
|
76
|
+
text: plain_text,
|
|
77
|
+
highlights: h,
|
|
78
|
+
summary: summary,
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def extract_highlights
|
|
85
|
+
highlights = []
|
|
86
|
+
@grid.each_with_index do |row, ri|
|
|
87
|
+
row_text = row.map { |c| c[:char] }.join
|
|
88
|
+
next if row_text.strip.empty?
|
|
89
|
+
|
|
90
|
+
fgs = row.map { |c| c[:fg] || c["fg"] || "default" }
|
|
91
|
+
.uniq.reject { |c| c == "default" }
|
|
92
|
+
bgs = row.map { |c| c[:bg] || c["bg"] || "default" }
|
|
93
|
+
.uniq.reject { |c| c == "default" }
|
|
94
|
+
bold = row.any? { |c| c[:bold] || c["bold"] }
|
|
95
|
+
italic = row.any? { |c| c[:italic] || c["italic"] }
|
|
96
|
+
underline = row.any? { |c| c[:underline] || c["underline"] }
|
|
97
|
+
|
|
98
|
+
next if fgs.empty? && bgs.empty? && !bold && !italic && !underline
|
|
99
|
+
|
|
100
|
+
h = { row: ri, text: row_text }
|
|
101
|
+
h[:bold] = true if bold
|
|
102
|
+
h[:italic] = true if italic
|
|
103
|
+
h[:underline] = true if underline
|
|
104
|
+
h[:fg] = fgs.size == 1 ? fgs.first : fgs unless fgs.empty?
|
|
105
|
+
h[:bg] = bgs.size == 1 ? bgs.first : bgs unless bgs.empty?
|
|
106
|
+
highlights << h
|
|
107
|
+
end
|
|
108
|
+
highlights
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module TUITD
|
|
6
|
+
# Executes TUI tests defined in JSON format.
|
|
7
|
+
#
|
|
8
|
+
# plan = File.read("test/hello.json")
|
|
9
|
+
# results = TUITD::TestRunner.new(plan).run
|
|
10
|
+
# puts results[:passed] # => true
|
|
11
|
+
#
|
|
12
|
+
# JSON format:
|
|
13
|
+
# {
|
|
14
|
+
# "name": "My test",
|
|
15
|
+
# "rows": 24, "cols": 80, "timeout": 10,
|
|
16
|
+
# "steps": [
|
|
17
|
+
# {"start": "my_tui"},
|
|
18
|
+
# {"wait_for_text": "> "},
|
|
19
|
+
# {"send": "hello\n"},
|
|
20
|
+
# {"assert_text": "hello"},
|
|
21
|
+
# {"assert_fg": [0, 0], "is": "cyan"},
|
|
22
|
+
# {"close": true}
|
|
23
|
+
# ]
|
|
24
|
+
# }
|
|
25
|
+
#
|
|
26
|
+
class TestRunner
|
|
27
|
+
Result = Struct.new(:step, :passed, :message, keyword_init: true)
|
|
28
|
+
|
|
29
|
+
def initialize(source)
|
|
30
|
+
raw = source.is_a?(String) ? JSON.parse(source) : source
|
|
31
|
+
@plan = raw.transform_keys(&:to_sym)
|
|
32
|
+
@plan[:steps] = @plan[:steps].map { |s| s.transform_keys(&:to_sym) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def run
|
|
36
|
+
results = []
|
|
37
|
+
all_passed = true
|
|
38
|
+
driver = nil
|
|
39
|
+
rows = @plan[:rows] || 40
|
|
40
|
+
cols = @plan[:cols] || 120
|
|
41
|
+
timeout = @plan[:timeout] || 30
|
|
42
|
+
chdir = @plan[:chdir]
|
|
43
|
+
|
|
44
|
+
@plan[:steps].each do |step|
|
|
45
|
+
action = step.keys.first.to_s
|
|
46
|
+
value = step.values.first
|
|
47
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
48
|
+
|
|
49
|
+
begin
|
|
50
|
+
r = case action
|
|
51
|
+
when "start"
|
|
52
|
+
driver&.close
|
|
53
|
+
driver = Driver.new(value.to_s, rows: rows, cols: cols, timeout: timeout, chdir: chdir)
|
|
54
|
+
driver.start
|
|
55
|
+
Result.new(step: action, passed: true, message: "Started: #{value}")
|
|
56
|
+
|
|
57
|
+
when "send"
|
|
58
|
+
ensure_driver!(driver)
|
|
59
|
+
driver.send(value.to_s)
|
|
60
|
+
Result.new(step: action, passed: true, message: "Sent #{value.to_s.length} characters")
|
|
61
|
+
|
|
62
|
+
when "send_key"
|
|
63
|
+
ensure_driver!(driver)
|
|
64
|
+
driver.send_keys(value.to_s.to_sym)
|
|
65
|
+
Result.new(step: action, passed: true, message: "Sent key: #{value}")
|
|
66
|
+
|
|
67
|
+
when "wait_for_text"
|
|
68
|
+
ensure_driver!(driver)
|
|
69
|
+
driver.wait_for_text(value.to_s)
|
|
70
|
+
Result.new(step: action, passed: true, message: "Found: #{value}")
|
|
71
|
+
|
|
72
|
+
when "wait_for_stable"
|
|
73
|
+
ensure_driver!(driver)
|
|
74
|
+
driver.wait_for_stable
|
|
75
|
+
Result.new(step: action, passed: true, message: "Stable")
|
|
76
|
+
|
|
77
|
+
when "assert_text"
|
|
78
|
+
ensure_driver!(driver)
|
|
79
|
+
state = State.new(driver.state_data)
|
|
80
|
+
if state.find_text(value.to_s).any?
|
|
81
|
+
Result.new(step: action, passed: true, message: "Text found: #{value}")
|
|
82
|
+
else
|
|
83
|
+
Result.new(step: action, passed: false, message: "Text NOT found: #{value}")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
when "assert_fg"
|
|
87
|
+
ensure_driver!(driver)
|
|
88
|
+
row, col = coords(step)
|
|
89
|
+
expected = step[:is] || step["is"]
|
|
90
|
+
state = State.new(driver.state_data)
|
|
91
|
+
actual = state.foreground_at(row, col)
|
|
92
|
+
if actual == expected
|
|
93
|
+
Result.new(step: action, passed: true, message: "FG at [#{row},#{col}] is #{expected}")
|
|
94
|
+
else
|
|
95
|
+
Result.new(step: action, passed: false, message: "FG at [#{row},#{col}] is #{actual}, expected #{expected}")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
when "assert_bg"
|
|
99
|
+
ensure_driver!(driver)
|
|
100
|
+
row, col = coords(step)
|
|
101
|
+
expected = step[:is] || step["is"]
|
|
102
|
+
state = State.new(driver.state_data)
|
|
103
|
+
actual = state.background_at(row, col)
|
|
104
|
+
if actual == expected
|
|
105
|
+
Result.new(step: action, passed: true, message: "BG at [#{row},#{col}] is #{expected}")
|
|
106
|
+
else
|
|
107
|
+
Result.new(step: action, passed: false, message: "BG at [#{row},#{col}] is #{actual}, expected #{expected}")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
when "assert_style"
|
|
111
|
+
ensure_driver!(driver)
|
|
112
|
+
row, col = coords(step)
|
|
113
|
+
state = State.new(driver.state_data)
|
|
114
|
+
actual = state.style_at(row, col)
|
|
115
|
+
expected = {}
|
|
116
|
+
expected[:bold] = step[:bold] unless step[:bold].nil?
|
|
117
|
+
expected[:italic] = step[:italic] unless step[:italic].nil?
|
|
118
|
+
expected[:underline] = step[:underline] unless step[:underline].nil?
|
|
119
|
+
match = expected.all? { |k, v| actual[k] == v }
|
|
120
|
+
if match
|
|
121
|
+
Result.new(step: action, passed: true, message: "Style at [#{row},#{col}] matches #{expected}")
|
|
122
|
+
else
|
|
123
|
+
Result.new(step: action, passed: false, message: "Style at [#{row},#{col}] is #{actual}, expected #{expected}")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
when "screenshot"
|
|
127
|
+
ensure_driver!(driver)
|
|
128
|
+
path = value.is_a?(String) ? value : "/tmp/tui_td_#{Time.now.to_i}.png"
|
|
129
|
+
driver.screenshot(path)
|
|
130
|
+
Result.new(step: action, passed: true, message: "Saved: #{path}")
|
|
131
|
+
|
|
132
|
+
when "html"
|
|
133
|
+
ensure_driver!(driver)
|
|
134
|
+
path = value.is_a?(String) ? value : "/tmp/tui_td_#{Time.now.to_i}.html"
|
|
135
|
+
HtmlRenderer.new(driver.state_data).render(path)
|
|
136
|
+
Result.new(step: action, passed: true, message: "Saved: #{path}")
|
|
137
|
+
|
|
138
|
+
when "close"
|
|
139
|
+
driver&.close
|
|
140
|
+
driver = nil
|
|
141
|
+
Result.new(step: action, passed: true, message: "Closed")
|
|
142
|
+
|
|
143
|
+
else
|
|
144
|
+
Result.new(step: action, passed: false, message: "Unknown action: #{action}")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
rescue StandardError => e
|
|
148
|
+
r = Result.new(step: action, passed: false, message: "#{e.class}: #{e.message}")
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
results << r
|
|
152
|
+
all_passed &&= r.passed
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
driver&.close
|
|
156
|
+
|
|
157
|
+
{
|
|
158
|
+
name: @plan[:name] || "(unnamed)",
|
|
159
|
+
passed: all_passed,
|
|
160
|
+
results: results.map(&:to_h)
|
|
161
|
+
}
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
private
|
|
165
|
+
|
|
166
|
+
def ensure_driver!(driver)
|
|
167
|
+
raise Error, "No session. Add a 'start' step first." if driver.nil?
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def coords(step)
|
|
171
|
+
pos = step[:assert_fg] || step[:assert_bg] || step[:assert_style]
|
|
172
|
+
pos = value if pos.nil? && (value = step.values.first).is_a?(Array)
|
|
173
|
+
row = pos.is_a?(Array) ? pos[0] : (pos[:row] || pos["row"] || 0)
|
|
174
|
+
col = pos.is_a?(Array) ? pos[1] : (pos[:col] || pos["col"] || 0)
|
|
175
|
+
[row, col]
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
data/lib/tui_td.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TUITD
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
require_relative "tui_td/version"
|
|
8
|
+
require_relative "tui_td/driver"
|
|
9
|
+
require_relative "tui_td/ansi_parser"
|
|
10
|
+
require_relative "tui_td/state"
|
|
11
|
+
require_relative "tui_td/screenshot"
|
|
12
|
+
require_relative "tui_td/html_renderer"
|
|
13
|
+
require_relative "tui_td/test_runner"
|
|
14
|
+
require_relative "tui_td/mcp/server"
|
|
15
|
+
require_relative "tui_td/cli"
|
|
16
|
+
|
|
17
|
+
module TUITD
|
|
18
|
+
|
|
19
|
+
# Convenience method: start a TUI driver, capture initial state
|
|
20
|
+
def self.drive(command, **opts)
|
|
21
|
+
driver = Driver.new(command, **opts)
|
|
22
|
+
driver.start
|
|
23
|
+
driver
|
|
24
|
+
end
|
|
25
|
+
end
|