Changelog
0.5.27 (2026-08-30)
-
window.opacityworks on Windows. Setting it below 1 rendered the window nearly invisible on the wgpu backend and fully invisible on the CPU one, for three stacked reasons. Default Windows builds never took the GPU path at all: the backend dispatch gated on a cargo feature that default builds don't enable, silently falling back to the CPU rasterizer even though the wgpu code was compiled in. The CPU rasterizer wrote an alpha byte of zero into every pixel and masked it back out of every blend, which DWM — composited per-pixel oncewindow.opacity < 1engages blur-behind — reads as a fully transparent window. And the wgpu clear color went out straight on surfaces that DWM interprets as premultiplied, so the background read as nearly transparent. All three are fixed by @shiena (#1583, landed as #1905): Windows defaults now take the wgpu path, the clear color is premultiplied exactly when the surface composites premultiplied, and the CPU framebuffer carries premultiplied alpha end to end — the convention every terminal that does transparency converged on (alacritty premultiplies its clear color the same way, kitty and ghostty premultiply everything). The alpha channel costs nothing: it rides the SWAR lane pair the green channel occupied alone (00AA00GGmirroring the existing00RR00BBtrick), so all four blend kernels do four channels in the same two multiplies as the old opaque three, held bit-exact to the scalar reference by randomized agreement tests. Around the fix, review hardening: wgpu picks Vulkan-class adapters whose surface can actually carry alpha when transparency is requested — DX12 swapchains on a plain window expose no such mode, the same gap behind wezterm's long-open WebGPU transparency bug — with a deterministic ranking that never lands on a software rasterizer and falls back gracefully when a broken driver refuses a device;[renderer] filtersedits live-reload on Windows now; and since a filter chain's final pass overwrites the frame's alpha, combining filters with opacity warns once instead of cancelling silently. -
Images drawn with kitty's Unicode-placeholder flow show up in Neovim. The protocol says a placeholder cell carrying no placement id may resolve to any virtual placement of its image, and both kitty and ghostty honor that; Rio only tried the exact id, so a placement registered as
p=Nwas never found for cells without one — which is the common case, because Neovim's TUI only emits the id-carrying underline color for cells that also have an underline attribute, and snacks.image doesn't underline its placeholders: the cells reserved space and nothing drew. Id 0 now falls back to the lowest placement id of the image, deterministic and still letting an explicitp=0win; placeholders also draw when the placement arrives after the transmission (a=tthena=p,U=1), and GPU uploads are tracked per image (#1893). -
librio: bracketed paste, mode bits, dynamic colors.
rio_surface_pastegives C embedders whatrio_surface_textcouldn't: a paste that arrives atomically, wrapped inESC[200~/ESC[201~when the program enabled mode 2004 (contributed by @ezemtsov, #1896). Review hardened it into the shape rioterm itself uses: the payload travels verbatim inside the brackets — editors are entitled to distinguish a pasted newline from Enter — minus ESC, ETX and the 8-bit CSI, so a malicious clipboard can never close the bracket early and inject keystrokes; without brackets, newlines normalize to the CR the Enter key produces. Alongside it,rio_surface_mode_bitsreports mouse reporting, application cursor keys, alternate screen and bracketed paste for input decisions the host makes on its own, andrio_render_state_dynamic_colorexposes the OSC 10/11/12 dynamic foreground, background and cursor colors — returning unset after an OSC 110/111/112 reset so the host falls back to its own scheme. -
librio can no longer get its host killed by SIGPIPE. Stress-running the new C-ABI test gate crashed roughly one run in eight with exit 141: during pty teardown, the signal machinery closes its internal wake pipe while a SIGCHLD from the dying child is in flight, and the signal handler writes into that pipe expecting
EPIPEback — an assumption that holds in Rust programs, which ignore SIGPIPE at startup, and kills C and Swift embedders, which don't. The wake sockets now opt out per-fd (SO_NOSIGPIPE, the same thing upstream mio does on Apple targets), and the C gate that found the bug stays SIGPIPE-naive on purpose, doubling as the regression test (#1899). The gate also stopped depending on the login shell's mood: it pastes into a/bin/catchild where the tty's own echo is the assertion, covering the unbracketed and bracketed paths end to end — mode 2004 enabled by a real child, verified through the new mode-bits API. Andnix buildruns the test suite green again: the sandbox ships no/bin/sleep, so the tests' quiet child spawns through/bin/sh(#1900). -
cargo install riotermworks on Windows again. The window icon was compiled into the publishedrio-windowlibrary crate from a path outside the crate — a file cargo cannot package — so every registry build of the crate on Windows died in its build script (#1887, diagnosed by @milkowski). The resource now lives in the binary crate, embedding the identical icon it already ships for the runtime window, which fixes two quieter bugs in the same motion: on the GNU toolchain the linker could prune the icon out of the library archive entirely, and right-clicking rio.exe → Properties used to advertiserio-window— "Winit fork maintained for Rio terminal" — as the product, because the resource compiler stamps version info from whichever crate runs it (#1898).
0.5.26 (2026-08-24)
- Massive PTY throughput improvement, most visible in programs that repaint the whole screen with truecolor (gradients, animations, terminal video, heavy TUIs). Rio's pseudo-terminal was created with a baud rate of 0, which makes the BSD/XNU kernel clamp the tty output queue to its ~100-byte floor — every write/read handshake moved ~150 bytes through a syscall round trip, capping drain throughput at a fraction of what the kernel can do. The pty now opens at 230400 baud (the kernel sizes its queue from the baud rate on macOS/BSD; on Linux it only fixes
sttyreporting "0 baud"), and the reader drains until the queue is empty before returning to poll. An unthrottled full-screen truecolor benchmark went from ~340 fps to ~1900 fps. - Per-cell truecolor styles no longer degrade over long sessions: the style intern table is now garbage-collected with a mark-and-sweep driven by per-row hints, so gradient- and animation-heavy output can run indefinitely. Previously the table could saturate at 65,535 distinct styles, after which new colors silently rendered with the default style.
- The renderer's frame snapshot now carries fully resolved per-row style values instead of a shared style table, removing an entire class of stale-color windows around alternate-screen switches, resets, and style-table growth.
- Copying text that includes color-erased blanks (e.g.
ESC[48;2;…m+ESC[K) no longer inserts NUL characters or another cell's combining marks into the clipboard, and session serialization now preserves those fills' background colors instead of emitting a wrong style. Hovering or clicking such a blank can no longer resolve an unrelated hyperlink's URL. - Embedder note (librio):
RenderState::style_of(square)is replaced bystyle_at(line, column, square)and per-rowrow_styles(line), matching the new resolved-style snapshot. - Hovering a URL underlines the URL, not a spot to its right. The hover path used regex byte offsets directly as cell columns, so any multi-byte character left of the match — box-drawing prefixes, CJK, emoji — dragged the underline right by one cell per extra UTF-8 byte; and it searched one visual row at a time, so soft-wrapped URLs matched truncated or not at all. Matching now runs over the logical unwrapped line under the pointer with a byte-to-cell map recording every byte's source cell, so bounds cannot drift regardless of encoding and a wrapped URL resolves as one whole link (#1884).
- macOS: resizing no longer flashes a stretched stale frame. The layer scaled its last-presented contents to the new bounds while the next frame rendered; its gravity is pinned top-left now, so old pixels stay put until real ones replace them.
- The glyph protocol spec caught up with its implementations. v1.10 caps decoded outlines at 64 KiB — the wire form is compressed, so the existing wire cap bounded nothing: a crafted payload expanded to ~768 KiB of points per slot, ~768 MiB per session — with the cap requested by Mitchell Hashimoto against the expansion while implementing the protocol in Ghostty, and enforced by the reference implementation with a
reason=outline_too_largerejection (#1878). v1.11 settleswidthas a render-level span: layout stays on systemwcwidth, awidth=2glyph overflows the following cell in pixels, and the client keeps that cell blank — withdrawing v1.7's "authoritative for all layout decisions" wording (#1879).
0.5.25 (2026-08-15)
-
The cursor trail actually works now. Five open issues traced to one pair of root causes: the animation's continuation flag was read before the trail advanced, so a trail could freeze mid-flight and stay painted until unrelated output arrived — the "trail covers the whole screen" screenshots (#1597) were that freeze photographing a scroll-sized jump — and the spring integration snapped to its destination whenever a frame gap exceeded the animation length, which at event-driven frame cadence was most of the time, the "feels like 15 FPS" choppiness of #1830. The motion model is now kitty's, verified line-by-line against its source: an exponential ease-out that composes exactly across irregular frames (no fixed-cadence render loop to depend on), with per-corner speed picked by direction alignment so the quad stretches into the smear. Around it, the behaviors the issue tracker asked for: the trail fades out when a program hides the cursor — yazi and lazygit redraws no longer animate a phantom (#1511) — jumps at or under a threshold of cells don't trail so typing stays trail-free, and four new
[effects]options:trail-cursor-color,trail-cursor-opacity(#1594),trail-cursor-decay = [fast_ms, slow_ms], andtrail-cursor-start-threshold(#1520). One invariant runs through the rework: layout is never travel — window resizes, font-size changes, display rescales, split-divider drags and tab-strip reflows snap the trail in place, and panel switches teleport it, so the smear only ever draws where the cursor actually went; and beam and underline trails converge onto the exact cursor geometry the grid draws, reusing its thickness and baseline math rather than approximating them (#1871). -
Rio launches on macOS 10.15 again. wgpu 30 references HDR color-space constants (
kCGColorSpaceExtendedDisplayP3, the BT.2100 pair) that only exist on macOS 11+; their use is runtime-gated, but the references linked as strong dyld imports, so the loader aborted the launch before a single line of Rio ran — theSymbol not foundcrash of #1873 and #1711, broken since the wgpu upgrade in 0.4.9. CoreGraphics is now weak-linked for the rio binary: the missing constants resolve to null instead of aborting, every CG symbol Rio actually calls has existed since 10.11, and an audit of the binary's import table found no other post-10.15 strong reference waiting behind this one. Upstream fixed the root cause on trunk a week after v30 shipped; gfx-rs/wgpu#10067 asks for a patch release, and the next wgpu upgrade retires the workaround. -
tmux new -ADXsno longer crashes Rio. The-Xflag SIGHUPs the shell of a background tab, and the close path only adjusted the focused-tab index when the dying tab was itself the focused one: removing a background tab shifted every index left while the focused index stayed put, and the very next tab lookup panicked out of bounds (#1848). The rewrite also closes two adjacent holes the crash exposed: a background tab's PTY exiting while the focused tab had splits used to leave a zombie tab that never closed, and a background tab that itself had splits could lose the whole tab, living splits included, when just one of its shells exited. The close path now searches every panel of every tab and removes exactly the panel that died. -
Hovering a link shows its target in a bottom-left pill, the way a browser status bar does. An OSC 8 link's display text is arbitrary, so its destination stayed invisible until you had already opened it; the pill shows the real URI, tail-elided so the scheme and host always survive truncation, and it never draws while a modal overlay (search, command palette, assistant, quit confirmation) would consume the click anyway (#1874, by @aymanbagabas).
-
librio: mouse reporting for embedders.
mouse_button()andmouse_motion()encode presses, releases and drags for programs that grab the mouse (X10 and DEC 1000/1002/1003), honoring shift-to-bypass so local selection stays reachable. Both return whether a report was written, telling the host when not to start its own selection. Exposed over the C ABI.