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.
@@ -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
@@ -142,7 +142,7 @@ module TUITD
142
142
 
143
143
  # Get structured terminal state as a Hash
144
144
  def state_data
145
- refresh_state! if @state.nil?
145
+ refresh_state!
146
146
  @state
147
147
  end
148
148
 
@@ -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
- ANSI_RGB = {
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: is_cursor?(ri, ci)
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
- classes << "cursor-cell" if run[:has_cursor]
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
@@ -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
- 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],
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
- 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
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 _dig(hash, *keys)
264
- keys.each do |k|
265
- return nil unless hash
266
- hash = hash[k] || hash[k.to_s]
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)