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
On main, after rearranging panes, the window becomes permanently
immovable. Grab handles temporarily set `window.isMovable = false` on
hover to prevent window dragging from interfering with pane drags
(fixing [#10110](https://github.com/ghostty-org/ghostty/issues/10110)),
but the restoration logic failed when views were destroyed during pane
rearrangement (which happens each time a pane is rearranged).
The previous approach managed `window.isMovable` state across the view
lifecycle:
1. `mouseEntered` → saved and disabled `window.isMovable`
2. View removed during rearrangement → `mouseExited` never fired
3. `deinit` ran with `self.window` already nil → restoration failed
4. Window stuck with `isMovable = false`
Instead of managing window state, prevent the mouseDown event from
reaching the window's drag handler by overriding mouse event handling in
the grab handle view.
Per [Apple's Event Handling
Guide](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/EventOverview/HandlingMouseEvents/HandlingMouseEvents.html):
> Custom NSView objects should not invoke super in their implementations
of NSResponder mouse-event-handling methods such as mouseDown:,
mouseDragged: and mouseUp: unless it is known that the inherited
implementation provides some needed functionality.
This eliminates all state management while solving both the original
issue (#10110) and the new bug.
AI disclosure: claude code found and wrote the fix. I tested it manually
to see that it works. I pressed claude quite hard here to come up with
the best fix, and looked at documentation to understand what the fix was
doing. It seems like this is a better approach overall to preventing the
main window from being dragged when grabbing the Surface Drag handle.
After rearranging panes, the window becomes permanently unmovable.
Grab handles temporarily set `window.isMovable = false` on hover to prevent
window dragging from interfering with pane dragging.
Override `viewWillMove(toWindow:)` to catch when the view is being removed from
the window. This lifecycle method is called before the window reference
becomes nil, allowing us to restore `window.isMovable`.
When using split panes with custom shaders, unfocused panes receive
frame re-draws when mod keys are held or when the mouse hovers over a
link. This behavior is desirable in general, but shaders have no way to
detect when the frames they are drawing are "real" or a momentary glitch
frame.
Fixes#8456 indirectly, by allowing the shader to be updated to handle
this situation.
This PR adds two new shadertoy uniforms:
```glsl
float iTimeFocus
```
- Timestamp of when the surface last gained focus (set to current iTime,
similar to iTimeCursorChange timestamp)
- Allows calculating time since focus: iTime - iTimeFocus
- Resets on each focus gain, enabling "focus trigger" animations
- Uses the same focus state signal that changes unfocused-split-fill
config option
```glsl
float iFocus
```
- Current focus state: 1.0 when focused, 0.0 when unfocused
- Simple boolean check: if (iFocus == 1.0)
- This indicates that the frame being rendered is a defocused frame (for
example, upon leaving the surface, or when a mod key is held or an OSC8
link is hovered), allowing shaders to apply consistently instead of
flashing.
## Example
This does nothing, but it does show the two costumes and how you might
use them
```glsl
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
if (iFocus == 1.0) {
// Focused: animate based on time since focus
float timeSinceFocus = iTime - iTimeFocus;
// Resume normal animation
} else {
// Unfocused: dim or hide effects
}
}
```
Here's the included example shader being applied to this test build:
https://github.com/user-attachments/assets/4932e12f-6a1c-42dd-81f3-19da9a551c95
And here's a [more exciting
shader](https://github.com/martinemde/dotfiles/blob/main/home/dot_config/ghostty/focus_vignette.glsl)
that locates your cursor upon switching to a surface, renders a
fade/zoom in of the cursor on focus, and applies a shadow vignette to
the defocused surfaces.
https://github.com/user-attachments/assets/32b98956-59f0-4e4b-9c2f-ab79b147205f
AI disclosure: I used claude code to generate everything is this PR, but
I reviewed it, understand what it is doing, tested it manually, and
believe it is correct. I also tested by directly creating shadertoys
that use the new costumes (shown above) and have uploaded the examples
here showing that it works.
When you press a keyboard shortcut that has a menu equivalent, the menu
bar should flash briefly. This is standard macOS behavior.
This change calls `performKeyEquivalent` on the main menu before
checking Ghostty bindings, so shortcuts with menu items get handled by
the menu system first.
This works for most shortcuts - Cmd+V, Cmd+N, Cmd+T, etc. all flash now.
Won't flash for `performable: true` (like Cmd+C copy) -- excluded from
menu: https://github.com/ghostty-org/ghostty/discussions/2811
The change skips menu handling when in a key sequence or key table, so
those still work correctly.
This updates our Imgui version from 1.90.6 to 1.92.5 which is almost 18
months of changes. In the process, we've also migrated from cimgui to
Dear Bindings, the official way to do C bindings.
This PR doesn't contain any functionality changes, only the dependency
change and API changes necessary to achieve the same behavior.
This makes it so that dragging a split over a tab in our app will focus
that tab. Presently, it's very hard if not impossible to get the drag to
focus the tab. This just does it manually.
This also fixes a bug where the last split drop out of a tab will close
the entire window, this affects main currently too.
This is still a draft because I'm chasing down one issue still where the
dropped surface fails to render properly and its unclear why yet.
We were previously storing the C struct which contained pointers into
ephemeral memory that could cause segfaults later on. I think this was a
tip regression, because in 1.2 despite doing this, we always referenced
static memory so it was fine. With tip, we now accept custom command
palette entries so it's dynamically allocated.
Following #10082 I took the opportunity to clean up the code in the
nautilus extension a little. Specifically, this pull request
- turns methods that weren't using `self` into functions,
- inlines a helper method that was trivial after #10082,
- extracts duplicate code in both menu item callbacks into a new helper
function, and
- removes the compatibility hack for the Nautilus 3.0 API, i.e. GNOME 42
and older.
I'm not that sure about the last point, as it's not clear to me what the
support baseline is here: GNOME 43 (the first version to support the
Nautilus 4.0 API) was released in Sep 2022, i.e. is almost as old as
Ghostty itself. The previous Debian stable release already included
GNOME 43, but Ubuntu 22.04 (the LTS before the current 24.04 LTS) still
includes GNOME 42. Does Ghostty support a system that old? If so, I'll
drop the corresponding commit from this PR.
Discussion: https://github.com/ghostty-org/ghostty/discussions/10104
Summary:
When background blur uses macOS glass styles, the titlebar becomes fully
transparent after opening a new tab. This updates the glass effect
view’s top inset during layout so
the glass layer continues to cover the titlebar area.
Root Cause:
The glass effect view’s top constraint is computed once using the theme
frame’s safe-area top inset. On macOS 26, opening a new tab changes the
titlebar height/safe-area,
but the constraint is never refreshed. That leaves an uncovered strip at
the top, which appears fully transparent.
Fix:
Track the glass view’s top constraint and update its constant on layout.
This keeps the glass layer aligned with the current safe-area inset
while avoiding unnecessary
reconfiguration.
Tests:
- Manual: `zig build run`, open a new tab with `background-opacity < 1`
and `background-blur = macos-glass-regular` and confirm titlebar is no
longer fully transparent.
- Note: automated tests not added; UI behavior is hard to exercise in
existing test suite.
Window position question:
Opening a new tab seems to reset the window position / trigger a
maximize-like behavior on my system. Is this intended (feature) or a
bug? I did not change this behavior in this PR.
AI Assistance:
This change was implemented with AI assistance (Codex) and reviewed by
me.