Draw image comparisons as real pixels in kitty and Ghostty #11

Merged
cyplo merged 1 commit from kitty-graphics into main 2026-11-09 17:38:19 +00:00
Owner

The half-block mosaic is gone where it matters: in kitty and Ghostty the side-by-side copies are now actual pixels through the kitty graphics protocol - a quiet (q=2) PNG transmit per image, a virtual placement letterboxed into its cell grid, drawn with unicode placeholders so the pictures scroll and reflow with the text like anything else on screen. Everywhere else keeps the half-block characters. Neither path queries the terminal, so the keyboard-freeze of the old PR #1 stays impossible by construction.

Detection without the probe that froze keyboards

  • The terminal is identified by environment alone: kitty's or Ghostty's TERM, or their window hints (KITTY_WINDOW_ID, GHOSTTY_RESOURCES_DIR); tmux and ordinary terminals fall back to half-blocks.
  • The cell size comes from the window-size ioctl (the call the kitty docs recommend) - a terminal that reports no pixel size gets half-blocks rather than a guess.

Verified against Ghostty's own core

tests/kitty_ghostty.rs runs the real binary in a pseudo-terminal at kitty cell geometry, feeds every byte it writes into libghostty-vt - the terminal emulation core of Ghostty - and asserts what Ghostty would draw:

  • both images stored with the exact placement grids (10x3, 8x3 at a 10x20 font) and the exact pixels (Triangle-fitted, byte-compared)
  • the placeholders side by side, same rows, one column apart
  • one transmit per image, across all frames
  • a detection matrix: seven environments, asserting the kitty renderer starts only where it should
  • wide, tall and huge images (4000x100, 100x4000, 2000x1500) downscaling to their exact grids - the shapes that make each branch of the fit arithmetic bind
  • a cell-symbol contract test pinning the bytes themselves (transmit position, diacritics, inheritance)

The nix work this needed

libghostty-vt is built from the exact commit the bindings were generated from (a887df42) - the C ABI only froze at 1.0, a month later, so an older binding must be matched, not assumed. Only the VT library is built (no GUI stack), via a zon2nix package cache, and linked through pkg-config. The .pc fixup in the derivation normalises its Libs: line to the conventional -L/-l form.

Two bugs the gate surfaced

  1. ratatui's frame diff eats the preview: it takes the transmit-bearing cell's long symbol for a wide glyph and skips every cell after it - the whole preview collapsed to one cell in the terminal stream. Fixed with CellDiffOption::ForcedWidth(1).
  2. image::resize silently ignores wrong dimensions (it fits within the box), hiding arithmetic bugs; the downscale now calls resize_exact, honest about the exact size it names.

Gate

Mutating this PR's whole diff against main, as CI will: 105 tested, 101 caught, 4 unviable, 0 missed. Reaching zero took three rounds - the first found 35 gaps, including two equivalent mutants hiding dead arithmetic (a min chain that could never bind, and a position condition whose first cell always matched), which are now removed rather than excluded.

Checked: 83/83 tests, clippy clean with -D warnings, statix clean, nix build of libghostty-vt green on the runner.

The half-block mosaic is gone where it matters: in **kitty and Ghostty** the side-by-side copies are now **actual pixels** through the kitty graphics protocol - a quiet (`q=2`) PNG transmit per image, a virtual placement letterboxed into its cell grid, drawn with unicode placeholders so the pictures scroll and reflow with the text like anything else on screen. Everywhere else keeps the half-block characters. Neither path queries the terminal, so the keyboard-freeze of the old PR #1 stays impossible by construction. ### Detection without the probe that froze keyboards - The terminal is identified **by environment alone**: kitty's or Ghostty's `TERM`, or their window hints (`KITTY_WINDOW_ID`, `GHOSTTY_RESOURCES_DIR`); tmux and ordinary terminals fall back to half-blocks. - The cell size comes from the **window-size ioctl** (the call the kitty docs recommend) - a terminal that reports no pixel size gets half-blocks rather than a guess. ### Verified against Ghostty's own core `tests/kitty_ghostty.rs` runs the real binary in a pseudo-terminal at kitty cell geometry, feeds **every byte it writes** into **libghostty-vt** - the terminal emulation core of Ghostty - and asserts what Ghostty would draw: - both images stored with the **exact placement grids** (10x3, 8x3 at a 10x20 font) and the **exact pixels** (Triangle-fitted, byte-compared) - the placeholders **side by side**, same rows, one column apart - **one transmit per image**, across all frames - a **detection matrix**: seven environments, asserting the kitty renderer starts only where it should - **wide, tall and huge** images (4000x100, 100x4000, 2000x1500) downscaling to their exact grids - the shapes that make each branch of the fit arithmetic bind - a cell-symbol contract test pinning the bytes themselves (transmit position, diacritics, inheritance) ### The nix work this needed libghostty-vt is built **from the exact commit the bindings were generated from** (`a887df42`) - the C ABI only froze at 1.0, a month later, so an older binding must be matched, not assumed. Only the VT library is built (no GUI stack), via a zon2nix package cache, and linked through pkg-config. The `.pc` fixup in the derivation normalises its `Libs:` line to the conventional `-L/-l` form. ### Two bugs the gate surfaced 1. **ratatui's frame diff eats the preview**: it takes the transmit-bearing cell's long symbol for a wide glyph and skips every cell after it - the whole preview collapsed to one cell in the terminal stream. Fixed with `CellDiffOption::ForcedWidth(1)`. 2. **`image::resize` silently ignores wrong dimensions** (it fits within the box), hiding arithmetic bugs; the downscale now calls `resize_exact`, honest about the exact size it names. ### Gate Mutating this PR's whole diff against `main`, as CI will: **105 tested, 101 caught, 4 unviable, 0 missed**. Reaching zero took three rounds - the first found 35 gaps, including two *equivalent mutants* hiding dead arithmetic (a `min` chain that could never bind, and a position condition whose first cell always matched), which are now removed rather than excluded. **Checked:** 83/83 tests, clippy clean with `-D warnings`, statix clean, `nix build` of libghostty-vt green on the runner.
Draw image comparisons as real pixels in kitty and Ghostty
All checks were successful
CI / Lint (pull_request) Successful in 29s
CI / Run tests (pull_request) Successful in 34s
CI / Nix checks and package build (pull_request) Successful in 1m6s
CI / Mutation testing (pull_request) Successful in 4m44s
CI / Nix checks and package build (push) Successful in 12s
CI / Lint (push) Successful in 26s
CI / Run tests (push) Successful in 33s
CI / Mutation testing (push) Successful in 12m52s
92e27a4e9d
In kitty and Ghostty the side-by-side copies are now actual pixels
through the graphics protocol - a quiet PNG transmit per image and a
virtual placement letterboxed into its cell grid, drawn with unicode
placeholders so the pictures scroll and reflow with the text. Every
other terminal keeps the half-block characters. Neither way queries
the terminal, so the keyboard-freeze of the earlier image attempt stays
impossible. The terminal is identified by environment alone, and the
cell size comes from the window-size ioctl; one that reports no pixel
size falls back to half-block characters.

Verified against Ghostty's own terminal core: tests/kitty_ghostty.rs
runs the binary in a pseudo-terminal at kitty cell geometry, feeds every
byte it writes into libghostty-vt, and checks what Ghostty would draw -
both images stored with the exact grid and the exact pixels, the
placeholders side by side, one transmit per image. The library is built
from the commit its bindings were generated from, because the C ABI
only froze later.

Two bugs this surfaced: ratatui's frame diff takes a long cell symbol
for a wide glyph and skips every cell after it, so the transmit-bearing
cell must declare unit width; and image::resize silently ignores a
wrong dimension because it fits within the box, so the downscale calls
resize_exact.

Co-Authored-By: GLM 5.3 <noreply@anthropic.com>
cyplo merged commit 92e27a4e9d into main 2026-11-09 17:38:19 +00:00
cyplo deleted branch kitty-graphics 2026-11-09 17:38:19 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
cyplo/deescalate!11
No description provided.