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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f09dac0e163cc0b1f2e699d66fe25fd656003b28ad17867a32154073cec8a62
4
- data.tar.gz: a46f622e3a7a89c0451e603e89266575eec49b346af851c8bce393230684c759
3
+ metadata.gz: e0062b5242a5e1ec97546b733761ce7a7fe559ee85d2e413cdce25798fa0861d
4
+ data.tar.gz: 2fbf22c11937739e51bc303d4ddbaedd6edb1015ef829046dc9d42bc5a1f2bf8
5
5
  SHA512:
6
- metadata.gz: adb1c08cd89f21f59e270c73d94f8542ada7949069ebb5c6dcad27dc860264df93f09dea8e8c2053e90f909ca9c5405254e0f51d2afbd6dba194e1b3d293e4d3
7
- data.tar.gz: 70597354fa770012221aa26f03a2ce4198f13ebb8ea66f1bacba20f7be740b1487d31c6908de19036889ae3ab0f60fc4a7cf57d993c5b938453f51618ecb9b28
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
- driver.start
215
- driver.wait_for_stable
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 (no new data for N milliseconds)
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
- if read_available!
108
- last_change = monotonic
109
- elsif (monotonic - last_change) * 1000 >= stable_ms
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
@@ -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 || char_ord > 126
361
+ return if char == " " || char_ord < 32
360
362
 
361
- rows_data = glyph_rows(char)
362
- return unless rows_data
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
- draw_glyph(image, px, py, rows_data, fg_rgb, bold: bold, italic: italic)
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