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