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 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.