Skip to content

terminal/osc: render iTerm2 OSC 1337 File= inline images#376

Merged
deblasis merged 2 commits into
windowsfrom
kitten-into-wintty/iterm2-images-render
May 18, 2026
Merged

terminal/osc: render iTerm2 OSC 1337 File= inline images#376
deblasis merged 2 commits into
windowsfrom
kitten-into-wintty/iterm2-images-render

Conversation

@deblasis
Copy link
Copy Markdown
Owner

Follow-up A to #375. The parser-only PR accepted iTerm2 File= sequences and emitted a Command carrying the raw base64 payload, but the handler was a debug-log stub. This wires the payload through to the existing kitty graphics renderer path so the image actually displays at the cursor.

Flow

OSC 1337 File=inline=1:BASE64
  -> iterm2.zig parser emits Command.iterm2_image_transmit
  -> stream.zig dispatches via self.handler.vt(.iterm2_image_transmit, payload)
  -> StreamHandler.iterm2ImageTransmit decodes base64 + builds a
     kitty graphics Command (transmit_and_display, format=png, medium=direct)
  -> Terminal.kittyGraphics() stores + renders through the same
     image storage and DX12 image-pass the kitty APC protocol uses

The synthesis helper lives in osc/parsers/iterm2.zig next to the parser so the iTerm2-to-kitty translation is testable in isolation. Format is pinned to PNG; Ghostty's kitty graphics decoder is PNG-only today, so iTerm2 JPEG and GIF will surface as a render error in the decoder. That matches what #375's deferred-work list called out.

The libghostty-vt stream_terminal handler is a no-op for this action because embedders supply their own renderer; termio's StreamHandler is the only path with the allocator and Terminal access needed to dispatch.

Tests

Three new tests in osc/parsers/iterm2.zig:

  • valid base64 yields transmit_and_display PNG command
  • invalid base64 returns error
  • minimal 1x1 PNG round-trips through helper (verifies PNG signature in decoded data)

Verification

  • Windows: zig build test passes (2878/2933, 55 skipped, 0 failures)
  • Linux: pending (will validate before merging)
  • Mac: pending (machine offline)

Deferred (still tracked from #375)

  • B: multipart FilePart/FileEnd/MultipartFile chunked images
  • C: geometry hints (width, height, preserveAspectRatio, size)
  • JPEG/GIF support (needs Ghostty kitty decoder change first)

Test plan

  • zig build test passes on Windows
  • zig build test-lib-vt passes on Linux
  • zig build test-lib-vt passes on Mac
  • smoke probe-iterm2-image.ps1 renders a real PNG in Wintty

deblasis added 2 commits May 18, 2026 18:58
The parser-only PR (#375) accepted iTerm2 File= sequences and emitted
an iterm2_image_transmit OSC Command carrying the raw base64 payload,
but the handler was a debug-log stub.

This wires the payload through to the existing kitty graphics renderer
path so the image actually displays at the cursor.

Flow:

  OSC 1337 File=inline=1:BASE64
    -> iterm2.zig parser emits Command.iterm2_image_transmit
    -> stream.zig dispatches via self.handler.vt(.iterm2_image_transmit, payload)
    -> StreamHandler.iterm2ImageTransmit decodes base64 + builds a
       kitty graphics Command (transmit_and_display, format=png, medium=direct)
    -> Terminal.kittyGraphics() stores + renders through the same
       image storage and DX12 image-pass the kitty APC protocol uses

The synthesis helper lives in osc/parsers/iterm2.zig next to the parser
so the iTerm2-to-kitty translation is testable in isolation. Format is
pinned to PNG; Ghostty's kitty graphics decoder is PNG-only today, so
iTerm2 JPEG and GIF will surface as a render error in the decoder. That
matches what the parser-only PR's deferred-work list called out.

The libghostty-vt stream_terminal handler is a no-op for this action
because embedders supply their own renderer; termio's StreamHandler is
the only path with the allocator and Terminal access needed to dispatch.

Tests:
- synthKittyCommand: valid base64 yields transmit_and_display PNG command
- synthKittyCommand: invalid base64 returns error
- synthKittyCommand: minimal 1x1 PNG round-trips through helper

Verified on Windows: zig build test passes (2878/2933, 55 skipped).
- synthKittyCommand: switch to ArrayList + toOwnedSlice, matching the
  in-place decode pattern in kitty graphics_command.zig decodeData and
  removing the errdefer-after-realloc latent footgun.
- synthKittyCommand: sniff the PNG signature on the decoded bytes and
  return error.UnsupportedFormat for non-PNG. iTerm2 emitters can send
  JPEG and GIF, but Ghostty's kitty graphics decoder is PNG-only;
  rejecting here surfaces a clearer error than letting the decoder
  reject mid-pipeline.
- Revert pub on osc.zig parsers; the new helper is reached via a
  direct iterm2_parser import in stream_handler.zig instead, so the
  rest of the parsers subtree stays internal.
- Reword stream_handler.zig log line to drop the implementation jargon
  ("synthesis failed") and use the {t} tag format for the error.

Tests:
- New: synthKittyCommand non-PNG bytes return UnsupportedFormat
- New: synthKittyCommand payload shorter than PNG signature returns UnsupportedFormat
- Renamed the original happy-path test to use the 1x1 PNG fixture; the
  short "abcd" test moves to the non-PNG case above.

Verified:
- Windows: zig build test passes (2879/2934, 55 skipped)
- Linux: zig build test-lib-vt -Dtest-filter=iterm2 passes (103/105, 2 skipped)
@deblasis deblasis marked this pull request as ready for review May 18, 2026 17:57
@deblasis deblasis merged commit 1b20b28 into windows May 18, 2026
47 checks passed
deblasis added a commit that referenced this pull request May 19, 2026
* terminal/osc: honor iTerm2 OSC 1337 File= geometry hints

Follow-up C to #376. The parser previously accepted File= but ignored
the geometry options block; the helper produced a kitty graphics
command with default Display (native size, cursor placement). This
threads width / height / preserveAspectRatio through to the Kitty
Display struct.

Mapping:

  iTerm2 hint            | Kitty Display
  -----------------------+----------------------------------------------
  width=N (cells)        | columns = N
  height=N (cells)       | rows = N
  width=auto / height=auto | leave field 0 (native sizing)
  preserveAspectRatio=0  | implicit stretch when both dims set;
                         | no-op when only one dim is given
  width=Npx, height=Npx  | log.warn + leave 0 (no Kitty primitive
                         | for pixel-scale)
  width=N%, height=N%    | log.warn + leave 0 (no percent primitive)
  size, name             | ignored per spec (unknown keys allowed)

Shape change:

  Command.iterm2_image_transmit went from [:0]const u8 to a struct
  carrying the raw base64 payload plus a Iterm2ImageHints sub-struct.
  Both types are pub on osc.Command so stream.zig and stream_handler.zig
  can reference them without widening the parsers tree visibility.

  The single options-walk now picks up inline=1 alongside the geometry
  hints rather than running two separate passes.

Tests (8 new parser + 3 new helper):

- parser: width and height in cells populate hints
- parser: width=auto leaves columns at 0
- parser: pixel-suffixed width leaves columns at 0
- parser: percent-suffixed width leaves columns at 0
- parser: case-insensitive Width and PreserveAspectRatio
- parser: preserveAspectRatio=1 keeps default true
- parser: non-numeric width is ignored
- existing inline=1 tests updated to assert .payload + default hints
- helper: hint columns and rows map to Display
- helper: only columns set leaves rows at 0 for aspect preservation
- helper: preserve_aspect_ratio=false with both dims allows stretch

Verification:

- Windows: zig build test -> 2889/2944 pass, 55 skipped, 0 failures
- Linux: pending (ubuntinovm offline at commit time)
- Mac: pending (macbookale offline)

* terminal/osc: fold reviewer feedback on geometry hints

- parseCellDim: width=0 / height=0 now log.warn instead of silently
  degrading. The iTerm2 grammar doesn't sanction zero, but some
  emitters send it; the warning surfaces what we couldn't honor while
  the return value still falls back to native sizing so downstream
  behavior is unchanged.
- Remove "per spec" overclaim on the unknown-keys comment. The iTerm2
  docs don't actually mandate that implementations ignore unknown
  options; reword to "iTerm2 and WezTerm do the same in practice."
- synthKittyCommand: log.debug when preserveAspectRatio=false is
  received with only one dimension set, so layout bisectors can see
  that the hint was parsed but couldn't be honored (Kitty stretch
  mode is implicit when both columns and rows are supplied).

No behavior changes for already-passing tests; 105/106 still green
on Windows.
deblasis added a commit that referenced this pull request May 21, 2026
* terminal/osc: render iTerm2 OSC 1337 File= inline images

The parser-only PR (#375) accepted iTerm2 File= sequences and emitted
an iterm2_image_transmit OSC Command carrying the raw base64 payload,
but the handler was a debug-log stub.

This wires the payload through to the existing kitty graphics renderer
path so the image actually displays at the cursor.

Flow:

  OSC 1337 File=inline=1:BASE64
    -> iterm2.zig parser emits Command.iterm2_image_transmit
    -> stream.zig dispatches via self.handler.vt(.iterm2_image_transmit, payload)
    -> StreamHandler.iterm2ImageTransmit decodes base64 + builds a
       kitty graphics Command (transmit_and_display, format=png, medium=direct)
    -> Terminal.kittyGraphics() stores + renders through the same
       image storage and DX12 image-pass the kitty APC protocol uses

The synthesis helper lives in osc/parsers/iterm2.zig next to the parser
so the iTerm2-to-kitty translation is testable in isolation. Format is
pinned to PNG; Ghostty's kitty graphics decoder is PNG-only today, so
iTerm2 JPEG and GIF will surface as a render error in the decoder. That
matches what the parser-only PR's deferred-work list called out.

The libghostty-vt stream_terminal handler is a no-op for this action
because embedders supply their own renderer; termio's StreamHandler is
the only path with the allocator and Terminal access needed to dispatch.

Tests:
- synthKittyCommand: valid base64 yields transmit_and_display PNG command
- synthKittyCommand: invalid base64 returns error
- synthKittyCommand: minimal 1x1 PNG round-trips through helper

Verified on Windows: zig build test passes (2878/2933, 55 skipped).

* terminal/osc: fold reviewer feedback on iTerm2 image render

- synthKittyCommand: switch to ArrayList + toOwnedSlice, matching the
  in-place decode pattern in kitty graphics_command.zig decodeData and
  removing the errdefer-after-realloc latent footgun.
- synthKittyCommand: sniff the PNG signature on the decoded bytes and
  return error.UnsupportedFormat for non-PNG. iTerm2 emitters can send
  JPEG and GIF, but Ghostty's kitty graphics decoder is PNG-only;
  rejecting here surfaces a clearer error than letting the decoder
  reject mid-pipeline.
- Revert pub on osc.zig parsers; the new helper is reached via a
  direct iterm2_parser import in stream_handler.zig instead, so the
  rest of the parsers subtree stays internal.
- Reword stream_handler.zig log line to drop the implementation jargon
  ("synthesis failed") and use the {t} tag format for the error.

Tests:
- New: synthKittyCommand non-PNG bytes return UnsupportedFormat
- New: synthKittyCommand payload shorter than PNG signature returns UnsupportedFormat
- Renamed the original happy-path test to use the 1x1 PNG fixture; the
  short "abcd" test moves to the non-PNG case above.

Verified:
- Windows: zig build test passes (2879/2934, 55 skipped)
- Linux: zig build test-lib-vt -Dtest-filter=iterm2 passes (103/105, 2 skipped)
deblasis added a commit that referenced this pull request May 21, 2026
* terminal/osc: honor iTerm2 OSC 1337 File= geometry hints

Follow-up C to #376. The parser previously accepted File= but ignored
the geometry options block; the helper produced a kitty graphics
command with default Display (native size, cursor placement). This
threads width / height / preserveAspectRatio through to the Kitty
Display struct.

Mapping:

  iTerm2 hint            | Kitty Display
  -----------------------+----------------------------------------------
  width=N (cells)        | columns = N
  height=N (cells)       | rows = N
  width=auto / height=auto | leave field 0 (native sizing)
  preserveAspectRatio=0  | implicit stretch when both dims set;
                         | no-op when only one dim is given
  width=Npx, height=Npx  | log.warn + leave 0 (no Kitty primitive
                         | for pixel-scale)
  width=N%, height=N%    | log.warn + leave 0 (no percent primitive)
  size, name             | ignored per spec (unknown keys allowed)

Shape change:

  Command.iterm2_image_transmit went from [:0]const u8 to a struct
  carrying the raw base64 payload plus a Iterm2ImageHints sub-struct.
  Both types are pub on osc.Command so stream.zig and stream_handler.zig
  can reference them without widening the parsers tree visibility.

  The single options-walk now picks up inline=1 alongside the geometry
  hints rather than running two separate passes.

Tests (8 new parser + 3 new helper):

- parser: width and height in cells populate hints
- parser: width=auto leaves columns at 0
- parser: pixel-suffixed width leaves columns at 0
- parser: percent-suffixed width leaves columns at 0
- parser: case-insensitive Width and PreserveAspectRatio
- parser: preserveAspectRatio=1 keeps default true
- parser: non-numeric width is ignored
- existing inline=1 tests updated to assert .payload + default hints
- helper: hint columns and rows map to Display
- helper: only columns set leaves rows at 0 for aspect preservation
- helper: preserve_aspect_ratio=false with both dims allows stretch

Verification:

- Windows: zig build test -> 2889/2944 pass, 55 skipped, 0 failures
- Linux: pending (ubuntinovm offline at commit time)
- Mac: pending (macbookale offline)

* terminal/osc: fold reviewer feedback on geometry hints

- parseCellDim: width=0 / height=0 now log.warn instead of silently
  degrading. The iTerm2 grammar doesn't sanction zero, but some
  emitters send it; the warning surfaces what we couldn't honor while
  the return value still falls back to native sizing so downstream
  behavior is unchanged.
- Remove "per spec" overclaim on the unknown-keys comment. The iTerm2
  docs don't actually mandate that implementations ignore unknown
  options; reword to "iTerm2 and WezTerm do the same in practice."
- synthKittyCommand: log.debug when preserveAspectRatio=false is
  received with only one dimension set, so layout bisectors can see
  that the hint was parsed but couldn't be honored (Kitty stretch
  mode is implicit when both columns and rows are supplied).

No behavior changes for already-passing tests; 105/106 still green
on Windows.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant