This fixes the Windows build failure discussed in
https://github.com/ghostty-org/ghostty/discussions/10148
When building on Windows, `symbols-unigen` and `props-unigen` crash with
`error.FileTooBig` because `stdout.end()` calls `setEndPos()` to
truncate the output. Windows does not support `SetEndOfFile` on pipes or
console handles - it returns `ERROR_INVALID_PARAMETER`, which Zig maps
to `error.FileTooBig`.
Using `flush()` instead of `end()` is correct here because:
1. `end()` flushes AND truncates - useful when overwriting files that
might have leftover content
2. For stdout captured as a pipe, there's nothing to truncate - we're
writing sequentially to a fresh pipe
3. `flush()` ensures all buffered data is sent, which is all that's
needed
CI before fix was failing with FileTooBig, after fix builds
successfully:
https://github.com/remorses/opentui/actions/runs/20671299561/job/59352503875
This PR simplifies and corrects the logic for placing a glyph
vertically, by using the `position.y` from `CoreText` directly, instead
of using an offset from the cell's starting `y`. The logic was incorrect
from the beginning, always treating the first glyph of a cell as being
at `y` of zero. We only need to be subtracting the cell's starting `x`
to align the glyphs to the cell grid.
Enabling the commented out logging, I found no instances of `position.y
differs from old offset.y` lines with `JetBrains Mono` with ligatures
turned on, but running
[ttylang](https://github.com/jacobsandlund/ttylang) (printing the
Universal Declaration of Human Rights in various languages) revealed 676
instances of this, with many only slightly off.
An example log from some Tai Tham text is the following, and this PR
adds a test based on this:
```
...pos=(0.00,-8.21) run_offset=(69.41,-8.21) cell_offset=(69.41,-8.21) old offset.y=0.00 cps = \u{1a49}\u{1a60} \u{1a3f}▸\u{1a69} \u{1a2f} → ᩉ᩠ᨿᩩᨯ
```
Browsers display this as:
ᩉ᩠ᨿᩩ
`main` is printing:
<img width="852" height="90" alt="CleanShot 2026-01-05 at 10 28 17@2x"
src="https://github.com/user-attachments/assets/c97b738c-8fe4-48b5-81f8-e0e79f1a9269"
/>
this PR prints:
<img width="958" height="90" alt="CleanShot 2026-01-05 at 10 29 07@2x"
src="https://github.com/user-attachments/assets/88fd26a7-8041-4b33-ab02-56f411204b04"
/>
Since this is a ligature of two different grapheme clusters, Ghostty
ends up subtracting too much of the `x` value with the `cell_offset.x`
(starting x), so neither of the screenshots above are correct, but the
second is closer and gets the `y` value right.
AI disclaimer: I didn't use AI for the code, but did ask it about this
Tai Tham text and why it wasn't a single grapheme cluster:
https://ampcode.com/threads/T-019b8ea2-1822-75bb-a8eb-55a9ddb9f7ea
Gtk implementation of #9945. Fixes#9948.
This adds session search to the command palette on Gtk, allowing you to
jump to any surface by title or working directory. The main difference
to the Mac OS implementation is that tabs do not have colors by which to
search.
The VT formatter was treating cells without text as blank and emitting
them as plain spaces, losing any background color styling. This caused
TUIs like htop to lose their background colors when rehydrating terminal
state (e.g., after detach/reattach in zmx).
For styled formats (VT/HTML), cells with background colors or style_id
are now emitted with proper SGR sequences and a space character instead
of being accumulated as unstyled blanks.
Adds handling for bg_color_palette and bg_color_rgb content tags which
were previously unreachable.
Reference: https://ampcode.com/threads/T-019b7a35-c3f3-73fc-adfa-00bbe9dbda3c
The issue is in ghostty_src/src/terminal/formatter.zig#L1117-L1129:
- Cells without text are treated as "blank" (line 1117-1119) - this includes cells that only have background colors
- When blank cells are emitted, they're plain spaces (line 1129) - writer.splatByteAll(' ', blank_cells) outputs spaces without any SGR styling
- Background-only cells (bg_color_palette, bg_color_rgb) are marked unreachable (lines 1233-1235) because the code assumes hasText() already filtered them
This means when htop draws a row like:
`[green bg]CPU: 45%[red bg] [default]`
The trailing cells with red background but no text get accumulated as blanks and emitted as plain spaces - losing the background color.
End the currently active key sequence, if any, and flush the
keys up to this point to the terminal, excluding the key that
triggered this action.
For example: `ctrl+w>escape=end_key_sequence` would encode
`ctrl+w` to the terminal and exit the key sequence.
Normally, an invalid sequence will reset the key sequence and
flush all data including the invalid key. This action allows
you to flush only the prior keys, which is useful when you want
to bind something like a control key (`ctrl+w`) but not send
additional inputs.
## Summary
When `paste_from_clipboard` is triggered but the clipboard contains no
text (e.g., an image), the action now returns `false` to indicate it
couldn't be performed. This enables the `performable:` keybind prefix to
work correctly for paste actions.
## Problem
On GTK/Linux, when a user has `keybind = ctrl+v=paste_from_clipboard`
and the clipboard contains an image (not text), pressing Ctrl+V does
nothing. Applications like `opencode` that handle their own clipboard
reading via `wl-paste` never receive the keypress.
## Solution
Make `clipboardRequest` return `bool` to indicate whether the action
could be performed. For paste requests on GTK, synchronously check if
the clipboard contains text formats before starting the async read. When
no text format is available, return `false`.
Users can now use:
```
keybind = performable:ctrl+v=paste_from_clipboard
```
When the clipboard has no text, the keybind is not consumed and Ctrl+V
passes through to the terminal application.
## Changes
- `Surface.startClipboardRequest` now returns `bool`
- `paste_from_clipboard` / `paste_from_selection` actions return the
result
- GTK apprt checks clipboard formats synchronously before async read
- Embedded apprt always returns `true` (can't check synchronously)
## Testing
1. Add `keybind = performable:ctrl+v=paste_from_clipboard` to config
2. Copy an image to clipboard
3. Open an application that handles image paste (e.g., `opencode`)
4. Press Ctrl+V
5. Image pastes successfully (app receives keypress and handles
clipboard itself)
## Disclaimer
Most of the changes is done with Opus 4.5
Make clipboardRequest return bool to indicate whether the action could
be performed. For paste requests, synchronously check if the clipboard
contains text formats before starting the async read.
This allows 'performable:paste_from_clipboard' keybinds to pass through
when the clipboard contains non-text content (e.g., images), enabling
terminal applications to handle their own clipboard reading.
Changes:
- Surface.startClipboardRequest now returns bool
- paste_from_clipboard/paste_from_selection actions return the result
- GTK apprt checks clipboard formats synchronously before async read
- Embedded apprt always returns true (can't check synchronously)
- All other call sites discard the return value with _
When paste_from_clipboard is triggered but the clipboard contains no
text (e.g., an image), send the raw Ctrl+V keypress to the terminal
instead of silently returning. This allows applications to handle
their own clipboard reading (e.g., via wl-paste for images on Wayland).
Our shell integration routines can now fail when resources are missing.
This change introduces tests to ensure that they leave behind a clean
environment upon failure.
The bash integration needed a little reordering to support this.
This is a regression introduced when we added macOS support for custom
entries. I mistakingly thought that only custom entries were in the
config, but we do initialize it with all!
Related to #10063
This fixes a crash that can happen if the SlidingWindow search portion
sees a zero-byte page.
We have more fixes to implement in the circular buffer handling but
putting the fix at this layer also prevents some unnecessary allocations
for zero-byte data.
Fixes#7643
This commit address the issue with 3 minor fixes:
1. Initialize ghostty lib before app start, or global allocator will
be null.
2. `addSublayer` should be called on CALayer object, which is the
property 'layer' of UIView
3. According to apple's [document](https://developer.apple.com/documentation/metal/mtlstoragemode/managed?language=objc),
managed storage mode is not supported by iOS. So always use shared
mode.
FYI, another [fix](https://github.com/mitchellh/libxev/pull/204) in libxev
is also required to make iOS app work.
Fixes#9958
Replaces #9989
This changes the search navigation logic to always scroll if there is a
selected search result so long as the search result isn't already within
the viewport.
Our existing logic already ensured that setupFeatures() was always
called, but that was happening from two code paths: explicitly when
shell integration is .none and implicitly via setup().
We can simplify this by always calling setupFeatures() once, outside of
the (automatic) shell integration path.
There's one small behavioral change: we previously didn't set up shell
features in the automatic shell integration path if we didn't have a
resources directory (as a side effect). Resources are required for shell
integrations, but we don't need them to export GHOSTTY_SHELL_FEATURES,
which could potentially still be useful on its on.
This adds some new special case handling for key sequences when an
unbound keyboard input is received. If the current keybinding set scope
(i.e. active tables) has a `catch_all` binding that would `ignore`
input, then the entire key sequence is dropped.
Normally, when an unbound key sequence is received, Ghostty encodes it
and sends it to the running program.
This special behavior is useful for things like Vim mode which have `g>g`
to scroll to top, and a `catch_all=ignore` to drop all other input. If
the user presses `g>h` (unbound), you don't want `gh` to show up in your
terminal input, because the `catch_all=ignore` indicates that the user
wants that mode to drop all unbound input.