Skip to content

terminal/osc: accept iTerm2 OSC 1337 File= inline images (parser-only)#375

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

terminal/osc: accept iTerm2 OSC 1337 File= inline images (parser-only)#375
deblasis merged 2 commits into
windowsfrom
kitten-into-wintty/iterm2-images

Conversation

@deblasis
Copy link
Copy Markdown
Owner

The OSC 1337 dispatcher already routed File=, FilePart=, FileEnd=, and MultipartFile= keys into the "unimplemented" log arm (osc/parsers/iterm2.zig:166), so wintty silently dropped iTerm2 inline-image escape sequences and treated the base64 payload as text the cursor can never reach.

This PR adds parser-side handling for the single-shot File= case.

Behavior

  • Split the value on the first : into options and base64 payload.
  • Walk the options for inline=1 (case-insensitive); reject otherwise.
    iTerm2 treats non-inline File= as a download-to-disk, which has no wintty analog.
  • Emit a new iterm2_image_transmit OSC Command carrying the raw base64 string.
  • Stream dispatch logs the byte count.

Deferred to follow-ups

  • Base64 decode + kitty graphics command synthesis (the bit that actually renders).
  • Multipart: FilePart, FileEnd, MultipartFile.
  • Geometry hints: width, height, preserveAspectRatio, size.
  • JPEG/GIF support (Ghostty's image decoder is PNG-only today).

Tests

Seven new OSC: 1337: File ... cases cover:

  • inline=1 happy path
  • extra options before inline (name, size)
  • missing inline rejected (download semantic)
  • explicit inline=0 rejected
  • no payload separator (no :)
  • empty payload
  • case-insensitive Inline=1

Verification

  • Windows: zig build test -Dtest-filter=1337 -> 91/92 pass, 1 skipped
  • Mac (macbookale): zig build test-lib-vt -Dtest-filter=1337 -> 93/95 pass, 2 skipped
  • Linux (ubuntinovm): zig build test-lib-vt -Dtest-filter=1337 -> 93/95 pass, 2 skipped

A smoke probe lives in wintty-smoke/probe-iterm2-image.ps1 (out of tree); it emits a tiny PNG via OSC 1337 and confirms the parser hit via the debug log.

Test plan

  • zig build test passes on Windows
  • zig build test-lib-vt passes on Mac
  • zig build test-lib-vt passes on Linux
  • Follow-up PR: base64 decode + kitty graphics synthesis
  • Follow-up PR: multipart + geometry hints

deblasis added 2 commits May 15, 2026 21:23
The OSC 1337 dispatcher already routed File=, FilePart=, FileEnd=, and
MultipartFile= keys into the "unimplemented" log arm, so wintty would
silently drop iTerm2 inline-image escape sequences and treat the
base64 payload as text the cursor can never reach.

Add parser-side handling for the single-shot File= case:
- Split the value on the first ':' into options and base64 payload.
- Walk the options for inline=1 (case-insensitive); reject otherwise
  (iTerm2 treats non-inline as a download-to-disk which has no
  wintty analog).
- Emit a new iterm2_image_transmit OSC Command carrying the raw
  base64 string.

Stream dispatch logs the byte count for now. Base64 decode + kitty
graphics command synthesis that actually renders the image is a
follow-up. Multipart (FilePart, FileEnd, MultipartFile) and geometry
hints (width, height, preserveAspectRatio, size) are also follow-ups.
- Log debug line on each rejection path (no separator, empty payload,
  missing inline=1), matching the existing "unimplemented OSC 1337"
  log style.
- Break out of the option loop once inline=1 is matched.
- Reword the comment on inline value matching to call out that the
  iTerm2 spec only documents literal "1" and "0".
- Reword the osc.Command doc-comment so it no longer implies the
  parser enforces PNG.
- Drop the multi-line follow-up narrative on the stream.zig handler;
  keep the byte-count log line.
- Rename the seven new tests to match the file's
  test "OSC: 1337: test <Key> ..." convention.
@deblasis deblasis marked this pull request as ready for review May 18, 2026 16:02
@deblasis deblasis merged commit 49cd1ea into windows May 18, 2026
98 checks passed
deblasis added a commit that referenced this pull request May 18, 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
#375)

* terminal/osc: accept iTerm2 OSC 1337 File= inline images (parser-only)

The OSC 1337 dispatcher already routed File=, FilePart=, FileEnd=, and
MultipartFile= keys into the "unimplemented" log arm, so wintty would
silently drop iTerm2 inline-image escape sequences and treat the
base64 payload as text the cursor can never reach.

Add parser-side handling for the single-shot File= case:
- Split the value on the first ':' into options and base64 payload.
- Walk the options for inline=1 (case-insensitive); reject otherwise
  (iTerm2 treats non-inline as a download-to-disk which has no
  wintty analog).
- Emit a new iterm2_image_transmit OSC Command carrying the raw
  base64 string.

Stream dispatch logs the byte count for now. Base64 decode + kitty
graphics command synthesis that actually renders the image is a
follow-up. Multipart (FilePart, FileEnd, MultipartFile) and geometry
hints (width, height, preserveAspectRatio, size) are also follow-ups.

* terminal/osc: fold reviewer feedback on iTerm2 File= parser

- Log debug line on each rejection path (no separator, empty payload,
  missing inline=1), matching the existing "unimplemented OSC 1337"
  log style.
- Break out of the option loop once inline=1 is matched.
- Reword the comment on inline value matching to call out that the
  iTerm2 spec only documents literal "1" and "0".
- Reword the osc.Command doc-comment so it no longer implies the
  parser enforces PNG.
- Drop the multi-line follow-up narrative on the stream.zig handler;
  keep the byte-count log line.
- Rename the seven new tests to match the file's
  test "OSC: 1337: test <Key> ..." convention.
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)
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