Commit Graph

9316 Commits

Author SHA1 Message Date
Jeffrey C. Ollie
4b7bf0b20e IPC: add +toggle-quick-terminal command (#12661)
Add `+toggle-quick-terminal` as a first-class IPC action, following the
same pattern as `+new-window`. This provides a proper CLI command
(`ghostty +toggle-quick-terminal`) to toggle the quick terminal on a
running Ghostty instance.

Closes discussion #12618
2026-05-17 21:03:09 -05:00
Jeffrey C. Ollie
cfac861794 split_tree: rescale split ratio when resizing (#12699)
Fixes `SplitTree.resize` not rescaling the split ratio to be relative to
the size of the split. Added a unit test for resizing a nested split.

Previously the new ratio was incorrectly calculated relative to the
entire grid. As a consequence resizing a nested split in the GTK app
would cause unexpected size changes like large jumps. E.g. in the
following recording the window has height ~1000px and the resize was
done using a keybind for `resize_split:up,10`. The change is much larger
than 10 pixels.



https://github.com/user-attachments/assets/ba375ddf-5b2f-45e4-8b12-69021ef2f8a8



Note that even with this fix, resizing by a small amount like 10 pixels
might not work at all (depending on window size and layout), because of
the same bug causing #11193 (see my PR #12698). Initially an inaccurate
split ratio will be set and eventually written back to the split tree
datastructure. That incorrect split ratio will be the same before and
after the small resize, so nothing actually changes in the UI.

The split tree implementation for the macOS app already calculates the
ratios correctly.

AI Disclosure
No AI was used, the bug was discovered and all code written by myself.
2026-05-17 20:20:19 -05:00
Jeffrey C. Ollie
4814aee44e gtk: fix invisible splits and focus being lost (#12698)
Fixes #11193 where terminal surfaces might not appear and focus might be
lost when creating multiple nested splits.

These bugs are caused by GTK initially allocating a tiny width/height to
deeply nested splits. For a split with a tiny size, the split ratio will
be set inaccurately e.g. to 1 which means that the right/bottom child of
the split is invisible. If that child is the terminal surface that
should have the focus, it will lose it. In the current implementation
the split ratio can be set at most once, which means the inaccurate
ratio never gets corrected and a surface (or an entire sub-tree of the
layout) will stay invisible.

The following explains the current implementation and bug in more
detail, it is a bit long, but I hope it will make it easier to review
this PR.

### Current Implementation

A split layout is a tree, in code represented by `datastruct/SplitTree`,
where inner nodes are splits and leafs are terminal surface. A split can
be either horizontal or vertical, and has a ratio that defines how its
space should be divided among the 2 children.
The counterpart in the GTK UI is the `apprt/gtk/class/SplitTree` widget
whose `onRebuild/buildTree` functions build a widget tree that has the
same structure as the `datastruct/SplitTree`. The widget tree consists
of a `SplitTreeSplit` widget for every split and a `Surface` widget for
every terminal surface.

A `SplitTreeSplit` widget wraps a `gtk.Paned` widget, which displays its
two children with a divider in between, either horizontally or
vertically. How much space each child gets is determined by 3
properties. `min_position` is always 0 in our case, `max_position`
corresponds to the width/height (for horizontal/vertical splits) of the
widget and `position` is where the divider should be. So `position` is
equivalent to the width/height of the left/top child and thereby also
determines the width/height of the right/bottom child. `SplitTreeSplit`
listens for changes in the 3 properties. If there is one, the
`propPosition`, `propMinPosition` or `propMaxPosition` function gets
triggered and an idle callback for the `onIdle` function is added.

We need to make sure that the widget tree and the `datastruct/SplitTree`
stay in sync.

If we e.g. create a new split or close a surface, the structure of the
split tree changes. In that case `gtk/class/SplitTree.onRebuild` will
completely rebuild the widget tree (the `Surface` widgets are actually
reused) to match the new tree structure. If we resize a split (i.e.
change the split ratio) via action/keybind, we also completely rebuild
the widget tree.

Additionally we need to make sure that for every
`SplitTreeSplit/gtk.Paned` the `position` divided by `max_position`
matches the ratio of the corresponding split node in our
`datastruct/SplitTree`. There are two ways the current implementation
keeps these ratios in sync, both are handled by the
`SplitTreeSplit.onIdle` function.
1. Initially when the widget tree is built, GTK allocates each widget a
size. Specifically it also sets the `position` and `max_position`
properties of each `gtk.Paned` widget, which will trigger the
`SplitTreeSplit.onIdle` function to run. GTK will not necessarily set
position correctly, it is the task of `onIdle` to make sure that the UI
matches the layout defined by the `datastruct/SplitTree`. `onIdle`
checks if `position/max_position` matches the ratio that the split
should have and if not calls `gtk.Paned.setPosition` to update it. This
can only happen once for each split since `onIdle` checks if the
position was set previously. The idea is that we should only ever need
to set the position once, because `gtk.Paned` will automatically keep
its current ratio whenever its size/`max-position` changes (if the
`setPosition` function has been called before). A size change can happen
e.g. because the entire window was resized or because an ancestor split
changed its split ratio.
2. The user can manually change the ratio between the two children of a
split by dragging the divider between them in the UI. When that happens
the `position` property in `gtk.Paned` changes and eventually the
`SplitTreeSplit.onIdle` function gets called. Since `setPosition` should
have already been called when the widget was initially sized, we should
fall through to the second case and write the current ratio back to the
`datastructure/SplitTree`.

The problem with `SplitTreeSplit.onIdle` is that sometimes the split
ratio cannot be set accurately given the current size of the `gtk.Paned`
widget. Because `onIdle` can only set the position/ratio once, any
previous inaccuracy can never get corrected.

For example with many nested vertical splits, GTK might initially
allocate a `gtk.Paned` widget a height of 1. It will have
`max_position=1` and `position=1`. When `onIdle` runs the current ratio
of `position/max_position = 1` is different from the desired ratio of
e.g. 0.5. But a ratio of 0.5 cannot be set, the position can only be 0
or 1 corresponding to a ratio of 0 or 1. The position will then get set
as 1 and can't be changed later. Even when the split later gets a larger
height, it will keep the ratio of 1 and the bottom child will stay
invisible. When the surface that should be focused initially becomes
invisible it loses focus and the focus will never be restored. That is
exactly what happens in the first screencast in the issue description
(#11193).

Another problem with `onIdle` is that the `setPosition` call in `onIdle`
will trigger another idle callback where the position change is
sometimes wrongly interpreted as a manual update and written back to the
tree. Also sometimes the initial ratio in a `gtk.Paned` can already be
correct, in which case position will not get set. The next manual
position update is then not detected as a manual update.

### Changes

`SplitTreeSplit.onIdle` is now able to set the split position every time
the widget is resized, an inaccurate initial ratio will be corrected. To
be able to distinguish a widget resize from a manual position update by
the user, we keep track of whether `max-position`, `position` or both
properties changed. If only `max-position` or both properties changed,
then the widget was resized. If just `position` changed it is a manual
update. This is kind of hacky but works. To verify I checked the source
code for `gtk.Paned`, see the comment in the code on `onIdle`.

`SplitTreeSplit` no longer listens to changes in `min-position`, that
should always be 0 (because we use the default resize/shrink properties
for `gtk.Paned`) and there is already an assert in `onIdle` that checks
that.

It can still happen that a surface initially gets allocated width/height
0 and loses focus. The only reliable way to detect when we can restore
focus, is to listen to the `map/unmap` signals exposed by `gtk.Widget`.
The `Surface` widget now listens to these signals on its `GlArea` child
(because that is where we want to put focus) and stores the current
state in the new `mapped` property. The `SplitTree` widget listens to
changes in that property: when surfaces become mapped, an idle callback
for the new `onRestoreFocus` function is added, which will check whether
the last focused surface is mapped and if so restore focus to it.

### Other possible solutions

Alternatively we could try to only set the split ratio once the split
has its correct final size, but I think it's hard to detect that
reliably. Or we could try to prevent the splits/surfaces from becoming
invisible in the first place by e.g. setting a minimal widget size. But
then you won't get the exactly correct layout and sometimes you do want
a surface to be tiny or invisible e.g. you can drag the divider in a
split all the way to one side.

The ideal solution would probably be to write a custom version of
`gtk.Paned` where you can provide the desired ratio on widget creation.
Then everything will be sized correctly from the start, focus will not
be lost. In terms of performance it would probably be better as well,
because right now there can be multiple rounds of resizes until every
split/surface has its correct size. I have played around with this a
bit, it is definitely possible. But you would have to implement the
divider widget, logic for layouting, handling gestures and co. That is a
bigger undertaking.

### Testing

Tested by creating/modifying deeply nested layouts, dragging split
dividers and resizing splits via keybind. Checked that ratios are
maintained when the window is resized and tested that it works with
zoom. Tested locally with hyprland and in a VM with KDE.

All the bugs described in the issue should be fixed.

### AI Disclosure

Discovered the bug and wrote all code/comments by myself. Used AI in
researching various internals of GTK.
2026-05-17 20:20:01 -05:00
Jeffrey C. Ollie
22b9df25e6 Fix "Available since"
Co-authored-by: Leah Amelia Chen <github@acc.pluie.me>
2026-05-17 14:42:27 -05:00
Daniel Kinzler
9f72eb9d7c added back accidentally deleted empty line 2026-05-15 17:52:48 +02:00
Daniel Kinzler
93d1142ada small formatting changes 2026-05-15 17:20:57 +02:00
Lukas
13ca032b1d config: clear command-palette-entry like keybind
After #1368, `command-palette-entry=` will no longer clear the entries like the documentation says. Since i couldn't find an existing issue or discussion about this, I assume no one is actually using it. So I put 1.4.0 here, lemme know if you want to change it to 1.3.2.

> I basically copied the `keybind` parsing code and doc.
2026-05-14 19:43:08 +02:00
Daniel Kinzler
54a38e8134 Distinguish resize and manual update using a combination of
max-position and position properties. Listening to drag events directly
did not work that well.
2026-05-14 15:45:29 +02:00
Daniel Kinzler
e59e27f8bd Fix nested splits disappearing and focus being lost.
The cause of these bugs is that GTK can initially allocate
a split/surface a width/height of 0 which causes it to
get unmapped and lose focus. Additionally the split ratio is
only set once but not accurately for tiny splits, which can keep
a surface invisible even when the split gets resized later.

To fix these problems the split ratio is always checked and
possibly corrected when a split gets resized. Changes in a split
ratio caused by the user dragging the divider are detected
separately using an event controller. If a surface loses focus
we restore it once the surface becomes mapped again.
2026-05-12 14:13:35 +02:00
Hua Jiang
81af65766f feat: add +toggle-quick-terminal IPC command
Expose toggle-quick-terminal as a proper IPC action so it can be
triggered via 'ghostty +toggle-quick-terminal' from the command line,
instead of calling the raw D-Bus org.gtk.Actions.Activate interface.

This follows the same pattern as the existing +new-window IPC command:

  - Add toggle_quick_terminal to apprt.ipc.Action enum (Zig + C ABI)
  - Create apprt/gtk/ipc/toggle_quick_terminal.zig (GTK D-Bus handler)
  - Route .toggle_quick_terminal in apprt/gtk/App.zig performIpc
  - Register toggle-quick-terminal GAction in application.zig
  - Add +toggle-quick-terminal CLI handler in cli/
  - Register in cli/ghostty.zig Action enum, runMain, and options
  - Add stub in apprt/embedded.zig
  - Update include/ghostty.h C header enum

Usage:
  ghostty +toggle-quick-terminal

Closes: #12618
2026-05-12 09:03:14 +08:00
kat
a330ee93e8 i18n: add Basque (eu) translation (#12544)
Same as with icelandic (#12301) we may be even fewer than them but let's
have this translated into Basque.

I also volunteer for the basque translation team.
2026-05-09 16:12:57 +00:00
Daniel Kinzler
57a9adce71 fix datastruct/SplitTree not calculating the correct new split ratio when resizing a split 2026-05-05 18:00:04 +02:00
kat
f9a9d33b3a docs(input): add documentation for missing action parameters (#12579)
### AI Disclosure

Claude generated all the commits, I reviewed it and created this PR
2026-05-05 08:59:39 +00:00
Lukas
5874ce633c Apply suggestions from code review
Co-authored-by: kat <65649991+00-kat@users.noreply.github.com>
Co-authored-by: Lukas <134181853+bo2themax@users.noreply.github.com>
2026-05-05 09:47:06 +02:00
Riccardo Mazzarini
c20fcfa1b4 Fix zero-width grapheme attachment during pending wrap
This PR fixes an issue where a zero-width combining mark could attach to
the wrong cell when the preceding character was written in the final
column and the cursor had a pending wrap.
2026-05-04 23:16:41 +02:00
Claude Opus 4.7
df44c6dd83 docs(input): document close_tab arguments 2026-05-04 18:39:02 +02:00
Claude Opus 4.7
b1b01741f6 docs(input): document navigate_search arguments 2026-05-04 16:57:56 +02:00
Claude Opus 4.7
248df8e7aa docs(input): document copy_to_clipboard arguments 2026-05-04 16:57:37 +02:00
Mikel Larreategi
cbd43fd483 feature: add basque translation 2026-05-01 13:34:23 +02:00
Aditya Bhargava
5871a2d4f0 zig-fmt cleanup 2026-04-27 16:28:23 -04:00
Aditya Bhargava
154169b054 Fix speedy high-resolution scrolling on Linux
Enforcing an absolute minimum of 1 for scroll events causes differing
scroll speeds between high-resolution and standard scroll wheels on
Linux. Since this was added to handle MacOS's precision scrolling
emulation, this patch alters the behaviour so that the absolute minimum
is only enforced on MacOS.

Fixes #11648.
2026-04-27 16:28:23 -04:00
Mitchell Hashimoto
34cbb5fa81 feat: add middle-click action configuration (#12478)
This PR addresses
https://github.com/ghostty-org/ghostty/discussions/12108 implemented
similarly to https://github.com/ghostty-org/ghostty/pull/8254 to allow
middle click + TrackPoint scrolling on MacOS. `primary-paste` naming
comes from `gtk_enable_primary_paste`.

The following configuration values for `middle-click-action` are
provided:
- `primary-paste` - Paste from the selection (or system) clipboard per
`copy-on-select`.
- `ignore` - Do nothing, ignore the middle click.

Tested locally on macOS with Zig 0.15.2 using `zig build
-Doptimize=ReleaseFast`.

Thank you!
2026-04-27 09:29:29 -07:00
Mitchell Hashimoto
0b56ae2cc7 gtk: fix quick terminal breaking when manually toggled off while auto-hide is enabled (#12471)
Fixes quick terminal breaking when auto-hide is enabled and quick
terminal is manually toggled off (#11679).

`quick-terminal-autohide` is implemented by the `Window.propIsActive`
function in `apprt/gtk/class/window.zig` which calls
`Window.toggleVisibility` when the quick terminal window becomes
inactive (loses focus). However `Window.propIsActive` is also triggered
when you manually hide the quick terminal because hiding it causes the
window to become inactive. Normally that should just toggle the quick
terminal off and immediately back on, but there is also a re-entrancy
issue. Manually toggling off the terminal causes the
`Application.toggleQuickTerminal` (in `apprt/gtk/class/application.zig`)
to run which sets off the call chain `Window.toggleVisibility ->
gtk_widget_set_visible -> ... GTK signal/event handling ... ->
Window.propIsActive -> Window.toggleVisibility ->
gtk_widget_set_visible`.
The nested calls to `gtk_widget_set_visible` cause the GTK window state
to become corrupted. The window is marked visible, but is not actually
visible or just shows a placeholder. What exactly happens depends on the
compositor and how it handles moving window focus.

Reproduced the bug on KDE and hyprland and verified the fix on both.

### Changes

`apprt/gtk/class/window.zig`: added check to `Window.propIsActive` to
only toggle quick-terminal if it is inactive **and** visible.

### AI Disclosure

Found the bug without AI using "printf debugging" then traced it through
GTK with valgrind. Used GPT5.4 in setting up valgrind and researching
how signals/events move through GTK internally.
2026-04-27 09:26:24 -07:00
Mitchell Hashimoto
1ed22a5210 renderer: fix preedit range width (#12479)
Related to #12466

`Preedit.range()` returns an inclusive range, but the end position was
calculated as `start + w`. For wide preedit text, this covers one extra
cell.

In Debug builds, Korean IME composition between existing Hangul
characters can panic with:
`index out of bounds: index 2, len 2`

I reproduced this reliably when there are two Hangul characters to the
right of the cursor. For example, type `가나다`, move the cursor between
`가` and `나`, then start a new Korean IME composition. With the old range
calculation, the renderer skips the first wide character plus the head
cell of the next wide character, then resumes on that character's spacer
tail.

This changes the inclusive end to `start + (w - 1)` and adds focused
tests for narrow, wide, and right-edge preedit ranges.

This does not fully fix the visual behavior reported in #12466. The
adjacent character can still disappear during composition, so this PR
only fixes the crash side of the problem.
2026-04-27 09:24:55 -07:00
Mitchell Hashimoto
c19ce03b3e fix: update Se terminfo entry to reset cursor to configured default (#12487)
## Problem

Current `Se` sequence (reset cursor style) is `\E[2 q`, which always
sets steady block, regardless of user config.

## Solution

Update sequence to `\E[0 q`, which sets the cursor style to the user
configured default cursor.

fix https://github.com/ghostty-org/ghostty/issues/12482
Helps with neovim issue: https://github.com/neovim/neovim/issues/38987

## AI Disclosure

I didn't use AI for this, haha. Unless you count random questions to
learn about terminfo beforehand, but I relied on [legit
resources](https://invisible-island.net/xterm/terminfo.html) for real
info. It says:

>  Se resets the cursor style to the terminal power-on default.

I think the useful interpretation is to set the user configured default.
2026-04-27 09:23:51 -07:00
Lukas
971753074b input: remove translated in capi
Follow up for a3462dd2bd
2026-04-27 13:39:51 +02:00
Kyle Sower
6c68650920 fix: update Se terminfo entry to reset cursor to configured default 2026-04-26 22:31:01 -05:00
dobbylee
ac67a6160c renderer: fix preedit range width 2026-04-27 01:17:43 +09:00
Andrei Lebedev
12ac19939c feat: add middle-click action configuration 2026-04-27 01:27:22 +10:00
Mitchell Hashimoto
c74f6d56d1 os: use GetTempPathW for allocTmpDir on Windows (#12469)
`allocTmpDir` previously read `%TMP%` via `getenvW` and returned `null`
if the variable wasn't set, requiring each caller to to deal with the
nullable. Unfortunately, there isn't a platform-neutral default value
that makes sense for those cases (i.e. `/tmp` is POSIX-y).

We now use `GetTempPathW` on Windows, which is the official way to get
this directory: `TMP` → `TEMP` → `USERPROFILE` → `GetWindowsDirectoryW`.

With a real system call behind it, the function no longer needs to be
nullable: the only remaining failure modes are OOM (propagated) and the
syscall itself failing or returning data we can't decode. In those later
cases, we use `C:\Windows\Temp` as a fallback, similar to how we use
`/tmp` in the POSIX case.

The Windows path always allocates so it still must be paired with
`freeTmpDir`, which matches the existing contract.

---

*AI Disclosure:* I verified the Windows path using Claude and Zig's
cross-compilation capabilities because I don't have a Windows
environment in which to test this. I do fully understand the code based
on my prior life as a Windows game developer though.
2026-04-25 20:49:54 -07:00
Mitchell Hashimoto
278041c4bc flatpak: terminate session if Ghostty disconnects from bus (#12427)
This makes sure that if Ghostty crashes, commands spawned are also
terminated automatically by the Flatpak Session Helper.

The few crashes I got left a lot of background processes, some of them
pretty heavy and took awhile to be figured out.
2026-04-25 20:49:33 -07:00
Jon Parise
8b90efd913 os: use GetTempPathW for allocTmpDir on Windows
`allocTmpDir` previously read `%TMP%` via `getenvW` and returned `null`
if the variable wasn't set, requiring each caller to to deal with the
nullable. Unfortunately, there isn't a platform-neutral default value
that makes sense for those cases (i.e. `/tmp` is POSIX-y).

We now use `GetTempPathW` on Windows, which is the official way to get
this directory: `TMP` → `TEMP` → `USERPROFILE` → `GetWindowsDirectoryW`.

With a real system call behind it, the function no longer needs to be
nullable: the only remaining failure modes are OOM (propagated) and the
syscall itself failing or returning data we can't decode. In those later
cases, we use `C:\Windows\Temp` as a fallback, similar to how we use
`/tmp` in the POSIX case.

The Windows path always allocates so it still must be paired with
`freeTmpDir`, which matches the existing contract.
2026-04-25 21:44:37 -04:00
Leorize
8b8f7136d0 flatpak: don't assume c_uint to be u32
Thanks pluie for the pointer.
2026-04-25 17:04:26 -07:00
Jon Parise
13ada38ac4 os: RANDOM_BASENAME_LEN -> random_basename_len 2026-04-25 16:29:25 -04:00
Mitchell Hashimoto
e9ca0f8c9a core: Acquire renderer state mutex before calling processLinks (#12463)
Holding the renderer state mutex is a documented precondition of
`processLinks`, but `mouseButtonCallback` previously called the function
without the mutex.

This creates a race with the I/O thread's `processOutput`, which can
prune scrollback pages while `processLinks` is reading them, resulting
in a use-after-free segfault. See
https://github.com/ghostty-org/ghostty/discussions/12409 (Linux: crash
while selecting text).


57b5e1e250/src/Surface.zig (L4354-L4355)


57b5e1e250/src/Surface.zig (L3822-L3824)

995e4e375 (os: open) changed the body of `processLinks` to be
non-trivial and documented the precondition, but the lock was not held
at the call site.
2026-04-25 13:21:38 -07:00
Mitchell Hashimoto
8e1dfbcf3e os: add randomTmpPath for allocating temp paths (#12465)
Factor TempDir's name generation into a reusable `randomBasename` (16
random bytes, url-safe base64) and add `randomTmpPath` on top, which
composes `allocTmpDir` + `randomBasename` into a single allocated path
in the form `{TMPDIR}/{prefix}{random}` (`mktemp(1)`-ish).

This is convenient for callers who want a unique path under TMPDIR (for
a temporary file, socket, etc.) without having to think about basename
buffer sizing or path joining.

Also, use `std.base64.url_safe_no_pad.Encoder` instead of the custom
base64 alphabet, which is exactly equivalent.
2026-04-25 13:14:30 -07:00
Jon Parise
c9d2285f63 os: add randomTmpPath for allocating temp paths
Factor TempDir's name generation into a reusable `randomBasename` (16
random bytes, url-safe base64) and add `randomTmpPath` on top, which
composes `allocTmpDir` + `randomBasename` into a single allocated path
in the form `{TMPDIR}/{prefix}{random}` (mktemp(1)-ish).

This is convenient for callers who want a unique path under TMPDIR (for
a temporary file, socket, etc.) without having to think about basename
buffer sizing or path joining.

Also, use `std.base64.url_safe_no_pad.Encoder` instead of the custom
base64 alphabet, which is exactly equivalent.
2026-04-25 15:52:40 -04:00
Mitchell Hashimoto
b613ffcfd8 surface: respect semantic prompt boundaries for links (#12435)
Link detection currently expands the clicked location to a full line
before running the configured regexes. When semantic prompt markers are
present, this can cause prompt text and neighboring content to be
matched together even though they are distinct semantic regions.

Use semantic prompt boundaries when selecting the text to inspect for
link matching. This keeps prompt text separate from the content beside
it and avoids folding prompt text into double-click link/path selection.

Add a regression test that models a prompt and command on the same line
and verifies the prompt region and input region remain separate.

----

this is a fix for the issue users reported in
https://github.com/ghostty-org/ghostty/discussions/11415

**disclaimer**: I used codex addon within Cursor to research this
bug/regression and find a proper fix for it.
2026-04-25 10:32:15 -07:00
Vasyl Zuziak
c4a671ba5a fix: remove test as has been suggested in comment 2026-04-25 19:26:22 +02:00
Enzo William
14c06312d5 feat: add default GTK keybinds for move_tab
Reassign jump_to_prompt from Ctrl+Shift+PageUp/PageDown to
Ctrl+Shift+Arrow Up/Down on GTK, freeing the idiomatic Linux
keybinds (Ctrl+Shift+PageUp/PageDown) for move_tab.

This matches the tab-moving keybinds used by Firefox, GNOME Terminal,
and VSCode. The new jump_to_prompt binding mirrors the macOS pattern
(Cmd+Shift+Arrow Up/Down).

Closes #4998

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-25 13:53:37 -03:00
Mitchell Hashimoto
69452b5c6f Sync middle-click paste with copy-on-select (#12443)
Implements the behavior from #9788.

Today, middle-click paste always reads from the selection clipboard (or
the
system clipboard on platforms without a selection clipboard). With this
change
it follows `copy-on-select`:

- `true`: selection clipboard (unchanged)
- `clipboard`: system clipboard
- `false`: selection clipboard (also unchanged, preserves traditional
X11
  middle-click behavior)

The idea is symmetry: if `copy-on-select = clipboard` writes selections
to the
system clipboard, middle-click should come back from there too.

Also updated the config doc comment, which previously claimed
middle-click
"will always use the selection clipboard".

### Testing

`zig build test` passes locally (macOS, Zig 0.15.2).

Built and runtime-tested via the fork's CI:
https://github.com/007hacky007/ghostty/actions/runs/24707475544 - I'm
running the resulting binary daily and the three `copy-on-select` modes
behave as described above.
2026-04-25 09:39:12 -07:00
Mitchell Hashimoto
8e2a13cb60 gtk/SurfaceScrolledWindow: wrap root child with another Adw.Bin (#12426)
Due to a known Gtk issue, the scrolled_window at the root of the
template is free-ed twice on dispose. This causes crashes when used with
GNOME 49 platform (Gtk 4.20, libadwaita 1.8.5).

Workaround this issue by wrapping the root child in another Adw.Bin,
similar to widgets like ResizeOverlay.

LLM was used to perform discovery against a manually recorded Valgrind
trace, and helped tracking down known fixes for this problem. The
comment in code was taken from another instance in the repository.

Fixes https://github.com/ghostty-org/ghostty/discussions/12306

Assisted-by: OpenAI GPT-5.4
2026-04-25 09:22:59 -07:00
Leorize
560b7ba8e8 gtk/SurfaceScrolledWindow: wrap root child with another Adw.Bin
Due to a known Gtk issue, the scrolled_window at the root of the
template is free-ed twice on dispose. This causes crashes when used with
GNOME 49 platform (Gtk 4.20, libadwaita 1.8.5).

Workaround this issue by wrapping the root child in another Adw.Bin,
similar to widgets like ResizeOverlay.

LLM was used to perform discovery against a manually recorded Valgrind
trace, and helped tracking down known fixes for this problem.

Fixes https://github.com/ghostty-org/ghostty/discussions/12306

Assisted-by: OpenAI GPT-5.4
2026-04-25 09:18:21 -07:00
Jesse Rosenstock
28f4676b5d core: Acquire renderer state mutex before calling processLinks
Holding the renderer state mutex is a documented precondition of
`processLinks`, but `mouseButtonCallback` previously called
the function without the mutex.

This creates a race with the I/O thread's `processOutput`, which can
prune scrollback pages while `processLinks` is reading them, resulting
in a use-after-free segfault.  See
https://github.com/ghostty-org/ghostty/discussions/12409 (Linux: crash
while selecting text).

57b5e1e250/src/Surface.zig (L4354-L4355)

57b5e1e250/src/Surface.zig (L3822-L3824)

995e4e375 (os: open) changed the body of `processLinks` to be
non-trivial and documented the precondition, but the lock was not held
at the call site.
2026-04-25 16:36:45 +02:00
Vasyl Zuziak
85dc4b1842 surface: respect semantic prompt boundaries for links
Link detection currently expands the clicked location to a full line
before running the configured regexes. When semantic prompt markers
are present, this can cause prompt text and neighboring content to be
matched together even though they are distinct semantic regions.

Use semantic prompt boundaries when selecting the text to inspect for
link matching. This keeps prompt text separate from the content beside
it and avoids folding prompt text into double-click link/path
selection.

Add a regression test that models a prompt and command on the same
line and verifies the prompt region and input region remain separate.
2026-04-25 15:46:25 +02:00
Mitchell Hashimoto
4ceeba4851 config: use Config to check key binding instead of App (#12415)
Previously `ghostty_app_key_is_binding` (unlike Surface) is just using
`config.keybind` to check whether a KeyEvent is in the set or not.

After this, I can add unit tests for keybinding more easily with dummy
configs.

I didn't find any usages of this in GTK, so it shouldn't affect
anything. ci will see if this is the case:)
2026-04-24 14:51:22 -07:00
0xDVC
44a2d8740a build: gate lib-vt xcframework on emit-xcframework with xcodebuild detection 2026-04-24 13:21:43 -07:00
Mitchell Hashimoto
6b69ea0517 libghostty: enable cross-compiling macOS from Linux/Windows
This allows libghostty-vt to be cross-compiled for macOS from non-macOS
platforms. I've updated pkg/apple-sdk to fallback to Zig's embedded
macOS headers if the macOS SDK is not found.

Additionally, CombineArchivesStep has been updated to use Linux
tooling on Linux.
2026-04-24 13:04:38 -07:00
Lukas
7c91cef28d config: use Config to check key binding instead of App
Previously `ghostty_app_key_is_binding` (unlike Surface) is just using `config.keybind` to check whether a KeyEvent is in the set or not.

After this, I can add unit tests for keybinding more easily, with dummy configs.
2026-04-24 18:26:56 +02:00
Mitchell Hashimoto
bf3047b9b2 benchmark: isolate parser hot loop from code-layout shifts
Extract the tight per-byte parsing loop from TerminalParser.step into
a separate noinline function (parseAll). This eliminates a ~20%
benchmark regression that appeared after the highway vendor changes
despite zero changes to the parser source code.

The root cause: the parser benchmark processes 50 MB of input through
a byte-at-a-time DFA loop that is highly sensitive to instruction
cache-line placement on Apple Silicon. The M-series cores fetch
aligned 16-byte blocks; when the loop head lands near the end of a
64-byte cache line (offset 60), only one instruction fits in the
first fetch versus four when aligned to offset 48. This causes ~29%
more cycles for identical instruction counts.

Previously the loop was inlined into the large step() function, so
any code change anywhere in the binary (like the highway vendor
restructuring) could shift the loop across a cache-line boundary.
By making parseAll noinline, the loop gets its own function placement
that is stable regardless of surrounding code changes.
2026-04-23 21:36:39 -07:00