Core Swap · Part 1 of 5
Program Pixels
Terminal Delight, the terminal I’m building, had just learned to open a picture itself: Alt+click a PNG path and it appears in a floating square beside the line that printed it. The next step was the pictures programs draw on their own, so I ran four picture programs in a pane. Nothing drew. kitten icat waited ten seconds and gave up, because the terminal’s reply to “what are you?” was too short for it to believe.
The pictures were being lost in the emulator core, the library that turns a program’s output into rows of cells. Terminal Delight borrowed alacritty’s, which drops a picture and closes the rows up behind it. So I recorded six picture programs and fed the same bytes to four cores. alacritty’s kept none, rio-vt from the Rio terminal and libghostty-vt from Ghostty kept all six, and wezterm-term kept four. Working around the core was no longer the only road, and the question became which core to use.
Story follows the pictures through four typesetters. Technical has the recordings, what each core kept, and why icat waited.
Story
Six pictures, four typesetters
A terminal shows what programs print, but the window doesn’t read a program’s output itself. A library called the emulator core does. It takes the stream of characters and control codes a program writes and sets them into a grid of cells, the way a typesetter sets a manuscript into lines of type, and the window draws whatever grid it’s handed. Terminal Delight, the terminal I’m building, was borrowing its core from another terminal, alacritty.
Some programs now send pictures down the same stream. A protocol from the kitty terminal lets an image viewer, a chart or a video player put real pixels on a terminal screen: the program sends the picture and it lands, usually, wherever the cursor happens to be. alacritty’s core was never built to keep one. Handed a manuscript with a photograph in it, this typesetter leaves the photograph out and closes up the page, so the next line of text moves up into the space the picture should have taken.
I found that out from the other end. Terminal Delight had just learned to open a picture on its own: Alt+click a PNG path in a pane and it opens in a floating square beside the line that printed it. The next step was pictures that programs draw, so four of them ran in a Terminal Delight pane. None drew anything, and each failed its own way. ratatui-image and viu asked whether pictures were welcome, got no answer, and drew with half-block characters instead. chafa never asked. kitten icat, kitty’s own picture tool, waited ten seconds and quit. Before it sends a picture, icat asks the terminal to describe itself (device attributes), and Terminal Delight answered with the shortest reply the standard allows, inherited unchanged from alacritty’s core. icat ignores a reply that short.
Two ways forward were open. Terminal Delight could keep alacritty’s typesetter and catch every photograph itself before it arrived, which means reading every byte of every pane first and working out where each picture landed. Or it could hire a typesetter that keeps photographs. Terminal Delight’s own review of its foundations, a month earlier, had set that second road aside for exactly this moment: swap the core “when image support becomes a committed TD feature”.
Which typesetters keep photographs is the kind of thing documentation answers confidently and not always correctly, so I watched them work instead. Six picture programs ran against a stand-in terminal that answered the way kitty would, and every byte each one sent was recorded. The same bytes then went to four cores, one at a time, and a small program read back what each had kept: where every picture sat, where the cursor ended up, and what text was left on the page.
alacritty’s kept nothing. In the recording where a test program prints a line, draws a picture six rows tall and prints a second line, alacritty’s put that second line on row 2, where the picture belonged. rio-vt, the core inside the Rio terminal, and libghostty-vt, Ghostty’s, kept all six pictures on the same cells and put the second line below the picture. wezterm-term, WezTerm’s core, kept four. The two it missed place their picture a different way: the program prints a run of special characters and the terminal paints the picture over them (placeholder cells). wezterm-term drew the picture at the cursor instead, and set the special characters as text beneath it.
A different typesetter would also end icat’s ten-second wait. rio-vt answers the describe-yourself question with a reply long enough for icat to believe.
So the road the foundation review had set aside turned into a shortlist. Three cores keep the pictures programs draw, from three terminals, each with its own way of being built and maintained. Choosing between them is what the rest of this series is about.
I’d love to work with you. See the portfolio, or get in touch.
Technical
Six recordings, four cores
Terminal Delight used alacritty_terminal 0.26 as its emulator core. That core has no Kitty graphics support: the parser discards the escape sequence, no image state is kept, and text after the picture takes the rows the picture would have covered. This part covers what that looked like in a real pane, the defect it exposed along the way, and the bake-off that turned a swap from an option into a shortlist.
In a real pane
Four picture programs ran in a hidden Terminal Delight window built from main, each in its own tab. mpv has to be told to draw and cannot be asked, so it ran against a stand-in that answers questions the way Terminal Delight does.1
| Program | How it decides it may draw | In Terminal Delight |
|---|---|---|
| kitten icat 0.49.1 | three Kitty queries, then device attributes | waits, then exits: “Timed out waiting for a response from the terminal” |
| ratatui-image 11.1.0 | a Kitty query, device attributes, the cell size | half-block characters |
| viu 1.6.1 | two Kitty queries | half-block characters |
| chafa 1.18.2 | never asks; reads TERM, which is alacritty | text symbols |
mpv 0.41.0, --vo=kitty | never asks; told by a flag | everything it sends is discarded |
The ten-second wait
icat’s run of --detect-support exited with status 1 after 10,076 ms, and a real picture after 10,044 ms.1 Terminal Delight answered device attributes (CSI c) with ESC[?6c, the reply alacritty_terminal sends, and kitty’s detector ignores a payload that short, so icat waits out its timeout. It is a defect whatever happens to pictures, and it was filed as one. rio-vt 0.5.28 answers ESC[?62;4;6;22;52c.2
Three roads
A picture program writes an escape sequence and, in the common case, the picture lands wherever the cursor is when its last byte arrives. Terminal Delight could own the read loop and scan every byte for pictures before alacritty’s parser drops them, patch the parser to hand pictures out, or swap the core. The first two keep a core that knows nothing about pictures and make Terminal Delight track where each one landed through every scroll, clear and resize. Terminal Delight’s foundation review of 2026-08-29 had already listed the third, for rio-vt or libghostty-vt, “when image support becomes a committed TD feature”.3
The bake-off
A stand-in terminal ran each program, answered its questions as kitty would, and recorded every byte. Harnesses then fed the same bytes to four cores in 4,096-byte chunks and read back placements, cursor, replies and text: alacritty_terminal 0.26, rio-vt 0.5.28, libghostty-vt 0.2.1 built with Zig 0.15.2, and wezterm-term at git b09b56c. viu’s recording points at a temporary file that no longer exists and was dropped.4
| Recording | alacritty | rio-vt | libghostty-vt | wezterm-term |
|---|---|---|---|---|
| chafa, raw pixels at the cursor | dropped | 40×12 at row 0 | 40×12 at row 0 | 40×12 at row 0 |
| kitten icat, a PNG at the cursor | dropped | 64×19 at row 0 | 64×19 at row 0 | 64×19 at row 0 |
| kitten icat, placeholder cells | 1,216 cells left as text | virtual placement | virtual placement | drawn at the cursor, cells left as text |
| ratatui-image, placeholder cells | 1,216 cells left as text | virtual placement | virtual placement | drawn at the cursor, cells left as text |
| mpv, four frames | dropped | 120×31 at row 3 | 120×31 at row 3 | 120×31 at row 3 |
| text, a 20×6 picture, text | dropped; second line on row 2 | at row 1; cursor row 8 | at row 1; cursor row 8 | at row 1; cursor row 8 |
| Placed | 0 of 6 | 6 of 6 | 6 of 6 | 4 of 6 |
rio-vt and libghostty-vt put every picture on the same cells and left the cursor on the same row in every recording. wezterm-term keeps no placement map; a picture lives as per-cell image slices, and its harness rebuilds each placement by scanning cells. It ignores the placeholder flag (U=1), draws the picture at the cursor, and leaves the placeholder characters as text; with ratatui-image those characters land on all but one row of the picture.4
Feeding the 12,988,693-byte mpv recording took libghostty-vt 19 ms, alacritty_terminal 40 ms, rio-vt 41 ms and wezterm-term 109 ms, medians of three runs on one machine. libghostty-vt needs a Zig toolchain at build time and a PNG decoder supplied by the embedder.4
What would falsify this
The harnesses ran each core outside Terminal Delight, on bytes recorded against a stand-in that answered like kitty. If a program sends different bytes to a real Terminal Delight that says yes to pictures, or a core keeps pictures in a harness and loses them inside Terminal Delight’s session host and windows, the bake-off measured the wrong thing, and the shortlist rests on it. The first is checked when a swapped build runs the same programs in a real pane.
I’d love to work with you. See the portfolio, or get in touch.
Sources
- Five programs in a real Terminal Delight pane — Each command, its stderr and a .done file holding the exit code and milliseconds, run in a hidden Terminal Delight window on 2026-09-24. mpv’s behaviour comes from the stand-in in evidence/stand-in.
- rio-vt 0.5.28, identify_terminal — The device-attributes reply as published on crates.io, read from the crate source.
- Terminal Delight’s foundation review, 2026-08-29, option F — docs/2026-08-29-foundation-interrogation-zed-gpui-quickshell.md in the terminal-delight repository: “Swap the VT seam (rio-vt / libghostty-vt)”, with the trigger quoted above.
- The bake-off: recordings, harnesses and results — corebake (alacritty_terminal and rio-vt), ghostbake (libghostty-vt) and wezbake (wezterm-term). Placements from each harness’s read-back in bake/results; timings from bake/results/parse-times.json, three runs each, with wezterm-term’s 300 ms reply-drain sleep subtracted.