neuroplast.ioHuman-machine interface research

Ghostty

github.com/ghostty-org/ghostty · MIT

For hottyterm

31 patches on 827d5c8. hottyterm is Ghostty with HOTTY, HTML over the TTY, built in: a program that prints HOTTY surfaces gets HTML and CSS laid out by hotty-blitz and drawn into the cells. Everything else stays Ghostty's: its config, its TERM, its resources. We mod Ghostty rather than fork it so that hottyterm stays upstream plus a short list of changes, most of them hooks into one new file, src/termio/hotty.zig, and so that going back to Ghostty means doing without HOTTY, never undoing it. A few of the patches are fixes any Ghostty user would have: touch scrolling on GTK, a resize race, kitty image uploads. hottyterm's name, icon and application id aren't in the set: scripts/brand.py generates them from brand/ as one commit on top.

The patches, each with its upstream fit:

  1. Programs draw HTML surfaces over the cells (HOTTY) — no
  2. A packaged Linux build finds its library next to bin/ — no
  3. HOTTY surfaces keep their images through many resizes — no
  4. The pointer reaches the cells through a surface's empty parts — no
  5. Animated GIFs, APNGs and WebPs in a surface play — no
  6. A program hears how many rows its surface's document needs — no
  7. A setting, hotty-net, says what a surface may fetch — no
  8. Hover reaches the program, and leaving the window ends it — no
  9. A scrolling surface takes the wheel before the terminal — no
  10. A key's release goes where its press went — no
  11. A plain click opens a link — maybe
  12. The click that focuses a terminal clicks too — maybe
  13. Only the IO loop's thread arms the surfaces' frame timer — no
  14. A touch drag scrolls (GTK) — yes
  15. A surface's text field reads keys as the program would — no
  16. A touch taps and scrolls where the finger is (GTK) — yes
  17. A touch drags what a surface keeps from panning — no
  18. Zooming a surface is a browser's zoom — no
  19. A resize no longer races a full-screen app's redraw — yes
  20. A kitty frame edit uploads only the rectangle it changed (OpenGL) — yes
  21. A surface is offered a key before the shortcuts — no
  22. No tabs: windows and full screen are the window manager's — no
  23. A surface's changed rectangles go straight into its image — no
  24. The macOS menu sees keys as key-remap makes them — maybe
  25. Metal writes a kitty frame edit's rectangles with a blit — yes
  26. A click on a surface carries the platform's click count — no
  27. Copy takes a surface's selection too — no
  28. A surface's text field cuts, pastes and undoes — no
  29. Copying from a surface follows its keymap — no
  30. Key sequences, edit commands and which-key menus — no
  31. Keys wait behind a paste, and a screen switch drops its commands — no

Programs draw HTML surfaces over the cells (HOTTY)

Upstream fit: no · patch 0001

What it changes: a program can send HOTTY, HTML over the TTY, on OSC 7279: documents that the terminal lays out and draws through hotty-blitz, Blitz behind a C ABI, and shows as kitty images placed in the cells, updated by kitty frame edits of the rectangles that changed. The logic is a new file, src/termio/hotty.zig. Upstream files get hooks only: Termio's settings, a flush per read and the resize; the stream handler's unknown OSC (#14452) and full reset; the Surface's keys, clicks, hover and font size; and linking libhotty_blitz.

Why we carry it: it is hottyterm, a terminal that speaks HOTTY natively, with no script, no network and no kitty graphics across the pty.

For upstream: no. It serves HOTTY, our protocol, and links a Rust library that Ghostty doesn't build.

A packaged Linux build finds its library next to bin/

Upstream fit: no · patch 0002

What it changes: the Linux executable gets an rpath of $ORIGIN/../lib, so a build unpacked anywhere finds libhotty_blitz.so in the lib/ beside its bin/. The macOS app links hotty-blitz statically and needs nothing. Two lines in GhosttyExe.

Why we carry it: hottyterm's Linux release archive, and the AUR and Homebrew packages made from it, ship the library that way.

For upstream: no. Ghostty links no shared library of its own, so it has nothing to find.

HOTTY surfaces keep their images through many resizes

Upstream fit: no · patch 0003

What it changes: each placement of a surface at a new size takes a new kitty image id. The ids cycled through 256 values, so a program that resizes its surfaces often came round to the id of another surface's live image and replaced it, and surfaces vanished after a few dozen resizes. Ids now cycle through 65,536 values and skip any that a surface holds or an image on either screen uses.

Why we carry it: HOTTY programs resize surfaces all the time, as plx's dialogs do.

For upstream: no. Only hotty.zig, which upstream doesn't have, assigns these ids.

The pointer reaches the cells through a surface's empty parts

Upstream fit: no · patch 0004

What it changes: a surface takes the pointer only where CSS hit testing finds an element there that takes it (pointer-events: none is skipped), looking from the top window down. Anywhere else, presses, motion and releases are reported to the program as on any cell, and the pointer has the shape the program set with OSC 22. The capabilities announce it.

Why we carry it: HOTTY SPEC §9.3, so that a program can float a surface over its own text and still be clicked through it.

For upstream: no. It only changes how HOTTY surfaces are hit.

Animated GIFs, APNGs and WebPs in a surface play

Upstream fit: no · patch 0005

What it changes: hotty-blitz says when an animation's next frame is due. A timer on the IO thread's loop fires then, renders the frame under the terminal lock as a frame edit, wakes the renderer and sets itself for the next one. With nothing playing there is no timer. While the program holds its output (mode 2026) the timer waits, so a batch still shows whole. One upstream line, in Termio, hands the loop to hotty.zig.

Why we carry it: HOTTY documents show images as a browser would.

For upstream: no. It animates HOTTY surfaces only.

A program hears how many rows its surface's document needs

Upstream fit: no · patch 0006

What it changes: a placement with f=1 is sent fit, the rows its document needs, whenever that changes (HOTTY SPEC §5.2). hotty-blitz finds it while rendering. Every path that renders on the IO thread (a flush, a resize, the animation timer) now sends the replies a render found, as the OSC handler does.

Why we carry it: a program sizes its surface to what it shows, as plx's dialogs and margin's prose do.

For upstream: no. It is part of HOTTY.

A setting, hotty-net, says what a surface may fetch

Upstream fit: no · patch 0007

What it changes: a new setting, hotty-net, in CSP syntax (img-src https:), empty by default, so nothing is fetched until the user allows it. A surface fetches only what both this and the document's own policy allow (HOTTY SPEC §7.2), and the capabilities report it. Fetches run on hotty-blitz's threads and wake the IO loop through an xev.Async when something arrives, which then draws as the animation timer does. Upstream lines: the field in Config.zig, and a call that applies it when the configuration changes.

Why we carry it: HOTTY documents may show remote images, and the user decides from where.

For upstream: no. It configures HOTTY only.

Hover reaches the program, and leaving the window ends it

Upstream fit: no · patch 0008

What it changes: a placement with v=1 hears hover, with the nearest id under the pointer, each time it changes, and out when the pointer leaves the window (HOTTY SPEC §9.4). The apprts report leaving the window as a move to -1, -1, which hotty.zig ignored, so a surface stayed hovered. Now the surface under the pointer is left then, unless a press holds the pointer for it.

Why we carry it: menus and tooltips in HOTTY programs.

For upstream: no. The fix is in hotty.zig's own handling; Ghostty's cells already handle leaving.

A scrolling surface takes the wheel before the terminal

Upstream fit: no · patch 0009

What it changes: a document that asks to scroll (a=doc with scroll=1, 2 or 3) scrolls as a page does. A wheel, a touchpad scroll or a touch drag over it is the document's while something in it can move that way. Past that, it goes on to the terminal as before (the scrollback, mouse reports, alternate scroll) unless overscroll-behavior stops it. Surface.scrollCallback hands the wheel to hotty.zig first (two upstream lines), which turns Ghostty's offsets into pixels, with mouse-scroll-multiplier. Scrollbars fade, so key and pointer events now set the frame timer after they render.

Why we carry it: HOTTY SPEC §5.3. Long documents, such as docs pages, scroll inside their surface.

For upstream: no. It routes the wheel to HOTTY surfaces.

A key's release goes where its press went

Upstream fit: no · patch 0010

What it changes: while a surface held the keyboard, every key release was swallowed, and while none did, every release was passed on. A program that asks the kitty keyboard protocol for event types could hear a press without its release, or a release without its press, when a surface took or gave back the keyboard between them. hotty.zig now keeps the keys whose press a surface took, and sends each release the same way. No upstream lines.

Why we carry it: programs that use the kitty keyboard protocol and HOTTY surfaces together.

For upstream: no. Only hotty.zig changes.

Upstream fit: maybe · patch 0011

What it changes: a new setting, hotty-link-click. With plain, the default, a click with no modifier opens a URL or an OSC 8 hyperlink in the cells, or a surface's link, as a browser does, and the pointer over one underlines it. A drag still selects, and Ctrl+click (Cmd+click on macOS) still opens. While the program reports the mouse, the click stays the program's. modifier is Ghostty's behaviour. Upstream lines: 4 in Surface.zig, 14 in Config.zig.

Why we carry it: links open as in a browser, in the cells and in HOTTY documents alike.

For upstream: maybe. The cells' half is small and generic, but the setting is named for HOTTY and the decision is made in hotty.zig. It would need a name of Ghostty's and its logic moved into Surface.zig.

The click that focuses a terminal clicks too

Upstream fit: maybe · patch 0012

What it changes: Ghostty drops the click that gives a terminal focus. On macOS the view declines first mouse and eats the click that moves split focus (#2595), and GTK suppresses the press and release on a terminal without focus. A new setting, hotty-focus-click: pass, the default, lets that click through, to a surface, a link, or the program as a mouse event, as a browser does. focus is Ghostty's behaviour. Upstream lines: 11 in Config.zig, 2 in Surface.zig, 1 in the GTK surface and 21 in the macOS app.

Why we carry it: a click on a button in a HOTTY document in another split, or another window, presses it.

For upstream: maybe. It is generic, but Ghostty drops the click on purpose (a click that only meant to focus vim would move its cursor), so it would only be wanted as an option, named as Ghostty names things.

Only the IO loop's thread arms the surfaces' frame timer

Upstream fit: no · patch 0013

What it changes: hotty.zig armed the IO loop's timer from whichever thread called it: the stream's reader, or the app's thread for keys and the pointer. xev's loops aren't thread-safe: kqueue's timer reset edits the loop's timer heap on the caller's thread, and every backend takes a new timer only on the loop's next turn, against its cached clock. Off the loop's thread it now wakes the loop instead, through the async that fetches already use, and the loop's tick sets the timer.

Why we carry it: the timer drives animations, fading scrollbars and coalesced resizes, and arming it from another thread was a data race.

For upstream: no. The timer is hotty.zig's.

A touch drag scrolls (GTK)

Upstream fit: yes · patch 0014

What it changes: on a touchscreen, a drag with one finger selected text instead of scrolling, since GTK emulates a pointer from it. A touch-only GestureDrag in the capture phase now turns each drag update into a precision scroll of its delta, through Surface.scrollCallback, so a touch drag goes wherever a touchpad's scroll goes: mouse reports, the alternate screen, the scrollback. A mouse drag still selects. The scrolled window's kinetic scrolling is off, so the same drag isn't scrolled twice. 68 lines, in the GTK apprt only.

Why we carry it: hottyterm on touchscreen laptops and tablets.

For upstream: yes. It is written in upstream's style, it fixes touch scrolling on Linux for every Ghostty user, and nothing in it is ours.

A surface's text field reads keys as the program would

Upstream fit: no · patch 0015

What it changes: the surface holding the keyboard gets each key as Ghostty encodes it for the program, in the modes the program set (hotty_host_key_bytes), rather than as a key code and the text the layout composed, so a text field's keymap is its program's (HOTTY SPEC §10.2, §10.4). What a binding writes (text:, csi:, esc:, cursor_key) goes to the surface first too: on macOS, Cmd+Left writes 0x01, which a field reads as Ctrl+a. Keys a field doesn't use reach the program as they came. Upstream lines: encodeKeyOpts is made public, and three one-line hooks go into performBindingAction.

Why we carry it: HOTTY text fields.

For upstream: no. It routes keys to HOTTY surfaces.

A touch taps and scrolls where the finger is (GTK)

Upstream fit: yes · patch 0016

What it changes: GTK moves no pointer for a touch, so the cursor stayed where the mouse last was: a touch drag scrolled whatever was under the mouse, and a tap clicked there. The touch's start point is now the cursor's position when it begins and while it scrolls, so mouseButtonCallback and scrollCallback see where the finger is. It builds on "A touch drag scrolls".

Why we carry it: the same touchscreens.

For upstream: yes, together with "A touch drag scrolls". It is generic and in upstream's style.

A touch drags what a surface keeps from panning

Upstream fit: no · patch 0017

What it changes: a touch that begins over a surface, where the surface takes the pointer, is hotty-blitz's to decide (hotty_host_touch): every phase of it goes there, taps included, and none to the pointer. While it may still become a drag, its moves wait. Once it pans, the terminal scrolls from where it began; once it drags, its moves are the surface's. A second finger, or the platform, cancels it. A touch over the cells, or one that only focuses a split, is the terminal's as before (HOTTY SPEC §9.1). The GTK surface gets the touch's offsets; the logic is in hotty.zig.

Why we carry it: sliders and drag and drop in HOTTY documents, on a touchscreen.

For upstream: no. It serves HOTTY surfaces.

Zooming a surface is a browser's zoom

Upstream fit: no · patch 0018

What it changes: Ctrl+= changed the font's size in points and kept the scale, so hotty-blitz laid a document out larger in CSS px with only the font grown, and each placement kept its old source rectangle, showing the top left of the new frame stretched. Now CSS px come from the configured font-size, and a zoom multiplies the scale instead: the layout stays, and everything grows (HOTTY SPEC §5.3). A resize resets each placement's source rectangle to its window at the new cell size, and wakes the IO mailbox after its replies. A config reload that changes font-size changes CSS px, and the program hears resize.

Why we carry it: zooming hottyterm zooms its documents as a browser does.

For upstream: no. It changes how HOTTY measures, not the cells.

A resize no longer races a full-screen app's redraw

Upstream fit: yes · patch 0019

What it changes: Termio.resize set the pty's size (TIOCSWINSZ, so the child gets SIGWINCH) before it took the renderer lock and resized the terminal. The read thread parses the child's output under that lock, so under load the frame a full-screen app drew for the new size could be parsed onto the old grid. The resize then moved it: a shorter screen trims rows from the top, and a wider frame wraps and scrolls. Images anchored to those rows moved with them. Now the terminal is resized first, and the pty size set while the lock is still held. The in-band size report (mode 2048) still follows. 11 lines in Termio.zig.

Why we carry it: plx's decorations showed from two sizes at once after a window resize under load.

For upstream: yes. It is a small fix to a race that any full-screen program can hit.

A kitty frame edit uploads only the rectangle it changed (OpenGL)

Upstream fit: yes · patch 0020

What it changes: a frame edit of the displayed frame (a=f with r) composes a rectangle into the image in place, but the renderer copied the whole image under the terminal lock and uploaded it into a new texture, so a one-cell change to a large image cost the whole image. Now the image logs each edit's rectangle with the generation it stamped. Anything else that stamps a generation breaks the chain, and a full chain (64 edits) keeps its latest half. A renderer with an earlier generation copies only those rectangles and writes them into the texture it has (Texture.replaceRegion). This patch does OpenGL; Metal follows in its own. About 250 lines outside tests, with tests of the chain and the renderer's region.

Why we carry it: HOTTY surfaces arrive as kitty frame edits of the rectangles that changed. It is part of what took a 10 Hz dashboard from about 15% of a core to 8%.

For upstream: yes. Every program that animates kitty images with frame edits gets cheaper frames, and nothing else behaves differently.

A surface is offered a key before the shortcuts

Upstream fit: no · patch 0021

What it changes: Ghostty's shortcuts ran before a surface had a key: Shift+Home and Shift+End scrolled the terminal, Ctrl+Shift+arrows switched tabs, and on macOS Cmd+Left became the 0x01 its binding writes. Now the surface holding the keyboard is offered a key that types no text first, named from the key event with Super as Meta (hotty_host_key_pressed). A key it doesn't use goes on to the shortcuts, and is then offered as bytes. hottyterm keeps the keys of a key sequence or a key table, and keys bound to opening, closing or picking windows and splits, quitting and the config. On macOS such a key's binding isn't consumed, so the menu doesn't take it first (HOTTY SPEC §10.4).

Why we carry it: text fields and key bindings inside HOTTY documents.

For upstream: no. It routes keys to HOTTY surfaces.

No tabs: windows and full screen are the window manager's

Upstream fit: no · patch 0022

What it changes: the default keys for tabs (new, close, previous and next, by number, move, the overview), for quitting, for closing windows and for full screen are gone. A new tab, from a binding, a menu or +new-tab, opens a new window. On GTK the menus lose their tab items, the header bar's button is New Window and the tab overview button is hidden. On macOS windows never group as tabs, whatever the system's tab setting, and the File menu loses New Tab and Close Tab.

Why we carry it: in hottyterm, tabs are plx's, and windows and full screen are the window manager's (the maintainer, 2026-10-10).

For upstream: no. It is a product choice of ours, the opposite of Ghostty's.

A surface's changed rectangles go straight into its image

Upstream fit: no · patch 0023

What it changes: hotty.zig sent each rectangle of a surface that changed through a kitty a=f command: converted to straight alpha in a new buffer, copied into a loading image, then composed into the image, with two more copies and allocations a rectangle. It is now converted from the frame into the image's pixels in place, and the region is logged for the renderer as a frame edit logs it. An image that can't take that (animated, not RGBA) is replaced whole. The conversion copies a row at once and fixes only translucent pixels, eight at a time. On a 10 Hz dashboard, delivering a frame fell from about 4.8M instructions to 1.9M. Only hotty.zig changes.

Why we carry it: delivering surface frames is hottyterm's busiest path.

For upstream: no. It is hotty.zig's own path.

The macOS menu sees keys as key-remap makes them

Upstream fit: maybe · patch 0024

What it changes: with key-remap swapping Command and Control (super=ctrl, ctrl=super), AppKit's menu still matched keys as they came in: the key that reaches macOS as Command hit Copy, Select All, Hide and Minimize with C, A, H and M, instead of reaching the terminal as Control. The menu now sees each key as remapped. A key that becomes no Command key is the terminal's; one that becomes a Command key goes to the menu as that. ghostty_surface_key_remap_mods gives Swift the remapped modifiers. The same patch drops the Mac's default keys for quit, close and full screen.

Why we carry it: hottyterm swaps Command and Control on the Mac, as it swaps Option and Alt (the maintainer, 2026-10-10).

For upstream: maybe. The menu fix is a real bug for anyone who swaps Command and Control with key-remap, but it shares the patch with our removal of the Mac's default keys, so it would need splitting first.

Metal writes a kitty frame edit's rectangles with a blit

Upstream fit: yes · patch 0025

What it changes: region updates of kitty images were OpenGL only, because Metal's replaceRegion writes from the CPU at once while frames in flight may still be drawing with the texture. Each backend now has replaceTextureRegions. OpenGL writes the rectangles as before. Metal encodes a blit from a staging buffer per rectangle into a command buffer of its own, committed before the frame's, so the queue and its hazard tracking order the write after the earlier frames' reads. 50 lines in Metal.zig, 14 in OpenGL.zig, 2 in renderer/image.zig.

Why we carry it: the same surface frames, on macOS.

For upstream: yes, together with "A kitty frame edit uploads only the rectangle it changed".

A click on a surface carries the platform's click count

Upstream fit: no · patch 0026

What it changes: GTK's click gesture counts presses by the user's double-click time and distance (n_press), which Ghostty ignored. The count now goes to hotty-blitz with the next press on a surface, which keeps a run of clicks to one element. Where the platform gives no count, as on macOS today, hotty.zig counts presses as Ghostty's own selection does, with click-repeat-interval and a cell's width. The capabilities report count. One upstream line, in the GTK surface.

Why we carry it: double and triple clicks in HOTTY documents select words and lines (HOTTY SPEC §9).

For upstream: no. Only HOTTY surfaces use the count.

Copy takes a surface's selection too

Upstream fit: no · patch 0027

What it changes: copy_to_clipboard takes the text selected in a surface when the terminal has no selection, and a release on a surface copies it as copy-on-select says, into PRIMARY by default. There is one selection: a press on a surface drops the terminal's, and a press on the cells drops the surfaces'. Ctrl+c and Super+c, bound as performable, copy only while something is selected, so Ctrl+c still interrupts otherwise (HOTTY SPEC §11).

Why we carry it: text selected in a HOTTY document copies as in a browser.

For upstream: no. It adds HOTTY surfaces to copying.

A surface's text field cuts, pastes and undoes

Upstream fit: no · patch 0028

What it changes: a text field's cut puts its text on the clipboard, and its paste reads the clipboard through paste_from_clipboard after the key, once the renderer lock is released. While a field has the keyboard, completeClipboardPaste types what is pasted into it, the terminal's own paste keys included, instead of writing it to the pty. Undo is the field's own, in hotty-blitz (HOTTY SPEC §10.2, §11).

Why we carry it: HOTTY text fields edit as a browser's do.

For upstream: no. It serves HOTTY text fields.

Copying from a surface follows its keymap

Upstream fit: no · patch 0029

What it changes: a selection in a surface's document is offered every key first (hotty_host_copy_key), before the surface holding the keyboard and the shortcuts. Its keymap copies with Ctrl+c and Cmd+c by default, and a program that binds one of them hears it while text is selected. A text field copies its selection by its own keymap. Ctrl+c and Super+c, which hottyterm binds to copying the cells, then copy nothing from a surface; Ctrl+Shift+c and the menu copy a surface's selection whatever the keymaps say (HOTTY SPEC §10.2, §11).

Why we carry it: the program decides what its copy keys do.

For upstream: no. It serves HOTTY keymaps.

Key sequences, edit commands and which-key menus

Upstream fit: no · patch 0030

What it changes: Mod is Command on macOS and Control elsewhere, and two new settings, hotty-leader and hotty-key-timeout, set the leader and the timeout of key sequences. A key of an edit command the program took (a=edit, per screen, dropped on leaving the alternate screen and on RIS) reaches it as a command event: a copy with a selection on the cells copies that instead, and a paste reads the clipboard first. The which-key menu, of a sequence that waits or the program's own, shows at the bottom right of the screen (HOTTY SPEC §10.4 to §10.6). Upstream lines: 13 in Config.zig, 1 in Surface.zig.

Why we carry it: HOTTY programs bind keys without knowing the platform's.

For upstream: no. It is part of HOTTY.

Keys wait behind a paste, and a screen switch drops its commands

Upstream fit: no · patch 0031

What it changes: keys pressed while a paste command waits for the clipboard wait too, and reach the program after it, in order. A paste whose clipboard hasn't come in 2 s goes without text. The stream handler syncs the screen at each switch, so leaving the alternate screen and coming back within one frame still drops the commands taken there (HOTTY SPEC §10.6).

Why we carry it: plx found both, as its panes paste and switch screens.

For upstream: no. It serves HOTTY edit commands.


neuroplast.io · Modding · github.com/neuroplastio

Starting the terminal…