Module: Echoes::Iterm2Images

Defined in:
lib/echoes/iterm2_images.rb

Overview

iTerm2 inline-image protocol (OSC 1337 with the ‘File=` verb). Wire format:

\e]1337;File=<key=value>[;<key=value>...]:<base64-payload>\a

Most of the heavy lifting (NSBitmapImageRep PNG/JPEG/TIFF/GIF decode, RGBA pixmap storage on the screen, blit through the multicell renderer) is already shipped for the Kitty graphics protocol — this module just parses the OSC 1337 envelope and delegates to Echoes::KittyGraphics::AppKitPng / Screen#put_kitty_image.

Out of scope (follow-ups, if anyone hits them):

- `inline=0` (save to disk / clipboard) — we ignore these.
- `preserveAspectRatio=0` (non-uniform stretch) — we always
  preserve aspect; user can request explicit `width=` + `height=`.
- The `name=`-base64 metadata — useful for "Save As" UIs we
  don't have.

Class Method Summary collapse

Class Method Details

.compute_cell_dimensions(params, image, screen) ⇒ Object

Translate the wire’s ‘width=` / `height=` into cell counts for the renderer. Supported forms (per iTerm2 docs):

N      — N character cells
Npx    — N pixels; rounded up to a whole cell
N%     — N% of the screen dimension
auto   — natural pixel size / cell pixel size (default)

nil or unrecognized → ‘auto`.



96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/echoes/iterm2_images.rb', line 96

def compute_cell_dimensions(params, image, screen)
  cell_w_px = screen.cell_pixel_width.to_f
  cell_h_px = screen.cell_pixel_height.to_f
  cells_w = parse_dim(params['width'],
                      natural_px: image[:width],
                      cell_px:    cell_w_px,
                      screen_size: screen.cols)
  cells_h = parse_dim(params['height'],
                      natural_px: image[:height],
                      cell_px:    cell_h_px,
                      screen_size: screen.rows)
  [cells_w, cells_h]
end

.decode_image(bytes) ⇒ Object

Bytes → width:, height:. Despite the name, AppKitPng handles every format NSBitmapImageRep recognizes (PNG / JPEG / GIF / TIFF / BMP) because it draws the decoded CGImage into a known RGBA8 CGBitmapContext.



84
85
86
87
# File 'lib/echoes/iterm2_images.rb', line 84

def decode_image(bytes)
  require_relative 'kitty_graphics_appkit'
  KittyGraphics::AppKitPng.decode(bytes)
end

.decode_payload(b64) ⇒ Object

Tolerant base64 decode (allows wrapped / whitespace payloads). Mirrors KittyGraphics.decode_payload to keep the two paths symmetric.



72
73
74
75
76
77
78
# File 'lib/echoes/iterm2_images.rb', line 72

def decode_payload(b64)
  cleaned = b64.to_s.delete("\r\n\t ")
  return nil if cleaned.empty?
  cleaned.unpack1('m0')
rescue ArgumentError
  nil
end

.handle(rest, screen:, writer: nil) ⇒ Object

Top-level entry point. ‘rest` is everything between `e]1337;` and the terminating BEL/ST. Returns truthy on successful image display; nil otherwise (silently).



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/echoes/iterm2_images.rb', line 28

def handle(rest, screen:, writer: nil)
  verb_args = rest.to_s
  return nil unless verb_args.start_with?('File=')

  params_str, payload_b64 = verb_args.byteslice(5..).split(':', 2)
  return nil if payload_b64.nil? || payload_b64.empty?

  params = parse_params(params_str || '')

  # iTerm2 only inlines images when `inline=1`. Absent means
  # "save to disk", which we don't support — bail.
  return nil unless params['inline'] == '1'

  bytes = decode_payload(payload_b64)
  return nil unless bytes

  image = decode_image(bytes)
  return nil unless image

  cells_w, cells_h = compute_cell_dimensions(params, image, screen)
  screen.put_kitty_image(
    rgba:    image[:rgba],
    width:   image[:width],
    height:  image[:height],
    cells_w: cells_w,
    cells_h: cells_h,
  )
  true
end

.parse_dim(value, natural_px:, cell_px:, screen_size:) ⇒ Object



110
111
112
113
114
115
116
117
118
119
120
# File 'lib/echoes/iterm2_images.rb', line 110

def parse_dim(value, natural_px:, cell_px:, screen_size:)
  return nil if value.nil? || value.empty? || value == 'auto'
  if value.end_with?('px')
    return nil if cell_px <= 0
    (value.to_f / cell_px).ceil
  elsif value.end_with?('%')
    (screen_size * value.to_f / 100.0).ceil
  else
    value.to_i
  end
end

.parse_params(str) ⇒ Object

Parse “k1=v1;k2=v2” into “k2”=>“v2”.



59
60
61
62
63
64
65
66
67
# File 'lib/echoes/iterm2_images.rb', line 59

def parse_params(str)
  out = {}
  str.split(';').each do |pair|
    k, v = pair.split('=', 2)
    next if k.nil? || k.empty?
    out[k] = (v || '').to_s
  end
  out
end