fixes#12873
comment/docs only change:
switched space and tab in default value of `selection-word-chars` so
there is no space at the value boundary
needed because markdown trims spaces at the beginning & end of a code
snippet
fixes#12873
comment/docs only change:
switched space and tab in default value of `selection-word-chars`
so there is no space at the value boundary
needed because markdown trims spaces at the beginning & end
of a code snippet
Fixes#12783 where opening the context menu (with right click) inside
the quick-terminal will hide the quick-terminal if autohide is enabled.
The cause of this issue is the quick-terminal window becoming inactive
and immediately active again when you open the context-menu. When the
window becomes inactive, the autohide feature hides the quick-terminal.
The temporary focus loss in GTK is triggered by GDK focus change events,
which probably originate from the windowing backend treating the context
menu as its own window. Whereas in GTK the context menu is not a
separate window but instead part of the widget tree of the window it was
opened from, so even when the context menu has focus that window is
still the active one in GTK.
As a fix `Window.propIsActive`, which implements the autohide logic,
will now do its work from a timeout callback, since there is probably no
reliable way to distinguish a temporary focus loss from a real one from
inside GTK and I'm not sure we can make any assumptions about the timing
of things happening in the windowing backend. A 100ms delay should be
long enough for the focus state to settle while still hiding the
quick-terminal quickly.
I reproduced the bug and verified the fix on Wayland with both Hyprland
and KDE. Temporary focus loss happens on X11+KDE as well, although it
doesn't matter there because there is no quick-terminal.
### AI Disclosure
No AI was used, code and comments were written by myself.
Add a render-state row-cells getter that encodes the current cell's full
grapheme cluster directly as UTF-8 into a caller-provided GhosttyBuffer.
The getter writes the base codepoint first, followed by any extra
grapheme codepoints, and follows the existing buffer-writer convention
where len is bytes written on success or required capacity on
GHOSTTY_OUT_OF_SPACE.
Previously C consumers could query grapheme codepoints, but bindings
that needed UTF-8 text had to reconstruct and encode the cluster
themselves. That duplicated terminal internals in downstream bindings
and made users pay for awkward cross-language struct handling. By owning
the UTF-8/grapheme behavior in libghostty, bindings can use one stable C
API and optionally wrap it with small binding-local helpers.
Add a render-state row-cells getter that encodes the current cell's
full grapheme cluster directly as UTF-8 into a caller-provided
GhosttyBuffer. The getter writes the base codepoint first, followed by
any extra grapheme codepoints, and follows the existing buffer-writer
convention where len is bytes written on success or required capacity
on GHOSTTY_OUT_OF_SPACE.
Previously C consumers could query grapheme codepoints, but bindings
that needed UTF-8 text had to reconstruct and encode the cluster
themselves. That duplicated terminal internals in downstream bindings
and made users pay for awkward cross-language struct handling. By
owning the UTF-8/grapheme behavior in libghostty, bindings can use one
stable C API and optionally wrap it with small binding-local helpers.
Add a render row-cells data key for querying whether the current cell
has explicit styling. This lets consumers avoid fetching a raw cell or
full style snapshot when all they need is the cell's HasStyling bit.
The new key is appended to the existing enum for ABI safety and is
served by the existing row-cells getter path. Existing data keys and
function exports are unchanged.
This was identified as an allocation hot-spot in Go renderers.
Add a render row-cells data key for querying whether the current cell has
explicit styling. This lets consumers avoid fetching a raw cell or full style
snapshot when all they need is the cell's HasStyling bit.
The new key is appended to the existing enum for ABI safety and is served by
the existing row-cells getter path. Existing data keys and function exports are
unchanged.
Expose whether the terminal viewport is currently pinned to the active
area through the libghostty-vt terminal data API. Previously embedders
could only infer this from scrollbar geometry, which was indirect and
could require the more expensive scrollbar calculation.
The new GHOSTTY_TERMINAL_DATA_VIEWPORT_ACTIVE value returns the exact
PageList viewport state as a bool. The scroll viewport test now verifies
the value while moving between the active area and scrollback.
Expose whether the terminal viewport is currently pinned to the active
area through the libghostty-vt terminal data API. Previously embedders
could only infer this from scrollbar geometry, which was indirect and
could require the more expensive scrollbar calculation.
The new GHOSTTY_TERMINAL_DATA_VIEWPORT_ACTIVE value returns the exact
PageList viewport state as a bool. The scroll viewport test now verifies
the value while moving between the active area and scrollback.
Close#12825
Skip the initial emissions from the focused surface appearance
publishers after a tab focus change. The focused surface is already
synced immediately, so the initial Combine values only repeat the same
titlebar and background updates. Subsequent derived config and OSC
background changes still resync the window appearance.
https://github.com/user-attachments/assets/f229fb95-4b4c-4040-85ac-0acfcc54ca82
Assigned to Codex GPT 5.5(medium)
PS: Sry for I don't write zig and let AI write this.
This change primarily focused on a revised +ssh-cache user interface,
but it also reworks a bunch of the internals.
The primary CLI improvement is support for positional arguments and a
consistent list output format that includes both the ISO-formatted
timestamp and relative age.
ghostty +ssh-cache # List all cached destinations
ghostty +ssh-cache user@example.com # Show that destination
ghostty +ssh-cache example.com # Show all users on that host
ghostty +ssh-cache --add=user@example.com # Manually add a destination
ghostty +ssh-cache --remove=user@example.com # Remove a destination
ghostty +ssh-cache --prune=30d # Remove entries older than 30 days
ghostty +ssh-cache --clear # Clear entire cache
Notable, we now support a --prune operation that replaces the previous
--expire-days flag that was never actually hooked up to anything (!!).
--prune also supports a wider range of Duration-based values.
We're also much more consistent with error codes: 0=success, 1=failure,
2=usage.
While working on those changes, I also reworked the cache internals,
particularly the code around timestamp handling and errors. For example,
I dropped the explicit error sets because they were growing unwieldy,
and in practice we only matched on a subset of those errors.
Lastly, overall test coverage should be much improved, especially around
the time- and allocation-related operations.
---
*AI Disclosure:* I made a lot of iterative, AI-assisted (Claude Opus
4.7) correctness passes over this work. It was particularly helpful in
tracing through the various failure modes, and it wrote those unit tests
in the process.
## Summary
`SurfaceView` caches the background color set by OSC 11 in
`backgroundColor`. `TerminalWindow.preferredBackgroundColor` consults
that cache before falling back to `derivedConfig.backgroundColor`, so
once OSC 11 has fired the cached value masks any later config change.
After a light/dark theme auto-switch (e.g. `theme =
light:my-light,dark:my-dark`) this leaves the window chrome on the
previous theme's color until the application next emits OSC 11.
In `ghosttyConfigDidChange`, after updating `derivedConfig`, drop the
cache when it no longer matches the new config-derived background. A
subsequent `ghosttyColorDidChange` repopulates it as before, so
within-config OSC 11 behavior is unchanged.
## Reproduction
1. Configure `theme = light:SomeLight,dark:SomeDark` where the two
themes have visibly different background colors.
2. Open a terminal session where any application (e.g. a shell startup
script) has sent OSC 11 to set a custom background color.
3. Switch macOS appearance (System Settings → Appearance).
4. **Before**: window chrome stays the previous theme's color until the
terminal next emits OSC 11.
5. **After**: window chrome immediately updates to the new theme's
background color.
## Changes
- `SurfaceView_AppKit.swift` — one guard: if the cached
`backgroundColor` disagrees with the new
`derivedConfig.backgroundColor`, set it to `nil`.
Refactor terminal text selection into a reusable `SelectionGesture`
state machine. Most importantly, this means our click+drag logic around
selection is now fully unit tested! And we found bugs! And fixed them!
The large line increase in this diff is mainly comments + tests.
I've wanted to do this forever so we can unit test this, but I was
kicked in the butt to do it recently because reimplementing selection
logic in libghostty consumers turns out to be complex and error prone
and we have a perfectly battle tested logic machine here so why not
extract it?
Behavioral changes from main surfaced via unit testing:
- Dragging now drags by output across semantic output blocks when the
initial press was an output selection. This matches the behavior of
dragging continuing whatever the initial selection logic was.
- Selection autoscroll now stops when the click anchor is invalidated by
a screen change (e.g. primary to alt)
- Deep press (macOS force touch) now selects the word at the original
press location and consumes the active drag gesture, preventing later
movement from dragging or autoscrolling that selection. This matches
built-in macOS apps.
- Mouse release records whether the gesture moved away from the pressed
cell, so link and prompt clicks are skipped after a drag while normal
clicks still activate them.
Example usage:
```zig
var gesture: terminal.SelectionGesture = .init;
defer gesture.deinit(t);
const press_selection = try gesture.press(t, .{
.time = try std.time.Instant.now(),
.pin = press_pin,
.xpos = mouse_x,
.ypos = mouse_y,
.max_distance = cell_width,
.repeat_interval = mouse_interval,
.word_boundary_codepoints = selection_word_chars,
.behaviors = &.{ .cell, .word, .output },
});
try t.screens.active.select(press_selection);
if (gesture.drag(t, drag_event)) |drag_selection| {
try t.screens.active.select(drag_selection);
}
gesture.release(t, .{ .pin = release_pin });
```
Selection gestures now treat releases with invalidated anchors as dragged,
so a press that crosses screen boundaries cannot also activate links or
prompt clicks on release. Cell drags that create a same-cell selection also
mark the gesture as dragged, which keeps click-only actions from firing
after a threshold-crossing drag.
Autoscroll now resolves the drag pin after moving the viewport instead of
reusing the pin from before the scroll. This keeps the selection aligned
with the row currently under the pointer. The inspector also validates the
tracked click pin before displaying it so stale pins from inactive screens
are ignored.
Close#12825
Skip the initial emissions from the focused surface appearance publishers after a tab focus change. The focused surface is already synced immediately, so the initial Combine values only repeat the same titlebar and background updates. Subsequent derived config and OSC background changes still resync the window appearance.
SurfaceView caches the background color set by OSC 11 in
backgroundColor. TerminalWindow.preferredBackgroundColor consults
that cache before falling back to derivedConfig.backgroundColor,
so once OSC 11 has fired the cached value masks any later config
change. After a light/dark theme auto-switch this leaves the
window chrome on the previous theme's color until the application
next emits OSC 11.
In ghosttyConfigDidChange, after updating derivedConfig, drop the
cache when it no longer matches the new config-derived background.
A subsequent ghosttyColorDidChange repopulates it as before, so
within-config OSC 11 behavior is unchanged.
## Problem
Every audio bell calls `gtk.MediaFile.newForFilename`, which spins up a
full GStreamer pipeline. The GTK4 GStreamer backend's GL sink starts
`gstglcontext`/`gldisplay-event` threads that are **never joined on
teardown**, so allocating a fresh `MediaFile` per ring leaks a pipeline
and ~4 threads on every bell. The old `notify::ended -> unref` handler
discarded the pipeline but did not (and could not) join those threads.
A long-running instance accumulated **705 threads over ~4h** of normal
use.
## Fix
Cache one `MediaFile` per surface (`priv.bell_media`), rebuilt only when
`bell-audio-path` changes and unref'd on `dispose`. Each bell now
replays the same pipeline via `seek(0)` + `play()` instead of creating a
new one. `seek(0)` is required so an ended stream plays again (cf.
#8957).
## Verification
Confirmed on a real running instance with the fix: GStreamer's global
element counter only ever reached `oggdemux4` over an hour of use (one
pipeline per bell-ringing surface, reused for every subsequent bell) and
the process thread count stayed flat — versus the per-bell growth
before.
## Commits
1. **The fix** — reuse one MediaFile per surface.
2. **Unit regression test** — guards the `bellMediaFile` reuse contract
(same path → same object, changed path → rebuild). Runs in the existing
`test-gtk` CI job; needs no display.
3. **End-to-end CI job** *(kept separate so it can be dropped
independently)* — `test/bell-leak.sh` + a `test-gtk-bell-leak` workflow
job that runs ghostty headless (Xvfb + software GL), rings 120 bells,
and fails if the thread count grows per-bell. It's heavier and more
environment-sensitive (needs Xvfb/Mesa/GStreamer on the runner), so it's
isolated for easy review/removal.
🤖 Generated with [Claude Code](https://claude.com/claude-code)