Supersedes #14071 (just additional review and local re-generation/re-org
of needed headers).
This updates our own bundled fontconfig (for static builds) to 2.18.3.
Note that fontconfig has changed their build process a bit since this
has been updated last; they are leaning on the Autoconf (and Meson as
they are now deprecating use of Autoconf) toolchain(s) to now generate a
number of headers that are a part of the build process.
Since servicing this dependency in an effort to keep the build pure Zig
is starting to get more complex, I've added some documentation on how to
actually get a snapshot of the fontconfig repository in a state where
files can be looked for and copied over as needed. Otherwise, we might
want to in the future consider removing this altogether and just rely on
system integrations.
Legacy Alt-as-Escape now prefixes the complete UTF-8 sequence for
non-ASCII input. When text is unavailable, the encoder falls back to the
UTF-8 encoding of the unshifted codepoint.
This fixes 16 xterm legacy cases and eight fixterms cases without
changing MOK2. The cases I'm talking about are in my comparison
harness...
The helper now writes Escape and the selected payload directly. It
preserves macOS Option-as-Alt translation and shifted ASCII behavior.
Legacy Alt-as-Escape now prefixes the complete UTF-8 sequence for
non-ASCII input. When text is unavailable, the encoder falls back to
the UTF-8 encoding of the unshifted codepoint.
This fixes 16 xterm legacy cases and eight fixterms cases without changing
MOK2. The cases I'm talking about are in my comparison harness...
The helper now writes Escape and the selected payload directly. It
preserves macOS Option-as-Alt translation and shifted ASCII behavior.
Follow-up to #14144
I wrote a harness that created all possible US-layout keyboard input
combinations with xterm patch 411 and Ghostty main and compared their
full encoding sequence. There are various miscompatibilities on purpose
but these were definitely bugs I wanted to address first.
- Normal-mode numeric keypad keys now preserve their numeric output
instead of falling through to generic MOK2 encoding. Application keypad
behavior is unchanged.
- F13 through F25 now emit their xterm-compatible function-key
sequences, including modifier parameters.
- Help and Context Menu now emit editing-key codes 28 and 29.
- Alt+Escape now emits `CSI 27;3;27~` under MOK2 while retaining the
traditional `ESC ESC` encoding otherwise.
After these changes, 2,081 of 2,096 cases match xterm exactly. The
remaining 15 differences are intentional.
#7425
Control-modified characters use xterm's MOK2 encoding.
I incorrectly believed previously that MOK2 ctrl chars were still
encoded as C0 bytes. This is wrong. I'm going to do a more in depth
audit if possible with every possible key combination against xterm to
see where we diverge but this fixes this for now without regressing any
tests.
Background: https://invisible-island.net/xterm/modified-keys.html
#7425
Control-modified characters use xterm's MOK2 encoding.
I incorrectly believed previously that MOK2 ctrl chars were still encoded
as C0 bytes. This is wrong. I'm going to do a more in depth audit if
possible with every possible key combination against xterm to see where
we diverge but this fixes this for now without regressing any tests.
Background: https://invisible-island.net/xterm/modified-keys.html
This focuses explicitly on the non-mmap allocations for a
`ghostty_terminal_new` result. The result is that we lower this portion
of the memory by almost half. The total benefit is smaller since 70% of
a terminal is mmap'd allocations, but this still yields an absolute ~5KB
savings on macOS on every new terminal (not just empty, but also with a
normal prompt and so on).
Four changes to make it happy, broken down into individual commits.
Nothing crazy:
- **Page list nodes are pooled individually.** The node pool was a
`std.heap.MemoryPool`, which sits on an arena that preheats and grows
1.5x, so we paid for wasted space. Nodes now come from `UntouchedPool`
(the same pool as page buffers) with a preheat of one, so the cost is
exactlyone node (well, exactly one bucket element size in whatever
allocator).
- **Pin pool and tracked pin set are sized for two pins.** Every screen
tracks exactly a viewport pin and a cursor pin at creation, but we
preheated eight pins and let the tracked pin map grow to 17 slots on the
first insert via doubling.
- **The kitty temp dir path is allocated only when set.** The C wrapper
embedded a 1 KiB `max_path_bytes` buffer that only embedders that set
`kitty_image_medium_temp_file` ever wrote to.
- **The default palette is shared instead of copied.** `DynamicPalette`
carried two full 1 KiB palettes, `current` and `original`, and
`original` was almost always the built-in default. It is now a pointer
to the shared built-in default, or to an allocator-owned copy when a
custom default is set. This introduces a new OOM path but we gracefully
handle it by either ignoring or resetting.
Memory measurements:
| Per terminal | Before | After |
|---------------------------------------------|----------|----------|
| phys_footprint delta, fresh | 30,066 B | 25,069 B |
| phys_footprint delta, styled prompt written | 47,023 B | 41,944 B |
| malloc zone bytes dirtied, fresh | 12,698 B | 7,782 B |
| malloc blocks live after `terminal_new` | 11,904 B | 6,688 B |
| malloc blocks live after the prompt | 12,320 B | 7,104 B |
I ran `ghostty-bench +terminal-stream` on a 500 MB ascii corpus, main vs
this branch interleaved, and there is no noticeable change.
**AI usage:** Fable did validation of the work, I did the
implementations, commit messages, and PR notes.
This updates all our page data structures so that the `0` value
(literally `@memset(0)`) means empty. This way, when we initialize a new
page via mmap (OS-guaranteed zeroed), we don't need to write to it, and
don't trigger the kernel to physically map the memory.
From Ghostty 1.3.1, our empty terminal physical memory usage goes from
128 KB to 48 KB (#14130) to 16 KB (this PR). And even with an empty
prompt written on my machine, it holds at 16KB, only increasing to two
pages (32 KB) with 24 rows written.
Here are some measurements.
| Per terminal | Before (macOS) | After (macOS) | Before (Linux) | After
(Linux) |
|-------------------------------------------------------|----------------|---------------|----------------|---------------|
| Page-list memory dirty, fresh | 48 KiB | 16 KiB | 24 KiB | 8 KiB |
| Page-list memory dirty, 24 visible rows written | 64 KiB | 32 KiB | 36
KiB | 20 KiB |
Note macOS uses 16KB pages and Linux generally uses 4 KB pages.
I ran `ghostty-bench +terminal-stream` main vs this branch and with
every normal workload the results are within noise (sometimes faster
sometimes slower).
**AI usage:** It was used as a judge/validator. The actual changes were
me, commit messages and PR messages all me.
This replaces the `std.heap.MemoryPool` used for page buffers with a
custom pool called `UntouchedPool`. This keeps its free list in a side
array and never reads/writes items until `create()`. This means that
demand-driven allocations (like mmaped pages) don't incur physical costs
until they're actually used.
The standard `std.heap.MemoryPool` uses an intrusive linked list for its
items which causes every item to be touched, which forces a full page-in
of memory.
It turns out we also had a lot of assertions and logic to work around
this in various ways (size of rows, asserting we overwrite the free list
entry, etc.) that we can now remove because of this.
For an 80x24 terminal on macOS (16 KB pages):
| Per terminal | Before | After |
|------------------------------|----------|----------|
| Page-list memory dirty | 128 KiB | 48 KiB |
| Process phys_footprint delta | 143 KiB | 62 KiB |
| Page-list virtual size | 2208 KiB | 1600 KiB |
The remaining 48 KB is the active page, because we sprinkle metadata
around the page which forces every page to be paged in. I'm going to
follow this up with some work trying to move all our metadata to the
front of the page so we only page one in until the rest is needed, but
not sure if its achievable.
Micro-benchmarks on the pool show that its twice the speed (slower) to
create/free due to the side list, but in an actual `+terminal-stream`
benchmark churning through pages, there is no measurable difference. I
think its a good trade.
This replaces the `std.heap.MemoryPool` used for page buffers with
a custom pool called `UntouchedPool`. This keeps its free list in a side
array and never reads/writes items until `create()`. This means that
demand-driven allocations (like mmaped pages) don't incur physical costs
until they're actually used.
The standard `std.heap.MemoryPool` uses an intrusive linked list for
its items which causes every item to be touched, which forces a full
page-in of memory.
It turns out we also had a lot of assertions and logic to work around
this in various ways (size of rows, asserting we overwrite the free
list entry, etc.) that we can now remove because of this.
For an 80x24 terminal on macOS (16 KB pages):
| Per terminal | Before | After |
|------------------------------|----------|----------|
| Page-list memory dirty | 128 KiB | 48 KiB |
| Process phys_footprint delta | 143 KiB | 62 KiB |
| Page-list virtual size | 2208 KiB | 1600 KiB |
The remaining 48 KB is the active page, because we sprinkle metadata
around the page which forces every page to be paged in. I'm going to
follow this up with some work trying to move all our metadata to the
front of the page so we only page one in until the rest is needed,
but not sure if its achievable.
Micro-benchmarks on the pool show that its twice the speed (slower) to
create/free due to the side list, but in an actual `+terminal-stream`
benchmark churning through pages, there is no measurable difference. I
think its a good trade.
This fixes a regression of 9a6469743 introduced in 6e8ed4e8b.
With this fix the mouse pointer will be restored to the previous (which
could have be set to something else via OSC22), not a hardcoded .text or
.default.
It also will change the cursor to a text selection if shift is held even
without mouse tracking, I think this is the expected behavior when a
cursor is set via OSC22. (Kitty additionaly, once you start selecting
changes to text (I-beam), this would be a follow-up if we desire to
behave like kitty with OSC22 pointers).
keyToMouseShape was initially designed with more of a transition table
model in mind to handle key presses/overrides based on very specific
cursor states. This never materialized, so I think it's safe to just
simply the process of handling overrides and/or passing along the
current cursor state from the terminal in the event of key presses.
Also removed a test that is essentially a duplicate of one before it now
(returning current surface shape in the event of no overrides).
I think this is the expected behavoir when a custom OSC22 pointer is set. Once
shift is released it will return to mouse_shape (whatever the pointer
was before shit held).
Fixes the runaway-thread bug reported in #14100 (vouched there).
`openThread` drains the spawned opener's stderr with
`takeDelimiterExclusive('\n')`. That function tosses only the exclusive
length, so the `'\n'` is never consumed. Once the child writes one line
to stderr, every subsequent call returns an empty slice without
advancing the stream: the `while (true)` loop spins forever — one pinned
core per affected `open()`, logging empty `os-open: open stderr=`
warnings at tens of thousands of messages per second for the lifetime of
the process — and `exe.wait()` is never reached, so the child is never
reaped.
This change reads inclusively (`takeDelimiterInclusive`, which does
consume the delimiter) and trims the `'\n'` for logging.
Observed in the wild embedding libghostty on macOS: several days of
uptime accumulated six leaked opener threads at ~70% of a core each
(~4.4 cores), from six link clicks whose `/usr/bin/open` wrote to
stderr. After the fix, the same workload shows zero `os-open` log
traffic and no leaked threads.
Repro without the fix: open a link whose handler writes to stderr (e.g.
an OSC 8 link with an unknown scheme), then watch a core pin and `log
stream --predicate 'subsystem == "com.mitchellh.ghostty"'` flood.
**AI disclosure** (per `AI_POLICY.md`): the bug was diagnosed and this
patch drafted with Claude Code (thread sampling, log analysis, and
reading the Zig 0.16 `std.Io.Reader` source to confirm
`takeDelimiterExclusive`/`takeDelimiterInclusive` toss semantics). I
reviewed the analysis and the change, understand both, and verified the
fix in a production build of the embedding app.
takeDelimiterExclusive never consumes the delimiter: it tosses only the
exclusive length, so the '\n' stays buffered. Once the spawned opener
writes a single line to stderr, every subsequent call returns an empty
slice without advancing the stream, and openThread's loop spins forever
- one pinned core per affected open(), logging empty
"open stderr=" warnings at tens of thousands of messages per second for
the lifetime of the process. The thread also never reaches exe.wait(),
so the child is never reaped.
Read inclusively instead (which does consume the delimiter) and trim
the '\n' for logging.
Repro: open a link whose handler writes to stderr, e.g. an OSC 8 link
with an unknown scheme; watch a core disappear and the unified log
flood with "os-open: open stderr=".
See discussion #14100.
A search that had already exhausted a screen's PageList never picked up
history pages prepended afterwards by incremental snapshot restore.
The lower level PageListSearch and so on could already handle this, we
just needed to let it know that more history existed to search. This
fixes that.
A search that had already exhausted a screen's PageList never picked up
history pages prepended afterwards by incremental snapshot restore.
The lower level PageListSearch and so on could already handle this, we
just needed to let it know that more history existed to search. This
fixes that.
Bumps
[flatpak/flatpak-github-actions/flatpak-builder](https://github.com/flatpak/flatpak-github-actions)
from 6.7 to 6.8.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/flatpak/flatpak-github-actions/releases">flatpak/flatpak-github-actions/flatpak-builder's
releases</a>.</em></p>
<blockquote>
<h2>v6.8</h2>
<ul>
<li>Add saveCache flag</li>
<li>Add ability to override artifact name</li>
<li>Add buildDebugBundle flag</li>
<li>Update tests, documentation and dependencies</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="7932741660"><code>7932741</code></a>
Update all dependencies and regenerate dist</li>
<li><a
href="09e3d61868"><code>09e3d61</code></a>
readme: Don't specify setting cache key to github.sha (<a
href="https://redirect.github.com/flatpak/flatpak-github-actions/issues/261">#261</a>)</li>
<li><a
href="23e622281a"><code>23e6222</code></a>
Update runtime versions and docker images to latest</li>
<li><a
href="a3ab43f581"><code>a3ab43f</code></a>
flatpak-builder: Add saveCache flag</li>
<li><a
href="8e357b1556"><code>8e357b1</code></a>
ci: Remove unnecessary 'needs' from debug bundle job</li>
<li><a
href="26e19caa3a"><code>26e19ca</code></a>
ci: Add test for artifact-name</li>
<li><a
href="06d246b4d5"><code>06d246b</code></a>
flatpak-builder: Add ability to override artifact name</li>
<li><a
href="a262264771"><code>a262264</code></a>
ci: Add job that uses build-debug-bundle</li>
<li><a
href="f7362292df"><code>f736229</code></a>
flatpak-builder: Add buildDebugBundle flag</li>
<li><a
href="3b10954431"><code>3b10954</code></a>
ci: Update actions to versions using Node 24</li>
<li>See full diff in <a
href="401fe28a83...7932741660">compare
view</a></li>
</ul>
</details>
<br />
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
</details>
Bumps [cachix/cachix-action](https://github.com/cachix/cachix-action)
from 5f2d7c5294214f71b873db4b969586b980625e71 to
38b082610b782e7e93e209c35fd730d399dee866.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/cachix/cachix-action/blob/master/RELEASE.md">cachix/cachix-action's
changelog</a>.</em></p>
<blockquote>
<h1>Release</h1>
<ol>
<li>
<p>Create and push a new tag:</p>
<pre lang="console"><code>git tag v17
git push origin v17
</code></pre>
</li>
<li>
<p>Wait for CI to pass.</p>
</li>
<li>
<p><a href="https://github.com/cachix/cachix-action/releases/new">Create
a release</a> for the new tag.</p>
</li>
<li>
<p>Move the major version tag to the latest release:</p>
<pre lang="console"><code>git tag -fa v17
git push origin v17 --force
</code></pre>
</li>
</ol>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="38b082610b"><code>38b0826</code></a>
dev: cleanup tests and dev files</li>
<li><a
href="0fe030c286"><code>0fe030c</code></a>
dist</li>
<li><a
href="792dafcfd0"><code>792dafc</code></a>
deps: bump dependencies</li>
<li><a
href="b690244fb5"><code>b690244</code></a>
ci: improve Nix compatibility test coverage</li>
<li><a
href="f495f3ffa2"><code>f495f3f</code></a>
Merge pull request <a
href="https://redirect.github.com/cachix/cachix-action/issues/217">#217</a>
from cachix/dependabot/github_actions/actions/checkout-7</li>
<li><a
href="9ee3c77d45"><code>9ee3c77</code></a>
chore(deps): bump actions/checkout from 6 to 7</li>
<li>See full diff in <a
href="5f2d7c5294...38b082610b">compare
view</a></li>
</ul>
</details>
<br />
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
</details>
Found another regression when investigating #14107 after the last fix.
This regression appears on macOS 15 and 26 as well: **New window by
Shortcuts.app or service menu while a window is visible would create a
tab**.
It appears that for `new-window` triggered by Shortcuts/Service, a small
delay is needed to avoid automatic tabbing. It's either removing
`NSWindow.userTabbingPreference == .always` completely or adding another
"delay" for cascading. The latter should be better.
Also fixes another cascading for `macos-titlebar-style = hidden`
previously missed.
https://github.com/rcaloras/bash-preexec/releases/tag/0.7.0
We only source bash-preexec for bash < 4.4, so most of this release is
inert for us: the PS0 function-substitution hook (bash >= 5.3) and the
array PROMPT_COMMAND handling (bash >= 5.1) are never reached. What we
do pick up is the simpler install string, per-prompt re-adjustment of
PROMPT_COMMAND when something else modifies it, preservation of $? and
$_ on early returns, and the first-command preexec fix.
We continue to carry one local modification: __bp_adjust_histcontrol
stays disabled in the DEBUG trap hook so the user's HISTCONTROL is
respected (#2269). The original justification was that we didn't use the
preexec command argument, which is no longer true because we use it for
the window title. The comment now explains the current reasoning: our
bash >= 4.4 integration also uses `history 1` without adjusting
HISTCONTROL and accepts the same inaccuracy for space-prefixed commands,
so the legacy path is kept consistent with it.
*AI Usage:* I asked Fable 5.1 to run a verification pass after my manual
upgrade, and it confirmed the expected behavior.