Follow up to #8289
The rise of agentic programming has eliminated the natural effort-based
backpressure that previously limited low-effort contributions. It is now
too easy to create large amounts of bad content with minimal effort.
Open source projects have always had poor quality issues, PRs, etc. That
comes with the territory. Unfortunately, the ease and carelessness by
which these are now manifested has increased the "bad" count by 10x if
not more. It's ruining it for the rest of us. This policy is a result of
the bad, and I'm sorry about it.
**Going forward, AI generated contributions will only be allowed for
accepted issues and maintainers.** Drive-by pull requests with AI
generated content will be immediately closed.
**Going further, users who contribute bad AI generated content will be
immediately banned from all future contributions.** This is a
zero-tolerance policy. If you use AI, you are responsible for the
quality of your contributions. If you're using low-effort AI to create
low-effort content, I have no human obligation to help you.
If you are a junior developer who is really trying to learn and get
better, then please put aside the AI, do your best, and I will still
help. I want to help. But I expect effort and organic thinking in
return.
> [!IMPORTANT]
>
> This is not an anti-AI stance. This is an anti-idiot stance. Ghostty
is written with plenty of AI assistance and many of our maintainers use
AI daily. We just want quality contributions, regardless of how they are
made.
Follow up to #8289
The rise of agentic programming has eliminated the natural effort-based
backpressure that previously limited low-effort contributions. It is
now too easy to create large amounts of bad content with minimal effort.
Open source projects have always had poor quality issues, PRs, etc. That
comes with the territory. Unfortunately, the ease and carelessness by which
these are now manifested has increased the "bad" count by 10x if not more.
It's ruining it for the rest of us. This policy is a result of the bad, and
I'm sorry about it.
**Going forward, AI generated contributions will only be allowed for
accepted issues and maintainers.** Drive-by pull requests with AI generated
content will be immediately closed.
**Going further, users who contribute bad AI generated content will be
immediately banned from all future contributions.** This is a zero-tolerance
policy. If you use AI, you are responsible for the quality of your
contributions. If you're using low-effort AI to create low-effort content,
I have no human obligation to help you.
If you are a junior developer who is really trying to learn and get
better, then please put aside the AI, do your best, and I will still
help. I want to help. But I expect effort and organic thinking in return.
This is not an anti-AI stance. This is an anti-idiot stance. Ghostty is
written with plenty of AI assistance and many of our maintainers use
AI daily. We just want quality contributions, regardless of how they are
made.
Refer to discussion #10000
When a tab contains only a single split, resize_split and
toggle_split_zoom actions now return false (not performed). This allows
keybindings marked with `performable: true` to pass the event through to
the terminal program.
The performable flag causes unperformed actions to be treated as if the
binding didn't exist, so the key event is sent to the terminal instead
of being consumed.
- Add isSplit() helper to SplitTree to detect single-pane vs split state
- Update GTK resizeSplit/toggleSplitZoom to return false when single
pane
- Update macOS resizeSplit/toggleSplitZoom to return Bool and check
isSplit
- Add unit test for isSplit method
Currently, if a user clicks the "xmark" button to close the search box,
the main interface does not regain focus until clicked again. This patch
focuses the surface view upon closing.
This adds a new single-file library called "Tripwire" in
`src/tripwire.zig`. This library helps inject failures around `try`
cases for the purpose of testing `errdefer`. It is fully optimized away
in non-test builds (even debug), turning into zero space and zero
assembly.
From this, I've verified (via unit tests w/ tripwire) and fixed a number
of errdefer issues:
* PageList init with non-standard pages that requires more than 1 page
can leak on allocation error on the 2nd+ loop
* Tabstop allocation failure on resize corrupts the internal state
(invalid cols)
* `Screen.selectionString` would leak memory on late allocation failures
* Screen search could leak memory on late allocation failures
* `SharedGrid.renderGlyph` in our font subsystem would corrupt the glyph
cache if failure occurred
* `SharedGrid.init` could leak memory if loading font metrics failed
In addition to the bugs found, there is now tripwire coverage around
more of our core and we should continue to add more. I've also added
significantly more explicit error sets as I found them.
**AI disclosure:** AI wrote some of the tests, but tripwire itself is
all handwritten and everything was reviewed.
Add errdefer cleanup for codepoints and glyphs hash maps in init().
Previously, if ensureTotalCapacity or reloadMetrics() failed after
allocating these maps, they would leak.
Add tripwire test to verify all failure points in init().
Add errdefer to remove cache entry after getOrPut if subsequent
operations fail (getPresentation, atlas.grow, renderGlyph). Without
this, failed renders would leave uninitialized/garbage entries in
the glyph cache, potentially causing crashes or incorrect rendering.
Add tripwire test to verify the rollback behavior.
- Don't expose package attributes that won't build (e.g. on macOS)
- Make the flake generally easier to read since there's none of that
`builtin.foldl' recursiveUpdate` nonsense anymore
- Don't expose package attributes that won't build (e.g. on macOS)
- Make the flake generally easier to read since there's none of that
`builtin.foldl' recursiveUpdate` nonsense anymore
See individual commits. An overview:
* Added explicit error sets in more places
* Removed `!` from functions that can't ever fail
* `renderer.cell.Contents.resize` `errdefer` now does proper cleanup
* `renderer.rebuildCells` can now only fail due to allocator OOM
* If shaping fails during rendering, that row is skipped (previously
it'd halt rendering there)
* Failed image rendering setup in the renderer now skips that image
(previously would halt the full frame)
* GPU texture cleanup for failed image setup now works properly
#9417
Adds palette and color scheme uniforms to custom shaders, allowing
custom shaders to access terminal color information:
- iPalette[256]: Full 256-color terminal palette (RGB)
- iBackgroundColor, iForegroundColor: Terminal colors (RGB)
- iCursorColor, iCursorText: Cursor colors (RGB)
- iSelectionBackgroundColor, iSelectionForegroundColor: Selection colors
(RGB)
Colors are normalized to [0.0, 1.0] range and update when the palette
changes via OSC sequences or configuration changes. The palette_dirty
flag tracks when colors need to be refreshed, initialized to true to
ensure correct colors on new surfaces.
Adds palette and color scheme uniforms to custom shaders, allowing
custom shaders to access terminal color information:
- iPalette[256]: Full 256-color terminal palette (RGB)
- iBackgroundColor, iForegroundColor: Terminal colors (RGB)
- iCursorColor, iCursorText: Cursor colors (RGB)
- iSelectionBackgroundColor, iSelectionForegroundColor: Selection
colors (RGB)
Colors are normalized to [0.0, 1.0] range and update when the palette
changes via OSC sequences or configuration changes. The palette_dirty
flag tracks when colors need to be refreshed, initialized to true to
ensure correct colors on new surfaces.
Dvorak input (and presumably others) on MacOS causes certain keys to not
work as expected: `[` `]` and `=`.
### Related
This fixes https://github.com/ghostty-org/ghostty/discussions/8743 as
well as an unmentioned problem where bracket navigation and equalize
panes are also broken.
This is similar to https://github.com/ghostty-org/ghostty/pull/8759 but
fixes more of the combos. Switching the `+` binding was not enough to
fix the problem for me since the right bracket physical keys is where
equals should be and overrides the combo.
### What this PR does
Switches several default keybindings from physical key codes to support
alternative keyboard layouts like Dvorak and keyboards with dedicated
plus keys. Effectively:
```diff
- .physical = .equal // or .bracket_left or .bracket_right
+ .unicode = '=' // or '[' or ']'
```
### Details
In testing, I found that all of these bindings need to be fixed
otherwise the bracket physical keys overshadows the dvorak plus key.
This seems like the right solution for the same reason that we don't use
any physical letter or number keys. They move around with different
layouts and `=`, `[`, and `]` are no different than other keys like `-`
and `0` which use unicode in other default keybinds.
With this fix, tab and pane navigation (cmd+[], cmd+shift+[]), as well
as increase font size (cmd+shift+equals and cmd+equals) and equalize
panes (ctrl+cmd+=) now work as expected on dvoark layout on MacOS.
Note, I switch between dvorak virtual layout on the laptop and a
physical dvorak keyboard (passed through qwerty input) so my combos
would need to change depending on which keyboard I was using if we used
physical keys only.
I consulted Claude Code to help try to understand what order and
precedence was being applied in this change, but I wrote and tested the
code myself (however, this is my first `zig` code so take that with a
grain of salt).
Switches several default keybindings from physical key codes
`.physical = .equal // or .bracket_left or .bracket_right`
to unicode characters
`.unicode = '=' // or '[' or ']'`
to support alternative keyboard layouts like Dvorak and
keyboards with dedicated plus keys (like German layouts).
I found in testing that all of these must be fixed at once otherwise
the bracket physical keys overshadew the correct (for dvorak) plus key.
With this fix, tab and pane navigation (cmd+[], cmd+shift+[]), as well
as cmd+shift+equals and cmd+equals work as expected on dvoark layout on MacOS.
# Add GSettings Support for Primary Paste
Implements support for `org.gnome.desktop.interface
gtk-enable-primary-paste` to allow users to disable middle-click paste.
Also refactors GTK Settings access into a reusable generic module.
## Changes
- **NEW**: `src/apprt/gtk/gsettings.zig` - Generic GTK Settings reader
supporting `bool` and `c_int` types, portal-aware for Flatpak/Snap
- **MODIFIED**: `src/apprt/gtk/class/surface.zig` - Reads primary paste
setting and refactors gtk-xft-dpi to use new module
## Behavior
- Setting `false` → Middle-click paste blocked
- Setting `true` or unavailable → Middle-click paste works (default)
- Uses GTK Settings API which automatically uses XDG Desktop Portal in
sandboxed environments
Note: No unit tests added as this is a thin wrapper around GTK Settings
API that's already tested indirectly through surface.zig. Happy to add
tests if desired, though they would require an active display
environment and skip on most CI systems.
The reporting of color scheme was handled asynchronously by queuing a
handler in the surface. This could lead to race conditions where the DSR
is reported after subsequent VT sequences.
Fixes#5922
This PR builds on https://github.com/ghostty-org/ghostty/pull/9678 ~so
the diff from there is included here (it's not possible to stack PRs
unless it's a PR against my own fork)--review that one first!~
This PR updates the `graphemeBreak` calculation to use `uucode`'s
`computeGraphemeBreakNoControl`, which has [tests in
uucode](215ff09730/src/x/grapheme.zig (L753))
that confirm it passes the `GraphemeBreakTest.txt` (minus some
exceptions).
Note that the `grapheme_break` (and `grapheme_break_no_control`)
property in `uucode` incorporates `emoji_modifier` and
`emoji_modifier_base`, diverging from UAX #29 but matching UTS #51. See
[this comment in
uucode](215ff09730/src/grapheme.zig (L420-L434))
for details.
The `grapheme_break_no_control` property and
`computeGraphemeBreakNoControl` both assume `control`, `cr`, and `lf`
have been filtered out, matching the current grapheme break logic in
Ghostty.
This PR keeps the `Precompute.data` logic mostly equivalent, since the
`uucode` `precomputedGraphemeBreak` lacks benchmarks in the `uucode`
repository (it was benchmarked in [the original PR adding `uucode` to
Ghostty](https://github.com/ghostty-org/ghostty/pull/8757)). Note
however, that due to `grapheme_break` being one bit larger than
`grapheme_boundary_class` and the new `BreakState` also being one bit
larger, the state jumps up by a factor of 8 (u10 -> u13), to 8KB.
## Benchmarks
~I benchmarked the old `main` version versus this PR for
`+grapheme-break` and surprisingly this PR is 2% faster (?). Looking at
the assembly though, I'm thinking something else might be causing that.
Once I get to the bottom of that I'll remove the below TODO and include
the benchmark results here.~
When seeing the speedup with `data.txt` and maybe a tiny speedup on
English wiki, I was surprised given the 1KB -> 8KB tables. Here's what
AI said when I asked it to inspect the assembly:
https://ampcode.com/threads/T-979b1743-19e7-47c9-8074-9778b4b2a61e, and
here's what it said when I asked it to predict the faster version:
https://ampcode.com/threads/T-3291dcd3-7a21-4d24-a192-7b3f6e18cd31
It looks like two loads got reordered and that put the load that
depended on stage1 -> stage2 -> stage3 second, "hiding memory latency".
So that makes the new one faster when looking up the `grapheme_break`
property. These gains go away with the Japanese and Arabic benchmarks,
which spend more time processing utf8, and may even have more grapheme
clusters too.
### with data.txt (200 MB ghostty-gen random utf8)
<img width="1822" height="464" alt="CleanShot 2025-11-26 at 08 42 03@2x"
src="https://github.com/user-attachments/assets/56d4ee98-21db-4eab-93ab-a0463a653883"
/>
### with English wiki dump
<img width="2012" height="506" alt="CleanShot 2025-11-26 at 08 43 15@2x"
src="https://github.com/user-attachments/assets/230fbfb7-272d-4a2a-93e7-7268962a9814"
/>
### with Japanese wiki dump
<img width="2008" height="518" alt="CleanShot 2025-11-26 at 08 43 49@2x"
src="https://github.com/user-attachments/assets/edb408c8-a604-4a8f-bd5b-80f19e3d65ee"
/>
### with Arabic wiki dump
<img width="2010" height="512" alt="CleanShot 2025-11-26 at 08 44 25@2x"
src="https://github.com/user-attachments/assets/81a29ac8-0586-4e82-8276-d7fa90c31c90"
/>
TODO:
* [x] Take a closer look at the assembly and understand why this PR (8
KB vs 1 KB table) is faster on my machine.
* [x] _(**edit**: checking this off because it seems unnecessary)_ If
this turns out to actually be unacceptably slower, one possibility is to
switch to `uucode`'s `precomputedGraphemeBreak` which uses a 1445 byte
table since it uses a dense table (indexed using multiplication instead
of bitCast, though, which did show up in the initial benchmarks from
https://github.com/ghostty-org/ghostty/pull/8757 a small amount.)
AI was used in some of the uucode changes in
https://github.com/ghostty-org/ghostty/pull/9678 (Amp--primarily for
tests), but everything was carefully vetted and much of it done by hand.
This PR was made without AI with the exception of consulting AI about
whether the "Prepend + ASCII" scenario is common (hopefully it's right
about that being uncommon).
## Summary
This PR adds a new `selection-word-chars` configuration option that
allows users to customize which characters mark word boundaries during
text selection operations (double-click, word selection, etc.).
## Motivation
This's been on my wishlist for a while. Inspired by #9069 which added
semicolon as a hardcoded word boundary, this PR takes the concept
further by making word boundaries fully configurable. Different
workflows and use cases benefit from different boundary characters - SQL
developers might want semicolons as boundaries, while others working
with file paths or URLs might prefer different settings.
This approach is similar to zsh's `WORDCHARS` environment variable,
giving users fine-grained control over text selection behavior.
## Changes
- **New config option**: `selection-word-chars` with default value `` `
\t'"│`|:;,()[]{}<>$` ``
- **Runtime UTF-8 parsing**: Boundary characters are parsed from UTF-8
string to u32 codepoints
- **Updated function signatures**: `selectWord()` and
`selectWordBetween()` now accept boundary characters as parameters
- **All call sites updated**: Surface.zig, embedded.zig, and all test
cases updated
## Usage
Users can now customize word boundaries in their config:
```ini
# Remove semicolon from boundaries (treat as part of words)
selection-word-chars = " \t'\"│`|:,()[]{}<>$"
# Remove periods for better URL selection
selection-word-chars = " \t'\"│`|:;,()[]{}<>$"
```
## Implementation Details
- Boundary characters are stored in `DerivedConfig` and passed through
to selection functions
- UTF-8 parsing happens at runtime with graceful fallback for invalid
input
- Null character (U+0000) is always included as a boundary automatically
- Multi-byte UTF-8 characters are fully supported
## AI Assistance Disclosure
With gratitude for the team and respect for the [Contributing
Guidelines](https://github.com/ghostty-org/ghostty/blob/main/CONTRIBUTING.md),
I want to disclose that this PR was written with AI assistance (Claude
Code). I have reviewed all the code, and to the extent of my
understanding, I'm prepared to answer any questions about the changes.
## Related
- Inspired by #9069
- Change all codepoint types from u32 to u21 to align with Zig stdlib
- Update ArrayList to use Zig 0.15 unmanaged pattern (.empty)
- Remove unnecessary @intCast when encoding UTF-8
- Fix formatEntry to use stack-allocated buffer