macOS menus call ghostty_surface_split, split_focus, and request_close
directly, while keybinds go through Surface.performBindingAction. GTK
already uses that path for splits and close. Route the embedded C APIs
the same way so menus and keybinds share one hook.
Co-authored-by: Cursor <cursoragent@cursor.com>
This migrates all remaining uses of `cImport` (and `addTranslateC` for
good measure) to using translate-c for C translation, ensuring that we
are ready for when `cImport` is removed from the language, and also that
all sources of C translation are using the same snapshot of the external
package (when can then be updated when we need to fix something).
A couple of notes:
* A few options have been added to support the new translations, namely
the ability to link libraries (passed through to `linkLibrary` on the
Translator side) and whether or not to initialize default values (looks
like `cImport` did this without a way to control it, but translate-c
does not do it by default).
* Using the new library linking option actually simplifies the process
of translating a number of the C packages as we have been shipping the
necessary headers for these packages already with the applicable
libraries. For some of the more complex translation processes though, we
still include the appropriate directories directly.
The Bash 4.4+ prompt hook saves the command status before doing its own
work and passes it to __ghostty_precmd. The function ignored that
argument and instead read the hook invocation status, causing command
end markers to report zero.
Also, existing scalar PROMPT_COMMAND entries can overwrite the last
command's status before Ghostty's appended hook runs. Capture and
restore the status before those commands, then consume the saved value
in the final hook.
Fixes#14247
Existing scalar PROMPT_COMMAND entries can overwrite the last command's
status before Ghostty's appended hook runs. Capture and restore the
status before those commands, then consume the saved value in the final
hook.
Keep array hooks as independent entries because Bash 5.1 and newer
restore the original status for each entry. Preserve the existing
PROMPT_COMMAND type and safely handle inherited prompt commands where
Ghostty's function definitions are absent.
This retains the fast Bash 4.4+ PS0 integration rather than using
bash-preexec's DEBUG trap.
See #14247
Bash includes additional variable attributes in declare output, so an
exported indexed array is reported with an -ax prefix instead of -a.
Match the indexed-array prefix without requiring a following space so
these values continue through the array-preserving path.
The Bash 4.4+ prompt hook saves the command status before doing its own
work and passes it to __ghostty_precmd. The function ignored that
argument and instead read the hook invocation status, causing command
end markers to report zero.
Use the explicit argument when present while retaining the current
status fallback required by the older bash-preexec path.
See #14247
This migrates all remaining uses of cImport (and addTranslateC for good
measure) to using translate-c for C translation, ensuring that we are
ready for when cImport is removed from the language, and also that all
sources of C translation are using the same snapshot of the external
package (when can then be updated when we need to fix something).
A couple of notes:
* A few options have been added to support the new translations, namely
the ability to link libraries (passed through to linkLibrary on the
Translator side) and whether or not to initialize default values
(looks like cImport did this without a way to control it, but
translate-c does not do it by default).
* Using the new library linking option actually simplifies the process
of translating a number of the C packages as we have been shipping the
necessary headers for these packages already with the applicable
libraries. For some of the more complex translation processes though,
we still include the appropriate directories directly.
Updates the Lithuanian translation for v1.4 and fills all 181 missing
strings, including the command palette entries.
Related to #13766.
Validation:
- Manually reviewed all translated strings
- `msgfmt --check --check-compatibility --check-accelerators` passes
- `msgcmp` against the translation template passes
AI usage: I used Pi with OpenAI Codex (gpt-5.6-sol) to draft the
translations and run gettext checks. I manually reviewed all
translations before submitting.
DECRQM requests can use a full 16-bit number but we truncated to 15-bits
for the reply (cause thats all we know about). Preserve, the full
16-bits for requests so we give the proper response.
Fixes#13758
### Problem
With `font-thicken = true` on macOS, IME preedit (composing) text
renders noticeably thinner than committed text.
### Cause & Fix
Committed and preedit text diverge in `src/renderer/generic.zig`:
```text
Committed: Shaper -> addGlyph -> renderGlyph(..., { .thicken = config.font_thicken, ... })
Preedit: State -> addPreeditCell -> renderCodepoint(..., { .grid_metrics })
```
Fix: Pass `self.config.font_thicken` and
`self.config.font_thicken_strength` in `addPreeditCell`.
### Testing
- Verified on macOS with `font-thicken = true`.
- Verified with `zig fmt --check`.
### AI Disclosure
AI (gemini-3.8-flash) assisted in tracing the call path; manually
verified and tested.
DECRQM requests can use a full 16-bit number but we truncated to 15-bits
for the reply (cause thats all we know about). Preserve, the full
16-bits for requests so we give the proper response.
GtkGLArea had numerous downsides that forced us to invent unsightly
hacks in our renderer to work around them, most chiefly the fact that it
holds its own GdkGLContext on the main thread (GL contexts are not at
all thread-safe), forcing us to keep our GL calls on the main thread. It
also does not interact well with triple-buffering and initialization is
forced to be this sort of deferred song-and-dance since we need to wait
for the GLArea to initialize its GL context before we can initialize the
renderer, the core surface, and then most things in the GTK surface.
We instead invent our own custom widget named RenderSurface that takes
simple DMABUFs and displays them. The task of obtaining a GL context
falls to manual EGL bindings, since we also need EGL to export OpenGL
textures into DMABUFs. We keep the EGL context solely on the render
thread meaning that the main thread never concerns itself with rendering
except when being notified that the renderer has pushed a new frame.
What makes this extra significant is that now the entire GTK apprt no
longer depends on OpenGL in any way, shape or form. As long as it is
being fed DMABUFs, it can render from whichever graphics API you want.
This means we can add more backends based on OpenGL ES or more likely
Vulkan rather painlessly in the future.
**AI disclosure**: I came up with the idea and let Pi implement most of
the nitty-gritty details around EGL, as well as replumbing the renderer
and cleaning up all the GTK-specific workarounds there. I then carefully
vetted every line of code and spent roughly as much time reviewing as
coding. Most of the documentation and all commit messages are in my own
words.
1. Speeds up runs by ~3x for projects with a large number of
dependencies by downloading in parallel (~55s → 16s on my workstation).
2. Adds some workarounds for Zig 0.16's package management. The changes
don't affect Ghostty itself, but do affect downstream projects that
embed libghostty-vt and want to create Nix packages of their own.
AI disclosure: Claude was used to diagnose CI failures and suggest
fixes.
Repeated builds were penalized for three reasons:
1. The gresource XML embedded the absolute cache paths of the compiled
.ui files. It now uses relative paths, resolved against a `--sourcedir`
passed to glib-compile-resources.
2. Blueprints were compiled through a small Zig wrapper, and a Run step
hashes the bytes of the executable it runs. A Zig binary does not relink
to the same bytes, so after a branch switch that touched the wrapper
every .ui moved, the gresource compiler re-ran and the whole app
recompiled for identical output. blueprint-compiler is now run directly
and the wrapper is reduced to a single version check whose output
nothing reads.
3. The gresource pipeline was built once per artifact. It is now
memoized on the *std.Build.
Cold builds and builds after real blueprint changes are unaffected. A
build after the wrapper relinks goes from ~43s to ~7s on my system.
AI disclosure: Claude Opus 5 was used to pinpoint the source of the
cache poisoning and to prepare a patch. Claude Fable 5.1 measured it,
traced the remaining churn to the wrapper's relinks, and replaced a
custom build step with stock ones. The commit message is human-written
based on the author's understanding of the changes.
Fixes https://github.com/ghostty-org/ghostty/issues/14135.
<img width="1398" height="652" alt="image"
src="https://github.com/user-attachments/assets/6901a05e-9d23-4e25-89a7-c16c1694a0f9"
/>
> The #9168 fix is no longer needed, since the frame is now higher than
the actual glyph.
The frame change observation only affects those who have a custom window
title font set in their config. I asked Claude to run some main thread
benchmarking compared to `main`; it will gain some delays for rapid
title changes and window resizing. The additional cost is brought by the
frequent frame updates which are done by AppKit. But that's necessary
for updating the title to the correct style.
> I tried to do some diffing and removing duplicates, but it will add
too many changes too, and I didn't think it's worth doing so.
The amount looks ok to me.
### `window-title-font-family = PT Mono`
| Phase | Metric | base | branch | Δ | ratio |
|---|---|---:|---:|---:|---:|
| Idle, 3 s | main-thread CPU | 2.8 ms | 2.8 ms | -0.0 | 1.00 |
| | process CPU | 11.3 ms | 11.2 ms | -0.1 | 0.99 |
| Paced title updates, 150 × 100 ms | main-thread CPU | 878.5 ms |
**946.7 ms** | **+68.3** | **1.08** |
| | process CPU | 1219.9 ms | 1335.0 ms | +115.1 | 1.09 |
| | wall | 17.24 s | 17.34 s | +0.1 | 1.01 |
| Title burst, 5000 back-to-back | main-thread CPU | 180.7 ms | 181.4 ms
| +0.7 | 1.00 |
| | process CPU | 254.2 ms | 254.8 ms | +0.6 | 1.00 |
| | wall | 2.31 s | 2.32 s | +0.0 | 1.00 |
| `toggle_maximize` × 16 (animated resize) | main-thread CPU | 1947.8 ms
| **2178.0 ms** | **+230.2** | **1.12** |
| | process CPU | 4166.8 ms | 4403.2 ms | +236.4 | 1.06 |
| | wall | 16.68 s | 16.72 s | +0.0 | 1.00 |
| Native fullscreen enter/exit × 2 | main-thread CPU | 196.8 ms | 195.9
ms | -0.9 | 1.00 |
| | process CPU | 360.8 ms | 361.5 ms | +0.7 | 1.00 |
| | wall | 8.47 s | 8.47 s | +0.0 | 1.00 |
### AI Disclosure
Asked Claude to generate the harness to run the benchmark and review my
changes. I did the changes myself.
IME preedit text was rendered with unthickened glyphs because
addPreeditCell omitted font_thicken options when calling
renderCodepoint, leaving them at their default false values.
Fixes#13758
Closes#2630 for macOS. #13621 added the `move_tab_to_new_window` action
and the GTK side; this does the same on macOS with AppKit.
With native tabs a tab is already a window, so the action forwards to
AppKit's own `moveTabToNewWindow:`. It's a no-op when the window is
alone in its tab group, same as the Window menu item. The "only
implemented on Linux" note comes off the doc comment in `Binding.zig`.
Tested on macOS 26, from a keybind and from the command palette. I have
no macOS 13-15 machine.
Full disclosure, written with Claude Code, I directed it, read every
line, and tested the result.
Repeated builds were penalized for three reasons:
1. The gresource XML embedded the absolute cache paths of the compiled
.ui files. It now uses relative paths, resolved against a `--sourcedir`
passed to glib-compile-resources.
2. Blueprints were compiled through a small Zig wrapper, and a Run step
hashes the bytes of the executable it runs. A Zig binary does not relink
to the same bytes, so after a branch switch that touched the wrapper
every .ui moved, the gresource compiler re-ran and the whole app
recompiled for identical output. blueprint-compiler is now run directly
and the wrapper is reduced to a single version check whose output
nothing reads.
3. The gresource pipeline was built once per artifact. It is now
memoized on the *std.Build.
Cold builds and builds after real blueprint changes are unaffected. A
build after the wrapper relinks goes from ~43s to ~7s on my system.
AI disclosure: Claude Opus 5 was used to pinpoint the source of the
cache poisoning and to prepare a patch. Claude Fable 5.1 measured it,
traced the remaining churn to the wrapper's relinks, and replaced a
custom build step with stock ones. The commit message is human-written
based on the author's understanding of the changes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RsfB3eoSTLfnKa6zi25teN
This commit refactors our use of translate-c, in preparation for larger
removal of `cImport` and better co-ordination between building of C
dependencies and translation of headers.
The major update is the creation of an internal helper package that
wraps our use of the external translate-c library. This allows us to not
only have better shorthand and a data-driven, declarative approach to C
translation (versus the otherwise more imperative approach), it also
funnels the external dependency into a single package instead of
spreading it out among what will be an increasingly larger amount of
places as dependencies in `pkg/` get updated.
It also includes some refactors, namely to the harfbuzz package, which
has had its individual settings refactored into helpers to allow for the
settings to be better shared between translation and the build of the
c-based static library.
Wuffs has also had a bit of a refactor too so that we don't generate a
file with all of the macro defines in it - we just send these in as `-D`
flags now.
The action and its keybind exist, and GTK implements them, but macOS had no
handler so the binding did nothing there. AppKit already has the command for
window tabs, so this forwards to it.
A window that isn't in a tab group, or is alone in one, is already a window of
its own, so there is nothing to move and the action reports it did nothing.
Implements the remaining macOS half of #2630.
This commit refactors our use of translate-c, in preparation for larger
removal of cImport and better co-ordination between building of C
dependencies and translation of headers.
The major update is the creation of an internal helper package that
wraps our use of the external translate-c library. This allows us to not
only have better shorthand and a data-driven, declarative approach to C
translation (versus the otherwise more imperative approach), it also
funnels the external dependency into a single package instead of
spreading it out among what will be an increasingly larger amount of
places as dependencies in "pkg/" get updated.
It also includes some refactors, namely to the harfbuzz package, which
has had its individual settings refactored into helpers to allow for the
settings to be better shared between translation and the build of the
c-based static library.
Wuffs has also had a bit of a refactor too so that we don't generate a
file with all of the macro defines in it - we just send these in as "-D"
flags now.
Implements TinyIO for Windows which is used to save binary and runtime
costs. As a reminder, binary costs are saved because `std.Io` uses a
vtable so compilers can't prune any unused functions, so you pay for the
full cost. We can noop unused functions to save. Runtime is saved
because there is less state to carry for unused functionality like
concurrency primitives.
The impl itself is mostly taken from Zig directly. I ran tests on
Windows (arm64) and verified everything works as expected so far!
Binary size measurements before/after:
| Mode | Io owner | ghostty-vt.dll | vs. Threaded |
|--------------|-----------------|---------------:|-------------:|
| ReleaseFast | std.Io.Threaded | 2,209,280 | |
| ReleaseFast | TinyIo | 1,826,816 | -382,464 |
| ReleaseSmall | std.Io.Threaded | 1,541,632 | |
| ReleaseSmall | TinyIo | 1,199,616 | -342,016 |
The runtime savings are relatively small, but 1KB per terminal ain't
nothing:
| Io owner | Private, +100 terminals | Private, startup |
|-----------------|------------------------:|-----------------:|
| std.Io.Threaded | +161,845,248 | 782,336 |
| TinyIo | +161,742,848 | 729,088 |
Implements TinyIO for Windows which is used to save binary and runtime
costs. As a reminder, binary costs are saved because `std.Io` uses a
vtable so compilers can't prune any unused functions, so you pay for the
full cost. We can noop unused functions to save. Runtime is saved
because there is less state to carry for unused functionality like
concurrency primitives.
The impl itself is mostly taken from Zig directly. I ran tests on
Windows (arm64) and verified everything works as expected so far!
Binary size measurements before/after:
| Mode | Io owner | ghostty-vt.dll | vs. Threaded |
|--------------|-----------------|---------------:|-------------:|
| ReleaseFast | std.Io.Threaded | 2,209,280 | |
| ReleaseFast | std.Io.failing | 1,815,552 | -393,728 |
| ReleaseFast | TinyIo | 1,826,816 | -382,464 |
| ReleaseSmall | std.Io.Threaded | 1,541,632 | |
| ReleaseSmall | std.Io.failing | 1,190,912 | -350,720 |
| ReleaseSmall | TinyIo | 1,199,616 | -342,016 |
The runtime savings are relatively small, but 1KB per terminal ain't nothing:
| Io owner | Private, +100 terminals | Private, startup |
|-----------------|------------------------:|-----------------:|
| std.Io.Threaded | +161,845,248 | 782,336 |
| TinyIo | +161,742,848 | 729,088 |