Ghostty
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:
- Programs draw HTML surfaces over the cells (HOTTY) — no
- A packaged Linux build finds its library next to bin/ — no
- HOTTY surfaces keep their images through many resizes — no
- The pointer reaches the cells through a surface's empty parts — no
- Animated GIFs, APNGs and WebPs in a surface play — no
- A program hears how many rows its surface's document needs — no
- A setting, hotty-net, says what a surface may fetch — no
- Hover reaches the program, and leaving the window ends it — no
- A scrolling surface takes the wheel before the terminal — no
- A key's release goes where its press went — no
- A plain click opens a link — maybe
- The click that focuses a terminal clicks too — maybe
- Only the IO loop's thread arms the surfaces' frame timer — no
- A touch drag scrolls (GTK) — yes
- A surface's text field reads keys as the program would — no
- A touch taps and scrolls where the finger is (GTK) — yes
- A touch drags what a surface keeps from panning — no
- Zooming a surface is a browser's zoom — no
- A resize no longer races a full-screen app's redraw — yes
- A kitty frame edit uploads only the rectangle it changed (OpenGL) — yes
- A surface is offered a key before the shortcuts — no
- No tabs: windows and full screen are the window manager's — no
- A surface's changed rectangles go straight into its image — no
- The macOS menu sees keys as key-remap makes them — maybe
- Metal writes a kitty frame edit's rectangles with a blit — yes
- A click on a surface carries the platform's click count — no
- Copy takes a surface's selection too — no
- A surface's text field cuts, pastes and undoes — no
- Copying from a surface follows its keymap — no
- Key sequences, edit commands and which-key menus — no
- 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.
A plain click opens a link
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.