tui-td 0.2.4 → 0.2.6
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 +15 -0
- data/README.md +26 -52
- data/lib/tui_td/ansi_parser.rb +312 -37
- data/lib/tui_td/ansi_utils.rb +75 -0
- data/lib/tui_td/driver.rb +1 -1
- data/lib/tui_td/html_renderer.rb +79 -72
- data/lib/tui_td/screenshot.rb +186 -67
- data/lib/tui_td/state.rb +11 -1
- data/lib/tui_td/test_runner.rb +141 -123
- data/lib/tui_td/version.rb +1 -1
- data/lib/tui_td.rb +1 -0
- metadata +2 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TUITD
|
|
4
|
+
# Shared ANSI color constants and helpers.
|
|
5
|
+
# Used by Screenshot, HtmlRenderer, and other color-aware renderers.
|
|
6
|
+
module ANSIUtils
|
|
7
|
+
ANSI_RGB = {
|
|
8
|
+
"black" => [0x00, 0x00, 0x00],
|
|
9
|
+
"red" => [0xAA, 0x00, 0x00],
|
|
10
|
+
"green" => [0x00, 0xAA, 0x00],
|
|
11
|
+
"yellow" => [0xAA, 0x55, 0x00],
|
|
12
|
+
"blue" => [0x00, 0x00, 0xAA],
|
|
13
|
+
"magenta" => [0xAA, 0x00, 0xAA],
|
|
14
|
+
"cyan" => [0x00, 0xAA, 0xAA],
|
|
15
|
+
"white" => [0xAA, 0xAA, 0xAA],
|
|
16
|
+
"bright_black" => [0x55, 0x55, 0x55],
|
|
17
|
+
"bright_red" => [0xFF, 0x55, 0x55],
|
|
18
|
+
"bright_green" => [0x55, 0xFF, 0x55],
|
|
19
|
+
"bright_yellow" => [0xFF, 0xFF, 0x55],
|
|
20
|
+
"bright_blue" => [0x55, 0x55, 0xFF],
|
|
21
|
+
"bright_magenta"=> [0xFF, 0x55, 0xFF],
|
|
22
|
+
"bright_cyan" => [0x55, 0xFF, 0xFF],
|
|
23
|
+
"bright_white" => [0xFF, 0xFF, 0xFF],
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
CUBE = [0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF].freeze
|
|
27
|
+
|
|
28
|
+
ANSI_INDEX = %w[
|
|
29
|
+
black red green yellow blue magenta cyan white
|
|
30
|
+
bright_black bright_red bright_green bright_yellow
|
|
31
|
+
bright_blue bright_magenta bright_cyan bright_white
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
DEFAULT_FG = [0xC0, 0xC0, 0xC0].freeze
|
|
35
|
+
DEFAULT_BG = [0x00, 0x00, 0x00].freeze
|
|
36
|
+
|
|
37
|
+
def resolve_color(name, fallback)
|
|
38
|
+
case name
|
|
39
|
+
when "default"
|
|
40
|
+
fallback
|
|
41
|
+
when /^#([0-9a-fA-F]{6})$/
|
|
42
|
+
[$1[0..1].to_i(16), $1[2..3].to_i(16), $1[4..5].to_i(16)]
|
|
43
|
+
when /\Acolor(\d+)\z/
|
|
44
|
+
xterm_256($1.to_i)
|
|
45
|
+
when /\Abright_(.+)\z/
|
|
46
|
+
ANSI_RGB[name] || fallback
|
|
47
|
+
else
|
|
48
|
+
ANSI_RGB[name] || fallback
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def xterm_256(index)
|
|
53
|
+
if index < 16
|
|
54
|
+
name = ANSI_INDEX[index]
|
|
55
|
+
ANSI_RGB[name] || DEFAULT_FG
|
|
56
|
+
elsif index < 232
|
|
57
|
+
r = CUBE[((index - 16) / 36) % 6]
|
|
58
|
+
g = CUBE[((index - 16) / 6) % 6]
|
|
59
|
+
b = CUBE[(index - 16) % 6]
|
|
60
|
+
[r, g, b]
|
|
61
|
+
else
|
|
62
|
+
v = 8 + (index - 232) * 10
|
|
63
|
+
[v, v, v]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def _dig(hash, *keys)
|
|
68
|
+
keys.each do |k|
|
|
69
|
+
return nil unless hash
|
|
70
|
+
hash = hash[k] || hash[k.to_s]
|
|
71
|
+
end
|
|
72
|
+
hash
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/lib/tui_td/driver.rb
CHANGED
data/lib/tui_td/html_renderer.rb
CHANGED
|
@@ -1,39 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "ansi_utils"
|
|
4
|
+
|
|
3
5
|
module TUITD
|
|
4
6
|
# Renders terminal state as a self-contained HTML document.
|
|
5
7
|
# Faithfully reproduces what a TUI application shows — colors, styles,
|
|
6
8
|
# cursor position — so an LLM or human can "see" the terminal.
|
|
7
9
|
class HtmlRenderer
|
|
8
|
-
|
|
9
|
-
"black" => [0x00, 0x00, 0x00],
|
|
10
|
-
"red" => [0xAA, 0x00, 0x00],
|
|
11
|
-
"green" => [0x00, 0xAA, 0x00],
|
|
12
|
-
"yellow" => [0xAA, 0x55, 0x00],
|
|
13
|
-
"blue" => [0x00, 0x00, 0xAA],
|
|
14
|
-
"magenta" => [0xAA, 0x00, 0xAA],
|
|
15
|
-
"cyan" => [0x00, 0xAA, 0xAA],
|
|
16
|
-
"white" => [0xAA, 0xAA, 0xAA],
|
|
17
|
-
"bright_black" => [0x55, 0x55, 0x55],
|
|
18
|
-
"bright_red" => [0xFF, 0x55, 0x55],
|
|
19
|
-
"bright_green" => [0x55, 0xFF, 0x55],
|
|
20
|
-
"bright_yellow" => [0xFF, 0xFF, 0x55],
|
|
21
|
-
"bright_blue" => [0x55, 0x55, 0xFF],
|
|
22
|
-
"bright_magenta"=> [0xFF, 0x55, 0xFF],
|
|
23
|
-
"bright_cyan" => [0x55, 0xFF, 0xFF],
|
|
24
|
-
"bright_white" => [0xFF, 0xFF, 0xFF],
|
|
25
|
-
}.freeze
|
|
26
|
-
|
|
27
|
-
CUBE = [0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF].freeze
|
|
28
|
-
|
|
29
|
-
ANSI_INDEX = %w[
|
|
30
|
-
black red green yellow blue magenta cyan white
|
|
31
|
-
bright_black bright_red bright_green bright_yellow
|
|
32
|
-
bright_blue bright_magenta bright_cyan bright_white
|
|
33
|
-
].freeze
|
|
34
|
-
|
|
35
|
-
DEFAULT_FG = [0xC0, 0xC0, 0xC0].freeze
|
|
36
|
-
DEFAULT_BG = [0x00, 0x00, 0x00].freeze
|
|
10
|
+
include ANSIUtils
|
|
37
11
|
|
|
38
12
|
def initialize(state)
|
|
39
13
|
@state = state
|
|
@@ -98,6 +72,49 @@ module TUITD
|
|
|
98
72
|
z-index: 1;
|
|
99
73
|
position: relative;
|
|
100
74
|
}
|
|
75
|
+
.cursor-cell.cursor-hidden {
|
|
76
|
+
outline: none !important;
|
|
77
|
+
border: none !important;
|
|
78
|
+
background-color: transparent !important;
|
|
79
|
+
color: inherit !important;
|
|
80
|
+
}
|
|
81
|
+
.cursor-cell.cursor-block {
|
|
82
|
+
outline: none;
|
|
83
|
+
background-color: #fff;
|
|
84
|
+
color: #000 !important;
|
|
85
|
+
}
|
|
86
|
+
.cursor-cell.cursor-block.blink {
|
|
87
|
+
animation: cursor-block-blink 1s step-end infinite;
|
|
88
|
+
}
|
|
89
|
+
.cursor-cell.cursor-underline {
|
|
90
|
+
outline: none;
|
|
91
|
+
border-bottom: 2px solid #fff;
|
|
92
|
+
}
|
|
93
|
+
.cursor-cell.cursor-underline.blink {
|
|
94
|
+
animation: cursor-underline-blink 1s step-end infinite;
|
|
95
|
+
}
|
|
96
|
+
.cursor-cell.cursor-bar {
|
|
97
|
+
outline: none;
|
|
98
|
+
border-left: 2px solid #fff;
|
|
99
|
+
}
|
|
100
|
+
.cursor-cell.cursor-bar.blink {
|
|
101
|
+
animation: cursor-bar-blink 1s step-end infinite;
|
|
102
|
+
}
|
|
103
|
+
@keyframes cursor-block-blink {
|
|
104
|
+
50% { background-color: transparent; color: inherit; }
|
|
105
|
+
}
|
|
106
|
+
@keyframes cursor-underline-blink {
|
|
107
|
+
50% { border-bottom-color: transparent; }
|
|
108
|
+
}
|
|
109
|
+
@keyframes cursor-bar-blink {
|
|
110
|
+
50% { border-left-color: transparent; }
|
|
111
|
+
}
|
|
112
|
+
@keyframes term-blink {
|
|
113
|
+
50% { opacity: 0; }
|
|
114
|
+
}
|
|
115
|
+
.term-blink {
|
|
116
|
+
animation: term-blink 1s step-end infinite;
|
|
117
|
+
}
|
|
101
118
|
CSS
|
|
102
119
|
end
|
|
103
120
|
|
|
@@ -129,17 +146,20 @@ module TUITD
|
|
|
129
146
|
bold = cell[:bold] || cell["bold"] || false
|
|
130
147
|
italic = cell[:italic] || cell["italic"] || false
|
|
131
148
|
underline = cell[:underline] || cell["underline"] || false
|
|
149
|
+
blink = cell[:blink] || cell["blink"] || false
|
|
132
150
|
|
|
133
|
-
style_key = [fg, bg, bold, italic, underline]
|
|
151
|
+
style_key = [fg, bg, bold, italic, underline, blink]
|
|
152
|
+
is_cur = is_cursor?(ri, ci)
|
|
134
153
|
|
|
135
|
-
if current_run && current_run[:key] == style_key
|
|
154
|
+
if current_run && current_run[:key] == style_key && !current_run[:has_cursor] && !is_cur
|
|
136
155
|
current_run[:chars] << char
|
|
137
156
|
else
|
|
138
157
|
current_run = {
|
|
139
158
|
key: style_key,
|
|
140
159
|
chars: [char],
|
|
141
160
|
style: cell_style(fg, bg, bold, italic, underline),
|
|
142
|
-
has_cursor:
|
|
161
|
+
has_cursor: is_cur,
|
|
162
|
+
blink: blink
|
|
143
163
|
}
|
|
144
164
|
runs << current_run
|
|
145
165
|
end
|
|
@@ -160,47 +180,41 @@ module TUITD
|
|
|
160
180
|
|
|
161
181
|
def render_run(run)
|
|
162
182
|
chars = run[:chars].map { |c| escape_html(c) }.join
|
|
163
|
-
return chars if run[:style].empty? && !run[:has_cursor]
|
|
183
|
+
return chars if run[:style].empty? && !run[:has_cursor] && !run[:blink]
|
|
164
184
|
|
|
165
185
|
classes = []
|
|
166
|
-
|
|
186
|
+
if run[:has_cursor]
|
|
187
|
+
classes << "cursor-cell"
|
|
188
|
+
cursor_vis = @cursor[:visible] != false && @cursor["visible"] != false
|
|
189
|
+
if !cursor_vis
|
|
190
|
+
classes << "cursor-hidden"
|
|
191
|
+
else
|
|
192
|
+
style_val = @cursor[:style] || @cursor["style"]
|
|
193
|
+
case style_val
|
|
194
|
+
when 0, 1
|
|
195
|
+
classes << "cursor-block blink"
|
|
196
|
+
when 2
|
|
197
|
+
classes << "cursor-block"
|
|
198
|
+
when 3
|
|
199
|
+
classes << "cursor-underline blink"
|
|
200
|
+
when 4
|
|
201
|
+
classes << "cursor-underline"
|
|
202
|
+
when 5
|
|
203
|
+
classes << "cursor-bar blink"
|
|
204
|
+
when 6
|
|
205
|
+
classes << "cursor-bar"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
classes << "term-blink" if run[:blink]
|
|
210
|
+
|
|
167
211
|
cls = classes.empty? ? "" : %( class="#{classes.join(" ")}")
|
|
168
212
|
style = run[:style].empty? ? "" : %( style="#{run[:style]}")
|
|
169
213
|
%(<span#{cls}#{style}>#{chars}</span>)
|
|
170
214
|
end
|
|
171
215
|
|
|
172
216
|
def is_cursor?(ri, ci)
|
|
173
|
-
@cursor[:row] == ri && @cursor[:col] == ci
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def resolve_color(name, fallback)
|
|
177
|
-
case name
|
|
178
|
-
when "default"
|
|
179
|
-
fallback
|
|
180
|
-
when /^#([0-9a-fA-F]{6})$/
|
|
181
|
-
[$1[0..1].to_i(16), $1[2..3].to_i(16), $1[4..5].to_i(16)]
|
|
182
|
-
when /\Acolor(\d+)\z/
|
|
183
|
-
xterm_256($1.to_i)
|
|
184
|
-
when /\Abright_(.+)\z/
|
|
185
|
-
ANSI_RGB[name] || fallback
|
|
186
|
-
else
|
|
187
|
-
ANSI_RGB[name] || fallback
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
def xterm_256(index)
|
|
192
|
-
if index < 16
|
|
193
|
-
name = ANSI_INDEX[index]
|
|
194
|
-
ANSI_RGB[name] || DEFAULT_FG
|
|
195
|
-
elsif index < 232
|
|
196
|
-
r = CUBE[((index - 16) / 36) % 6]
|
|
197
|
-
g = CUBE[((index - 16) / 6) % 6]
|
|
198
|
-
b = CUBE[(index - 16) % 6]
|
|
199
|
-
[r, g, b]
|
|
200
|
-
else
|
|
201
|
-
v = 8 + (index - 232) * 10
|
|
202
|
-
[v, v, v]
|
|
203
|
-
end
|
|
217
|
+
(@cursor[:row] || @cursor["row"]) == ri && (@cursor[:col] || @cursor["col"]) == ci
|
|
204
218
|
end
|
|
205
219
|
|
|
206
220
|
def css_color(rgb)
|
|
@@ -217,12 +231,5 @@ module TUITD
|
|
|
217
231
|
end
|
|
218
232
|
end
|
|
219
233
|
|
|
220
|
-
def _dig(hash, *keys)
|
|
221
|
-
keys.each do |k|
|
|
222
|
-
return nil unless hash
|
|
223
|
-
hash = hash[k] || hash[k.to_s]
|
|
224
|
-
end
|
|
225
|
-
hash
|
|
226
|
-
end
|
|
227
234
|
end
|
|
228
235
|
end
|
data/lib/tui_td/screenshot.rb
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "chunky_png"
|
|
4
|
+
require_relative "ansi_utils"
|
|
4
5
|
|
|
5
6
|
module TUITD
|
|
6
7
|
class Screenshot
|
|
8
|
+
include ANSIUtils
|
|
9
|
+
|
|
7
10
|
CELL_W = 8
|
|
8
11
|
CELL_H = 16
|
|
9
12
|
|
|
@@ -104,40 +107,98 @@ module TUITD
|
|
|
104
107
|
0x00, 0x70, 0x18, 0x18, 0x18, 0x18, 0x0e, 0x0e, 0x18, 0x18, 0x18, 0x18, 0x70, 0x00, 0x00, 0x00, # } (125)
|
|
105
108
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x7e, 0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # ~ (126)
|
|
106
109
|
].freeze
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
"
|
|
111
|
-
"
|
|
112
|
-
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
"
|
|
116
|
-
|
|
117
|
-
"
|
|
118
|
-
"
|
|
119
|
-
"
|
|
120
|
-
"
|
|
121
|
-
"
|
|
122
|
-
"
|
|
123
|
-
"
|
|
124
|
-
"
|
|
125
|
-
"
|
|
110
|
+
BOX_CHARS = {
|
|
111
|
+
# horizontal
|
|
112
|
+
"─" => [false, false, true, true, :light],
|
|
113
|
+
"━" => [false, false, true, true, :heavy],
|
|
114
|
+
"═" => [false, false, true, true, :double],
|
|
115
|
+
# vertical
|
|
116
|
+
"│" => [true, true, false, false, :light],
|
|
117
|
+
"┃" => [true, true, false, false, :heavy],
|
|
118
|
+
"║" => [true, true, false, false, :double],
|
|
119
|
+
# corners
|
|
120
|
+
"┌" => [false, true, false, true, :light],
|
|
121
|
+
"┍" => [false, true, false, true, :light],
|
|
122
|
+
"┎" => [false, true, false, true, :light],
|
|
123
|
+
"┏" => [false, true, false, true, :heavy],
|
|
124
|
+
"┐" => [false, true, true, false, :light],
|
|
125
|
+
"┑" => [false, true, true, false, :light],
|
|
126
|
+
"┒" => [false, true, true, false, :light],
|
|
127
|
+
"┓" => [false, true, true, false, :heavy],
|
|
128
|
+
"└" => [true, false, false, true, :light],
|
|
129
|
+
"▼" => [true, false, false, true, :light],
|
|
130
|
+
"┖" => [true, false, false, true, :light],
|
|
131
|
+
"┗" => [true, false, false, true, :heavy],
|
|
132
|
+
"┘" => [true, false, true, false, :light],
|
|
133
|
+
"┙" => [true, false, true, false, :light],
|
|
134
|
+
"┚" => [true, false, true, false, :light],
|
|
135
|
+
"┛" => [true, false, true, false, :heavy],
|
|
136
|
+
# double corners
|
|
137
|
+
"╔" => [false, true, false, true, :double],
|
|
138
|
+
"╗" => [false, true, true, false, :double],
|
|
139
|
+
"╚" => [true, false, false, true, :double],
|
|
140
|
+
"╝" => [true, false, true, false, :double],
|
|
141
|
+
# T-junctions
|
|
142
|
+
"├" => [true, true, false, true, :light],
|
|
143
|
+
"┣" => [true, true, false, true, :heavy],
|
|
144
|
+
"┤" => [true, true, true, false, :light],
|
|
145
|
+
"┫" => [true, true, true, false, :heavy],
|
|
146
|
+
"┬" => [false, true, true, true, :light],
|
|
147
|
+
"┳" => [false, true, true, true, :heavy],
|
|
148
|
+
"┴" => [true, false, true, true, :light],
|
|
149
|
+
"┻" => [true, false, true, true, :heavy],
|
|
150
|
+
# double T-junctions
|
|
151
|
+
"╠" => [true, true, false, true, :double],
|
|
152
|
+
"╣" => [true, true, true, false, :double],
|
|
153
|
+
"╦" => [false, true, true, true, :double],
|
|
154
|
+
"╩" => [true, false, true, true, :double],
|
|
155
|
+
# crosses
|
|
156
|
+
"┼" => [true, true, true, true, :light],
|
|
157
|
+
"╋" => [true, true, true, true, :heavy],
|
|
158
|
+
"╬" => [true, true, true, true, :double],
|
|
159
|
+
# single lines (ends)
|
|
160
|
+
"╴" => [false, false, true, false, :light],
|
|
161
|
+
"╵" => [true, false, false, false, :light],
|
|
162
|
+
"╶" => [false, false, false, true, :light],
|
|
163
|
+
"╷" => [false, true, false, false, :light],
|
|
164
|
+
"╸" => [false, false, true, false, :heavy],
|
|
165
|
+
"╹" => [true, false, false, false, :heavy],
|
|
166
|
+
"╺" => [false, false, false, true, :heavy],
|
|
167
|
+
"╻" => [false, true, false, false, :heavy],
|
|
168
|
+
# mixed corners/junctions
|
|
169
|
+
"┿" => [true, true, true, true, :light],
|
|
170
|
+
"╀" => [true, true, true, true, :light],
|
|
171
|
+
"╁" => [true, true, true, true, :light],
|
|
172
|
+
"╂" => [true, true, true, true, :light],
|
|
173
|
+
"╃" => [true, true, true, true, :heavy],
|
|
174
|
+
"╄" => [true, true, true, true, :heavy],
|
|
175
|
+
"╅" => [true, true, true, true, :heavy],
|
|
176
|
+
"╆" => [true, true, true, true, :heavy],
|
|
177
|
+
"╇" => [true, true, true, true, :heavy],
|
|
178
|
+
"╈" => [true, true, true, true, :heavy],
|
|
179
|
+
"╉" => [true, true, true, true, :heavy],
|
|
180
|
+
"╊" => [true, true, true, true, :heavy],
|
|
181
|
+
"╒" => [false, true, false, true, :double],
|
|
182
|
+
"╓" => [false, true, false, true, :double],
|
|
183
|
+
"╕" => [false, true, true, false, :double],
|
|
184
|
+
"╖" => [false, true, true, false, :double],
|
|
185
|
+
"╘" => [true, false, false, true, :double],
|
|
186
|
+
"╙" => [true, false, false, true, :double],
|
|
187
|
+
"╛" => [true, false, true, false, :double],
|
|
188
|
+
"╜" => [true, false, true, false, :double],
|
|
189
|
+
"╞" => [true, true, false, true, :double],
|
|
190
|
+
"╟" => [true, true, false, true, :double],
|
|
191
|
+
"╡" => [true, true, true, false, :double],
|
|
192
|
+
"╢" => [true, true, true, false, :double],
|
|
193
|
+
"╤" => [false, true, true, true, :double],
|
|
194
|
+
"╥" => [false, true, true, true, :double],
|
|
195
|
+
"╧" => [true, false, true, true, :double],
|
|
196
|
+
"╨" => [true, false, true, true, :double],
|
|
197
|
+
"╪" => [true, true, true, true, :double],
|
|
198
|
+
"╫" => [true, true, true, true, :double]
|
|
126
199
|
}.freeze
|
|
127
|
-
private_constant :ANSI_RGB
|
|
128
200
|
|
|
129
|
-
|
|
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
|
|
201
|
+
private_constant :FONT
|
|
141
202
|
|
|
142
203
|
def initialize(state)
|
|
143
204
|
@state = state
|
|
@@ -181,6 +242,12 @@ module TUITD
|
|
|
181
242
|
|
|
182
243
|
fill_rect(image, px, py, CELL_W, CELL_H, bg_rgb)
|
|
183
244
|
|
|
245
|
+
if box_drawing?(char)
|
|
246
|
+
draw_box_character(image, px, py, char, fg_rgb)
|
|
247
|
+
draw_underline(image, px, py, CELL_W, fg_rgb) if underline
|
|
248
|
+
return
|
|
249
|
+
end
|
|
250
|
+
|
|
184
251
|
return if char == " " || char.ord < 32 || char.ord > 126
|
|
185
252
|
|
|
186
253
|
rows_data = glyph_rows(char)
|
|
@@ -191,36 +258,6 @@ module TUITD
|
|
|
191
258
|
draw_underline(image, px, py, CELL_W, fg_rgb) if underline
|
|
192
259
|
end
|
|
193
260
|
|
|
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
261
|
def fill_rect(image, x, y, w, h, rgb)
|
|
225
262
|
color = ChunkyPNG::Color.rgb(*rgb)
|
|
226
263
|
h.times do |dy|
|
|
@@ -260,12 +297,94 @@ module TUITD
|
|
|
260
297
|
w.times { |dx| image[px + dx, y] = color }
|
|
261
298
|
end
|
|
262
299
|
|
|
263
|
-
def
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
300
|
+
def box_drawing?(char)
|
|
301
|
+
char_ord = char.ord
|
|
302
|
+
char_ord >= 0x2500 && char_ord <= 0x257F
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def draw_box_character(image, px, py, char, fg_rgb)
|
|
306
|
+
config = BOX_CHARS[char]
|
|
307
|
+
|
|
308
|
+
unless config
|
|
309
|
+
char_ord = char.ord
|
|
310
|
+
if [0x2500, 0x2501, 0x2504, 0x2505, 0x2508, 0x2509, 0x254c, 0x254d, 0x2550].include?(char_ord)
|
|
311
|
+
style = [0x2501, 0x2505, 0x2509, 0x254d].include?(char_ord) ? :heavy : (char_ord == 0x2550 ? :double : :light)
|
|
312
|
+
config = [false, false, true, true, style]
|
|
313
|
+
elsif [0x2502, 0x2503, 0x2506, 0x2507, 0x250a, 0x250b, 0x254e, 0x254f, 0x2551].include?(char_ord)
|
|
314
|
+
style = [0x2503, 0x2507, 0x250b, 0x254f].include?(char_ord) ? :heavy : (char_ord == 0x2551 ? :double : :light)
|
|
315
|
+
config = [true, true, false, false, style]
|
|
316
|
+
else
|
|
317
|
+
config = [true, true, true, true, :light]
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
up, down, left, right, style = config
|
|
322
|
+
cx = px + 4
|
|
323
|
+
cy = py + 8
|
|
324
|
+
|
|
325
|
+
color = ChunkyPNG::Color.rgb(*fg_rgb)
|
|
326
|
+
|
|
327
|
+
if style == :double
|
|
328
|
+
if left
|
|
329
|
+
(px..(cx + 2)).each { |x| image[x, py + 6] = color }
|
|
330
|
+
(px..(cx + 2)).each { |x| image[x, py + 10] = color }
|
|
331
|
+
end
|
|
332
|
+
if right
|
|
333
|
+
((cx - 2)..(px + 7)).each { |x| image[x, py + 6] = color }
|
|
334
|
+
((cx - 2)..(px + 7)).each { |x| image[x, py + 10] = color }
|
|
335
|
+
end
|
|
336
|
+
if up
|
|
337
|
+
(py..(cy + 2)).each { |y| image[px + 2, y] = color }
|
|
338
|
+
(py..(cy + 2)).each { |y| image[px + 6, y] = color }
|
|
339
|
+
end
|
|
340
|
+
if down
|
|
341
|
+
((cy - 2)..(py + 15)).each { |y| image[px + 2, y] = color }
|
|
342
|
+
((cy - 2)..(py + 15)).each { |y| image[px + 6, y] = color }
|
|
343
|
+
end
|
|
344
|
+
elsif style == :heavy
|
|
345
|
+
if left
|
|
346
|
+
(px..cx).each do |x|
|
|
347
|
+
image[x, cy - 1] = color
|
|
348
|
+
image[x, cy] = color
|
|
349
|
+
image[x, cy + 1] = color
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
if right
|
|
353
|
+
(cx..(px + 7)).each do |x|
|
|
354
|
+
image[x, cy - 1] = color
|
|
355
|
+
image[x, cy] = color
|
|
356
|
+
image[x, cy + 1] = color
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
if up
|
|
360
|
+
(py..cy).each do |y|
|
|
361
|
+
image[cx - 1, y] = color
|
|
362
|
+
image[cx, y] = color
|
|
363
|
+
image[cx + 1, y] = color
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
if down
|
|
367
|
+
(cy..(py + 15)).each do |y|
|
|
368
|
+
image[cx - 1, y] = color
|
|
369
|
+
image[cx, y] = color
|
|
370
|
+
image[cx + 1, y] = color
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
else # :light
|
|
374
|
+
if left
|
|
375
|
+
(px..cx).each { |x| image[x, cy] = color }
|
|
376
|
+
end
|
|
377
|
+
if right
|
|
378
|
+
(cx..(px + 7)).each { |x| image[x, cy] = color }
|
|
379
|
+
end
|
|
380
|
+
if up
|
|
381
|
+
(py..cy).each { |y| image[cx, y] = color }
|
|
382
|
+
end
|
|
383
|
+
if down
|
|
384
|
+
(cy..(py + 15)).each { |y| image[cx, y] = color }
|
|
385
|
+
end
|
|
267
386
|
end
|
|
268
|
-
hash
|
|
269
387
|
end
|
|
388
|
+
|
|
270
389
|
end
|
|
271
390
|
end
|
data/lib/tui_td/state.rb
CHANGED
|
@@ -4,13 +4,23 @@ module TUITD
|
|
|
4
4
|
# Represents the parsed state of a terminal screen.
|
|
5
5
|
# Provides high-level query methods for AI consumption.
|
|
6
6
|
class State
|
|
7
|
-
attr_reader :rows, :cols, :grid, :cursor
|
|
7
|
+
attr_reader :rows, :cols, :grid, :cursor, :cursor_visible, :cursor_style, :mouse_mode, :mouse_format
|
|
8
8
|
|
|
9
9
|
def initialize(data)
|
|
10
|
+
raise ArgumentError, "State data must include :size key" unless data[:size]
|
|
11
|
+
raise ArgumentError, "State data must include :rows key" unless data[:rows]
|
|
12
|
+
|
|
10
13
|
@rows = data[:size][:rows]
|
|
11
14
|
@cols = data[:size][:cols]
|
|
12
15
|
@grid = data[:rows]
|
|
13
16
|
@cursor = data[:cursor]
|
|
17
|
+
|
|
18
|
+
cursor_info = data[:cursor].is_a?(Hash) ? data[:cursor] : {}
|
|
19
|
+
@cursor_visible = data.key?(:cursor_visible) ? data[:cursor_visible] : (cursor_info[:visible] != false)
|
|
20
|
+
@cursor_style = data.key?(:cursor_style) ? data[:cursor_style] : (cursor_info[:style] || 1)
|
|
21
|
+
|
|
22
|
+
@mouse_mode = data[:mouse_mode] || :none
|
|
23
|
+
@mouse_format = data[:mouse_format] || :normal
|
|
14
24
|
end
|
|
15
25
|
|
|
16
26
|
# Get plain text of the entire terminal (no ANSI)
|