- Fix wayland freezing.
- Windows: native
MessageBoxWclose-confirmation dialog whenconfirm-before-quit = true(default), mirroring macOS'sapplicationShouldTerminateNSAlertinrio-window/src/platform_impl/macos/app_delegate.rs. Replaces rio's in-window GPU-rendered confirmation modal on Windows. NewEventLoopRunner.confirm_before_quit: Cell<bool>set via the same publicEventLoop::set_confirm_before_quitthat macOS already uses (cfg gate widened toany(target_os = "macos", target_os = "windows")). TheWM_CLOSEhandler inevent_loop.rschecks the flag andvsync_state.window_count() <= 1(only the last window prompts — matches macOS's app-wide quit semantics) and on confirm-No swallows the message so the window stays open. rioterm'sWindowEvent::CloseRequestedhandler now follows the same fast path on Windows as on macOS —routes.remove(&window_id)+ maybe-exit()— since the user already confirmed at the OS layer. - Windows render loop now matches the macOS / CVDisplayLink model exactly:
Window::request_redrawsets a per-windowArc<AtomicBool>dirty flag instead of immediately callingRedrawWindow(RDW_INTERNALPAINT). The DwmFlush worker thread (rio-window/src/platform_impl/windows/event_loop/vsync.rs) is the single source of frame timing — per composition cycle it iterates the window registry and, for each window wheredirty || should_present_after_input(1 s post-input window, identical to macOS), callsRedrawWindow(.., RDW_INVALIDATE). ExistingWM_PAINT→RedrawRequestedpath is unchanged. Plumbing: newvsync::VSyncSharedState(Arc<RwLock<HashMap<HWND, Arc<AtomicBool>>>>registry +Mutex<Instant>for last input) lives onActiveEventLoop, threaded intoWindowDataand theWindowstruct so input handlers,request_redraw, and the worker all share it.Window::Dropunregisters beforeDESTROY_MSG_ID. Replaces the earlier "always invalidate per vsync" zed mirror — that approach worked but didn't compose with the app's ownrequest_redraw, which made scroll feel locked to DWM tick. Now scroll-driven renders queue immediately into the next vsync (≤16 ms wait) and idle frames are skipped entirely. Net behaviour matches macOS one-to-one. - Windows:
window.blur = truenow actually blurs (was a no-op stub). Implementation callsDwmSetWindowAttributewithDWMWA_SYSTEMBACKDROP_TYPE = DWMSBT_TRANSIENTWINDOW(Acrylic) when blur is on,DWMSBT_NONEwhen off — uses the existingset_system_backdropplumbing inrio-window/src/platform_impl/windows/window.rs. Wired both at window creation (viaattributes.blurinon_create) and at runtime via the cross-platformWindow::set_blur. Requires Windows 11 22H2+; older builds silently no-op (matches the previous behaviour). - Windows: vsync-driven render loop with 1-second post-input sustain (matches macOS / Linux / zed behaviour). All inside
rio-window— frontends still just respond toRedrawRequested. New worker thread (platform_impl/windows/event_loop/vsync.rs) callsDwmFlush()per composition cycle; when DWM is disabled, the monitor is asleep, or the call returns under the 1 ms threshold (RDP / occluded), it falls back tothread::sleepat the queriedDwmGetCompositionTimingInfo.qpcRefreshPeriodinterval, with arateRefreshnumerator/denominator fallback for spuriously low values and a 16.6 ms (60 Hz) default if both queries fail. Same heuristic zed uses ingpui_windows/src/vsync.rs. Each tickPostMessageW's a registered custom message (Winit::VsyncTick) to the existingthread_msg_target; the handler inthread_event_target_callbackenumerates thread windows viaEnumThreadWindows, filters withIsWindowVisible, and postsRedrawWindow(hwnd, RDW_INTERNALPAINT)per visible window so the existingWM_PAINTpath emitsRedrawRequestedunchanged. The fan-out is gated on a newEventLoopRunner.should_present_after_input()(1 s window after the last input event) — same field/method names as the macOS / Wayland / X11 implementations.mark_input_receivedis hooked intopublic_window_callback_innerforWM_KEY{DOWN,UP},WM_SYSKEY{DOWN,UP},WM_MOUSE{MOVE,WHEEL,HWHEEL}, all four button down/up pairs,WM_TOUCH, andWM_POINTER{DOWN,UPDATE,UP}.EventLoop::dropnow joins the worker thread (via astub()-and-mem::replaceswap) before destroyingthread_msg_targetso the worker can neverPostMessageWto a freed HWND. New windows-sys featureWin32_System_PerformanceforQueryPerformanceFrequency.build.rsno longer panics when cross-compiling between host and target OS (#[cfg(target_os = ...)]branches now also checkCARGO_CFG_TARGET_OS), socargo check --target x86_64-pc-windows-gnuactually works from a macOS workstation. - Linux: vsync-driven render loop with 1-second post-input sustain (matches macOS / zed behaviour). Two changes inside
rio-window(no rioterm churn — frontends still just respond toRedrawRequested). Wayland (platform_impl/linux/wayland/event_loop/mod.rs): thewl_callback::Donehandler now unconditionally re-arms the nextwl_surface.frame()and drives an auto-loop, modelled on zed'sgpui_linux/src/linux/wayland/window.rs:572-587. Compositors stop delivering Done for occluded windows so the loop pauses naturally without an explicit visibility check. X11 (platform_impl/linux/x11/mod.rs): a newcalloop::timer::Timersource reschedules at the primary monitor's xrandr-derived refresh rate (monitor::mode_refresh_rate_millihertz, fallback 60 Hz); each tick setsEventLoopState.vsync_pendingand the main pump fans the flag out to every visible window via the existingredraw_senderchannel. Mirrors zed'sgpui_linux/src/linux/x11/client.rs:1934-1971. Both paths gate the syntheticRedrawRequestedon a newshould_present_after_input()(1 s window after the last input event) — same field/method names as the existing macOS implementation inplatform_impl/macos/window_delegate.rs:139,997.mark_input_receivedis hooked into the WaylandKeyboardHandlerandPointerHandlerand into the X11 KeyPress/KeyRelease + XInput2 ButtonPress/Release/Motion dispatch sites inevent_processor.rs. Net effect: ProMotion / 144 Hz / 240 Hz monitors now stay at peak refresh during typing/scroll on Wayland and X11; idle terminals don't burn extra CPU because the gate falls back to the existing dirty-driven path outside the 1 s window. - Full kitty graphics protocol Unicode-placeholder support (
kitten icat --unicode-placeholderand any tmux/yazi/etc. that uses U+10EEEE placement cells). Wire-protocol bugs fixed: (1) the APC parser storedU=1into a privatecmd.virtual_placementfield that was never propagated toPlacementRequest, soplace_graphicalways fell through to the direct-overlay branch; (2)place_virtual_graphicwas auto-writing U+10EEEE cells to the grid, racing kitty's own writes (per the spec the application emits the cells, the terminal only stores metadata); (3) icat usesa=T,U=1(combined transmit-and-display) which goes throughkitty_transmit_and_display, notplace_graphic— that handler now also routes virtual placements correctly and pushes pixel data topending_images(the regular overlay path did this implicitly viaplace_kitty_overlay); (4) the diacritics table had 299 entries instead of the canonical 297 (two stray entries\u{06EA}and\u{06ED}from earlier work), shifting every index past the divergence point soimage_id_highdecoded as the wrong byte (e.g.0x5Cstored,0x5Edecoded). Replaced with the 297 entries fromkitty/gen/rowcolumn-diacritics.txt. Shader bug fixed:image.metalinterpretedsource_rect.zwas size but the new code passes it as end-coords; switched tomix(.xy, .zw, corner)(only worked before because every overlay used the full-image default[0,0,1,1]). Renderer side, modeled cell-by-cell on ghostty'sgraphics_unicode.zig: newRow.kitty_virtual_placeholderper-row dirty flag set inCrosswords::inputand checked by the scan loop, so we only walk rows that contain a placeholder (page.zig:1953-1958); newIncompletePlacement+can_append/append/completeimplements kitty's diacritic continuation rules — a cell with missing row/col/high-byte diacritics inherits from the previous cell on the row and the col field can be omitted to mean "auto-increment from prev" (graphics_unicode.zig:407-535); the renderer now walks each visible row left-to-right, builds runs of consecutive cells that belong to the same(image_id, placement_id, image_row)with sequential image columns, and pushes ONEGraphicOverlayper run instead of one per placement (matches ghostty'sPlacementIterator); per-run aspect-preserving fit, centering, and source-rect clipping factored intokitty_virtual::compute_run_geometry— handles partial visibility (placement scrolled half off-screen renders only the visible image slice), runs that fall entirely in the centering padding (returnsNone), and runs that straddle the padding boundary (clips both the screen rect and the source rect so the rendered slice exactly covers the fitted-image area). Placeholder cells render withstyle.background_color = Noneso the per-cell bg quad doesn't cover the BelowText image.GraphicOverlaygained asource_rect: [f32; 4]field (default[0,0,1,1]viaFULL_SOURCE_RECT) wired through to the existingImageInstance.source_rect. Tests cover: the 297-entry diacritic table,IncompletePlacement::from_cellacross all input shapes (Indexed fg, Spec fg + 3rd diacritic, underline=placement_id, missing diacritics, Named fg → 0), allcan_appendcases (row inherit, col inherit, sequential col, col jump, row mismatch, image-id mismatch, image-id-high inherit), a 3-cell run with only the first cell carrying diacritics, parser-levelU=1propagation, the metadata-only contract ofplace_virtual_graphic, an end-to-end pass that feeds icat's exact wire format and asserts both the resulting grid cells AND the per-row dirty flag, andcompute_run_geometryacross exact-fit / image-taller-than-grid (horizontal centering + left-padding cull) / image-wider-than-grid (vertical centering + top-row cull) / partial-visibility-scrolled-off-top / origin-offset / zero-sized-image cases. - macOS Metal renderer: triple-buffered pipeline modeled on zed's
gpui_macos::InstanceBufferPool.CAMetalLayer.maximumDrawableCountis now 3 (was 2). The six per-frame buffers (text vertices, quad instances, kitty/sixel image instances, bg-image instance, bg-fill instance, Globals uniform) collapsed into a single pooledmetal::Bufferper frame: text/quad/image data bump-allocates from the pool buffer with 256-byte aligned offsets, andGlobals(transform +input_colorspace, ~80 B) goes throughset_vertex_bytes/set_fragment_bytesso no buffer is needed for it at all. The pool starts at 2 MiB, doubles on overflow up to a 256 MiB cap (matches zed). On overflow we end-encoding, drop the never-committed command buffer, grow the pool, and retry the frame from scratch — old smaller buffers in flight get rejected byreleaseaftergrowand dropped naturally, no "all 3 slots must grow together" coordination.command_buffer.add_completed_handlerreleases the buffer back to theArc<Mutex<InstanceBufferPool>>on the GPU completion thread. Net effect: CPU can stay up to 3 frames ahead of the GPU without racing the previously singleStorageModeSharedbuffers (the old code was relying on luck — every frame shared one allocation), and ProMotion 120 Hz is now reachable without dropped frames during heavy scenes.MetalRenderer::resizeis gone (no more uniform buffer to refresh on resize);Renderer::render_metalnow owns command-buffer / encoder / drawable / commit, soSugarloaf::render_metalis just a one-liner that handsbg_colorand theMetalContextover. - Fix Nerd Font glyphs from an
extrasfamily (e.g.extras = [{family="JetBrainsMono Nerd Font Mono"}]) overflowing into the next grid cell.FontLibraryData::loadwas loading every extras/symbol-map font withis_emoji = true, which madefont_cache::resolve_withclamp every glyph from that font towidth = 2.0; the terminal grid still budgeted one cell per PUA codepoint, so the layout advanced 2× per glyph and the next character painted on top of the right half. Extras and symbol-map fonts now load withis_emoji = false, andFontData::from_data/from_sliceauto-promote tois_emoji = truewhen the SFNT carries a color table (COLR,CBDT,CBLC, orsbix) — mirroring ghostty'sFT_HAS_COLOR()/ CoreText SBIX check, so a real emoji font inextrasstill gets the wide-cell / color-atlas treatment without needing a config flag. - Removed the dedicated
fonts.emojiconfig slot. Rio now relies entirely onfonts.extrasplus the color-table auto-detection above: dropextras = [{family="Apple Color Emoji"}](or any color family) and it's picked up as emoji; otherwise the bundled Twemoji continues to serve as the default color-emoji fallback. User[fonts.emoji]sections in existing configs are silently ignored instead of erroring. - PUA / Nerd Font glyphs with an adjacent blank cell now fill the full 2-cell constraint crisply instead of rendering half-sized or blurry.
pua_constraint_widthin the rioterm renderer still picks 1 or 2 cells based on neighbour content; the compositor's fit pass (sugarloaf/src/renderer/compositor.rs) dropped the.min(1.0)cap so the glyph can scale up to the 2-cell slot — and to avoid stretching a bitmap atlas entry (which is blurry),GlyphCacheSession::get_at_size(id, size)now rasterizes the glyph fresh atfont_size × cellswhencells > 1. Cache key(id, size)already disambiguates the two rasterizations, so 1-cell and 2-cell lookups for the same codepoint coexist. Fixes JetBrainsMono NF Mono (patched to ~1-cell advance) showing as half-sized inside a 2-cell slot next to Cascadia NF whose glyphs render ~2 cells wide natively. - Fix vertical positioning of constraint-fit PUA glyphs. The compositor was placing the scaled glyph at
baseline − entry.top × scale, which drifts upward as the scale grows because the top-bearing scales faster than the descent-bearing. Switched to cell-centered placement (topline + (line_height − sh) / 2.0), matching ghostty's.align_vertical = .center1choice forisSymbol(cp)— symbols aren't baseline-anchored the way text characters are. - macOS Metal renderer: wide-gamut pipeline with ghostty-compatible
nativealpha blending.CAMetalLayeruses plainBGRA8Unormtagged withkCGColorSpaceDisplayP3(earlier iterations usedBGRA8Unorm_sRGB, which makes Metal do linear-light blending — physically correct but visibly brighter on AA edges and translucent overlays than ghostty's and Terminal.app's default). The three pipeline color attachments moved back toBGRA8Unormto match. Fragment shaders (renderer.metal,image.metal) nowsrgb_to_linear → sRGB-to-P3 matrix → linear_to_srgbbefore returning, emitting gamma-encoded DisplayP3 bytes that the drawable stores verbatim and the compositor displays directly.MTLClearColorgoes through the same encode on the Rust side (sugarloaf::prepare_output_rgb_f64) so the first cleared pixel matches a shader-drawn quad of the same theme colour. Net effect: same saturation as before (P3 gamut preserved), same glyph weight as ghosttyalpha-blending = native/ Terminal.app (no more AA-edge brightening). Themetalcrate doesn't wrapCAMetalLayer.colorspaceyet, so the setter goes throughmsg_send!; theCGColorSpacehandle ismem::forget-ed to guarantee the pointer stays live for the layer's lifetime, and a post-set read-back logs a warning if the colorspace didn't take. - Breaking (macOS):
[window] colorspacedefault flipped fromdisplay-p3tosrgb. The semantics also changed to match ghostty'swindow-colorspace: the setting now describes how Rio interprets input color bytes (hex values in config, ANSI direct-color sequences), not which colorspace the Metal surface uses. The Metal surface is always DisplayP3 regardless of this setting. Withsrgb(the new default), Rio applies a Bradford-adapted sRGB D65 → DisplayP3 D65 matrix (in linear light, aftersrgb_to_linear) so#ff0000renders as sRGB-standard red — matching ghostty and every other app. Users who preferred the previous more-saturated P3-interpreted look can pin it withcolorspace = "display-p3". The matrix runs in the fragment shader (prepare_output_rgbinrenderer.metal/image.metal) driven by a newinput_colorspace: u8field on theGlobalsuniform, and at the Rust boundary for theMTLClearColor(sugarloaf::prepare_output_rgb_f64) so the first HW-cleared pixel lands in the same colorspace as shader-emitted pixels.MetalRenderer::newtakes the colorspace at construction and writes it into every uniform upload; the Metal encoder now binds the uniform buffer to fragment slot 1 (in addition to the existing vertex slot 1) sofs_main/image_fs_maincan read the flag. - New
OpenCommandPalettekey-binding action (defaultCmd+Shift+Pon macOS,Ctrl+Shift+Pelsewhere), user-remappable viaopencommandpalettein[bindings]. Replaces the hardcodedCmd/Ctrl+Shift+Pbranch that used to sit inscreen::process_key_eventahead of the binding system, so the palette shortcut now composes with mode-gating like every other action. - New
List Fontscommand palette entry: selecting it keeps the palette open and swaps the list for the host system's font families (viafont-kit'sSystemSource), fuzzy-filterable. Enter copies the selected family to the clipboard and closes the palette. Each font row shows a hand-drawn (rect-only) rounded copy icon on the right to advertise the action. - Command palette scrollbar now reuses the terminal scrollbar's look and behaviour through shared primitives in
renderer::scrollbar(opacity_from_last_scroll,compute_thumb,draw_thumb) — 6 px wide,[0.6, 0.6, 0.6, 0.5]gray, no rounded corners, 2 s visibility + 300 ms fade after the last scroll event. Only appears when the user has actually scrolled and resets on palette close / query change. Sugarloaf::font_family_names()+FontLibrary::family_names()for enumerating the host font catalog.- Rio
Attributesre-exported at the sugarloaf crate root so downstream callers don't have to reach intofont_introspector.