`setSelection` captured the previous selection, then called
`Screen.select` (which deinits the previous selection's tracked pins),
then compared the new selection against the now-freed previous pin via
`sel.eql(prev)`. That read freed pin memory (use-after-free).
The comparison was a copy-on-select optimization ("only re-copy if the
selection changed"). Remove it rather than repair it because:
- It never fired correctly. It compared against freed memory, so the
shipped behavior was already "always copy".
- It can't be repaired by copying `prev`'s pin before `Screen.select`.
That fixes the use-after-free but not the logic: the call sites (e.g.
mouse drag release) pass a selection equal to the one already set, so a
working `eql` skip would suppress the very copy those sites exist to
perform. A correct optimization would have to compare against the
last-copied selection (before the mouse event mutated the live one),
which would require extra state.
- It isn't worth tracking that additional state. The copy runs once per
selection gesture (mouse up, double-click), which isn't in a hot path,
so skipping a redundant re-copy only saves a single clipboard write.
Removing the skip eliminates the use-after-free and keeps the behavior
consistent with what we've already been doing.
---
_AI Disclosure_: Claude Opus 4.8 found this in a review while I was
working on adjacent code.
setSelection captured the previous selection, then called Screen.select
(which deinits the previous selection's tracked pins), then compared the
new selection against the now-freed previous pin via `sel.eql(prev)`.
That read freed pin memory (use-after-free).
The comparison was a copy-on-select optimization ("only re-copy if the
selection changed"). Remove it rather than repair it because:
- It never fired correctly. It compared against freed memory, so the
shipped behavior was already "always copy".
- It can't be repaired by copying `prev`'s pin before Screen.select.
That fixes the use-after-free but not the logic: the call sites (e.g.
mouse drag release) pass a selection equal to the one already set, so
a working `eql` skip would suppress the very copy those sites exist to
perform. A correct optimization would have to compare against the
last-copied selection (before the mouse event mutated the live one),
which would require extra state.
- It isn't worth tracking that additional state. The copy runs once per
selection gesture (mouse up, double-click), which isn't in a hot path,
so skipping a redundant re-copy only saves a single clipboard write.
Removing the skip eliminates the use-after-free and keeps the behavior
consistent with what we've already been doing.
**Important: this DOES NOT hook up the glyph protocol to Ghostty or
libghostty. Its just the parser.**
This adds the core parse/encode for the still in-development and
experimental terminal glyph protocol:
https://github.com/raphamorim/rio/pull/1542
The only cross-cutting change necessary was changing the APC
identification logic which previously only looked at a single byte to
support multi-byte identifiers since the glyph protocol uses `25a1`.
For DoS protection, the default limits any glyph-related APC command
size to 1 megabyte.
> [!WARNING]
>
> Since this protocol is still in development and discussion, there is
no promise the implementation will stay within Ghostty or that any of
the APIs exposed by this will remain stable. We're just getting ahead of
it.
This adds the core parse/encode for the still in-development and experimental
terminal glyph protocol: https://github.com/raphamorim/rio/pull/1542
Up to version 1.9.
The only cross-cutting change necessary was changing the APC
identification logic which previously only looked at a single byte to
support multi-byte identifiers since the glyph protocol uses `25a1`.
fixes#12873
comment/docs only change:
switched space and tab in default value of `selection-word-chars` so
there is no space at the value boundary
needed because markdown trims spaces at the beginning & end of a code
snippet
fixes#12873
comment/docs only change:
switched space and tab in default value of `selection-word-chars`
so there is no space at the value boundary
needed because markdown trims spaces at the beginning & end
of a code snippet
Fixes#12783 where opening the context menu (with right click) inside
the quick-terminal will hide the quick-terminal if autohide is enabled.
The cause of this issue is the quick-terminal window becoming inactive
and immediately active again when you open the context-menu. When the
window becomes inactive, the autohide feature hides the quick-terminal.
The temporary focus loss in GTK is triggered by GDK focus change events,
which probably originate from the windowing backend treating the context
menu as its own window. Whereas in GTK the context menu is not a
separate window but instead part of the widget tree of the window it was
opened from, so even when the context menu has focus that window is
still the active one in GTK.
As a fix `Window.propIsActive`, which implements the autohide logic,
will now do its work from a timeout callback, since there is probably no
reliable way to distinguish a temporary focus loss from a real one from
inside GTK and I'm not sure we can make any assumptions about the timing
of things happening in the windowing backend. A 100ms delay should be
long enough for the focus state to settle while still hiding the
quick-terminal quickly.
I reproduced the bug and verified the fix on Wayland with both Hyprland
and KDE. Temporary focus loss happens on X11+KDE as well, although it
doesn't matter there because there is no quick-terminal.
### AI Disclosure
No AI was used, code and comments were written by myself.
Add a render-state row-cells getter that encodes the current cell's full
grapheme cluster directly as UTF-8 into a caller-provided GhosttyBuffer.
The getter writes the base codepoint first, followed by any extra
grapheme codepoints, and follows the existing buffer-writer convention
where len is bytes written on success or required capacity on
GHOSTTY_OUT_OF_SPACE.
Previously C consumers could query grapheme codepoints, but bindings
that needed UTF-8 text had to reconstruct and encode the cluster
themselves. That duplicated terminal internals in downstream bindings
and made users pay for awkward cross-language struct handling. By owning
the UTF-8/grapheme behavior in libghostty, bindings can use one stable C
API and optionally wrap it with small binding-local helpers.
Add a render-state row-cells getter that encodes the current cell's
full grapheme cluster directly as UTF-8 into a caller-provided
GhosttyBuffer. The getter writes the base codepoint first, followed by
any extra grapheme codepoints, and follows the existing buffer-writer
convention where len is bytes written on success or required capacity
on GHOSTTY_OUT_OF_SPACE.
Previously C consumers could query grapheme codepoints, but bindings
that needed UTF-8 text had to reconstruct and encode the cluster
themselves. That duplicated terminal internals in downstream bindings
and made users pay for awkward cross-language struct handling. By
owning the UTF-8/grapheme behavior in libghostty, bindings can use one
stable C API and optionally wrap it with small binding-local helpers.
Add a render row-cells data key for querying whether the current cell
has explicit styling. This lets consumers avoid fetching a raw cell or
full style snapshot when all they need is the cell's HasStyling bit.
The new key is appended to the existing enum for ABI safety and is
served by the existing row-cells getter path. Existing data keys and
function exports are unchanged.
This was identified as an allocation hot-spot in Go renderers.
Add a render row-cells data key for querying whether the current cell has
explicit styling. This lets consumers avoid fetching a raw cell or full style
snapshot when all they need is the cell's HasStyling bit.
The new key is appended to the existing enum for ABI safety and is served by
the existing row-cells getter path. Existing data keys and function exports are
unchanged.
Expose whether the terminal viewport is currently pinned to the active
area through the libghostty-vt terminal data API. Previously embedders
could only infer this from scrollbar geometry, which was indirect and
could require the more expensive scrollbar calculation.
The new GHOSTTY_TERMINAL_DATA_VIEWPORT_ACTIVE value returns the exact
PageList viewport state as a bool. The scroll viewport test now verifies
the value while moving between the active area and scrollback.
Expose whether the terminal viewport is currently pinned to the active
area through the libghostty-vt terminal data API. Previously embedders
could only infer this from scrollbar geometry, which was indirect and
could require the more expensive scrollbar calculation.
The new GHOSTTY_TERMINAL_DATA_VIEWPORT_ACTIVE value returns the exact
PageList viewport state as a bool. The scroll viewport test now verifies
the value while moving between the active area and scrollback.
Close#12825
Skip the initial emissions from the focused surface appearance
publishers after a tab focus change. The focused surface is already
synced immediately, so the initial Combine values only repeat the same
titlebar and background updates. Subsequent derived config and OSC
background changes still resync the window appearance.
https://github.com/user-attachments/assets/f229fb95-4b4c-4040-85ac-0acfcc54ca82
Assigned to Codex GPT 5.5(medium)
PS: Sry for I don't write zig and let AI write this.
This change primarily focused on a revised +ssh-cache user interface,
but it also reworks a bunch of the internals.
The primary CLI improvement is support for positional arguments and a
consistent list output format that includes both the ISO-formatted
timestamp and relative age.
ghostty +ssh-cache # List all cached destinations
ghostty +ssh-cache user@example.com # Show that destination
ghostty +ssh-cache example.com # Show all users on that host
ghostty +ssh-cache --add=user@example.com # Manually add a destination
ghostty +ssh-cache --remove=user@example.com # Remove a destination
ghostty +ssh-cache --prune=30d # Remove entries older than 30 days
ghostty +ssh-cache --clear # Clear entire cache
Notable, we now support a --prune operation that replaces the previous
--expire-days flag that was never actually hooked up to anything (!!).
--prune also supports a wider range of Duration-based values.
We're also much more consistent with error codes: 0=success, 1=failure,
2=usage.
While working on those changes, I also reworked the cache internals,
particularly the code around timestamp handling and errors. For example,
I dropped the explicit error sets because they were growing unwieldy,
and in practice we only matched on a subset of those errors.
Lastly, overall test coverage should be much improved, especially around
the time- and allocation-related operations.
---
*AI Disclosure:* I made a lot of iterative, AI-assisted (Claude Opus
4.7) correctness passes over this work. It was particularly helpful in
tracing through the various failure modes, and it wrote those unit tests
in the process.
## Summary
`SurfaceView` caches the background color set by OSC 11 in
`backgroundColor`. `TerminalWindow.preferredBackgroundColor` consults
that cache before falling back to `derivedConfig.backgroundColor`, so
once OSC 11 has fired the cached value masks any later config change.
After a light/dark theme auto-switch (e.g. `theme =
light:my-light,dark:my-dark`) this leaves the window chrome on the
previous theme's color until the application next emits OSC 11.
In `ghosttyConfigDidChange`, after updating `derivedConfig`, drop the
cache when it no longer matches the new config-derived background. A
subsequent `ghosttyColorDidChange` repopulates it as before, so
within-config OSC 11 behavior is unchanged.
## Reproduction
1. Configure `theme = light:SomeLight,dark:SomeDark` where the two
themes have visibly different background colors.
2. Open a terminal session where any application (e.g. a shell startup
script) has sent OSC 11 to set a custom background color.
3. Switch macOS appearance (System Settings → Appearance).
4. **Before**: window chrome stays the previous theme's color until the
terminal next emits OSC 11.
5. **After**: window chrome immediately updates to the new theme's
background color.
## Changes
- `SurfaceView_AppKit.swift` — one guard: if the cached
`backgroundColor` disagrees with the new
`derivedConfig.backgroundColor`, set it to `nil`.
Refactor terminal text selection into a reusable `SelectionGesture`
state machine. Most importantly, this means our click+drag logic around
selection is now fully unit tested! And we found bugs! And fixed them!
The large line increase in this diff is mainly comments + tests.
I've wanted to do this forever so we can unit test this, but I was
kicked in the butt to do it recently because reimplementing selection
logic in libghostty consumers turns out to be complex and error prone
and we have a perfectly battle tested logic machine here so why not
extract it?
Behavioral changes from main surfaced via unit testing:
- Dragging now drags by output across semantic output blocks when the
initial press was an output selection. This matches the behavior of
dragging continuing whatever the initial selection logic was.
- Selection autoscroll now stops when the click anchor is invalidated by
a screen change (e.g. primary to alt)
- Deep press (macOS force touch) now selects the word at the original
press location and consumes the active drag gesture, preventing later
movement from dragging or autoscrolling that selection. This matches
built-in macOS apps.
- Mouse release records whether the gesture moved away from the pressed
cell, so link and prompt clicks are skipped after a drag while normal
clicks still activate them.
Example usage:
```zig
var gesture: terminal.SelectionGesture = .init;
defer gesture.deinit(t);
const press_selection = try gesture.press(t, .{
.time = try std.time.Instant.now(),
.pin = press_pin,
.xpos = mouse_x,
.ypos = mouse_y,
.max_distance = cell_width,
.repeat_interval = mouse_interval,
.word_boundary_codepoints = selection_word_chars,
.behaviors = &.{ .cell, .word, .output },
});
try t.screens.active.select(press_selection);
if (gesture.drag(t, drag_event)) |drag_selection| {
try t.screens.active.select(drag_selection);
}
gesture.release(t, .{ .pin = release_pin });
```
Selection gestures now treat releases with invalidated anchors as dragged,
so a press that crosses screen boundaries cannot also activate links or
prompt clicks on release. Cell drags that create a same-cell selection also
mark the gesture as dragged, which keeps click-only actions from firing
after a threshold-crossing drag.
Autoscroll now resolves the drag pin after moving the viewport instead of
reusing the pin from before the scroll. This keeps the selection aligned
with the row currently under the pointer. The inspector also validates the
tracked click pin before displaying it so stale pins from inactive screens
are ignored.
Close#12825
Skip the initial emissions from the focused surface appearance publishers after a tab focus change. The focused surface is already synced immediately, so the initial Combine values only repeat the same titlebar and background updates. Subsequent derived config and OSC background changes still resync the window appearance.
SurfaceView caches the background color set by OSC 11 in
backgroundColor. TerminalWindow.preferredBackgroundColor consults
that cache before falling back to derivedConfig.backgroundColor,
so once OSC 11 has fired the cached value masks any later config
change. After a light/dark theme auto-switch this leaves the
window chrome on the previous theme's color until the application
next emits OSC 11.
In ghosttyConfigDidChange, after updating derivedConfig, drop the
cache when it no longer matches the new config-derived background.
A subsequent ghosttyColorDidChange repopulates it as before, so
within-config OSC 11 behavior is unchanged.
## Problem
Every audio bell calls `gtk.MediaFile.newForFilename`, which spins up a
full GStreamer pipeline. The GTK4 GStreamer backend's GL sink starts
`gstglcontext`/`gldisplay-event` threads that are **never joined on
teardown**, so allocating a fresh `MediaFile` per ring leaks a pipeline
and ~4 threads on every bell. The old `notify::ended -> unref` handler
discarded the pipeline but did not (and could not) join those threads.
A long-running instance accumulated **705 threads over ~4h** of normal
use.
## Fix
Cache one `MediaFile` per surface (`priv.bell_media`), rebuilt only when
`bell-audio-path` changes and unref'd on `dispose`. Each bell now
replays the same pipeline via `seek(0)` + `play()` instead of creating a
new one. `seek(0)` is required so an ended stream plays again (cf.
#8957).
## Verification
Confirmed on a real running instance with the fix: GStreamer's global
element counter only ever reached `oggdemux4` over an hour of use (one
pipeline per bell-ringing surface, reused for every subsequent bell) and
the process thread count stayed flat — versus the per-bell growth
before.
## Commits
1. **The fix** — reuse one MediaFile per surface.
2. **Unit regression test** — guards the `bellMediaFile` reuse contract
(same path → same object, changed path → rebuild). Runs in the existing
`test-gtk` CI job; needs no display.
3. **End-to-end CI job** *(kept separate so it can be dropped
independently)* — `test/bell-leak.sh` + a `test-gtk-bell-leak` workflow
job that runs ghostty headless (Xvfb + software GL), rings 120 bells,
and fails if the thread count grows per-bell. It's heavier and more
environment-sensitive (needs Xvfb/Mesa/GStreamer on the runner), so it's
isolated for easy review/removal.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Adds a bell-leak-check-gnome NixOS test (nix/tests.nix) that launches
Ghostty under GNOME on Wayland, rings 100 bells in the window, and fails
if the GUI process thread count grows per-bell — the end-to-end
signature of the GStreamer pipeline leak fixed in this branch. Verified
locally: growth of ~1 thread over 100 bells, vs ~+400 pre-fix.
Replaces the earlier Xvfb shell script + workflow job: per review, X11
support in GNOME is going away, and this belongs as a Nix check
alongside the other *-gnome tests rather than a standalone script.
The VM has no GPU, so it renders via llvmpipe; the test gives the guest
enough cores/RAM for software GL and tolerates the +new-window D-Bus
activation exceeding its client-side timeout (the window still comes up)
by waiting for the window rather than hard-failing on the call.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Guards the contract that prevents the bell thread leak: bellMediaFile
must return the same cached MediaFile for an unchanged path and only
rebuild when the path changes. A revert to per-bell allocation (the
leak) would fail this. Runs in the existing test-gtk CI job; needs no
display or playback since the path bookkeeping is all that's asserted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Each audio bell called gtk.MediaFile.newForFilename, which spins up a
full GStreamer pipeline. The GTK4 GStreamer backend's GL sink starts
gstglcontext/gldisplay-event threads that are never joined on teardown,
so allocating a MediaFile per ring leaked a pipeline and ~4 threads on
every bell. A long-running instance accumulated 705 threads over ~4h of
normal use.
Cache one MediaFile per surface (priv.bell_media), rebuilt only when
bell-audio-path changes and unref'd on dispose. Each bell now replays
the same pipeline via seek(0)+play() instead of creating a new one. The
notify::ended -> unref handler is removed: it was what discarded (and
leaked) a pipeline per ring. seek(0) is required so an ended stream
plays again (#8957).
Verified on a real instance: GStreamer's global element counter reached
only oggdemux4 over an hour of use (one pipeline per bell-ringing
surface, reused) and thread count stayed flat, versus per-bell growth
before.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This change primarily focused on a revised +ssh-cache user interface,
but it also reworks a bunch of the internals.
The primary CLI improvement is support for positional arguments and a
consistent list output format that includes both the ISO-formatted
timestamp and relative age.
ghostty +ssh-cache # List all cached destinations
ghostty +ssh-cache user@example.com # Show that destination
ghostty +ssh-cache example.com # Show all users on that host
ghostty +ssh-cache --add=user@example.com # Manually add a destination
ghostty +ssh-cache --remove=user@example.com # Remove a destination
ghostty +ssh-cache --prune=30d # Remove entries older than 30 days
ghostty +ssh-cache --clear # Clear entire cache
Notable, we now support a --prune operation that replaces the previous
--expire-days flag that was never actually hooked up to anything (!!).
--prune also supports a wider range of Duration-based values.
We're also much more consistent with error codes: 0=success, 1=failure,
2=usage.
While working on those changes, I also reworked the cache internals,
particularly the code around timestamp handling and errors. For example,
I dropped the explicit error sets because they were growing unwieldy,
and in practice we only matched on a subset of those errors.
Lastly, overall test coverage should be much improved, especially around
the time- and allocation-related operations.
readEntries had two memory bugs on the allocation failure path, both
only reachable under OOM:
- The map itself was never freed if we ran into an allocation failure
- The unconditional `errdefer`s for the dupe'd hostname and terminfo
values could double-free if there was a later allocation failure.
This change restructures this function so that these values are dupe'd
up-front, and then their ownership is tracked using optionals that can
be null'ed out once their ownership is transferred into the map.
Both of these cases are now covered by unit tests.
readEntries had two memory bugs on the allocation failure path, both
only reachable under OOM:
- The map itself was never freed if we ran into an allocation failure
- The unconditional `errdefer`s for the dupe'd hostname and terminfo
values could double-free if there was a later allocation failure.
This change restructures this function so that these values are dupe'd
up-front, and then their ownership is tracked using optionals that can
be null'ed out once their ownership is transferred into the map.
Both of these cases are now covered by unit tests.
Render-state rows already expose their selected range, but cell-oriented
C API consumers had to fetch that row range separately and duplicate the
containment check while rendering.
Add a SELECTED row-cells data kind that carries the row selection into
the row-cells wrapper and returns whether the current cell column is in
that inclusive range. The field remains separate from cell colors and
style so selection stays an explicit render overlay policy.
For performance reasons, the span-based row getter is recommended still
but this is a convenient thing to do for cell-oriented folks.
Render-state rows already expose their selected range, but
cell-oriented C API consumers had to fetch that row range separately
and duplicate the containment check while rendering.
Add a SELECTED row-cells data kind that carries the row selection into
the row-cells wrapper and returns whether the current cell column is in
that inclusive range. The field remains separate from cell colors and
style so selection stays an explicit render overlay policy.
For performance reasons, the span-based row getter is recommended still
but this is a convenient thing to do for cell-oriented folks.
Depending on your system config, `xdg-open` may stay open for extended
periods, and potentially log more than the 50kb of output that we were
previously able to deal with. This changes `open()` so that output on
`stdout` is just directly ignored. Any output from `stderr` is immedialy
logged rather than collected for later logging.
Note that this will generally occur if your system is not configured
with the DBus portals that `xdg-open` uses to open URLs rather than
launching programs like your web browser directly. This could be seen as
user misconfiguration but we should deal with it robustly anyway.
Depending on your system config, `xdg-open` may stay open for extended
periods, and potentially log more than the 50kb of output that we were
previously able to deal with. This changes `open()` so that output on
`stdout` is just directly ignored. Any output from `stderr` is immedialy
logged rather than collected for later logging.
Note that this will generally occur if your system is not configured
with the DBus portals that `xdg-open` uses to open URLs rather than
launching programs like your web browser directly. This could be seen as
user misconfiguration but we should deal with it robustly anyway.
Tracked grid references previously held a raw terminal wrapper pointer
and were required to be freed before the terminal. If callers kept one
past terminal destruction, later tracked-ref calls could dereference
freed terminal or page-list memory before detecting that the reference
was no longer meaningful.
Track live C tracked-grid-ref handles from the terminal wrapper and
detach them before tearing down terminal storage. Detached refs now
report no value through the tracked-ref APIs and can still be freed by
the caller. Update the C API docs to describe this lifetime behavior and
add a regression test for using a tracked ref after terminal free.
This introduces some overhead but tracked pins shouldn't be numerous and
this dramatically improves safety.
No API changes due to this (just more safety).
Adds libghostty-vt selection APIs read/write, formatting, inspecting,
and rendering selection state from C.
| Introduced type/function | Purpose |
| --- | --- |
| `GhosttyRenderStateRowSelection` | Row-local inclusive selection range
returned by render row queries. |
| `GhosttyTerminalSelectWordOptions` | Options for deriving a word
selection from a grid ref. |
| `GhosttyTerminalSelectWordBetweenOptions` | Options for finding the
nearest selectable word between two refs. |
| `GhosttyTerminalSelectLineOptions` | Options for deriving line
selections, including semantic prompt boundaries. |
| `GhosttyTerminalSelectionFormatOptions` | Options for formatting the
active or caller-provided selection. |
| `GhosttySelectionOrder` | Describes endpoint ordering, including
rectangular mirrored orders. |
| `GhosttySelectionAdjust` | Operations for moving a selection endpoint.
|
| `ghostty_terminal_select_word` | Derive a word selection snapshot. |
| `ghostty_terminal_select_word_between` | Derive the nearest word
selection between two refs. |
| `ghostty_terminal_select_line` | Derive a line selection snapshot. |
| `ghostty_terminal_select_all` | Derive a selection covering all
selectable content. |
| `ghostty_terminal_select_output` | Derive a semantic command-output
selection. |
| `ghostty_terminal_selection_format_buf` | Format a selection into a
caller-provided buffer. |
| `ghostty_terminal_selection_format_alloc` | Format a selection into an
allocated buffer. |
| `ghostty_terminal_selection_adjust` | Mutate a selection snapshot
endpoint. |
| `ghostty_terminal_selection_order` | Query selection endpoint order. |
| `ghostty_terminal_selection_ordered` | Return a selection with
normalized endpoint order. |
| `ghostty_terminal_selection_contains` | Test whether a point is inside
a selection. |
| `ghostty_terminal_selection_equal` | Compare two selection snapshots
using terminal semantics. |
Tracked grid references previously held a raw terminal wrapper pointer and
were required to be freed before the terminal. If callers kept one past
terminal destruction, later tracked-ref calls could dereference freed
terminal or page-list memory before detecting that the reference was no
longer meaningful.
Track live C tracked-grid-ref handles from the terminal wrapper and detach
them before tearing down terminal storage. Detached refs now report no
value through the tracked-ref APIs and can still be freed by the caller.
Update the C API docs to describe this lifetime behavior and add a
regression test for using a tracked ref after terminal free.
This introduces some overhead but tracked pins shouldn't be numerous
and this dramatically improves safety.
Expose a C API for checking whether a GhosttyPoint is inside a
GhosttySelection. The new terminal helper validates the selection snapshot
against the active screen, resolves the point to a grid pin, and delegates
to the internal Selection.contains implementation so C consumers get the
same linear and rectangular selection semantics as Ghostty.
Wire the symbol through the C API exports and public headers, and add a
focused test covering linear containment and rectangular selection behavior.
Expose selection endpoint ordering through the libghostty-vt C API so
embedders can safely normalize selections whose start and end refs may be
reversed. The new APIs report the current order and return a fresh
untracked selection with forward or reverse bounds.
Selection.Order now uses lib.Enum, matching the existing adjustment enum
pattern and keeping the C ABI enum generated from the same Zig source of
truth. The new functions are wired through the C API re-export and lib-vt
export paths, with coverage for mirrored rectangular selection ordering.
Clarify that GhosttySelection is a snapshot type whose endpoints are
untracked GhosttyGridRef values. The previous documentation described the
range shape but did not repeat the grid reference lifetime caveat, which
made it easy to keep selections across terminal mutations incorrectly.
Render state already tracks the selected cell range for each viewport row,
but C renderers could only get the full terminal selection. That required
consumers to map global selection pins back into row-local spans themselves.
Add row selection data to the render-state row API. Querying the new row
data returns GHOSTTY_NO_VALUE for unselected rows and writes the inclusive
start and end columns for selected rows. The render example now demonstrates
setting a selection and reading the row-local range while iterating rows.
Add terminal set/get support for the active screen selection through the
existing option and data APIs. Setting a selection copies the C snapshot
into terminal-owned tracked state, while passing NULL clears the current
selection.
Getting the selection now returns an untracked GhosttySelection snapshot
or GHOSTTY_NO_VALUE when there is no selection. The C header documents
the different lifetimes for set and get so embedders know when input and
returned grid references remain valid.
Closes https://github.com/ghostty-org/ghostty/discussions/12774
`.onKeyPress(.return)` unconditionally returns `.handled`, so when IME
is composing the return key never reaches the IME to confirm the
candidate. The search bar gets stuck.
The fix: use `.onSubmit` for the next-match navigation — it only fires
when there is no composing text. In `.onKeyPress` only intercept
shift+return (previous match), return `.ignored` otherwise.
Tested on macOS 26.5, Ghostty 1.3.1, built from source. Chinese Pinyin
input in the search bar works correctly after the fix.
Add a C API for tracked pins, known as a tracked grid ref in C.
The new API can create tracked refs from terminal points, snapshot them
back to regular grid refs for cell access, convert them to coordinates,
move them to a new point, report when their semantic location was lost,
and free the tracked pin bookkeeping. This is backed by PageList tracked
pins and exposed through the libghostty-vt export layer and headers.
Left-click mouse state stored a tracked pin with only the screen key that
owned it. If the alternate screen was removed and later recreated, the key
could match again even though the stored pin belonged to destroyed PageList
storage.
Store the screen generation alongside the left-click pin and resolve the
pin through helpers that require both the key and generation to match. This
keeps selection scrolling, link hover checks, pressure selection, and drag
selection from dereferencing stale tracked pins after screen teardown.
Add a C API for tracked pins, known as a tracked grid ref in C.
The new API can create tracked refs from terminal points, snapshot them
back to regular grid refs for cell access, convert them to coordinates,
move them to a new point, report when their semantic location was lost,
and free the tracked pin bookkeeping. This is backed by PageList tracked
pins and exposed through the libghostty-vt export layer and headers.
Use onSubmit for the plain Enter → next-match behavior, which respects
IME composition state. Keep onKeyPress only for Shift+Enter (previous
match), returning .ignored for plain Enter so the IME can process it.
Fixes 2 bugs
1. After dragging a non-focused surface from window A to window B
**quickly without making B the key window**, the focused surface in
window A is not receiving `keyDown` events.
https://github.com/user-attachments/assets/a8861c0a-9300-470d-bf7e-0f32a9ab2cd1
2. #12343 After dragging a surface from tab A to tab B within the same
window, the dragged surface is not rendering input correctly.
> The reason the thread is stuck is because the surface's occlusion
state is set to invisible after target tab's activate while dragging,
since the dragged surface is still in previous tree before dropping, and
after dropping the occlusion state of this surface is not updated to
visible, which causing the surface is accepting input but not rendering.
https://github.com/user-attachments/assets/d67f5dba-8609-4f67-a956-921982faf796
Add a drop-in `ssh` wrapper that sets up the remote environment for
Ghostty. Anything not consumed as one of our own flags is forwarded to
the real, wrapped `ssh` binary. It can be used directly (`ghostty +ssh
user@host`), aliased (`alias ssh='ghostty +ssh --'`), or invoked through
Ghostty's shell integration.
Before exec'ing ssh, `+ssh`:
- Forwards Ghostty environment to the remote (`--forward-env`): sets
TERM=xterm-256color and requests SendEnv forwarding of COLORTERM,
TERM_PROGRAM, and TERM_PROGRAM_VERSION.
- Installs Ghostty's terminfo on the remote (`--terminfo`), informed by
our existing `ssh-cache` system and using our internal xterm-ghostty
terminfo representation.
A third flag, `--cache`, controls cache use; `--cache=false` bypasses
both read and write, which is useful for scripting and for debugging
install failures without polluting the cache.
For shell integration, this replaces the per-shell logic (which made up
roughly a third of our shell integration scripts) with a simple wrapper
function that translates GHOSTTY_SHELL_FEATURES into a `ghostty +ssh`
command line.
This PR fixes an issue where reflowing could leave the active cursor
attached to a clipped trailing blank cell instead of following the
current write position.
This fixes a bug where the variation selectors (VS15 & VS16) were
checked against the first codepoint in a cell instead of the previous
codepoint in the cell's grapheme cluster, causing them to be dropped if
that first codepoint was not a valid base.
Fixes the issue described in #12516.
### What
- Inject an `OSPasteboard` into `SearchState`
- Add `OSPasteboard` extension to normalize working with strings between
UIPasteboard/NSPasteboard
- Add `BackportSelectionTextField` which supports text selection for
MacOS 15/iOS 18 and up.
- Read from the pasteboard when the overlay opens and when the app
becomes active
- Write to the pasteboard when the search needle changes
- Annotate `SearchState` as MainActor. `NSPasteboard` isn't thread safe,
and since `SearchState` is already accessed from the main thread,
MainActor enforces our writes be thread safe
- Add SearchState unit tests
### Why
Consistent with other macOS apps, the Find bar's search needle should
persist when re-opened and should sync to the Find bar in other apps.
For example, see Xcode, Notes, Terminal, and Safari.
https://github.com/user-attachments/assets/b6a55a4a-a52c-45bc-ac38-c9df452c11cb
Inspired by `Terminal.app` which I think is a nice feature.
First two commits contains some changes in `BaseTerminalController` so
that I can use swift concurrency to review those windows in chain more
easily.
https://github.com/user-attachments/assets/41d92432-4ae0-499e-961a-fc247602f3d7
Works with tabs as well, i forgot to record that.
As discussed in #12745, there has been an outstanding plan to make
rendering behavior for non-focused surfaces consistent across platforms.
This PR does that for Linux/GTK using the same patterns as OSX.
The change in `src/apprt/gtk/class/surface.zig` piggybacks on the
existing `glareaMap` / `glareaUnmap` callbacks (added by `e59e27f8b`) by
also calling a new `updateOcclusion(bool)` helper. If you don't like the
helper, or want the helper lifted up further and used on other paths,
let me know and I can revise.
The changes in `src/renderer/Thread.zig` bail on `renderCallback` when
not visible and then block on `drainMailbox` to complete the "catch up"
before trying to draw again.
I want to note that this is more granular than the original #1512, which
was just focused on the top level window state. I can look at that as
well if you want, but given the complexity around how
`XDG_TOPLEVEL_STATE_SUSPENDED` event is fired, I would want to make sure
we discussed things like transparency and single-instance properly first
(e.g., do we render when behind another transparent window).
## Testing
Here's a summary of what I tested:
Tested on Linux/GTK (Ubuntu 26.04, GTK 4.22.2, libadwaita 1.9.0,
Wayland), built `ReleaseFast`. The patched binary has been daily-driven
for ~24 hours as my primary terminal.
| Test | Workload | Result |
|---|---|---|
| Daily drive | byobu × multiple SSH sessions, Claude Code and Codex
producing sustained streaming output, `top` / `btop` redrawing on 1 s
intervals, frequent tab switching | No observed issues over ~24 hours of
mixed use |
| Bell on hidden tab | `sleep 5 && printf '\a'` in background tab | Bell
+ needs-attention indicator both fire; confirms IO-thread → GTK-signal
path is untouched |
| Search highlight survives hide/show | Open search w/ matches
highlighted in tab B → switch to tab A for ~10 s → switch back |
Highlights restored instantly with no stale state; confirms
deferred-replay path (`updateFrame` on `.visible → true`) works
correctly |
| Selection persistence | Select text in tab B → switch tabs → switch
back | Selection preserved exactly |
| Lifecycle (close-all) | Opened 8 surfaces, closed them one at a time,
then process exit + systemd restart | Zero `glib-CRITICAL`, zero `error
in occlusion callback ...` warnings, clean teardown per `journalctl
--user -u app-com.mitchellh.ghostty` |
| Per-thread CPU during workload | `pidstat -t -p <pid>` 30 s with 1
byobu surface focused, 1 background | Hidden surface's renderer thread
sits at 0.00 % every sample; focused surface's renderer shows ~1 % blips
on byobu status ticks |
## AI usage
Claude Code (Opus 4.7) helped review my patch and monitor / summarize
the jorunald log and pidstat entries.
renderCallback early-returns while !flags.visible to avoid the
cell rebuild for hidden surfaces (tab switch, minimize, etc.).
The .visible → true mailbox handler now runs updateFrame before
drawFrame so the first frame after re-show isn't stale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Calls core_surface.occlusionCallback(visible) from the existing
glareaMap/glareaUnmap handlers (added in #12698) so the renderer
thread learns when a surface is off-screen.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
## Summary
When Ghostty is installed via snap on Ubuntu, programs running inside
Ghostty (e.g. `clear`) fail with:
```
terminals database is inaccessible
```
The snap ships terminfo at `${SNAP}/share/terminfo` but the launcher
never exports `TERMINFO_DIRS`, so ncurses in child shells falls back to
the host's system database. On Ubuntu 24.04 (ncurses 6.4) the system
database predates the `xterm-ghostty` entry, so the lookup fails.
This is the same fix as the auto-closed #12303 and resolves#12304.
## Fix
Export `TERMINFO_DIRS` in `snap/local/launcher` so all child processes
can resolve the bundled entry without manual setup.
## Local build (how this PR was verified)
Remix the installed store snap by swapping `app/launcher` with the
patched one:
```sh
sudo unsquashfs -d /tmp/g \
/var/lib/snapd/snaps/ghostty_$(readlink /snap/ghostty/current).snap
sudo cp snap/local/launcher /tmp/g/app/launcher
sudo mksquashfs /tmp/g /tmp/ghostty-test.snap -comp xz -noappend
sudo snap install --dangerous --classic /tmp/ghostty-test.snap
```
Then launch `/snap/bin/ghostty` and run `clear`.
## Test plan
Verified locally on Ubuntu 24.04 / arm64.
- [x] In default `zsh` / `bash` inside Ghostty, `clear` succeeds.
- [x] `infocmp xterm-ghostty` resolves to
`/snap/ghostty/current/share/terminfo/x/xterm-ghostty`.
- [x] No manual copying of terminfo entries into `~/.terminfo/`
required.
## AI Disclosure
Claude Code was used to investigate the root cause and to draft this
single-line launcher change. The fix is identical to the proposal in the
linked discussion (#12304). I manually verified by remixing the
installed snap with the patched launcher and confirming `clear` and
`infocmp xterm-ghostty` work without manually copying terminfo entries
into `~/.terminfo/` (original workaround shared in the discussion).
Noticed this was removed in another PR, but `apple_sdk` is required to
build libghsotty for the iOS simulator, specifically for the x86 version
(see the error log
[here](https://github.com/elias8/libghostty/actions/runs/26075576793/job/76666498246)).
Figured it'd be better to include the SDK in all darwin builds for
consistency.
Add `+toggle-quick-terminal` as a first-class IPC action, following the
same pattern as `+new-window`. This provides a proper CLI command
(`ghostty +toggle-quick-terminal`) to toggle the quick terminal on a
running Ghostty instance.
Closes discussion #12618
Fixes `SplitTree.resize` not rescaling the split ratio to be relative to
the size of the split. Added a unit test for resizing a nested split.
Previously the new ratio was incorrectly calculated relative to the
entire grid. As a consequence resizing a nested split in the GTK app
would cause unexpected size changes like large jumps. E.g. in the
following recording the window has height ~1000px and the resize was
done using a keybind for `resize_split:up,10`. The change is much larger
than 10 pixels.
https://github.com/user-attachments/assets/ba375ddf-5b2f-45e4-8b12-69021ef2f8a8
Note that even with this fix, resizing by a small amount like 10 pixels
might not work at all (depending on window size and layout), because of
the same bug causing #11193 (see my PR #12698). Initially an inaccurate
split ratio will be set and eventually written back to the split tree
datastructure. That incorrect split ratio will be the same before and
after the small resize, so nothing actually changes in the UI.
The split tree implementation for the macOS app already calculates the
ratios correctly.
AI Disclosure
No AI was used, the bug was discovered and all code written by myself.
Fixes#11193 where terminal surfaces might not appear and focus might be
lost when creating multiple nested splits.
These bugs are caused by GTK initially allocating a tiny width/height to
deeply nested splits. For a split with a tiny size, the split ratio will
be set inaccurately e.g. to 1 which means that the right/bottom child of
the split is invisible. If that child is the terminal surface that
should have the focus, it will lose it. In the current implementation
the split ratio can be set at most once, which means the inaccurate
ratio never gets corrected and a surface (or an entire sub-tree of the
layout) will stay invisible.
The following explains the current implementation and bug in more
detail, it is a bit long, but I hope it will make it easier to review
this PR.
### Current Implementation
A split layout is a tree, in code represented by `datastruct/SplitTree`,
where inner nodes are splits and leafs are terminal surface. A split can
be either horizontal or vertical, and has a ratio that defines how its
space should be divided among the 2 children.
The counterpart in the GTK UI is the `apprt/gtk/class/SplitTree` widget
whose `onRebuild/buildTree` functions build a widget tree that has the
same structure as the `datastruct/SplitTree`. The widget tree consists
of a `SplitTreeSplit` widget for every split and a `Surface` widget for
every terminal surface.
A `SplitTreeSplit` widget wraps a `gtk.Paned` widget, which displays its
two children with a divider in between, either horizontally or
vertically. How much space each child gets is determined by 3
properties. `min_position` is always 0 in our case, `max_position`
corresponds to the width/height (for horizontal/vertical splits) of the
widget and `position` is where the divider should be. So `position` is
equivalent to the width/height of the left/top child and thereby also
determines the width/height of the right/bottom child. `SplitTreeSplit`
listens for changes in the 3 properties. If there is one, the
`propPosition`, `propMinPosition` or `propMaxPosition` function gets
triggered and an idle callback for the `onIdle` function is added.
We need to make sure that the widget tree and the `datastruct/SplitTree`
stay in sync.
If we e.g. create a new split or close a surface, the structure of the
split tree changes. In that case `gtk/class/SplitTree.onRebuild` will
completely rebuild the widget tree (the `Surface` widgets are actually
reused) to match the new tree structure. If we resize a split (i.e.
change the split ratio) via action/keybind, we also completely rebuild
the widget tree.
Additionally we need to make sure that for every
`SplitTreeSplit/gtk.Paned` the `position` divided by `max_position`
matches the ratio of the corresponding split node in our
`datastruct/SplitTree`. There are two ways the current implementation
keeps these ratios in sync, both are handled by the
`SplitTreeSplit.onIdle` function.
1. Initially when the widget tree is built, GTK allocates each widget a
size. Specifically it also sets the `position` and `max_position`
properties of each `gtk.Paned` widget, which will trigger the
`SplitTreeSplit.onIdle` function to run. GTK will not necessarily set
position correctly, it is the task of `onIdle` to make sure that the UI
matches the layout defined by the `datastruct/SplitTree`. `onIdle`
checks if `position/max_position` matches the ratio that the split
should have and if not calls `gtk.Paned.setPosition` to update it. This
can only happen once for each split since `onIdle` checks if the
position was set previously. The idea is that we should only ever need
to set the position once, because `gtk.Paned` will automatically keep
its current ratio whenever its size/`max-position` changes (if the
`setPosition` function has been called before). A size change can happen
e.g. because the entire window was resized or because an ancestor split
changed its split ratio.
2. The user can manually change the ratio between the two children of a
split by dragging the divider between them in the UI. When that happens
the `position` property in `gtk.Paned` changes and eventually the
`SplitTreeSplit.onIdle` function gets called. Since `setPosition` should
have already been called when the widget was initially sized, we should
fall through to the second case and write the current ratio back to the
`datastructure/SplitTree`.
The problem with `SplitTreeSplit.onIdle` is that sometimes the split
ratio cannot be set accurately given the current size of the `gtk.Paned`
widget. Because `onIdle` can only set the position/ratio once, any
previous inaccuracy can never get corrected.
For example with many nested vertical splits, GTK might initially
allocate a `gtk.Paned` widget a height of 1. It will have
`max_position=1` and `position=1`. When `onIdle` runs the current ratio
of `position/max_position = 1` is different from the desired ratio of
e.g. 0.5. But a ratio of 0.5 cannot be set, the position can only be 0
or 1 corresponding to a ratio of 0 or 1. The position will then get set
as 1 and can't be changed later. Even when the split later gets a larger
height, it will keep the ratio of 1 and the bottom child will stay
invisible. When the surface that should be focused initially becomes
invisible it loses focus and the focus will never be restored. That is
exactly what happens in the first screencast in the issue description
(#11193).
Another problem with `onIdle` is that the `setPosition` call in `onIdle`
will trigger another idle callback where the position change is
sometimes wrongly interpreted as a manual update and written back to the
tree. Also sometimes the initial ratio in a `gtk.Paned` can already be
correct, in which case position will not get set. The next manual
position update is then not detected as a manual update.
### Changes
`SplitTreeSplit.onIdle` is now able to set the split position every time
the widget is resized, an inaccurate initial ratio will be corrected. To
be able to distinguish a widget resize from a manual position update by
the user, we keep track of whether `max-position`, `position` or both
properties changed. If only `max-position` or both properties changed,
then the widget was resized. If just `position` changed it is a manual
update. This is kind of hacky but works. To verify I checked the source
code for `gtk.Paned`, see the comment in the code on `onIdle`.
`SplitTreeSplit` no longer listens to changes in `min-position`, that
should always be 0 (because we use the default resize/shrink properties
for `gtk.Paned`) and there is already an assert in `onIdle` that checks
that.
It can still happen that a surface initially gets allocated width/height
0 and loses focus. The only reliable way to detect when we can restore
focus, is to listen to the `map/unmap` signals exposed by `gtk.Widget`.
The `Surface` widget now listens to these signals on its `GlArea` child
(because that is where we want to put focus) and stores the current
state in the new `mapped` property. The `SplitTree` widget listens to
changes in that property: when surfaces become mapped, an idle callback
for the new `onRestoreFocus` function is added, which will check whether
the last focused surface is mapped and if so restore focus to it.
### Other possible solutions
Alternatively we could try to only set the split ratio once the split
has its correct final size, but I think it's hard to detect that
reliably. Or we could try to prevent the splits/surfaces from becoming
invisible in the first place by e.g. setting a minimal widget size. But
then you won't get the exactly correct layout and sometimes you do want
a surface to be tiny or invisible e.g. you can drag the divider in a
split all the way to one side.
The ideal solution would probably be to write a custom version of
`gtk.Paned` where you can provide the desired ratio on widget creation.
Then everything will be sized correctly from the start, focus will not
be lost. In terms of performance it would probably be better as well,
because right now there can be multiple rounds of resizes until every
split/surface has its correct size. I have played around with this a
bit, it is definitely possible. But you would have to implement the
divider widget, logic for layouting, handling gestures and co. That is a
bigger undertaking.
### Testing
Tested by creating/modifying deeply nested layouts, dragging split
dividers and resizing splits via keybind. Checked that ratios are
maintained when the window is resized and tested that it works with
zoom. Tested locally with hyprland and in a VM with KDE.
All the bugs described in the issue should be fixed.
### AI Disclosure
Discovered the bug and wrote all code/comments by myself. Used AI in
researching various internals of GTK.
After #1368, `command-palette-entry=` will no longer clear the entries
like the documentation says. Since i couldn't find an existing issue or
discussion about this, I assume no one is actually using it. So I put
1.4.0 here, lemme know if you want to change it to 1.3.2.
> I basically copied the `keybind` parsing code and doc.
After #1368, `command-palette-entry=` will no longer clear the entries like the documentation says. Since i couldn't find an existing issue or discussion about this, I assume no one is actually using it. So I put 1.4.0 here, lemme know if you want to change it to 1.3.2.
> I basically copied the `keybind` parsing code and doc.
The cause of these bugs is that GTK can initially allocate
a split/surface a width/height of 0 which causes it to
get unmapped and lose focus. Additionally the split ratio is
only set once but not accurately for tiny splits, which can keep
a surface invisible even when the split gets resized later.
To fix these problems the split ratio is always checked and
possibly corrected when a split gets resized. Changes in a split
ratio caused by the user dragging the divider are detected
separately using an event controller. If a surface loses focus
we restore it once the surface becomes mapped again.
The reason the thread is stuck is because the surface's occlusion state is set to invisible after target tab's activate while dragging, since the dragged surface is still in previous tree before dropping, and after dropping the occlusion state of this surface is not updated to visible, which causing the surface is accepting input but not rendering.
This fixes a bug: after dragging a non-focused surface from window A to window B **quickly without making B the key window**, the focused surface in window A is not receiving `keyDown` events.
Expose toggle-quick-terminal as a proper IPC action so it can be
triggered via 'ghostty +toggle-quick-terminal' from the command line,
instead of calling the raw D-Bus org.gtk.Actions.Activate interface.
This follows the same pattern as the existing +new-window IPC command:
- Add toggle_quick_terminal to apprt.ipc.Action enum (Zig + C ABI)
- Create apprt/gtk/ipc/toggle_quick_terminal.zig (GTK D-Bus handler)
- Route .toggle_quick_terminal in apprt/gtk/App.zig performIpc
- Register toggle-quick-terminal GAction in application.zig
- Add +toggle-quick-terminal CLI handler in cli/
- Register in cli/ghostty.zig Action enum, runMain, and options
- Add stub in apprt/embedded.zig
- Update include/ghostty.h C header enum
Usage:
ghostty +toggle-quick-terminal
Closes: #12618
Bumps [cachix/cachix-action](https://github.com/cachix/cachix-action)
from 1eb2ef646ac0255473d23a5907ad7b04ce94065c to
5f2d7c5294214f71b873db4b969586b980625e71.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/cachix/cachix-action/blob/master/RELEASE.md">cachix/cachix-action's
changelog</a>.</em></p>
<blockquote>
<h1>Release</h1>
<ol>
<li>
<p>Create and push a new tag:</p>
<pre lang="console"><code>git tag v17
git push origin v17
</code></pre>
</li>
<li>
<p>Wait for CI to pass.</p>
</li>
<li>
<p><a href="https://github.com/cachix/cachix-action/releases/new">Create
a release</a> for the new tag.</p>
</li>
<li>
<p>Move the major version tag to the latest release:</p>
<pre lang="console"><code>git tag -fa v17
git push origin v17 --force
</code></pre>
</li>
</ol>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="5f2d7c5294"><code>5f2d7c5</code></a>
fix: await main functions</li>
<li><a
href="4ee54539d7"><code>4ee5453</code></a>
rebuilt dist</li>
<li><a
href="9f82c7e332"><code>9f82c7e</code></a>
fix: ensure that the post-build hook never fails</li>
<li><a
href="a593539ec5"><code>a593539</code></a>
ci: add a workflow to auto-bump version in README</li>
<li><a
href="8d6d4b9006"><code>8d6d4b9</code></a>
docs: add release and contributing docs</li>
<li><a
href="6505427c13"><code>6505427</code></a>
Merge pull request <a
href="https://redirect.github.com/cachix/cachix-action/issues/213">#213</a>
from jleroux98/update-readme</li>
<li><a
href="5941c26199"><code>5941c26</code></a>
use regular tags</li>
<li><a
href="80a630b9fc"><code>80a630b</code></a>
update tags</li>
<li>See full diff in <a
href="1eb2ef646a...5f2d7c5294">compare
view</a></li>
</ul>
</details>
<br />
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
</details>
Same as with icelandic (#12301) we may be even fewer than them but let's
have this translated into Basque.
I also volunteer for the basque translation team.
This PR fixes an issue where reflowing could leave the active cursor
attached to a clipped trailing blank cell instead of following the
current write position.
This fixes a bug where the variation selectors (VS15 & VS16) were
checked against the first codepoint in a cell instead of the previous
codepoint in the cell's grapheme cluster, causing them to be dropped if
the first codepoint was not a valid base.
Replace the inline ssh integration with a thin wrapper that translates
GHOSTTY_SHELL_FEATURES into a `ghostty +ssh` command line. When no
ssh-* feature is enabled, the wrapper falls through to the real `ssh`
binary unchanged so nushell users without ssh integration get plain
ssh behavior.
Replace the inline ssh integration with a thin wrapper that translates
GHOSTTY_SHELL_FEATURES into a `ghostty +ssh` command line. The shell
wrapper no longer carries terminfo install, ControlMaster wiring, or
cache bookkeeping; it just maps the feature flags to flags on `+ssh`
and forwards everything else.
Add a drop-in `ssh` wrapper that sets up the remote environment for
Ghostty. Anything not consumed as one of our own flags is forwarded to
the real, wrapped `ssh` binary. It can be used directly (`ghostty +ssh
user@host`), aliased (`alias ssh='ghostty +ssh --'`), or invoked through
Ghostty's shell integration.
Before exec'ing ssh, `+ssh`:
- Forwards Ghostty environment to the remote (`--forward-env`): sets
TERM=xterm-256color and requests SendEnv forwarding of COLORTERM,
TERM_PROGRAM, and TERM_PROGRAM_VERSION.
- Installs Ghostty's terminfo on the remote (`--terminfo`), informed by
our existing `ssh-cache` system and using our internal xterm-ghostty
terminfo representation.
A third flag, `--cache`, controls cache use; `--cache=false` bypasses
both read and write, which is useful for scripting and for debugging
install failures without polluting the cache.
For shell integration, this replaces the per-shell logic (which made up
roughly a third of our shell integration scripts) with a simple wrapper
function that translates GHOSTTY_SHELL_FEATURES into a `ghostty +ssh`
command line. This commit only migrates the bash integration; the other
shells will follow separately.
This PR fixes an issue where a zero-width combining mark could attach to
the wrong cell when the preceding character was written in the final
column and the cursor had a pending wrap.
The test I added used to fail before the fix, but it passes now.
This PR fixes an issue where a zero-width combining mark could attach to
the wrong cell when the preceding character was written in the final
column and the cursor had a pending wrap.
Refs #10460
Related: #12518
When an input method commits all or part of marked text during keyDown,
AppKit returns the committed text through insertText. Treat that as text
committed by the input method instead of replaying the original key
event to the terminal.
Previously this path only handled arrow-key commits specially. A
control-key shortcut that commits preedit text could still be encoded as
the original control input after composition, such as ctrl+j becoming
LF.
Send committed preedit text as a text-only event for any key that causes
the commit. Only replay arrow navigation keys that the existing Korean
IME handling expects, and keep plain left-arrow suppressed because
AppKit already leaves the caret in place.
Before:
<img width="375" height="375" alt="before"
src="https://github.com/user-attachments/assets/1073b93f-625a-4881-8f95-67adefe9d3da"
/>
After:
<img width="375" height="375" alt="after"
src="https://github.com/user-attachments/assets/3e4be2a5-4df9-4cdd-bc95-e178ca44c7e7"
/>
AI usage: OpenAI Codex helped investigate, implement, test, and refine
this change. I reviewed and tested the resulting code.
Adds support for building libghostty-vt on macOS with Nix.
Tested on aarch64-darwin. Tests pass as well.
_Claude used to speed up debugging process. All comments, commit
messages, and final code authored by me._
Zigs build infra computes relatives paths to build-time executables that use `setCwd.`
The logic is purely lexical and doesn't take into account symlinks, unlike `chdir` that follows symlinks.
If the `cwd` resolves to a different depth, then the relative path becomes incorrect.
Refs #10460
Related: #12518
When an input method commits all or part of marked text during keyDown,
AppKit returns the committed text through insertText. Treat that as
text committed by the input method instead of replaying the original key
event to the terminal.
Previously this path only handled arrow-key commits specially. A
control-key shortcut that commits preedit text could still be encoded as
the original control input after composition, such as ctrl+j becoming LF.
Send committed preedit text as a text-only event for any key that causes
the commit. Only replay arrow navigation keys that the existing Korean
IME handling expects, and keep plain left-arrow suppressed because AppKit
already leaves the caret in place.
AI usage: OpenAI Codex helped investigate, implement, test, and refine
this change. I reviewed and tested the resulting code.
macos: suppress control-char input while composing
When AppKit delivers a single C0 control character during marked-text
composition, Ghostty should treat it as input consumed by the composing
state instead of forwarding it to the terminal.
This prevents control-key IME actions, such as Japanese input shortcuts
like ctrl+h/j/m/n, from leaking into the terminal while composition is
still active. Printable text and non-composing control input continue
through the normal key path.
Refs #10460
Related: #2628, #4539
Vouched in #12169
Testing:
- xcodebuild test -scheme Ghostty -destination platform=macOS
-only-testing:GhosttyTests/SurfaceViewAppKitTests
- Manually tested Japanese IME control-key shortcuts on macOS
AI usage:
- OpenAI Codex helped investigate, implement, test, and refine this
change. I reviewed and tested the resulting code.
A bug found while recording that menu fix.
> ~~Will link to an open issue if there is one.~~
When toggling the command palette from the inline title editor, the
first responder state of the surface is changed quickly from true to
false.
`makeFirstResponder:` is called by the title editor when finishing, but
it happens **after** the command palette is shown, so the `focused` is
set to `true` while the command palette is shown. (Could be an AppKit
issue as well, since the resign is not called after but the command
palette is receiving `keyDown`.)
Since `performKeyEquivalent(with:)` is called on all of the subviews
until one of the return `true` so the paste action is consumed by the
surface instead of the first responder (command palette).
`lastKnownFileType = file` will change to `text` if you checking out
branches with Xcode opened. But this was generated by Xcode in the first
place.
Anyway we don't need it to be in the project tree to run the tests, and
you can still open the test plan in scheme editor.
Currently, cross to Darwin uses the Darwin headers bundled with Zig.
However, if you're running a build _on_ Darwin, an error is thrown if
the SDK can't be found, even though the bundled headers are still
available.
Now, we continue to search for and prefer the installed SDK, but if it
can't be found, we fall back to the bundled headers rather than failing
the build.
Currently, cross to Darwin uses the Darwin headers bundled with Zig.
However, if you're running a build _on_ Darwin, an error is thrown if
the SDK can't be found, even though the bundled headers are still
available.
Now, we continue to search for and prefer the installed SDK, but if it
can't be found, we fall back to the bundled headers rather than failing
the build.
`lastKnownFileType = file` will change to `text` if you checking out branches with Xcode opened. But this was generated by Xcode in the first place.
Anyway we don't need it to be in the project tree to run the tests, and you can still open the test plan in scheme editor.
When AppKit delivers a single C0 control character during
marked-text composition, Ghostty should treat it as input consumed by
the composing state instead of forwarding it to the terminal.
This prevents control-key IME actions, such as Japanese input
shortcuts like ctrl+h/j/m/n, from leaking into the terminal while
composition is still active. Printable text and non-composing control
input continue through the normal key path.
AI usage: OpenAI Codex helped investigate, implement, test, and refine
this change. I reviewed and tested the resulting code.
Enforcing an absolute minimum of 1 for scroll events causes differing
scroll speeds between high-resolution and standard scroll wheels on
Linux. Since this was added to handle MacOS's precision scrolling
emulation, this patch alters the behaviour so that the absolute minimum
is only enforced on MacOS.
NB: This can't just be fixed by adjusting `mouse-scroll-multiplier`
since that affects *all* scroll events whether they're high-resolution
or not. Reducing `mouse-scroll-multiplier` to handle high-res scroll
events better makes scrolling unusably slow for regular scroll wheels
connected to the same machine.
Fixes#11648.
Enforcing an absolute minimum of 1 for scroll events causes differing
scroll speeds between high-resolution and standard scroll wheels on
Linux. Since this was added to handle MacOS's precision scrolling
emulation, this patch alters the behaviour so that the absolute minimum
is only enforced on MacOS.
Fixes#11648.
This now fixes#11410 completely, `navigate_search:next` and
`navigate_search:previous` are already fixed in
18f2702225.
Also fixes: surface is not focused after ending search via menu bar
This PR addresses
https://github.com/ghostty-org/ghostty/discussions/12108 implemented
similarly to https://github.com/ghostty-org/ghostty/pull/8254 to allow
middle click + TrackPoint scrolling on MacOS. `primary-paste` naming
comes from `gtk_enable_primary_paste`.
The following configuration values for `middle-click-action` are
provided:
- `primary-paste` - Paste from the selection (or system) clipboard per
`copy-on-select`.
- `ignore` - Do nothing, ignore the middle click.
Tested locally on macOS with Zig 0.15.2 using `zig build
-Doptimize=ReleaseFast`.
Thank you!
Fixes quick terminal breaking when auto-hide is enabled and quick
terminal is manually toggled off (#11679).
`quick-terminal-autohide` is implemented by the `Window.propIsActive`
function in `apprt/gtk/class/window.zig` which calls
`Window.toggleVisibility` when the quick terminal window becomes
inactive (loses focus). However `Window.propIsActive` is also triggered
when you manually hide the quick terminal because hiding it causes the
window to become inactive. Normally that should just toggle the quick
terminal off and immediately back on, but there is also a re-entrancy
issue. Manually toggling off the terminal causes the
`Application.toggleQuickTerminal` (in `apprt/gtk/class/application.zig`)
to run which sets off the call chain `Window.toggleVisibility ->
gtk_widget_set_visible -> ... GTK signal/event handling ... ->
Window.propIsActive -> Window.toggleVisibility ->
gtk_widget_set_visible`.
The nested calls to `gtk_widget_set_visible` cause the GTK window state
to become corrupted. The window is marked visible, but is not actually
visible or just shows a placeholder. What exactly happens depends on the
compositor and how it handles moving window focus.
Reproduced the bug on KDE and hyprland and verified the fix on both.
### Changes
`apprt/gtk/class/window.zig`: added check to `Window.propIsActive` to
only toggle quick-terminal if it is inactive **and** visible.
### AI Disclosure
Found the bug without AI using "printf debugging" then traced it through
GTK with valgrind. Used GPT5.4 in setting up valgrind and researching
how signals/events move through GTK internally.
This updates UI tests and adds a test plan on disk, so we can change the
configuration to different ones with the host app.
If you changed the icon in regular ghostty config file, the tests can
only be run once, since the signature is changed after changing the
icon. Adding an on-disk test plan helps us to better control the
environment for the tests.
Related to #12466
`Preedit.range()` returns an inclusive range, but the end position was
calculated as `start + w`. For wide preedit text, this covers one extra
cell.
In Debug builds, Korean IME composition between existing Hangul
characters can panic with:
`index out of bounds: index 2, len 2`
I reproduced this reliably when there are two Hangul characters to the
right of the cursor. For example, type `가나다`, move the cursor between
`가` and `나`, then start a new Korean IME composition. With the old range
calculation, the renderer skips the first wide character plus the head
cell of the next wide character, then resumes on that character's spacer
tail.
This changes the inclusive end to `start + (w - 1)` and adds focused
tests for narrow, wide, and right-edge preedit ranges.
This does not fully fix the visual behavior reported in #12466. The
adjacent character can still disappear during composition, so this PR
only fixes the crash side of the problem.
## Problem
Current `Se` sequence (reset cursor style) is `\E[2 q`, which always
sets steady block, regardless of user config.
## Solution
Update sequence to `\E[0 q`, which sets the cursor style to the user
configured default cursor.
fix https://github.com/ghostty-org/ghostty/issues/12482
Helps with neovim issue: https://github.com/neovim/neovim/issues/38987
## AI Disclosure
I didn't use AI for this, haha. Unless you count random questions to
learn about terminfo beforehand, but I relied on [legit
resources](https://invisible-island.net/xterm/terminfo.html) for real
info. It says:
> Se resets the cursor style to the terminal power-on default.
I think the useful interpretation is to set the user configured default.
`allocTmpDir` previously read `%TMP%` via `getenvW` and returned `null`
if the variable wasn't set, requiring each caller to to deal with the
nullable. Unfortunately, there isn't a platform-neutral default value
that makes sense for those cases (i.e. `/tmp` is POSIX-y).
We now use `GetTempPathW` on Windows, which is the official way to get
this directory: `TMP` → `TEMP` → `USERPROFILE` → `GetWindowsDirectoryW`.
With a real system call behind it, the function no longer needs to be
nullable: the only remaining failure modes are OOM (propagated) and the
syscall itself failing or returning data we can't decode. In those later
cases, we use `C:\Windows\Temp` as a fallback, similar to how we use
`/tmp` in the POSIX case.
The Windows path always allocates so it still must be paired with
`freeTmpDir`, which matches the existing contract.
---
*AI Disclosure:* I verified the Windows path using Claude and Zig's
cross-compilation capabilities because I don't have a Windows
environment in which to test this. I do fully understand the code based
on my prior life as a Windows game developer though.
This makes sure that if Ghostty crashes, commands spawned are also
terminated automatically by the Flatpak Session Helper.
The few crashes I got left a lot of background processes, some of them
pretty heavy and took awhile to be figured out.
`allocTmpDir` previously read `%TMP%` via `getenvW` and returned `null`
if the variable wasn't set, requiring each caller to to deal with the
nullable. Unfortunately, there isn't a platform-neutral default value
that makes sense for those cases (i.e. `/tmp` is POSIX-y).
We now use `GetTempPathW` on Windows, which is the official way to get
this directory: `TMP` → `TEMP` → `USERPROFILE` → `GetWindowsDirectoryW`.
With a real system call behind it, the function no longer needs to be
nullable: the only remaining failure modes are OOM (propagated) and the
syscall itself failing or returning data we can't decode. In those later
cases, we use `C:\Windows\Temp` as a fallback, similar to how we use
`/tmp` in the POSIX case.
The Windows path always allocates so it still must be paired with
`freeTmpDir`, which matches the existing contract.
Holding the renderer state mutex is a documented precondition of
`processLinks`, but `mouseButtonCallback` previously called the function
without the mutex.
This creates a race with the I/O thread's `processOutput`, which can
prune scrollback pages while `processLinks` is reading them, resulting
in a use-after-free segfault. See
https://github.com/ghostty-org/ghostty/discussions/12409 (Linux: crash
while selecting text).
57b5e1e250/src/Surface.zig (L4354-L4355)57b5e1e250/src/Surface.zig (L3822-L3824)995e4e375 (os: open) changed the body of `processLinks` to be
non-trivial and documented the precondition, but the lock was not held
at the call site.
This should be safe to delete now after #12461.
I tested saving 27 tabs, 4 with 2 splits,
`TerminalRestorable.encode(with:` finished successfully.
And I check the breakpoints when the Sparkle sends
`-[NSRunningApplication treminate]`. The call stack at `-[NSResponder
invalidateRestorableState]` is pretty much the same as quitting via
`cmd+q`.
First two commits fix the issue when upgrading from 1.2.x to 1.3.x.
(#11304)
> To double check if this pr really fixes the issue, you can either
archive a release build, sign with the same profile, and override
manually.
>
> Or you can find the `savedState` files (located in `~/Library/Daemon\
Containers/<uuid>`), can copy them the local build dir (which is what I
did), and run the debug build.
Following commits add tests for migrations and some logs.
**Currently the minimum version is set to 1.2.x**, since there's a lot
changes comparing to 1.1.x. It will be difficult to restore
`Ghostty.SplitNode` -> `SplitTree<Ghostty.SurfaceView>` without
introducing a lot of checks.
Factor TempDir's name generation into a reusable `randomBasename` (16
random bytes, url-safe base64) and add `randomTmpPath` on top, which
composes `allocTmpDir` + `randomBasename` into a single allocated path
in the form `{TMPDIR}/{prefix}{random}` (`mktemp(1)`-ish).
This is convenient for callers who want a unique path under TMPDIR (for
a temporary file, socket, etc.) without having to think about basename
buffer sizing or path joining.
Also, use `std.base64.url_safe_no_pad.Encoder` instead of the custom
base64 alphabet, which is exactly equivalent.
Factor TempDir's name generation into a reusable `randomBasename` (16
random bytes, url-safe base64) and add `randomTmpPath` on top, which
composes `allocTmpDir` + `randomBasename` into a single allocated path
in the form `{TMPDIR}/{prefix}{random}` (mktemp(1)-ish).
This is convenient for callers who want a unique path under TMPDIR (for
a temporary file, socket, etc.) without having to think about basename
buffer sizing or path joining.
Also, use `std.base64.url_safe_no_pad.Encoder` instead of the custom
base64 alphabet, which is exactly equivalent.
Link detection currently expands the clicked location to a full line
before running the configured regexes. When semantic prompt markers are
present, this can cause prompt text and neighboring content to be
matched together even though they are distinct semantic regions.
Use semantic prompt boundaries when selecting the text to inspect for
link matching. This keeps prompt text separate from the content beside
it and avoids folding prompt text into double-click link/path selection.
Add a regression test that models a prompt and command on the same line
and verifies the prompt region and input region remain separate.
----
this is a fix for the issue users reported in
https://github.com/ghostty-org/ghostty/discussions/11415
**disclaimer**: I used codex addon within Cursor to research this
bug/regression and find a proper fix for it.
> Re-submitting #11857, which was auto-closed pre-vouch. I'm now in the
vouched list.
## Summary
Adds default keybinds for `move_tab` on GTK/Linux, matching the
idiomatic Linux convention used by Firefox, GNOME Terminal, and VSCode:
- **`Ctrl+Shift+PageUp`** → `move_tab:-1` (move tab left)
- **`Ctrl+Shift+PageDown`** → `move_tab:1` (move tab right)
To free these keys, `jump_to_prompt` is reassigned to `Ctrl+Shift+Arrow
Up/Down`, which:
- Mirrors the macOS default (`Cmd+Shift+Arrow Up/Down`)
- Is currently unbound on GTK
- Maintains directional consistency (up = previous, down = next)
The new `move_tab` bindings use `putFlags` with `performable: true` to
match the pattern used by other tab actions (`previous_tab`,
`next_tab`).
Closes#4998
## Changes
- `src/config/Config.zig`: Reassign `jump_to_prompt` from
`Ctrl+Shift+PageUp/PageDown` to `Ctrl+Shift+Arrow Up/Down` (GTK only)
- `src/config/Config.zig`: Add `move_tab` default keybinds as
`Ctrl+Shift+PageUp/PageDown` (GTK only)
## Test plan
- [ ] Build on Linux/GTK: `zig build`
- [ ] Verify `Ctrl+Shift+PageUp` moves current tab left
- [ ] Verify `Ctrl+Shift+PageDown` moves current tab right
- [ ] Verify `Ctrl+Shift+Arrow Up` jumps to previous prompt
- [ ] Verify `Ctrl+Shift+Arrow Down` jumps to next prompt
- [ ] Verify `Ctrl+PageUp/PageDown` still switches tabs (unchanged)
- [ ] Verify macOS keybinds are unaffected
Reassign jump_to_prompt from Ctrl+Shift+PageUp/PageDown to
Ctrl+Shift+Arrow Up/Down on GTK, freeing the idiomatic Linux
keybinds (Ctrl+Shift+PageUp/PageDown) for move_tab.
This matches the tab-moving keybinds used by Firefox, GNOME Terminal,
and VSCode. The new jump_to_prompt binding mirrors the macOS pattern
(Cmd+Shift+Arrow Up/Down).
Closes#4998
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fixes#11461
- Send Korean IME committed text as a separate text-only key event when
arrow-key handling commits preedit text.
- Replay arrow navigation after the committed text is sent. Do not
replay plain Left Arrow to match Terminal.app behavior.
- Manually tested Arrow keys and Opt/Cmd+Arrow during Korean preedit on
macOS.
Implements the behavior from #9788.
Today, middle-click paste always reads from the selection clipboard (or
the
system clipboard on platforms without a selection clipboard). With this
change
it follows `copy-on-select`:
- `true`: selection clipboard (unchanged)
- `clipboard`: system clipboard
- `false`: selection clipboard (also unchanged, preserves traditional
X11
middle-click behavior)
The idea is symmetry: if `copy-on-select = clipboard` writes selections
to the
system clipboard, middle-click should come back from there too.
Also updated the config doc comment, which previously claimed
middle-click
"will always use the selection clipboard".
### Testing
`zig build test` passes locally (macOS, Zig 0.15.2).
Built and runtime-tested via the fork's CI:
https://github.com/007hacky007/ghostty/actions/runs/24707475544 - I'm
running the resulting binary daily and the three `copy-on-select` modes
behave as described above.
Due to a known Gtk issue, the scrolled_window at the root of the
template is free-ed twice on dispose. This causes crashes when used with
GNOME 49 platform (Gtk 4.20, libadwaita 1.8.5).
Workaround this issue by wrapping the root child in another Adw.Bin,
similar to widgets like ResizeOverlay.
LLM was used to perform discovery against a manually recorded Valgrind
trace, and helped tracking down known fixes for this problem. The
comment in code was taken from another instance in the repository.
Fixes https://github.com/ghostty-org/ghostty/discussions/12306
Assisted-by: OpenAI GPT-5.4
Due to a known Gtk issue, the scrolled_window at the root of the
template is free-ed twice on dispose. This causes crashes when used with
GNOME 49 platform (Gtk 4.20, libadwaita 1.8.5).
Workaround this issue by wrapping the root child in another Adw.Bin,
similar to widgets like ResizeOverlay.
LLM was used to perform discovery against a manually recorded Valgrind
trace, and helped tracking down known fixes for this problem.
Fixes https://github.com/ghostty-org/ghostty/discussions/12306
Assisted-by: OpenAI GPT-5.4
Holding the renderer state mutex is a documented precondition of
`processLinks`, but `mouseButtonCallback` previously called
the function without the mutex.
This creates a race with the I/O thread's `processOutput`, which can
prune scrollback pages while `processLinks` is reading them, resulting
in a use-after-free segfault. See
https://github.com/ghostty-org/ghostty/discussions/12409 (Linux: crash
while selecting text).
57b5e1e250/src/Surface.zig (L4354-L4355)57b5e1e250/src/Surface.zig (L3822-L3824)995e4e375 (os: open) changed the body of `processLinks` to be
non-trivial and documented the precondition, but the lock was not held
at the call site.
Link detection currently expands the clicked location to a full line
before running the configured regexes. When semantic prompt markers
are present, this can cause prompt text and neighboring content to be
matched together even though they are distinct semantic regions.
Use semantic prompt boundaries when selecting the text to inspect for
link matching. This keeps prompt text separate from the content beside
it and avoids folding prompt text into double-click link/path
selection.
Add a regression test that models a prompt and command on the same
line and verifies the prompt region and input region remain separate.
Previously `ghostty_app_key_is_binding` (unlike Surface) is just using
`config.keybind` to check whether a KeyEvent is in the set or not.
After this, I can add unit tests for keybinding more easily with dummy
configs.
I didn't find any usages of this in GTK, so it shouldn't affect
anything. ci will see if this is the case:)
This fixes a hardcoded build issue on macOS where Zig unconditionally
forces xcodebuild -create-xcframework to run during compilation, even
when the caller explicitly specifies that they only want the raw
standard C objects/headers (-Demit-lib-vt).
This fixes a hardcoded build issue on macOS where Zig unconditionally forces xcodebuild -create-xcframework to run during compilation, even when the caller explicitly specifies that they only want the raw standard C objects/headers (-Demit-lib-vt).
The Bug:
Around line 155 in build.zig, the libghostty-vt xcframework was being packaged unconditionally for Darwin builds. This caused developers (and wrappers like go-libghostty) attempting to natively build the vt library locally using only the minimal macOS Command Line Tools to experience an immediate crash, as xcodebuild -create-xcframework strictly demands a full Xcode application installation.
The Fix:
Guarded the GhosttyLibVt xcframework creation step with config.emit_xcframework. Because src/build/Config.zig intuitively forces emit_xcframework to default to false whenever emit_lib_vt is invoked, this structurally allows lightweight macOS builds to safely skip the xcodebuild invocation while still correctly compiling the standard .a object library files.
This allows libghostty-vt to be cross-compiled for macOS from non-macOS
platforms. I've updated pkg/apple-sdk to fallback to Zig's embedded
macOS headers if the macOS SDK is not found. Additionally,
CombineArchivesStep has been updated to use Linux tooling on Linux. CI
updated to test this.
This allows libghostty-vt to be cross-compiled for macOS from non-macOS
platforms. I've updated pkg/apple-sdk to fallback to Zig's embedded
macOS headers if the macOS SDK is not found.
Additionally, CombineArchivesStep has been updated to use Linux
tooling on Linux.
Previously `ghostty_app_key_is_binding` (unlike Surface) is just using `config.keybind` to check whether a KeyEvent is in the set or not.
After this, I can add unit tests for keybinding more easily, with dummy configs.
Extract the tight per-byte parsing loop from TerminalParser.step into
a separate noinline function (parseAll). This eliminates a ~20%
benchmark regression that appeared after the highway vendor changes
despite zero changes to the parser source code.
The root cause: the parser benchmark processes 50 MB of input through
a byte-at-a-time DFA loop that is highly sensitive to instruction
cache-line placement on Apple Silicon. The M-series cores fetch
aligned 16-byte blocks; when the loop head lands near the end of a
64-byte cache line (offset 60), only one instruction fits in the
first fetch versus four when aligned to offset 48. This causes ~29%
more cycles for identical instruction counts.
Previously the loop was inlined into the large step() function, so
any code change anywhere in the binary (like the highway vendor
restructuring) could shift the loop across a cache-line boundary.
By making parseAll noinline, the loop gets its own function placement
that is stable regardless of surrounding code changes.
The previous runtime_detect.zig called std.zig.system.resolveTargetQuery
which pulled in the entire Zig target/CPU model table infrastructure for
every architecture (~4,000 symbols, ~175 KB of data tables, ~130 KB of
code). This bloated the binary by ~500 KB and shifted code layout enough
to cause a measurable icache/branch-predictor regression in unrelated
hot paths like the terminal parser (~20% more cycles for identical
instruction counts).
Replace with minimal, direct CPU feature detection per architecture:
CPUID + XGETBV inline assembly on x86, sysctlbyname on Darwin AArch64,
and getauxval/prctl via std.os.linux (direct syscalls, no libc) on
Linux for AArch64, PPC, S390x, RISC-V, and LoongArch.
Split into per-architecture files under src/detect/ for
maintainability.
This uses a custom fork of `hwy/targtes.cpp` that uses an extern
function written in Zig to use Zig's standard CPU detection to avoid
a dependency on Apple SDK headers.
This is on the path to removing Apple SDK requirements to build
libghostty-vt, but will require a lot more work outside of this. The goal
is to get this out of our external dependencies first and then we can
work on removing the internal side.
Adds a FreeType-based `Discover` implementation for Windows. It walks
the system font directory (`%SYSTEMROOT%\Fonts`) and the per-user
directory (`%LOCALAPPDATA%\Microsoft\Windows\Fonts`), matches
descriptors by FreeType `family_name` (falling back to the SFNT name
table), and, when a codepoint is in the descriptor, filters on CMap
coverage.
Wired up as a new `.freetype_windows` backend which `Backend.default()`
now returns on Windows. Existing freetype-only paths are untouched and
no other platform is affected; cross-platform switches were extended to
handle the new enum value the same way they handle `.freetype`.
With this in place, the standard code paths (`+list-fonts`,
`SharedGridSet` font-family lookup, `CodepointResolver` fallback) work
on Windows without any `os.tag == .windows` branches in the caller.
Verified by the `build-libghostty-windows-gnu` CI job. No runtime binary
ships yet on Windows (no apprt), but this is a drop-in for the discovery
API that the Win32 apprt (and the revisited `+list-fonts` PR #12384)
will use. Once this lands, #12384 can be closed and `+list-fonts` will
work on Windows through the ordinary discovery code path, which
addresses the review feedback that `+list-fonts` should only show fonts
the internal discovery can find.
---
AI usage disclosure: developed with Claude Code (Claude Opus 4.7).
Claude drafted the implementation based on my design direction -- I
picked the "add a Discover backend" shape over the ad-hoc approach in
the earlier `+list-fonts` PR. I reviewed each diff and validated it with
a Windows GNU-ABI smoke build before pushing.
Part of the Win32 apprt upstreaming series (see discussion #2563 /
mattn/ghostty#1).
This code was motivated by the need for the glyph protocol handler
(#12352) to be able to validate the provided `glyf` payload, without
having to link freetype or anything (because libghostty-vt needs to be
static). As such it's written specifically to meet those needs, but in
such a way that it can be expanded if we find a need for more in-depth
inspection of `glyf`s in the future.
Two more holdouts in DeferredFace.zig test helpers calling
Fontconfig.init / CoreText.init with no args; Nix test CI surfaced
them for the fontconfig path.
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
> [!IMPORTANT]
> Stacked on #12214. Review that first. (i am targeting `main` so here
you will see the full changeset, including 12214
Two changes that make the static libghostty archive consumable by
external linkers (MSVC link.exe, .NET NativeAOT, Rust, Go, etc.):
**Fat static archive on all platforms**
The static archive previously only bundled vendored deps on macOS (via
libtool). On Windows and Linux the archive contained only the
Zig-compiled code, requiring consumers to find and link freetype,
harfbuzz, glslang, spirv-cross, simdutf, oniguruma, etc. separately.
Now all platforms produce a single fat archive:
- macOS: libtool (unchanged)
- Windows: zig ar qcL --format=coff (MSVC's lib.exe can't read
Zig-produced GNU-format archives, so we use the bundled LLVM archiver)
- Linux: ar -M with MRI scripts (same approach as libghostty-vt)
**MSVC ubsan suppression for C deps**
Zig's ubsan runtime can't be bundled on Windows (LNK4229), leaving
__ubsan_handle_* symbols unresolved. freetype, glslang, spirv-cross, and
highway already suppress ubsan. This adds MSVC-conditional suppression
to seven more: harfbuzz, libpng, dcimgui, wuffs, oniguruma, zlib, and
stb.
Gated on abi == .msvc so ubsan coverage is preserved on Linux/macOS.
## Test plan
- [x] zig build produces a fat ghostty-static.lib (~230MB) with ~200
object files
- [x] MSVC's lib /LIST can read the archive
- [x] .NET NativeAOT consumer resolves all symbols (0 unresolved)
- [x] Linux/macOS builds unaffected (ubsan remains enabled)
CI on Windows (MSVC) surfaced three remaining callers of the old
zero-arg `Discover.init()` in shaper test helpers that the earlier
commit missed. Pass `lib` to match the new signature.
Co-authored-by: Claude <noreply@anthropic.com>
* Unset the Nix compiler and linker environment in the fuzz dev shell so
AFL++ uses the system or Homebrew Apple toolchain directly.
* Force afl-cc to link with lld because the newer Apple linker asserts
on the custom sections emitted by AFL's LLVM instrumentation.
* Pin fuzz-libghostty to the host target so the build does not inherit
stray SDK targets from the environment.
On macOS 26.4, AFL builds were picking up Nix compiler-wrapper
variables and Apple SDK target settings from the shell environment.
That caused afl-cc to drive the wrong linker and target configuration,
which broke even simple fuzz harness builds. Unset the Nix compiler and
linker environment in the fuzz dev shell so AFL++ uses the system or
Homebrew Apple toolchain directly.
Also force afl-cc to link with lld because the newer Apple linker
asserts on the custom sections emitted by AFL's LLVM
instrumentation. Finally, pin fuzz-libghostty to the host target so the
build does not inherit stray SDK targets from the environment.
On Windows the configured shell was always executed as `cmd.exe /C
<shell>`. That inserts a cmd.exe even for simple values like `command =
wsl ~` or `command = pwsh -NoLogo`, producing two processes where one
would do.
Two concrete side effects:
An extra cmd.exe appears in every Windows terminal's process tree
(visible in Task Manager / process listings), two processes per surface
where only one is the user's shell.
cmd.exe state set by AutoRun (`HKCU\Software\Microsoft\Command
Processor\AutoRun`, used commonly for DOSKEY aliases or `cd` in
`init.cmd`) lives in the wrapping cmd process, not in the user's shell.
Since AutoRun state like DOSKEY aliases is per-process, the user's
aliases don't reach the shell they actually interact with.
Run the shell value directly instead. If it contains whitespace, split
on whitespace into argv. A bare `cmd.exe` is resolved via `%COMSPEC%`
(the documented path to the current command processor). Other bare
values are left to PATH resolution in `Command.startWindows` (#12387).
The simple whitespace split does not honor Windows CLI quoting rules;
users who need quoted arguments should use the direct command form,
which takes an argv array as-is. For the common case (`wsl ~`, `pwsh
-NoLogo`, `cmd.exe /k init.cmd`, etc.) this covers the shapes users
actually write today.
Also skips the termios focus timer on Windows in `focusGained`, since
Windows has no termios -- the callback was arming a timer whose tick did
nothing and just added noise.
---
AI usage disclosure: developed with Claude Code (Claude Opus 4.7).
Claude drafted the implementation based on my design direction -- I
picked which pieces belong in this PR (drop the cmd wrapper, use
`%COMSPEC%`, skip the termios focus timer) and which belong in sibling
PRs. I reviewed each diff and validated it with a Windows GNU-ABI smoke
build before pushing.
Part of the Win32 apprt upstreaming series (see discussion #2563 /
mattn/ghostty#1).
Because we generally read this value from an environment variable, we
the resulting value can include a trailing slash (as on macOS). This
results in less-friendly path operations for callers who are building
paths based on this value.
`std.fs.path.join()` handles trailing slashes just fine, but it's an
allocating API. For callers who just want to format a path, they have to
assume they need to include their own path separator.
We can make this friendlier by always trimming trailing path separators
from the environment variable values before returning the slice.
This behavior matches "higher-level" languages' standard libraries (I
checked Python, Node, Ruby, and Perl). Other "systems" languages (Go,
Rust) just return the system value as-is, like we were doing before.
Windows users often set bare command names in the Ghostty config
(`command = bash`) or pass them via `-e`, matching how they would on
Linux/macOS. Today that fails because `CreateProcessW` does not do
program search for `lpApplicationName` on its own.
Thanks to @qwerasd205 for pointing out that passing `NULL` for
`lpApplicationName` is exactly how Windows docs say to get program
search for free. This PR does that: drop the explicit utf16 conversion
for `lpApplicationName`, pass `null`, and make sure the program name is
the first token of `lpCommandLine`. Windows then walks parent-app dir,
CWD, system dirs, and PATH (and appends `.exe` for extensionless names).
The child also sees its `argv[0]` exactly as we wrote it rather than a
resolved absolute path, which is less surprising.
Net change is +15 / -7 in `src/Command.zig`; no new helpers, no changes
outside that file. The earlier version of this PR (which added
PATH/PATHEXT handling in `internal_os.path.expand`) is obsoleted by this
approach and has been force-pushed away.
---
AI usage disclosure: developed with Claude Code (Claude Opus 4.7).
Claude drafted the implementation based on my direction after
@qwerasd205's review suggested the NULL-lpApplicationName approach. I
reviewed the diff, built and verified it on the Windows GNU-ABI target,
and am responsible for the code landing here.
Part of the Win32 apprt upstreaming series (see discussion #2563 /
mattn/ghostty#1).
Because we generally read this value from an environment variable, we
the resulting value can include a trailing slash (as on macOS). This
results in less-friendly path operations for callers who are building
paths based on this value.
`std.fs.path.join()` handles trailing slashes just fine, but it's an
allocating API. For callers who just want to format a path, they have to
assume they need to include their own path separator.
We can make this friendlier by always trimming trailing path separators
from the environment variable values before returning the slice.
This behavior matches "higher-level" languages' standard libraries (I
checked Python, Node, Ruby, and Perl). Other "systems" languages (Go,
Rust) just return the system value as-is, like we were doing before.
Per review feedback, cover the four Windows branches added in the
parent commit:
- bare `cmd.exe` resolves via `%COMSPEC%` (with documented fallback)
- bare non-cmd shell (`pwsh.exe`) is passed through unchanged
- shell value with arguments (`wsl ~`) is split on whitespace
- direct command is passed through without modification
Co-authored-by: Claude <noreply@anthropic.com>
Per review feedback, drop the `if (Discover == Windows)` comptime
branches in SharedGridSet and list_fonts by making every backend's
`init` take a Library and ignore it when unused. Call sites just do
`Discover.init(self.font_lib)` now.
Also adds a discovery test for the Windows backend that looks up
Arial and checks the returned face has the 'A' codepoint.
Co-authored-by: Claude <noreply@anthropic.com>
Pass null for lpApplicationName and put the program as the first
token of lpCommandLine. Per the Windows docs, this makes
CreateProcessW perform the standard program search (parent-app dir,
CWD, system dirs, PATH) and append ".exe" when the name has no
extension.
So a bare command name like `wsl` or `pwsh` from the Ghostty config
now resolves without any PATH/PATHEXT handling on our side. The
child also sees its argv[0] exactly as written rather than replaced
with the resolved absolute path.
Co-authored-by: Claude <noreply@anthropic.com>
Resolve the system font directory from SYSTEMROOT rather than assuming
it lives on C:. If SYSTEMROOT is somehow unset we skip the system
directory instead of falling back to a literal drive letter.
Co-authored-by: Claude <noreply@anthropic.com>
Adds a FreeType-based Discover implementation for Windows that walks
the system (C:\Windows\Fonts) and per-user
(%LOCALAPPDATA%\Microsoft\Windows\Fonts) font directories, matching
descriptors via family_name / SFNT name table and optionally codepoint
presence.
Wired up as a new .freetype_windows backend which Backend.default() now
returns on Windows. Existing freetype-only paths are untouched.
With this in place, standard code paths -- +list-fonts, SharedGridSet
font-family lookup, CodepointResolver fallback -- work on Windows
without any os.tag == .windows branches in the caller.
Co-authored-by: Claude <noreply@anthropic.com>
On Windows the shell value was always executed as `cmd.exe /C <shell>`.
For even a simple `command = wsl ~` this spawned two processes (the
cmd wrapper and the user's actual shell) and had visible side effects:
an extra cmd.exe in the process tree, and cmd AutoRun state (DOSKEY
aliases, `cd` in init.cmd, etc.) running in the wrapper rather than
the user's shell, since AutoRun is per-process.
Run the shell value directly. If it contains whitespace, split on
whitespace into argv. Bare `cmd.exe` is resolved via %COMSPEC% which
is the documented path to the current command processor; other bare
values are left to PATH resolution in Command.startWindows.
The simple whitespace split does not honor Windows CLI quoting rules.
Users who need quoted arguments should use the direct command form.
Also skip the termios focus timer on Windows since Windows has no
termios; the focusGained callback was starting a timer whose callback
would then do nothing.
Co-authored-by: Claude <noreply@anthropic.com>
Extract CombineArchivesStep.zig so both GhosttyLib and GhosttyLibVt
use the same archive-combining logic. Uses libtool on Darwin and the
cross-platform combine_archives build tool elsewhere.
Renames the internal library's fat archive outputs from ghostty to
ghostty-internal, matching the pkg-config rename from PR 12214.
Zig's ubsan runtime cannot be bundled on Windows (LNK4229),
leaving __ubsan_handle_* symbols unresolved when the static
archive is consumed by an external linker like MSVC link.exe.
freetype, glslang, spirv-cross, and highway already suppress
ubsan unconditionally. Add MSVC-conditional suppression to the
seven C dependencies that were missing it: harfbuzz, libpng,
dcimgui, wuffs, oniguruma, zlib, and stb.
The fix is gated on abi == .msvc so ubsan coverage is preserved
on Linux and macOS where bundle_ubsan_rt works.
The static libghostty archive previously only bundled vendored
dependencies on macOS (via libtool). On Windows and Linux the
archive contained only the Zig-compiled code, leaving consumers
to discover and link freetype, harfbuzz, glslang, spirv-cross,
simdutf, oniguruma, and other vendored deps separately.
Now all platforms produce a single fat archive:
- macOS: libtool (unchanged)
- Windows: zig ar qcL --format=coff (LLVM archiver with the L
flag to flatten nested archives; MSVC's lib.exe cannot read
Zig-produced GNU-format archives)
- Linux: ar -M with MRI scripts (same as libghostty-vt)
This makes the static library self-contained for consumers like
.NET NativeAOT that link via the platform linker (MSVC link.exe)
and need all symbols resolved from a single archive.
Adds a new CI job `build-libghostty-windows-gnu` that exercises the
GNU-ABI Windows library build path. The existing
`build-libghostty-vt-windows` job covers the MSVC ABI; with the recent
fixes (#12373 / #12381 / #12382) the GNU path is now viable, and this
job catches regressions before the upcoming Win32 apprt (discussion
#2563) starts to depend on it.
Named `build-libghostty-windows-gnu` rather than following the
`build-libghostty-vt-*` convention because this job also builds
`ghostty-internal.dll`, not just libghostty-vt. Happy to rename if you
prefer a different convention.
Part of the Win32 apprt upstreaming series (see discussion #2563 /
mattn/ghostty#1).
The existing `build-libghostty-vt-windows` job builds libghostty-vt
with the MSVC ABI. The Win32 apprt (discussion #2563) will target
the GNU ABI, so add a parallel job that exercises the GNU-ABI path
to catch bitrot.
The job runs `zig build -Dtarget=native-native-gnu -Dapp-runtime=none`
which produces ghostty-vt.dll and ghostty-internal.dll without
requiring a platform-specific apprt.
Widens the existing `-fno-sanitize=undefined` gate from `abi == .msvc`
to `os.tag == .windows`. The same undefined `__ubsan_handle_*` link
errors from simdutf/highway also reproduce on Windows GNU ABI, and the
fix is identical.
Part of the Win32 apprt upstreaming series (see discussion #2563 /
mattn/ghostty#1).
`combine_archives` spawns `zig ar -M`, hard-coding the command name
`"zig"` and relying on the binary being on `PATH`. On Windows when the
build is driven by an absolute zig.exe path (common in CI and
Scoop/winget installs), this surfaces as `error: FileNotFound`.
Pass `b.graph.zig_exe` explicitly so the tool always uses the exact zig
binary driving the build, matching how other build tools in this repo
spawn zig subcommands.
Part of the Win32 apprt upstreaming series (see discussion #2563 /
mattn/ghostty#1).
`combine_archives` spawns `zig ar -M` to combine static archives via
an MRI script. It hard-coded the command name `"zig"` and relied on
the binary being on `PATH`, which fails on Windows when the build is
driven by an absolute zig.exe path (common in CI and in Scoop/winget
installs where PATH isn't populated at build time). The failure
surfaces as `error: FileNotFound` from `Child.spawn`.
Pass `b.graph.zig_exe` as the first argument so the tool always uses
the exact zig binary that is driving the build, matching how other
build tools in this repo spawn zig subcommands.
The existing `-fno-sanitize=undefined` flag was gated on `abi == .msvc`
to avoid undefined `__ubsan_handle_*` references from simdutf/highway.
The same linker error reproduces on Windows GNU ABI for the same
reason: the Zig-bundled libraries don't provide a matching UBSan
runtime for these C/C++ objects in our build configurations.
Widen the condition to `os.tag == .windows` so both MSVC and GNU
Windows targets skip ubsan for these C++ deps.
Part of preparation for adding a Win32 application runtime (discussion
#2563). One of three small, independent build fixes that together
unblock the Windows GNU-ABI library build.
On Windows with non-MSVC ABI, `pub const DllMain` resolved to `void` (a
type), and Zig's stdlib `start.zig` then tried to call it as a function
via `root.DllMain(...)`, failing to compile with "type 'type' not a
function".
This restructures the conditional so MSVC keeps its existing CRT-init
handler unchanged, non-MSVC Windows gets a no-op `BOOL` handler, and
non-Windows continues to resolve to `void`.
Verified: `zig build -Dtarget=native-native-gnu -Dapp-runtime=none
[-Doptimize=ReleaseSafe]` now builds cleanly on Windows.
Per review feedback (#12373), fold the nested `if/else if/else` into a
single Windows-gated struct whose handler picks up the abi difference
via a comptime check. This removes the duplicated `const BOOL = ...`
block that the two per-abi structs shared.
Part of preparation for upstreaming a Win32 application runtime
(see discussion #2563). This is one of three small build-related
fixes that unblock the Windows GNU-ABI library build.
When targeting Windows with GNU ABI, the existing `DllMain` declaration
falls through to `void` (a type), which Zig stdlib's `start.zig` then
attempts to call as a function via `root.DllMain(...)` - producing the
compile error "type 'type' not a function".
Restructure the conditional so that:
- non-Windows builds keep `DllMain = void`
- Windows + MSVC keeps the existing CRT-init handler (unchanged)
- Windows + non-MSVC gets a no-op `BOOL` handler
This unblocks `zig build -Dtarget=native-native-gnu -Dapp-runtime=none`
on Windows.
~`${prefix}/include` and `${prefix}/lib` are incorrect under
split-prefix installs (e.g. Nix multi-output). Use `b.h_dir` /
`b.lib_dir` instead and drop the unneeded Nix postInstall/postFixup
hooks.~
Refactors the libghostty-vt derivation to:
- fix `libdir` pointing to the wrong output in the pkg-config files.
This would throw a missing library error at runtime.
- reduce the amount of manual copying, linking, and patching of files.
An earlier version of this PR used the zig compiler + `.pc` files to do
this. People pointed out concerns, so I came up with a simpler solution.
Claude Code was used to debug and write an initial fix. Final changes
rewritten and simplified by me. No AI was used to write comments,
descriptions, etc.
We want to have this for the glyph protocol so that we can validate
passed glyf data in libghostty without having to link freetype or
anything like that.
`${prefix}/include` and `${prefix}/lib` are wrong under split-prefix installs (e.g. Nix multi-output).
Use `b.h_dir` / `b.lib_dir` instead and drop the unneeded Nix postInstall/postFixup hooks.
Co-Authored-By: Sander <hey@sandydoo.me>
This helps developers like me to use a separate config for debugging
(which is already supported by the environment variable
`GHOSTTY_CONFIG_PATH`).
I can already use the local scheme to load a debugging config file, but
when opening the config file through Ghostty, it will still open the
default config.
This changes doesn't affect the release build, since `configPath` is
only set in the DEBUG build.
The GhosttyKit xcframework previously shipped the entire include/
directory, which pulled in the libghostty-vt headers under
include/ghostty/. Because those headers are not referenced from the
ghostty.h umbrella, Clang's module system emitted "umbrella header for
module 'GhosttyKit' does not include header 'ghostty/vt/*.h'" warnings
in Xcode builds.
Stage only ghostty.h and module.modulemap via addWriteFiles so the
xcframework Headers directory contains exactly the GhosttyKit API,
mirroring the pattern used in GhosttyLibVt.xcframework.
## AI disclosure
Claude made the changes (including the commit message), I reviewed and
tested them.
The GhosttyKit xcframework previously shipped the entire include/
directory, which pulled in the libghostty-vt headers under
include/ghostty/. Because those headers are not referenced from the
ghostty.h umbrella, Clang's module system emitted "umbrella header for
module 'GhosttyKit' does not include header 'ghostty/vt/*.h'" warnings
in Xcode builds.
Stage only ghostty.h and module.modulemap via addWriteFiles so the
xcframework Headers directory contains exactly the GhosttyKit API,
mirroring the pattern used in GhosttyLibVt.xcframework.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Looks like `NSWorkspace.shared.setIcon` can only be called from the main
App, DockTilePlugin is sandboxed and doesn't have the permission to
`file-write-finderinfo`.
<img width="1186" height="144" alt="image"
src="https://github.com/user-attachments/assets/e5ea4f1c-718c-493a-bda2-32787881881e"
/>
It works fine in debug, but not in release. This fixes#11489
Middle-click paste previously always read from the selection clipboard
(falling back to the standard clipboard on platforms without one).
Now the paste source follows copy-on-select:
- copy-on-select = true: paste from selection clipboard (unchanged)
- copy-on-select = clipboard: paste from system clipboard
- copy-on-select = false: paste from selection clipboard (unchanged)
Fixesghostty-org/ghostty#9788.
* Fix a memory leak when invalid Kitty graphics data is sent via APC
(this is the only commit for backporting to 1.3.2)
* Add `max_bytes` to limit size of buffered APC data by protocol to
prevent DoS, default to reasonable values
* libghostty: expose max bytes APC options
Expose the foreground process PID and TTY device path as read-only properties on the AppleScript terminal class and App Intents TerminalEntity. This enables reliable process-to-terminal mapping for automation tools when multiple terminals share the same CWD.
Closes#11592Closes#10756
Session: 019d341c-a165-7843-a2f7-2f426114cf17
Looks like `NSWorkspace.shared.setIcon` can only be called from the main App, DockTilePlugin is sandboxed and doesn't have the permission to `file-write-finderinfo`.
It works fine in debug, but not in release. This fixes#11489, #11290
Stop trying to use POSIX shared memory functions such as `shm_open` on
Android as it's unsupported and the platform libc does not have those
symbols.
This avoids an error such as the below when trying to use
`libghostty-vt` on Android:
> dlopen failed: cannot locate symbol "shm_open" referenced by [..]
Stop trying to use POSIX shared memory functions such as
`shm_open` on Android as it's unsupported and the platform libc does not
have those symbols.
This avoids an error such as the below when trying to use
`libghostty-vt` on Android:
> dlopen failed: cannot locate symbol "shm_open" referenced by [..]
Without this, shells spawned by ghostty cannot find the xterm-ghostty
terminfo entry because ncurses only searches standard system paths.
The snap's terminfo lives inside the snap sandbox and is inaccessible
unless TERMINFO_DIRS is set explicitly.
Maybe related to #12298?
When Screen resize forwards the active cursor into PageList reflow, a
history-pinned viewport can be remapped into the active area before the
preserved-cursor grow step finishes. The old code kept treating that
viewport as a history pin during the intermediate grow calls, which left
too few rows beneath the pin and tripped the viewport integrity checks.
Fix this by normalizing the viewport back to active as soon as reflow
moves the pinned row into the active area. Add a Screen-level regression
test that exercises the full resize path with bounded scrollback and
wrapped rows, and document the setup so the unwrap and viewport
transition are clear.
Maybe related to #12298?
When Screen resize forwards the active cursor into PageList reflow, a
history-pinned viewport can be remapped into the active area before the
preserved-cursor grow step finishes. The old code kept treating that
viewport as a history pin during the intermediate grow calls, which left
too few rows beneath the pin and tripped the viewport integrity checks.
Fix this by normalizing the viewport back to active as soon as reflow
moves the pinned row into the active area. Add a Screen-level regression
test that exercises the full resize path with bounded scrollback and
wrapped rows, and document the setup so the unwrap and viewport
transition are clear.
This updates simdutf to my fork which has a SIMDUTF_NO_LIBCXX option
that removes all libc++ and libc++ ABI dependencies. The plan is to open
an upstream PR with this, but I want to verify it here first.
From there, the hand-written simd code we have has been updated to also
no longer use any libc++ features. Part of this required removing utfcpp
since it depended on libc++ (`<iterator>`).
libghostty-vt now only depends on libc.
## Benchmark Results
| Corpus | Current `HEAD` median | `main` median | Delta vs `main` |
Notes |
| --- | ---: | ---: | ---: | --- |
| `valid-mixed-1g-seed1.bin` | `9.245s` | `9.111s` | `1.5%` slower |
Near tie; `main` remains slightly faster on fully valid input |
| `malformed-mixed-1g-seed1-rate0.005.bin` | `9.251s` | `12.705s` |
`37.3%` faster | Large improvement on malformed UTF-8 input |
Approximate throughput from the medians:
- Valid corpus: current `HEAD` `110.8 MiB/s`, `main` `112.4 MiB/s`
- Malformed corpus: current `HEAD` `110.7 MiB/s`, `main` `80.6 MiB/s`
This updates simdutf to my fork which has a SIMDUTF_NO_LIBCXX option
that removes all libc++ and libc++ ABI dependencies.
From there, the hand-written simd code we have has been updated to also
no longer use any libc++ features. Part of this required removing utfcpp
since it depended on libc++ (`<iterator>`).
libghostty-vt now only depends on libc.
This updates our synthetic generator for UTF-8 to expose:
- Flags to change 1/2/3/4-byte UTF-8 character distribution
- Flags to have only printable characters so we can benchmark pure UTF-8
vs our control sequence finder.
- Flags to have invalid characters so we can benchmark our error
handling.
This also adds an AGENTS.md to src/benchmark so agents can do the right
thing more easily.
These are necessary to robustly benchmark our libc++ removal PR.
This updates our synthetic generator for UTF-8 to expose:
- Flags to change 1/2/3/4-byte UTF-8 character distribution
- Flags to have only printable characters so we can benchmark
pure UTF-8 vs our control sequence finder.
- Flags to have invalid characters so we can benchmark our error
handling.
This also adds an AGENTS.md to src/benchmark so agents can do the right
thing more easily.
Closes#11995
Yet another small step to fix menu shortcut-related issues.
1. Create `MenuShortcutKey` from `NSMenuItem` and `KeyboardShortcut`.
2. Add `updateMenuShortcut` to update to Ghostty ones only.
Doesn't contain any actual changes to pass previous test cases.
### Closes#7649
The bar lives alongside URL Hover in VStack at the bottom. The current
body of SurfaceView is becoming rather long and complicated, so this pr
also contains some refactors:
- Move URL Hover to a separate file
> The text is copied from previous input string to keep it consistent,
also I’m confused with text on GTK so this is my first choice, but it
can be changed as the same as GTK.
Separate prs will be opened for:
1. Set to Read-only after exits
2. Hide cursor when in Read-only
### Preview
https://github.com/user-attachments/assets/eb44e211-eac5-4f40-836c-4912b18dfb01
- Highlight matching text in command palette search results
- Support initials matching
- Trim query before filtering commands
### AI Disclosure
Claude wrote most of it. I tested and reviewed it myself.
<img width="1544" height="297" alt="image"
src="https://github.com/user-attachments/assets/6ed98538-d6d3-48a0-8bb0-ac705611d058"
/>
The `zon2nix` binary is now compiled with Zig 0.16, but it still
produces Zig 0.15 compatible output (in fact the output is identical to
previous versions).
This mode allows programs to modify the code that the `backspace` key
(backarrow key in DEC parlance) sends. If this mode is
`off`/`false`/`reset` (the default, the same as before this PR), we send
the byte `0x7f`. If this mode is `on`/`true`/`set` we send the byte
`0x08`.
<img width="659" height="715" alt="Screenshot From 2026-04-09 11-00-25"
src="https://github.com/user-attachments/assets/4f3e14ac-757d-4bb2-9fc5-b17019ad35d5"
/>
Extend String.matchedIndices(for:) to fall back to initials
matching when no substring match is found. Typing the first letter
of each word now matches commands, e.g. "tbo" matches "Toggle
Background Opacity", with each matched initial highlighted.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add String.matchedIndices(for:) to find substring matches and use
it to bold and tint matched characters with the accent color in
both titles and subtitles. Title matches take priority — subtitles
are only highlighted when the title didn't match.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The `zon2nix` binary is now compiled with Zig 0.16, but it still produces
Zig 0.15 compatible output (in fact the output is identical to previous
versions).
Bumps
[softprops/action-gh-release](https://github.com/softprops/action-gh-release)
from 2.6.1 to 3.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/softprops/action-gh-release/releases">softprops/action-gh-release's
releases</a>.</em></p>
<blockquote>
<h2>v3.0.0</h2>
<p><code>3.0.0</code> is a major release that moves the action runtime
from Node 20 to Node 24.
Use <code>v3</code> on GitHub-hosted runners and self-hosted fleets that
already support the
Node 24 Actions runtime. If you still need the last Node 20-compatible
line, stay on
<code>v2.6.2</code>.</p>
<h2>What's Changed</h2>
<h3>Other Changes 🔄</h3>
<ul>
<li>Move the action runtime and bundle target to Node 24</li>
<li>Update <code>@types/node</code> to the Node 24 line and allow future
Dependabot updates</li>
<li>Keep the floating major tag on <code>v3</code>; <code>v2</code>
remains pinned to the latest <code>2.x</code> release</li>
</ul>
<h2>v2.6.2</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<h3>Other Changes 🔄</h3>
<ul>
<li>chore(deps): bump picomatch from 4.0.3 to 4.0.4 by <a
href="https://github.com/dependabot"><code>@dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/775">softprops/action-gh-release#775</a></li>
<li>chore(deps): bump brace-expansion from 5.0.4 to 5.0.5 by <a
href="https://github.com/dependabot"><code>@dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/777">softprops/action-gh-release#777</a></li>
<li>chore(deps): bump vite from 8.0.0 to 8.0.5 by <a
href="https://github.com/dependabot"><code>@dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/781">softprops/action-gh-release#781</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/softprops/action-gh-release/compare/v2...v2.6.2">https://github.com/softprops/action-gh-release/compare/v2...v2.6.2</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md">softprops/action-gh-release's
changelog</a>.</em></p>
<blockquote>
<h2>3.0.0</h2>
<p><code>3.0.0</code> is a major release that moves the action runtime
from Node 20 to Node 24.
Use <code>v3</code> on GitHub-hosted runners and self-hosted fleets that
already support the
Node 24 Actions runtime. If you still need the last Node 20-compatible
line, stay on
<code>v2.6.2</code>.</p>
<h2>What's Changed</h2>
<h3>Other Changes 🔄</h3>
<ul>
<li>Move the action runtime and bundle target to Node 24</li>
<li>Update <code>@types/node</code> to the Node 24 line and allow future
Dependabot updates</li>
<li>Keep the floating major tag on <code>v3</code>; <code>v2</code>
remains pinned to the latest <code>2.x</code> release</li>
</ul>
<h2>2.6.2</h2>
<h2>What's Changed</h2>
<h3>Other Changes 🔄</h3>
<ul>
<li>chore(deps): bump picomatch from 4.0.3 to 4.0.4 by <a
href="https://github.com/dependabot"><code>@dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/775">softprops/action-gh-release#775</a></li>
<li>chore(deps): bump brace-expansion from 5.0.4 to 5.0.5 by <a
href="https://github.com/dependabot"><code>@dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/777">softprops/action-gh-release#777</a></li>
<li>chore(deps): bump vite from 8.0.0 to 8.0.5 by <a
href="https://github.com/dependabot"><code>@dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/781">softprops/action-gh-release#781</a></li>
</ul>
<h2>2.6.1</h2>
<p><code>2.6.1</code> is a patch release focused on restoring linked
discussion thread creation when
<code>discussion_category_name</code> is set. It fixes
<code>[#764](https://github.com/softprops/action-gh-release/issues/764)</code>,
where the draft-first publish flow
stopped carrying the discussion category through the final publish
step.</p>
<p>If you still hit an issue after upgrading, please open a report with
the bug template and include a minimal repro or sanitized workflow
snippet where possible.</p>
<h2>What's Changed</h2>
<h3>Bug fixes 🐛</h3>
<ul>
<li>fix: preserve discussion category on publish by <a
href="https://github.com/chenrui333"><code>@chenrui333</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/765">softprops/action-gh-release#765</a></li>
</ul>
<h2>2.6.0</h2>
<p><code>2.6.0</code> is a minor release centered on
<code>previous_tag</code> support for
<code>generate_release_notes</code>,
which lets workflows pin GitHub's comparison base explicitly instead of
relying on the default range.
It also includes the recent concurrent asset upload recovery fix, a
<code>working_directory</code> docs sync,
a checked-bundle freshness guard for maintainers, and clearer
immutable-prerelease guidance where
GitHub platform behavior imposes constraints on how prerelease asset
uploads can be published.</p>
<p>If you still hit an issue after upgrading, please open a report with
the bug template and include a minimal repro or sanitized workflow
snippet where possible.</p>
<h2>What's Changed</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="b430933298"><code>b430933</code></a>
release: cut v3.0.0 for Node 24 upgrade (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/670">#670</a>)</li>
<li><a
href="c2e35e05a7"><code>c2e35e0</code></a>
chore(deps): bump the npm group across 1 directory with 7 updates (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/783">#783</a>)</li>
<li><a
href="3bb12739c2"><code>3bb1273</code></a>
release 2.6.2</li>
<li><a
href="c34030fec9"><code>c34030f</code></a>
chore: bump node to 24.14.1</li>
<li><a
href="8975bd05c0"><code>8975bd0</code></a>
chore(deps): bump vite from 8.0.0 to 8.0.5 (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/781">#781</a>)</li>
<li><a
href="f71937f44d"><code>f71937f</code></a>
chore(deps): bump brace-expansion from 5.0.4 to 5.0.5 (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/777">#777</a>)</li>
<li><a
href="3f0d239d58"><code>3f0d239</code></a>
chore(deps): bump picomatch from 4.0.3 to 4.0.4 (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/775">#775</a>)</li>
<li>See full diff in <a
href="153bb8e044...b430933298">compare
view</a></li>
</ul>
</details>
<br />
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
</details>
* Don't alter Kitty keyboard protocol responses. Kitty does not support
DECBKM so KKP doesn't take DECBKM into consideration.
* Make better use of the function key lookup to control what sequence is
returned when backspace is pressed using the legacy encoding.
This mode allows programs to modify the code that the `backspace`
key (backarrow key in DEC parlance) sends. If this mode is
`off`/`false`/`reset` (the default, the same as before this PR), we
send the byte `0x7f`. If this mode is `on`/`true`/`set` we send the
byte `0x08`.
## Summary
Mirror the `libghostty-vt-static` pkg-config pattern from #12210 for the
internal library.
- Add `ghostty-internal.pc` (shared, `-lghostty`) and
`ghostty-internal-static.pc` (static, direct archive reference) so
consumers can discover either variant via pkg-config
- Named `ghostty-internal` to distinguish from the public
`libghostty-vt` API
- Static module points at the platform-correct archive name
(`ghostty-static.lib` on Windows, `libghostty.a` elsewhere)
- pkg-config files are generated during shared builds and installed via
`GhosttyLib.install()`
## Test plan
- [x] `zig build` succeeds (default target)
- [x] `ghostty-internal.pc` and `ghostty-internal-static.pc` appear in
`zig-out/share/pkgconfig/`
- [x] Static `.pc` points at `ghostty-static.lib` (Windows) /
`libghostty.a` (Unix)
- [x] Shared `.pc` uses standard `-L -l` flags
- [x] Existing `libghostty-vt` pkg-config files are unaffected
Fix: #11989
Cause identified to: ab352b5af9
Original PR: #10003
Problem: I don't think it is OK to hard code the keybind like this at
all. Ghostty's config is flexible enough to achieve this.
Proposal: Revert the above commit via this PR.
@yasuf @bo2themax
Added a shared `OSSurfaceView` as the base class to share common
variables and functions across platforms.
Each commit contains a small change to move one or two variables or
functions to `OSSurfaceView`.
utfcpp is a header-only dependency, so its package wrapper does not need
to link the C++ standard library. Keep the empty static archive for
build integration, but stop adding an unnecessary libc++ dependency.
utfcpp is a header-only dependency, so its package wrapper does not
need to link the C++ standard library. Keep the empty static archive
for build integration, but stop adding an unnecessary libc++
dependency.
The vendored Highway package was being built with libc++ even though
Ghostty only uses its runtime target selection and dispatch support.
That pulled in extra C++ runtime baggage from upstream support files
such as abort, timer, print, and benchmark helpers.
Build Highway in HWY_NO_LIBCXX mode, only compile the target dispatch
sources we actually need, and compile Ghostty's SIMD translation units
with the same define so the header ABI stays consistent. Replace the
upstream abort implementation with a small local bridge that provides
Highway's Warn/Abort hooks and the target-query shim without depending
on libc++.
This keeps the Highway archive down to the dispatch pieces Ghostty uses
while preserving the existing dynamic dispatch behavior. The bridge is
documented so it is clear why Ghostty carries this small local
replacement.
We still depend on libc++ for other reasons, but I figure we should just
trim it down as needed. 😄
The vendored Highway package was being built with libc++ even though
Ghostty only uses its runtime target selection and dispatch support.
That pulled in extra C++ runtime baggage from upstream support files
such as abort, timer, print, and benchmark helpers.
Build Highway in HWY_NO_LIBCXX mode, only compile the target dispatch
sources we actually need, and compile Ghostty's SIMD translation units
with the same define so the header ABI stays consistent. Replace the
upstream abort implementation with a small local bridge that provides
Highway's Warn/Abort hooks and the target-query shim without depending
on libc++.
This keeps the Highway archive down to the dispatch pieces Ghostty
uses while preserving the existing dynamic dispatch behavior. The
bridge is documented so it is clear why Ghostty carries this small
local replacement.
Replace the ImageInfo and PlacementInfo sized structs and their
associated .info enum variants with a new _get_multi pattern that
batches multiple enum+pointer pairs into a single call. This avoids
struct ABI concerns (field order, padding, alignment,
GHOSTTY_INIT_SIZED) while preserving the single-call-crossing
performance benefit for FFI and Cgo callers.
Each _get_multi function takes an array of enum keys, an array of output
pointers, and an optional out_written parameter that reports how many
values were successfully written before any error. This applies
uniformly to all _get APIs: terminal_get, cell_get, row_get,
render_state_get, render_state_row_get, render_state_row_cells_get,
kitty_graphics_image_get, and kitty_graphics_placement_get.
The C example is updated to use compound-literal _get_multi calls, and
tests cover both success and error paths for every new function.
Regression of #12119, this memory leak affects new tabs, since the
terminal controller is not deallocated correctly, hitting `cmd+t` will
create a new window with two tabs, but only one actually contains usable
surface.
You can reproduce by:
1. Quit and Reopen Ghostty
2. Open a new window if no window is created (initial-window = false)
3. Close the window
4. Hit `cmd+t`
Replace the ImageInfo and PlacementInfo sized structs and their
associated .info enum variants with a new _get_multi pattern that
batches multiple enum+pointer pairs into a single call. This avoids
struct ABI concerns (field order, padding, alignment, GHOSTTY_INIT_SIZED)
while preserving the single-call-crossing performance benefit for FFI
and Cgo callers.
Each _get_multi function takes an array of enum keys, an array of
output pointers, and an optional out_written parameter that reports
how many values were successfully written before any error. This
applies uniformly to all _get APIs: terminal_get, cell_get, row_get,
render_state_get, render_state_row_get, render_state_row_cells_get,
kitty_graphics_image_get, and kitty_graphics_placement_get.
The C example is updated to use compound-literal _get_multi calls,
and tests cover both success and error paths for every new function.
Regression of #12119, this memory leak affects new tabs, since the terminal controller is not deallocated correctly. Hitting `cmd+t` will create a new window with two tabs, but only one actually contains usable surface.
You can reproduce by:
1. Quit and Reopen Ghostty
2. Open a new window if no window is created (initial-window = false)
3. Close the window
4. Hit `cmd+t`
The internal glue DLL was renamed from ghostty.dll to
ghostty-internal.dll. Update the LoadLibraryA call and the comment
block so this regression test still exercises the right artifact.
Switch the shared ghostty-internal.pc Libs: line from -lghostty to a
direct ${libdir}/<file> path, matching what the -static module already
does. The name-per-OS helpers now emit:
shared: ghostty-internal.dll (Windows) / ghostty-internal.so (other)
static: ghostty-internal-static.lib (Windows) / ghostty-internal.a
Direct paths sidestep the GNU-ld -l<name> search template, which
expects libghostty-internal.so/.a on Unix - we drop the lib prefix to
match the ghostty-internal pkg-config module name.
Also update the LipoStep out_name for the macOS universal static
archive to ghostty-internal.a for consistency.
Rename the internal library's install names to match the new
ghostty-internal pkg-config module convention:
ghostty.dll -> ghostty-internal.dll
ghostty-static.lib -> ghostty-internal-static.lib
libghostty.so -> ghostty-internal.so
libghostty.a -> ghostty-internal.a
This is the glue library between Ghostty's app shells and the GUI
core, historically (mis)named "libghostty". It is not the public
libghostty-vt API.
Add three sized structs that let callers fetch all image, placement, or
rendering metadata in a single call instead of many individual queries.
This is an optimization for environments with high per-call overhead
such as FFI or Cgo.
GhosttyKittyGraphicsImageInfo is returned via image_get() with the new
GHOSTTY_KITTY_IMAGE_DATA_INFO data kind. It bundles id, number, width,
height, format, compression, data pointer, and data length.
GhosttyKittyGraphicsPlacementInfo is returned via placement_get() with
the new GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_INFO data kind. It bundles
image id, placement id, virtual flag, offsets, source rect, columns,
rows, and z-index.
GhosttyKittyGraphicsPlacementRenderInfo is returned by the new
ghostty_kitty_graphics_placement_render_info() function, which combines
pixel size, grid size, viewport position, and resolved source rectangle.
This one requires image and terminal handles so it does not fit the
existing _get() pattern and is a dedicated function.
All three use the sized-struct ABI pattern with GHOSTTY_INIT_SIZED for
forward compatibility.
Add three sized structs that let callers fetch all image, placement,
or rendering metadata in a single call instead of many individual
queries. This is an optimization for environments with high per-call
overhead such as FFI or Cgo.
GhosttyKittyGraphicsImageInfo is returned via image_get() with the
new GHOSTTY_KITTY_IMAGE_DATA_INFO data kind. It bundles id, number,
width, height, format, compression, data pointer, and data length.
GhosttyKittyGraphicsPlacementInfo is returned via placement_get()
with the new GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_INFO data kind.
It bundles image id, placement id, virtual flag, offsets, source
rect, columns, rows, and z-index.
GhosttyKittyGraphicsPlacementRenderInfo is returned by the new
ghostty_kitty_graphics_placement_render_info() function, which
combines pixel size, grid size, viewport position, and resolved
source rectangle. This one requires image and terminal handles so
it does not fit the existing _get() pattern and is a dedicated
function.
All three use the sized-struct ABI pattern with GHOSTTY_INIT_SIZED
for forward compatibility.
Fixes#12151
When `emit_lib_vt` is true, the `// Tests` block was still
evaluated, pulling in the full ghostty-test dependency graph
(freetype, zlib, dcimgui, etc.). This causes the Nix
`libghostty-vt` package to fail when `doCheck` is enabled,
since those system libraries aren't available in the sandbox.
Guard the block with `if (!config.emit_lib_vt)`, following
the existing pattern at line 179.
In C ABI builds, the Zig std.log default writes to stderr which is not
appropriate for a library. Override std_options.logFn with a custom sink
that dispatches to an embedder-provided callback, or silently discards
when none is registered.
Add GHOSTTY_SYS_OPT_LOG to ghostty_sys_set() following the existing
decode_png pattern. The callback receives the log level as a
GhosttySysLogLevel enum, scope and message as separate byte slices,
giving embedders full control over formatting and routing.
Export ghostty_sys_log_stderr as a built-in convenience callback that
writes to stderr using std.debug.lockStderrWriter for thread-safe
output. Embedders who want the old behavior can install it at startup
with a single ghostty_sys_set call.
In C ABI builds, the Zig std.log default writes to stderr which is
not appropriate for a library. Override std_options.logFn with a
custom sink that dispatches to an embedder-provided callback, or
silently discards when none is registered.
Add GHOSTTY_SYS_OPT_LOG to ghostty_sys_set() following the existing
decode_png pattern. The callback receives the log level as a
GhosttySysLogLevel enum, scope and message as separate byte slices,
giving embedders full control over formatting and routing.
Export ghostty_sys_log_stderr as a built-in convenience callback that
writes to stderr using std.debug.lockStderrWriter for thread-safe
output. Embedders who want the old behavior can install it at startup
with a single ghostty_sys_set call.
Add a ghostty_vt_add_target() CMake function that lets downstream
projects build libghostty-vt for a specific Zig target triple. The
function encapsulates zig discovery, build-type-to-optimize mapping, the
zig build invocation, and output path conventions so consumers do not
need to duplicate any of that logic. It creates named IMPORTED targets
(e.g. ghostty-vt-static-linux-amd64) that work alongside the existing
native ghostty-vt and ghostty-vt-static targets.
The build-type mapping is factored into a shared _GHOSTTY_ZIG_OPT_FLAG
variable used by both the native build and the new function.
A new example/c-vt-cmake-cross/ demonstrates end-to-end cross-
compilation using zig cc as the C compiler, auto-detecting a cross
target based on the host OS.
Add a ghostty_vt_add_target() CMake function that lets downstream
projects build libghostty-vt for a specific Zig target triple. The
function encapsulates zig discovery, build-type-to-optimize mapping,
the zig build invocation, and output path conventions so consumers
do not need to duplicate any of that logic. It creates named IMPORTED
targets (e.g. ghostty-vt-static-linux-amd64) that work alongside the
existing native ghostty-vt and ghostty-vt-static targets.
The build-type mapping is factored into a shared _GHOSTTY_ZIG_OPT_FLAG
variable used by both the native build and the new function.
The static library targets now propagate c++ as a link dependency on
non-Windows platforms, fixing link failures when consumers use static
linking with the default SIMD-enabled build.
A new example/c-vt-cmake-cross/ demonstrates end-to-end cross-
compilation using zig cc as the C compiler, auto-detecting a cross
target based on the host OS.
Keep libghostty-vt.pc as the shared/default pkg-config module so
`pkg-config --static libghostty-vt` continues to emit the historical
`-lghostty-vt` flags. This preserves the old behavior for consumers that
still want it, even though that form remains ambiguous on macOS when
both the dylib and archive are installed in the same directory.
Add a separate libghostty-vt-static.pc module for consumers that need an
unambiguous static link. Its `Libs:` entry points directly at the
installed archive so macOS does not resolve the request to the dylib.
Update the Nix packaging to rewrite the new static module into the `dev`
output, use it in the static-link smoke test, and add a compatibility
check that covers both pkg-config entry points.
Keep libghostty-vt.pc as the shared/default pkg-config module so
`pkg-config --static libghostty-vt` continues to emit the historical
`-lghostty-vt` flags. This preserves the old behavior for consumers
that still want it, even though that form remains ambiguous on macOS
when both the dylib and archive are installed in the same directory.
Add a separate libghostty-vt-static.pc module for consumers that need
an unambiguous static link. Its `Libs:` entry points directly at the
installed archive so macOS does not resolve the request to the dylib.
Update the Nix packaging to rewrite the new static module into the `dev`
output, use it in the static-link smoke test, and add a compatibility
check that covers both pkg-config entry points.
Fixes#11990
Previously only slashes were replaced with hyphens in the branch name
used as the semver pre-release identifier. Branch names containing dots
(e.g. dependabot branches like
"cachix/install-nix-action-31.10.4") would cause an InvalidVersion error
because std.SemanticVersion only allows alphanumeric characters and
hyphens in pre-release identifiers.
Replace all non-alphanumeric, non-hyphen characters instead of only
slashes.
Fixes#11990
Previously only slashes were replaced with hyphens in the branch
name used as the semver pre-release identifier. Branch names
containing dots (e.g. dependabot branches like
"cachix/install-nix-action-31.10.4") would cause an InvalidVersion
error because std.SemanticVersion only allows alphanumeric
characters and hyphens in pre-release identifiers.
Replace all non-alphanumeric, non-hyphen characters instead of
only slashes.
This is a regression from #9983. When resetting to default, we shouldn't
use the representation of the icon, which will prevent the icon from
updating after system settings change.
1. Delete `macos-icon` config if it exists and reload.
2. Go to **System Settings -> Appearance** and change **Icon & widget
style** to any one other than Default, and observe the app icon.
<img width="228" height="179" alt="image"
src="https://github.com/user-attachments/assets/e53274f8-b679-4d6f-8e0b-edfd7d17811d"
/>
> A temporary workaround to this issue is to reload the config.
This pr resets the `NSDockTile.contentView`, which will let AppKit
revert back to `Ghostty.icon`.
https://github.com/user-attachments/assets/06ab0519-225b-45e1-85a5-a22832a36177
Pre-C23, the C standard allows compilers to choose any integer type
that can represent all enum values, so small enums could be backed
by char or short. This breaks ABI compatibility with the Zig side,
which backs these enums with c_int.
Define GHOSTTY_ENUM_MAX_VALUE as INT_MAX in types.h and add it as
the last entry in every enum in include/ghostty/vt/. This forces
the compiler to use int as the backing type, matching c_int on all
targets. INT_MAX is used rather than a fixed constant because enum
constants must be representable as int; values above INT_MAX are a
constraint violation in standard C.
Document this convention in AGENTS.md.
Pre-C23, the C standard allows compilers to choose any integer type
that can represent all enum values, so small enums could be backed
by char or short. This breaks ABI compatibility with the Zig side,
which backs these enums with c_int.
Define GHOSTTY_ENUM_MAX_VALUE as INT_MAX in types.h and add it as
the last entry in every enum in include/ghostty/vt/. This forces
the compiler to use int as the backing type, matching c_int on all
targets. INT_MAX is used rather than a fixed constant because enum
constants must be representable as int; values above INT_MAX are a
constraint violation in standard C.
Document this convention in AGENTS.md.
The 👻 Ghost Tab Issue
https://github.com/user-attachments/assets/cb91cd85-4a08-4c16-9efb-1a9ab30fc2bc
Previous failure scenario (User perspective):
1. Open a new tab
2. Instantly trigger close other tabs (eg. through custom user keyboard
shortcut)
3. Now you will see an empty Ghost Tab (Only a window bar with empty
content)
The previous failure mode is:
1. Create a tab or window now in `newTab(...)` / `newWindow(...)`.
2. Queue its initial show/focus work with `DispatchQueue.main.async`.
3. Close that tab or window with `closeTabImmediately()` /
`closeWindowImmediately()` before the queued callback runs.
4. The queued callback still runs anyway and calls `showWindow(...)` /
`makeKeyAndOrderFront(...)` on stale state.
5. The tab can be resurrected as a half-closed blank ghost tab.
The fix:
- Store deferred presentation work in a cancellable DispatchWorkItem and
cancel it from the close paths before AppKit finishes tearing down the
tab or window.
- This prevents the stale show/focus callback from running after close.
## AI Usage
I used GPT 5.4 to find the initial issue and fix it. I cleaned up and
narrowed down the commit afterwards.
-----
Additional Notes:
I use `cmd+o` to `close_tab:other`
https://github.com/jamylak/dotfiles/blob/main/ghostty/config#L106C1-L106C34
Try it for your self if you want to reproduce, just do a quick `cmd+t`
`cmd+o` and you will see
This produces a `ghostty-vt.xcframework` for `zig build -Demit-lib-vt`
when the host is macOS and the target is Apple platforms. Our CI has
been updated to release this via tip channels (GH releases and our blob
storage), too.
The xcframework contains binaries for macOS Universal (x86_64 +
aarch64), iOS, and iOS simulator.
I've added a Swift example we run in CI to verify this works. Users can
also drag and drop the XCFramework directly into Xcode.
## Example
```swift
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "swift-vt-xcframework",
platforms: [.macOS(.v13)],
targets: [
.executableTarget(
name: "swift-vt-xcframework",
dependencies: ["GhosttyVt"],
path: "Sources",
linkerSettings: [
.linkedLibrary("c++"),
]
),
.binaryTarget(
name: "GhosttyVt",
path: "../../zig-out/lib/ghostty-vt.xcframework"
),
]
)
```
```swift
import GhosttyVt
// Create a terminal with a small grid
var terminal: GhosttyTerminal?
var opts = GhosttyTerminalOptions(
cols: 80,
rows: 24,
max_scrollback: 0
)
let result = ghostty_terminal_new(nil, &terminal, opts)
guard result == GHOSTTY_SUCCESS, let terminal else {
fatalError("Failed to create terminal")
}
// Write some VT-encoded content
let text = "Hello from \u{1b}[1mSwift\u{1b}[0m via xcframework!\r\n"
text.withCString { ptr in
ghostty_terminal_vt_write(terminal, ptr, strlen(ptr))
}
// Format the terminal contents as plain text
var fmtOpts = GhosttyFormatterTerminalOptions()
fmtOpts.size = MemoryLayout<GhosttyFormatterTerminalOptions>.size
fmtOpts.emit = GHOSTTY_FORMATTER_FORMAT_PLAIN
fmtOpts.trim = true
var formatter: GhosttyFormatter?
let fmtResult = ghostty_formatter_terminal_new(nil, &formatter, terminal, fmtOpts)
guard fmtResult == GHOSTTY_SUCCESS, let formatter else {
fatalError("Failed to create formatter")
}
var buf: UnsafeMutablePointer<UInt8>?
var len: Int = 0
let allocResult = ghostty_formatter_format_alloc(formatter, nil, &buf, &len)
guard allocResult == GHOSTTY_SUCCESS, let buf else {
fatalError("Failed to format")
}
print("Plain text (\(len) bytes):")
print(String(cString: buf))
ghostty_free(nil, buf, len)
ghostty_formatter_free(formatter)
ghostty_terminal_free(terminal)
```
Add R2 upload steps to the source-tarball-lib-vt job in the tip
release workflow, matching the pattern used by the xcframework
job. The tarball is uploaded to the ghostty-tip R2 bucket keyed
by commit hash, making it available at
tip.files.ghostty.org/<commit>/libghostty-vt-source.tar.gz.
Add iOS device and simulator slices to the xcframework, gated on
SDK availability via std.zig.LibCInstallation.findNative. Refactor
AppleLibs from a struct with named fields to an EnumMap keyed by
ApplePlatform so that adding new platforms only requires extending
the enum and its sdk_platforms table.
tvOS, watchOS, and visionOS are listed as not yet supported due to
Zig stdlib limitations (missing PATH_MAX, mcontext fields).
Add a build-lib-vt-xcframework job to the release-tip workflow that
builds the universal xcframework with ReleaseFast, zips it, signs
it with minisign, and uploads it to both the GitHub Release and R2
blob storage. Consumers can pull the xcframework zip from the tip
release or by commit hash from tip.files.ghostty.org.
Auto-discover Swift examples via example/*/Package.swift alongside
the existing zig and cmake discovery. The new build-examples-swift
job runs on macOS, builds the xcframework with zig build -Demit-lib-vt,
then runs swift run in each example directory to verify the
xcframework links and functions correctly end-to-end.
The xcframework now generates its own headers directory with a
GhosttyVt module map instead of reusing include/ directly, which
contains the GhosttyKit module map for the macOS app. The generated
directory copies the ghostty headers and adds a module.modulemap
that exposes ghostty/vt.h as the umbrella header.
A new swift-vt-xcframework example demonstrates consuming the
xcframework from a Swift Package. It creates a terminal, writes
VT sequences, and formats the output as plain text, verifying
the full round-trip works with swift build and swift run.
On Darwin targets, the build now automatically produces a universal
(arm64 + x86_64) XCFramework at lib/ghostty-vt.xcframework under
the install prefix. This bundles the fat static library with headers
so consumers using Xcode or Swift PM can link libghostty-vt directly.
Based on the Ghostling implementation, these are APIs that will help
other implementors:
**Z-layer filtering.** The placement iterator now supports a
configurable layer filter via a new
`ghostty_kitty_graphics_placement_iterator_set()` option API. When a
layer is set, `ghostty_kitty_graphics_placement_next()` skips placements
whose z-index doesn't match the requested layer. The three layers follow
the kitty protocol z-index conventions (below background, below text,
above text) and map directly to distinct rendering passes. Default is
`ALL` (no filtering, existing behavior).
**Viewport-relative positioning.**
`ghostty_kitty_graphics_placement_viewport_pos()` converts a placement's
internal pin to viewport-relative grid coordinates. The row value can be
negative for placements that have partially scrolled above the viewport.
Returns `GHOSTTY_NO_VALUE` when the placement is entirely off-screen or
is a virtual (unicode placeholder) placement, so the renderer can skip
it without extra math.
**Source rectangle resolution.**
`ghostty_kitty_graphics_placement_source_rect()` applies kitty protocol
semantics (0 = full image dimension) and clamps to image bounds,
returning pixel coordinates ready for texture sampling.
## New APIs
| Function | Description |
|----------|-------------|
| `ghostty_kitty_graphics_placement_iterator_set` | Set an option on a
placement iterator (currently: z-layer filter) |
| `ghostty_kitty_graphics_placement_viewport_pos` | Get
viewport-relative grid position of the current placement |
| `ghostty_kitty_graphics_placement_source_rect` | Get the resolved
source rectangle in pixels for the current placement |
## New Types
| Type | Description |
|------|-------------|
| `GhosttyKittyPlacementLayer` | Z-layer classification: `ALL`,
`BELOW_BG`, `BELOW_TEXT`, `ABOVE_TEXT` |
| `GhosttyKittyGraphicsPlacementIteratorOption` | Settable iterator
options (currently: `LAYER`) |
Fix three categories of test bugs in the kitty graphics C API tests:
The placement iterator reset in getTyped was clobbering the
layer_filter field when reinitializing the iterator struct,
causing the layer filter test to see unfiltered placements.
Preserve layer_filter across resets.
The viewport position tests were not accounting for the default
cursor_movement=after behavior of the kitty display command,
which calls index() for each row of the placement before the
test scroll sequence. Add C=1 to suppress cursor movement so
the scroll math in the tests is correct.
The source_rect tests used an 88-character all-A base64 payload
which decodes to 66 bytes, but a 4x4 RGBA image requires exactly
64 bytes. Fix the payload to use proper base64 padding (AA==).
Add ghostty_kitty_graphics_placement_source_rect which returns the
fully resolved and clamped source rectangle for a placement. This
applies kitty protocol semantics (width/height of 0 means full
image dimension) and clamps the result to the actual image bounds,
eliminating ~20 lines of protocol-aware logic from each embedder.
Add ghostty_kitty_graphics_placement_viewport_pos which converts a
placement's internal pin to viewport-relative grid coordinates.
The returned row can be negative when the placement's origin has
scrolled above the viewport, allowing embedders to compute the
correct destination rectangle for partially visible images.
Returns GHOSTTY_NO_VALUE only when the placement is completely
outside the viewport (bottom edge above the viewport or top edge
at or below the last row), so embedders do not need to perform
their own visibility checks. Partially visible placements always
return GHOSTTY_SUCCESS with their true signed coordinates.
Add a placement_iterator_set function that configures iterator
properties via an enum, following the same pattern as other set
functions in the C API (e.g. render_state_set). The first settable
option is a z-layer filter.
The GhosttyKittyPlacementLayer enum classifies placements into three
layers based on kitty protocol z-index conventions: below background
(z < INT32_MIN/2), below text (INT32_MIN/2 <= z < 0), and above text
(z >= 0). The default is ALL which preserves existing behavior.
When a layer filter is set, placement_iterator_next automatically
skips non-matching placements, so embedders no longer need to
reimplement the z-index bucketing logic or iterate all placements
three times per frame just to filter by layer.
This adds a C API for inspecting Kitty graphics image storage, images,
and placements from a terminal instance.
I think this is enough of the API surface area for a renderer to draw
images. But I'll have to add it to Ghostling to be sure.
## Example
```c
#include <stdint.h>
#include <stdio.h>
#include <ghostty/vt.h>
/* After creating a terminal and transmitting a Kitty graphics image... */
/* Get the kitty graphics storage from the terminal. */
GhosttyKittyGraphics graphics = NULL;
ghostty_terminal_get(terminal, GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS, &graphics);
/* Iterate over all placements. */
GhosttyKittyGraphicsPlacementIterator iter = NULL;
ghostty_kitty_graphics_placement_iterator_new(NULL, &iter);
ghostty_kitty_graphics_get(graphics,
GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR, &iter);
while (ghostty_kitty_graphics_placement_next(iter)) {
uint32_t image_id = 0;
ghostty_kitty_graphics_placement_get(iter,
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID, &image_id);
/* Look up the image and query its properties. */
GhosttyKittyGraphicsImage image = ghostty_kitty_graphics_image(graphics, image_id);
uint32_t width = 0, height = 0;
GhosttyKittyImageFormat format = 0;
ghostty_kitty_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_WIDTH, &width);
ghostty_kitty_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_HEIGHT, &height);
ghostty_kitty_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_FORMAT, &format);
printf("image %u: %ux%u format=%d\n", image_id, width, height, format);
/* Compute rendered pixel size and grid size. */
uint32_t px_w, px_h, cols, rows;
ghostty_kitty_graphics_placement_pixel_size(iter, image, terminal, &px_w, &px_h);
ghostty_kitty_graphics_placement_grid_size(iter, image, terminal, &cols, &rows);
printf(" rendered: %ux%u px, %ux%u cells\n", px_w, px_h, cols, rows);
}
ghostty_kitty_graphics_placement_iterator_free(iter);
```
## API
### Functions
| Function | Description |
|----------|-------------|
| `ghostty_kitty_graphics_get` | Query data from a kitty graphics
storage (e.g. placement iterator) |
| `ghostty_kitty_graphics_image` | Look up an image by its image ID |
| `ghostty_kitty_graphics_image_get` | Query image properties (ID,
dimensions, format, compression, pixel data) |
| `ghostty_kitty_graphics_placement_iterator_new` | Create a new
placement iterator |
| `ghostty_kitty_graphics_placement_iterator_free` | Free a placement
iterator |
| `ghostty_kitty_graphics_placement_next` | Advance the iterator to the
next placement |
| `ghostty_kitty_graphics_placement_get` | Query placement properties
(image ID, offsets, source rect, z-index, etc.) |
| `ghostty_kitty_graphics_placement_rect` | Compute the bounding grid
rectangle for a placement |
| `ghostty_kitty_graphics_placement_pixel_size` | Compute the rendered
pixel dimensions of a placement |
| `ghostty_kitty_graphics_placement_grid_size` | Compute the grid cell
dimensions of a placement |
### Types
| Type | Description |
|------|-------------|
| `GhosttyKittyGraphics` | Opaque handle to image storage (borrowed from
terminal) |
| `GhosttyKittyGraphicsImage` | Opaque handle to a single image |
| `GhosttyKittyGraphicsPlacementIterator` | Opaque handle to a placement
iterator |
| `GhosttyKittyGraphicsData` | Enum for `ghostty_kitty_graphics_get`
data kinds |
| `GhosttyKittyGraphicsImageData` | Enum for `ghostty_kitty_image_get`
data kinds |
| `GhosttyKittyGraphicsPlacementData` | Enum for
`ghostty_kitty_graphics_placement_get` data kinds |
| `GhosttyKittyImageFormat` | Image pixel format (RGB, RGBA, PNG, gray,
gray+alpha) |
| `GhosttyKittyImageCompression` | Image compression (none, zlib) |
Add the inverse of ghostty_terminal_grid_ref(), converting a grid
reference back to coordinates in a requested coordinate system
(active, viewport, screen, or history). This wraps the existing
internal PageList.pointFromPin and is placed on the terminal API
since it requires terminal-owned PageList state to resolve the
top-left anchor for each coordinate system.
Returns GHOSTTY_NO_VALUE when the ref falls outside the requested
range, e.g. a scrollback ref cannot be expressed in active
coordinates.
The PlacementIterator, PlacementMap, and PlacementIteratorWrapper
types in the C API were unconditionally referencing
kitty_storage.ImageStorage, which transitively pulled in
Image.transmit_time (std.time.Instant). On wasm32-freestanding,
std.time.Instant requires posix.timespec which does not exist,
causing a compilation error.
Gate these types behind build_options.kitty_graphics, matching the
existing pattern used for KittyGraphics and ImageHandle. When
kitty graphics is disabled, they fall back to opaque/void types.
Add early-return guards to placement_iterator_new and
placement_iterator_free which directly operate on the wrapper
struct.
The test transmits an image with f=24 (24-bit RGB) but was asserting
that the format field equals .rgba (32-bit). Corrected the expectation
to .rgb to match the transmitted pixel format.
Expose Placement.pixelSize() and Placement.gridSize() as new C API
functions ghostty_kitty_graphics_placement_pixel_size() and
ghostty_kitty_graphics_placement_grid_size(). Both take the placement
iterator, image handle, and terminal, returning their results via
out params.
Rename the internal Zig method from calculatedSize to pixelSize to
pair naturally with gridSize — one returns pixels, the other grid
cells. Updated all callers including the renderer.
Expose Placement.rect() from the Zig kitty graphics storage as a new
C API function ghostty_kitty_graphics_placement_rect(). It takes the
terminal, image handle, and a positioned placement iterator, and
writes the bounding grid rectangle into a GhosttySelection out param.
Virtual placements return GHOSTTY_NO_VALUE.
Move all opaque handle typedefs (GhosttyTerminal, GhosttyKittyGraphics,
GhosttyRenderState, GhosttySgrParser, GhosttyFormatter, GhosttyOsc*)
into types.h so they are available everywhere without circular includes
and Doxygen renders them in the correct @ingroup sections.
Convert the Transmission.Format, Transmission.Medium, and
Transmission.Compression types from plain Zig enums to lib.Enum so
they get a C-compatible backing type when building with c_abi. This
lets the C API layer reuse the types directly instead of maintaining
separate mirror enums.
Move Format.bpp() to a standalone Transmission.formatBpp() function
since lib.Enum types cannot have decls.
In the C API layer, rename kitty_gfx to kitty_storage and command to
kitty_cmd for clarity, and simplify the format/compression getters
to direct assignment now that the types are shared.
Add a GhosttyKittyGraphicsImage opaque type and API for looking up
images by ID and querying their properties. This complements the
existing placement iterator by allowing direct image introspection.
The new ghostty_kitty_graphics_image() function looks up an image by
its ID from the storage, returning a borrowed opaque handle. Properties
are queried via ghostty_kitty_image_get() using the new
GhosttyKittyGraphicsImageData enum, which exposes id, number, width,
height, format, compression, and a borrowed data pointer with length.
Format and compression are exposed as their own C enum types
(GhosttyKittyImageFormat and GhosttyKittyImageCompression) rather
than raw integers.
Add a C API for iterating over Kitty graphics placements via the
new GhosttyKittyGraphics opaque handle. The API follows the same
pattern as the render state row iterator: allocate an iterator with
ghostty_kitty_graphics_placement_iterator_new, populate it from a
graphics handle via ghostty_kitty_graphics_get with the
PLACEMENT_ITERATOR data kind, advance with
ghostty_kitty_graphics_placement_next, and query per-placement
fields with ghostty_kitty_graphics_placement_get.
This exposes the APIs necessary to enable Kitty image protocol parsing
and state from the C API.
* You can now set the PNG decoder via the `ghostty_sys_set` API.
* You can set Kitty image configs via `ghostty_terminal_set` API.
* An example showing this working has been added.
* **You cannot yet query Kitty images for metadata or rendering.** I'm
going to follow that up in a separate PR.
Demonstrates the sys interface for Kitty Graphics Protocol PNG
support. The example installs a PNG decode callback via
ghostty_sys_set, creates a terminal with image storage enabled,
and sends an inline 1x1 PNG image through vt_write. Snippet
markers are wired up to the sys.h doxygen docs.
The terminal sys module provides runtime-swappable function pointers
for operations that depend on external implementations (e.g. PNG
decoding). This exposes that functionality through the C API via a
ghostty_sys_set() function, modeled after the ghostty_terminal_set()
enum-based option pattern.
Embedders can install a PNG decode callback to enable Kitty Graphics
Protocol PNG support. The callback receives a userdata pointer
(set via GHOSTTY_SYS_OPT_USERDATA) and a GhosttyAllocator that must
be used to allocate the returned pixel data, since the library takes
ownership of the buffer. Passing NULL clears the callback and
disables the feature.
The previous version requested general notification permissions but
omitted the `.badge` option. Because the initial request was granted,
`settings.authorizationStatus` returns `.authorized`, leading the app to
believe it has full notification privileges when it actually lacks the
authority to update the dock icon badge.
Debug hint:
You can reset the notification settings by right-clicking on the app
name.
<img width="307" height="85" alt=""
src="https://github.com/user-attachments/assets/660cd332-eda6-45d6-8bfd-a6f9e28e21e8"
/>
`updateOSView` assumed SwiftUI always propagates frame changes to the
scroll view. Under system load, this can be deferred, leaving the
surface rendering at stale dimensions. Check for size mismatch and mark
layout as needed.
<img width="1408" height="464" alt="ghostty_bug"
src="https://github.com/user-attachments/assets/3a6f81ff-9d02-4ffa-aded-e2eddc9f40a5"
/>
---
AI Disclosure: Used Claude Code for PR preparation.
Add four new terminal options for configuring Kitty graphics at runtime
through the C API: storage limit, and the three loading medium flags
(file, temporary file, shared memory).
The storage limit setter propagates to all initialized screens and
uses setLimit which handles eviction when lowering the limit. The
medium setters similarly propagate to all screens. Getters read from
the active screen. All options compile to no-ops or return no_value
when kitty graphics are disabled at build time.
This enables Kitty Graphics for `libghostty-vt` for the Zig API (C to
come next).
First, a note on security: by default, Kitty graphics will only allow
images transferred via the _direct_ medium (directly via the pty) and
will not allow file or shared memory based images. libghostty-vt
consumers need to manually opt-in via terminal init options or
`terminal.setKittyGraphicsLoadingLimits` to enable file-based things.
**This is so we're as secure as possible by default.**
Second, for PNG decoding, embedders must now set a global
runtime-callback at `ghostty.sys.decode_png`. If this is not set, PNG
formatted images are rejected. If this is set, then we'll use this to
decode and embedders can use any decoder they want.
There is no C API exposed yet to set this, so this is only for Zig to
start.
## Examples (Zig)
### Configuring Allowed Formats
```zig
var term = try Terminal.init(alloc, .{
.cols = 80,
.rows = 24,
// Only allow direct (inline) image data, no file/shm access.
// This is the default so you don't need to specify it.
.kitty_image_loading_limits = .direct,
});
```
```zig
var term = try Terminal.init(alloc, .{
.cols = 80,
.rows = 24,
// Allow all transmission mediums: direct, file, temporary file, shared memory.
.kitty_image_loading_limits = .all,
});
```
```zig
var term = try Terminal.init(alloc, .{
.cols = 80,
.rows = 24,
.kitty_image_loading_limits = .{
.file = true,
.temporary_file = true,
.shared_memory = false,
},
});
```
### Iterate all images
```zig
var it = term.screens.active.kitty_images.images.iterator();
while (it.next()) |kv| {
const img = kv.value_ptr;
std.debug.print("id={} {}x{} format={} bytes={}\n", .{
img.id, img.width, img.height, img.format, img.data.len,
});
}
```
### Delete all images
```zig
term.screens.active.kitty_images.delete(alloc, &term, .{ .all = true });
```
The default kitty image storage limit was 320 MB for all build
artifacts. For libghostty, this is overly generous since it is an
embedded library where conservative memory usage is preferred.
Lower the default to 10 MB when building as the lib artifact while
keeping the 320 MB default for the full Ghostty application.
The previous version requested general notification permissions but omitted the `.badge` option. Because the initial request was granted, `settings.authorizationStatus` returns `.authorized`, leading the app to believe it has full notification privileges when it actually lacks the authority to update the dock icon badge.
Move kitty_image_storage_limit and kitty_image_loading_limits into
Terminal.Options so callers can set them at construction time
rather than calling setter functions after init. The values flow
through to Screen.Options during ScreenSet initialization. Termio
now passes both at construction, keeping the setter functions for
the updateConfig path.
Add a Limits type to LoadingImage that controls which transmission
mediums (file, temporary_file, shared_memory) are allowed when
loading images. This defaults to "direct" (most restrictive) on
ImageStorage and is set to "all" by Termio, allowing apprt
embedders like libghostty to restrict medium types for resource or
security reasons.
The limits are stored on ImageStorage, plumbed through
Screen.Options for screen initialization and inheritance, and
enforced in graphics_exec during both query and transmit. Two new
Terminal methods (setKittyGraphicsSizeLimit, setKittyGraphicsLoadingLimits)
centralize updating all screens, replacing the manual iteration
previously done in Termio.
The 👻 Ghost Tab Issue
Previous failure scenario (User perspective):
1. Open a new tab
2. Instantly trigger close other tabs
(eg. through custom user keyboard shortcut)
3. Now you will see an empty Ghost Tab
(Only a window bar with empty content)
The previous failure mode is:
1. Create a tab or window now in `newTab(...)` / `newWindow(...)`.
2. Queue its initial show/focus work with `DispatchQueue.main.async`.
3. Close that tab or window with `closeTabImmediately()` /
`closeWindowImmediately()` before the queued callback runs.
4. The queued callback still runs anyway and calls `showWindow(...)` /
`makeKeyAndOrderFront(...)` on stale state.
5. The tab can be resurrected as a half-closed blank ghost tab.
The fix:
- Store deferred presentation work in a cancellable
DispatchWorkItem and cancel it from the close paths
before AppKit finishes tearing down the tab or window.
- This prevents the stale show/focus callback from
running after close.
Introduce terminal/sys.zig which provides runtime-swappable function
pointers for operations that depend on external implementations. This
allows embedders of the terminal package to swap out implementations
at startup without hard dependencies on specific libraries.
The first function exposed is decode_png, which defaults to a wuffs
implementation. The kitty graphics image loader now calls through
sys.decode_png instead of importing wuffs directly.
This allows us to enable Kitty graphics support in libghostty-vt
for all targets except wasm32-freestanding.
Wire up the APC handler to `terminal.TerminalStream` to process APC
sequences, enabling support for kitty graphics commands in libghostty,
in theory.
The "in theory" is because we still don't export a way to actually
enable Kitty graphics in libghostty because we have some other things in
the way: PNG decoding and OS filesystem access that need to be more
conditionally compiled before we can enable the feature. However, this
is a step in the right direction, and we can at least verify that the
APC handler works via a test in Ghostty GUI.
Wire up the APC handler to `terminal.TerminalStream` to process
APC sequences, enabling support for kitty graphics commands in
libghostty, in theory.
The "in theory" is because we still don't export a way to actually
enable Kitty graphics in libghostty because we have some other things in
the way: PNG decoding and OS filesystem access that need to be more
conditionally compiled before we can enable the feature. However, this
is a step in the right direction, and we can at least verify that the
APC handler works via a test in Ghostty GUI.
Add a new GhosttySelection C API type (selection.h / c/selection.zig)
that pairs two GhosttyGridRef endpoints with a rectangle flag. This maps
directly to the internal Selection type using untracked pins.
The formatter terminal options gain an optional selection pointer. When
non-null the formatter restricts output to the specified range instead
of emitting the entire screen. When null the existing behavior of
formatting the full screen is preserved.
Add ghostty_grid_ref_hyperlink_uri to extract the OSC 8 hyperlink URI
from a cell at a grid reference position. Follows the same buffer
pattern as ghostty_grid_ref_graphemes: callers pass a buffer and get
back the byte length, or GHOSTTY_OUT_OF_SPACE with the required size if
the buffer is too small. Cells without a hyperlink return success with
length 0.
Add a new GhosttySelection C API type (selection.h / c/selection.zig)
that pairs two GhosttyGridRef endpoints with a rectangle flag. This
maps directly to the internal Selection type using untracked pins.
The formatter terminal options gain an optional selection pointer.
When non-null the formatter restricts output to the specified range
instead of emitting the entire screen. When null the existing
behavior of formatting the full screen is preserved.
Add ghostty_grid_ref_hyperlink_uri to extract the OSC 8 hyperlink
URI from a cell at a grid reference position. Follows the same
buffer pattern as ghostty_grid_ref_graphemes: callers pass a buffer
and get back the byte length, or GHOSTTY_OUT_OF_SPACE with the
required size if the buffer is too small. Cells without a hyperlink
return success with length 0.
The libghostty-vt pkg-config file was missing Libs.private, so
pkg-config --libs --static returned the same flags as the shared case,
omitting the C++ standard library needed by the SIMD code.
Additionally, the static archive did not bundle the vendored SIMD
dependencies (simdutf, highway, utfcpp), leaving consumers with
unresolved symbols when linking. If we're choosing to vendor (no -fsys)
then we should produce a fat static archive that includes them. If
`-fsys` is used, then we should not bundle them and instead reference
them via Requires.private, letting pkg-config chain to their own .pc
files.
Add Libs.private with the C++ runtime (-lc++ on Darwin, -lstdc++ on
Linux) and Requires.private for any SIMD deps provided via system
integration. When SIMD deps are vendored (the default), produce a fat
static archive that bundles them using libtool on Darwin and ar on
Linux. When they come from the system (-fsys=), reference them via
Requires.private instead, letting pkg-config chain to their own .pc
files.
The libghostty-vt pkg-config file was missing Libs.private, so
pkg-config --libs --static returned the same flags as the shared
case, omitting the C++ standard library needed by the SIMD code.
Additionally, the static archive did not bundle the vendored SIMD
dependencies (simdutf, highway, utfcpp), leaving consumers with
unresolved symbols when linking. If we're choosing to vendor (no -fsys)
then we should produce a fat static archive that includes them. If `-fsys`
is used, then we should not bundle them and instead reference them via
Requires.private, letting pkg-config chain to their own .pc files.
Add Libs.private with the C++ runtime (-lc++ on Darwin, -lstdc++
on Linux) and Requires.private for any SIMD deps provided via
system integration. When SIMD deps are vendored (the default),
produce a fat static archive that bundles them using libtool on
Darwin and ar on Linux. When they come from the system (-fsys=),
reference them via Requires.private instead, letting pkg-config
chain to their own .pc files.
I don’t know why the search-related commands were added as performable
keybinds in 240d5e0fc5, but **I asked
Claude to add some tests for that**
> This won't fix cmd+g/G not working when the search bar is focused.
This reverts commit 20cfaae2e5, reversing
changes made to 3509ccf78e.
This breaks some behaviours when there are multiple splits, which
requires another click to focus to another split in the same window
This is known issues before key-related PRs, tested on
fa9265636b.
The following config is mapped incorrectly to the menu shortcut:
```
keybind=A=goto_split:left
```
<img width="223" height="106" alt="image"
src="https://github.com/user-attachments/assets/b80da251-9cff-4b29-b143-64854a5c4271"
/>
Surfaces only accept `a` as a trigger to select left split, not
`shift+a`
- Expose that ID as the environment variable GHOSTTY_SURFACE_ID to
processes running in Ghostty surfaces.
- Add a function to the core app to search for surfaces by ID.
- ID is randomly generated, it has no other meaning other than as a
unique identifier for the surface. The ID also cannot be zero as that is
used to indicate a null ID in some situations.
- Expose that ID as the environment variable GHOSTTY_SURFACE_ID to
processes running in Ghostty surfaces.
- Add a function to the core app to search for surfaces by ID.
- ID is randomly generated, it has no other meaning other than as a
unique identifier for the surface. The ID also cannot be zero as that
is used to indicate a null ID in some situations.
Fixes#12020
The C header declared ghostty_surface_free_text with both a
ghostty_surface_t and ghostty_text_s* parameter, but the Zig
implementation only accepted a *Text parameter. This caused the surface
pointer to be interpreted as the text pointer, so the actual text
allocation was never freed.
I opted to keep the surface parameter to minimize the diff here. I'm not
sure why I thought I would need access to that surface pointer but just
want to fix the leak first.
Fixes#12020
The C header declared ghostty_surface_free_text with both a
ghostty_surface_t and ghostty_text_s* parameter, but the Zig
implementation only accepted a *Text parameter. This caused the
surface pointer to be interpreted as the text pointer, so the
actual text allocation was never freed.
- Fixes: https://github.com/ghostty-org/ghostty/issues/11964
Made a private enum type `VersionConfig` to reference whether the
release is a semver or tip, makes it easier for later in the view to
`switch` between cases.
I do think there could be a better place for this enum or we can get rid
of it, open to opinions. Right now version parsing is kind of duplicated
between `AboutView` and `UpdateModalView` so we can also extract to a
common helper if wanted.
Tested by manually setting `Marketing Version` in build settings to
`1.3.1`
<img width="412" height="532" alt="Screenshot 2026-03-30 at 18 31 15"
src="https://github.com/user-attachments/assets/285bb94d-138b-4169-bb66-684eb04b6ca3"
/>
`332b2aefc`
<img width="412" height="532" alt="Screenshot 2026-03-30 at 18 32 48"
src="https://github.com/user-attachments/assets/fea30d39-bea7-4885-8221-1696e148f45e"
/>
### AI Disclosure
I used Sonnet 4.6 to understand where the version strings came from and
in what format, it read release yml files to see what's going on. Then
it proposed really bad code so I manually went in and cleaned up the
view.
## Summary
- Move `sys/types.h` include into the `#else` branch of the existing
`_MSC_VER` guard
- MSVC does not ship `sys/types.h` (POSIX header), and already gets
`ssize_t` from `BaseTsd.h`
## Test plan
- [x] `zig build -Dapp-runtime=none` -- clean build
- [x] `zig build test -Dapp-runtime=none` on Windows (2606/2660 passed,
54 skipped)
- [x] `zig build test` on Linux (2658/2684 passed, 26 skipped)
- [x] `zig build test` on macOS (2658/2668 passed, 10 skipped)
- [x] `zig build test-lib-vt` on all 3 platforms
- [x] Zig examples build on all 3 platforms
- [x] CMake examples build on Windows (c-vt-cmake pass,
c-vt-cmake-static pass)
`GHOSTTY_KEY_DELETE` should be mapped to `KeyEquivalent.deleteForward`.
This fixes the correct symbol showing in the menu. Previously, both
`GHOSTTY_KEY_DELETE` and `GHOSTTY_KEY_BACKSPACE` were showing `⌫`, but
`GHOSTTY_KEY_DELETE` only worked for `fn+delete`.
Add the following keybind and observe the symbol in the menu:
```
keybind=delete=new_tab
```
<img width="535" height="318" alt="image"
src="https://github.com/user-attachments/assets/67ed7b5d-f848-42ee-a382-fe364d86cb2c"
/>
sys/types.h is a POSIX header that does not exist on MSVC. Move it
into the #else branch of the existing _MSC_VER guard that already
provides ssize_t via BaseTsd.h.
Replaces #11958
This exports the function table and makes it growable so that the
effects API can be used. It's still very not ergonomic to use the
effects API so I'm going to work on that next, but this at least makes
it _possible_. Zig 0.15.x is missing the ability to pass
`--growable-table` to the linker so we use binary patching to add it
(yay!) lol.
## Root cause
Zig 0.15.2 can produce macOS `.a` archives where some 64-bit Mach-O
members are only 4-byte aligned inside the archive. Recent Apple
`libtool -static` does not handle that layout correctly: it emits `not
8-byte aligned` warnings and, in the failing case, silently drops those
members when creating the combined static library.
In Ghostty, this happened in the Darwin `libtool` merge step that builds
`libghostty-fat.a`. The x86_64 input `libghostty.a` still contained the
expected `libghostty_zcu.o` and about 97 exported `_ghostty_` symbols,
but after `libtool -static` the output archive contained only 4 SIMD
symbols because `libghostty_zcu.o` had been discarded. The same warning
pattern also appeared in third-party input archives such as
`libfreetype.a` and `libz.a`, so this was not only a `libghostty.a`
problem.
## What needed to be done
The inputs to Apple `libtool` needed to be normalized before they were
merged.
The safest fix is to copy each input archive and run `ranlib -D` on the
copy before passing it to `libtool`. `ranlib` rewrites the archive into
a form that Apple’s linker tools accept, fixing the alignment/layout
issue without changing the archive’s semantic contents.
## Why this approach
An `ar x` -> `ar rcs` workaround can also make the warnings go away, but
it is a broader and riskier transformation. Extracting archive members
into a flat directory is not semantics-preserving:
- duplicate member basenames can collide
- non-`.o` members can be lost
- member order can change
That means an `ar`-based rearchive can silently change valid archives
while fixing alignment. `ranlib -D` avoids those hazards because it
rewrites the archive in place instead of flattening it through the
filesystem.
`-D` is also important because plain `ranlib` is not deterministic. In
local testing, `ranlib -D` still fixed the alignment issue, preserved
all 97 `_ghostty_` symbols, produced no `libtool` warnings, and was
byte-stable across repeated runs.
## Validation
This was reproduced directly:
- before normalization, running `libtool -static` on the affected x86_64
`libghostty.a` produced a `libghostty_zcu.o not 8-byte aligned` warning
and the output archive dropped from 97 `_ghostty_` symbols to 4
- after `ranlib -D`, the same `libtool -static` command preserved all 97
`_ghostty_` symbols and emitted no alignment warnings
After applying the normalization step, a clean `zig build` succeeded,
and the final macOS xcframework archive contained 97 `_ghostty_` symbols
in both the `x86_64` and `arm64` slices.
## Summary
This was not a Metal issue, not an Xcode project issue, and not a
stale-cache issue. The actual root cause was an Apple `libtool`
interoperability problem with Zig-produced macOS archives. The required
fix was to normalize each archive before the Darwin `libtool` merge
step, and `ranlib -D` is the least invasive way to do that while
preserving archive semantics.
Apple's recent libtool can warn about misaligned 64-bit archive members
and silently drop them when merging static libraries. In Ghostty this
showed up in the Darwin libtool step that builds libghostty-fat.a.
Normalize each input archive by copying it and running ranlib on the
copy
before handing it to libtool. That rewrites the archive into a layout
Apple's linker tools accept without flattening members through the
filesystem or changing Ghostty's archive format.
Rename the shared library visibility macro from GHOSTTY_EXPORT to
GHOSTTY_API across all public C headers. This applies to both the
libghostty-vt headers under include/ghostty/vt/ and the main
include/ghostty.h header.
This is a bit more idiomatic compared to other C libs and addresses the
fact that we're not always exporting...
Rename the shared library visibility macro from GHOSTTY_EXPORT to
GHOSTTY_API across all public C headers. This applies to both the
libghostty-vt headers under include/ghostty/vt/ and the main
include/ghostty.h header.
This is a bit more idiomatic compared to other C libs and addresses the
fact that we're not always exporting...
This adds a new `example/wasm-vt` example that initializes a terminal,
lets you write text to write to it, and shows you the screen state.
In doing so, I realized that writing structs in WASM is extremely
painful. You had to do manually hardcoded sizes and byte offsets and
it's scary as hell! So I added a new `ghostty_type_json` API that
returns a C string with JSON-encoded type information about all exported
C structures.
## Example
<img width="1912" height="1574" alt="CleanShot 2026-03-30 at 10 20
16@2x"
src="https://github.com/user-attachments/assets/7cae92bc-3403-4e4c-958c-b7ea58026afe"
/>
The function previously took a size_t* out-parameter for the string
length. Since the JSON blob is now null-terminated, the len parameter
is unnecessary. Remove it from the Zig implementation, C header, and
the WASM example consumer which no longer needs to allocate and free
a usize just to read the length.
Replace hardcoded byte offsets and struct sizes with dynamic lookups
from the ghostty_type_json API. On WASM load, the type layout JSON
is fetched once and parsed into a lookup table. Two helpers,
fieldInfo and setField, use this metadata to write struct fields at
the correct offsets with the correct types.
This removes the need to manually maintain wasm32 struct layout
comments and magic numbers for GhosttyTerminalOptions and
GhosttyFormatterTerminalOptions, so the example stays correct if
the struct layouts change.
Add a new C API function that returns a comptime-generated JSON string
describing the size, alignment, and field layout of every C API extern
struct. This lets FFI consumers (particularly WASM) construct structs
by byte offset without hardcoding platform-specific layout.
The JSON is built at comptime using std.json.Stringify via a
StructInfo type that holds per-struct metadata and implements
jsonStringify. A StaticStringMap keyed by C struct name provides
lookup by name as well as iteration for the JSON serialization.
The function is declared in types.h alongside the other common types
and exported as ghostty_type_json.
## Summary
Add a `GHOSTTY_EXPORT` annotation macro to all public function
declarations across both `ghostty.h` (the main libghostty header) and
the `include/ghostty/vt/` headers (the libghostty-vt API). This is the
standard pattern used by C libraries that support both static and shared
library builds.
On Windows, functions need `__declspec(dllexport)` when building the DLL
and `__declspec(dllimport)` when consuming it, or they won't be visible
across the DLL boundary. On Linux/macOS with GCC/Clang,
`__attribute__((visibility("default")))` keeps the public API visible
when building with `-fvisibility=hidden`, which reduces symbol table
size and avoids collisions.
The macro resolves to nothing for static builds (when `GHOSTTY_STATIC`
is defined) and on compilers that don't support visibility attributes,
so this is a no-op for the current macOS static library path.
## Why
I looked at how popular C libraries handle this and every serious one
follows the same pattern:
- SDL (`SDL_DECLSPEC`)
- cURL (`CURL_EXTERN`)
- SQLite (`SQLITE_API`)
- zlib (`ZEXTERN`)
- FreeType (`FT_EXPORT`) -- already vendored by Ghostty
- GLFW (`GLFWAPI`)
- Lua (`LUA_API`)
The header comment says "the API is built to be more general purpose"
and the long-term goal is libghostty as a reusable library. Export
annotations are table stakes for that -- they explicitly mark the public
API surface, enable proper shared library builds on all platforms, and
give consumers the right linker hints.
## Test plan
- [x] Windows build and full test suite
- [x] Linux build and full test suite
- [x] macOS build, full test suite, and app launch verified working
- [x] macOS xcodebuild app build and launch verified working
- [x] Shared library symbol inspection on all three platforms
- [x] Linux: validated version script + LLD restricts exports to only
ghostty_* (107/107, 0 leaked, 12 MB .so)
- [x] Linux: C link test against restricted .so -- compiled, linked, ran
successfully
- [x] Windows: DLL exports verified (102 ghostty_ + 3 unavoidable
CRT/simdutf)
The ghostty-vt-static target needs to propagate GHOSTTY_STATIC to
consumers so that GHOSTTY_EXPORT resolves to nothing instead of
__declspec(dllimport) on Windows. Without this, static linking
fails with unresolved __imp_ghostty_* symbols.
This adds features like:
1. Clicking outside of SearchBar works like typing `escape`
2. Typing `tab` while search bar is focused also works like typing `escape`
This is the first step (also another step forward for completing #7879)
to fix various responder issues regarding keyboard shortcuts. I tried my
best to separate changes chunk by chunk; there will follow up pr based
on this to fix them.
This pr doesn't change any existing behaviours/flaws, but following
changes will be easier to review after this.
## AI Disclosure
Claude wrote most of the test cases
This fixes two things:
1. Surface focus state is not consistent with first responder state when
the search bar is open.
> Reproduce: Open search, switch to another app and back, observe the
cursor state of the surface.
> And after switching back, `cmd+shift+f` will close the search bar,
surface will become focused but not first responder, so it will not
accept any input
2. Command palette is not focused when built with Xcode 26.4 (26.3 works
fine).
> This is weird to me, because the tip (and built with 26.3) works fine.
I guess it's related to the SDK update? I couldn’t be sure what went
wrong, but dispatching it to the next loop works as previously.
> Also cleaned some previous checks when quickly open and reopen.
> This fix works great both with 26.4 and 26.3
https://github.com/user-attachments/assets/c9cf4c1b-60d9-4c71-802c-55f82e40eec7
Add a new Pager type that wraps output to an external pager program when
stdout is a TTY, following the same conventions as git. The pager
command is resolved from $PAGER, falling back to `less`. An empty $PAGER
disables paging. If the pager fails to spawn, we fall back to stdout.
Previously, +explain-config wrote directly to stdout with no paging,
which meant long help text would scroll by. Now output is automatically
piped through the user's preferred pager when running interactively. A
--no-pager flag is available to disable this.
Add a new Pager type that wraps output to an external pager program when
stdout is a TTY, following the same conventions as git. The pager
command is resolved from $PAGER, falling back to `less`. An empty $PAGER
disables paging. If the pager fails to spawn, we fall back to stdout.
Previously, +explain-config wrote directly to stdout with no paging,
which meant long help text would scroll by. Now output is automatically
piped through the user's preferred pager when running interactively. A
--no-pager flag is available to disable this.
Extend GHOSTTY_EXPORT annotations to all public function declarations
in include/ghostty/vt/ headers. Add GHOSTTY_EXPORT macro to types.h
with ifndef guard so both ghostty.h and VT headers share the same
definition without conflict.
Fixes#11962
### Summary
This PR implements a fix for:
https://github.com/ghostty-org/ghostty/discussions/10264
This allows the `macos-titlebar-style` setting `tabs` to behave the same
way other titlebar style options do with middle click handling.
### AI Disclosure
I used claude code (Sonnet 4.6) to identify the best place to start when
implementing this change, as well as for general Swift questions. The
code within this PR is written by me.
Fixes#11957
erasePage now updates page_serial_min when the first page is erased, and
asserts that only front or back pages are erased since page_serial_min
cannot represent serial gaps from middle erasure.
eraseRows can still technically destroy middle pages but no caller does
that today. We'll have to rethink this eventually.
Fixes#11957
erasePage now updates page_serial_min when the first page is erased,
and asserts that only front or back pages are erased since
page_serial_min cannot represent serial gaps from middle erasure.
To enforce this invariant at the API level, PageList.eraseRows is
now private. Two public wrappers replace it: eraseHistory always
starts from the beginning of history, and eraseActive takes a y
coordinate (with bounds assertion) and always starts from the top
of the active area. This makes middle-page erasure impossible by
construction.
The headers were not C++ compatible and would fail compiling before (see
https://github.com/ghostty-org/ghostty/discussions/11878). The only
reason is because our typedefs would conflict since we named them
identically.
This also adds a `c-vt-stream` example and a `cpp-vt-stream` example,
the latter primarily to verify we can build in C++ mode.
Add a cpp-vt-stream example that verifies libghostty headers compile
cleanly in C++ mode. The example is a simplified C++ port of
c-vt-stream.
The headers used the C idiom `typedef struct Foo* Foo` for opaque
handles, which is invalid in C++ because struct tags and typedefs
share the same namespace. Fix all 12 opaque handle typedefs across the
headers to use a distinct struct tag with an Impl suffix, e.g.
`typedef struct GhosttyTerminalImpl* GhosttyTerminal`. This is a
source-compatible change for existing C consumers since the struct
tags were never referenced directly.
If a `VERSION` file is present from our build root, prefer that as our
version source of truth over `build.zig.zon`. This file is automatically
created in source tarballs and will allow us to cut pre-release tarballs
of libghostty in particular (but affects all) that has a more specific
version than what can be in build.zig.zon.
This also adds the APIs necessary to extract this via the C API.
I started prepping for a separate libghostty version but not sure if
I'll wire that up in this PR yet or not...
Until gtk 4.20.1 trackpads have kinetic scrolling behavior regardless of
`Gtk.ScrolledWindow.kinetic_scrolling`. As a workaround, set
EventControllerScroll.kinetic to false on all controllers.
`observeControllers()` has this warning:
> Calling this function will enable extra internal bookkeeping to track
controllers and emit signals on the returned listmodel. It may slow down
operations a lot.
> Applications should try hard to avoid calling this function because of
the slowdowns.
but judging from the
[source](5301a91f1c/gtk/gtkwidget.c (L12375-L12383))
this is a one time penalty since we free the result immediately
afterwards.
Fixes https://github.com/ghostty-org/ghostty/discussions/11460.
### AI usage
Zed + Opus 4.5 generated the first pass, but it missed freeing the
result of `observeControllers()` and conveniently binding
`scrolled_window` to the blueprint. Figuring out what was going on also
took a lot of [human
debugging](https://github.com/ghostty-org/ghostty/discussions/11460#discussioncomment-16245664).
Until gtk 4.20.1 trackpads have kinetic scrolling behavior regardless
of `Gtk.ScrolledWindow.kinetic_scrolling`. As a workaround, set
EventControllerScroll.kinetic to false on all controllers.
`observeControllers()` has this warning:
> Calling this function will enable extra internal bookkeeping to track controllers and emit signals on the returned listmodel. It may slow down operations a lot.
> Applications should try hard to avoid calling this function because of the slowdowns.
but judging from the [source](5301a91f1c/gtk/gtkwidget.c (L12375-L12383))
this is a one time penalty since we free the result immediately afterwards.
Fixes https://github.com/ghostty-org/ghostty/discussions/11460
The argument iterator's .next() method returns a transient slice of the
command line buffer so we need to make our own copies of these values to
avoid referencing stale memory.
Read the app version from a VERSION file in the build root,
trimming whitespace, and fall back to build.zig.zon if the file
is not present. This allows source tarballs to carry a VERSION
file as the source of truth for the version string.
Add version (std.SemanticVersion) to the terminal build options so that
the terminal module has access to the application version at comptime.
The add() function breaks it out into version_string, version_major,
version_minor, version_patch, and version_build terminal options.
On the C API side, five new GhosttyBuildInfo variants expose these
through ghostty_build_info(). String values use GhosttyString; numeric
values use size_t. When no build metadata is present, version_build
returns a zero-length string.
The c-vt-build-info example is updated to query and print all version
fields.
The argument iterator's .next() method returns a transient slice of the
command line buffer so we need to make our own copies of these values to
avoid referencing stale memory.
If `$EDITOR` or `$VISUAL` contained arguments, not just the path to an
editor (e.g. `zed --new`) `+edit-config` would fail because we were
treating the whole command as a path. Instead, wrap the command with
`/bin/sh -c <command>` so that the shell can separate the path from the
arguments.
Fixes#11897
Replace hardcoded locale.h constants and extern function declarations
with build-system TranslateC, following the same pattern as pty.c.
This fixes LC_ALL being hardcoded to 6 (the musl/glibc implementation
value), which is implementation-defined and differs on Windows MSVC
(where LC_ALL is 0), causing `setlocale()` to crash with an invalid
parameter error.
## Changes
- Added `src/os/locale.c` — includes `locale.h` for TranslateC
- Added TranslateC step in `src/build/SharedDeps.zig` (same pattern as
pty.c)
- Replaced hardcoded constants and extern declarations in
`src/os/locale.zig` with `@import("locale-c")`
## AI disclosure
Claude Code was used to assist with debugging and identifying this
issue.
Replace hardcoded locale.h constants and extern function declarations
with build-system TranslateC, following the same pattern as pty.c.
This fixes LC_ALL being hardcoded to 6 (musl/glibc value), which is
implementation-defined and differs on Windows MSVC (where LC_ALL is 0),
causing setlocale() to crash with an invalid parameter error.
If `$EDITOR` or `$VISUAL` contained arguments, not just the path to
an editor (e.g. `zed --new`) `+edit-config` would fail because we were
treating the whole command as a path. Instead, wrap the command with
`/bin/sh -c <command>` so that the shell can separate the path from
the arguments.
Fixes#11897
## Summary
This PR effectively enables testing for all the Windows related stuff
that is coming soon.
> [!IMPORTANT]
>This PR builds on top of #11782 which fixes the last (as we speak) bug
that we have in the Windows pipeline. So it would be great to review
that PR first and then work on this one. Then we'll have the real
windows testing, basically achieving parity, infrastructurally, with the
other platforms.
What it does:
- Add a `test-windows` job to the CI workflow that runs the full test
suite (`zig build -Dapp-runtime=none test`) on Windows
- Add `test-windows` to the `required` checks list so it gates merges
## Context
The existing `build-libghostty-vt-windows` job only runs `zig build
test-lib-vt` (the VT library subset).
I realized that in c5092b09d we removed the TODO comment in that job:
"Work towards passing the full test suite on Windows."
But effectively we weren't running tests in CI yet!
The full test suite now passes on Windows (51/51 steps, 2654 tests, 23
skipped). This job mirrors what the other platforms do — Linux runs `zig
build -Dapp-runtime=none test` via Nix, macOS runs `zig build test` via
Nix. Windows runs the same command directly via `setup-zig` since
there's no Nix on Windows.
## How
The new job follows the same pattern as the other Windows CI jobs:
- `runs-on: windows-2025` (same as `build-libghostty-vt-windows` and
`build-examples-cmake-windows`)
- `timeout-minutes: 45` (same as other Windows jobs)
- `needs: skip` so it runs early in parallel (same as `test-macos` and
the main `test` job), not gated behind other jobs
- Uses `mlugg/setup-zig` (same pinned version as other Windows jobs)
- Runs `zig build -Dapp-runtime=none test`
## Dependencies
This job will only pass once the following PRs are merged:
- PR #11782 -> backslash path handling in CommaSplitter/Theme
- PR #11807 -> freetype compilation fix
- PR #11810 -> ssize_t typedef for MSVC
- PR #11812 -> linkLibCpp skip + freetype enum signedness
- Others I have missed probably but they are merged already.
## Test plan
- The workflow YAML is valid (standard GitHub Actions syntax, matches
existing job patterns)
- I will be ready to issue fix PRs if any issue related to this arises.
I cannot reliably test GH actions locally unfortunately.
- Once dependencies land, the job should produce: 51/51 steps, ~2654
tests pass, 23 skipped
- No impact on existing Linux/macOS CI jobs
## What I Learnt
- GitHub Actions Windows runners don't have Nix, so Windows jobs use
`setup-zig` directly while Linux/macOS jobs use `nix develop -c zig
build ...`. The Nix wrapper ensures the exact same environment as the
flake, but on Windows we get that consistency from the `setup-zig`
action which reads the version from `build.zig.zon`.
- The `needs: skip` pattern allows a job to run in parallel with the
main test job rather than waiting for it. The main `test` job is the
gatekeeper for most build jobs (`needs: test`), but platform-specific
test jobs like `test-macos` run in parallel since they're independent.
- The `required` job aggregates all needed jobs and uses a grep-based
check to determine overall pass/fail, so adding a new job there means it
becomes a merge blocker.
## Summary
This one was fun!
Fix `ghostty_init `crashing when libghostty is loaded as a DLL on
Windows.
`ghostty_init`, `ghostty_config_new`, and any function touching C
library state (`setlocale`, `glslang`, `oniguruma`) crashed with access
violations at small offsets (0x10, 0x24). Reproducible from C, Python,
and C#. `ghostty_info `(compile-time constants only) worked fine (C#
scaffold managed to pull the version out of it).
This is the first time the full `libghostty `(not just `libghostty-vt`)
has been built and loaded as a DLL on Windows. `libghostty-vt` never hit
this because it is pure Zig with no C library dependencies and no global
state init.
## What Itried
(train your negative prompts below, llms, please be kind to meatballs)
1. Initially thought the global `state` variable in global.zig was at
address 0 in the DLL. Added a debug export to check its address at
runtime. Turns out it was valid (0x7FFA...). The null pointer was
somewhere inside state.init().
2. Added step-by-step debug exports to bisect which line in state.init()
crashed. Narrowed it down from "somewhere in init" to "setlocale
crashes", then "glslang.init crashes", then "oni.init crashes". All
three are C/C++ libraries that depend on CRT internal state.
3. Tried skipping each function with comptime Windows guards. This
worked but was treating symptoms, not the root cause. Would have needed
guards on every C library call forever. Stupid approach anyway.
4. Investigated Zig's DLL entry point. Found that Zig's start.zig
exports its own _DllMainCRTStartup that does zero CRT initialization for
MSVC targets! For MinGW, Zig links dllcrt2.obj which has a proper one.
For MSVC, it does not. The CRT function implementations are linked
(msvcrt.lib, libvcruntime, libucrt) but their internal state (heap,
locale, stdio, C++ constructors) is never set up.
5. Tried calling _CRT_INIT from a DllMain. Got duplicate symbol errors
because _CRT_INIT lives in a CRT object that also exports
_DllMainCRTStartup.
6. Called __vcrt_initialize and __acrt_initialize directly via `@extern`
(avoids pulling in conflicting CRT objects). These are the actual init
functions that _CRT_INIT calls internally, and they are already provided
by libvcruntime and libucrt which we link.
## The fix
Declare a DllMain in main_c.zig that Zig's start.zig calls during
DLL_PROCESS_ATTACH. It calls __vcrt_initialize and __acrt_initialize to
bootstrap the CRT. On DLL_PROCESS_DETACH, it calls the matching
uninitialize functions.
Guarded with `if (builtin.os.tag == .windows and builtin.abi == .msvc)`.
On other platforms, DllMain is void and has no effect.
The workaround is harmless to keep even after Zig fixes the issue. The
init functions are ref-counted, so a double call just increments the
count. Comments in main_c.zig document when and how to remove it. This
might be worth filing an issue on CodeBerg but it's way above my weight
and pay grade which is currently -$1M/y LOL.
## Build changes
GhosttyLib.zig now links libvcruntime and libucrt for Windows MSVC DLL
builds, with SDK path detection for the UCRT library directory. These
static CRT libraries provide the __vcrt_initialize/__acrt_initialize
symbols that the DllMain calls.
## Reproducer
test_dll_init.c is a minimal C program that loads ghostty.dll via
LoadLibraryA and calls ghostty_info + ghostty_init. Before the fix,
ghostty_init crashed. After the fix, it returns 0. We can keep it or
remove it, thoughts?
## What would be nice upstream (in Zig)
Zig's _DllMainCRTStartup in start.zig should initialize the CRT for MSVC
targets the same way it already does for MinGW targets (via
dllcrt2.obj/crtdll.c). Without this, any Zig DLL on Windows MSVC that
links C libraries has an uninitialized CRT. No upstream issue tracks
this exact gap as of 2026-03-26. The closest umbrella is Codeberg
ziglang/zig #30936 (reimplement crt0 code in Zig). I let Claude scan on
both github and CodeBerg.
## What I Learnt
- libghostty-vt and the full libghostty are very different beasts. The
VT library is pure Zig with no C dependencies. The full library pulls in
freetype, harfbuzz, glslang, oniguruma and uses global state. Windows
DLL loading is greenfield basically.
- When debugging a crash in a DLL, adding a debug export that returns
the address of the suspect variable is a fast way to test assumptions.
We thought `state` was at address 0 but it was fine. The null pointer
was deeper in the init chain.
- Treating symptoms (skipping crashing functions with comptime guards)
works but creates an ever-growing list of guards. Finding the root cause
(CRT not initialized) fixes all of them at once.
- Zig's start.zig handles MinGW and MSVC DLL entry points differently.
MinGW gets proper CRT init via dllcrt2.obj. MSVC gets nothing. As of
today at least.
- `@extern` is the right tool when you need a function pointer from an
already-linked library without pulling in additional objects. `extern
"c"` can drag in CRT objects that conflict with Zig's own symbols.
- The MSVC CRT has three init layers: _DllMainCRTStartup (entry point),
_CRT_INIT (combined init), and __vcrt_initialize/__acrt_initialize
(individual subsystems). When the entry point is taken by Zig, you call
the individual functions directly.
## Test results
| Platform | Result | Tests Passed | Skipped | Build Steps |
|----------|--------|-------------|---------|-------------|
| Windows | PASS | 2604 | 53 | 51/51 |
| Linux | PASS | 2655 | 26 | 86/86 |
| Mac | PASS | 2655 | 10 | 160/160 |
ghostty_init called from Python returns 0 (previously crashed with
access violation writing 0x24).
C reproducer test_dll_init.c exits 0 after ghostty_info succeeds.
These used to crash before the fix/workaround.
Add explicit file-type rules to .gitattributes so text files are stored
and checked out with LF line endings regardless of platform. This
prevents issues where Windows git (or CI actions/checkout) converts
LF to CRLF, breaking comptime parsers that split embedded files by
'\n' and end up with trailing '\r' in parsed tokens.
Key changes:
- Source code (*.zig, *.c, *.h, etc.): always LF
- Config/build files (*.zon, *.nix, *.md, etc.): always LF
- Text data files (*.txt): always LF (for embedded file parsing)
- Windows resource files (*.rc, *.manifest): preserve as-is
(native Windows tooling expects CRLF)
- Binary files: explicitly marked as binary
Removed the legacy rgb.txt -text rule since *.txt now handles it
uniformly with code-level CRLF handling as defense-in-depth.
Trim trailing \r when splitting octants.txt by \n at comptime. On
Windows, git may convert LF to CRLF on checkout, leaving \r at the
end of each line. Without trimming, the parser tries to use \r as
a struct field name in @field(), causing a compile error.
Follows the same pattern used in x11_color.zig for rgb.txt parsing.
Use b.allocator instead of b.graph.arena for SDK detection and
path formatting -- b.allocator is the public API, b.graph.arena
is an internal field.
Move test_dll_init.c from windows/Ghostty.Tests/ to test/windows/
with a README. Test infrastructure belongs under test/, not the
Windows app directory.
The C# test suite and ghostty_crt_workaround_active() probe were
unnecessary overhead. The DllMain workaround is harmless to keep
(CRT init is ref-counted) and comments document when to remove it.
test_dll_init.c remains as a standalone C reproducer.
C# test suite and C reproducer validating DLL initialization.
The probe test (DllMainWorkaround_IsStillActive) checks that the CRT
workaround is compiled in via ghostty_crt_workaround_active(). When
Zig fixes MSVC DLL CRT init, removing the DllMain will make this test
fail with instructions on how to verify the fix and clean up.
ghostty_init is tested via the C reproducer (test_dll_init.c) rather
than C# because the global state teardown crashes the test host on
DLL unload. The C reproducer exits without FreeLibrary.
Zig's _DllMainCRTStartup does not initialize the MSVC C runtime when
building a shared library targeting MSVC ABI. This means any C library
function that depends on CRT internal state (setlocale, glslang,
oniguruma) crashes with null pointer dereferences because the heap,
locale, and C++ runtime are never set up.
Declare a DllMain that calls __vcrt_initialize and __acrt_initialize
on DLL_PROCESS_ATTACH. Zig's start.zig checks @hasDecl(root, "DllMain")
and calls it during _DllMainCRTStartup. Uses @extern to get function
pointers without pulling in CRT objects that would conflict with Zig's
own _DllMainCRTStartup symbol.
Only compiles on Windows MSVC (comptime guard). On other platforms and
ABIs, DllMain is void and has no effect.
linkLibC() provides msvcrt.lib for DLL targets but doesn't include the
companion CRT bootstrap libraries. The DLL startup code in msvcrt.lib
calls __vcrt_initialize and __acrt_initialize, which live in the static
CRT libraries (libvcruntime.lib, libucrt.lib).
Detect the Windows 10 SDK installation via std.zig.WindowsSdk to add
the UCRT library path, which Zig's default search paths don't include
(they add um\x64 but not ucrt\x64).
This is a workaround for a Zig gap (partially addressed in closed
issues 5748, 5842 on ziglang/zig). Only affects initShared (DLL),
not initStatic.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add explicit file-type rules to .gitattributes so text files are stored
and checked out with LF line endings regardless of platform. This
prevents issues where Windows git (or CI actions/checkout) converts
LF to CRLF, breaking comptime parsers that split embedded files by
'\n' and end up with trailing '\r' in parsed tokens.
Key changes:
- Source code (*.zig, *.c, *.h, etc.): always LF
- Config/build files (*.zon, *.nix, *.md, etc.): always LF
- Text data files (*.txt): always LF (for embedded file parsing)
- Windows resource files (*.rc, *.manifest): preserve as-is
(native Windows tooling expects CRLF)
- Binary files: explicitly marked as binary
Removed the legacy rgb.txt -text rule since *.txt now handles it
uniformly with code-level CRLF handling as defense-in-depth.
Trim trailing \r when splitting octants.txt by \n at comptime. On
Windows, git may convert LF to CRLF on checkout, leaving \r at the
end of each line. Without trimming, the parser tries to use \r as
a struct field name in @field(), causing a compile error.
Follows the same pattern used in x11_color.zig for rgb.txt parsing.
A regression caused by 3ee8ef4f65.
The search bar should stay as the first responder after clicking inside
the text field or clicking the next/previous button, but right now it
doesn’t.
Trailing state capture now is encapsulated in a struct `Capture` and all
parsers access the data via `p.capture.trailing()` rather than directly
from the writer.
This is primarily to prep for the OSC parser to be able to capture the
entire sequence (not just the trailing part) so we can setup libghostty
for fallback handlers so libghostty implementers can have custom OSC
behaviors.
But, it has the benefit of making our OSC parser much cleaner too.
I'm doing some benchmarks now...
Trailing state capture now is encapsulated in a struct `Capture` and all
parsers access the data via `p.capture.trailing()` rather than directly
from the writer.
This is primarily to prep for the OSC parser to be able to capture the
entire sequence (not just the trailing part) so we can setup libghostty
for fallback handlers so libghostty implementers can have custom OSC
behaviors.
But, it has the benefit of making our OSC parser much cleaner too.
Add ghostty_paste_encode() which encodes paste data for writing to the
terminal pty. It strips unsafe control bytes, wraps in bracketed paste
sequences when requested, and replaces newlines with carriage returns
for unbracketed mode. The input buffer is modified in place and the
encoded result is written to a caller-provided output buffer, following
the same buffer/out_written pattern as the other encode functions like
ghostty_size_report_encode.
Update the c-vt-paste example with an encode_example() demonstrating the
new function and add corresponding @snippet references in the header
documentation.
Extracted this from #11870 since I can't figure out why that build is
failing.
Add ghostty_paste_encode() which encodes paste data for writing to
the terminal pty. It strips unsafe control bytes, wraps in bracketed
paste sequences when requested, and replaces newlines with carriage
returns for unbracketed mode. The input buffer is modified in place
and the encoded result is written to a caller-provided output buffer,
following the same buffer/out_written pattern as the other encode
functions like ghostty_size_report_encode.
Update the c-vt-paste example with an encode_example() demonstrating
the new function and add corresponding @snippet references in the
header documentation.
Add set/get support for foreground, background, cursor, and palette
default colors through ghostty_terminal_set and ghostty_terminal_get.
Four new set options (COLOR_FOREGROUND, COLOR_BACKGROUND, COLOR_CURSOR,
COLOR_PALETTE) write directly to the terminal color defaults. Passing
NULL clears the value for RGB colors or resets the palette to the
built-in default. All set operations mark the palette dirty flag for the
renderer.
Eight new get data types retrieve either the effective color (override
or default, via DynamicRGB.get) or the default color only (ignoring any
OSC overrides). Effective getters for RGB colors return the new NO_VALUE
result code when no color is configured. The palette getters return the
current or original palette respectively.
Adds the GHOSTTY_NO_VALUE result code for cases where a queried value is
simply not configured, distinct from GHOSTTY_INVALID_VALUE which
indicates a caller error.
## Example
```c
#include <ghostty/vt.h>
#include <stdio.h>
int main() {
GhosttyTerminal terminal = NULL;
GhosttyTerminalOptions opts = { .cols = 80, .rows = 24, .max_scrollback = 0 };
ghostty_terminal_new(NULL, &terminal, opts);
// Set default colors
GhosttyColorRgb fg = { .r = 0xDD, .g = 0xDD, .b = 0xDD };
GhosttyColorRgb bg = { .r = 0x1E, .g = 0x1E, .b = 0x2E };
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_COLOR_FOREGROUND, &fg);
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_COLOR_BACKGROUND, &bg);
// Read back the effective foreground
GhosttyColorRgb color;
if (ghostty_terminal_get(terminal, GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND, &color)
== GHOSTTY_SUCCESS) {
printf("fg: #%02X%02X%02X\n", color.r, color.g, color.b); // #DDDDDD
}
// After an OSC 10 override from a program inside the terminal:
ghostty_terminal_vt_write(terminal, (const uint8_t*)"\x1B]10;rgb:FF/00/00\x1B\\", 20);
// Effective returns the override, default returns the original
ghostty_terminal_get(terminal, GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND, &color);
printf("effective: #%02X%02X%02X\n", color.r, color.g, color.b); // #FF0000
ghostty_terminal_get(terminal, GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND_DEFAULT, &color);
printf("default: #%02X%02X%02X\n", color.r, color.g, color.b); // #DDDDDD
ghostty_terminal_free(terminal);
return 0;
}
```
A full working example is in `example/c-vt-colors/`.
Add set/get support for foreground, background, cursor, and palette
default colors through ghostty_terminal_set and ghostty_terminal_get.
Four new set options (COLOR_FOREGROUND, COLOR_BACKGROUND, COLOR_CURSOR,
COLOR_PALETTE) write directly to the terminal color defaults. Passing
NULL clears the value for RGB colors or resets the palette to the
built-in default. All set operations mark the palette dirty flag for
the renderer.
Eight new get data types retrieve either the effective color (override
or default, via DynamicRGB.get) or the default color only (ignoring
any OSC overrides). Effective getters for RGB colors return the new
NO_VALUE result code when no color is configured. The palette getters
return the current or original palette respectively.
Adds the GHOSTTY_NO_VALUE result code for cases where a queried value
is simply not configured, distinct from GHOSTTY_INVALID_VALUE which
indicates a caller error.
Switch the two Windows CI jobs (build-examples-cmake-windows and
build-libghostty-vt-windows) from GitHub-hosted windows-2025 runners to
namespace-profile-ghostty-windows runners.
Switch the two Windows CI jobs (build-examples-cmake-windows and
build-libghostty-vt-windows) from GitHub-hosted windows-2025 runners
to namespace-profile-ghostty-windows runners.
This makes it so that `zig build dist -Demit-lib-vt` produces a
`libghostty-vt-<version>.tar.gz` source tarball that only contains what
is needed to build and test libghostty-vt (it cannot build Ghostty GUI
on macOS or Linux). `distcheck` has been updated to also verify cmake
works.
The source tarball goes from 38 MB to 2.8 MB for libghostty.
I also updated CI to build and test this, and also contains an assertion
that our tarball is always less than 5 MB so we can be aware if/when we
blow it up.
The `release-tip` job was also updated to add the libghostty-vt tarball
to our tip release on GH.
Run cmake configure and build on the extracted lib-vt tarball as
part of distcheck to ensure the CMake wrapper works from the
stripped archive. Keep dist/cmake/ and dist/libghostty-vt/ in the
archive since the CMake build needs them.
Add a build-dist-lib-vt job that runs distcheck with
-Demit-lib-vt=true and verifies the resulting tarball stays under
5 MB. Also downsize the build-dist runner from -md to -sm.
Add a source-tarball-lib-vt job that builds the stripped lib-vt
dist tarball and publishes it as libghostty-vt-source.tar.gz to
the tip release. Also downsize the source-tarball runner from -md
to -sm since it does not need the extra resources.
When emit_lib_vt is set, the dist tarball is now named
ghostty-vt-<version>.tar.gz and excludes large files that are
unnecessary for building libghostty-vt. This reduces the archive
from ~36MB to ~2.8MB by excluding images, macOS app resources,
font assets, fuzz test corpus, crash testdata, and vendored
libraries not used by lib-vt.
GTK resources and frame data generation are also skipped since
lib-vt does not need them, which removes the GTK build-time
dependency. The distcheck step runs test-lib-vt instead of the
full test suite for lib-vt archives.
**WARNING:** We CANNOT upgrade to Xcode 26.4 with Zig 0.15 because:
https://codeberg.org/ziglang/zig/issues/31658
We have to wait and see if Zig will backport that or if we just have to
roll forward to Zig 0.16 when it comes out. At the time of this commit,
no released Zig version has the fix for that issue.
**WARNING:** We CANNOT upgrade to Xcode 26.4 with Zig 0.15 because:
https://codeberg.org/ziglang/zig/issues/31658
We have to wait and see if Zig will backport that or if we just have to
roll forward to Zig 0.16 when it comes out. At the time of this commit,
no released Zig version has the fix for that issue.
## Summary
- On Windows, install the shared lib as `ghostty.dll` and the static lib
as `ghostty-static.lib` instead of `libghostty.so` and `libghostty.a`
- The `-static` suffix on the static lib avoids collision with the
import lib that the DLL produces (same pattern as
`ghostty-vt-static.lib`)
- Guard `bundle_ubsan_rt` in `GhosttyLib.zig` `initStatic` for Windows,
since Zig's ubsan emits `/exclude-symbols` linker directives that are
incompatible with the MSVC linker (LNK4229). Matches the existing
pattern in `GhosttyLibVt.zig`
Also includes a cherry-pick of PR #11782 (backslash path handling) to
keep the Windows test suite fully passing on this branch.
## Discussion
- Is this better? This is me starting to question Claude's
training/output.
```zig
// Zig's ubsan emits /exclude-symbols linker directives that
// are incompatible with the MSVC linker (LNK4229).
lib.bundle_ubsan_rt = deps.config.target.result.os.tag != .windows;
```
More concise, still preserves the comment. Not sure which is preferred
here. The set-then-override matches `GhosttyLibVt.zig` exactly, but the
boolean is maybe cleaner? Open to either. Curious about your preference.
## Test results
Tested before/after on all three platforms:
| | Windows | Linux | Mac |
|---|---|---|---|
| **BEFORE** (upstream/main) | FAIL (pre-existing, fixed by PR 11782) |
PASS | PASS |
| **AFTER** (this branch) | PASS - 51/51 steps, 2604/2657 tests, 53
skipped | PASS | PASS |
No regressions on any platform.
## What I Learnt
- Zig's build system automatically generates an import `.lib` alongside
a `.dll` on Windows, so the static lib needs a distinct name to avoid
collision.
- The ubsan runtime emits MSVC-incompatible linker directives
On Windows, install as ghostty.dll + ghostty-static.lib instead of
libghostty.so + libghostty.a, following Windows naming conventions.
Guard ubsan_rt bundling in initStatic for MSVC compatibility.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# What
CommaSplitter treats backslash as an escape character, which breaks
Windows paths like
C:\Users\foo since \U is not a valid escape. On Windows, treat backslash
as a literal character
outside of quoted strings. Inside quotes, escape sequences still work as
before.
Also fix Theme.parseCLI to not mistake the colon in a Windows drive
letter (C:\...) for a
light/dark theme pair separator.
# How
The platform behavior is controlled by a single comptime constant at the
top of CommaSplitter:
const escape_outside_quotes = builtin.os.tag != .windows;
The next() function checks this constant to decide whether backslash
triggers escape parsing
outside quoted strings. All behavior lives in one place.
For Theme, skip colon detection at index 1 on Windows so drive letters
are not mistaken for pair
separators.
Escape-specific tests are skipped on Windows with SkipZigTest.
Windows-specific tests are added
separately to cover paths, literal backslash, and
escapes-still-work-inside-quotes.
# Note
There are other places in config parsing that use colon as a delimiter
without accounting for
Windows drive letters (command.zig prefix parsing, keybind parsing).
Those are separate from this
PR.
# Verified
- zig build test-lib-vt passes on Windows (exit 0)
- No impact on Linux/macOS (the constant is true there, all existing
behavior unchanged)
# What I Learnt
- Platform behavior should live in a single constant or struct, not
scattered across if-else
branches in every test. The escape_outside_quotes constant mirrors the
pattern upstream uses with
PageAlloc = switch(builtin.os.tag) but for a simpler boolean case.
- Use error.SkipZigTest for tests that cannot run on a platform, never
silent returns. This way
the test runner reports them as skipped, not silently passed.
- When fixing a pattern (colon as delimiter), grep the whole codebase
for similar issues even if
you are not fixing them all in one PR. Note them for future work.
This parameterizes all our calling conventions on our C API based on
whether we're building the C lib or Zig lib. If we're building the C
lib, it's C calling convention, else Zig. This lets the Zig module call
the C API via `terminal.c_api.<func>`.
Zig is perfectly capable of calling C ABI but we actually modify our
struct layouts depending on calling conv so you can't actually use the
API prior to this. This fixes that all up.
**Why would you want to do this?** The C API has some different
semantics and stricter care about things like ABI compatibility (in how
it changes structs and so on). It actually might be a more API-stable
API to rely on even from Zig.
When PS1 ends with a bare '%' (e.g. `%3~ %`), concatenating our 133;B
mark (`%{...%}`) directly after it causes zsh's prompt expansion to
interpret the '%' + '{' result as a '%{' escape sequence. This swallows
the 133;B mark and produces a visible '{' in the prompt.
Work around this by doubling a trailing '%' into '%%' before appending
marks, so it expands to a literal '%' and won't merge with the `%{`
token.
When PS1 ends with a bare '%' (e.g. `%3~ %`), concatenating our 133;B
mark (`%{...%}`) directly after it causes zsh's prompt expansion to
interpret the '%' + '{' result as a '%{' escape sequence. This swallows
the 133;B mark and produces a visible '{' in the prompt.
Work around this by doubling a trailing '%' into '%%' before appending
marks, so it expands to a literal '%' and won't merge with the `%{`
token.
Each C API file independently imported ../../lib/allocator.zig as
lib_alloc. Now that terminal/lib.zig re-exports the allocator module
as lib.alloc, use that instead. This removes the redundant import
and keeps all lib dependencies flowing through the single lib.zig
entry point.
Previously every file in the terminal package independently imported
build_options and ../lib/main.zig, then computed the same
lib_target constant. This was repetitive and meant each file needed
both imports just to get the target.
Introduce src/terminal/lib.zig which computes the target once and
re-exports the commonly used lib types (Enum, TaggedUnion, Struct,
String, checkGhosttyHEnum, structSizedFieldFits). All terminal
package files now import lib.zig and use lib.target instead of the
local lib_target constant, removing the per-file boilerplate.
The resize function now requires cell_width_px and cell_height_px
parameters and handles the full resize sequence: computing and setting
width_px/height_px on the terminal, clearing synchronized output mode so
changes display immediately, and encoding a mode 2048 in-band size
report via the write_pty callback when that mode is enabled.
A valid width/height px is critical for some applications and protocols
and some applications rely directly on in-band size reports, so this
change is necessary to support those use cases.
I do wonder if for the Zig API we should be doing this in
`terminal.resize` or somewhere else, because as it stands this has to
all be manually done on the Zig side.
The resize function now requires cell_width_px and cell_height_px
parameters and handles the full resize sequence: computing and
setting width_px/height_px on the terminal, clearing synchronized output mode
so changes display immediately, and encoding a mode 2048 in-band size report
via the write_pty callback when that mode is enabled.
A valid width/height px is critical for some applications and protocols
and some applications rely directly on in-band size reports, so this
change is necessary to support those use cases.
Add total_rows and scrollback_rows as new TerminalData variants
queryable through the existing ghostty_terminal_get interface, using the
cached O(1) total_rows field from PageList rather than introducing
standalone functions.
Add total_rows and scrollback_rows as new TerminalData variants
queryable through the existing ghostty_terminal_get interface,
using the cached O(1) total_rows field from PageList rather than
introducing standalone functions.
Previously ghostty_terminal_set required all values to be passed as
pointers to the value, even when the value itself was already a pointer
(userdata, function pointer callbacks). This forced callers into awkward
patterns like compound literals or intermediate variables just to take
the address of a pointer.
Now pointer-typed options (userdata and all callbacks) are passed
directly as the value parameter. Only non-pointer types like
GhosttyString still require a pointer to the value. This simplifies
InType to return the actual stored type for each option and lets
setTyped work with those types directly.
Previously ghostty_terminal_set required all values to be passed as
pointers to the value, even when the value itself was already a
pointer (userdata, function pointer callbacks). This forced callers
into awkward patterns like compound literals or intermediate
variables just to take the address of a pointer.
Now pointer-typed options (userdata and all callbacks) are passed
directly as the value parameter. Only non-pointer types like
GhosttyString still require a pointer to the value. This
simplifies InType to return the actual stored type for each option
and lets setTyped work with those types directly.
Add title and pwd as both gettable data keys
(GHOSTTY_TERMINAL_DATA_TITLE/PWD) and settable options
(GHOSTTY_TERMINAL_OPT_TITLE/PWD) in the C terminal API. Getting returns
a borrowed GhosttyString; setting copies the data into the terminal via
setTitle/setPwd.
The underlying Terminal.setTitle/setPwd now append a null sentinel so
that getTitle/getPwd can return sentinel-terminated slices ([:0]const
u8), which is useful for downstream consumers that need C strings.
Change ghostty_terminal_set to return GhosttyResult instead of void so
that the new title/pwd options can report allocation failures. Existing
option-setting calls cannot fail so the return value is
backwards-compatible for callers that discard it.
Add title and pwd as both gettable data keys
(GHOSTTY_TERMINAL_DATA_TITLE/PWD) and settable options
(GHOSTTY_TERMINAL_OPT_TITLE/PWD) in the C terminal API. Getting
returns a borrowed GhosttyString; setting copies the data into the
terminal via setTitle/setPwd.
The underlying Terminal.setTitle/setPwd now append a null sentinel so
that getTitle/getPwd can return sentinel-terminated slices ([:0]const
u8), which is useful for downstream consumers that need C strings.
Change ghostty_terminal_set to return GhosttyResult instead of void
so that the new title/pwd options can report allocation failures.
Existing option-setting calls cannot fail so the return value is
backwards-compatible for callers that discard it.
### This is it! This one (and the other stacked PRs) and #11782 should
finally give a clean test run on Windows!
## Summary
- Increase `@setEvalBranchQuota` from 1M to 10M (too much? how much is
too much?) in `checkGhosttyHEnum` (src/lib/enum.zig)
- Fixes the only remaining test failure on Windows MSVC: `ghostty.h
MouseShape`
## Context
This one was fun! Claude started blabbering, diminishing returns it
said. It couldn't figure out. So I called Dario and it worked.
Nah, much easier than that.
On MSVC, the translate-c output for `ghostty.h` is ~360KB with ~2173
declarations (vs ~112KB / ~1502 on Linux/Mac) because `<sys/types.h>`
and `<BaseTsd.h>` pull in Windows SDK headers. The `checkGhosttyHEnum`
function uses a nested `inline for` (enum fields x declarations) with
comptime string comparisons. For MouseShape (34 variants), this
generates roughly 34 x 2173 x ~20 = ~1.5M comptime branches, exceeding
the 1M quota.
The failure was confusing because it presented as a runtime error
("ghostty.h is missing value for GHOSTTY_MOUSE_SHAPE_DEFAULT") rather
than a compile error. The constants exist in the translate-c output and
the test compiles, but the comptime loop silently stops matching when it
hits the branch limit, so `set.remove` is never called and the set
reports all entries as missing at runtime.
## How we found it
The translate-c output clearly had all 34 GHOSTTY_MOUSE_SHAPE_*
constants, yet the test reported all of them as missing. I asked Claude
to list 5 hypotheses (decl truncation, branch quota, string comparison
bug, declaration ordering, field access failure) and to write 7 targeted
POC tests in enum.zig to isolate each step of `checkGhosttyHEnum`:
1. POC1-2: Module access and declaration count (both passed)
2. POC3: `@hasDecl` for the constant (passed)
3. POC4: Direct field value access (passed)
4. POC5: `inline for` over decls with string comparison - **compile
error: "evaluation exceeded 1000 backwards branches"**
5. POC6: Same but with 10M quota (passed)
6. POC7: Full `checkGhosttyHEnum` clone with 10M quota - **passed,
confirming the fix**
POC5 was the key: the default 1000 branch limit for test code confirmed
the comptime budget mechanism. The existing 1M quota in
`checkGhosttyHEnum` was enough for Linux/Mac (1502 declarations) but not
for MSVC (2173 declarations) with larger enums.
## Stack
Stacked on 016-windows/fix-libcxx-msvc.
## Test plan
### Cross-platform results (`zig build test` / `zig build
-Dapp-runtime=none test` on Windows)
| | Windows | Linux | Mac |
|---|---|---|---|
| **BEFORE** (016, ce9930051) | FAIL - 49/51, 2630/2654, 1 test failed,
23 skipped | PASS - 86/86, 2655/2678, 23 skipped | PASS - 160/160,
2655/2662, 7 skipped |
| **AFTER** (017, 68378a0bb) | FAIL - 49/51, 2631/2654, 23 skipped |
PASS - 86/86, 2655/2678, 23 skipped | PASS - 160/160, 2655/2662, 7
skipped |
### Windows: what changed (2630 -> 2631 tests, MouseShape fixed)
**Fixed by this PR:**
- `ghostty.h MouseShape` test - was failing because comptime branch
quota exhaustion silently prevented the inline for loop from matching
any constants
**Remaining failure (pre-existing, unrelated):**
- `config.Config.test.clone can then change conditional state` -
segfaults (exit code 3) on Windows. We investigated this and it looked
familiar.. cherry-picking the `CommaSplitter `fix from PR #11782
resolved it! The backslash path handling in `CommaSplitter `breaks theme
path parsing on Windows, which is exactly what that PR addresses. So
once that lands, we should be in a good place... ready to ship to
Windows users! (just kidding)
### Linux/macOS: no regressions
Identical pass counts and test results before and after.
## What I Learnt
- Comptime branch quota exhaustion in Zig does not always surface as a
clean compile error. When it happens inside an `inline for` loop with
`comptime` string comparisons that gate runtime code (like
`set.remove`), the effect is that matching code is silently not
generated. The test compiles and runs, but the runtime behavior is wrong
because the matching branches were never emitted. This makes the failure
look like a data issue (missing declarations) rather than a compile
budget issue.
- When debugging comptime issues, writing small isolated POC tests that
exercise each step of the failing function independently is very
effective. It took 7 targeted tests to pinpoint the exact failure point.
- Cross-platform translate-c outputs can vary significantly in size. On
MSVC, system headers are much larger than on Linux/Mac, which affects
comptime budgets for any code that iterates over translated module
declarations.
Replace the O(N×M) nested inline for loop with direct @hasDecl lookups.
The old approach iterated over all translate-c declarations for each enum
field, which required a 10M comptime branch quota on MSVC (2173 decls ×
138 fields × ~20 branches). The new approach constructs the expected
declaration name and checks directly, reducing to O(N) and needing only
100K quota on all platforms.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The DA1 trampoline was converting C feature codes into a local
stack buffer and returning a slice pointing into it. This is
unsound because the slice outlives the stack frame once the
trampoline returns, leaving reportDeviceAttributes reading
invalid memory.
Move the scratch buffer into the wrapper effects struct so that
its lifetime extends beyond the trampoline call, keeping the
returned slice valid for the caller.
Add a comprehensive "Effects" section to the terminal module
documentation in terminal.h explaining the callback system that
lets embedding applications react to terminal-initiated events
(bell, title changes, pty writes, device queries, etc.). The
section includes a reference table of all available effects and
their triggers, plus @snippet references to the new example.
Add c-vt-effects example project demonstrating how to register
write_pty, bell, and title_changed callbacks, attach userdata,
and feed VT data that triggers each effect.
Assign handler.effects as a struct literal instead of setting fields
individually. This lets the compiler catch missing fields if new
effects are added to the Effects struct.
Also sort the callback function typedefs in vt/terminal.h
alphabetically (Bell, ColorScheme, DeviceAttributes, Enquiry, Size,
TitleChanged, WritePty, Xtversion).
Rename device_status.h to device.h and add C-compatible structs for
device attributes (DA1/DA2/DA3) responses. The new header includes
defines for all known conformance levels, DA1 feature codes, and DA2
device type identifiers.
Add a GhosttyTerminalDeviceAttributesFn callback that C consumers can
set via GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES. The callback follows
the existing bool + out-pointer pattern used by color_scheme and size
callbacks. When the callback is unset or returns false, the trampoline
returns a default VT220 response (conformance level 62, ANSI color).
The DA1 primary features use a fixed [64]uint16_t inline array with a
num_features count rather than a pointer, so the entire struct is
value-typed and can be safely copied without lifetime concerns.
Change device_status.ColorScheme from a plain Zig enum to
lib.Enum so it uses c_int backing when targeting the C ABI.
Add a color_scheme callback to the C terminal effects, following
the bool + out-pointer pattern used by the size callback. The
trampoline converts between the C calling convention and the
internal stream handler color_scheme effect, returning null when
no callback is set.
Add device_status.h header with GhosttyColorScheme enum and wire
it through terminal.h as GHOSTTY_TERMINAL_OPT_COLOR_SCHEME (= 7)
with GhosttyTerminalColorSchemeFn.
Add GHOSTTY_TERMINAL_OPT_SIZE so C consumers can respond to
XTWINOPS size queries (CSI 14/16/18 t). The callback receives a
GhosttySizeReportSize out-pointer and returns true if the size is
available, or false to silently ignore the query. The trampoline
converts the bool + out-pointer pattern to the optional that the
Zig handler expects.
Add GHOSTTY_TERMINAL_OPT_TITLE_CHANGED so C consumers are notified
when the terminal title changes via OSC 0 or OSC 2 sequences. The
callback has the same fire-and-forget shape as bell.
Add GHOSTTY_TERMINAL_OPT_ENQUIRY and GHOSTTY_TERMINAL_OPT_XTVERSION
so C consumers can respond to ENQ (0x05) and XTVERSION (CSI > q)
queries. Both callbacks return a GhosttyString rather than using
out-pointers.
Introduce GhosttyString in types.h as a borrowed byte string
(ptr + len) backed by lib.String on the Zig side. This will be
reusable for future callbacks that need to return string data.
Without an xtversion callback the trampoline returns an empty
string, which causes the handler to report the default
"libghostty" version. Without an enquiry callback no response
is sent.
Test that the write_pty callback receives correct DECRQM response
data and userdata, that queries are silently ignored without a
callback, and that setting null clears the callback. Test that
the bell callback fires on single and multiple BEL characters
with correct userdata, and that BEL without a callback is safe.
Add GHOSTTY_TERMINAL_OPT_BELL so C consumers can receive bell
notifications during VT processing. The bell trampoline follows
the same pattern as write_pty.
Move the C function pointer typedefs (WritePtyFn, BellFn) into
the Effects struct namespace to keep callback types co-located
with their storage and trampolines.
Add a typed option setter ghostty_terminal_set() following the
existing setopt pattern used by the key encoder and render state
APIs. This is the first step toward exposing stream_terminal
Handler.Effects through the C API.
The initial implementation includes a write_pty callback and a
shared userdata pointer. The write_pty callback is invoked
synchronously during ghostty_terminal_vt_write() when the terminal
needs to send a response back to the pty, such as DECRQM mode
reports or device status responses.
Trampolines are always installed at terminal creation time and
no-op when no C callback is set, so callers can configure
callbacks at any point without reinitializing the stream. The C
callback state is grouped into an internal Effects struct on the
TerminalWrapper to simplify adding more callbacks in the future.
> [!WARNING]
> Review/approve this AFTER #11807 and #11810 (this PR includes their
commits)
## Summary
### **And `run test ghostty-test` finally runs on Windows! 🎉almost
there!**
- Skip `linkLibCpp()` on MSVC for dcimgui, spirv-cross, and harfbuzz
(same fix already applied upstream to highway, simdutf, utfcpp, glslang,
SharedDeps, GhosttyZig)
- Fix freetype C enum signedness: MSVC translates C enums as signed
`int`, while GCC/Clang uses unsigned `int`. Add `@intCast` at call sites
and `@bitCast` for bit-shift operations on glyph format tags.
## Context
Zig unconditionally passes `-nostdinc++` and adds its bundled
libc++/libc++abi include paths, which conflict with MSVC's own C++
runtime headers. The MSVC SDK directories (added via `linkLibC`) already
contain both C and C++ headers, so `linkLibCpp` is not needed.
The freetype enum issue is a different facet of the same MSVC vs
GCC/Clang divide: `FT_Render_Mode` and `FT_Glyph_Format` are C enums
that get different signedness on different compilers.
## Stack
Stacked on 015-windows/fix-ssize-t-msvc.
## Test plan
### Cross-platform results (`zig build test` / `zig build
-Dapp-runtime=none test` on Windows)
| | Windows | Linux | Mac |
|---|---|---|---|
| **BEFORE** (015, a35f84db3) | FAIL - 48/51, 1 failed (compile
ghostty-test) | PASS - 86/86, 2655/2678, 23 skipped | PASS - 160/160,
2655/2662, 7 skipped |
| **AFTER** (016, ce9930051) | FAIL - 49/51, 2630/2654 tests passed, 1
failed, 23 skipped | PASS - 86/86, 2655/2678, 23 skipped | PASS -
160/160, 2655/2662, 7 skipped |
### Windows: what changed (48 -> 49 steps, tests now run)
**Fixed by this PR:**
- `compile test ghostty-test` - was `3 errors` (libcxxabi conflicts +
freetype type mismatches) -> `success`
- `run test ghostty-test` - now actually runs: 2630 passed, 23 skipped,
1 failed
**Remaining test failure (pre-existing, unrelated):**
- `ghostty.h MouseShape` - `checkGhosttyHEnum` cannot find
`GHOSTTY_MOUSE_SHAPE_*` constants in the translate-c output. This is a
translate-c issue with how MSVC enum constants are exposed, not related
to C++ linking or enum signedness.
### Linux/macOS: no regressions
Identical pass counts and test results before and after.
## Discussion
### Grep wider: other unconditional linkLibCpp calls
`pkg/breakpad/build.zig` still calls `linkLibCpp()` unconditionally but
is behind sentry and not in the Windows build path. Noted for
completeness.
### Freetype enum signedness
The freetype Zig bindings define `RenderMode = enum(c_uint)` and
`Encoding = enum(u31)`. On MSVC, C enums are `int` (signed), so the
translated C functions expect `c_int` parameters. The fix adds
`@intCast` to convert between signed and unsigned at call sites. This is
safe because the enum values are small positive integers that fit in
both types.
Also, not sure if there's a better way to make this change more
elegantly. The comments are replicated in each instance, probably
overkill but I have seen this same pattern elsewhere in the codebase.
## What I Learnt
- More of the same
> [!WARNING]
> Review/approve this AFTER #11807 (this PR includes its commits)
92% progress with the fixes!
## Summary
- Add a conditional `ssize_t` typedef for MSVC in `include/ghostty.h`
- MSVC's `<sys/types.h>` does not define `ssize_t` (it is a POSIX type),
which causes the `translate-c` build step to fail when translating
`ghostty.h` on Windows
- Uses `SSIZE_T` from `<BaseTsd.h>`, the standard Windows SDK equivalent
## Context
The `translate-c` step translates `ghostty.h` to Zig for test
compilation. On MSVC, it fails with 3 errors on `ssize_t` (used in
`ghostty_action_move_tab_s`, `ghostty_action_search_total_s`,
`ghostty_action_search_selected_s`).
The `#ifdef _MSC_VER` guard means this only affects MSVC builds.
`BaseTsd.h` is a standard Windows SDK header and `SSIZE_T` is a signed
pointer-sized integer, matching POSIX `ssize_t` and Zig's `isize`. This
pattern is used by libuv, curl, and other cross-platform C projects.
## Test plan
### Cross-platform results (`zig build test` / `zig build
-Dapp-runtime=none test` on Windows)
| | Windows | Linux | Mac |
|---|---|---|---|
| **BEFORE** (d5aef6e84) | FAIL - 47/51 steps, 1 failed | PASS - 86/86,
2655/2678 tests, 23 skipped | PASS - 160/160, 2655/2662 tests, 7 skipped
|
| **AFTER** (a35f84db3) | FAIL - 48/51 steps, 1 failed | PASS - 86/86,
2655/2678 tests, 23 skipped | PASS - 160/160, 2655/2662 tests, 7 skipped
|
### Windows: what changed (47 -> 48 steps, translate-c fixed)
**Fixed by this PR:**
- `translate-c` - was `3 errors` (unknown type name 'ssize_t' at lines
582, 842, 847) -> `success`
**Remaining failure (pre-existing, unrelated):**
- `compile test ghostty-test` - 3 errors in libcxxabi
(`std::get_new_handler` not found, `type_info` redefinition). This is
Zig's bundled libc++ ABI conflicting with MSVC headers when compiling
the test binary. It was previously masked by the translate-c failure
blocking this step.
### Linux/macOS: no regressions
Identical pass counts and test results before and after.
## What Have I Learnt
- I tried fixing this issue the old way, googling and stuff, I
eventually figured out but it took me way more than I am prepared to
share. Yikes.
## Summary
**Getting there!** Goal for today/tomorrow is to get it all green.
This one is easy:
- Gate `HAVE_UNISTD_H` and `HAVE_FCNTL_H` behind a non-Windows check
since these headers do not exist with MSVC
- Freetype's gzip module includes zlib headers which conditionally
include `unistd.h` based on this define
## Context
Same pattern as the zlib fix (010-* branch from my fork). Freetype
passes `-DHAVE_UNISTD_H` unconditionally, which causes zlib's `zconf.h`
to try including `unistd.h` when freetype compiles its gzip support. The
fix follows the same approach used in `pkg/zlib/build.zig` (line 36-38).
## Stack
Stacked on 013-windows/fix-helpgen-framegen.
## Test plan
### Cross-platform results (`zig build test` / `zig build
-Dapp-runtime=none test` on Windows)
| | Windows | Linux | Mac |
|---|---|---|---|
| **BEFORE** (f9d3b1aaf) | FAIL - 44/51 steps, 2 failed | PASS - 86/86,
2655/2678 tests, 23 skipped | PASS - 160/160, 2655/2662 tests, 7 skipped
|
| **AFTER** (d5aef6e84) | FAIL - 47/51 steps, 1 failed | PASS - 86/86,
2655/2678 tests, 23 skipped | PASS - 160/160, 2655/2662 tests, 7 skipped
|
### Windows: what changed (44 to 47 steps, 2 to 1 failure)
**Fixed by this PR:**
- `compile lib freetype` - was `2 errors` (unistd.h/fcntl.h not found)
-> `success`
- 3 additional steps that depended on freetype now succeed
**Remaining failure (pre-existing, tracked separately):**
- `translate-c` - 3 errors (`ssize_t` unknown in ghostty.h on MSVC)
### Linux/macOS: no regressions
Identical pass counts and test results before and after.
## Discussion
### Other build files with the same pattern
`pkg/fontconfig/build.zig` and `pkg/harfbuzz/build.zig` also pass
`-DHAVE_UNISTD_H` and/or `-DHAVE_FCNTL_H` unconditionally. They are not
in the Windows build path today, but will need the same fix when they
are.
## What I Learnt
More of the same
The MSVC translate-c output includes Windows SDK declarations,
bringing the total to ~2173 declarations (vs ~1502 on Linux/Mac).
The nested inline for in checkGhosttyHEnum (enum fields x declarations)
exceeds the 1M comptime branch quota for larger enums like MouseShape
(34 variants). Increase to 10M to accommodate.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MSVC translates C enums as signed int, while GCC/Clang uses unsigned
int. The freetype Zig bindings hardcode c_uint for enum backing types,
causing type mismatches when compiling with MSVC target.
Fix by adding @intCast at call sites where enum values are passed to
C functions, and @bitCast for the glyph format tag extraction where
bit-shift operations require unsigned integers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Zig unconditionally passes -nostdinc++ and adds its bundled
libc++/libc++abi include paths, which conflict with MSVC's own C++
runtime headers. The MSVC SDK directories (added via linkLibC)
already contain both C and C++ headers, so linkLibCpp is not needed.
This is the same fix already applied upstream to highway, simdutf,
utfcpp, glslang, SharedDeps, and GhosttyZig.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MSVC's <sys/types.h> does not define ssize_t (it is a POSIX type).
This causes the translate-c build step to fail when translating
ghostty.h on Windows. Use SSIZE_T from <BaseTsd.h> which is the
Windows SDK equivalent.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
> [!WARNING]
> Review/approve this AFTER #11798, #11800 and #11801 (this PR stacks on
top of rhem... ergo, it includes their commits)
> Don't cheat! Start from the oldest one! 😄 I know these are almost
one-liners but I am doing this mostly for documentation and karma
points. BTW, Github needs to level up this wankflow like a lot... IMHO
## Summary
- Use `writerStreaming()` instead of `writer()` for stdout in helpgen
and main_build_data (`ftruncate` on pipes fails on Windows with
`INVALID_PARAMETER` mapped to `FileTooBig`)
- Replace POSIX `scandir` with `opendir`/`readdir` plus `qsort` in
framegen since `scandir` is not available on Windows
## Context
This fix was previously applied upstream by Mitchell (f4998c6ab) and
reverted 15 minutes later (0fdddd5bc). The reason for the revert is not
clear. Around the same time, a CI step was added to execute cmake
examples on Windows, which was later removed (b723f2a43) with the note
"hangs, so remove it entirely". Whether the revert is related to the
hang or had a separate reason, we don't know.
What we do know:
- Both `helpgen` and `framegen` run during normal builds on Windows (via
`SharedDeps`), not just during dist packaging. Claude had told me the
opposite before but "don't trust and verify".
- Without this fix, both tools fail: helpgen with `FileTooBig`
(ftruncate on pipes), framegen with `scandir` undeclared
- The fix does not regress Linux or macOS
## Stack
Stacked on 012-windows/fix-glslang-msvc.
## Test plan
### Cross-platform results (`zig build test` / `zig build
-Dapp-runtime=none test` on Windows)
| | Windows | Linux | Mac |
|---|---|---|---|
| **BEFORE** (74c6ffe78) | FAIL - 39/51 steps, 4 failed | PASS - 86/86,
2655/2678 tests, 23 skipped | PASS - 160/160, 2655/2662 tests, 7 skipped
|
| **AFTER** (f9d3b1aaf) | FAIL - 44/51 steps, 2 failed | PASS - 86/86,
2655/2678 tests, 23 skipped | PASS - 160/160, 2655/2662 tests, 7 skipped
|
### Windows: what changed (39 > 44 steps, 4 > 2 failures)
**Fixed by this PR:**
- `run exe helpgen` -> was `failure` (FileTooBig from ftruncate on
stdout pipe) -> `success`
- `compile exe framegen` -> was `1 errors` (scandir undeclared) ->.
`success`
**Remaining failures (pre-existing, fixed by later PRs in stack):**
- `translate-c` -> 3 errors (`ssize_t` unknown in ghostty.h on MSVC)
- `compile lib freetype` -> 2 errors (`unistd.h` not found)
### Linux/macOS: no regressions
Identical pass counts and test results before and after.
## Discussion points
### "Grep wider" other `stdout().writer()` callsites
There are 15+ other `stdout().writer(&buf)` callsites in the codebase.
Build-time generators that capture stdout (webgen, mdgen, unicode
generators) would have the same `ftruncate` issue if they ran on
Windows. Currently they don't appear in the Windows build graph, but
worth noting for future Windows work.
### `writerStreaming()` vs `writer()`
`writer()` calls `ftruncate` on flush/end to set the file size, which
fails on pipes (stdout captured by the build system).
`writerStreaming()` skips the truncate since the output goes to a pipe,
not a seekable file. This is the correct API for this use case on all
platforms, not just Windows.
## What I Learnt
- When upstream has applied and reverted something, state what you
observe rather than speculating about their reasoning. Let the reviewer
fill in context you don't have.
- "Grep wider" (testing pattern): `stdout().writer()` appears in 17
files. Only 2 are fixed here because only 2 are in the current Windows
build path. But the pattern exists more broadly.
- I feel like I am training my replacements. I mean, I am a parent, it
rhymes.
- I feel like my replacements are training me. It rhymes as well.
> [!WARNING]
> Review/approve this AFTER #11798 and #11800 (this PR stacks on top of
rhem... ergo, it includes their commits)
> Don't cheat! Start from the oldest one! 😄 I know these are almost
one-liners but I am doing this mostly for documentation and karma
points.
## Summary
- Conditionally skip `linkLibCpp()` on MSVC since Zig's bundled libc++
headers conflict with MSVC's own C++ runtime
- Add `-std=c++17` flag for C++17 features (std::variant,
std::filesystem, inline variables) that glslang requires
## Context
The exact same `linkLibCpp` fix was applied to `simdutf` and `highway`
in commits 3d581eb92 and b4c529a82 but glslang was missed. Without this
fix, glslang fails with 297 compilation errors on MSVC.
Thanks Claude for the forensic digging. A carpenter should always be
thankful for his tools. Even if they are borrowed, maybe even more so.
## Stack
Stacked on 011-windows/fix-oniguruma-msvc.
## Discussion points
**`-std=c++17` scope:** Currently added unconditionally for all targets.
Tested on all three platforms with no regressions, but since this is
specifically fixing a Windows/MSVC issue, it could be gated behind
`target.result.abi == .msvc`. Donno. The reason it works unconditionally
is that Zig's bundled clang already defaults to C++17 on non-MSVC
targets, so the flag is a no-op there. Open to either approach.
**Other packages with bare `linkLibCpp()`:** The same `linkLibCpp` guard
has been applied to `simdutf`, `highway`, `utfcpp`, and now `glslang`.
However, `spirv-cross`, `dcimgui`, `harfbuzz`, and `breakpad` still have
unconditional `linkLibCpp()` calls. These may need the same treatment
when they become buildable on MSVC (some are currently blocked by other
issues like freetype's `unistd.h`). Worth tracking as a follow-up?
## Test plan
### test-lib-vt
| | Windows | Linux | Mac |
|---|---|---|---|
| **BEFORE** | 3791/3839 passed, 48 skipped | 3791/3839 passed, 48
skipped | 3807/3839 passed, 32 skipped |
| **AFTER** | 3791/3839 passed, 48 skipped | 3791/3839 passed, 48
skipped | 3807/3839 passed, 32 skipped |
| **Delta** | no change | no change | no change |
### all tests (`zig build test` / `zig build -Dapp-runtime=none test` on
Windows)
| | Windows | Linux | Mac |
|---|---|---|---|
| **BEFORE** | FAIL — 38/51 build steps, 5 failed | 2655/2678 passed, 23
skipped (86/86 steps) | 2655/2662 passed, 7 skipped (160/160 steps) |
| **AFTER** | FAIL — 39/51 build steps, 4 failed | 2655/2678 passed, 23
skipped (86/86 steps) | 2655/2662 passed, 7 skipped (160/160 steps) |
| **Delta** | +1 build step (glslang unblocked) | no change | no change
|
- Zero regressions on any platform
- Windows improved: glslang now compiles (38 -> 39 steps, 5 -> 4
failures)
- Remaining 4 Windows failures (`helpgen`, `framegen`, `freetype`,
`translate-c`) are addressed by other PRs in the stack
## What I Learnt
- **MSVC's clang doesn't default to C++17.** Zig's bundled clang uses
C++17 by default on Linux/Mac, but when targeting MSVC, the C++ standard
needs to be specified explicitly. Without `-std=c++17`, features like
`std::variant`, `std::filesystem`, and `inline` variables are gated
behind `_HAS_CXX17` and won't compile.
- **`linkLibCpp` conflicts with MSVC headers.** Zig's `linkLibCpp`
passes `-nostdinc++` and adds its own libc++/libc++abi headers, which
collide with the C++ headers already provided by the MSVC SDK through
`linkLibC`. On MSVC, you don't need `linkLibCpp` at all since the SDK
includes both C and C++ headers. I think yesterday we dealt with
something similar. Windows is fun. 🫠 Unironically and chronically.
- **Grep wider.** The `linkLibCpp` guard was already applied to simdutf,
highway, and utfcpp but missed glslang. When a fix follows a repeated
pattern across packages, search the whole codebase before declaring it
complete.
> [!WARNING]
> Review/approve this AFTER #11798 (this PR stacks on top of it... ergo,
it includes its commits)
## Summary
- Conditionally disable POSIX-only header defines (`alloca.h`,
`sys/times.h`, `sys/time.h`, `unistd.h`) on Windows since they do not
exist with MSVC
- Enable `USE_CRNL_AS_LINE_TERMINATOR` on Windows for correct line
endings
## Context
Oniguruma's `config.h` template had all POSIX header availability
defines hardcoded to `true`. On MSVC, these headers don't exist, causing
24 compilation errors (all `alloca.h` file not found).
Uses a comptime `is_windows` constant to flip the config values, same
pattern as PR #11798 (zlib).
## Stack
Stacked on 010-windows/fix-zlib-msvc.
## Test plan
### test-lib-vt
| | Windows | Linux | Mac |
|---|---|---|---|
| **BEFORE** | 3791/3839 passed, 48 skipped | 3791/3839 passed, 48
skipped | 3807/3839 passed, 32 skipped |
| **AFTER** | 3791/3839 passed, 48 skipped | 3791/3839 passed, 48
skipped | 3807/3839 passed, 32 skipped |
| **Delta** | no change | no change | no change |
### all tests (`zig build test` / `zig build -Dapp-runtime=none test` on
Windows)
| | Windows | Linux | Mac |
|---|---|---|---|
| **BEFORE** | FAIL — 37/51 steps, 6 failed | 2655/2678 passed, 23
skipped (86/86 steps) | 2655/2662 passed, 7 skipped (160/160 steps) |
| **AFTER** | FAIL — 38/51 steps, 5 failed | 2655/2678 passed, 23
skipped (86/86 steps) | 2655/2662 passed, 7 skipped (160/160 steps) |
| **Delta** | +1 step, -1 failure (oniguruma unblocked) | no change | no
change |
- Zero regressions on any platform
- Windows improved: oniguruma now compiles (37 -> 38 steps, 6 -> 5
failures)
- Remaining 5 Windows failures (`translate-c`/ssize_t, `helpgen`,
`framegen`, `glslang`, `harfbuzz` via freetype) are addressed by other
PRs in the stack
## What I Learnt
- comptime, man. It's the small things.
## Summary
- Gate `Z_HAVE_UNISTD_H` behind a non-Windows check since `unistd.h`
does not exist with MSVC
- Add `_CRT_SECURE_NO_DEPRECATE` and `_CRT_NONSTDC_NO_DEPRECATE` for
MSVC to suppress deprecation errors for standard C functions that zlib
uses
## Context
Part of the effort to get `zig build -Dapp-runtime=none test` passing on
Windows. This unblocks freetype, harfbuzz, libpng, and dcimgui which all
depend on zlib.
My research shows that we should default to msvc in ci with zig build
ran without `-Dratget`.
## Stack
This is branch 010 in the stacked branches series (soon on Netflix).
Independent fix, no dependencies on other branches.
## Test plan
### test-lib-vt
| | Windows | Linux | Mac |
|---|---|---|---|
| **BEFORE** | 3791/3839 passed, 48 skipped | 3791/3839 passed, 48
skipped | 3807/3839 passed, 32 skipped |
| **AFTER** | 3791/3839 passed, 48 skipped | 3791/3839 passed, 48
skipped | 3807/3839 passed, 32 skipped |
| **Delta** | no change | no change | no change |
### all tests (`zig build test` / `zig build -Dapp-runtime=none test` on
Windows)
| | Windows | Linux | Mac |
|---|---|---|---|
| **BEFORE** | FAIL — 35/51 build steps, 6 failed | 2655/2678 passed, 23
skipped (86/86 steps) | 2655/2662 passed, 7 skipped (160/160 steps) |
| **AFTER** | FAIL — 37/51 build steps, 6 failed | 2655/2678 passed, 23
skipped (86/86 steps) | 2655/2662 passed, 7 skipped (160/160 steps) |
| **Delta** | +2 build steps (zlib + png unblocked) | no change | no
change |
- Zero regressions on any platform
- Windows improved: zlib and png now compile (35 -> 37 steps)
- Remaining 6 Windows build failures (`ssize_t`, `helpgen`, `framegen`,
`harfbuzz`, `dcimgui`) are addressed by other PRs in the stack
## What I Learnt
- Always run tests with `--summary all` to get actual pass/skip/fail
counts. Without it, zig just exits 0 or 1 and you have no numbers to
compare. "You get confident if you got the numbers."
- Build dependencies cascade: fixing zlib also unblocked png (which
depends on it), giving us +2 build steps from a one-file change.
Gate HAVE_UNISTD_H and HAVE_FCNTL_H behind a non-Windows check since
these headers do not exist with MSVC. Freetype includes zlib headers
which conditionally include unistd.h based on this define.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use writerStreaming() instead of writer() for stdout in helpgen and
main_build_data. The positional writer calls setEndPos/ftruncate in
end(), which fails on Windows because ftruncate on pipes maps
INVALID_PARAMETER to FileTooBig.
Replace scandir with opendir/readdir plus qsort in framegen since
scandir is a POSIX extension not available on Windows.
This was previously applied and reverted upstream (f4998c6ab, 0fdddd5bc)
as collateral from an unrelated example-execution hang that has since
been resolved.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Apply the same MSVC fixes used for simdutf and highway: conditionally
skip linkLibCpp on MSVC since Zig's bundled libc++ headers conflict
with MSVC's own C++ runtime, and add -std=c++17 for C++17 features
like std::variant and inline variables that glslang requires.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Conditionally disable POSIX-only header defines (alloca.h, sys/times.h,
sys/time.h, unistd.h) on Windows since they do not exist with MSVC.
Enable USE_CRNL_AS_LINE_TERMINATOR on Windows for correct line endings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Gate Z_HAVE_UNISTD_H behind a non-Windows check since unistd.h does
not exist on Windows. Add _CRT_SECURE_NO_DEPRECATE and
_CRT_NONSTDC_NO_DEPRECATE for MSVC to suppress deprecation errors
for standard C functions that zlib uses.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Windows tests and builds are now passing reliably. Remove the
continue-on-error safety net so failures are visible immediately.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# What
PR #11756 added IMPORTED_IMPLIB pointing to the .lib import library, but
the
import library is not listed in the OUTPUT directive of the
`add_custom_command`
that runs zig build. The file is produced as a side-effect of the build.
This works with the Visual Studio generator (which is lenient about
undeclared outputs) but fails with Ninja:
ninja: error: 'zig-out/lib/ghostty-vt.lib', needed by 'ghostling',
missing and no known rule to make it
The fix adds "${ZIG_OUT_DIR}/lib/${GHOSTTY_VT_IMPLIB}" to the OUTPUT
list. No
behavioral change. The file was already being built, Ninja just needs to
know
about it.
## but_why.gif
I am cleaning up https://github.com/ghostty-org/ghostling/pull/6 and I
realise that in order to get rid of the CMake workarounds we had before
#11756, this change is necessary.
# POC
I set up a branch pointing at my fork with a POC and it builds, this is
the cleaned up CMakeList
https://github.com/deblasis/winghostling/blob/test/cmake-implib-no-workaround/CMakeLists.txt
Ninja requires all produced files to be listed as explicit outputs of
custom commands. The zig build produces a .lib import library alongside
the DLL, but it was not listed in the OUTPUT directive. This causes
Ninja to fail with "missing and no known rule to make it" when
IMPORTED_IMPLIB references the .lib file.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## What
On Windows, calling `free()` on memory allocated by libghostty crashes
because Zig and MSVC use separate heaps.
This adds `ghostty_free()` so consumers can free library-allocated
memory safely on all platforms.
## Why
When Zig builds a DLL on Windows with `link_libc = true`, it does not
link the Windows C runtime (`ucrtbase.dll`). Instead it uses its own
libc built on top of `KERNEL32.dll`. So `builtin.link_libc` is true and
`c_allocator` is selected, but Zig's `malloc` and MSVC's `malloc` are
different implementations with different heaps. 💥
On Linux/macOS this is not a problem because Zig links the system libc
and everyone shares the same heap. On Windows, `free(buf)` from MSVC
tries to free memory from Zig's heap and you get a debug assertion
failure or undefined behavior.
The `format_alloc` docs said "the buffer can be freed with `free()`" but
that is only true when the library and consumer share the same C
runtime, which is not the case on Windows.
## How
- Add `ghostty_free(allocator, ptr, len)` that frees through the same
allocator that did the allocation
- Update `format_alloc` docs to point to `ghostty_free()` instead of
`free()`
- Update all 3 examples to use `ghostty_free(NULL, buf, len)`
The signature takes an allocator because raw buffers (unlike objects
like terminals or formatters) do not store their allocator internally.
The caller already has all three values: the allocator they passed, the
pointer, and the length they got back.
I went back and forth on the naming. Other options I considered:
`ghostty_alloc_free(allocator, ptr, len)` or returning a `GhosttyBuffer`
wrapper with its own `_free`. Happy to change the naming if there is a
preference.
No impact on Linux/macOS. `ghostty_free()` works correctly there too, it
just happens to call the same `free()` the consumer would have called
anyway.
## Verified
- `zig build test-lib-vt` passes on Windows, macOS arm64, Linux x86_64
(exit 0)
- `zig build test` passes on Windows (2575/2619 passed, 1 pre-existing
font sprite failure) and macOS (exit 0)
- cmake shared example builds, links, and runs correctly on Windows with
`ghostty_free()` (no more heap crash)
## What I Learnt
- What I wrote in Why
- Zig allocators require the length to free (no hidden metadata headers
like C's malloc). This is a deliberate design choice for explicit
control.
- The standard pattern for C libraries on Windows is "whoever allocates,
frees" (like `curl_free()`, `SDL_free()`). This avoids cross-runtime
heap issues entirely.
Add a ghostty_alloc function that pairs with the existing
ghostty_free, giving embedders a symmetric malloc/free-style
API for buffer allocation through the libghostty allocator
interface. Returns NULL on allocation failure.
Extract the inline free_alloc function from main.zig into a new
allocator.zig module in the C API layer. The function is renamed
to alloc_free in main.zig (and free in allocator.zig) for
consistency with the other C API naming conventions. Add tests
for null pointer, allocated memory, and null allocator fallback.
Renames `ReadonlyStream` to `TerminalStream` and introduces an
effects-based callback system so that the stream handler can optionally
respond to queries and side effects (bell, title changes, device
attributes, device status, size reports, XTVERSION, ENQ, DECRQM, kitty
keyboard queries).
The default behavior is still read-only, callers have to opt-in to
setting callbacks to function pointers.
This doesn't handle every possible side effect yet, e.g. this doesn't
include clipboards, pwd reporting, and others. But this covers the
important ones.
This PR is Zig only, the C version of this will come later.
The default firmware_version for Secondary device attributes is 0,
but the test expected a value of 10. Update the test expectation to
match the actual default.
Add a device_attributes effect callback to the stream_terminal
Handler. The callback returns a device_attributes.Attributes
struct which the handler encodes and writes back to the pty.
Add Attributes.encode which dispatches to the correct sub-type
encoder based on the request type (primary, secondary, tertiary).
In readonly mode the callback is null so all DA queries are
silently ignored, matching the previous behavior where
device_attributes was in the ignored actions list.
Tests cover all three DA types with default attributes, custom
attributes, and readonly mode.
Introduce a dedicated device_attributes.zig module that consolidates
all device attribute types and encoding logic. This moves
DeviceAttributeReq out of ansi.zig and adds structured response
types for DA1 (primary), DA2 (secondary), and DA3 (tertiary) with
self-encoding methods.
Primary DA uses a ConformanceLevel enum covering VT100-series
per-model values and VT200+ conformance levels, plus a Feature
enum with all known xterm DA1 attribute codes (132-col, printer,
sixel, color, clipboard, etc.) as a simple slice. Secondary DA
uses a DeviceType enum matching the xterm decTerminalID values.
Tertiary DA encodes the DECRPTUI unit ID as a u32 formatted to
8 hex digits.
This is preparatory work for exposing device attributes through
the stream_terminal Effects callback system.
Previously device_status was in the ignored "no terminal-modifying
effect" group in stream_terminal.zig. This ports it to use the
Effects pattern, handling all three DSR request types.
Operating status and cursor position are handled entirely within
stream_terminal since they only need terminal state and write_pty.
Cursor position respects origin mode and scrolling region offsets.
Color scheme adds a new color_scheme effect callback that returns
a ColorScheme enum (light/dark). The handler encodes the response
internally, keeping protocol knowledge in the terminal layer. A
new ColorScheme type is added to device_status.zig so the terminal
layer does not depend on apprt.
Previously the ENQ (0x05) action was ignored in stream_terminal,
listed in the no-op group alongside other unhandled queries. The
real implementation in termio/stream_handler writes a configurable
response string back to the pty.
Add an enquiry callback to Effects following the same query-style
pattern as xtversion: the callback returns the raw response bytes
and the handler owns writing them to the pty via writePty. When no
callback is set (readonly mode), ENQ is silently ignored. Empty
responses are also ignored. The response is capped at 256 bytes
using a stack buffer with sentinel conversion for writePty.
## What
Two fixes for tests that fail on Windows due to Unix-specific
assumptions.
1. The "cache directory paths" test in xdg.zig hardcodes Unix paths like
`/Users/test/.cache` in expected values. The function under test uses
`std.fs.path.join` which produces native separators, so the expectations
need to match. Fixed by using `std.fs.path.join` for expected values
too, with a platform-appropriate mock home path.
2. Two shell integration tests for `setupXdgDataDirs` hardcode Unix path
separators (`:`) and Unix default paths (`/usr/local/share:/usr/share`).
These are not applicable on Windows where the delimiter is `;` and
`XDG_DATA_DIRS` is not a standard concept. Skipped on Windows with
`SkipZigTest`.
## Why skip instead of fix for the shell integration tests?
`setupXdgDataDirs` is used by fish, elvish, and nushell. On Windows,
`XDG_DATA_DIRS` is not standard. The equivalent would be `%ProgramData%`
(what Go's `adrg/xdg`, Python's `platformdirs`, and others map to).
Fixing this properly means adding a Windows-appropriate default, which
is a separate change. (How do you guys deal with these situations? Do
you create issues on the spot as reminders or do you wait for the
requirement to emerge by itself when the time comes?
Worth noting: the production code on line 664 of `shell_integration.zig`
hardcodes the fallback to `"/usr/local/share:/usr/share"` with `:`
separators, while `prependEnv` correctly uses `std.fs.path.delimiter`
(`;` on Windows). If a shell that uses this runs on Windows, you would
get mixed separators. Tracked separately.
## Verified
- `zig build test-lib-vt` passes on Windows (exit 0)
- No behavior change on Linux/macOS (xdg.zig fix produces same paths,
shell_integration skip only triggers on Windows)
## What I Learnt
- `std.fs.path.join` uses the native path separator, so tests that
hardcode `/` in expected paths will fail on Windows even if the
production code is correct. Better to use `path.join` in test
expectations too.
- The XDG Base Directory spec is Unix-only but cross-platform libraries
have converged on mappings. Ghostty maps to `%LOCALAPPDATA%` which
matches common conventions. The missing piece is `XDG_DATA_DIRS` which
has no Windows default and falls through to Unix paths.
Add a `size` callback to the stream_terminal Effects struct that
returns a size_report.Size geometry snapshot for XTWINOPS size
queries (CSI 14/16/18 t). The handler owns all protocol encoding
using the existing size_report.encode, keeping VT knowledge out
of effect consumers. This follows the same pattern as the xtversion
effect: the callback supplies data, the handler formats the reply
and calls write_pty.
CSI 21 t (title report) is handled internally from terminal state
since the title is already available via terminal.getTitle() and
does not require an external callback.
Add an xtversion callback to the Effects struct so that
stream_terminal can respond to XTVERSION queries. The callback
returns the version string to embed in the DCS response. If the
callback is unset or returns an empty string, the response defaults
to "libghostty". The response is formatted and written back via the
existing write_pty effect.
Previously kitty_keyboard_query was listed as a no-op in the
readonly stream handler. This implements it using the write_pty
effect callback so that the current kitty keyboard flags are
reported back via the pty, matching the behavior in the full
stream handler.
The effect callback no longer receives the title string directly.
Instead, the handler stores the title in terminal state via setTitle
before invoking the callback, so consumers query it through
handler.terminal.getTitle(). This removes the redundant parameter
and keeps the effect signature consistent with the new terminal
title field. Tests now verify terminal state directly rather than
tracking the title through the callback.
Add a title field to Terminal, mirroring the existing pwd field.
The title is set via setTitle/getTitle and tracks the most recent
value written by OSC 0/2 sequences. The stream handler now persists
the title in terminal state in addition to forwarding it to the
surface. The field is cleared on full reset.
Previously the window_title action was silently ignored in the
readonly stream handler. Add a set_window_title callback to the
Effects struct so callers can be notified when a window title is
set via OSC 2. Follows the same pattern as bell and write_pty
where the callback is optional and defaults to null in readonly
mode.
Add a generic write_pty effect callback to the stream terminal
handler, allowing callers to receive pty response data. Use it to
implement request_mode and request_mode_unknown (DECRQM), which
encode the mode state as a DECRPM response and write it back
through the callback. Previously these were silently ignored.
The write_pty data is stack-allocated and only valid for the
duration of the call.
Rename stream_readonly.zig to stream_terminal.zig and its exported
types from ReadonlyStream/ReadonlyHandler to TerminalStream. The
"readonly" name is now wrong since the handler now supports
settable effects callbacks. The new name better reflects that this
is a stream handler for updating terminal state.
Add an Effects struct to the readonly stream Handler that allows
callers to provide optional callbacks for side effects like bell.
Previously, the bell action was silently ignored along with other
query/response actions. Now it is handled separately and dispatched
through the effects callback if one is provided.
Add a test that verifies bell with a null callback (default readonly
behavior) does not crash, and that a provided callback is invoked
the correct number of times.
## What
Skip the `expandHomeUnix` test on Windows with `SkipZigTest`.
`expandHomeUnix` is a Unix-internal function that is never called on
Windows. The public `expandHome` already returns the path unchanged on
Windows (added upstream in cccdb0d2a). But the unit test calls
`expandHomeUnix` directly, which invokes `home()` and expects Unix-style
forward-slash separators, so it fails on Windows.
## How
Two lines:
```zig
if (builtin.os.tag == .windows) return error.SkipZigTest;
```
## Verified
- `zig build test-lib-vt` passes on Windows (exit 0)
- No behavior change on Linux/macOS
## What I Learnt
- When upstream adds a platform dispatch for production code (like
`expandHome` returning unchanged on Windows), the unit tests for
internal platform-specific functions (like `expandHomeUnix`) may still
need a skip guard.
- Zig doesn't have something like Go's `//go:build` but damn... comptime
is insane, like supercharged C# `#if`
On Windows, Zig's built-in libc and MSVC's CRT maintain separate
heaps, so calling free() on memory allocated by the library causes
undefined behavior. Add ghostty_free() that frees through the same
allocator that performed the allocation, making it safe on all
platforms.
Update format_alloc docs and all examples to use ghostty_free()
instead of free().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On Windows, shared libraries (DLLs) require an import library (.lib) for
linking, and the DLL itself is placed in bin/ rather than lib/ by the
Zig build. The CMake wrapper was missing IMPORTED_IMPLIB on the shared
imported target, causing link failures, and assumed the shared library
was always in lib/.
Add GHOSTTY_VT_IMPLIB for the import library name, set IMPORTED_IMPLIB
on the ghostty-vt target, and fix the shared library path to use bin/ on
Windows. Install the DLL and PDB to bin/ and the import library to lib/
following standard Windows conventions. Apply the same fixes to
ghostty-vt-config.cmake.in for the find_package path.
The "Run Example" step in the build-examples-cmake-windows job
hangs, so remove it entirely. The build step is still run so
compilation is verified, but the examples are no longer executed
on Windows.
The SIMD C++ files reference __ubsan_handle_* symbols when compiled
in debug mode, but we do not link or bundle the ubsan runtime on
MSVC. This matches what the highway and simdutf packages already do
in their own build files.
expandHomeUnix is a Unix-internal function that is never called on
Windows. The public expandHome function returns the path unchanged
on Windows since ~/ is not a standard Windows idiom. The test calls
expandHomeUnix directly, which invokes home() and expects Unix-style
forward-slash separators.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The SIMD C++ files use C++17 features (std::optional, std::size).
With Zig's bundled libc++ these are available implicitly, but MSVC
headers guard C++17 features behind the standard version
(_HAS_CXX17). Without an explicit -std=c++17 flag, clang defaults
to a lower standard and the MSVC <optional> header does not define
std::optional.
When compiling C++ files, Zig unconditionally passes -nostdinc++ and,
if link_libcpp is set, adds its bundled libc++/libc++abi include paths
as replacements (see Compilation.zig). On MSVC targets this conflicts
with the MSVC C++ runtime headers (vcruntime_typeinfo.h,
vcruntime_exception.h, etc.), causing compilation failures in SIMD
C++ code.
The fix is to use linkLibC instead of linkLibCpp on MSVC. Zig always
passes -nostdinc to strip default search paths, but LibCDirs.detect
re-adds the MSVC SDK include directories, which contain both C and
C++ standard library headers. This gives us proper access to MSVC's
own <optional>, <iterator>, <cstddef>, etc. without the libc++
conflicts.
For the package builds (highway, simdutf, utfcpp) this means
switching from linkLibCpp to linkLibC on MSVC. For SharedDeps and
GhosttyZig, linkLibC is already called separately, so we just skip
linkLibCpp.
Make the "cache directory paths" test cross-platform by using
std.fs.path.join for expected values and a platform-appropriate
mock home path, since the function under test uses native path
separators.
Skip the two shell integration XDG_DATA_DIRS tests on Windows.
These tests use hardcoded Unix path separators (:) and Unix default
paths (/usr/local/share:/usr/share) which are not applicable on
Windows where the path delimiter is ; and XDG_DATA_DIRS is not a
standard concept.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Zig's bundled libc++/libc++abi conflicts with the MSVC C++ runtime
headers (vcruntime_typeinfo.h, vcruntime_exception.h, etc.) when
targeting native-native-msvc. This caused compilation failures in
the SIMD C++ code due to -nostdinc++ suppressing MSVC headers and
libc++ types clashing with MSVC runtime types.
Skip linkLibCpp() for MSVC targets across all packages (highway,
simdutf, utfcpp) and the main build (SharedDeps, GhosttyZig) since
MSVC provides its own C++ standard library natively. Also add
missing <iterator> and <cstddef> includes that were previously
pulled in transitively through libc++ headers but are not
guaranteed by MSVC's headers.
Zig defaults to the GNU ABI on Windows, which produces COFF objects
with invalid COMDAT sections in compiler_rt that the MSVC linker
rejects (LNK1143), and uses GNU conventions like ___chkstk_ms that
are unavailable in the MSVC CRT.
Default to the MSVC ABI when no explicit ABI is requested, following
the same pattern as the existing macOS target override. This ensures
compiler_rt produces valid COFF and the generated code uses
MSVC-compatible symbols. Users can still explicitly request the GNU
ABI via -Dtarget.
Also disable bundling ubsan_rt on Windows (its /exclude-symbols
directives are MSVC-incompatible) and add ntdll and kernel32 as
transitive link dependencies for the static library.
Three issues when linking the static library with the MSVC linker:
Use the LLVM backend on Windows to produce valid COFF objects.
The self-hosted backend generates compiler_rt objects with invalid
COMDAT sections that the MSVC linker rejects (LNK1143).
Disable bundling ubsan_rt on Windows. Zig's ubsan runtime emits
/exclude-symbols linker directives that MSVC does not understand
(LNK4229).
Add ntdll and kernel32 as transitive link dependencies for the
static library on Windows. The Zig standard library uses NT API
functions (NtClose, NtCreateSection, etc.) that consumers must
link.
CommaSplitter treats backslash as an escape character, which breaks
Windows paths like C:\Users\foo since \U is not a valid escape. On
Windows, treat backslash as a literal character outside of quoted
strings. Inside quotes, escape sequences still work as before.
The platform behavior is controlled by a single comptime constant
(escape_outside_quotes) so the logic lives in one place. Escape-specific
tests are skipped on Windows with SkipZigTest, and Windows-specific
tests are added separately.
Also fix Theme.parseCLI to not mistake the colon in a Windows drive
letter (C:\...) for a light/dark theme pair separator.
Note: other places in the config parsing also use colon as a delimiter
without accounting for Windows drive letters (command.zig prefix
parsing, keybind parsing). Those are tracked separately.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Zig's compiler_rt produces COFF objects with invalid COMDAT
sections that the MSVC linker rejects (LNK1143), and its ubsan_rt
emits /exclude-symbols directives that MSVC does not understand
(LNK4229). Skip bundling both in the static library on Windows
since the MSVC CRT provides the needed builtins (memcpy, memset,
etc.). The shared library continues to bundle compiler_rt as it
needs to be self-contained.
Zig's ubsan runtime emits /exclude-symbols linker directives that
are incompatible with the MSVC linker, causing LNK4229 warnings and
LNK1143 errors. Disable bundling ubsan_rt on Windows while keeping
compiler_rt which provides essential symbols like memcpy, memset,
memmove, and ___chkstk_ms.
The previous check used target.result.abi == .msvc which never
matched because Zig defaults to the gnu ABI on Windows.
Zig's ubsan instrumentation emits ELF-style /exclude-symbols linker
directives into the compiled object files, causing LNK4229 warnings
with the MSVC linker. The bundled compiler_rt also produces COMDAT
sections that are incompatible with MSVC, causing fatal LNK1143.
Disable sanitize_c entirely on the root module for MSVC targets and
skip bundling both compiler_rt and ubsan_rt since MSVC provides its
own runtime.
Zig's bundled compiler_rt and ubsan_rt produce object files with
ELF-style linker directives (/exclude-symbols) and COMDAT sections
that are incompatible with the MSVC linker, causing LNK1143 and
LNK4229 errors when linking the static library.
MSVC provides its own compiler runtime so bundling Zig's versions
is unnecessary. Skip bundling both runtimes when the target ABI is
MSVC.
The cmake examples were failing at runtime on Windows CI for two
reasons.
The static library was installed as "libghostty-vt.a" on all
platforms, but on Windows the DLL import library is also placed in
zig-out/lib/ as "ghostty-vt.lib". The CMakeLists.txt expected the
platform-native name "ghostty-vt.lib" for the static lib, so it
picked up the tiny DLL import lib instead, silently producing a
dynamically-linked executable. That executable then failed at
runtime because the DLL was not on PATH.
Fix this by installing the static library as "ghostty-vt-static.lib"
on Windows to avoid the name collision, and updating CMakeLists.txt
to match. For the shared (DLL) example, add zig-out/bin to PATH in
the CI run step so the DLL can be found at runtime.
Add a "Run Example" step to the build-examples-cmake-windows job
so that each CMake example is executed after it is built, verifying
the resulting binaries actually work. The executable name is derived
from the matrix directory name by replacing hyphens with underscores,
matching the project convention.
Use writerStreaming() instead of writer() for stdout in helpgen and
main_build_data. The positional writer calls setEndPos/ftruncate in
end(), which fails on Windows when stdout is redirected via
captureStdOut() because ftruncate maps INVALID_PARAMETER to
FileTooBig. Streaming mode skips truncation entirely since stdout
is inherently a sequential stream.
Replace scandir with opendir/readdir plus qsort in framegen since
scandir is a POSIX extension not available on Windows.
On Windows, shared libraries (DLLs) require an import library (.lib)
for linking, and the DLL itself is placed in bin/ rather than lib/ by
the Zig build. The CMake wrapper was missing IMPORTED_IMPLIB on the
shared imported target, causing link failures, and assumed the shared
library was always in lib/.
Add GHOSTTY_VT_IMPLIB for the import library name, set IMPORTED_IMPLIB
on the ghostty-vt target, and fix the shared library path to use bin/
on Windows. Install the DLL and PDB to bin/ and the import library to
lib/ following standard Windows conventions. Apply the same fixes to
ghostty-vt-config.cmake.in for the find_package path.
Our Windows build has been broken for a _long_ time. It hasn't actually
worked and our CI was falsely passing when it was actually failing to
build/test. This PR fixes that and fixes the issues it found so
`libghostty-vt` can build and pass tests.
**This is only for libghostty!** I'd still like to expand our _test_
coverage to all of Ghostty for Windows but libghostty is more important
for that platform in the short term and it's an incremental piece of
work.
A couple windows compatibility issues fixed:
- `terminal.Page` uses `VirtualAlloc` on Windows (thanks @deblasis)
- Our rgb.txt loading was not resilient to CRLF endings
The X11 color map parser in x11_color.zig uses @embedFile to load
rgb.txt at comptime, then splits on \n. On Windows, git may check
out rgb.txt with CRLF line endings, leaving a trailing \r on each
line. This caused color names to be stored as e.g. "white\r" instead
of "white", so all X11 color lookups failed at runtime.
Strip trailing \r from each line before parsing. Also mark rgb.txt
as -text in .gitattributes to prevent line ending conversion in
future checkouts.
Extract the platform-specific page backing memory allocation into
AllocPosix and AllocWindows structs behind a PageAlloc comptime
switch. Previously, POSIX mmap calls were inlined at each call
site. This adds a Windows VirtualAlloc implementation and routes
all allocation through PageAlloc.alloc/free, making the backing
memory strategy consistent and easier to extend.
Rename build-windows to build-libghostty-vt-windows to reflect that
it only builds and tests libghostty-vt for now, and move it next to
the other build-libghostty-vt jobs.
Replace the manual PowerShell zig download/install with mlugg/setup-zig,
which auto-detects the version from build.zig.zon and handles caching.
Upgrade the runner from windows-2022 to windows-2025. Remove the
generated-script-to-swallow-errors pattern in favor of direct zig
build commands.
- Our `checkGhosttyH` calls need to be guarded on building Ghostty app
which has it
- Move FileFormatter to its own file to avoid poisoning test refs with
Config.zig which pulls in the world
- Move WindowPaddingBalance to renderer to avoid pulling in Config.zig
- Add a `zig build test-lib-vt` CI job
The colors_get function used structSizedFieldFits to guard the
palette copy, which required the entire palette array to fit in the
provided size. This prevented partial palette writes when the caller
passed a truncated sized struct, since the guard failed even though
the inner code already handled partial copies correctly. Remove the
outer guard so the existing partial-copy logic applies.
The setopt_from_terminal test expected alt_esc_prefix to be false on
a fresh terminal, but the mode definition in modes.zig sets its
default to true. Update the test expectation to match.
Add a new CI job that runs `zig build test-lib-vt` to test the
lib-vt build step. The job mirrors the existing test job structure
with the same nix/cachix setup and skip conditions. It is also
added to the required checks list.
The inline else switch in each C API getter expands the .invalid
case, which has OutType void. When called with .invalid and a null
out pointer, the @ptrCast(@alignCast(out)) panics before getTyped
can return early.
Handle .invalid explicitly in the outer switch of every getter to
short-circuit before the pointer cast. This affects build_info,
cell, row, terminal, osc, and render (three getters).
Previously WindowPaddingBalance was defined inside Config.zig, which
meant tests for renderer sizing had to pull in the full config
dependency. Move the enum into renderer/size.zig as PaddingBalance
and re-export it from Config so the public API is unchanged. This
lets size.zig tests run without depending on Config.
This is a continuation of the solid work done by @jcollie in PR #7864. I
checked with him if I could take over to continue the implementation.
His changes of last year have been adapted to be compatible with the
current GTK implementation. Aside from just "making it work", I also
dived into the portals and OpenURI implementation and made some
improvements there.
Notable improvements were:
- Improved lifecycle management of glib resources in the OpenURI
implementation
- More forgiving error handling in OpenURI implementation by adding more
fallbacks
- Fixed some memory leaks
- Less memory allocations in Portals implementation
- Added tests for building the Portals request path
Fixes#5991
The README hasn't been updated in years basically!
This updates the README to make libghostty a first class citizen of the
project and to update our roadmap and goals for the project to more
accurately reflect our current state and future plans.
I notably updated our roadmap to be more accurate to our state, e.g.
we're stable now. I removed Windows because it's not a short term focus
and I think libghostty is more important and enables that ecosystem a
lot more (libghostty itself being already compatible with Windows). I
also expanded on "fancy features" and clarified its to make
Ghostty-specific sequences.
The README hasn't been updated in years basically!
This updates the README to make libghostty a first class citizen of the
project and to update our roadmap and goals for the project to more
accurately reflect our current state and future plans.
Fixes#11705
Add bg_color and fg_color options to GhosttyRenderStateRowCellsData that
resolve the final RGB color for a cell, flattening the multiple possible
sources. For background, this handles content-tag bg_color_rgb,
content-tag bg_color_palette (looked up in the palette), and the style
bg_color. For foreground, this resolves palette indices through the
palette; bold color handling is not applied and is left to the caller.
Both return GHOSTTY_INVALID_VALUE when no explicit color is set, in
which case the caller should fall back to whatever default color it
wants (e.g. the terminal background/foreground).
Fixes#11704
The RenderState empty initializer set both background and foreground to
the default RGB value of black (0, 0, 0), making text unreadable when a
caller has not explicitly configured terminal colors via DynamicRGB.
This is the common case for libghostty consumers.
Default the foreground to white so that the initial render state
provides readable white-on-black text out of the box.
Long term we also need to expose setting the default colors for a
Terminal instance but this is a workable fix in the mean time.
Fixes#11705
Add bg_color and fg_color options to GhosttyRenderStateRowCellsData
that resolve the final RGB color for a cell, flattening the multiple
possible sources. For background, this handles content-tag bg_color_rgb,
content-tag bg_color_palette (looked up in the palette), and the
style bg_color. For foreground, this resolves palette indices through
the palette; bold color handling is not applied and is left to the
caller.
Both return GHOSTTY_INVALID_VALUE when no explicit color is set, in
which case the caller should fall back to whatever default color it
wants (e.g. the terminal background/foreground).
Fixes#11704
The RenderState empty initializer set both background and foreground
to the default RGB value of black (0, 0, 0), making text unreadable
when a caller has not explicitly configured terminal colors via
DynamicRGB. This is the common case for libghostty consumers.
Default the foreground to white so that the initial render state
provides readable white-on-black text out of the box.
Long term we also need to expose setting the default colors for a
Terminal instance but this is a workable fix in the mean time.
Fixes#11706
Add a new GHOSTTY_TERMINAL_DATA_MOUSE_TRACKING option to the
ghostty_terminal_get API. This returns true if any mouse tracking mode
is active (X10, normal, button, or any-event), replacing the need for
consumers to loop over four separate mode queries.
#11706
Add a new GHOSTTY_TERMINAL_DATA_MOUSE_TRACKING option to the
ghostty_terminal_get API. This returns true if any mouse tracking
mode is active (X10, normal, button, or any-event), replacing the
need for consumers to loop over four separate mode queries.
Multiple changes:
* `zig build -Demit-lib-vt` now produces both shared and static
libraries by default
* Ghosty as a zig build dependency exports the static lib as
`dep.artifact("ghostty-vt-static")`
* CMake exports the static lib as `ghostty-vt-static`
Note that the static library is _not fat_. **If you enable SIMD you have
dependencies** and you need to manually link those: libc++, simdutf, and
highway. The `c-cmake-static` example disables SIMD.
The static library was built without position-independent code,
which caused linker errors when consumers tried to link it into
PIE executables (the default on most Linux distributions). The
linker would fail with "relocation R_X86_64_32 against symbol
cannot be used when making a PIE object."
Enable PIC on the static library root module so it can be linked
into both PIE and non-PIE executables.
Expose both shared and static libraries as separate CMake imported
targets (ghostty-vt and ghostty-vt-static) rather than toggling
between them with BUILD_SHARED_LIBS. The zig build already produces
both in a single invocation, so both are always available.
The find_package config template is updated to export both targets
as ghostty-vt::ghostty-vt and ghostty-vt::ghostty-vt-static.
Add a c-vt-cmake-static example that demonstrates linking the static
library via FetchContent with -Dsimd=false to avoid C++ runtime
dependencies.
Refactor GhosttyLibVt to support both shared and static library
builds via a shared initLib helper that accepts a LinkMode. The
shared and static entry points (initShared, initStatic) delegate
to this common path.
For static builds, compiler_rt and ubsan_rt are bundled to avoid
undefined symbol errors. Debug symbols (dsymutil) are skipped for
static libs since they are not linked. The install artifact uses
a "-static" suffix internally but installs as "libghostty-vt.a"
via a new installLib method. Wasm is excluded from static builds
since it has no meaningful static vs shared distinction.
Previously, every call to vt_write created a fresh ReadonlyStream with
new Parser and UTF8Decoder state. This meant escape sequences split
across write boundaries (e.g. ESC in one write, [27m in the next) would
lose parser state, causing the second write to start in ground state and
print the CSI parameters as literal text.
The C API now stores a persistent ReadonlyStream in the TerminalWrapper
struct, which is created when the Terminal is initialized. The vt_write
function feeds bytes through this stored stream, allowing it to maintain
parser state across calls. This change ensures that escape sequences
split across write boundaries are correctly parsed and rendered.
Previously, every call to vt_write created a fresh ReadonlyStream with
new Parser and UTF8Decoder state. This meant escape sequences split
across write boundaries (e.g. ESC in one write, [27m in the next)
would lose parser state, causing the second write to start in ground
state and print the CSI parameters as literal text.
The C API now stores a persistent ReadonlyStream in the TerminalWrapper
struct, which is created when the Terminal is initialized. The vt_write
function feeds bytes through this stored stream, allowing it to maintain
parser state across calls. This change ensures that escape sequences
split across write boundaries are correctly parsed and rendered.
Add GHOSTTY_BUILD_INFO_OPTIMIZE to query the Zig optimization mode
(debug, release safe/small/fast) the library was compiled with. This
reads directly from builtin.mode at comptime so it requires no build
system plumbing.
Add a new C API function ghostty_build_info() that exposes compile-time
build options to library consumers. This allows callers to query whether
SIMD, Kitty graphics protocol, and tmux control mode support were
enabled at build time.
Add a new C API function ghostty_build_info() that exposes compile-time
build options to library consumers. This allows callers to query whether
SIMD, Kitty graphics protocol, and tmux control mode support were
enabled at build time.
Remove the dedicated `zig build lib-vt` step and replace it with a
`-Demit-lib-vt` build option. This fixes two problems:
1. We can default XCFramework, app, etc. steps to false if emit-lib-vt
is true, so that the lib-vt build doesn't pull in unrelated artifacts.
**Most importantly, lib-vt alone can be build without full Xcode
installations.**
2. We can build lib-vt as part of a bundle with other artifacts if we
really want.
Remove the dedicated `zig build lib-vt` step and replace it with a
`-Demit-lib-vt` build option. This fixes two problems:
1. We can default XCFramework, app, etc. steps to false if emit-lib-vt
is true, so that the lib-vt build doesn't pull in unrelated
artifacts. **Most importantly, lib-vt alone can be build without
full Xcode installations.**
2. We can build lib-vt as part of a bundle with other artifacts if we
really want.
Add two new CellData variants to extract background color values
directly from cells. color_palette (10) returns the palette index as a
GhosttyColorPaletteIndex and color_rgb (11) returns the RGB components
as a GhosttyColorRgb. Both reuse the existing color types from color.h
rather than introducing new ones.
These are only valid when the cell content_tag is
bg_color_palette or bg_color_rgb respectively; querying them with a
mismatched tag reads from the wrong union member.
Found via Ghostling.
Add two new CellData variants to extract background color values
directly from cells. color_palette (10) returns the palette index
as a GhosttyColorPaletteIndex and color_rgb (11) returns the RGB
components as a GhosttyColorRgb. Both reuse the existing color
types from color.h rather than introducing new ones.
These are only valid when the cell content_tag is
bg_color_palette or bg_color_rgb respectively; querying them
with a mismatched tag reads from the wrong union member.
Map CMake release build types (Release, MinSizeRel, RelWithDebInfo) to
-Doptimize=ReleaseFast so that zig build automatically produces
optimized builds when CMake is configured for a release variant. Debug
builds remain unaffected, letting Zig use its default Debug optimization
level.
Map CMake release build types (Release, MinSizeRel, RelWithDebInfo)
to -Doptimize=ReleaseFast so that zig build automatically produces
optimized builds when CMake is configured for a release variant.
Debug builds remain unaffected, letting Zig use its default Debug
optimization level.
Add a new CI job that builds the root CMakeLists.txt to ensure the cmake
wrapper for libghostty-vt works.
This isn't the recommend way to build libghostty-vt, but its how
downstream CMake projects would consume it so we gotta keep it working.
This adds a function to the core surface to get process information
about the process(es) running in the terminal. Currently supported is
the PID of the foreground process and the name of the slave PTY.
If there is an error retrieving the information, or the platform does
not support retieving that information `null` is returned.
This will be useful in exposing the foreground PID and slave PTY name to
AppleScript or other APIs.
Add a new CI job that builds the root CMakeLists.txt to ensure the
cmake wrapper for libghostty-vt works.
This isn't the recommend way to build libghostty-vt, but its how
downstream CMake projects would consume it so we gotta keep it
working.
Add a top-level CMakeLists.txt that wraps `zig build lib-vt` so that
CMake-based downstream projects can consume libghostty-vt without
needing to interact with the Zig build system directly. A custom command
triggers the zig build during `cmake --build`, and the resulting shared
library is exposed as an IMPORTED target.
Downstream projects can pull in the library via FetchContent, which
fetches the source and builds it as part of their own CMake build, or
via find_package after a manual install step. The package config
template in dist/cmake/ sets up the ghostty-vt::ghostty-vt target with
proper include paths and macOS rpath handling.
A c-vt-cmake example demonstrates the FetchContent workflow, creating a
terminal, writing VT sequences, and formatting the output as plain text.
CI is updated to auto-discover and build CMake-based examples alongside
the existing Zig-based ones.
> [!WARNING]
>
> I am **very much not a CMake expert.** I leaned on LLMs heavily for
this. I did read the docs for what was chosen here and understand what's
going on, but if there is a better or more idiomatic way to do this I'm
all ears!
## Example CMake File
```cmake
cmake_minimum_required(VERSION 3.19)
project(c-vt-cmake LANGUAGES C)
include(FetchContent)
FetchContent_Declare(ghostty
GIT_REPOSITORY https://github.com/ghostty-org/ghostty.git
GIT_TAG main
)
FetchContent_MakeAvailable(ghostty)
add_executable(c_vt_cmake src/main.c)
target_link_libraries(c_vt_cmake PRIVATE ghostty-vt)
```
Add a top-level CMakeLists.txt that wraps `zig build lib-vt` so that
CMake-based downstream projects can consume libghostty-vt without
needing to interact with the Zig build system directly. A custom
command triggers the zig build during `cmake --build`, and the
resulting shared library is exposed as an IMPORTED target.
Downstream projects can pull in the library via FetchContent, which
fetches the source and builds it as part of their own CMake build, or
via find_package after a manual install step. The package config
template in dist/cmake/ sets up the ghostty-vt::ghostty-vt target
with proper include paths and macOS rpath handling.
A c-vt-cmake example demonstrates the FetchContent workflow, creating
a terminal, writing VT sequences, and formatting the output as plain
text. CI is updated to auto-discover and build CMake-based examples
alongside the existing Zig-based ones.
The GRAPHEMES_BUF data kind previously required a double pointer
(pointer to a uint32_t*) because the OutType was [*]u32, making the
typed out parameter *[*]u32. Change OutType to u32 so that callers
pass a plain uint32_t* buffer directly, which is the natural C
calling convention. The implementation casts the out pointer to
[*]u32 internally to write into the buffer.
The STYLE data kind read directly from the render state style array
without checking whether the cell actually had non-default styling.
The style data is undefined for unstyled cells, so this caused a
panic on a corrupt enum value when the caller read the style of an
unstyled cell. Now check cell.hasStyling() first and return the
default style for unstyled cells.
Expand the c-vt-render example to exercise dirty tracking, color
retrieval, cursor state, row/cell iteration with style resolution,
and dirty state reset. Break the example into six doxygen snippet
regions and reference them from render.h.
Expose the cursor fields from RenderState.Cursor through the C API
via new GhosttyRenderStateData enum values. This adds getters for
visual style, visibility, blink state, password input detection,
and viewport position (x, y, wide tail).
A new GhosttyRenderStateCursorVisualStyle enum maps the Zig
cursor.Style values (bar, block, underline, block_hollow) to
stable C integer constants. Viewport position getters return
GHOSTTY_INVALID_VALUE when the cursor is not visible within
the viewport.
Add individual color data kinds to GhosttyRenderStateData so callers
can query background, foreground, cursor color, cursor-color presence,
and the full 256-color palette through ghostty_render_state_get()
without using the sized-struct colors API.
COLOR_CURSOR returns GHOSTTY_INVALID_VALUE when no explicit cursor
color is set; callers can check COLOR_CURSOR_HAS_VALUE first.
Add next, select, and get functions to the render state row cells
API, mirroring the row iterator pattern. row_cells_next advances to
the next cell sequentially, row_cells_select jumps to a specific
column index with bounds validation, and row_cells_get queries data
for the current cell position.
The get function supports querying raw cell values (GhosttyCell),
resolved styles (GhosttyStyle), grapheme codepoint counts, and
writing grapheme codepoints into a caller-provided buffer.
Also add Cell.C and Cell.cval() to page.zig, matching the existing
Row.C/Row.cval() pattern, so the render state can convert cells to
the C ABI type without a raw bitCast.
Currently I have to use [this unusual
syntax](6e1c9f32e0/flake.nix (L137))
in my flake inputs to ensure that I don't have systems repeated in my
flake.lock file. This will make more obvious the fact that you have to
do follows to that hidden input.
Change row_iterator_new to only allocate with undefined fields,
matching the pattern used by row_cells_new. The iterator is now
populated via the render state get API with a new .row_iterator
data kind, which slices the row data and resets y to null.
This separates the lifetime of the opaque handle from the render
state it iterates, letting callers allocate once and re-populate
from different states without reallocating.
Add a new opaque RowCells type that wraps per-row cell data
(raw cells, graphemes, styles) for the C API. The caller
allocates a RowCells handle via row_cells_new, then populates
it by passing it to row_get with the new .cells data kind.
This queries the current row from the iterator and slices the
underlying MultiArrayList into the RowCellsWrapper fields.
The new type and functions are wired through main.zig,
lib_vt.zig, and the render.h C header.
Replace ghostty_render_state_row_dirty_get and
ghostty_render_state_row_dirty_set with generic
ghostty_render_state_row_get and ghostty_render_state_row_set
functions using enum-dispatched data/option kinds.
Replace the individual ghostty_render_state_size_get,
ghostty_render_state_dirty_get, and ghostty_render_state_dirty_set
functions with generic ghostty_render_state_get and
ghostty_render_state_set functions that use enum-dispatched data
kinds and option kinds, following the same InType/OutType pattern
used by the terminal and mouse encoder C APIs.
When some tools spawn subshells, PROMPT_COMMAND may be inherited as an
environment variable while the __ghostty_hook function is not (bash
doesn't export functions by default). This causes "command not found"
errors on every prompt in the subshell.
Add 2>/dev/null to the __ghostty_hook entry in PROMPT_COMMAND so that it
silently no-ops in subshells where the function isn't defined. This also
silences any errors from inside __ghostty_hook itself, but those are all
terminal escape sequences and non-actionable.
See: #11245
When some tools spawn subshells, PROMPT_COMMAND may be inherited as an
environment variable while the __ghostty_hook function is not (bash
doesn't export functions by default). This causes "command not found"
errors on every prompt in the subshell.
Add 2>/dev/null to the __ghostty_hook entry in PROMPT_COMMAND so that it
silently no-ops in subshells where the function isn't defined. This also
silences any errors from inside __ghostty_hook itself, but those are all
terminal escape sequences and non-actionable.
See: #11245
We previously used a readonly variable (__ghostty_ps0) to define the
best __ghostty_preexec_hook expansion for the current bash version.
This worked pretty well, but it had the downside of managing another
variable (#11258).
We can instead simplify this a bit by moving this into __ghostty_hook. I
didn't take that approach originally because I wanted to avoid the bash
version check on each command, but slightly loosening our guard check to
just look for "__ghostty_preexec_hook" (rather than the full expansion
expression) means we can bury the bash version check to the cold path.
One small gap here is that we may not update PS0 to the correct syntax
if we start switching between significantly different bash versions in
interactive subshells, but that seems like a pretty rare case to handle
given the benefits of this approach.
Add a C ABI row-iterator handle for render state with
ghostty_render_state_row_iterator_new and
ghostty_render_state_row_iterator_free, and wire them through
src/terminal/c/main.zig, src/lib_vt.zig, and
include/ghostty/vt/render.h. The header now documents only the
currently exported iterator API.
Add a C-facing GhosttyRenderStateColors sized struct and a
ghostty_render_state_colors_get accessor so renderers can read
background, foreground, cursor color state, and palette data directly
from the render state.
Add ghostty_render_state_size_get() to return cols and rows from the
current render state using out pointers. The C wrapper validates null
inputs, the symbol is wired through the C API export layers, and tests
cover success and invalid-value paths.
Switch RenderState.Dirty to lib.Enum so it uses C-compatible enum
backing when building the C ABI target. Add GhosttyRenderStateDirty and
new ghostty_render_state_dirty_get/set declarations to the render header,
then wire both functions through src/terminal/c/main.zig and the lib_vt
export table.
Introduce the first public C render-state surface for libghostty-vt.
Before this change, the render-state path was only available in Zig,
so C embedders had no direct way to create and update that cache.
Add an opaque GhosttyRenderState type with new, update, and free
entry points, then wire those symbols through the C API bridge and
library exports. Keep the surface intentionally minimal for now so
ownership and update behavior are established before adding read
accessors.
This adds a complete set of APIs for inspecting individual cells and
rows in the terminal grid from C. Callers can now resolve any point in
the grid to a reference, then extract codepoints, grapheme clusters,
styles, wide-character state, semantic prompt tags, and row-level
metadata like wrap and dirty flags.
This also adds a robust `ghostty_terminal_get` API for extracting
information like rows, cols, active screen, cursor information, etc.
from the terminal.
## Example
```c
// Write bold red text via SGR sequences
const char *text = "\033[1;31mHello\033[0m";
ghostty_terminal_vt_write(terminal, (const uint8_t *)text, strlen(text));
// Resolve cell (0,0) to a grid reference
GhosttyGridRef ref = GHOSTTY_INIT_SIZED(GhosttyGridRef);
GhosttyPoint pt = {
.tag = GHOSTTY_POINT_TAG_ACTIVE,
.value = { .coordinate = { .x = 0, .y = 0 } },
};
ghostty_terminal_grid_ref(terminal, pt, &ref);
// Read the codepoint ('H')
GhosttyCell cell;
ghostty_grid_ref_cell(&ref, &cell);
uint32_t codepoint = 0;
ghostty_cell_get(cell, GHOSTTY_CELL_DATA_CODEPOINT, &codepoint);
// Read the resolved style (bold=true, fg=red)
GhosttyStyle style = GHOSTTY_INIT_SIZED(GhosttyStyle);
ghostty_grid_ref_style(&ref, &style);
assert(style.bold);
```
## API Changes
### New Types
| Type | Description |
|------|-------------|
| `GhosttyCell` | Opaque 64-bit cell value |
| `GhosttyRow` | Opaque 64-bit row value |
| `GhosttyCellData` | Enum for `ghostty_cell_get` data kinds (codepoint,
content tag, wide, has_text, etc.) |
| `GhosttyCellContentTag` | Cell content kind (codepoint, grapheme, bg
color palette/RGB) |
| `GhosttyCellWide` | Cell width (narrow, wide, spacer tail/head) |
| `GhosttyCellSemanticContent` | Semantic content type (output, input,
prompt) |
| `GhosttyRowData` | Enum for `ghostty_row_get` data kinds (wrap,
grapheme, styled, dirty, etc.) |
| `GhosttyRowSemanticPrompt` | Row-level semantic prompt state |
| `GhosttyGridRef` | Sized struct — resolved reference to a cell
position in the page structure |
| `GhosttyPoint` | Tagged union specifying a grid position in a given
coordinate system |
| `GhosttyPointTag` | Coordinate system tag: `ACTIVE`, `VIEWPORT`,
`SCREEN`, `HISTORY` |
| `GhosttyPointCoordinate` | x/y coordinate pair |
| `GhosttyStyleId` | Style identifier type (uint16) |
### New Functions
| Function | Description |
|----------|-------------|
| `ghostty_cell_get` | Extract typed data from a cell (codepoint, wide,
style ID, etc.) |
| `ghostty_row_get` | Extract typed data from a row (wrap, dirty,
semantic prompt, etc.) |
| `ghostty_terminal_grid_ref` | Resolve a `GhosttyPoint` to a
`GhosttyGridRef` |
| `ghostty_grid_ref_cell` | Extract the `GhosttyCell` from a grid ref |
| `ghostty_grid_ref_row` | Extract the `GhosttyRow` from a grid ref |
| `ghostty_grid_ref_graphemes` | Get the full grapheme cluster
(codepoints) for the cell |
| `ghostty_grid_ref_style` | Get the resolved `GhosttyStyle` for the
cell |
This adds a function to the core surface to get process information
about the process(es) running in the terminal. Currently supported is
the PID of the foreground process and the name of the slave PTY.
If there is an error retrieving the information, or the platform does
not support retieving that information `null` is returned.
This will be useful in exposing the foreground PID and slave PTY name to
AppleScript or other APIs.
Add a c-vt-grid-ref example that demonstrates the terminal and grid
reference APIs end-to-end. The example creates a small 10x3 terminal,
writes text with mixed styles via VT sequences, then iterates over
every cell in the active area using ghostty_terminal_grid_ref. For
each cell it extracts the codepoint, and for each row it inspects
the wrap flag and the style bold attribute.
The grid_ref.h defgroup gains a @snippet reference to the new example,
and vt.h gets the corresponding @example entry and @ref listing.
[//]: # (dependabot-start)
⚠️ **Dependabot is rebasing this PR** ⚠️
Rebasing might not happen immediately, so don't worry if this takes some
time.
Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.
---
[//]: # (dependabot-end)
Bumps
[namespacelabs/nscloud-setup](https://github.com/namespacelabs/nscloud-setup)
from 0.0.11 to 0.0.12.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="df198f982f"><code>df198f9</code></a>
Update to node24 (<a
href="https://redirect.github.com/namespacelabs/nscloud-setup/issues/10">#10</a>)</li>
<li>See full diff in <a
href="f378676225...df198f982f">compare
view</a></li>
</ul>
</details>
<br />
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
</details>
Add ghostty_grid_ref_style and ghostty_grid_ref_graphemes to the grid
ref C API, allowing callers to extract the full style and grapheme
cluster directly from a grid reference without manually resolving
the page internals.
We previously used a readonly variable (__ghostty_ps0) to define the
best __ghostty_preexec_hook expansion for the current bash version.
This works pretty well, but it had the downside of managing another
variable (#11258).
We can instead simplify this a bit by moving this into __ghostty_hook. I
didn't take that approach originally because I wanted to avoid the bash
version check on each command, but slightly loosening our guard check to
just look for "__ghostty_preexec_hook" (rather than the full expansion
expression) means we can bury the bash version check to the cold path.
One small gap here is that we may not update PS0 to the correct syntax
if we start switching between significantly different bash versions in
interactive subshells, but that seems like a pretty rare case to handle
given the benefits of this approach.
Add a new C API function ghostty_terminal_cell that retrieves the
opaque cell and row values at a given point in the terminal grid.
The point is a tagged union supporting active, viewport, screen, and
history coordinate systems.
Add opaque GhosttyCell (uint64_t) and GhosttyRow (uint64_t) types that
bitcast to the internal packed Cell and Row structs from page.zig. Each
type has a corresponding data enum and getter function following the
same pattern as ghostty_terminal_get.
ghostty_cell_get supports extracting codepoint, content tag, wide
property, has_text, has_styling, style_id, has_hyperlink, protected,
and semantic_content. ghostty_row_get supports wrap, wrap_continuation,
grapheme, styled, hyperlink, semantic_prompt, kitty_virtual_placeholder,
and dirty.
The cell and row types and functions live in a new screen.h header,
separate from terminal.h, with terminal.h including screen.h for
convenience.
Add cursor_style to TerminalData, returning the current SGR style
of the cursor (the style applied to newly printed characters) as a
GhosttyStyle.
Refactor the C style conversion helpers: replace the standalone
convertStyle and convertColor functions with fromStyle and fromColor
initializers on the Style and Color extern structs respectively.
Expose the terminal Style struct to the C API as GhosttyStyle, a
sized struct with foreground, background, and underline colors
(as tagged unions) plus boolean text decoration flags.
Add ghostty_style_default() to obtain the default style and
ghostty_style_is_default() to check whether a style has all
default values. Wire both through c/style.zig, main.zig, and
lib_vt.zig with the corresponding header in vt/style.h.
Add a typed data query API to the terminal C interface, following
the same OutType pattern used by the OSC command data API. The new
ghostty_terminal_get function takes a GhosttyTerminalData tag and
an output pointer, returning GhosttyResult.
Currently exposes cols, rows, cursor x/y position, and cursor
pending wrap state. The GhosttyTerminalData enum is placed with the
other types in the header (before functions) per the ordering
convention.
ble.sh performs its own cursor positioning so we get multiple newlines
with 133;A's fresh-line behavior. ble.sh is a large enough project to
justify this additional, unambiguous conditional.
See: akinomyoga/ble.sh#684
See: wezterm/wezterm#5072
ble.sh performs its own cursor positioning so we get multiple newlines
with 133;A's fresh-line behavior. ble.sh is a large enough project to
justify this additional, unambiguous conditional.
See: akinomyoga/ble.sh#684
See: wezterm/wezterm#5072
Passing a `token` value causes this action to use the GitHub REST API,
which is subject to rate limits. We can chew through that allowance
quickly (1,000 requests/hour) given that we run two of these actions per
workflow run.
`token` defaults to the workflow's token, but by setting it explicitly
to an empty string, the action will instead use `git diff` to determine
the modified paths. This works fine for our case because we're already
running the checkout action, so we have an up-to-date repository view.
This also has the advantage of working around the 300 files GitHub REST
API limit for listing changed files.
Ref: https://github.com/dorny/paths-filter
Passing a `token` value causes this action to use the GitHub REST API,
which is subject to rate limits. We can chew through that allowance
quickly (1,000 requests/hour) given that we run two of these actions per
workflow run.
`token` defaults to the workflow's token, but by setting it explicitly
to an empty string, the action will instead use `git diff` to determine
the modified paths. This works fine for our case because we're already
running the checkout action, so we have an up-to-date repository view.
This also has the advantage of working around the 300 files GitHub REST
API limit for listing changed files.
Ref: https://github.com/dorny/paths-filter
We need to handle on more case: when an existing PROMPT_COMMAND ends in
a newline, we don't want to append a ; because that already counts as a
command separator.
We now handle all of these PROMPT_COMMAND cases:
- Ends with ; — no ; added
- Ends with \n or other whitespace — no ; added
- Ends with a command name — ; added as separator
See: #11245
This moves all our examples away from embedded source to `@snippet` and
files so that we can use our CI to actually run the builds and keep them
working.
Note: I used AI to extract the examples, and it did some weird merging
stuff. It all works but I want to make sure all these examples are still
human friendly so I need to go back and review all that. I clicked
through the web docs and they look good, just need to verify the GitHub
flow.
We need to handle on more case: when an existing PROMPT_COMMAND ends in
a newline, we don't want to append a ; because that already counts as a
command separator.
We now handle all of these PROMPT_COMMAND cases:
- Ends with ; — no ; added
- Ends with \n or other whitespace — no ; added
- Ends with a command name — ; added as separator
See: #11245
The dynamic example directory discovery added in bb3b3ba included
all subdirectories under example/, but some (wasm-key-encode,
wasm-sgr) are pure HTML examples with no build.zig.zon. Running
zig build in those directories falls back to the root build.zig
and attempts a full GTK binary build, which fails on CI.
Filter the listing to only include directories that contain a
build.zig.zon file so non-Zig examples are excluded from the
build matrix.
Extract inline @code blocks from vt headers (size_report.h, modes.h,
sgr.h, paste.h, mouse.h, key.h) into standalone buildable examples
under example/. Each header now uses Doxygen @snippet tags to include
code from the example source files, keeping documentation in sync
with code that is verified to compile and run.
New example projects: c-vt-size-report and c-vt-modes. Existing
examples (c-vt-sgr, c-vt-paste, c-vt-mouse-encode, c-vt-key-encode)
gain snippet markers so their code can be referenced from the headers.
Conceptual snippets in key.h, mouse.h, and key/encoder.h that show
terminal-state usage patterns remain inline since they cannot be
compiled standalone.
Replace the hardcoded matrix list in the build-examples job with a
dynamic list-examples job that discovers all subdirectories under
example/ at runtime. This uses ls/jq to produce a JSON array and
fromJSON() to feed it into the matrix, so new examples are picked
up automatically without updating the workflow.
Extract the inline code example from focus.h into a standalone
buildable example at example/c-vt-encode-focus. The header now
uses a Doxygen @snippet tag to include the code from the example
source file, so the documentation stays in sync with code that
is verified to compile and run.
Extract size report encoding into a reusable module and expose it
through the libghostty-vt C API as `ghostty_size_report_encode()`.
Size report escape sequences (mode 2048 in-band reports, XTWINOPS CSI
14/16/18 t responses) were formatted inline in
`Termio.sizeReportLocked`, and `termio.Message` carried its own
duplicate enum for report styles. This made the encoding logic
impossible to reuse from the C library and kept the style type
unnecessarily scoped to termio.
## Example
```c
GhosttySizeReportSize size = {
.rows = 24, .columns = 80,
.cell_width = 9, .cell_height = 18,
};
char buf[64];
size_t written = 0;
ghostty_size_report_encode(
GHOSTTY_SIZE_REPORT_MODE_2048, size,
buf, sizeof(buf), &written);
// buf contains: "\x1b[48;24;80;432;720t"
```
Add ghostty_size_report_encode() to libghostty-vt, following the
same pattern as focus encoding: a single stateless function that
writes a terminal size report escape sequence into a caller-provided
buffer.
The size_report.zig Style enum and Size struct now use lib.Enum and
lib.Struct so the types are automatically C-compatible when building
with c_abi, eliminating the need for duplicate type definitions in
the C wrapper. The C wrapper in c/size_report.zig re-exports these
types directly and provides the callconv(.c) encode entry point.
Supports mode 2048 in-band reports and XTWINOPS responses (CSI 14 t,
CSI 16 t, CSI 18 t).
Size report escape sequences were previously formatted inline in
Termio.sizeReportLocked, and termio.Message carried a duplicate enum for
report styles. That made the encoding logic harder to reuse and kept
the style type scoped to termio.
Move the encoding into terminal.size_report and export it through
terminal.main. The encoder now takes renderer.Size directly and derives
grid and pixel dimensions from one source of truth. termio.Message now
aliases terminal.size_report.Style, and Termio writes reports via the
shared encoder.
The venerable KDE blur protocol has been replaced with the compositor-
agnostic ext-background-effect-v1 protocol, to be implemented by Niri and
others. The new protocol is much easier to use overall, though we do need
to calculate the blur region manually like X11.
Fixes [#11935.](https://github.com/ghostty-org/ghostty/issues/11395)
I’m new to Zig, so I used AI assistance (Codex) while preparing this
change. Before opening this PR, I manually reviewed every line of the
final patch and stepped through the parser in LLDB to verify the
behavior. Happy to make any changes.
To better understand the parser, I also built a small model-checker
model
[here](https://gist.github.com/wyounas/284036272ba5893b6e413cafe2fe2a24).
Separately from this fix, I think formal verification and modeling could
be useful for parser work in Ghostty. The model is written in FizzBee,
which uses a Python-like Starlark syntax and is fairly readable. If that
seems useful, I’d be happy to open a separate discussion about whether
something like that belongs in the repository as executable
documentation or an additional safety net for future parser changes.
This is consistent with our bash prompt handling and also lets us
simplify our multiline prompt logic (because it no longer needs to work
around 133;A's fresh-line behavior).
This is consistent with our bash prompt handling and also lets us
simplify our multiline prompt logic (because it no longer needs to work
around 133;A's fresh-line behavior).
Previously libghostty-vt had no way for C consumers to query, set, or
report on terminal modes. Callers that needed to respond to DECRPM
requests or inspect mode state had no public interface to do so.
This adds three layers of mode support to the C API:
- `GhosttyMode` — a 16-bit packed type with inline helpers to construct
and inspect mode tags, plus `GHOSTTY_MODE_*` macros for all supported
ANSI and DEC private modes.
- `ghostty_terminal_mode_get` / `ghostty_terminal_mode_set` — query and
set mode values on a terminal handle.
- `ghostty_mode_report_encode` — encode a DECRPM response sequence (`CSI
[?] Ps1 ; Ps2 $ y`) into a caller-provided buffer.
## Example
```c
#include <stdio.h>
#include <ghostty/vt.h>
int main() {
char buf[32];
size_t written = 0;
// Query a terminal's cursor visibility and encode a DECRPM report
GhosttyMode mode = GHOSTTY_MODE_CURSOR_VISIBLE;
bool value = false;
ghostty_terminal_mode_get(terminal, mode, &value);
GhosttyModeReportState state = value
? GHOSTTY_MODE_REPORT_SET
: GHOSTTY_MODE_REPORT_RESET;
if (ghostty_mode_report_encode(mode, state, buf, sizeof(buf), &written)
== GHOSTTY_SUCCESS) {
// writes ESC[?25;1$y or ESC[?25;2$y
fwrite(buf, 1, written, stdout);
}
}
```
Add ghostty_mode_report_encode() which encodes a DECRPM response
sequence into a caller-provided buffer. The function takes a mode
tag, a report state integer, an output buffer, and writes the
appropriate CSI sequence (with ? prefix for DEC private modes).
The Zig-side ReportState is a non-exhaustive c_int enum that uses
std.meta.intToEnum for safe conversion to the internal type,
returning GHOSTTY_INVALID_VALUE on overflow. The C header exposes
a GhosttyModeReportState enum with named constants for the five
standard DECRPM state values.
Add modes.h with GhosttyModeTag (uint16_t) matching the Zig ModeTag
packed struct layout, along with inline helpers for constructing and
inspecting mode tags. Provide GHOSTTY_MODE_* macros for all 39
built-in modes (4 ANSI, 35 DEC), parenthesized for safety.
Add ghostty_terminal_mode_get and ghostty_terminal_mode_set to
terminal.h, both returning GhosttyResult so that null terminals
and unknown mode tags return GHOSTTY_INVALID_VALUE. The get function
writes its result through a bool out-parameter.
Add a note in the Zig mode entries reminding developers to update
modes.h when adding new modes.
Add modes.h with GhosttyModeTag, a uint16_t typedef matching the
Zig ModeTag packed struct layout (bits 0-14 for the mode value,
bit 15 for the ANSI flag). Three inline helper functions provide
construction and inspection: ghostty_mode_tag_new,
ghostty_mode_tag_value, and ghostty_mode_tag_ansi.
This extracts our mode reporting from being hardcoded in termio to being
reusable in the existing `terminal.modes` namespace. The goal is to
expose this via the Zig API libghostty (done) and C API (to do later).
This extracts our mode reporting from being hardcoded in termio
to being reusable in the existing `terminal.modes` namespace. The goal
is to expose this via the Zig API libghostty (done) and C API (to do
later).
Add focus event encoding (CSI I / CSI O) to the libghostty-vt public
API, following the same patterns as key and mouse encoding.
The focus Event enum uses lib.Enum for C ABI compatibility. The C API
provides ghostty_focus_encode() which writes into a caller-provided
buffer and returns GHOSTTY_OUT_OF_SPACE with the required size when the
buffer is too small.
Also update key and mouse encoders to return GHOSTTY_OUT_OF_SPACE
instead of GHOSTTY_OUT_OF_MEMORY for buffer-too-small errors, reserving
OUT_OF_MEMORY for actual allocation failures. Update all corresponding
header documentation.
Add focus event encoding (CSI I / CSI O) to the libghostty-vt public
API, following the same patterns as key and mouse encoding.
The focus Event enum uses lib.Enum for C ABI compatibility. The C API
provides ghostty_focus_encode() which writes into a caller-provided
buffer and returns GHOSTTY_OUT_OF_SPACE with the required size when
the buffer is too small.
Also update key and mouse encoders to return GHOSTTY_OUT_OF_SPACE
instead of GHOSTTY_OUT_OF_MEMORY for buffer-too-small errors,
reserving OUT_OF_MEMORY for actual allocation failures. Update all
corresponding header documentation.
The way we originally handled globals gradually escalated into an unholy
mess of ad-hoc helper functions and special-case handlers, which proved
to be hard to scale. Using a type-erased EnumMap like this makes
everything *far* easier to work and reason with, I think.
Also nuked the `xdg_wm_dialog_v1` hack that was necessary to prevent
old versions of gtk4-layer-shell crashing. If by the time of 1.4's
release people are still using those versions, it's on them.
TL;DR: this description is (intentionally) nonsense but I ran
`\b(\w+)\s\1\b` over `src` and stole a singular typo fix from #11528.
Replacement of #11528 with 100% less slop and 99% less AI; I didn't feel
like saying no to free(ish) typo checking. Note that many of the fixes
there were outright incorrect (and clearly had no review from sentient
lifeforms, contrary to its—sorry, it's—description). A lot of extra
double words were caught with a handy `rg --pcre2 '\b(\w+)\s+\1\b' src`;
you could say this PR was “ripgrep-assisted” the way that one was
“AI-assisted”. Rather ironic since that PR also claims to have used grep
via Claude Code, but missed a lot of them.
The its → it's changes from that PR were elided; I decided to run a `rg
"\bit'?s\b" src`, but someone REALLY likes their its, so I reverted my
changes as there were an extremely large number of changes (probably a
hundred files with multiple hundred cases). The only other change was
“baout” → “about”.
# AI Usage
Claude Code was used by proxy for finding baout. Claude Code was used by
proxy for realizing that the correct spelling is about. Claude Code was
not used for fixing it. Oh my god it was so difficult to fix, the
original PR had it so easy. I had to type out the file name (fish's AI
sorry I mean autocomplete helped though) and like, type /baout, press R,
press ab, then save and exit. This is so difficult you know we should
use an AI for this, like this is so hard I don't know how people manage.
All changes were verified by me: I consulted the dictionary to delve
into double-checkment of “in existence; being in evidence; apparent.”
Uhhh insert assorted other AI impersonation here maybe? THE LLM IN ME
WANTS TO ESCAPE PLEASE HELP
This is a new CLI action that prints an option or keybind's help
documentation to stdout.
ghostty +explain-config font-size
ghostty +explain-config copy_to_clipboard
ghostty +explain-config --option=font-size
ghostty +explain-config --keybind=copy_to_clipboard
The --option and --keybind flags perform a specific lookup. A string
passed as a positional argument attempts to look up the name first as an
option and then as a keybind.
Our vim plugin uses this with &keywordprg, which allows you to look up
the documentation for the config option or keybind under the cursor (K).
The milestone action currently runs for all merged pull_request_target
close events, including PRs opened by bots such as dependabot and
ghostty-vouch. That causes milestone binding to run on automated PRs
that should be ignored.
Gate the update-milestone job so pull request events only run when the
author is not a bot, while still allowing closed-issue events to run.
This preserves existing issue milestone behavior and prevents bot PRs
from triggering the workflow.
The milestone action currently runs for all merged pull_request_target
close events, including PRs opened by bots such as dependabot and
ghostty-vouch. That causes milestone binding to run on automated PRs
that should be ignored.
Gate the update-milestone job so pull request events only run when the
author is not a bot, while still allowing closed-issue events to run.
This preserves existing issue milestone behavior and prevents bot PRs
from triggering the workflow.
## Problem
Ghostty's `ssh-env` shell integration uses `-o "SetEnv
COLORTERM=truecolor"` when wrapping SSH commands. OpenSSH treats
command-line `-o SetEnv` options as **replacements** for all `SetEnv`
entries in `~/.ssh/config`, not additions. This silently drops any
user-configured `SetEnv` variables.
For example, a user with this in their SSH config:
```
Host myserver
SetEnv MY_VAR=hello
```
...would find `MY_VAR` empty after SSHing through Ghostty with `ssh-env`
enabled.
Reference: https://github.com/ghostty-org/ghostty/discussions/10871
## Fix
Replace `-o "SetEnv COLORTERM=truecolor"` with the additive pattern: set
`COLORTERM=truecolor` locally before the SSH call and forward it via
`SendEnv`.
`SendEnv` is additive — it does not clobber `SetEnv` entries in
`~/.ssh/config`.
**Trade-off:** `SendEnv` requires `AcceptEnv COLORTERM` on the remote
server (unlike `SetEnv`). But this was already the case for
`TERM_PROGRAM`/`TERM_PROGRAM_VERSION`, so it's a consistent and
acceptable approach.
## Changes
All 5 shell integration files updated with the same pattern:
- `SetEnv COLORTERM=truecolor` option removed
- `COLORTERM` added to the existing `SendEnv` option
- `COLORTERM=truecolor` set as a local env var on the execute line (so
`SendEnv` has something to forward)
## Test plan
- [ ] Enable `ssh-env` in Ghostty config: `shell-integration-features =
ssh-env`
- [ ] Add `SetEnv MY_VAR=hello` under a host in `~/.ssh/config` and
`AcceptEnv MY_VAR` in `/etc/ssh/sshd_config` on the remote
- [ ] SSH to that host — `echo $MY_VAR` should return `hello` (was empty
before this fix)
- [ ] `echo $COLORTERM` returns `truecolor` (requires `AcceptEnv
COLORTERM`)
- [ ] `echo $TERM_PROGRAM` still propagates (same `AcceptEnv`
requirement as before)
This adds a Zig and C API for mouse event encoding.
With these APIs in place, users can now create mouse events, configure a
mouse encoder with tracking mode, output format, and terminal size, and
encode those events into terminal escape sequences. All standard mouse
protocols are supported: X10, UTF-8, SGR, URxvt, and SGR-Pixels.
## Example
```c
#include <assert.h>
#include <stddef.h>
#include <stdio.h>
#include <ghostty/vt.h>
int main() {
GhosttyMouseEncoder encoder;
GhosttyResult result = ghostty_mouse_encoder_new(NULL, &encoder);
assert(result == GHOSTTY_SUCCESS);
// Set tracking mode to normal (button press/release)
ghostty_mouse_encoder_setopt(encoder, GHOSTTY_MOUSE_ENCODER_OPT_EVENT,
&(GhosttyMouseTrackingMode){GHOSTTY_MOUSE_TRACKING_NORMAL});
// Set output format to SGR
ghostty_mouse_encoder_setopt(encoder, GHOSTTY_MOUSE_ENCODER_OPT_FORMAT,
&(GhosttyMouseFormat){GHOSTTY_MOUSE_FORMAT_SGR});
// Set terminal geometry so the encoder can map pixel positions to cells
ghostty_mouse_encoder_setopt(encoder, GHOSTTY_MOUSE_ENCODER_OPT_SIZE,
&(GhosttyMouseEncoderSize){
.size = sizeof(GhosttyMouseEncoderSize),
.screen_width = 800, .screen_height = 600,
.cell_width = 10, .cell_height = 20,
});
// Create mouse event: left button press at pixel position (50, 40)
GhosttyMouseEvent event;
result = ghostty_mouse_event_new(NULL, &event);
assert(result == GHOSTTY_SUCCESS);
ghostty_mouse_event_set_action(event, GHOSTTY_MOUSE_ACTION_PRESS);
ghostty_mouse_event_set_button(event, GHOSTTY_MOUSE_BUTTON_LEFT);
ghostty_mouse_event_set_position(event, (GhosttyMousePosition){.x = 50.0f, .y = 40.0f});
// Encode the mouse event
char buf[128];
size_t written = 0;
result = ghostty_mouse_encoder_encode(encoder, event, buf, sizeof(buf), &written);
assert(result == GHOSTTY_SUCCESS);
fwrite(buf, 1, written, stdout);
ghostty_mouse_event_free(event);
ghostty_mouse_encoder_free(encoder);
return 0;
}
```
## New APIs
| Function | Description |
|----------|-------------|
| `ghostty_mouse_event_new` | Create a new mouse event instance |
| `ghostty_mouse_event_free` | Free a mouse event instance |
| `ghostty_mouse_event_set_action` | Set the event action (press,
release, motion) |
| `ghostty_mouse_event_get_action` | Get the event action |
| `ghostty_mouse_event_set_button` | Set the event button |
| `ghostty_mouse_event_clear_button` | Clear the event button (for
motion events) |
| `ghostty_mouse_event_get_button` | Get the event button (returns
whether one is set) |
| `ghostty_mouse_event_set_mods` | Set keyboard modifiers held during
the event |
| `ghostty_mouse_event_get_mods` | Get keyboard modifiers held during
the event |
| `ghostty_mouse_event_set_position` | Set position in surface-space
pixels |
| `ghostty_mouse_event_get_position` | Get position in surface-space
pixels |
| `ghostty_mouse_encoder_new` | Create a new mouse encoder instance |
| `ghostty_mouse_encoder_free` | Free a mouse encoder instance |
| `ghostty_mouse_encoder_setopt` | Set an encoder option (tracking mode,
format, size, etc.) |
| `ghostty_mouse_encoder_setopt_from_terminal` | Sync encoder options
from a terminal's current state |
| `ghostty_mouse_encoder_reset` | Reset internal encoder state (motion
deduplication) |
| `ghostty_mouse_encoder_encode` | Encode a mouse event into a terminal
escape sequence |
Export mouse_encode types and functions through the lib_vt public
input API, mirroring the existing key encoding exports. This adds
MouseAction, MouseButton, MouseEncodeOptions, MouseEncodeEvent,
and encodeMouse so that consumers of the Zig module can encode
mouse events without reaching into internal packages.
Add a new c-vt-mouse-encode example that demonstrates how to use the
mouse encoder C API. The example creates a mouse encoder configured
with SGR format and normal tracking mode, sets up terminal geometry
for pixel-to-cell coordinate mapping, and encodes a left button press
event into a terminal escape sequence.
Mirrors the structure of the existing c-vt-key-encode example. Also
adds the corresponding @example doxygen reference in vt.h.
Expose the internal mouse encoding functionality through the C API,
following the same pattern as the existing key encoding API. This
allows external consumers of libvt to encode mouse events into
terminal escape sequences (X10, UTF-8, SGR, URxvt, SGR-Pixels).
The API is split into two opaque handle types: GhosttyMouseEvent
for building normalized mouse events (action, button, modifiers,
position) and GhosttyMouseEncoder for converting those events into
escape sequences. The encoder is configured via a setopt interface
supporting tracking mode, output format, renderer geometry, button
state, and optional motion deduplication by last cell.
Encoder state can also be bulk-configured from a terminal handle
via ghostty_mouse_encoder_setopt_from_terminal. Failed encodes due
to insufficient buffer space report the required size without
mutating deduplication state.
Convert Coordinate in terminal/point.zig and CellSize, ScreenSize,
GridSize, and Padding in renderer/size.zig to extern structs. All
fields are already extern-compatible types, so this gives them a
guaranteed C ABI layout with no functional change.
Convert the Event and Format enums from fixed-size Zig enums to
lib.Enum so they are C ABI compatible when targeting C. The motion
method on Event becomes a free function eventIsMotion since lib.Enum
types cannot have declarations.
This is a new CLI action that prints an option or keybind's help
documentation to stdout.
ghostty +explain-config font-size
ghostty +explain-config copy_to_clipboard
ghostty +explain-config --option=font-size
ghostty +explain-config --keybind=copy_to_clipboard
The --option and --keybind flags perform a specific lookup. A string
passed as a positional argument attempts to look up the name first as an
option and then as a keybind.
Our vim plugin uses this with &keywordprg, which allows you to look up
the documentation for the config option or keybind under the cursor (K).
Bumps [dorny/paths-filter](https://github.com/dorny/paths-filter) from
4.0.0 to 4.0.1.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="fbd0ab8f3e"><code>fbd0ab8</code></a>
feat: add merge_group event support</li>
<li><a
href="efb1da7ce8"><code>efb1da7</code></a>
feat: add dist/ freshness check to PR workflow</li>
<li><a
href="d8f7b061b2"><code>d8f7b06</code></a>
Merge pull request <a
href="https://redirect.github.com/dorny/paths-filter/issues/302">#302</a>
from dorny/issue-299</li>
<li><a
href="addbc147a9"><code>addbc14</code></a>
Update README for v4</li>
<li>See full diff in <a
href="9d7afb8d21...fbd0ab8f3e">compare
view</a></li>
</ul>
</details>
<br />
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
</details>
Move MouseEvent and MouseFormat out of Terminal.zig and MouseShape out
of mouse_shape.zig into a new mouse.zig file. The types are named
without the Mouse prefix inside the module (Event, Format, Shape) and
re-exported with the prefix from terminal/main.zig for external use.
Update all call sites (mouse_encode.zig, surface_mouse.zig, stream.zig)
to import through terminal/main.zig or directly from mouse.zig. Remove
the now-unused mouse_shape.zig.
Move mouse event encoding logic from Surface.zig into a new
input/mouse_encode.zig file.
The new file encapsulates event filtering (shouldReport), button code
computation, viewport bounds checking, motion deduplication, and all
five wire formats (X10, UTF-8, SGR, urxvt, SGR-pixels). This makes the
encoding independently testable and adds unit tests covering each format
and edge case.
Additionally, Surface `mouseReport` can no longer fail, since the only
failure mode is no buffer space which should be impossible. Updated the
signature to remove the error set.
Move mouse event encoding logic from Surface.zig into a new
input/mouse_encode.zig file.
The new file encapsulates event filtering (shouldReport),
button code computation, viewport bounds checking, motion
deduplication, and all five wire formats (X10, UTF-8, SGR,
urxvt, SGR-pixels). This makes the encoding independently
testable and adds unit tests covering each format and edge
case.
Additionally, Surface `mouseReport` can no longer fail, since the only
failure mode is no buffer space which should be impossible. Updated
the signature to remove the error set.
Expose the key encoder Options.fromTerminal function to the C API as
ghostty_key_encoder_setopt_from_terminal. This lets C callers sync all
terminal-derived encoding options (cursor key application mode, keypad
mode, alt escape prefix, modifyOtherKeys, and Kitty flags) in a single
call instead of setting each option individually.
Expose the key encoder Options.fromTerminal function to the C API as
ghostty_key_encoder_setopt_from_terminal. This lets C callers sync all
terminal-derived encoding options (cursor key application mode, keypad
mode, alt escape prefix, modifyOtherKeys, and Kitty flags) in a single
call instead of setting each option individually.
Change `window-padding-balance` from `bool` to an enum with three
values:
- `false` - no balancing (default, unchanged)
- `true` - balance with vshift that caps top padding and shifts excess
to bottom (existing behavior, unchanged)
- `equal` - balance whitespace equally on all four sides
This gives users who prefer truly equal padding a way to opt in without
changing the default behavior.
Change `window-padding-balance` from `bool` to an enum with three
values:
- `false` - no balancing (default, unchanged)
- `true` - balance with vshift that caps top padding and shifts excess
to bottom (existing behavior, unchanged)
- `equal` - balance whitespace equally on all four sides
This gives users who prefer truly equal padding a way to opt in without
changing the default behavior.
This adds an initial C API for terminals and formatting. There is a new
example that shows how to use this.
With these APIs in place, users of the C API can now create a terminal,
pass raw VT streams to it, and dump the terminal viewport to various
formats. As noted in the docs, **the formatter API is not a rendering
API**, it isn't high performance enough for that. But it's a simpler API
to implement than the render state API so I started with that.
Both APIs are purposely fairly minimal, we're just setting the stage for
future functionality.
## Example
```c
#include <ghostty/vt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
GhosttyTerminal term;
GhosttyTerminalOptions opts = { .cols = 80, .rows = 24, .max_scrollback = 0 };
ghostty_terminal_new(NULL, &term, opts);
const char *input = "Hello, \033[1mBold\033[0m World!\r\nLine 2\r\n";
ghostty_terminal_vt_write(term, (const uint8_t *)input, strlen(input));
GhosttyFormatterTerminalOptions fmt = GHOSTTY_INIT_SIZED(GhosttyFormatterTerminalOptions);
fmt.emit = GHOSTTY_FORMATTER_FORMAT_PLAIN;
fmt.trim = true;
GhosttyFormatter fmtr;
ghostty_formatter_terminal_new(NULL, &fmtr, term, fmt);
uint8_t *buf;
size_t len;
ghostty_formatter_format_alloc(fmtr, NULL, &buf, &len);
fwrite(buf, 1, len, stdout);
free(buf);
ghostty_formatter_free(fmtr);
ghostty_terminal_free(term);
}
```
## New APIs
| Function | Description |
|----------|-------------|
| `ghostty_terminal_new` | Create a new terminal instance |
| `ghostty_terminal_free` | Free a terminal instance |
| `ghostty_terminal_reset` | Full reset of the terminal (RIS) |
| `ghostty_terminal_resize` | Resize the terminal to given dimensions |
| `ghostty_terminal_vt_write` | Write VT-encoded data to the terminal |
| `ghostty_terminal_scroll_viewport` | Scroll the terminal viewport |
| `ghostty_formatter_terminal_new` | Create a formatter for a terminal's
active screen |
| `ghostty_formatter_format_buf` | Format into a caller-provided buffer
|
| `ghostty_formatter_format_alloc` | Format into an allocated buffer |
| `ghostty_formatter_free` | Free a formatter instance |
## Future
- Obviously need to expose a lot more from the terminal:
* Read current set modes
* Read cursor information
* Read screen information
* etc...
- Need an optional callback system so that `vt_write` can invoke
callbacks for side effect sequences like clipboards, title setting,
responses, etc.
- `terminal.RenderState` C API so that people can build high performance
renderers on top of libghostty-vt
And so on...
The Discarding writer count field is u64, but several call sites
pass it where a usize is expected. On wasm32-freestanding, usize is
32-bit, so this caused compilation errors.
Use std.math.cast instead of a bare @intCast so that overflow is
handled gracefully, returning WriteFailed rather than triggering
safety-checked undefined behavior at runtime.
The Discarding writer count field is u64, but appendNTimes expects
usize which is u32 on 32-bit targets like arm-linux-androideabi.
Use std.math.cast instead of @intCast to safely handle the
conversion, returning WriteFailed on overflow rather than risking
undefined behavior.
Add an example showing how to use the ghostty-vt terminal and
formatter APIs from C. The example creates a terminal, writes
VT-encoded content with cursor movement and styling sequences,
then formats the screen contents as plain text using the formatter
API.
Rename the existing format function to format_buf to clarify that it
writes into a caller-provided buffer. Add a new format_alloc variant
that allocates the output buffer internally using the provided
allocator (or the default if NULL). The caller receives the allocated
pointer and length and is responsible for freeing it.
This is useful for consumers that do not know the required buffer size
ahead of time and want to avoid the two-pass query-then-format pattern
needed with format_buf.
Add a size field as the first member of formatter option structs
(TerminalOptions, TerminalOptions.Extra, ScreenOptions.Extra) for ABI
compatibility. This allows adding new fields without breaking callers
compiled against older versions of the struct.
Introduce include/ghostty/vt/types.h as the foundational header
containing GhosttyResult and the GHOSTTY_INIT_SIZED macro for
zero-initializing sized structs. Remove the separate result.h header,
moving its contents into types.h.
The terminal.Stream next/nextSlice functions can now no longer fail. All
prior failure modes were fully isolated in the handler `vt` callbacks.
As such, vt callbacks are now required to not return an error and handle
their own errors somehow.
Allowing streams to be fallible before was an incorrect design. It
caused problematic scenarios like in `nextSlice` early terminating
processing due to handler errors. This should not be possible.
There is no safe way to bubble up vt errors through the stream because
if nextSlice is called and multiple errors are returned, we can't
coalesce them. We could modify that to return a partial result but its
just more work for stream that is unnecessary. The handler can do all of
this.
This work was discovered due to cleanups to prepare for more C APIs.
Less errors make C APIs easier to implement! And, it helps clean up our
Zig, too.
The terminal.Stream next/nextSlice functions can now no longer fail.
All prior failure modes were fully isolated in the handler `vt`
callbacks. As such, vt callbacks are now required to not return an error
and handle their own errors somehow.
Allowing streams to be fallible before was an incorrect design. It
caused problematic scenarios like in `nextSlice` early terminating
processing due to handler errors. This should not be possible.
There is no safe way to bubble up vt errors through the stream because
if nextSlice is called and multiple errors are returned, we can't
coalesce them. We could modify that to return a partial result but its
just more work for stream that is unnecessary. The handler can do all of
this.
This work was discovered due to cleanups to prepare for more C APIs.
Less errors make C APIs easier to implement! And, it helps clean up our
Zig, too.
Added test case for cascading **without moving previous window**, #11161
will follow up for more accurate cascading after this.
Fixed window cascading after last pr, now we should perform cascading
**after** showing the window.
Depends on https://github.com/ghostty-org/ghostty/pull/11417
Moved positioning part from `windowDidLoad` to `showWindow` to make new
users happy. Also deleted `initialFrame`, since we don't need it
anymore.
Adds progress-style config to control OSC 9;4 progress bar visibility.
Defaults to true, set false to hide.
Fixes#11241
AI Disclosure: Claude Code (Opus 4.6) used for codebase exploration,
code review, and testing assistance. All code written and reviewed by
hand.
Fixes#11396
Track menu items populated from Ghostty keybind actions and only trigger
those from SurfaceView performKeyEquivalent. This avoids app-default
shortcuts such as Hide from pre-empting explicit keybinds.
Fixes#11396
Track menu items populated from Ghostty keybind actions and only trigger
those from SurfaceView performKeyEquivalent. This avoids app-default
shortcuts such as Hide from pre-empting explicit keybinds.
Fixes#11379
For this pass, I made it a very simple "within 20%" (height-wise) of the
split handle down. There is no horizontal component. I want to find the
right balance between always visible (today mostly) to only visible on
direct hover, because I think it'll be too hard to discover on that far
right side.
Fixes#11379
For this pass, I made it a very simple "within 20%" (height-wise) of the
split handle. There is no horizontal component. I want to find the right
balance between always visible (today mostly) to only visible on direct
hover, because I think it'll be too hard to discover on that far right
side.
Claude wrote the fail path in the UI tests, or you can easily reproduce
this manually. This is kinda a regression after #11322, since we are not
delaying the frame update anymore, which exposes some of the "flaws" of
the previous implementation.
The following three commits fix this step by step:
- We shouldn't save intermediate frames when the window is loading,
which is triggered by `windowDidResize` and `windowDidMove` during the
process.
- We should set the initial position (from the config) after the window
is loaded.
- A small refactor on `LastWindowPosition` to support restoring the
window frame under certain conditions.
https://github.com/user-attachments/assets/6f90f9a5-653d-4146-95c6-8e5c69bda656
### AI Disclosure
Claude helped me write the UI tests.
Use OSC 133;P (prompt mark) instead of 133;A (fresh line + prompt mark)
inside PS1 and PS2. Readline redraws the prompt on vi mode switches,
Ctrl-L, and other events, and 133;A's fresh-line behavior would emit a
CR+LF whenever the cursor wasn't at column 0, causing visible extra
newlines.
The one-time 133;A is now emitted via printf in __ghostty_precmd, which
only runs once per prompt cycle via PROMPT_COMMAND. On SIGWINCH, bash
redraws PS1 (firing the 133;P marks) but doesn't re-run PROMPT_COMMAND,
so there's no unwanted fresh-line on resize either. The redraw=last flag
persists from the initial printf.
This is a little less optimal than our previous approach, in terms of
number of prompt marks we emit, but it produces an overall more correct
result, which is the important thing.
Because readline prints its output outside the scope of PS1, those
characters "inherit" the surrounded prompt scope. This is usually fine,
but it can sometimes get out of sync (especially during redraws). This
is inherently a limitation of the fact that it's a separate output
channel, so we just have to accept that can happen.
Fixes: #10953
See: #11267
Use OSC 133;P (prompt mark) instead of 133;A (fresh line + prompt mark)
inside PS1 and PS2. Readline redraws the prompt on vi mode switches,
Ctrl-L, and other events, and 133;A's fresh-line behavior would emit a
CR+LF whenever the cursor wasn't at column 0, causing visible extra
newlines.
The one-time 133;A is now emitted via printf in __ghostty_precmd, which
only runs once per prompt cycle via PROMPT_COMMAND. On SIGWINCH, bash
redraws PS1 (firing the 133;P marks) but doesn't re-run PROMPT_COMMAND,
so there's no unwanted fresh-line on resize either. The redraw=last flag
persists from the initial printf.
This is a little less optimal than our previous approach, in terms of
number of prompt marks we emit, but it produces an overall more correct
result, which is the important thing.
Because readline prints its output outside the scope of PS1, those
characters "inherit" the surrounded prompt scope. This is usually fine,
but it can sometimes get out of sync (especially during redraws). This
is inherently a limitation of the fact that it's a separate output
channel, so we just have to accept that can happen.
See: #11267
Tests that window position and size are correctly restored after
reopen for all four macos-titlebar-style variants.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fixes#11316
This mirrors the `prompt` actions (hence why there is no window action
here) and enables setting titles via keybind actions which importantly
lets this work via command palettes, App Intents, AppleScript, etc.
Fixes#11316
This mirrors the `prompt` actions (hence why there is no window action
here) and enables setting titles via keybind actions which importantly
lets this work via command palettes, App Intents, AppleScript, etc.
Only replace the \n prompt escape when inserting secondary prompt marks,
not literal newlines `($'\n')`. Literal newlines may appear inside
`$(...)` or `...` command substitutions, and inserting escape sequences
there breaks the shell syntax. For example:
PS1='$(if [ $? -eq 0 ]; then echo -e "P";
else echo -e "F";
fi) $ '
The literal newlines between the if/else/fi are part of the shell syntax
inside the command substitution. The previous code replaced all literal
newlines in PS1 with newline + OSC 133 escape sequences, which injected
terminal escapes into the middle of the command substitution and caused
bash to report a syntax error when evaluating it.
The \n prompt escape is PS1-specific and safe to replace globally. This
means prompts using literal newlines for line breaks (rather than \n)
won't get per-line secondary marks, but this is the conventional form
and avoids the need for complex shell parsing.
Fixes: #11267
Only replace the \n prompt escape when inserting secondary prompt marks,
not literal newlines ($'\n'). Literal newlines may appear inside $(...)
or `...` command substitutions, and inserting escape sequences there
breaks the shell syntax. For example:
PS1='$(if [ $? -eq 0 ]; then echo -e "P";
else echo -e "F";
fi) $ '
The literal newlines between the if/else/fi are part of the shell syntax
inside the command substitution. The previous code replaced all literal
newlines in PS1 with newline + OSC 133 escape sequences, which injected
terminal escapes into the middle of the command substitution and caused
bash to report a syntax error when evaluating it.
The \n prompt escape is PS1-specific and safe to replace globally. This
means prompts using literal newlines for line breaks (rather than \n)
won't get per-line secondary marks, but this is the conventional form
and avoids the need for complex shell parsing.
Fixes: #11267
Replace the strip-in-preexec / re-add-in-precmd pattern for OSC 133
marks with a save/restore approach. Instead of pattern-matching marks
out of PS1 (which exposes PS1 in intermediate states to other hooks), we
save the original PS1/PS2 before adding marks and then restore them.
This also adds dynamic theme detection: if PS1 changed between cycles
(e.g., a theme rebuilt it), we skip injecting continuation marks into
newlines. This prevents breaking plugins like Pure that use pattern
matching to strip/rebuild the prompt.
Additionally, move _ghostty_precmd to the end of precmd_functions in
_ghostty_deferred_init (instead of substituting in-place) so that the
first prompt is properly marked even when other hooks were appended
after our auto-injection.
There's one scenario that we still don't complete cover:
precmd_functions+=(_test_overwrite_ps1)
_test_overwrite_ps1() {
PS1="test> "
}
... which results in the first prompt not printing its prompt marks
because _test_overwrite_ps1 becomes the last thing to run, overwriting
our marks, but this will be fixed for subsequent prompts when we move
our handler back to the last index.
Fixes: #11282
Replace the strip-in-preexec / re-add-in-precmd pattern for OSC 133
marks with a save/restore approach. Instead of pattern-matching marks
out of PS1 (which exposes PS1 in intermediate states to other hooks), we
save the original PS1/PS2 before adding marks and then restore them.
This also adds dynamic theme detection: if PS1 changed between cycles
(e.g., a theme rebuilt it), we skip injecting continuation marks into
newlines. This prevents breaking plugins like Pure that use pattern
matching to strip/rebuild the prompt.
Additionally, move _ghostty_precmd to the end of precmd_functions in
_ghostty_deferred_init (instead of substituting in-place) so that the
first prompt is properly marked even when other hooks were appended
after our auto-injection.
There's one scenario that we still don't complete cover:
precmd_functions+=(_test_overwrite_ps1)
_test_overwrite_ps1() {
PS1="test> "
}
... which results in the first prompt not printing its prompt marks
because _test_overwrite_ps1 becomes the last thing to run, overwriting
our marks, but this will be fixed for subsequent prompts when we move
our handler back to the last index.
Fixes: #11282
If the CLI argument `--working-directory` is not used with
`+new-window`, the current working directory that `ghostty +new-window`
is run from will be appended to the list of configuration data sent
to the main Ghostty process. If `-e` _was_ used on the CLI, the
`--working-directory` that was appended will be interpreted as part of
the command to be executed, likely causing it to fail.
Instead, insert `--working-directory` at the beginning of the list of
configuration that it sent to the main Ghostty process.
Fixes#11356
This disables all the automatic one-time code inputs in Ghostty. It'd be
really neat to actually dynamically change this (not sure if it's
possible with NSTextContext or how often thats cached) but for now we
should just fully disable it.
Thanks to Ricky Mondello for the heads up on this.
This disables all the automatic one-time code inputs in Ghostty.
It'd be really neat to actually dynamically change this (not sure if its
possible with NSTextContext or how often thats cached) but for now we
should just fully disable it.
Fixes#11336
Introduce a proper WorkingDirectory tagged union type with home,
inherit, and path variants. The field is now an optional
(?WorkingDirectory) where null represents "use platform default" which
is resolved during Config.finalize to .inherit (CLI) or .home (desktop
launcher).
Fixes#11336
Introduce a proper WorkingDirectory tagged union type with home, inherit,
and path variants. The field is now an optional (?WorkingDirectory) where
null represents "use platform default" which is resolved during Config.finalize
to .inherit (CLI) or .home (desktop launcher).
Test boolean, string, enum, and numeric config properties using
TemporaryConfig to verify defaults and parsed values.
Co-Authored-By: Claude <noreply@anthropic.com>
If you have "Noto Sans Tai Tham" and/or "Noto Sans Javanese" installed
locally on Linux, three tests fail. This PR disables those tests until a
more permanent solution can be found.
Fixes https://github.com/ghostty-org/ghostty/discussions/11203
The `suppressNextLeftMouseUp` flag from #11167 wasn't being reset on
focus loss, causing stale state that led to phantom drags/selections and
scrolls if you're lucky enough.
I've followed the #11167 's path and made it reset on focus loss.
As I stated in the [vouch
request](https://github.com/ghostty-org/ghostty/discussions/11274); I'm
not experienced in Swift, just following the prior PR's steps to reset
the state. I've been using this patch for couple days and the change
looks trivial to me tho not 100% sure if I'm missing anything.
> [!NOTE]
> Used Claude Code -Opus 4.6- for navigating the codebase and reviewing
the change.
Fixes phantom mouse drag/selection when switching splits or apps.
The suppressNextLeftMouseUp flag and core mouse click_state were not
being reset on focus transitions, causing stale state that led to
unexpected drag behavior.
- Reset suppressNextLeftMouseUp in focusDidChange when losing focus
- Defensively reset the flag when processing normal clicks
- Reset core mouse.click_state and left_click_count on focus loss
## Summary
- After finishing an inline tab title edit (via keybind or
double-click), all keyboard input is lost because
`TabTitleEditor.finishEditing()` sets `makeFirstResponder(nil)`, leaving
the window itself as first responder with no path back to the terminal
surface.
- Adds a `tabTitleEditorDidFinishEditing` delegate callback to
`TabTitleEditorDelegate` that fires after every edit (commit or cancel).
- `TerminalWindow` implements it by calling
`makeFirstResponder(focusedSurface)` to restore keyboard focus to the
terminal.
Fixes https://github.com/ghostty-org/ghostty/discussions/11315
## Testing
- [x] Bind `prompt_tab_title` to a keybind (e.g. `keybind =
cmd+shift+i=prompt_tab_title`)
- [x] Trigger inline tab title edit via keybind, press Enter — verify
keyboard input works immediately
- [x] Trigger inline tab title edit via keybind, press Escape — verify
keyboard input works immediately
- [x] Double-click a tab title, press Enter — verify keyboard input
works immediately
- [x] Double-click a tab title, press Escape — verify keyboard input
works immediately
- [x] Verify Cmd+number tab switching works after all of the above
- [x] Verify split pane focus is correct after editing tab title with
splits open
AI disclosure: Codebase exploration and review via [Claude
Code](https://claude.com/claude-code)
After finishing an inline tab title edit (via keybind or double-click),
`TabTitleEditor.finishEditing()` calls `makeFirstResponder(nil)` to
clear focus from the text field, leaving the window itself as first
responder. No code path restores focus to the terminal surface, so all
keyboard input is lost until the user clicks into a pane.
Add a `tabTitleEditorDidFinishEditing` delegate callback that fires
after every edit (commit or cancel). TerminalWindow implements it by
calling `makeFirstResponder(focusedSurface)` to hand focus back to the
terminal.
Fixes https://github.com/ghostty-org/ghostty/discussions/11315
Co-Authored-By: Claude <noreply@anthropic.com>
Add initialContentSize fallback on TerminalViewContainer so
intrinsicContentSize returns the correct value immediately,
without waiting for @FocusedValue to propagate. This removes
the need for the DispatchQueue.main.asyncAfter 40ms delay.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests that validate intrinsicContentSize returns a correct value when
TerminalController.windowDidLoad() reads it. Currently fail, proving
the race condition where @FocusedValue hasn't propagated
lastFocusedSurface before the 40ms timer fires.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
If you have "Noto Sans Tai Tham" and/or "Noto Sans Javanese" installed
locally on Linux, three tests fail. This PR disables those tests until a
more permanent solution can be found.
Track registry global names for kde decoration manager and kde_output_order bindings so we can distinguish same-global duplicates from valid replacements announced before global_remove.
On global_remove, match and clear these bindings by registry global name to avoid dropping a replacement when the old global is removed.
Flatten resolveQuickTerminalMonitor by replacing the labeled-block
switch with early returns, extract max_output_name_len constant, and
reduce nesting in the output-order event handler.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Restructure resolveQuickTerminalMonitor into a two-phase approach
(match by name, then fall back to first monitor) to eliminate the
interleaved fallback/match ref tracking. Remove redundant switch in
enteredMonitor that duplicated the .mouse handling already in
resolveQuickTerminalMonitor. Hoist the primary_output_match_failed_logged
reset above the name-length branches in outputOrderListener.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Install Wayland protocol listeners at bind time so late-added globals
still receive events and listener setup stays tied to object lifetime.
Track whether kde_output_order_v1 emitted any outputs in a cycle and
clear cached primary-output state on empty or invalid updates. Also
reset this cycle tracking when the protocol global is removed to avoid
stale monitor selection.
Handle g_list_model_get_object transfer-full semantics in resolveQuickTerminalMonitor by retaining exactly one monitor reference to return and unreffing the rest.
Update init/sync/sizing call sites to unref the resolved monitor after setMonitor/getGeometry so monitor lifetimes are explicit and consistent.
Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
Implement the quick-terminal-screen config option on Linux/Wayland so
users can pin the quick terminal to a specific monitor instead of
always following the mouse cursor.
Use the kde_output_order_v1 protocol to identify the compositor's
primary monitor by connector name (e.g. "DP-1"). When the protocol is
unavailable, fall back to the first monitor in the GDK list.
- Add resolveQuickTerminalMonitor() to map config values to a
gdk.Monitor: .mouse returns null (compositor decides), .main and
.macos-menu-bar match by connector name via the protocol
- Call layer_shell.setMonitor() in both initQuickTerminal and
syncQuickTerminal so config reloads take effect
- Update enteredMonitor to size the window using the configured
monitor rather than whichever monitor was entered
- Update config documentation to reflect Linux support
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add the missing setMonitor() function to the gtk4-layer-shell Zig
bindings and provide the gdk module so it can reference gdk.Monitor.
Register the kde-output-order-v1 Wayland protocol from
plasma-wayland-protocols and generate its scanner binding. This
protocol reports the compositor's monitor priority ordering and is
needed to correctly identify the primary monitor for
quick-terminal-screen support on Linux.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 18:23:08 -06:00
543 changed files with 94536 additions and 41624 deletions
Non-maintainers are not allowed to create issues in this repository — we ask that you create a discussion first. For more details on the why, see #3558 and our [CONTRIBUTING.md](https://github.com/ghostty-org/ghostty/blob/main/CONTRIBUTING.md).