tui-td 0.2.7 → 0.2.9
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 +13 -0
- data/lib/tui_td/cairo_renderer.rb +109 -0
- data/lib/tui_td/cli.rb +12 -2
- data/lib/tui_td/driver.rb +28 -4
- data/lib/tui_td/screenshot.rb +26 -4
- data/lib/tui_td/unifont_glyphs.rb +2789 -0
- data/lib/tui_td/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e0062b5242a5e1ec97546b733761ce7a7fe559ee85d2e413cdce25798fa0861d
|
|
4
|
+
data.tar.gz: 2fbf22c11937739e51bc303d4ddbaedd6edb1015ef829046dc9d42bc5a1f2bf8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 306311eefc1fc30811cd630ac23f35f02ce7d14250c9bdd1cc63a7508a199193af475947fed85bc24198b3060903ce38b392f056ddba326fc22741b3ba28568c
|
|
7
|
+
data.tar.gz: 90bcfdd7d9f9f9357758deb5f7ebb8b3a7f20ca919f398909ecd03801106998fcf190c5341141324ec427618e9a32ad40cfbc117ae7bb474b5421e1677b4980e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 0.2.9
|
|
4
|
+
|
|
5
|
+
- Fix: `wait_for_stable` uses parsed terminal grid comparison instead of raw byte arrival, preventing interactive TUIs that repaint cell-by-cell (e.g., glow) from timing out
|
|
6
|
+
- Fix: `cmd_capture` catches timeout for interactive TUIs and proceeds with whatever was rendered
|
|
7
|
+
- New tests for `driver.rb` (39), `cli.rb` (19), and `mcp/server.rb` (27) — 85 new tests total
|
|
8
|
+
|
|
9
|
+
## 0.2.8
|
|
10
|
+
|
|
11
|
+
- Unicode bitmap font in screenshot renderer: 2766 glyphs from GNU Unifont 17.0.04 covering Latin, Greek, Cyrillic, Arabic, Turkish, Math, Arrows, Box Drawing, Symbols, and Dingbats
|
|
12
|
+
- Cairo renderer as optional fallback for characters not in Unifont (e.g. CJK), with 3x supersampling and box-filter downsampling for sharp edges
|
|
13
|
+
- Rendering priority: Spleen (ASCII 33–126) → Unifont (127+, 2766 glyphs) → Cairo (fallback)
|
|
14
|
+
- Full test coverage for Unifont glyphs and Cairo renderer
|
|
15
|
+
|
|
3
16
|
## 0.2.7
|
|
4
17
|
|
|
5
18
|
- Screenshot rendering for 23 special characters: blocks (▀ ▄ █), triangles (▲ ▼), arrows (↑ ↓ → ←), half blocks (▌ ▐)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TUITD
|
|
4
|
+
module CairoRenderer
|
|
5
|
+
CELL_W = 8
|
|
6
|
+
CELL_H = 16
|
|
7
|
+
DEFAULT_FONT = "Arial Unicode MS"
|
|
8
|
+
FONT_SIZE = 12.0
|
|
9
|
+
RENDER_SCALE = 3
|
|
10
|
+
|
|
11
|
+
@available = false
|
|
12
|
+
@cache = {}
|
|
13
|
+
|
|
14
|
+
begin
|
|
15
|
+
require "cairo"
|
|
16
|
+
@available = true
|
|
17
|
+
rescue LoadError
|
|
18
|
+
# Cairo not available; render_glyph_onto is a no-op
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def available?
|
|
23
|
+
@available
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def render_glyph_onto(cpn_image, px, py, char, fg_rgb, bold:, italic:)
|
|
27
|
+
return unless available?
|
|
28
|
+
|
|
29
|
+
alpha = glyph_alpha(char, bold: bold, italic: italic)
|
|
30
|
+
composite(alpha, cpn_image, px, py, fg_rgb)
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
warn "CairoRenderer: #{e.message}" if $DEBUG
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def glyph_alpha(char, bold:, italic:)
|
|
38
|
+
key = [char.ord, bold, italic]
|
|
39
|
+
@cache[key] ||= render_surface(char, bold, italic)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def render_surface(char, bold, italic)
|
|
43
|
+
# Render at higher resolution then box-filter downsample for smoother edges
|
|
44
|
+
scale = RENDER_SCALE
|
|
45
|
+
big_w = CELL_W * scale
|
|
46
|
+
big_h = CELL_H * scale
|
|
47
|
+
surface = Cairo::ImageSurface.new(Cairo::Format::ARGB32, big_w, big_h)
|
|
48
|
+
context = Cairo::Context.new(surface)
|
|
49
|
+
|
|
50
|
+
context.antialias = Cairo::ANTIALIAS_NONE
|
|
51
|
+
|
|
52
|
+
fo = Cairo::FontOptions.new
|
|
53
|
+
fo.hint_style = Cairo::HINT_STYLE_FULL
|
|
54
|
+
fo.hint_metrics = Cairo::HINT_METRICS_ON
|
|
55
|
+
context.font_options = fo
|
|
56
|
+
|
|
57
|
+
slant = italic ? Cairo::FONT_SLANT_ITALIC : Cairo::FONT_SLANT_NORMAL
|
|
58
|
+
weight = bold ? Cairo::FONT_WEIGHT_BOLD : Cairo::FONT_WEIGHT_NORMAL
|
|
59
|
+
context.select_font_face(DEFAULT_FONT, slant, weight)
|
|
60
|
+
context.set_font_size(FONT_SIZE * scale)
|
|
61
|
+
|
|
62
|
+
extents = context.text_extents(char)
|
|
63
|
+
x_off = ((big_w - extents.width) / 2.0) - extents.x_bearing
|
|
64
|
+
y_off = ((big_h - extents.height) / 2.0) - extents.y_bearing
|
|
65
|
+
|
|
66
|
+
context.set_source_rgba(1.0, 1.0, 1.0, 1.0)
|
|
67
|
+
context.move_to(x_off, y_off)
|
|
68
|
+
context.show_text(char)
|
|
69
|
+
|
|
70
|
+
data = surface.data
|
|
71
|
+
stride = surface.stride
|
|
72
|
+
scale_sq = scale * scale
|
|
73
|
+
|
|
74
|
+
alpha_grid = Array.new(CELL_H) { Array.new(CELL_W, 0) }
|
|
75
|
+
CELL_H.times do |dy|
|
|
76
|
+
CELL_W.times do |dx|
|
|
77
|
+
sum = 0
|
|
78
|
+
scale.times do |sy|
|
|
79
|
+
row_off = (dy * scale + sy) * stride
|
|
80
|
+
scale.times do |sx|
|
|
81
|
+
sum += data.getbyte(row_off + (dx * scale + sx) * 4 + 3)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
alpha_grid[dy][dx] = sum / scale_sq
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
alpha_grid
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def composite(alpha_grid, cpn_image, px, py, fg_rgb)
|
|
92
|
+
fr, fg, fb = fg_rgb
|
|
93
|
+
|
|
94
|
+
CELL_H.times do |dy|
|
|
95
|
+
CELL_W.times do |dx|
|
|
96
|
+
a = alpha_grid[dy][dx]
|
|
97
|
+
next if a < 24
|
|
98
|
+
|
|
99
|
+
factor = [a, 255].min / 255.0
|
|
100
|
+
r = (fr * factor).to_i
|
|
101
|
+
g = (fg * factor).to_i
|
|
102
|
+
b = (fb * factor).to_i
|
|
103
|
+
cpn_image[px + dx, py + dy] = ChunkyPNG::Color.rgb(r, g, b)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
data/lib/tui_td/cli.rb
CHANGED
|
@@ -211,8 +211,18 @@ module TUITD
|
|
|
211
211
|
cmd = args.join(" ")
|
|
212
212
|
|
|
213
213
|
driver = Driver.new(cmd, **globals.slice(:rows, :cols, :timeout, :chdir))
|
|
214
|
-
|
|
215
|
-
|
|
214
|
+
begin
|
|
215
|
+
driver.start
|
|
216
|
+
rescue TimeoutError
|
|
217
|
+
# Interactive TUI that never stabilizes (e.g., glow without -p).
|
|
218
|
+
# Proceed with whatever was rendered before the timeout.
|
|
219
|
+
driver.refresh
|
|
220
|
+
end
|
|
221
|
+
begin
|
|
222
|
+
driver.wait_for_stable
|
|
223
|
+
rescue TimeoutError
|
|
224
|
+
# Ignored — already have rendered state from start
|
|
225
|
+
end
|
|
216
226
|
|
|
217
227
|
case globals[:format]
|
|
218
228
|
when :json
|
data/lib/tui_td/driver.rb
CHANGED
|
@@ -96,17 +96,28 @@ module TUITD
|
|
|
96
96
|
refresh_state!
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
-
# Wait for output to stabilize (
|
|
99
|
+
# Wait for output to stabilize (grid content unchanged for N milliseconds)
|
|
100
100
|
def wait_for_stable(stable_ms: 300)
|
|
101
101
|
deadline = monotonic + @timeout
|
|
102
102
|
last_change = monotonic
|
|
103
|
+
last_grid = nil
|
|
103
104
|
|
|
104
105
|
loop do
|
|
105
106
|
raise TimeoutError, "Timeout waiting for stable output" if monotonic > deadline
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
had_data = read_available!
|
|
109
|
+
process_alive = process_alive?
|
|
110
|
+
|
|
111
|
+
if had_data
|
|
112
|
+
current_grid = parse_grid_snapshot
|
|
113
|
+
if current_grid != last_grid
|
|
114
|
+
last_grid = current_grid
|
|
115
|
+
last_change = monotonic
|
|
116
|
+
end
|
|
117
|
+
elsif !process_alive
|
|
118
|
+
# Process exited and no more data — final state reached
|
|
119
|
+
break
|
|
120
|
+
elsif last_grid && (monotonic - last_change) * 1000 >= stable_ms
|
|
110
121
|
break
|
|
111
122
|
end
|
|
112
123
|
|
|
@@ -246,6 +257,19 @@ module TUITD
|
|
|
246
257
|
end
|
|
247
258
|
end
|
|
248
259
|
|
|
260
|
+
def parse_grid_snapshot
|
|
261
|
+
@output_mutex.synchronize do
|
|
262
|
+
ANSIParser.parse(@output_buffer, @rows, @cols)[:rows]
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def process_alive?
|
|
267
|
+
return false unless @pid
|
|
268
|
+
Process.waitpid(@pid, Process::WNOHANG).nil?
|
|
269
|
+
rescue Errno::ECHILD
|
|
270
|
+
false
|
|
271
|
+
end
|
|
272
|
+
|
|
249
273
|
def monotonic
|
|
250
274
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
251
275
|
end
|
data/lib/tui_td/screenshot.rb
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "chunky_png"
|
|
4
4
|
require_relative "ansi_utils"
|
|
5
|
+
require_relative "cairo_renderer"
|
|
6
|
+
require_relative "unifont_glyphs"
|
|
5
7
|
|
|
6
8
|
module TUITD
|
|
7
9
|
class Screenshot
|
|
@@ -356,12 +358,32 @@ module TUITD
|
|
|
356
358
|
return
|
|
357
359
|
end
|
|
358
360
|
|
|
359
|
-
return if char == " " || char_ord < 32
|
|
361
|
+
return if char == " " || char_ord < 32
|
|
360
362
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
+
# ASCII printable (33-126): use Spleen bitmap font (pixel-perfect)
|
|
364
|
+
if char_ord <= 126
|
|
365
|
+
rows_data = glyph_rows(char)
|
|
366
|
+
if rows_data
|
|
367
|
+
draw_glyph(image, px, py, rows_data, fg_rgb, bold: bold, italic: italic)
|
|
368
|
+
draw_underline(image, px, py, CELL_W, fg_rgb) if underline
|
|
369
|
+
return
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Unicode (127+): try Unifont bitmap first (pixel-perfect like Spleen)
|
|
374
|
+
if char_ord > 126
|
|
375
|
+
unifont_rows = UnifontGlyphs.rows(char_ord)
|
|
376
|
+
if unifont_rows
|
|
377
|
+
draw_glyph(image, px, py, unifont_rows, fg_rgb, bold: bold, italic: italic)
|
|
378
|
+
draw_underline(image, px, py, CELL_W, fg_rgb) if underline
|
|
379
|
+
return
|
|
380
|
+
end
|
|
381
|
+
end
|
|
363
382
|
|
|
364
|
-
|
|
383
|
+
# Fallback: render via Cairo for characters not in Unifont
|
|
384
|
+
if CairoRenderer.available?
|
|
385
|
+
CairoRenderer.render_glyph_onto(image, px, py, char, fg_rgb, bold: bold, italic: italic)
|
|
386
|
+
end
|
|
365
387
|
|
|
366
388
|
draw_underline(image, px, py, CELL_W, fg_rgb) if underline
|
|
367
389
|
end
|