A handful of improvements. See individual commits.
1. **Actually compare values for the binding set.** This sounds crazy,
but up until now (for _years_) we've only compared _the hash value_ of a
trigger or action for our binding set. It's actually astounding this
hasn't bit us or at least not that we know of. This could result in
different triggers overwriting each other. Anyways, we actually compare
them now.
2. **Use an `ArrayHashMap` for sets.** This has been on the back burner
for awhile. Using an array hash map is a good idea in general (see:
https://github.com/ziglang/zig/issues/17851) but it also is a nicer API
for our use case and cleaned things up.
All unit tests pass, many new unit tests added particularly for equality
comparison. Hopeful this doesn't regress any bindings but this is the
right path forward so we should fix those if they come up.
**AI disclosure:** AI helped write the deepEqual unit tests, otherwise
everything else is certified meat.
This is recommended for ongoing performance:
https://github.com/ziglang/zig/issues/17851
Likely not an issue for this particular use case which is why it never
bit us; we don't actively modify this map much once it is created. But,
its still good hygiene and ArrayHashMap made some of the API usage
nicer.
We previously only compared the hashes for triggers and actions for hash
map equality. I'm genuinely surprised this never bit us before because
it can result in false positives when two different values have the same
hash. Fix that up!
Fixes#10239
The main menu uses first responder which will hit a surface. If a
binding would target `all:` we need to avoid it. To achieve this, our
`is_key_binding` API now returns information about the binding (if any).
I've cleaned up the Swift to implement this.
In doing this I realized we have to do the same for `performable` since
main menus will effectively always consume.
Fixes#10227
The big comment in `search/screen.zig` describes the solution well. The
problem is that our search is discrete by page and a page can contain
some amount of history as well.
For zero-scrollback screens, we need to fully prune any history lines.
For everyone else, everything in the PageList is scrollable and visible
so we should search it.
The big comment in `search/screen.zig` describes the solution well. The
problem is that our search is discrete by page and a page can contain
some amount of history as well.
For zero-scrollback screens, we need to fully prune any history lines.
For everyone else, everything in the PageList is scrollable and visible
so we should search it.
Partial #10227
This fixes the scrollbar part of #10227, but not the search part.
The way PageList works is that max_size is advisory: we always allocate
on page boundaries so we always have _some_ extra space (usually, unless
you ask for a byte-perfect max size). Normally this is fine, it doesn't
cause any real issues. And this has been true since Ghostty 1.0.
But with the introduction of scrollbars (and search), we were exposing
this hidden space to the user. To fix this, the easiest approach is to
special-case the zero-scrollback scenario, since it is already
documented that scrollback limit is not _exact_ and is subject to some
minimum allocations. But with zero-scrollback we really expect NOTHING.
Partial #10227
This fixes the scrollbar part of #10227, but not the search part.
The way PageList works is that max_size is advisory: we always allocate
on page boundaries so we always have _some_ extra space (usually, unless
you ask for a byte-perfect max size). Normally this is fine, it doesn't
cause any real issues.
But with the introduction of scrollbars (and search), we were exposing
this hidden space to the user. To fix this, the easiest approach is to
special-case the zero-scrollback scenario, since it is already
documented that scrollback limit is not _exact_ and is subject to some
minimum allocations. But with zero-scrollback we really expect NOTHING.
This replaces the OSC parser with one that only uses a state machine to
determine which OSC is being handled, rather than parsing the whole OSC.
Once the OSC command is determined the remainder of the data is stored
in a buffer until the terminator is found. The data is then parsed to
determine the final OSC command.
This PR introduces a new `key-remap` configuration option that allows
users to remap modifier keys at the application level without affecting
system-wide settings.
## Issue
Closes#5160
## Usage
```ini
# Make Ctrl act as Cmd within Ghostty
key-remap = ctrl=super
# Swap Ctrl and Cmd
key-remap = ctrl=super
key-remap = super=ctrl
# Remap only left Alt to Ctrl
key-remap = left_alt=ctrl
```
### Supported Modifiers
| Generic | Left-sided | Right-sided |
|---------|------------|-------------|
| `ctrl` / `control` | `left_ctrl` | `right_ctrl` |
| `alt` / `opt` / `option` | `left_alt` | `right_alt` |
| `shift` | `left_shift` | `right_shift` |
| `super` / `cmd` / `command` | `left_super` | `right_super` |
## Behavior
Per the issue specification:
- **One-way remapping**: `ctrl=super` means Ctrl becomes Super, but
Super remains Super
- **Non-transitive**: `ctrl=super` + `alt=ctrl` → Alt becomes Ctrl (NOT
Super)
- **Sided support**: Generic modifiers match both sides; use `left_*` or
`right_*` for specific sides
- **Immediate effect**: Changes apply on config reload
## Limitations
- Implemented in Zig core, works on both macOS and Linux
- Only modifier keys can be remapped (not regular keys)
Command-based shell detection has been extracted to its own function
(detectShell), which is nicer for testing. It now uses argIterator to
determine the command's executable, rather than the previous string
operations, which allows us to handle command strings containing quotes
and spaces.
Also, our shell-specific setup functions now use a consistent signature,
which simplifies the calling code quite a bit.
Command-based shell detection has been extracted to its own function
(detectShell), which is nicer for testing. It now uses argIterator to
determine the command's executable, rather than the previous string
operations, which allows us to handle command strings containing quotes
and spaces.
Also, our shell-specific setup functions now use a consistent signature,
which simplifies the calling code quite a bit.
## Why
When double-clicking on a URL like `https://example.com`, only `https`
or `//example.com` gets selected because `:` and `/` are word
boundaries. Users expect the entire URL to be selected.
## How
Added URL detection to the double-click handler in `Surface.zig`:
1. Before falling back to `selectWord`, try to detect if the clicked
position is part of a URL
2. Uses the pre-compiled link regexes from user configuration (same
patterns used for cmd+click)
3. If a URL is found at the position, select the entire URL
4. Otherwise, fall back to normal word selection
The implementation:
- Respects user's link configuration (disabled URLs won't trigger
selection)
- Reuses pre-compiled regexes from `DerivedConfig.links` (no per-click
compilation)
- Follows the same patterns as `linkAtPos`
## What
- `src/Surface.zig`: Added `urlAtPin()` helper function and modified
double-click handler
- `src/terminal/StringMap.zig`: Added 2 tests for URL detection
following existing test patterns
When double-clicking text, first check if the position is part of a URL
using the default URL regex pattern. If a URL is detected, select the
entire URL instead of just the word.
This follows the feedback from PR #2324 to modify the selection behavior
rather than introducing a separate selectLink function. The implementation
uses the existing URL regex from config/url.zig which already handles
various URL schemes (http, https, ftp, ssh, etc.) and file paths.
The URL detection runs before the normal word selection, falling back to
selectWord if no URL is found at the clicked position.
Adds `-fPIC` flag for musl targets when building highway and simdutf C++
dependencies, matching the existing freebsd behavior.
## What is PIC?
Position Independent Code (PIC) generates machine code using relative
addresses instead of absolute ones, allowing the code to be loaded at
any memory address. This is required when linking static libraries into
shared libraries (.so files).
## Why both freebsd and musl need it
Both freebsd and musl use strict relocation policies that reject non-PIC
code in shared libraries. Without `-fPIC`, linking fails with errors
like:
```
relocation R_X86_64_PC32 cannot be used against symbol '__cxa_begin_catch'
```
glibc is more permissive and handles these relocations at runtime, which
is why linux-gnu targets work without this flag.
## Context
This enables cross-compiling ghostty-vt to musl/Alpine Linux targets.
Discovered while integrating ghostty-vt into opentui:
https://github.com/sst/opentui/pull/440
---
This PR was created with help from Claude Opus 4.5.