When the kitty keyboard protocol "report all keys as escape codes" mode
was active, composed/IME text (e.g. from dead keys or compose sequences)
was silently dropped.
This happened because the composed text is sent within our GTK apprt
with key=unidentified and no unshifted_codepoint, so no kitty entry was
found and the encoder returned without producing any output. The
plain-text fallback was also skipped because report_all bypasses it.
Send composed text as raw UTF-8 when no kitty entry is found, matching
the behavior of Kitty on Linux for me.
Fixes#10049
Fixes#7937
Added `computeInitialSize` to GTK `Surface` and call it in GTK
`Application` before the first `present()`, so the window manager
centers the correct size on initial show.
The issue occurs because the core `Surface.recomputeInitialSize()` runs
only after the renderer is initialized. In GTK, the `GLArea` isn’t
realized until after `present()`, so the initial size arrives too late
for WM centering.
**Limitations**: when we precompute size before `present()` we do not
have access to padding, so the sizing will be very slightly off... but
since it is only off a few pixels I was unable to tell visually that it
wasn't perfectly centered.
**Other thoughts**: I was hesitant to make changes to core `Surface`
because the issue is Linux-specific, but it may make sense to extract a
helper from `recomputeInitialSize` to avoid duplicating the sizing math.
**AI Disclosure:** I used AI to explore the project, help with any
language / API questions (I've never used zig before and rarely use
gtk), and make implementation suggestions.
When cursor-click-to-move is set to false, disable all prompt
click-to-move mechanisms including shell-native methods such as OSC 133
cl= (arrow key synthesis) and click_events.
I forgot to port this config over when we did the OSC133 stuff.
Also update the config documentation to accurately describe the current
behavior.
Fixes#11138
When insertBlanks clears the entire region from cursor to the right
margin (scroll_amount == 0), a wide character whose head is at the right
margin gets cleared but its spacer_tail just beyond the margin is left
behind, causing a "spacer tail not following wide" page integrity
violation.
Move the right-margin wide-char cleanup from inside the scroll_amount >
0 block to before it, so it runs unconditionally — matching the
rowWillBeShifted pattern of cleaning up boundary-straddling wide chars
up front.
Found via AFL++ fuzzing. #11109
When deleteLines or insertLines count >= scroll region height, all rows
go through the clear-only path (no shifting). This path did not call
rowWillBeShifted, leaving orphaned spacer_tail cells when wide
characters straddled the right margin boundary, causing a "spacer tail
not following wide" page integrity violation.
Add rowWillBeShifted before clearCells in the else branch of both
functions.
Found via AFL++ fuzzing. #11109
When deleteLines or insertLines count >= scroll region height, all rows
go through the clear-only path (no shifting). This path did not call
rowWillBeShifted, leaving orphaned spacer_tail cells when wide characters
straddled the right margin boundary, causing a "spacer tail not following
wide" page integrity violation.
Add rowWillBeShifted before clearCells in the else branch of both
functions.
Found via AFL++ fuzzing. #11109
resizeWithoutReflowGrowCols has a fast path that reuses existing page
capacity when growing columns: it simply bumps page.size.cols without
touching cell data. If any row has a spacer_head at the old last column
(from a wide char that did not fit), that cell is no longer at the end
of the now-wider row, causing a page integrity violation.
Fix by checking for spacer_head cells at the old last column before
taking the fast path. If any are found, fall through to the slow path
which handles spacer heads correctly via cloneRowFrom.
Found by AFL++ stream fuzzer. #11109
printCell, when overwriting a wide cell with a narrow cell at x<=1 and
y>0, unconditionally sets the last cell of the previous row to .narrow.
This is intended to clear a spacer_head left by a wrapped wide char, but
the cell could be a spacer_tail if a wide char fit entirely on the
previous row. Setting a spacer_tail to .narrow orphans the preceding
.wide cell, which later causes an integrity violation in insertBlanks
(assert that the cell after a .wide is .spacer_tail).
Fix by guarding the assignment so it only fires when the previous row's
last cell is actually a .spacer_head. The same fix is applied in both
the .wide and .spacer_tail branches of printCell.
Found by AFL++ stream fuzzer.
insertBlanks checks whether the last source cell being shifted is wide
and clears it to avoid splitting, but it did not check the destination
cells at the right edge of the scroll region. When a wide character
straddles the right scroll margin (head at the margin, spacer_tail just
beyond it), the swap loop displaced the wide head without clearing the
orphaned spacer_tail, causing a page integrity violation
(InvalidSpacerTailLocation).
Fix by checking the cell at the right margin (last destination cell)
before the swap loop and clearing it along with its spacer_tail when it
is wide.
Found by AFL++ stream fuzzer. #11109
Printing a wide character at the right edge of the screen with an active
hyperlink triggered a page integrity violation (UnwrappedSpacerHead).
printCell wrote the spacer_head to the cell and then called
cursorSetHyperlink, whose internal integrity check observed the
spacer_head before printWrap had a chance to set the row wrap flag.
Fix by setting row.wrap = true before calling printCell for the
spacer_head case, so all integrity checks see a consistent state.
printWrap sets wrap again afterward, which is harmless. Found by AFL++
stream fuzzer.
A trailing colon with no following sub-parameter (e.g. "ESC[58:4:m")
leaves the colon separator bit set on the last param without adding
another entry to the params array. When the SGR parser later iterates to
that param (4 = underline) and sees the colon bit, it entered the colon
path which asserted slice.len >= 2, but the slice only had one element.
Replace the assert with a bounds check that treats the malformed
sequence as a default single underline.
Add a regression test reproducing the crash from AFL++ fuzzing
(afl-out/stream/default/crashes/id:000021).
#11109
A trailing colon with no following sub-parameter (e.g. "ESC[58:4:m")
leaves the colon separator bit set on the last param without adding
another entry to the params array. When the SGR parser later iterates
to that param (4 = underline) and sees the colon bit, it entered the
colon path which asserted slice.len >= 2, but the slice only had one
element.
Replace the assert with a bounds check that treats the malformed
sequence as a default single underline.
Add a regression test reproducing the crash from AFL++ fuzzing
(afl-out/stream/default/crashes/id:000021).
A fuzz crash found that CSI g with a parameter that saturates to
u16 max (65535) causes @enumFromInt to panic when narrowing to
TabClear (enum(u8)). Use std.meta.intToEnum instead, which safely
returns an error for out-of-range values.
CSI @ (ICH) with an explicit parameter of 0 should be clamped to 1,
matching xterm behavior. Previously, a zero count reached
Terminal.insertBlanks which called clearCells with an empty slice,
triggering an out-of-bounds panic.
Fix the stream dispatch to clamp 0 to 1 via @max, and add a defensive
guard in insertBlanks for count == 0. Found by AFL++ stream fuzzer.
CSI ? W (cursor tabulation control) accessed input.params[0] without
first checking that params.len > 0, causing an index out-of-bounds
panic when the sequence had an intermediate but no parameters.
Add a params.len == 1 guard before accessing params[0].
Found by AFL++ fuzzing.
This adds a `test/fuzz-libghostty` which is a standalone `zig build`
target for building an AFL++ instrumented executable for fuzzing the
libghostty-vt parser. I also added a `pkg/afl++` (based on zig-afl-kit)
so instrumenting objects and using AFL++ is a bit easier.
Fuzzing `libghostty-vt`'s parser is as easy as `zig build run`, but see
the README for a lot more details. I ran the fuzzer for ~14 hours total
and only found one crash #11088. I'm pretty confident at this point our
Parser layer isn't obviously crash-able, but need to instrument more
places to fuzz.
We don't use Zig's built-in fuzzing yet because as of 0.15 (our current
stable), it isn't ready and AFL++ is an industry proven tool to do this.
This fixes a bug in the key state sequence overlay.
## Demo
In my ghostty config, I have
keybind = ctrl+space>escape=ignore
keybind = ctrl+space>p=toggle_command_palette
...
because I use `ctrl+space>` sequences for most things and so hitting
`esc` is my way to bail out of the sequence if I change my mind.
I just switched to tip and got the new GTK key sequence overlay. Here's
what I saw. In these screen recordings, the sequence of keys I press is
ctrl+space, escape, ctrl+space, escape, ctrl+space, escape, ctrl+space,
p
https://github.com/user-attachments/assets/4a37bc7e-b75c-4bd1-99de-f21f4211b5b5
after the fix:
https://github.com/user-attachments/assets/023be88e-1299-4219-920c-1b1134b2888c
## Notes
I believe this was also a leak, since the queued keys wouldn't be
deinited.
**AI usage:** Claude Code suggested the fix, then I read enough code to
convince myself that it makes sense.
When users have something like
[log]
showSignature = true
in their .gitconfig files, invocations of the log or show git sub-command
emit additional information about signatures. This additional output
disturbs the generation of short_hash in GitVersion.zig, the additional text
is copied verbatim into the string and then shown in the CSI >q output.
To fix it always suppress the output of the signature information. This
has no effects when the setting is disabled anyway.
When a DCS sequence has more than MAX_PARAMS parameters, entering
dcs_passthrough would write to params[params_idx] without a bounds
check, causing an out-of-bounds access. Drop the entire DCS hook
when params overflow, consistent with how csi_dispatch handles it.
Found by AFL fuzzing.
While it was renamed from ko_KR.UTF-8.po to ko.po in #10976, @uhojin,
a Korean locale maintainer, notes [1] that “ko_KR [*South* Korean] makes
more sense in locale context just to avoid any potential confusion
between 한국어 vs 조선어”.
Despite ko_KP (North Korean) not being present in glibc (as of version
2.43), and the ISO639 maintainers expressing disapproval of ko_KP [2],
it is possible opinions may change in the future, and individual
opinions may be contested—disambiguating doesn't hurt.
[1]: https://github.com/ghostty-org/ghostty/pull/10976#discussion_r2861424171
[2]: https://github.com/ghostty-org/ghostty/pull/10976#discussion_r2861359240
Following the discussion at #10852, I believe this is the right default.
I'm willing to continue to revisit this decisions, but Ghostty 1.3 is
around the corner and I don't think such a change like this should be
pushed into it.
I think palette generation is best left as a _theme author_ tool. A
Ghostty color theme could include `palette-generate=true` if it wants
to customize the 256-color palette more easily. Of course, end users can
as well anytime.
Another part of my reasoning is that TUI programs who want this behavior
can already achieve it themselves by mixing dark/light theme detection
via CSI 996 (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/)
with OSC 4/10/11 color query and change sequences, both of which are
decently supported in the terminal ecosystem and fully supported in
Ghostty.
I'm also open to considering some kind of new sequence to make this
easier for TUIs (probably a mode) where they can opt-in to palette
generation plus "harmonius" palettes (see `palette-harmonius`) and
Ghostty does it on demand then. I think that'd solve the legacy vs new
TUI argument where legacy programs can continue to make assumptions
about the palette and new programs can opt-in to a more dynamic palette
without having to do a lot of work themselves.
## Summary
Ports the phantom mouse-motion position-equality check from the GTK
runtime to the embedded runtime (used by macOS).
On macOS, TUI apps like Zellij that frequently update the window title
cause phantom `mouseMoved` events at the same coordinates. These flow
through `embedded.zig` → `Surface.zig` `cursorPosCallback` →
`showMouse()`, which explicitly calls
`NSCursor.setHiddenUntilMouseMoves(false)` and unhides the cursor,
defeating `mouse-hide-while-typing`.
The GTK runtime already filters these in PR #4973 (for #3345):
```zig
const is_cursor_still = @abs(priv.cursor_pos.x - pos.x) < 1 and
@abs(priv.cursor_pos.y - pos.y) < 1;
if (is_cursor_still) return;
```
This PR adds the same check to `embedded.zig`'s `cursorPosCallback`,
using the already-stored `self.cursor_pos` field.
## Test plan
- [x] Enable `mouse-hide-while-typing = true` in Ghostty config
- [ ] Run a TUI app that updates the window title frequently (e.g.
Zellij)
- [ ] Type — cursor should hide and stay hidden despite title updates
- [ ] Move the mouse — cursor should reappear normally
- [ ] Verify no regressions with normal mouse movement,
focus-follows-mouse, or link hovering
Implements parsing for OSC 3008, which allows terminal emulators to keep
track of the stack of processes that have current control over the tty.
The implementation mirrors existing `semantic_prompt.zig` architecture
and natively maps UAPI definitions to Zig structures with lazy
evaluation for optional metadata.
Fixes#10900
On macOS, TUI apps like Zellij that frequently update the window title
cause phantom mouse-move events to be generated at the same coordinates.
These phantom events reach cursorPosCallback in the core, which calls
showMouse() and explicitly unhides the cursor via
NSCursor.setHiddenUntilMouseMoves(false), defeating the
mouse-hide-while-typing feature.
This ports the same position-equality check already present in the GTK
runtime (added in PR #4973 for issue #3345) to the embedded runtime used
by macOS. If the cursor position hasn't changed by more than 1px, the
event is discarded.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use `std.meta.stringToEnum` in ContextType and ExitStatus
- Ensure `parseInt` only accepts digits for pids
- Use `@tagName` for string representation in Field
- Rename `fields_raw` to `metadata`
- Rename `readField` to `readOption`
Implements parsing for OSC 3008, which allows terminal emulators to keep track of the stack of processes that have current control over the tty. The implementation mirrors existing `semantic_prompt.zig` architecture and natively maps UAPI definitions to Zig structures with lazy evaluation for optional metadata.
Fixes#10900
* ensure that `ghostty.h` compiles during basic Zig tests
* ensure that non-exhaustive enums are kept synchronized between
`ghostty.h` and their respective Zig counterpart.
* adjust some enums that varied from established conventions
With zsh, when installing the ghostty terminfo on a server via the
ssh-terminfo shell integration, parts of the terminfo get mangled. In
particular, the newline escape sequence in
```
> infocmp -0 -x xterm-ghostty | grep ind=
...,ind=\n,indn=...
```
gets interpreted by `print` as a literal newline, which then just gets ignored / does not have the intended effect.
Documentation for the `-r` flag of `print` used in the fix is [here](https://zsh.sourceforge.io/Doc/Release/Shell-Builtin-Commands.html#:~:text=Ignore%20the%20escape%20conventions%20of%20echo.).
### Testing locally
You can directly demonstrate this locally. This outputs a host of warning messages:
```
ssh_terminfo=$(infocmp -0 -x xterm-ghostty 2>/dev/null)
print "$ssh_terminfo" | tic -x -
```
Whereas
```print -r "$ssh_terminfo" | tic -x -```
or
```infocmp -0 -x xterm-ghostty | tic -x -```
work without issue.
### Testing remotely
The most visible way is to observe the output of `htop` before and after the change.
More directly, the output of `infocmp -x xterm-ghostty | grep " ind="` should be
```ich=\E[%p1%d@, ich1=\E[@, il=\E[%p1%dL, il1=\E[L, ind=\n,```
instead of
```ich=\E[%p1%d@, ich1=\E[@, il=\E[%p1%dL, il1=\E[L, ind=,```
---
Discussed in #11031.
---
AI disclosure: I used Claude for parts of figuring out what was going on. The fix itself and the rest was written and tested by myself.
The C struct Palette.C declared colors as [265]Color.C, but the
terminal palette is 256 colors (terminal.color.Palette = [256]RGB)
and the C header ghostty_config_palette_s correctly uses colors[256].
The mismatch causes ghostty_config_get to write 265×3 = 795 bytes
through a pointer sized for 256×3 = 768 bytes, producing a 27-byte
buffer overflow. On macOS Release builds with stack protector enabled,
this triggers __stack_chk_fail → SIGABRT on launch.
Fixes#10406
ImGui_ImplOpenGL3_Shutdown() calls imgl3wShutdown() which dlcloses the
GL library handles but does not zero out the imgl3w function pointer
table (imgl3wProcs). When a GLArea is re-realized (e.g. during
reparenting), ImGui_ImplOpenGL3_Init() calls ImGui_ImplOpenGL3_InitLoader()
which checks "if (glGetIntegerv == nullptr)". Since the stale pointers
are non-null, it skips re-initialization. The next GL call through a
dangling function pointer causes a SIGSEGV.
Fix this by introducing ImGui_ImplOpenGL3_ShutdownWithLoaderCleanup()
which calls the normal shutdown and then zeroes the imgl3wProcs table,
forcing the next Init to reload GL function pointers via imgl3wInit().
Also properly destroy the ImGui context and reset widget state in
glAreaUnrealize so re-realize starts clean. This was extra but was
probably leaking memory.
Specifically:
iCurrentCursorStyle
iPreviousCursorStyle
iCurrentCursorVisible
iPreviousCursorVisible
Visibility calculated and updated independently from the typical cursor
unifrom updates to preserve cursor style even when not in the viewport
or set to be hidden