# Ghostty

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

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0002-hottyterm-a-relocatable-rpath-for-packaged-Linux-bui.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0003-hottyterm-surface-image-ids-never-collide.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0004-hottyterm-the-pointer-passes-through-what-a-surface-.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0005-hottyterm-animated-images-play.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0006-hottyterm-fit-reaches-the-program.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0007-hottyterm-surfaces-fetch-what-hotty-net-allows.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0008-hottyterm-hover-reaches-the-program-and-leaving-the-.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0009-hottyterm-a-document-that-scrolls-takes-the-wheel-fi.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0010-hottyterm-a-key-s-release-goes-where-its-press-went.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0011-hottyterm-a-plain-click-opens-a-link-hotty-link-clic.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0012-hottyterm-the-click-that-focuses-a-terminal-clicks-t.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0013-hottyterm-only-the-loop-s-thread-sets-the-frame-time.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0014-gtk-a-touch-drag-scrolls.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0015-hottyterm-a-surface-names-a-key-from-the-bytes-the-p.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0016-gtk-a-touch-taps-and-scrolls-where-the-finger-touche.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0017-hottyterm-a-touch-drags-what-opts-out-of-panning-SPE.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0018-hottyterm-a-zoom-is-a-browser-s-zoom-SPEC-5.3.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0019-termio-set-the-pty-size-after-the-grid-s-under-the-r.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0020-renderer-a-kitty-frame-edit-uploads-only-the-rectang.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0021-hottyterm-a-surface-is-offered-a-key-as-pressed-firs.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0022-hottyterm-no-tabs-and-windows-and-full-screen-are-th.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0023-hottyterm-a-frame-s-rectangles-go-straight-into-the-.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0024-hottyterm-the-macOS-menu-sees-keys-as-key-remap-make.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0025-renderer-Metal-writes-a-kitty-frame-edit-s-rectangle.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0026-hottyterm-a-HOTTY-click-carries-the-platform-s-count.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0027-hottyterm-Control-c-and-Command-c-copy-a-surface-s-s.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0028-hottyterm-a-text-field-cuts-pastes-and-undoes-SPEC-1.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0029-hottyterm-copying-follows-the-keymap-SPEC-10.2-11.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0030-hottyterm-key-sequences-edit-commands-and-menus-SPEC.patch)

**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](https://github.com/neuroplastio/hottyterm/blob/8da4b7f60c7c3ca3f6ade1a8217dc19951447ac7/mods/ghostty/0031-hottyterm-keys-wait-behind-a-paste-command-and-a-scr.patch)

**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](https://neuroplast.io/) · [Modding](https://neuroplast.io/modding) · [github.com/neuroplastio](https://github.com/neuroplastio)
