Core Swap · Part 5 of 5
Read Loop
By the final round two cores were left, and the weights that would decide between them were written down before either was measured. libghostty-vt, from Ghostty, was faster: with twenty panes open it read 8 MB of text in 59 milliseconds, against 82 for rio-vt, from the Rio terminal. rio-vt was the one agents could work on. All three of its agents finished the task, and all three of libghostty-vt’s stopped at the same misdocumented setting. On the sealed weights rio-vt scored 0.82 and libghostty-vt 0.72, and speed and memory would have had to outweigh everything else together to reverse that.
So Terminal Delight moved to Rio’s core, and the move was bigger than a new library. Terminal Delight had been running on alacritty’s read loop, its code for starting shells and its lock as well as its core, and Rio’s core can’t run inside them. The design gave Terminal Delight its own. It was built the same day: every test passes on both cores, and a program’s picture draws in a real window. Terminal Delight’s documentation explains the swap on its front page.
Story tells it as an engine swap. Technical has the final weights, the evidence behind the vibe, and the design of the loop Terminal Delight takes over.
Story
The engine and its mounts
By the last round of choosing a new emulator core for Terminal Delight, the library that turns a program’s output into the grid of cells a terminal draws, two candidates were left. libghostty-vt, from Ghostty, was the faster. With twenty panes open it read 8 MB of text in 59 milliseconds, against 82 for rio-vt, the core inside the Rio terminal. rio-vt was the one agents could work on. Given the same task, all three of its agents finished, and all three of libghostty-vt’s stopped at the same setting, one documented as lines that counts bytes.
The choice between them had already been written down, as weights set before either had been measured: speed and memory 5, an agent being able to work on it 4, a published package used as it ships 3, pictures 2, maintainers 1, other picture formats a half, build toolchain a quarter. rio-vt came to 0.82 and libghostty-vt to 0.72. For libghostty-vt to tie, speed and memory would have had to weigh 17.7, more than every other weight put together.
The vibe had evidence too, drawn from each project’s own pages. Canario, a terminal built on Rio’s engine, calls itself “the terminal that thinks like a browser”, and its tabs badge an agent that is waiting for input, which is the job Terminal Delight’s agent wall does. Rio’s recent commits pin each promise in a test, and one gives a missing cell colour an explicit “none” instead of a default, a rule Terminal Delight lives by: unknown is not zero. Ghostty was the bigger pull, sixty-one thousand stars to Rio’s seven and a half thousand, with a core more rigorously tested underneath. My note after reading it all was “Vibes just went up a bunch for Rio”.
Choosing the engine turned out to be the easy half of what car people call an engine swap. The work is everything the engine bolts onto, because the old mounts were shaped for the old engine. Terminal Delight had never used only alacritty’s core. alacritty’s read loop, the thread that reads each pane’s output and feeds it to the core, ran every pane. Its code started each shell in a pseudoterminal, the kernel device a shell talks to as if it were a screen and keyboard, and its lock was the one the drawing code and the read loop shared. Terminal Delight’s session host, which keeps every terminal running while windows come and go, had built the handover to a newly attached window out of that lock. A window’s copy of a hosted pane existed mostly to dress a socket up as the pseudoterminal alacritty’s loop expected.
Rio’s core can’t run inside alacritty’s loop, and Rio’s own loop copies alacritty’s lock word for word, so taking it would have swapped one set of borrowed mounts for another. The design gives Terminal Delight its own. Each pane gets a small read loop Terminal Delight owns, which holds the core and the part that parses its input under one lock. That turns the handover into a plain rule: every byte is either already in the snapshot a new window receives or is sent to it live afterwards. Terminal Delight starts its own shells, and a failed resize reports an error instead of ending the whole program, as alacritty’s did. Every other part of Terminal Delight reads the core through a small set of its own types, numbered exactly the way alacritty numbered them, so a window running Rio’s core still agrees with a session host started before the swap. alacritty’s core stays behind a switch until Rio’s has held up in daily use.
Pictures follow the weights. They are drawn over the grid while you look at a pane, dropped when you switch to another tab, and never carried in a snapshot. And kitten icat, which waited ten seconds in every pane at the start of all this, gets an answer it believes the first time it asks what the terminal is.
The new engine went onto its new mounts the same day. The old test suite, run on Rio’s core, passed all but one of its 1,874 tests, and that one was real: When a program switches off a mouse-reporting mode that isn’t the one in use, alacritty clears just that mode and Rio’s core turns mouse reporting off altogether, so the code that copies a pane to a newly attached window was switching drag reporting on and then off again. Reading Rio’s source turned up the next thing: asked what it is, Rio’s core says Rio, and promises sixel pictures and clipboard access that Terminal Delight doesn’t provide, so Terminal Delight now answers as itself. The last thing only showed up in a real window. A program can send a picture as a temporary file that the terminal deletes once it has read it, and a pane on the session host is read twice, host first, so the window’s copy found nothing. The host now puts those pixels into the window’s copy of the stream before it deletes the file.
A review before the merge fed both engines the same bytes and found what Rio’s core still let through. Thirteen bytes asking for one accented letter to be repeated took it 9.7 seconds and 2.8 GB, where alacritty took 1.4 milliseconds. A picture command that never ended grew to 837 MB. And before deleting a temporary picture file, it only checked for a telltale word somewhere in the path, so a path that climbed out of the temporary folder could delete any file. Terminal Delight now stops all three at its own side of the mounts, without a patch to Rio’s code, and a new window reads 92 of the 94 test screens exactly as the old host did.
I’d love to work with you. See the portfolio, or get in touch.
Technical
Sealed weights, then an owned loop
This part has two halves. The first applies the weights sealed before the final runs to the two cores left, now that agent fluency is measured. The second is what choosing rio-vt required of Terminal Delight, which turned out to be more than a dependency change: alacritty_terminal supplied the machinery around the core as well as the core.
The final weights
| Dimension | Weight | rio-vt | libghostty-vt |
|---|---|---|---|
| Speed and memory | 5 | 0.63 | 0.76 |
| Agent fluency | 4 | 1.00 | 0.74 |
| Published, used unpatched | 3 | 1.00 | 0.75 |
| Pictures (Kitty features of 14) | 2 | 0.93 | 1.00 |
| Maintainers | 1 | 0.30 | 0.30 |
| Other picture protocols | 0.5 | 1.00 | 0.00 |
| Build toolchain | 0.25 | 0.70 | 0.40 |
| Score | of 15.75 | 0.82 | 0.72 |
For libghostty-vt to tie, speed and memory would have to weigh 17.7, more than the other weights together (10.75). The earlier fit, rewrite and API-stability weights are not part of this sum; with them added back before fluency was measured, rio-vt led 0.70 to 0.66.1
The evidence behind the vibe
Both projects are built with AI assistance, visibly in different ways. Ghostty’s repository carries AGENTS.md, CLAUDE.md and an AI policy for outside contributors, and says the project “is written with plenty of AI assistance”. Rio’s has no agent files; its commits show the habit in their shape, pinning each contract in a test (“document RIO_COLOR_NONE contract, pin it in test”). One person carries 92% of Rio’s commits.2 Canario, built on Rio’s engine, badges a tab whose agent needs input and shows its progress.3 libghostty-vt is presented as Ghostty’s core extracted for embedding, with zero dependencies, SIMD parsing, fuzzing and Valgrind, and as an early alpha with no API stability promised yet.4 On 2026-09-25 Ghostty had 61,539 GitHub stars and Rio 7,544.2
What alacritty_terminal was supplying
An inventory of Terminal Delight’s dependency on alacritty_terminal found about 200 distinct items and about 415 production lines naming them, 227 of those in the pane view. Most of the architecture sits outside the grid:5
EventLoopowns each pane’s reader thread, parser and writer.ttyforks the shell, andPty::on_resizecallsprocess::exit(1)whenTIOCSWINSZfails, which in a session host would end every pane at once.FairMutexis the lock the renderer and the reader share, and the host’s attach fence is built from its lease: take the lease, then the unfair lock, because the fair one would take the lease again and deadlock.- The window’s replica,
socketpty.rs, exists to dress a socket as the PTY that loop expects, down to naming twopub(crate)poll keys by value. grid_hash, the check that a window’s copy matches its host, hashes alacritty’s own bit layouts, and a window refuses a host whose protocol version differs.
rio-vt’s Crosswords can’t be driven by alacritty’s EventLoop. rio-vt ships its own loop, Machine, but its FairMutex is alacritty’s code copied verbatim, it keeps the parser where a snapshot can’t reach it, and a socket would have to be ported to its mio 0.6 fork.6
The design
The architecture gate records eight decisions, each against what it was chosen over.7
- A boundary Terminal Delight owns.
app/src/vt/holds Terminal Delight’s own types for positions, colours, cell flags, modes, cursor, selection, events and pictures, and nothing outside it names a core crate; a test greps for it. - Terminal Delight owns the read loop. One thread per pane over
polling: read outside the lock, then under it tee to an attached window (host), count bytes (a window’s copy), parse, bump the content generation, post a redraw. It watches the parser’s synchronized-update deadline and reports exit once.socketpty.rs’s workarounds are deleted rather than ported. - The parser lives inside the terminal lock, so a snapshot can flush an open synchronized update before it encodes. Under alacritty’s loop, bytes could be counted and teed while still buffered inside the parser when a snapshot was taken.
- Terminal Delight starts its own shells, through
rustix-openpty,setsidandTIOCSCTTY, with the environment it already set, and a failed resize returns an error. - alacritty 0.26’s numbering is frozen as Terminal Delight’s encoding. Flags, modes and named colours keep alacritty’s bit values, pinned bit by bit in a test, so
grid_hashagrees across builds and the host protocol stays at version 1. A new window can then attach to a session host that is still running the old build, which matters because that host is never restarted. - alacritty stays as a non-default
core-alacrittyfeature, so the refactor can be proven on the old core first, with every existing test green, and a fallback build is one flag away; a follow-up removes it once rio-vt has held. - rio-vt runs with grapheme clustering off. It turns DEC mode 2027 on by default, which changes cell widths from the ones alacritty computes, and the existing tests and a mixed host and window both depend on those. It is built with
default-features = falseand thegraphicsfeature, and given real cell pixels, the two silent picture traps every rio-vt agent found in the fluency test. - Pictures are attentional. Placements that intersect the viewport are drawn over the grid through gpui; when a pane’s tab stops being shown its pictures are dropped and its textures freed; snapshots carry none. Placeholder cells stay text in this change.
kitten icat’s ten-second wait, from the first part of this series, ends because Terminal Delight now answers device attributes with ESC[?62;22c, long enough for icat’s detector. rio-vt’s own reply, ESC[?62;4;6;22;52c, would also have ended it, but it claims sixel pictures and OSC 52 clipboard access, which Terminal Delight neither draws nor acts on.
What the swap took
The swap was built on 2026-09-25, on a branch, in slices that each ended with the checks continuous integration runs green: formatting, clippy with warnings as errors, and the whole test suite. The suite runs on both cores, cargo test for rio-vt and cargo test --features core-alacritty for the fallback.8
| Slice | What changed | What proved it |
|---|---|---|
| Terminal Delight’s own loop, still on alacritty | app/src/vt/; the attach fence becomes one lock; socketpty.rs deleted | 1,875 unit tests and six integration suites, unchanged in meaning |
| rio-vt as the core | the rio-vt adapter; alacritty behind core-alacritty | 1,873 of 1,874 tests on the first run; the one failure below |
| Pictures | the core lists and forgets pictures; the pane draws them; the window forgets them when a tab is hidden | tests that place, scroll, resend and forget a picture, and a real pane whose shell prints one |
| Real windows | hidden windows on their own sessions, photographed | the probe answers and the photographs below |
Where the cores parted
The one test that failed on rio-vt, the_cursor_and_the_modes_come_back, was right. Both cores keep one of the mouse-reporting modes (DEC 9, 1000, 1002, 1003) at a time, and they part only when a program resets a mode that isn’t the active one: alacritty clears that one mode, which was already off, and rio-vt clears mouse reporting altogether. The snapshot encoder restored the modes one at a time, so a later ?1003l undid an earlier ?1002h. The encoder now restores them as one setting, which is correct on either core and leaves the hash unchanged. Going the other way, the generated round-trip sweep, ignored for months because of a defect only alacritty produces, passes all 1,200 seeds on rio-vt and runs in the suite again.9
Reading rio-vt’s source for what the suite never asked found that it introduces itself as Rio. Terminal Delight answers as itself:
| Question | alacritty said | rio-vt says | Terminal Delight says |
|---|---|---|---|
Device attributes, CSI c | ?6c | ?62;4;6;22;52c | ?62;22c |
| XTVERSION | nothing | Rio 0.5.28 | terminal-delight 0.3.0 |
| Kitty keyboard query | nothing | ?0u | nothing |
The last difference showed up only in a real window. A picture sent as a temporary file (t=t) or shared memory (t=s) is deleted by the first terminal that reads it, and a pane on the session host is read by the host first. The same PNG sent three ways drew twice, inline and as a file, and the temporary file drew nothing. The host now rewrites those two into inline data in the window’s copy of the stream, before the original is deleted, and a test that fails without the rewrite holds it.10
Before the merge
A review hashed the same 94 screens with the build from before the swap and with the swapped build on both cores, and three second reviewers fed both cores the same bytes. Every finding was rio-vt’s, and each was fixed at Terminal Delight’s side of the boundary, with no patch to the crate.12
- Blank cells after a scroll, a clear or an erase now read as alacritty wrote them, background only. rio-vt had filled them with the whole pen, so an inverse or underline pen flooded the new lines. A new window now agrees with the old host on 92 of the 94 screens.
- Three denial-of-service paths stop before rio-vt’s parser. Combining marks:
e, U+0301 andESC[65535b, thirteen bytes, took 9.7 s and 2.8 GB, against 1.4 ms on alacritty, and a character now keeps at most 32 marks. A picture command that never ends, which grew to 837 MB, is capped at 4 MiB. A picture read from a FIFO held the lock and, on the session host, every pane’s keystrokes behind it; only a regular file in/dev/shmnow reaches the core. - A temporary picture file (
t=t) reaches rio-vt as a plain file, which it reads and leaves alone, and Terminal Delight deletes it by kitty’s rule: the marker in the file’s name, and a canonical parent of/tmp,/dev/shmor$TMPDIR. rio-vt’s own check looked only for the marker somewhere in the path, so/tmp/tty-graphics-protocol-x/../../home/you/noteswould have been deleted. - Synchronized updates no longer tear, hiding a tab forgets both screens’ pictures and gives their memory back, and a window attached to a host from before the swap keeps click and drag reporting.
In a window
With the screens asleep, hidden windows on their own sessions were photographed through Hyprland’s per-window capture. A probe in a pane asked the questions icat asks. The Kitty graphics query came back OK with the device attributes in 0.2 ms, where icat had waited 10 seconds. The kitty keyboard query came back empty. Switching to another tab logged that the pane forgot one picture and dropped one texture. The same program on the build then running on my desk drew nothing, and its next line sat where the picture should have been. A window from this build attached to a session host from that build and worked, answering programs the old way until that host is replaced.
Latency
The echo bench writes one byte the way the window writes a keystroke and times it until the echo is in the grid, 1,000 samples a run, with the build from before the swap and the swapped build run alternately and nothing else running.11
| Pane, load | alacritty p50 / p99 (µs) | rio-vt p50 / p99 (µs) |
|---|---|---|
| Window-owned, quiet | 30 / 43 | 24 / 39 |
| On the host, quiet | 67 / 126 | 56 / 83 |
| Window-owned, 8 busy panes beside it | 30 / 43 | 25 / 37 |
| On the host, 8 busy panes | 68 / 137 | 53 / 81 |
| Window-owned, 19 busy panes | 30 / 47 | 25 / 39 |
| On the host, 19 busy panes | 68 / 136 | 53 / 81 |
Under a flood of yes at full rate, which the bench measures but does not gate on, the hosted pane’s p99 fell (3,477 and 4,557 µs to 3,222 and 3,067 over two rounds; 12,631 to 8,541 with 19 panes) and the window-owned pane’s rose (177 and 815 to 471 and 1,724; 2,099 to 3,037). The rounds differ from each other by up to four times, and the cause is not established.
Reading a whole terminal costs a little more, because rio-vt assembles each cell from a packed word and a style table: the divergence hash over 10,030 lines takes 29.6 ms against 26.5, once per probe or attach, never per frame.9
What would falsify this
The choice rests on weights an agent translated from four answers of mine, and on measurements taken in harnesses outside Terminal Delight. Speed and memory would need a weight of 17.7 to reverse it, so no plausible reading of those answers does, but a core that behaves differently inside Terminal Delight over weeks of daily use could, which is why the alacritty core is still one flag away. The design makes claims a running build can break. If a window on rio-vt attached to a host still on alacritty keeps failing its divergence check on screens that look identical, the frozen encoding is wrong. If the window-owned pane’s tail under a saturating flood turns out to be the loop rather than the machine, owning the loop cost something the median does not show. And if a picture survives its tab being hidden and shown again, the pictures are not attentional.
I’d love to work with you. See the portfolio, or get in touch.
Sources
- The final scores under the sealed weights — Per-dimension scores and totals in vibes/final-2026-09-25.json, including the speed-and-memory weight that would tie the two. The earlier-weights figure is vibes/rank-2026-09-25.json, computed before fluency was measured.
- Repository facts for both projects — Stars, workflows and agent files from the GitHub API on 2026-09-25 (repo-facts.txt); commit shares and the commit message quoted, in summary.json.
- Canario, on Rio’s site — Read on 2026-09-25.
- Mitchell Hashimoto, “Libghostty is coming” — The announcement of libghostty-vt, read by the research agents on 2026-09-25.
- Every dependency Terminal Delight has on alacritty_terminal — A read-only inventory over app/src at main 62613cf, taken before the swap began.
- How rio-vt 0.5.28 embeds — Read from the published crate sources, with every claim that shapes a decision re-checked against the code before it was relied on.
- The architecture gate — The design, its eight decisions and what each was chosen over, and the risks with what answers each.
- The pull request that builds the swap — Pull request 825: the gates, the slices as commits, the code under app/src/vt, and the follow-ups it filed.
- Where rio-vt and alacritty part, and what Terminal Delight did about each — Each difference with the test or the source that found it, and the cost of reading a full history through the boundary.
- Pictures on the wire between a session host and its window — The rewrite, and a_picture_sent_as_a_temporary_file_reaches_the_attached_window in app/src/host.rs, which fails without it.
- The echo bench — Run twice on each build, alternating, and once with 19 flooding panes, on 2026-09-25; the numbers are in the swap’s status page.
- The review before the merge — The section “Review, before merge”: what the review measured against the old host, the fixes at the boundary in app/src/vt/text.rs, kitty.rs and compat.rs, and the follow-ups it filed.