Replaces #9785
This adds session search to the command palette on macOS. Session search
lets you jump to any running terminal based on title or working
directory. The command palette shows you the title, working directory,
and tab color (if any) to help you identify the terminal you care about.
This also enhances our command palette in general to support better
sorting, more stable sorts when keys are identical, etc.
## Demo
https://github.com/user-attachments/assets/602a9424-e182-4651-bf08-378e9c5e1616
## Future
Since this inherits the command palette infrastructure, we don't have
fuzzy search capabilities yet, but I still think this is useful already.
We should add fuzzy searching in the future.
Thanks @phaistonian for the inspiration here but I did do a full
rewrite.
**AI disclosure:** I used AI to assist with this in places, but I
understand everything and touched up almost everything manually.
This PR fixes an issue in the change I merged yesterday (#9921), which
was reported by @quonb. Apology
I verified the fix by testing a wide range of URL schemes:
```
echo "https://example.com"
echo "http://example.com"
echo "mailto:test@example.com"
echo "ftp://example.com/file.txt"
echo "file:/Users/you/file.txt"
echo "ssh:user@example.com"
echo "git://github.com/user/repo.git"
echo "ssh://example.com/path"
echo "tel:+12123456789"
echo "ipns://example.com/path"
echo "gemini://example.com/"
echo "gopher://example.com/1menu"
echo "news:comp.lang.zig"
```
#1123 added a warning when the OpenGL version is too old, but it is
never used because GTK enforces the version set with
gl_area.setRequiredVersion() before prepareContext() is called: we end
up with a generic "failed to make GL context" error:
```
warning(gtk_ghostty_surface): failed to make GL context current: Unable to create a GL context warning(gtk_ghostty_surface): this error is almost always due to a library, driver, or GTK issue warning(gtk_ghostty_surface): this is a common cause of this issue: https://ghostty.org/docs/help/gtk-opengl-context
```
This patch removes the requirement at the GTK level and lets the ghostty
renderer check, now failing as follow:
```
info(opengl): loaded OpenGL 4.2
error(opengl): OpenGL version is too old. Ghostty requires OpenGL 4.3 warning(gtk_ghostty_surface): failed to initialize surface err=error.OpenGLOutdated warning(gtk_ghostty_surface): surface failed to initialize err=error.SurfaceError
```
(Note that this does not render a ghostty window, unlike the previous
error which rendered the "Unable to acquire an OpenGL context for
rendering." view, so while the error itself is easier to understand it
might be harder to view)
-----------------------
I looked for why I couldn't get a context at all mostly to see what
OpenGL 4.3 functions were required to see if there'd be easy fallbacks,
but it also took me a good 15 minutes this morning to understand that
there was nothing wrong with my gtk/mesa versions and it's just my
hardware being old, so I think it'd good to take in itself even if the
displaying itself is a bit meh.
Happy to try to display it in the error page if you think it's important
and I can figure out how (haven't looked much yet)
#1123 added a warning when the OpenGL version is too old, but
it is never used because GTK enforces the version set with
gl_area.setRequiredVersion() before prepareContext() is called:
we end up with a generic "failed to make GL context" error:
warning(gtk_ghostty_surface): failed to make GL context current: Unable to create a GL context
warning(gtk_ghostty_surface): this error is almost always due to a library, driver, or GTK issue
warning(gtk_ghostty_surface): this is a common cause of this issue: https://ghostty.org/docs/help/gtk-opengl-context
This patch removes the requirement at the GTK level and lets the ghostty
renderer check, now failing as follow:
info(opengl): loaded OpenGL 4.2
error(opengl): OpenGL version is too old. Ghostty requires OpenGL 4.3
warning(gtk_ghostty_surface): failed to initialize surface err=error.OpenGLOutdated
warning(gtk_ghostty_surface): surface failed to initialize err=error.SurfaceError
(Note that this does not render a ghostty window, unlike the previous
error which rendered the "Unable to acquire an OpenGL context for
rendering." view, so while the error itself is easier to understand it
might be harder to view)
This is a subcommand I've been using for some time. It takes an optional
issue/PR number as context and produces a prompt to review a branch for
how well it addresses the issue along with any isolated issues it spots.
Example:
https://ampcode.com/threads/T-019b2877-475f-758d-ae88-93c722561576
This is a subcommand I've been using for some time. It takes an optional
issue/PR number as context and produces a prompt to review a branch for
how well it addresses the issue along with any isolated issues it spots.
Example: https://ampcode.com/threads/T-019b2877-475f-758d-ae88-93c722561576
Related Issue: #5047
Discussion: #4664
### Investigation
The behavior of iTerm mentioned on the Discussion thread was as follows:
- `cmd + u` can be used to toggle "Use Transparency"
- The "Use Transparency" toggle operates on a per-surface basis
- The "Use Transparency" state persists even after reloading the config
### Summary
Based on the investigation and discussions in the preceding pull
requests, this implements the `toggle_background_opacity` keybind action
for macOS with the following specifications:
- Switches background opacity on a per-surface basis
- The background opacity state persists even after reloading the config
- Background opacity switching functionality is also available in Quick
Terminal
- Does nothing if `background-opacity` is set to 1 or higher
- Does nothing if in fullscreen mode
### Verification
This functionality has been tested across following scenarios,
confirming correct action behavior for:
- Split Window
- Each split window maintains synchronized background opacity states
- Tab Group
- Background opacity states do not synchronize between tabs
- Quick Terminal
- Background opacity states remain synchronized even when windows are
closed
- Command Palette
### AI Disclosure
This pull request was made with assistance from Claude Code.
I reviewed all AI-generated code and wrote the final output manually.
https://github.com/user-attachments/assets/e46ff8f0-42f2-442f-97dd-d5f5c33b10f1
This PR fixes an issue #9563 where relative file paths were not being
resolved against the terminal’s current working directory before
opening.
#### Verification
Tested with directories containing:
```
/tmp/test/test
❯ du -h .
0B ./spaces-end
0B ./with dot.
0B ./space middle
8.0K .
```
Parent directory resolution also works as expected:
```
/tmp/test/test
❯ du -h ..
0B ../test/spaces-end
0B ../test/with dot.
0B ../test/space middle
8.0K ../test
16K ..
```
@mitchellh
In your original description you mentioned that “Links should work for all situations as they do in iTerm2.”
I noticed that, for example, when running `ls`, the paths are not clickable, while they are clickable in iTerm2.
If you think this case should also be handled, I can open a separate PR for it once this one is accepted.
Implements #8399.
I didn't save `position` in this pr, since I don't see its necessity.
Changing `quick-terminal-position` requires a relaunch; saving and
restoring this would have to deal with conflicts. I also don't see why a
user would change this frequently.
> [!NOTE]
> Used GPT to proofread my comments
This adds a couple optimizations to our CI:
1. macOS matrix gone, only test that freetype builds in addition to
Coretext.
2. Move Flatpak out to a triggered build on main similar to snaps. This
was our longest build and avoiding it in PRs is a win since it rarely
fails.
This changes our GHA that updates our color schemes to also upload it to
our dependency mirror at the same time. This prevents issues where the
upstream disappears, which we've had many times.
This disables a bunch of configurations that we don't need to actually
test for. The main one we want to keep building is Freetype because we
sometimes use this to compare behaviors, but Coretext is the default.
This is one of the primary drivers of CI CPU time.
This changes our GHA that updates our color schemes to also upload it
to our dependency mirror at the same time. This prevents issues where
the upstream disappears, which we've had many times.
I had a bit of the same annoyance as #9064. I agree that with balanced
padding, you should expect horizontal jitter when resizing horizontally,
and vertical jitter when resizing vertically. Diagonal jitter, however,
happened because the top padding was upper bounded by the left padding,
coupling the vertical and horizontal wobbling. This looks kind of janky,
and it's surprising that the balance between top and bottom padding
changes as you vary only the width of the window.
With this PR, the upper bound is instead equal to the maximum left
padding. Since this only depends on the config, not the current window
width, the diagonal wobbling is avoided. Both the top and left padding
still have the same range of motion as before.
An alternative could be to bound by the minimum or median left padding
instead. Open to suggestions.
**Before**
https://github.com/user-attachments/assets/d12c5870-f05d-450f-89fc-c59eab90e199
**After**
https://github.com/user-attachments/assets/35b80bb0-9ea2-41c1-8502-3a8eec51dbc6
## Description
This PR implements the basic functionality for "Liquid Glass" style
background support (#8155). I used OpenCode (Claude 4) in some capacity
to write this as I'm not super familiar with AppKit / SwiftUI. A good
chunk of this I still needed to write by hand since Claude doesn't
understand the Glass APIs, but I'm not 100% if the implementation here
makes the best decisions since the practices in Ghostty config and
separation of the AppKit code and SwiftUI seemed inconsistent to me.
Some of the combinations of options obviously create entirely unreadable
terminals, but I've found that regular glass and transparent with
opacity to be fairly readable. We *don't* enable this feature by default
since it would of course break existing users setups.
## Open Questions
- [x] How to determine the correct cornerRadius? For now this is
eyeballed, I can't see any macOS public API or clearly documented
constants.
- [x] Should boolean options be exposed for reasonable defaults?
- [x] Should the option need to be namespaced to macos-\*?
## Screenshots
0% Opacity, Regular
<img width="917" height="683" alt="image"
src="https://github.com/user-attachments/assets/ccb96ba7-5df2-4284-8526-e07bbb62e3e5"
/>
50% Opacity, Transparent
<img width="880" height="680" alt="image"
src="https://github.com/user-attachments/assets/5bdf12f9-3209-4aa9-8a4f-9a6eb4f95894"
/>
0% Opacity, Transparent
<img width="860" height="681" alt="image"
src="https://github.com/user-attachments/assets/1b33d400-4d8b-479a-94d7-47b844743e52"
/>
Reported by Mr. Hashimoto in discord.
These changes removedthe following warning:
`evaluation warning: 'system' has been renamed to/replaced by
'stdenv.hostPlatform.system'`
This adds a couple of Nix-based VM integration tests:
- Does `ghostty +version` run successfully?
- Can we create a new terminal window? (This is detected by setting the
background to a color that doesn't appear
normally on the desktop and seeing if we can detect that color in a
screenshot).
Obviously more can be done but I thought that these would be a couple of
good first steps.
The whole test suite can be run with `nix flake check`. Individual tests
can be run with a command like this:
```
nix run .#check.x86_64-linux.<test name>.driver
```
Bug: When toggling a Ghostty window between fullscreen and windowed mode
in the i3 window manager, or between tabbed mode and default tiling
mode, window borders would permanently stop being drawn around the
terminal window.
I bisected to find that the issue was first introduced in aa2dbe2.
The below explanation + the code in this PR was 100% generated by claude
code, since I don't know the first thing about x11 or terminal
development, and have never written any Zig before. I verified this code
change builds and solves the issue for me which I hope is more helpful
than just a bug report. If this code is actually garbage and the issue
should be fixed a different way it wouldn't shock me and this PR can
just be closed in favor of one from someone who knows what they're
doing.
Anyway this is the explanation that the llm generated:
Root cause was that syncAppearance() was updating X11 properties on
every call during window transitions, even when values hadn't changed.
These redundant property updates interfered with i3's border management.
The fix adds caching to syncBlur() and syncDecorations() to only update
X11 properties when values actually change, eliminating unnecessary
property changes during fullscreen transitions.
When toggling a Ghostty window between fullscreen and windowed mode in
the i3 window manager, window borders would disappear and not return.
Root cause was that syncAppearance() was updating X11 properties on
every call during window transitions, even when values hadn't changed.
These redundant property updates interfered with i3's border management.
The fix adds caching to syncBlur() and syncDecorations() to only update
X11 properties when values actually change, eliminating unnecessary
property changes during fullscreen transitions.
Closes#9894
**Problem**: Since #9850, a local build incorrectly identifies itself as
1.3.0 stable (in lieu of tip w/ commit info).
**Bug**: `@import("root")` in `Config.zig` resolves to the compilation
root (`main.zig`), not `build.zig` where the marker was defined. So
`@hasDecl` always returned false (i.e. all builds treated as
dependencies).
**Solution**: More or less @rockorager's original approach from #9850.
> _Use `@src().file` to get ghostty's source directory and compare it
with `b.build_root`. When they differ, ghostty is a dependency and we
skip git detection._
However, there is a potential edge case where if a downstream project
also has a `src/build/Config.zig` this will fail silently - seems
unlikely, but worth noting.
**Testing**:
```
$ zig build -p $HOME/.local -Doptimize=ReleaseFast
$ ghostty --version
Ghostty 1.3.0-main+a0a915a06
Version
- version: 1.3.0-main+a0a915a06
- channel: tip
```
---
> **AI Disclosure**: I used Claude Code to review and identify edge
cases.
Bumps
[actions/download-artifact](https://github.com/actions/download-artifact)
from 6.0.0 to 7.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/download-artifact/releases">actions/download-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<h2>v7 - What's new</h2>
<blockquote>
<p>[!IMPORTANT]
actions/download-artifact@v7 now runs on Node.js 24 (<code>runs.using:
node24</code>) and requires a minimum Actions Runner version of 2.327.1.
If you are using self-hosted runners, ensure they are updated before
upgrading.</p>
</blockquote>
<h3>Node.js 24</h3>
<p>This release updates the runtime to Node.js 24. v6 had preliminary
support for Node 24, however this action was by default still running on
Node.js 20. Now this action by default will run on Node.js 24.</p>
<h2>What's Changed</h2>
<ul>
<li>Update GHES guidance to include reference to Node 20 version by <a
href="https://github.com/patrikpolyak"><code>@patrikpolyak</code></a>
in <a
href="https://redirect.github.com/actions/download-artifact/pull/440">actions/download-artifact#440</a></li>
<li>Download Artifact Node24 support by <a
href="https://github.com/salmanmkc"><code>@salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/415">actions/download-artifact#415</a></li>
<li>fix: update <code>@actions/artifact</code> to fix Node.js 24
punycode deprecation by <a
href="https://github.com/salmanmkc"><code>@salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/451">actions/download-artifact#451</a></li>
<li>prepare release v7.0.0 for Node.js 24 support by <a
href="https://github.com/salmanmkc"><code>@salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/452">actions/download-artifact#452</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/patrikpolyak"><code>@patrikpolyak</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/download-artifact/pull/440">actions/download-artifact#440</a></li>
<li><a href="https://github.com/salmanmkc"><code>@salmanmkc</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/download-artifact/pull/415">actions/download-artifact#415</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/download-artifact/compare/v6.0.0...v7.0.0">https://github.com/actions/download-artifact/compare/v6.0.0...v7.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="37930b1c2a"><code>37930b1</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/452">#452</a>
from actions/download-artifact-v7-release</li>
<li><a
href="72582b9e0a"><code>72582b9</code></a>
doc: update readme</li>
<li><a
href="0d2ec9d4cb"><code>0d2ec9d</code></a>
chore: release v7.0.0 for Node.js 24 support</li>
<li><a
href="fd7ae8fda6"><code>fd7ae8f</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/451">#451</a>
from actions/fix-storage-blob</li>
<li><a
href="d484700543"><code>d484700</code></a>
chore: restore minimatch.dep.yml license file</li>
<li><a
href="03a808050e"><code>03a8080</code></a>
chore: remove obsolete dependency license files</li>
<li><a
href="56fe6d904b"><code>56fe6d9</code></a>
chore: update <code>@actions/artifact</code> license file to 5.0.1</li>
<li><a
href="8e3ebc4ab4"><code>8e3ebc4</code></a>
chore: update package-lock.json with <code>@actions/artifact</code><a
href="https://github.com/5"><code>@5</code></a>.0.1</li>
<li><a
href="1e3c4b4d49"><code>1e3c4b4</code></a>
fix: update <code>@actions/artifact</code> to ^5.0.0 for Node.js 24
punycode fix</li>
<li><a
href="458627d354"><code>458627d</code></a>
chore: use local <code>@actions/artifact</code> package for Node.js 24
testing</li>
<li>Additional commits viewable in <a
href="018cc2cf5b...37930b1c2a">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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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
[actions/upload-artifact](https://github.com/actions/upload-artifact)
from 5.0.0 to 6.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/upload-artifact/releases">actions/upload-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v6.0.0</h2>
<h2>v6 - What's new</h2>
<blockquote>
<p>[!IMPORTANT]
actions/upload-artifact@v6 now runs on Node.js 24 (<code>runs.using:
node24</code>) and requires a minimum Actions Runner version of 2.327.1.
If you are using self-hosted runners, ensure they are updated before
upgrading.</p>
</blockquote>
<h3>Node.js 24</h3>
<p>This release updates the runtime to Node.js 24. v5 had preliminary
support for Node.js 24, however this action was by default still running
on Node.js 20. Now this action by default will run on Node.js 24.</p>
<h2>What's Changed</h2>
<ul>
<li>Upload Artifact Node 24 support by <a
href="https://github.com/salmanmkc"><code>@salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/719">actions/upload-artifact#719</a></li>
<li>fix: update <code>@actions/artifact</code> for Node.js 24 punycode
deprecation by <a
href="https://github.com/salmanmkc"><code>@salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/744">actions/upload-artifact#744</a></li>
<li>prepare release v6.0.0 for Node.js 24 support by <a
href="https://github.com/salmanmkc"><code>@salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/745">actions/upload-artifact#745</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v5.0.0...v6.0.0">https://github.com/actions/upload-artifact/compare/v5.0.0...v6.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="b7c566a772"><code>b7c566a</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/745">#745</a>
from actions/upload-artifact-v6-release</li>
<li><a
href="e516bc8500"><code>e516bc8</code></a>
docs: correct description of Node.js 24 support in README</li>
<li><a
href="ddc45ed9bc"><code>ddc45ed</code></a>
docs: update README to correct action name for Node.js 24 support</li>
<li><a
href="615b319bd2"><code>615b319</code></a>
chore: release v6.0.0 for Node.js 24 support</li>
<li><a
href="017748b48f"><code>017748b</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/744">#744</a>
from actions/fix-storage-blob</li>
<li><a
href="38d4c7997f"><code>38d4c79</code></a>
chore: rebuild dist</li>
<li><a
href="7d27270e0c"><code>7d27270</code></a>
chore: add missing license cache files for <code>@actions/core</code>,
<code>@actions/io</code>, and mi...</li>
<li><a
href="5f643d3c94"><code>5f643d3</code></a>
chore: update license files for <code>@actions/artifact</code><a
href="https://github.com/5"><code>@5</code></a>.0.1 dependencies</li>
<li><a
href="1df1684032"><code>1df1684</code></a>
chore: update package-lock.json with <code>@actions/artifact</code><a
href="https://github.com/5"><code>@5</code></a>.0.1</li>
<li><a
href="b5b1a91840"><code>b5b1a91</code></a>
fix: update <code>@actions/artifact</code> to ^5.0.0 for Node.js 24
punycode fix</li>
<li>Additional commits viewable in <a
href="330a01c490...b7c566a772">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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
We rely on temporarily setting ZDOTDIR to our `zsh` resource directory
to implement automatic shell integration. Setting ZDOTDIR in a system
file like /etc/zshenv overrides our ZDOTDIR value, preventing our shell
integration from being loaded.
The only way to prevent /etc/zshenv from being run is via the --no-rcs
flag. (The --no-globalrcs only applies to system-level files _after_
/etc/zshenv is loaded.) Unfortunately, there doesn't appear to be a way
to run a "bootstrap" script (to reimplement the Zsh startup sequence
manually, similar to how our bash integration works) and then enter an
interactive shell session.
https://zsh.sourceforge.io/Doc/Release/Files.html
Given all of the above, document this as an unsupported configuration
for automatic shell integration and point affected users at our manual
shell integration option.
See: #8000
- Removes unnecessary marker constant from build.zig that existed
solely to signal build root status
- Uses filesystem check (@src().file access) instead of compile-time
declaration lookup to detect when ghostty is a dependency
- Same behavior with less indirection: file resolves from build root
only when ghostty is the main project
We rely on temporarily setting ZDOTDIR to our `zsh` resource directory
to implement automatic shell integration. Setting ZDOTDIR in a system
file like /etc/zshenv overrides our ZDOTDIR value, preventing our shell
integration from being loaded.
The only way to prevent /etc/zshenv from being run is via the --no-rcs
flag. (The --no-globalrcs only applies to system-level files _after_
/etc/zshenv is loaded.) Unfortunately, there doesn't appear to be a way
to run a "bootstrap" script (to reimplement the Zsh startup sequence
manually, similar to how our bash integration works) and then enter an
interactive shell session.
https://zsh.sourceforge.io/Doc/Release/Files.html
Given all of the above, document this as an unsupported configuration
for automatic shell integration and point affected users at our manual
shell integration option.
Fixes#9905
This fixes a major compatibility issue with the CSI S sequence: When our
top margin is at the top (row 0) without left/right margins, we should
be creating scrollback. Previously, we were only deleting.
I'm going to push this to 1.3 just given the feature of regressing any
other VT behaviors from this.
Also note the TODO, we implement the scrollback behavior by treating it
logically like an `index` (but scrolling above the bottom region). This
is expensive for large N but in most cases scroll up is small. Still, we
should optimize this one day.
**AI disclosure:** I used AI to investigate the xterm behavior, but got
it to return line numbers for me to verify the logic myself. I also used
it to identify missing test cases and fill those out initially, but I
modified them myself after. The actual core logic was all hand-written.
Fixes#9905
This fixes a major compatibility issues with the CSI S sequence:
When our top margin is at the top (row 0) without left/right
margins, we should be creating scrollback. Previously, we were
only deleting.
Fixes#9868 (shift+backspace part only)
You have to use `fish_key_reader -V` to verify this since it uses
different modes than Kitty's key reader. This matches the encoding to
Kitty.
Due to security issues, `sudo` implementations may not preserve
environment variables unless appended with `--preserve-env=list`.
Signed-off-by: definfo <hjsdbb1@gmail.com>
Tried my hand at #8432
Currently lacking tests and some sort of visual indicator that the
surface is locked.
Also, not entirely sure if I needed to touch `application.zig`.
Found what needed changing with help from Copilot (and added Docs w/
Copilot), but wrote most of the code myself.
(Claude-generated content below)
## Fix bash shell integration use-after-free bug
### Problem
After updating to the latest `main` branch, Ghostty fails to launch with
bash shell integration enabled on Linux (Ubuntu 25.10). The terminal
window briefly appears and then closes with an error like:
```
/bin/sh: 1: ically: not found
```
The error message varies but typically contains fragments of words (like
"ically" from "automatically").
Related issue was possibly [reported in the comments of PR
#9881](https://github.com/ghostty-org/ghostty/pull/9881) shortly after
it was merged.
### Root Cause Analysis
The bug is a **use-after-free** in the bash shell integration setup.
In `setupBash()`, the `ShellCommandBuilder` is initialized with a
`stackFallback` allocator:
```zig
var stack_fallback = std.heap.stackFallback(4096, alloc);
var cmd = internal_os.shell.ShellCommandBuilder.init(stack_fallback.get());
```
When `cmd.toOwnedSlice()` is called, it returns a slice allocated from
the `stackFallback` allocator. Since the command string (e.g., "bash
--posix") easily fits within 4096 bytes, this memory is allocated on the
**stack**.
When `setupBash()` returns, the stack frame is deallocated, but the
returned `.shell` command still points to this now-invalid stack memory.
When the command is later used in `execCommand()` to build the `/bin/sh
-c "..."` invocation, it reads garbage data.
The garbage data often contains "ically" because it picks up fragments
from nearby memory—likely from the string literal "automatically" in the
comment on line 280:
```zig
// being manually sourced or automatically injected (from here).
```
This is a [known footgun with Zig's stackFallback
allocator](https://github.com/ziglang/zig/issues/16344) - memory
allocated from the stack portion becomes invalid once the stack frame is
exited.
### Bisect / Blame
The bug was introduced **~5 hours ago** (earlier today):
| Field | Value |
|-------|-------|
| **Commit** | `04fecd7c07fccad423ab1c33324a1997e142b6e2` |
| **PR** | [#9881](https://github.com/ghostty-org/ghostty/pull/9881)
(os/shell: introduce ShellCommandBuilder) |
| **Date** | Fri Dec 12 08:59:44 2025 -0500 |
| **Merged as** | `dd06d8a13` |
The previous implementation in commit `c0deaaba4` used
`std.mem.joinZ(alloc, " ", args.items)` which correctly allocated the
result using the arena allocator. The refactor to use
`ShellCommandBuilder` inadvertently changed the allocation strategy to
use stack memory for the returned value.
Other uses of `stackFallback` in the codebase are safe because they
either:
- Copy data to another allocator before the function returns (e.g.,
`env.put()` in `setupXdgDataDirs`)
- Use the data only within the function scope (e.g.,
`stream_handler.zig`)
### The Fix
Copy the command string to the arena allocator before returning:
```zig
const cmd_str = try cmd.toOwnedSlice();
return .{ .shell = try alloc.dupeZ(u8, cmd_str) };
```
This ensures the memory remains valid for the lifetime needed by the
caller, matching the behavior of other `stackFallback` usage patterns in
the codebase.
### Testing
- Verified the fix on Ubuntu 25.10 with Ghostty built from source
- Before fix: Terminal crashes immediately with "ically: not found"
error
- After fix: Terminal launches successfully with shell integration
working
```
info(io_exec): shell integration automatically injected shell=.bash
info(io_exec): started subcommand path=/bin/sh pid=...
info(surface): surface closed addr=... # Normal close, no "abnormal process exit"
```
### Platform
Primarily affects Linux where `.shell` commands are wrapped in `/bin/sh
-c "..."`. macOS uses a different execution path via `login(1)`.
The ShellCommandBuilder uses a stackFallback allocator, which means
toOwnedSlice() may return memory allocated on the stack. When setupBash()
returns, this stack memory becomes invalid, causing a use-after-free.
This manifested as garbage data in the shell command string, often
appearing as errors like "/bin/sh: 1: ically: not found" (where "ically"
was part of nearby memory, likely from the comment "automatically").
The fix copies the command string to the arena allocator before returning,
ensuring the memory remains valid for the lifetime of the command.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This builder is an efficient way to construct space-separated shell
command strings.
We use it in setupBash to avoid using an intermediate array of arguments
to construct our bash command line.
I think at this point all moderators and helpers can agree with me in
that LLM-generated responses are a blight upon this Earth.
Also probably worth putting in a clause against AI-generated assets
This builder is an efficient way to construct space-separated shell
command strings.
We use it in setupBash to avoid using an intermediate array of arguments
to construct our bash command line.
I think at this point all moderators and helpers can agree with me in
that LLM-generated responses are a blight upon this Earth.
Also probably worth putting in a clause against AI-generated assets
(cf. the Commit Goods situation)
This adds the ability to change a _tab_ title. The previous
functionality was tied to a specific _surface_. A tab title will stick
to the current tab regardless of active splits and so on.
This follows the nomenclature that macOS terminal app does which is
"title vs terminal title" (although we explicitly use "tab" in various
places, I may remove that in the future).
**This is macOS only. GTK is tracked here: #9880**. I did macOS only
because thats the machine I'm on. It'll be trivial to add this to GTK,
too.
## Demo
https://github.com/user-attachments/assets/d9446785-d919-4212-8553-db50c56c8c2f
(The option is also in the main menu, the context menu, and the command
palette)
## AI Disclosure
This PR was done fully with Amp, I didn't write a single line of code at
the time of writing this PR description. I reviewed everything though
and fully understand it all. Its a mimic more or less of the prompt
surface title work (although we did unify some stuff like the apprt
action).
Prior to #7044, on macOS, our shell-integrated command line would be
executed under `exec -l`, which causes bash to be started as a login
shell. This matches the macOS platform norms.
The change to direct command execution meant that we'd skip that path,
and bash would start as a normal interactive (non-login) shell on macOS.
We fixed this in #7253 by adding `--login` to the `bash` direct command
on macOS.
This avoided some of the overhead of starting an extra process just to
get a login shell, but it unfortunately doesn't quite match the bash
environment we get when shell integration isn't enabled (namely, `$0`
doesn't get the login-shell-identifying `-` prefix).
Instead, this change implements the approach proposed in #7254, which
switches the bash shell integration path to use a `.shell` command,
giving us the same execution environment as the non-shell-integrated
command.
Prior to #7044, on macOS, our shell-integrated command line would be
executed under exec -l, which causes bash to be started as a login
shell. This matches the macOS platform norms.
The change to direct command execution meant that we'd skip that path,
and bash would start as a normal interactive (non-login) shell on macOS.
We fixed this in #7253 by adding `--login` to the `bash` direct command
on macOS.
This avoided some of the overhead of starting an extra process just to
get a login shell, but it unfortunately doesn't quite match the bash
environment we get when shell integration isn't enabled (namely, $0
doesn't get the login-shell-identifying "-" prefix).
Instead, this change implements the approach proposed in #7254, which
switches the bash shell integration path to use a .shell command, giving
us the same execution environment as the non-shell-integrated command.
Fixed: When reflowing content with many graphemes, the code incorrectly
increased hyperlink_bytes capacity
instead of grapheme_bytes, causing GraphemeMapOutOfMemory errors.
Added: An unit test for this specific issue.
This PR was written primarily by Opus.
Closes: #9863
When reflowing content with many graphemes, the code incorrectly increased hyperlink_bytes capacity
instead of grapheme_bytes, causing GraphemeMapOutOfMemory errors.
Description:
Context menu works on tabs with titlebar-style=tabs on MacOS Tahoe 26
Closes#9817
Demo:
https://github.com/user-attachments/assets/60eaae6e-a3ff-41eb-8c86-ba700490d6e2
Note:
- Tried first a passthrough-views approach, but AppKit’s internal
toolbar subviews continued intercepting right-clicks.
- Runtime subclassing proposed by Claude also worked but was rejected as
too fragile.
- Final solution routes secondary-click events at the window level using
sendEvent(_:), forwarding them to the tab bar only when the click is
visually within its bounds.
AI Disclosure:
AI (Claude Code and Codex) assisted with early explorations, but final
implementation was developed manually after evaluating and discarding
the unsafe subclassing approach proposed by Claude.
With this PR, the macos scrollbar always uses the overlay style. If the
OS preferred style is `.legacy`, we flash the scroller when the mouse is
moved over it, such that users can still click and drag without relying
on scroll wheels or gestures.
Implements #9610.
There are a few lines of code that could technically be removed after
this change as they're only needed to make surfaces work correctly with
the legacy scrollbar, but I decided to leave them in since they do no
harm (see code comments). This ensures correct behavior if, for whatever
reason, some corner case brings back the legacy scrollbar, or if someone
decides to experiment with scrollbar styles in the future.
### Background
After #9344, the Ghostty theme won't change after switching systems',
and reverting #9344 will bring back the issue it fixed.
The reason these two issues are related is because the scheme change is
based on changes of `effectiveAppearance`, which is also affected by
setting the window's `appearance` or changing
`NSAppearance.currentDrawing()`.
### Changes
Instead of observing `effectiveAppearance`, we now explicitly update the
color scheme of surfaces, so that we can control when it happens to
avoid callback loops and redundant updates.
### Regression Tests
- [x] #8282
- [x] Reloading with `window-theme = light` should update Ghostty with
the default dark theme with a dark window theme (break before
[#83104ff](83104ff27a))
- [x] `window-theme = light \n macos-titlebar-style = native` should
update Ghostty with the default dark theme with a light window theme
- [x] Reloading from the default config to `theme=light:3024
Day,dark:3024 Night \n window-theme = light`, should update Ghostty with
the theme `3024 Day` with a light window theme (break on
[#d39cc6d](d39cc6d478))
- [x] Using `theme=light:3024 Day,dark:3024 Night`; Switching the
system's appearance should change Ghostty's appearance (break on
[#d39cc6d](d39cc6d478))
- [x] Reloading from `theme=light:3024 Day,dark:3024 Night` with a light
window theme to the default config, should update Ghostty with the
default dark theme with a dark window theme
- [x] Reloading from the default config to `theme=light:3024
Day,dark:3024 Night \n window-theme=dark`, should update Ghostty with
the theme `3024 Night` with a dark window theme
- [x] Reloading from `theme=light:3024 Day,dark:3024 Night \n
window-theme=dark` to `theme=light:3024 Day,dark:3024 Night` with light
system appearance, should update Ghostty from dark to light
- [x] Reload with quick terminal open
When ghostty is used as a Zig dependency, detect this by comparing the
build root with ghostty's source directory. Skip git detection entirely
and use the version from build.zig.zon.
This fixes build failures when downstream projects have git tags that
don't match ghostty's version format. Previously, ghostty would read the
downstream project's git tags and panic at Config.zig:246 with:
\`\`\`
tagged releases must be in vX.Y.Z format matching build.zig
\`\`\`
**Reproduction:**
1. Create a project that uses ghostty as a Zig dependency
2. Tag the project with a version like \`v0.2.0\`
3. Run \`zig build\` → panic
**Fix:**
Use \`@src().file\` to get ghostty's source directory and compare it
with \`b.build_root\`. When they differ, ghostty is a dependency and we
skip git detection.
Thread:
https://ampcode.com/threads/T-197e6c33-b8f8-4b23-8fc8-7f6b6edd9f35
Detect if ghostty is being built as a dependency by comparing the build
root with ghostty's source directory. When used as a dependency, skip
git detection entirely and use the version from build.zig.zon.
This fixes build failures when downstream projects have git tags that
don't match ghostty's version format. Previously, ghostty would read
the downstream project's git tags and panic at Config.zig:246 with
"tagged releases must be in vX.Y.Z format matching build.zig".
Related to #1935
This adds a new structure `terminal.tmux.Viewer` which continues
building on all the prior tmux control mode work to add a full
bidirectional reconciliation loop to discover and sync terminal states
from tmux to Ghostty and vice versa. **This is the core, cross-platform
business logic that will power the GUIs, later.**
Our prior work were protocol building blocks, and this PR is an actual
functional piece of work. You can now start Ghostty, run `tmux -CC
attach`, and we _will_ be creating full blown terminals internal that
capture the content and mirror the state exactly (barring inevitable
bugs in something this complex). But, we don't yet show them visually.
:) And we don't yet send inputs to it (it's a viewer only, for now).
**This sucked.** The control mode protocol is difficult, to put it
mildly, for a variety of reasons. Correctness of this is going to be
hard. Therefore, I focused really hard on this design to make it **fully
unit test friendly.** We're able to simulate full tmux sessions and runt
through our state machine and assert various states. I think this will
be critical to correctness as we eventually collect real world data.
> [!WARNING]
>
> This does actually have user-impacting changes! When you run `tmux -CC
attach` we will now run our full control mode client. This could result
in bugs or crashes or other problems. This only activates if you have a
real tmux session, though, so it should be avoidable by most users.
Since we don't actually take our state and send it to the GUI or
anything, this should be pretty safe.
**AI disclosure:** I used AI for a lot of the protocol reverse
engineering and documentation to figure out how it all works. I designed
the architecture myself and implemented most of it manually.
Hello! I did read `CONTRIBUTING.md`. I understand that the preference is
to receive pull requests for existing open issues and acknowledge that
I'm skipping a step wrt the preferred workflow since there aren't any
open issues yet for #8419. Living dangerously and hoping for the best
here. I hope you'll see that I'm acting in good faith 🤞.
In my attempts to debug why `quick-terminal-size` config wasn't working
reliably for me, I discovered that the
[`ghostty_config_quick_terminal_size_s`](08c9661683/include/ghostty.h (L460-L480))
struct used to initialize `QuickTerminalSize` in Swift-land, more often
than not, didn't match what I had in my config file (e.g.
`quick-terminal-size = 75%, 50%`)
<details>
08c9661683/include/ghostty.h (L460-L480)08c9661683/macos/Sources/Ghostty/Ghostty.Config.swift (L507-L510)
</details>
Almost all mismatches seemed to be downstream of `tag` (for `primary`
and/or `secondary` `Size`) being an unexpected value for the
`ghostty_quick_terminal_size_tag_e` enum type, which led runtime
execution to fall into the `default` branch in this `switch` control
flow.
08c9661683/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift (L27-L38)
Looking at `src/config/CApi.zig`, `src/config/c_get.zig`,
`src/config/Config.zig`, and [Zig documentation for `extern
enum`](https://ziglang.org/documentation/master/#extern-enum), led me to
conclude that the crux of the issue is lack of guaranteed C ABI
compatibility for `Tag`
08c9661683/src/config/Config.zig (L7986-L7990)08c9661683/include/ghostty.h (L461-L465)
Further research revealed that based on C language spec alone, one
cannot assume a fixed width for `enum` and that the behaviour is
compiler dependant. But given how the Zig documentation suggests using
`enum(c_int)` for C-ABI-compatible enums and extern enums + comments
elsewhere in `Config.zig` suggesting that `enum(c_int)` was chosen for
extern compatibility,
08c9661683/src/config/Config.zig (L4877)
I think that this fix (changing `enum(u8)` to `enum(c_int)`) is likely
the most straightforward and appropriate one.
Despite the existence of #6937 it seems that there are still quite a few
people who aren't sure of the norms surrounding how to use Ghostty's
discussion spaces, so I chose to spell it all out here.
Regarding AI policy I also added a section heavily discouraging AI
responses within PRs and contributing to platforms one can't test on. I
don't understand what is wrong with vibecoders on macOS who just think
that the GTK side will somehow Just Work, despite us being pretty much
the only major piece of software using zig-gobject that I know of.
I am truly not sure why the tests never caught this, but I just fell for
the oldest trick in the book
Fixes a crash on GTK when a file is dragged then dropped into the
terminal - should not impact 1.2 as this is a casualty of the Zig 0.15
port
This fixes an issue I have on both macOS and Linux (ARM and x86_64)
where stack traces are broken for inlined functions. They don't point to
the proper location in the source code, making debugging difficult.
Release builds use the same previous function.
cc @qwerasd205
This fixes an issue I have on both macOS and Linux (ARM and x86_64)
where stack traces are broken for inlined functions. They don't point to
the proper location in the source code, making debugging difficult.
Release builds use the same previous function.
### Background
After #9344, the Ghostty theme won't change after switching systems', and reverting #9344 will bring back the issue it fixed.
The reason these two issues are related is because the scheme change is based on changes of `effectiveAppearance`, which is also affected by setting the window's `appearance` or changing `NSAppearance.currentDrawing()`.
### Changes
Instead of observing `effectiveAppearance`, we now explicitly update the color scheme of surfaces, so that we can control when it happens to avoid callback loops and redundant updates.
### Regression Tests
- [x] #8282
- [x] Reloading with `window-theme = light` should update Ghostty with the default dark theme with a dark window theme (break before [#83104ff](83104ff27a))
- [x] `window-theme = light \n macos-titlebar-style = native` should update Ghostty with the default dark theme with a light window theme
- [x] Reloading from the default config to `theme=light:3024 Day,dark:3024 Night \n window-theme = light`, should update Ghostty with the theme `3024 Day` with a light window theme (break on [#d39cc6d](d39cc6d478))
- [x] Using `theme=light:3024 Day,dark:3024 Night`; Switching the system's appearance should change Ghostty's appearance (break on [#d39cc6d](d39cc6d478))
- [x] Reloading from `theme=light:3024 Day,dark:3024 Night` with a light window theme to the default config, should update Ghostty with the default dark theme with a dark window theme
- [x] Reloading from the default config to `theme=light:3024 Day,dark:3024 Night \n window-theme=dark`, should update Ghostty with the theme `3024 Night` with a dark window theme
- [x] Reloading from `theme=light:3024 Day,dark:3024 Night \n window-theme=dark` to `theme=light:3024 Day,dark:3024 Night` with light system appearance, should update Ghostty from dark to light
- [x] Reload with quick terminal open
# Conflicts:
# macos/Sources/Features/Terminal/BaseTerminalController.swift
This should save on CI quite a bit. This will cancel our GHA runs when
you push to the same ref, except for `main`, where I want to make sure
every commit is tested.
This should save on CI quite a bit. This will cancel our GHA runs when
you push to the same ref, except for `main`, where I want to make sure
every commit is tested.
This was found by LLM hunting! We were not holding the lock properly
during these operations. There aren't any known cases where we can
directly attribute these races to issues but we did find at least one
consistent crash for a user when `linkAtPos` wasn't properly locked (in
another PR).
This continues those fixes.
https://ampcode.com/threads/T-6fc49f25-7f2f-4039-adb4-f86aaeced6d5
This was found by LLM hunting! We were not holding the lock properly
during these operations. There aren't any known cases where we can
directly attribute these races to issues but we did find at least one
consistent crash for a user when `linkAtPos` wasn't properly locked (in
another PR).
This continues those fixes.
From #9812
I'm not sure if this is the root cause of the crash in #9812 but the
LLM-discovered issue that we are not holding a lock here appears to be a
real issue. I manually traced the code paths and thought about this and
looked where we call `mouseRefreshLinks` in other places and this
appears to be a real bug.
From #9812
I'm not sure if this is the root cause of the crash in #9812 but the
LLM-discovered issue that we are not holding a lock here appears to be a
real issue. I manually traced the code paths and thought about this and
looked where we call `mouseRefreshLinks` in other places and this
appears to be a real bug.
Continuing just build foundation for #1935. There are no user-facing
changes here.
- This adds more command parsing for control mode output. I _believe_ we
now parse enough to at least render everything initially for most tmux
sessions. At least, from the VT parsing side (we haven't done any GUI
work whatsoever to react to this).
- This also adds a new layout string parser, e.g.
`159x48,0,0{79x48,0,0,79x48,80,0}` into a useful struct. We'll need this
to convert our visible layouts into actual GUI components, eventually.
- This adds a new output format parser for commands such as
`list-windows`. Control mode sends output framed in `%begin/%end`
blocks, but the output is in an arbitrary format depending on the input
command and unrelated to control mode itself. The output format parser
will let us parse this output.
I think this is a good place to stop and PR. I think next up I might try
hooking up a state machine to `src/termio/stream_handler.zig` to start
actually working with this. Before that, it may behoove me to change
`stream_readonly` to support non-readonly operations via callback so I
can do DCS and maybe unit test this whole thing too.... we'll see where
the wind blows.
**AI disclosure:** AI wrote many tests and did a lot of implementation
here. I reviewed everything manually though and understand it all
completely.
If the BEL character is received too frequently, the GUI thread can be
starved and Ghostty will lock up and eventually crash. This PR limits
BEL handling to 1 per 100ms.
Fixes#9800.
If the BEL character is received too frequently, the GUI thread can be
starved and Ghostty will lock up and eventually crash. This PR limits
BEL handling to 1 per 100ms.
Fixes#9800.
## Problem
When ghostty is used as a dependency (e.g., in another Zig project), the
build can panic on Linux with:
\`\`\`
GhosttyDist.zig:173:34: 0x1768046 in exists (build.zig)
if (std.fs.accessAbsolute(b.pathFromRoot(self.dist), .{})) {
\`\`\`
This happens because \`b.pathFromRoot()\` can return a relative path
(e.g., when the build root is \`.\`), but \`std.fs.accessAbsolute()\`
asserts the path must be absolute.
## Solution
Replace \`std.fs.accessAbsolute(b.pathFromRoot(...))\` with
\`b.build_root.handle.access(...)\`, which uses the build root's
directory handle directly and works correctly with relative paths.
Context:
https://ampcode.com/threads/T-7511e7e4-4b4a-4f11-9c3c-817aaa519b84
The main thing to emphasize is that end users should never source
.zshenv directly; it's only meant to be used as part of our shell
injection environment.
At the moment, there's no way to guard against accidentally use, but we
can consider making e.g. GHOSTTY_SHELL_FEATURES always defined in this
environment to that it can be used to differentiate the cases.
In practice, it's unlikely that people actually source this .zshenv
script directly, so hopefully this additional documentation clarifies
things well enough.
The ghostty-integration script can be manually sourced, and it uses the
Zsh 5.1+ features, so that's a better place to guard against older Zsh
versions.
This also keeps the .zshenv script focused on just bootstrapping our
automatic shell integration.
I also changed the version check to a slightly more idiomatic pattern.
The ghostty-integration script can be manually sourced, and it uses the
Zsh 5.1+ features, so that's a better place to guard against older Zsh
versions.
This also keeps the .zshenv script focused on just bootstrapping our
automatic shell integration.
I also changed the version check to a slightly more idiomatic pattern.
Replace std.fs.accessAbsolute(b.pathFromRoot(...)) with
b.build_root.handle.access(...) since pathFromRoot can return
relative paths, but accessAbsolute asserts the path is absolute.
The main thing to emphasize is that end users should never source
.zshenv directly; it's only meant to be used as part of our shell
injection environment.
At the moment, there's no way to guard against accidentally use, but we
can consider making e.g. GHOSTTY_SHELL_FEATURES always defined in this
environment to that it can be used to differentiate the cases.
In practice, it's unlikely that people actually source this .zshenv
script directly, so hopefully this additional documentation clarifies
things well enough.
I think it makes sense that if there was already text from the previous
search that it should match based on that, this doesnt remove that it
highlights everything so you can press backspace to delete and start
from a empty search
Fixes#189
This adds the UI for search to GTK. There is still polish to be done in
follow-ups but this makes search work well with GTK to start!
**AI disclosure:** Believe it or not, almost this entire PR was
AI-written. Amp did an excellent job looking at our existing codebase,
comparing it to the macOS codebase, writing blueprint files, etc. I
reviewed everything written, modified some basics, and verified it
manually and under Valgrind.
## Demo
<img width="1654" height="1234" alt="CleanShot 2025-11-29 at 20 52
11@2x"
src="https://github.com/user-attachments/assets/0eb38367-398f-4165-9838-7a35465857bc"
/>
## Future Improvements
- When dragging the overlay, we should change the cursor and show drop
targets
- There's probably some small stylistic tweaks we can make to this
- I'm not sure the CSS is right for both light and dark modes so we may
need to tweak that
This fixes memory corruption where future matches on a fully dirty row
would write highlights out of bounds. It was easy to reproduce in debug
by searching for `$` in `ghostty +boo`
When columns shrink during resize-without-reflow, page.size.cols is
updated but page.capacity.cols retains the old larger value. When
adjustCapacity later runs (e.g., to expand style/grapheme storage),
it was creating a new page using page.capacity which has the stale
column count, causing size.cols to revert to the old value.
This caused a crash in render.zig where an assertion checks that
page.size.cols matches PageList.cols.
Fix by explicitly copying page.size.cols to the new page after
creation, matching how size.rows is already handled.
Amp-Thread-ID: https://ampcode.com/threads/T-976bc49a-7bfd-40bd-bbbb-38f66fc925ff
Co-authored-by: Amp <amp@ampcode.com>
This fixes memory corruption where future matches on a fully dirty row
would write highlights out of bounds. It was easy to reproduce in debug
by searching for `$` in `ghostty +boo`
not an accepted issue
https://github.com/ghostty-org/ghostty/discussions/9745 but it seemed
simple enough to add, pretty much copies how the previewer sorts based
on color
This allows --color argument to work when using --plain to sort the list
returned
Fixes#2660
Rather than calculate our window frame size based on various chrome
calculations, we now utilize SwiftUI layouts and view intrinsic content
sizes with `setContentSize` to setup our content size ignoring all our
other widgets.
I'm sure there's some edge cases I'm missing here but this should be a
whole lot more reliable on the whole.
Fixes#2660
Rather than calculate our window frame size based on various chrome
calculations, we now utilize SwiftUI layouts and view intrinsic content
sizes with `setContentSize` to setup our content size ignoring all our
other widgets.
I'm sure there's some edge cases I'm missing here but this should be a
whole lot more reliable on the whole.
This fixes regression of #5690, which kind of comes from #9576. 05b42919d5 (before #9576) has weird behaviours too, restored windows are not properly focused. With this pr, we only order `selectedWindow` front so we won't mess up with its selection state and the order of the tab group.
This means that the pin we're using to track our position in the
PageList was part of a node that got reused/recycled at some point. We
can't make any meaningful guarantees about the state of the PageList.
This only happens with scrollback pruning so we can treat it as a
complete search.
This means that the pin we're using to track our position in the
PageList was part of a node that got reused/recycled at some point. We
can't make any meaningful guarantees about the state of the PageList.
This only happens with scrollback pruning so we can treat it as a
complete search.
Swift conveniently converts strings to UTF-8 encoded cstrings when
passing them to external functions, however our libghostty functions
also take a length and we were using String.count for that, which
returns the number of _characters_ not the byte length, which caused
searches with multi-byte characters to get truncated.
I went ahead and changed _all_ invocations that pass a string length to
use the utf-8 byte length even if the string is comptime-known and all
ASCII, just so that it's proper and if someone copies one of the calls
in the future for user-inputted data they don't reproduce this bug.
ref:
https://developer.apple.com/documentation/swift/string/counthttps://developer.apple.com/documentation/swift/stringprotocol/lengthofbytes(using:)
Swift conveniently converts strings to UTF-8 encoded cstrings when
passing them to external functions, however our libghostty functions
also take a length and we were using String.count for that, which
returns the number of _characters_ not the byte length, which caused
searches with multi-byte characters to get truncated.
I went ahead and changed _all_ invocations that pass a string length to
use the utf-8 byte length even if the string is comptime-known and all
ASCII, just so that it's proper and if someone copies one of the calls
in the future for user-inputted data they don't reproduce this bug.
ref:
https://developer.apple.com/documentation/swift/string/counthttps://developer.apple.com/documentation/swift/stringprotocol/lengthofbytes(using:)
Move the search result counter inside the search text field using an
overlay, preventing layout shift when results appear.
**Before:** The counter appeared as a separate element in the HStack,
causing the text field to shift when results loaded.
**After:** The counter is overlaid inside the text field on the right
side with reserved padding, eliminating layout shift.
---
**AI Disclosure: The was entirely authored with Claude Code,
specifically with Claude Opus 4.5.**
Move the search result counter (e.g. "1/30") inside the search text
field using an overlay, preventing layout shift when results appear.
This PR was authored with Claude Code.
#189 for macOS
This adds a search GUI for macOS, including macOS-standard menu bar
items, and keybindings that match other native macOS applications such
as Terminal app, Safari, and others. This introduces a new keybinding
action `start_search` to start a blank search for GUIs.
This PR also resolves a number of minor issues found in the search
subsystem and renderer related to search from prior PRs. This should
result in overall improved search stability. **Please note there are
still known issues (bottom of this PR).**
> [!WARNING]
>
> **A note on stability:** I know a lot of people are eager to have this
feature, and I'm excited
> for this feature to soon be available in tip releases. But note that
new features like this are
> always filled with performance issues, bugs, crashes, etc. That's the
point of tip releases:
> to find and address these before wider availability. We will do our
best to respond rapidly to
> major issues found in tip, but don't expect perfect functionality
immediately!
## Demo
https://github.com/user-attachments/assets/3b81752e-d7e5-4875-9864-92497333b23e
> [!NOTE]
>
> You can drag the search window to any of the four corners if its
getting in the way of reading the terminal.
## Known Issues
TODO prior to this PR merging:
- [x] Single-byte search terms cause a crash since our sliding window
can't handle it. This PR temporarily requires search terms with length
2+ before starting a search to avoid it. 😄
- [x] The way `ScreenSearch` prunes history is fundamental unsafe. With
a rapidly growing screen that could reach history limits and an active
search at the same time, Ghostty is almost guaranteed to crash
currently. The workaround is to not search actively scrolling screens
(new data) for now.
- ScreenSearch has to restart on resize. We don't have any good way to
handle reflow since our search results are unpinned. We can look into
this later but this fixes correctness.
- PageList now tracks serial number by node that monotonically increases
on any alloc or reuse. Our screen search uses this to prune invalid
history results.
When pasting text in GTK, the current version properly prioritizes
text/plain;charset=utf-8 when the content is offered by another
application, but when pasting from ghostty to itself the mime type
selection algorithm prefers the offer order and matches `text/plain`,
which then converts non-ASCII UTF-8 into a bunch of escaped hex
characters (e.g. 日本語 becomes \E6\97\A5\E6\9C\AC\E8\AA\9E)
This is being discussed on the GTK side[1], but until everyone gets an
updated GTK it cannot hurt to offer the UTF-8 variant first (and one of
the GTK dev claims it actually is a bug not to do it, but the wayland
spec is not clear about it, so other clients could behave similarly)
Link: https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/9189 [1]
Fixes#9682
When pasting text in GTK, the current version properly prioritizes
text/plain;charset=utf-8 when the content is offered by another
application, but when pasting from ghostty to itself the mime type
selection algorithm prefers the offer order and matches `text/plain`,
which then converts non-ASCII UTF-8 into a bunch of escaped hex
characters (e.g. 日本語 becomes \E6\97\A5\E6\9C\AC\E8\AA\9E)
This is being discussed on the GTK side[1], but until everyone gets an
updated GTK it cannot hurt to offer the UTF-8 variant first (and one of
the GTK dev claims it actually is a bug not to do it, but the wayland
spec is not clear about it, so other clients could behave similarly)
Link: https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/9189 [1]
Fixes#9682
Continuing #189
This adds the `navigate_search:previous` and `next` key bindings which
allow search matches to be navigated. The currently selected search
match is highlighted using a new `search-selected-foreground/background`
configuration with a reasonable default.
As search results are navigated, the viewport moves to keep them
visible.
## Demo
https://github.com/user-attachments/assets/facc9f3e-e327-4c65-b5f7-0279480ac357
This causes linker issues for some libghostty users. I don't know why we
never saw these issues with Ghostty release builds, but generally
speaking I think its fine to do this for 3rd party code unless we've
witnessed an issue. And these deps have been stable for a long, long
time.
It's clear from the surrounding code that the RGB values returned from
`cell_text_vertex` are expected to be linearized. This was not the case
for the cursor cell, resulting in the cursor text being too bright when
using `cursor-text = cell-background`.
Fixes #8353/#9138.
xref #8960, which would also fixe this issue, but I don't know where
that's heading
This causes linker issues for some libghostty users. I don't know why we
never saw these issues with Ghostty release builds, but generally
speaking I think its fine to do this for 3rd party code unless we've
witnessed an issue. And these deps have been stable for a long, long
time.
The march towards #189 continues.
This hooks up the search thread to the main surface and all the state
necessary for the renderer to show search results in the viewport! This
also adds a `search` binding which takes a query to start/stop a search.
**This still doesn't add any search GUI,** which will come later, the
internals must happen first.
A non-blank binding will start or change the search term. An empty
binding will stop search:
```
keybind = cmd+f=search:Hello
keybind = shift+cmd+f=search:
```
> [!NOTE]
>
> Obviously, search will eventually have a GUI. The point of this PR is
primarily to connect all the various internal systems more than
anything. GUI will come soon.
## Demo
https://github.com/user-attachments/assets/06af5a3b-280e-4804-b506-419b92a95f99
## Major Changes
The only major changes required as part of this is the introduction of
what I'm calling the terminal "highlight" system. This is a generic
system for highlighting portions of the terminal contents. These will
ultimately underpin what we currently call "selections" (selecting text
with your mouse/keyboard) but that is far too large a change to make in
one PR.
Therefore, this PR introduces highlights and the only consumer is the
entire search subsystem.
## Limitations
Still plenty of limitations we need to keep marching towards resolving:
- ~~Search matches are styled the same as selections. They should be
different.~~
- There is no way to iterate search matches, yet.
- There is no GUI for inputting a search query or viewing total matches,
yet.
- ~~I think searches are case-sensitive currently and they should
probably not be.~~ Done, for ASCII.
But hey, it's something!
These now properly match the FreeType API- compared directly in the unit
tests against the values provided by the FreeType header itself.
This was ridiculously wrong before, like... wow.
FcLangSetHasLang returns FcLangResult enum values:
- FcLangEqual (0): Exact match
- FcLangDifferentTerritory (1): Same language, different territory
- FcLangDifferentLang (2): Different language
The previous comparison to FcTrue (1) caused:
- Exact matches (0) to incorrectly return false
- Partial matches (1) to incorrectly return true
This fix changes the comparison to FcLangEqual (0) so hasLang()
correctly returns true only for exact language matches.
Fixes emoji font detection which relies on checking for 'und-zsye'
language tag support.
## Summary
Adds a new environment variable `GHOSTTY_QUICK_TERMINAL=1` that is set
when a terminal surface is launched as a quick terminal. This allows
shell configurations to conditionally adjust behavior based on whether
the terminal is a quick terminal.
Closes#3985
<img width="1430" height="228" alt="CleanShot 2025-11-23 at 12 39 27
png"
src="https://github.com/user-attachments/assets/1085c19b-9d58-4603-a03d-662bfde47095"
/>
## Motivation
Quick terminals are designed as ephemeral, singleton windows for quick
commands. Many users (especially tmux users) have shell configurations
that automatically attach to or start tmux sessions on terminal launch.
This automatic behavior conflicts with the quick terminal use case.
Example from the issue:
```zsh
# Users want to skip this in quick terminals
if [[ -z "$TMUX" ]]; then
tmux attach || tmux new
fi
With this PR, users can now detect quick terminals and adjust their shell behavior:
if [[ -z "$TMUX" ]] && [[ -z "$GHOSTTY_QUICK_TERMINAL" ]]; then
tmux attach || tmux new
fi
```
## Implementation
- macOS: Added GHOSTTY_QUICK_TERMINAL=1 to SurfaceConfiguration
environment variables in QuickTerminalController.swift:345-351
- Linux/GTK: Added environment variable check in
src/apprt/gtk/class/surface.zig:1469-1472 within defaultTermioEnv()
The environment variable is automatically inherited by split surfaces.
## Note on Scripting API
I'm aware that @rhodes-b mentioned this functionality was planned for
the scripting API (issue #3985). However, I believe this simpler
environment variable approach provides immediate value for tmux users
and shell configuration use cases, while the scripting API can later
provide more comprehensive surface introspection capabilities.
## AI Assistance Disclosure
This PR was written with Claude Code assistance. The Linux/GTK
implementation was fully AI-generated. I implemented the mac version
myself after Claude Code helped me understand the codebase architecture
and locate the appropriate files.
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 fixes an issue with the VS15/VS16 handling in terminal `print`,
where before we didn't check for valid sequences if the base code point
is an emoji. This meant that a VS15 after an emoji that isn't part of a
valid sequence would narrow the cell, despite that the emoji doesn't
have a text presentation.
This PR also uses a single `is_emoji_vs_base` instead of the
`is_emoji_vs_emoji` and `is_emoji_vs_text`. See the [uucode
comment](215ff09730/src/config.zig (L239-L254))
for why I'm recommending this.
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.
This updates uucode to the latest version, with the following changes:
b309dfb4e2...31655fba3c
For this PR, the new `uucode` version has two changes that contribute to
the diff:
* The `grapheme_break` now includes `emoji_modifier` and
`emoji_modifier_base`, so we don't need to grab those from
`is_emoji_modifier` and `is_emoji_modifier_base`.
* The `wcwidth` calculation has been split into `wcwidth_standalone`
(width of a code point when it's the only code point in a grapheme) and
`wcwidth_zero_in_grapheme` (whether the code point is _not_ contributing
to the width of a multi-code-point grapheme). To keep the current
Ghostty behavior for this PR, this sets `width` to 0 if
`wcwidth_zero_in_grapheme`, but with the exception of
`is_emoji_modifier` (see comment).
* While this PR isn't affected by it, take a look at
[wcwidth.zig](https://github.com/jacobsandlund/uucode/blob/main/src/x/config_x/wcwidth.zig)
for the considerations that went into determining the width of a code
point, along with many comments.
* See
[x/grapheme.zig](https://github.com/jacobsandlund/uucode/blob/main/src/x/grapheme.zig)
for the calculation of the width of a grapheme based on the width of the
code points with exceptions, again with many comments.
* See
[resources/wcwidth](https://github.com/jacobsandlund/uucode/tree/main/resources/wcwidth)
for a comparison of other unicode libraries calculation of "wcwidth".
PRs will follow this in a moment to also take advantage of the new
`uucode` version for:
* Better grapheme segmentation
* Correct VS15/VS16 handling
* More correct cell width calculation (especially certain scripts such
as Devanagari)
This makes `cursorStyle` utilize `RenderState` to determine the
appropriate cursor style. This moves the cursor style logic outside the
critical area, although it was cheap to begin with.
This always removes `viewport_is_bottom` which had no practical use.
This makes `cursorStyle` utilize `RenderState` to determine the
appropriate cursor style. This moves the cursor style logic outside the
critical area, although it was cheap to begin with.
This always removes `viewport_is_bottom` which had no practical use.
This adds a new API called `RenderState` that produces the proper state
required for a renderer to draw a viewport and updates our renderer to
use it instead of the prior screen clone method.
The newsworthy change is here is that we've shortened the critical area
where the renderer holds the terminal lock and blocks IO by anywhere
from **2x to 5x faster**, and in about half the frames we're now in the
critical area for **zero microseconds** because `RenderState` has much
better dirty/damage tracking.
**For libghostty Zig users**, this API is available to the Zig module
and can be used to create your own renderers (to any artifact, it
doesn't have to be graphical! It is even useful for text like a tmux
clone or something).
## Differences vs Old Method
The renderer previously called `Screen.clone`. This produces a copy of
all the screen data (even stuff the renderer may not need) and attempts
to create a standalone, fully functional `Screen` allocation. I didn't
microbenchmark this to understand exactly where the slowdown was, but I
think it was based primarily in two places:
- Screens have a minimum allocation of two Ghostty "pages" which are
quite large. I think this allocation was the primary expensive part.
Without refactoring our entire Screen/PageList work, this was
unavoidable.
- The clone was unconditional and would produce a fully new Screen on
every frame.
The new structure `RenderState` is stateful and each frame calls
`update` on the prior value and it modifies itself in place. This
addresses the above two points in a few ways:
- Allocation is minimized to only what we need, no full pages.
- Since it is stateful (updating in-place), we only change dirty data
and don't need to copy everything. **Note the benchmarks below only test
_full updates_, so you aren't even seeing the benefits of this!**
- Also since it is stateful, we cache expensive calculations (such as
for selections) so future screen updates can reuse those cached values
rather than recompute them.
- We retain memory from prior updates even when the screen is dirty, so
even in dirty states, it's unlikely we allocate.
More details in the "Design" section.
## Benchmarks
Here are some microbenchmarks on the previous render critical area
versus the new one.
For each of the benchmarks below, ignore the time units (milliseconds)
and instead **focus on the relative speedup.** The benchmarks are all
doing a full render frame setup around 1000 times, because the actual
cost of a frame update is in dozens or hundreds of microseconds.
> [!NOTE]
>
> I'm still working on some more benchmarks before merging. I'll update
this space.
### Full screen, single style
<img width="1542" height="480" alt="CleanShot 2025-11-21 at 07 35 13@2x"
src="https://github.com/user-attachments/assets/c28c9f33-d1aa-4723-8a8e-3c6d70fe3667"
/>
### Full screen, plaintext
<img width="1642" height="612" alt="CleanShot 2025-11-21 at 07 36 06@2x"
src="https://github.com/user-attachments/assets/b51f57cf-7c48-46c8-a347-8ecc0bdd3d47"
/>
### Full screen, different style per cell (pathological case)
<img width="1704" height="456" alt="CleanShot 2025-11-21 at 07 37 55@2x"
src="https://github.com/user-attachments/assets/71a98250-d8d1-47ab-ae69-5e6b3b60bf2d"
/>
### Critical Area: Typing at Shell Prompt
| This PR | Main Branch |
---------|--------
| <img width="1064" height="764" alt="CleanShot 2025-11-21 at 14 44
31@2x"
src="https://github.com/user-attachments/assets/8a0ab3a1-3d68-41f0-9469-bc08a4569286"
/> | <img width="1040" height="684" alt="CleanShot 2025-11-21 at 14 47
10@2x"
src="https://github.com/user-attachments/assets/04ffa607-8841-436b-b6e9-eeeb6ee9482d"
/> |
### Critical Area: Neovim Scrolling
| This PR | Main Branch |
---------|--------
| <img width="1054" height="748" alt="CleanShot 2025-11-21 at 14 45
06@2x"
src="https://github.com/user-attachments/assets/ccafaee8-720f-41be-820d-fd705835607a"
/> | <img width="1068" height="796" alt="CleanShot 2025-11-21 at 14 47
48@2x"
src="https://github.com/user-attachments/assets/68087496-d371-4c7c-8b4c-b967dbaeaa7c"
/> |
### Critical Area: `btop -u 100`
This is closer to a pathological case, about as close as you get with a
real tool in the wild. `btop` uses hundreds of unique styles and updates
many cells across many rows very frequently (every 100ms in this case).
You can see that some of our frame times in this case are similar but
there are _so many more near-idle frames_ thanks to our dirty tracking.
| This PR | Main Branch |
---------|--------
| <img width="1088" height="900" alt="CleanShot 2025-11-21 at 14 45
44@2x"
src="https://github.com/user-attachments/assets/ea63f0eb-f06e-4d00-95a3-c55a3755cc67"
/> | <img width="1078" height="906" alt="CleanShot 2025-11-21 at 14 48
18@2x"
src="https://github.com/user-attachments/assets/cef360de-2b12-440f-8c4c-6a69b5ce4058"
/> |
### "DOOM Fire"
Fullscreen on my macOS when from 770 FPS to ~808 FPS consistently, a
solid 5% increase repeatedly.
<img width="3520" height="2392" alt="CleanShot 2025-11-21 at 07 45
29@2x"
src="https://github.com/user-attachments/assets/033effca-0abb-4ff8-a21b-83214d118d12"
/>
### IO
While this was rendering focused, the smaller critical area does help IO
performance a bit.
We've already tracked down the remaining issues to the IO thread going
to sleep and overhead with context switching. We're investigating
switching to a spin lock for the IO thread only in another track of
work.
> [!NOTE]
>
> **This is comparing with `main`, which already has a 20-30%
performance improvement over v1.2.3.**
<img width="982" height="698" alt="image"
src="https://github.com/user-attachments/assets/52a86f6c-6f09-45fe-9ac7-ca62c7ac6ee4"
/>
## Design
The design of the API is a _stateful_ `RenderState` struct that you call
`update` on each frame with the target terminal, and it only updates
what is needed. RenderState keeps track of rows, cells, hyperlinks,
selections, etc.
```zig
// Start empty
var state: terminal.RenderState = .empty;
defer state.deinit(alloc);
// Each frame update it with a terminal.
// THIS IS THE ONLY PART THAT ACCESS `t`
try state.update(alloc, &t);
// Access render data (can be outside any locking for `t`)
...
```
The ergonomics of the `RenderState` structure a wee bit clunky because
we make use of struct-of-arrays (SoA, Zig's MultiArrayList) to better
optimize cache locality for renderer data vs. update data (what we need
to update the render state is different from what we need to draw).
Once you get used to the API though, it's pretty beautiful. I mean, look
at this:
```zig
for (
0..,
row_data.items(.raw),
row_data.items(.cells),
) |y, row, cells| {
const cells_slice = cells.slice();
for (
0..,
cells_slice.items(.raw),
cells_slice.items(.grapheme),
) |x, cell, graphemes| {
```
## Improvements
This PR makes various improvements across the board:
- It bears repeating in case it was missed previously that the critical
area time of a render has gone down 2x to 5x when there is work and is
now free when there is no work (the previous implementation always did
work).
- Font shaping is much more efficient now and only requires access to a
render state.
- Selection handling is now cached and works with dirty tracking.
Previously, if you had an active selection, we'd search the entire
screen multiple times (like... once per row). Yikes.
- Hyperlink handling is _much_ more efficient. Instead of iterating
through the entire screen contents _per configured link_ we now cache
the screen contents as a string and search one whole string multiple
times. Obvious, but we didn't do this before.
- The `contrainedWidth` and `rowNeverExtendBg` helper methods are now
both much more efficient and live within the renderer package rather
than being awkwardly in the terminal package.
## Future Notes
- Our `terminal.Selection` API is very bad. It conceptually makes sense
and I understand why I designed it this way (easy) but it makes it hard
to render or manipulate performantly.
**AI Disclosure:** AI was used only to assist with writing some tests
and converting some tests. The primary logic is all organic, meatbag
produced.
This change should give more consistent results between high and low DPI
displays, and generally approximate the authorial intent of the metrics
a little better.
Also changed the cell height adjustment to prioritize the top or bottom
when adjusting by an odd number depending on whether the face is higher
or lower in the cell than it "should" be. This should make it easier for
users who have an issue with a glyph protruding from the cell to adjust
the height and resolve it.
The code that re-creates the font descriptor from scratch using the same
attributes also rubs off the magic dust that makes CoreText not throw a
fit at us for using a "hidden" system font (name prefixed with a dot) by
name when we use the descriptor. This means that a small subset of chars
that only have glyphs in these fallback system fonts like ".CJK Symbols
Fallback HK Regular" and ".DecoType Nastaleeq Urdu UI" would not be able
to be rendered, since when we requested the font with the non-magical
descriptor CoreText would complain in the console and give us Times New
Roman instead.
Using `CTFontDescriptorCreateCopyWithAttributes` to clear the charset
attribute instead of recreating from scratch makes the copy come out
magical, and CoreText lets us instantiate the font from it, yippee!
### ℹ️ For anyone who came to this PR from a search engine rabbit hole
trying to figure this out for your own code:
It seems like if you want to use one of these fonts, you have to avoid
creating a new descriptor from scratch with the name. To create a
descriptor with one of these names that CoreText won't get upset about,
you have to get it in one of these ways (there may be others but these
are ones that definitely work at time of writing):
- Get it from `CTFontCreateForString`(`WithLanguage`) with a character
only provided by that font.
- Get it from `CTFontCreateUIFontForLanguage` or any of the mechanisms
for generally receiving a system font that you don't necessarily have
full control over the exact choice of font with.
- Get it from a collection created with
`CTFontCollectionCreateMatchingFontDescriptors`
- Get it from `CTFontCreateWithGraphicsFont` ... and it seems that for
now, `CGFontCreateWithFontName` lets you use these "hidden" names
without complaining :)
I wouldn't trust any of these methods to necessarily *continue* working
in to the future, since Apple seems to be trying to dissuade
*intentional* use of any particular named hidden system font.
Realistically, the `CreateForString` option is the most likely to stick
around.
If all else fails, check FireFox or Chrome's source code to see how
they're solving it in current year.
And to help it get indexed for those looking for it...
<details>
<summary>CoreText's angry message</summary>
```
CoreText note: Client requested name ".CJKSymbolsFallbackHK-Regular", it will get TimesNewRomanPSMT rather than the intended font. All system UI font access should be through proper APIs such as CTFontCreateUIFontForLanguage() or +[NSFont systemFontOfSize:].
CoreText note: Set a breakpoint on CTFontLogSystemFontNameRequest to debug.
```
</details>
Draw proper anti-aliased dots now instead of rectangles, thanks to z2d
this is very easy to do, and the results are very nice, no more weird
gaps in dotted underlines if your cell is the wrong number of pixels
across.
Use z2d to draw the undercurl instead of the manual raster code we had
before- the code was cool but unnecessarily complicated. Plus z2d lets
us have rounded caps on the undercurl which is neat.
Also make sure we won't draw off the canvas with our underlines-- the
canvas has padding but it's not infinite.
This change should give more consistent results between high and low DPI
displays, and generally approximate the authorial intent of the metrics
a little better.
Also changed the cell height adjustment to prioritize the top or bottom
when adjusting by an odd number depending on whether the face is higher
or lower in the cell than it "should" be. This should make it easier for
users who have an issue with a glyph protruding from the cell to adjust
the height and resolve it.
Add descriptions to fish shell completions
Claude Code used to understand the codebase and reason through the edge
cases (several iterations)
- Using first sentence from the Config to add description for each
config, even if sentence spans few lines
- Several options can share the same description, code doesn't duplicate
description, see screenshot in the posted issue thread below
Fixes [#9531](https://github.com/ghostty-org/ghostty/issues/9531)
The code that re-creates the font descriptor from scratch using the same
attributes also rubs off the magic dust that makes CoreText not throw a
fit at us for using a "hidden" system font (name prefixed with a dot) by
name when we use the descriptor. This means that a small subset of chars
that only have glyphs in these fallback system fonts like ".CJK Symbols
Fallback HK Regular" and ".DecoType Nastaleeq Urdu UI" would not be able
to be rendered, since when we requested the font with the non-magical
descriptor CoreText would complain in the console and give us Times New
Roman instead.
Using `CTFontDescriptorCreateCopyWithAttributes` to clear the charset
attribute instead of recreating from scratch makes the copy come out
magical, and CoreText lets us instantiate the font from it, yippee!
A whole bunch of optimizations in hot paths in the IO processing areas
of our code (well, one of them covers everything). I validated that each
commit either improved one or more of our vtebench results, or improved
the time it takes to process 2 years worth (2.4GB) of data from
asciinema.
## vtebench
<img width="1278" height="903" alt="image"
src="https://github.com/user-attachments/assets/bad46777-4606-4870-b7d7-8df0c4bb3b39"
/>
(I decided to patch vtebench to report in nanoseconds instead of
milliseconds since clearly it was not designed for a machine as fast as
mine. Nanoseconds gives much more useful results when the numbers are
this low.)
Do note the *slight* regression in the "unicode" test, this is probably
because I added a branch hint in `Terminal.print` in order to optimize
for printing narrow characters, since they make up the vast majority of
characters typically printed in the terminal, but the vtebench "unicode"
test is pretty much all wide characters.
This shouldn't have a negative effect on users of CJK languages since
it's a *very* slight reduction in speed and they will still be printing
many narrow characters, especially in TUIs; spaces, box drawing
characters, symbols, punctuation, etc.
## asciinema processing
I wrote a program that uses libghostty to push 2 years worth (2.4GB) of
data from publicly uploaded asciinema recordings in to the terminal as
fast as possible- since it's just libghostty, there's no renderer
overhead happening, it's just the core terminal emulation, effectively
everything that io-reader thread does if it didn't have wait for the
renderer ever.
On main, this took roughly 26.1–26.7 seconds to process, on this branch
it takes just 18.4–18.6 seconds, that's a ~30% improvement in raw IO
processing speed when processing real world data!
## Summary of changes
In order of commits:
- Fixed a bug that I hit when trying to have Ghostty process all that
asciinema data, in certain bad cases it was possible to accidentally
insert the `0` hyperlink ID in to a page, which would then cause a
lockup in ReleaseFast mode when trying to clone that page since the
string alloc would try to iterate `1..0` to allocate 0 chunks.
- I noticed in profiling Ghostty that `std.debug.assert` was showing up
in the profile, which it should not have been since its doc comment
promises that it will be optimized out in ReleaseFast- but evidently
something is wrong with Zig, or that comment's promise is based on an
expectation from LLVM that it fails to meet - but either way, by
replacing all uses of `assert` with a version that is explicitly marked
`inline`, that function call overhead in tight loops and hotpaths is
avoided. This change alone accounts for like a third of the IO
processing time improvement, though it had minimal impact on vtebench
scores.
- I optimized the SGR parser somewhat by adding branch hints and
removing the `.reset_underline` action, replacing it with `.{ .underline
= .none }`.
- Gated a somewhat expensive assert in RefCountedSet behind a runtime
safety check.
- Improved the performance of `Style.eql` and `Style.hash` since these
are hot functions, called extremely frequently since adding styles to
the style set is a very common operation. Achieved this by making `eql`
less generic - explicitly comparing each part of the style rather than
looping over fields - and ordering checks from most likely to differ to
least likely to differ so that differences can be found as soon as
possible; and changed the hash from xxhash to simply folding the packed
struct down to 64 bits and then using `std.hash.int`. Also manually
inlined the code from `std.meta.activeTag` in `Packed.fromStyle`, since
profiling showed it in the callstack and it's a single cast so it really
should not have the function call overhead.
- Explicitly marked some trivial functions as inline, the optimizer
would already have been doing this (probably) but doing it explicitly
gives the optimizer more time to spend on other things. Added cold
branch hints to "should be impossible" and error-returning paths that
should be very rare, and unlikely branch hints to a lot of "invalid"
paths- to optimize for receiving valid data.
- Removed a branch in the parser csi param action, just unconditionally
multiply by 10 before adding digit value, even if it's the first digit.
This codepath is rarely hit since we have a fast path for this in the
stream code, but the stream code already has this optimization so I just
copied it over.
- `CharsetState.charsets` used to be an `EnumArray`, but the
layout/access logic for that was less-than-ideal, and the access
functions were not inlining-- and these are very hot since we access
this for every single print, so I wrote a bespoke struct to hold that
info instead, gained a couple percent of IO perf with that.
- Added branch hints based on the data I derived from the asciinema
dump, which gave big boost to vtebench results, especially for the
cursor movement and dense cells tests (which makes sense, since cursor
movement and setting attributes both got `likely` hints :p) -- data at
https://github.com/qwerasd205/asciinema-stats
- This is probably the most invasive change in this PR: I removed the
dirty bitset from `Page` and replaced it with a dirty flag on each row,
for the majority of operations this is faster to write, since the row
being dirtied is probably already loaded and probably will be written to
for other changes as well. This gave a couple percent IO processing
improvement. The only exception is scrolling-type operations, which are
extremely efficient by just moving rows around with a single memmov, so
looping through the rows to mark each dirty slows them down, and indeed
after this change the scrolling benchmarks in vtebench regressed,
*however*...
- Added a "full page dirty" flag on `Page`, which is set when an
operation is performed that dirties most or all the rows in the page,
which is used for scrolling-type operations. This *does* make the dirty
tracking slightly less precise for these operations, but with the
caching and stuff we do in the renderer, I don't think `rebuildCells` is
a bottleneck, so rebuilding a few extra rows shouldn't hurt. After this
change, all the scrolling benchmarks in vtebench improved drastically.
- Tiny micro-improvements to RefCountedSet; streamlined the control flow
in `lookup`, added an unlikely branch hint in `insert` for the branch
that resurrects dead items since dead items aren't that common.
- Improve SGR parser performance again by using `@call(.always_inline`
to explicitly inline calls to `StaticBitSet.isSet` (for the separator
list), since I noticed they weren't being inlined, causing function call
overhead in a hotpath.
- I noticed that `clearGrapheme` and `clearHyperlink` would check every
cell in the row after they were done in order to update the
`grapheme`/`hyperlink` flag on the row if there were none left, which
isn't great since `clearCells` called these functions for multiple cells
in the same row back-to-back, which leads to a ton of excess work. I
separated the flag updating parts of these functions out and called them
only if necessary (if the cells being cleared were the full row then the
flag could unconditionally be set to false) and only after all the cells
were cleared. This gave a nice improvement to IO processing since
clearCells is evidently a very hot function.
- Removed inline annotations on `Page.clearGrapheme` and
`Page.clearHyperlink` in favor of inlining directly at the one callsite
that benefited from inlining, this improved IO processing speed.
- Inlined trivial function `Charset.table`.
- Inlined `size.getOffset` and `size.intFromBase` as they are both
trivial pointer math that often benefits from surrounding context.
---
If you'd like me to separate out the trivial improvements (branch hints,
inline annotations, 1-line changes) from the functionality-changing ones
(pretty much just the changes to dirty tracking), just let me know!
These were actually hurting performance lol, except in the places where
I added the `.always_inline` calls- for some reason if these functions
aren't inlined there it really messes up the top region scrolling
benchmark in vtebench and I'm not entirely certain why...
This PR partially addresses #4504 with a one-liner, all feedback is very
welcome.
### AI disclaimer
I used Claude Code to help navigate the various layers of the rendering
stack, to instrument a ton of intermediate now-deleted log statements,
and ultimately to identify and fix this bug. I directed it
conversationally, gave it experiments to run, audited all of its work,
threw most of it out, and finally landed on this extremely small and
simple change that fixes the issue _for me_ but I think for a lot of
other cases as well. The fix ended up being small, so hopefully it's
easy to review and discuss.
Details:
# Fix horizontal glyph spacing on non-Retina displays
On external monitors (1.0x scale, 72 DPI), text had excessive horizontal
spacing between glyphs. The issue was less noticeable on Retina displays
(2.0x scale, 144 DPI) due to higher pixel density, but the proportional
spacing error was identical.
## Before
Background is ghostty 1.2.3 from homebrew, foregreound is iTerm2:
<img width="862" height="938" alt="Screenshot 2025-10-31 at 3 04 47 PM"
src="https://github.com/user-attachments/assets/feff5279-05cc-4008-b2f5-8ea3b4d6d14b"
/>
## After
Background is ghostty tip + this change, foreground is iTerm2:
<img width="659" height="774" alt="Screenshot 2025-10-31 at 3 00 42 PM"
src="https://github.com/user-attachments/assets/702dc7f8-bb46-43ec-8156-f69d003e8a37"
/>
(my iTerm2 has some custom thickening / brightening you can see; that's
not a part of this change)
## Root Cause
The metrics calculation in Metrics.zig used `@ceil()` to round the cell
width from CoreText's glyph measurements, which created a mismatch
between cell width (which determines glyph positions) and the actual
glyph advances.
At 72 DPI with a 12pt font:
- CoreText returns glyph advance: 7.224609375 pixels
- Cell width was ceiled to: 8 pixels
- Gap per character: 0.78 pixels (~10.8% error)
At 144 DPI with the same font:
- CoreText returns glyph advance: 14.44921875 pixels (exactly 2x)
- Cell width was ceiled to: 15 pixels
- Gap per character: 0.55 pixels (~3.8% error)
The error at high DPI is much better than at low DPI, since the absolute
error is always no more than 1px.
## Fix
Changed `@ceil(face_width)` to `@round(face_width)`. This makes cell
width match the glyph advances better, reducing the error to at most
0.5px:
- 72 DPI: round(7.22) = 7
- 144 DPI: round(14.45) = 14
Height continues using `@ceil()` since vertical space can be slightly
larger without visual issues, but... should it? I'm not sure; this is a
good topic for discussion.
This improves the `clearCells` function since it only has to update once
after clearing all of the individual cells, or not at all if the whole
row was cleared since then it knows for sure that it cleared them all.
This also makes it so that the row style flag is properly tracked when
cells are cleared but not the whole row.
I am so sick and tired of people complaining that the build instructions
on the website are wrong when they clearly haven't realized the difference
between Git-based and tarball-based builds, so here's the extra work to
make sure people actually realize that
Bumps [actions/checkout](https://github.com/actions/checkout) from 5.0.0
to 5.0.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v5.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Port v6 cleanup to v5 by <a
href="https://github.com/ericsciple"><code>@ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v5...v5.0.1">https://github.com/actions/checkout/compare/v5...v5.0.1</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="93cb6efe18"><code>93cb6ef</code></a>
Cleanup actions/checkout@v6 auth style (<a
href="https://redirect.github.com/actions/checkout/issues/2301">#2301</a>)</li>
<li>See full diff in <a
href="08c6903cd8...93cb6efe18">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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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/install-nix-action](https://github.com/cachix/install-nix-action)
from 31.8.3 to 31.8.4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/cachix/install-nix-action/releases">cachix/install-nix-action's
releases</a>.</em></p>
<blockquote>
<h2>v31.8.4</h2>
<h2>What's Changed</h2>
<ul>
<li>nix: 2.32.3 -> 2.32.4 by <a
href="https://github.com/github-actions"><code>@github-actions</code></a>[bot]
in <a
href="https://redirect.github.com/cachix/install-nix-action/pull/261">cachix/install-nix-action#261</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/cachix/install-nix-action/compare/v31.8.3...v31.8.4">https://github.com/cachix/install-nix-action/compare/v31.8.3...v31.8.4</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="0b0e072294"><code>0b0e072</code></a>
Merge pull request <a
href="https://redirect.github.com/cachix/install-nix-action/issues/261">#261</a>
from cachix/create-pull-request/patch</li>
<li><a
href="16d2e3294d"><code>16d2e32</code></a>
nix: 2.32.3 -> 2.32.4</li>
<li>See full diff in <a
href="7ec16f2c06...0b0e072294">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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
Currently, the scroller's appearance is the same as the window's.
So, with the light system appearance and the following config:
```
window-theme = system
macos-titlebar-style = native
```
It's hard to see where the scroller is. This pr changes the
scroller’s(ScrollView) appearance to match the surface's background
colour, so it's always easier to find.
> Changing `verticalScroller?.appearance` doesn't seem to work
<img width="601" height="630" alt="image"
src="https://github.com/user-attachments/assets/9dc18439-9dcb-479a-802a-de439b7dc9d8"
/>
This adds a benchmark and some test coverage for a `screen-clone`
benchmark. This benchmarks the screen cloning which is a hot spot for
lock contention for the renderer + IO threads. I wasn't able to
meaningfully speed this up, but still want to commit this benchmark.
### Problem
Custom icon configuration (`macos-icon = custom-style` with
`macos-icon-screen-color`) stopped working, reverting to the default
icon.
### Root cause
The `ColorList.clone()` method only cloned the `colors` array but not
the `colors_c` array. The Swift code reads from `colors_c` via the C API
(`ghostty_config_get`), so when configs were cloned, the C-accessible
color list was empty.
### Why it broke
This bug was introduced in the original implementation in 29929a473 (Dec
2024), but remained dormant until commit f60bdb0fa (Sep 19, 2025), which
moved the icon-setting `switch` statement into `syncAppearance()`. Since
`syncAppearance()` is called with cloned configs from
`ghosttyConfigDidChange()` (which receives cloned configs from
`Ghostty.App.swift:1639`), the icon code now ran with cloned configs
that had empty `colors_c` arrays.
### Fix
Clone both arrays in `ColorList.clone()`:
```zig
.colors = try self.colors.clone(alloc),
.colors_c = try self.colors_c.clone(alloc), // Added
```
### Testing
- Added ColorList.test.clone test case that verifies both colors and
colors_c arrays are properly cloned
- Verified test fails without the fix (expected 3 colors_c items, found
0)
- Verified test passes with the fix
- Confirmed custom icon now persists correctly with both initial config
load and subsequent config change notifications
### Discussion
I opened a discussion to report this
([9616](https://github.com/ghostty-org/ghostty/discussions/9616)) that
this PR will resolve.
> [!NOTE]
> **LLM Usage Disclosure**
> This bug was investigated and debugged with assistance from Claude
Code. The root cause analysis and fix were developed through interactive
debugging.
This could cause a 0-length hyperlink to be present in the screen,
which, in ReleaseFast, causes a lockup as the string alloc tries to
iterate `1..0` to allocate 0 chunks.
I encountered these bugs while trying to benchmark Ghostty for
performance work.
- Tmux control mode parsing would start accessing deallocated memory
after entering the "broken" state, and worse yet, would cause a
double-free once it was supposed to be deinited.
- Despite our best efforts, CoreText can still produce non-monotonic
(non-ltr) runs. Our renderer code relies on monotonic ltr ordering so in
the rare case where this happens we just sort the buffer before
returning it.
- C1 (8-bit) controls can be executed in certain parser states, so we
need to handle them in the stream's `execute` function. Luckily this was
pretty straightforward since all C1 controls are equivalent to `ESC`
followed by `C1 - 0x40`.
- `Terminal.Screen`'s `cursorScrollDown` function could cause memory
corruption because of `eraseRow` moving the cursor's tracked pin to a
different page. In fixing this, I actually reduced the complexity of
that codepath.
- **Bonus!** Added a nice helper function to `Offset.Slice` so that you
can just do `offset_slice.slice()` instead of
`offset_slice.offset.ptr(base)[0..offset_slice.len]`. Much more
readable.
### `vtebench` before/after
<img width="984" height="691" alt="image"
src="https://github.com/user-attachments/assets/ef20dcc5-d611-4763-9107-355d715a6c0b"
/>
Doesn't seem like any of these changes caused a performance regression.
It was previously possible for `eraseRow` to move the cursor pin to a
different page, and then the call to `cursorChangePin` would try to free
the cursor style from that page even though that's not the page it
belongs to, which creates memory corruption in release modes and
integrity violations or assertions in debug mode.
As a bonus, this should actually be faster this way than the old code,
since it avoids needless work that `cursorChangePin` otherwise does.
These can be unambiguously invoked in certain parser states, and as such
we need to handle them. In real world use they are extremely rare, hence
the branch hint. Without this, we get illegal behavior by trying to cast
the value to the 7-bit C0 enum.
Progress towards #189
## Search Thread
This completes an initial implementation of the search thread. This is a
separate thread that manages a terminal search operation, triggering
event callbacks for various scenarios. The performance goal of the
search thread is to minimize time spent within the critical area of the
terminal lock while making forward progress on search outside of that.
The search thread sends two messages back to the caller right now:
- Total matches: sent continuously as new matches are found, just a
number
- Viewport matches: a list of `Selection` structures spanning the
current viewport of matches within the visible viewport. Sent whenever
the viewport changes (location or content).
I think that's enough to build a rudimentary search UI.
For this initial implementation, the search also relies on a "refresh
timer" which trigger every 24ms (40 FPS) to grab the lock and look for
any reconciliation that needs to happen: viewport moved, active screen
changed, active area changed, etc. This is a total guess and is
arbitrary currently. The value should be tuned to balance responsiveness
and IO throughput (lock-holding). I actually suspect this may be too
frequent right now.
A TODO is noted for the future to pause the refresh timer when the
terminal being search isn't focused. We'll have to do that before
shipping search because the way its built right now we will definitely
consume unnecessary CPU while unfocused. But only while search is
active.
## `ViewportSearch`
I also determined the need for what is called the `ViewportSearch`
layer. This is similar to `ActiveSearch` in that it throws away and
re-searches an area, but is tuned towards efficiently detecting viewport
changes. I found its more efficient to continuously research the visible
viewport than to hunt for those matches within the cached ScreenSearch
results, which can be very large.
## Future
Next up we need to hook up the search thread to some keybindings to
start and stop the search so we can then trigger those from our apprts
(GUIs).
I highly suspect this is going to expose some major performance issues
(overly active message sending being the likely culprit) that we can fix
up in the search thread thereafter. Up until this point we can only run
this stuff in isolation in unit tests which is good for testing
correctness but difficult for testing resource usage.
There are some written TODOs in the Thread.zig file in this PR. I may
address some of them before merging, since I think a couple are pretty
obvious performance gotchas.
This PR changes our primary/alt screen tracking from being by-value
fields that are copied during switch to heap-allocated pointers that are
stable. This is motivated now by #189 since our search thread needs a
stable screen pointer, but this will also help us in the future with our
future N-screens proposal I have.
Also, as a nice bonus, alt screen memory is now initialized on demand
when you first enter alt-screen, so this saves a few MB of memory for a
new terminal if you never use alt screen!
This is something I've wanted to do for a veryyyyyy long time, but the
annoyance of the task really held me back. I finally pushed through and
did this with the help of some AI agents for the rote tasks (renaming
stuff).
Chugging along towards #189
This adds significantly more internal work for searching. A long time
ago, I added #2885 which had a hint of what I was thinking of. This
simultaneously builds on this and changes direction.
The change of direction is that instead of making PageList fully
concurrency safe and having a search thread access it concurrently, I'm
now making an architectural shift where our search thread will grab the
big lock (blocking all IO/rendering), but with the bet that we can make
our critical areas small enough and time them well enough that the
performance hit while actively searching will be minimal. **Results yet
to be seen, but the path to implement this is much, much simpler.**
## Rearchitecting Search
To that end, this PR builds on #2885 by making `src/terminal/search` and
entire package (rather than a single file).
```mermaid
graph TB
subgraph Layer5 ["<b>Layer 5: Thread Orchestration</b>"]
Thread["<b>Thread</b><br/>━━━━━━━━━━━━━━━━━━━━━<br/>• MPSC queue management<br/>• libxev event loop<br/>• Message handling<br/>• Surface mailbox communication<br/>• Forward progress coordination"]
end
subgraph Layer4 ["<b>Layer 4: Screen Coordination</b>"]
ScreenSearch["<b>ScreenSearch</b><br/>━━━━━━━━━━━━━━━━━━━━━<br/>• State machine (tick + feed)<br/>• Result caching<br/>• Per-screen (alt/primary)<br/>• Composes Active + History search<br/>• Interrupt handling"]
end
subgraph Layer3 ["<b>Layer 3: Domain-Specific Search</b>"]
ActiveSearch["<b>ActiveSearch</b><br/>━━━━━━━━━━━━━━━━━━━━━<br/>• Active area only<br/>• Invalidate & re-search<br/>• Small, volatile data"]
PageListSearch["<b>PageListSearch</b><br/>━━━━━━━━━━━━━━━━━━━━━<br/>• History search (reverse order)<br/>• Separated tick/feed ops<br/>• Immutable PageList assumption<br/>• Garbage pin detection"]
end
subgraph Layer2 ["<b>Layer 1: Primitive Operations</b>"]
SlidingWindow["<b>SlidingWindow</b><br/>━━━━━━━━━━━━━━━━━━━━━<br/>• Manual linked list node management<br/>• Circular buffer maintenance<br/>• Zero-allocation search<br/>• Match yielding<br/>• Page boundary handling"]
end
Thread --> ScreenSearch
ScreenSearch --> ActiveSearch
ScreenSearch --> PageListSearch
ActiveSearch --> SlidingWindow
PageListSearch --> SlidingWindow
classDef layer5 fill:#0a0a0a,stroke:#ff0066,stroke-width:3px,color:#ffffff
classDef layer4 fill:#0f0f0f,stroke:#ff6600,stroke-width:3px,color:#ffffff
classDef layer3 fill:#141414,stroke:#ffaa00,stroke-width:3px,color:#ffffff
classDef layer2 fill:#1a1a1a,stroke:#00ff00,stroke-width:3px,color:#ffffff
class Thread layer5
class ScreenSearch layer4
class ActiveSearch,PageListSearch layer3
class SlidingWindow layer2
style Layer5 fill:#050505,stroke:#ff0066,stroke-width:2px,color:#ffffff
style Layer4 fill:#080808,stroke:#ff6600,stroke-width:2px,color:#ffffff
style Layer3 fill:#0c0c0c,stroke:#ffaa00,stroke-width:2px,color:#ffffff
style Layer2 fill:#101010,stroke:#00ff00,stroke-width:2px,color:#ffffff
```
Within the package, we have composable layers that let us test each
point:
- `SlidingWindow`: The lowest layer, the caller manually adds linked
list page nodes and it maintains a sliding window we search over,
yielding results without allocation (besides the circular buffers to
maintain the sliding window).
- `PageListSearch`: Searches a PageList structure in reverse order
(assumption: more recent matches are more valuable than older), but
separates out the `tick` (search, but no PageList access) and `feed`
(PageList access, prep data for search but don't search) operations.
This lets us `feed` in a critical area and `tick` outside. **This
assumes an immutable PageList, so this is for history.**
- `ActiveSearch`: Searches only the active area of a PageList. The
expectation is that the active area changes much more regularly, but it
is also very small (relative to scrollback). Throws away and re-searches
the active area as necessary.
- `ScreenSearch`: Composes the previous three components to coordinate
searching an active terminal screen. You'd have one of these per screen
(alt vs primary). This also caches results unlike the other components,
with the expectation that the caller will revisit the results as screens
change (so if you switch from neovim back to your shell and vice versa
with a search active, it won't start over).
- `Thread`: A dedicated search thread that will receive messages via
MPSC queues while managing the forward progress of a `ScreenSearch` and
sending matches back to the surface mailbox for apprt rendering. **The
thread component is not functional, just boilerplate, in this PR.**
ScreenSearch is a state machine that moves in an iterative `tick` +
`feed` fashion. This will let us "interrupt" the search with updates on
the search thread (read our mailbox via libxev loops for example) and
will let us minimize critical areas with locks (only `feed`).
Each component is significantly unit tested, especially around page
boundary cases. Given the complexity, there is no way this is perfect,
but the architecture is such that we can easily add regression tests as
we find issues.
## Other Changes, Notes
The only change to actually used code is that tracked pins in a
`PageList` can now be flagged as "garbage." A garbage tracked pin is one
that had to be moved in a non-sensical way because the previous location
it tracked has been deleted. This is used by the searcher to detect that
our history was pruned.
**If my assumption about the big lock is wrong** and this ends up being
godawful for performance, then it should still be okay because more
granular locking and reference counting such as that down by @dave-fl in
#8850 can be pushed into these components and reused. So this work is
still valuable on its own.
## Future
This PR is still just a bunch of internals, split out into its own PR so
I don't make one huge 10K diff PR. There are a number of future tasks:
- Flesh out `ScreenSearch` and hook it up to `Thread`
- Pull search thread management into `Surface` (or possibly the render
thread or shared render state since active area changes can be
synchronized with renderer frame rebuilds. Not sure yet.)
- Send updates back to the surface thread so that apprts can update UI.
- Apprt actions, input bindings, etc. to hook this all up (the easy
part, really).
The next step is to continue to flesh out the `ScreenSearch` as required
and hook it up to `Thread`.
**AI disclosure:** AI reviewed the code and assisted with some tests,
but didn't write any of the logic or design. This is beyond its ability
(or my ability to spec it out clearly enough for AI to succeed).
Fixes#9579
Protect against panics caused by integer overflows by using functions
that allow integer overflows to be caught instead of causing a panic.
Also protect against DOS from images that might not cause an overflow
but do consume an absurd amount of memory by limiting images to a
maximum size of 4GiB.
Fixes#9579
Protect against panics caused by integer overflows by using functions
that allow integer overflows to be caught instead of causing a panic.
Also protect against DOS from images that might not cause an
overflow but do consume an absurd amount of memory by limiting
images to a maximum size of 4GiB (which is the maximum size of
`image-storage-limit`).
Fixes#9532
Supersedes #9554
Turns out the explicit `UTF8_STRING` atom was not needed after all and
GTK adds it automatically when running under X11; just having the
explicit UTF-8 charset type is enough.
This corrects situations where it may not be necessary to include
(Wayland), in addition to removing a duplicate atom under X11.
Importantly, this also corrects issues under Wayland in some scenarios,
such as using Electron-based apps (e.g., VSCode/Codium under Ubuntu
24.04 LTS).
Turns out this was not needed after all and GTK adds it automatically
when running under X11; just having the explicit UTF-8 charset type is
enough.
This corrects situations where it may not be necessary to include
(Wayland), in addition to removing a duplicate atom under X11.
Importantly, this also corrects issues under Wayland in some scenarios,
such as using Electron-based apps (e.g., VSCode/Codium under Ubuntu
24.04 LTS).
Bumps
[cachix/install-nix-action](https://github.com/cachix/install-nix-action)
from 31.8.2 to 31.8.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/cachix/install-nix-action/releases">cachix/install-nix-action's
releases</a>.</em></p>
<blockquote>
<h2>v31.8.3</h2>
<h2>What's Changed</h2>
<ul>
<li>nix: 2.32.2 -> 2.32.3 by <a
href="https://github.com/github-actions"><code>@github-actions</code></a>[bot]
in <a
href="https://redirect.github.com/cachix/install-nix-action/pull/260">cachix/install-nix-action#260</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/cachix/install-nix-action/compare/v31.8.2...v31.8.3">https://github.com/cachix/install-nix-action/compare/v31.8.2...v31.8.3</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="7ec16f2c06"><code>7ec16f2</code></a>
Merge pull request <a
href="https://redirect.github.com/cachix/install-nix-action/issues/260">#260</a>
from cachix/create-pull-request/patch</li>
<li><a
href="5afc2ac89d"><code>5afc2ac</code></a>
nix: 2.32.2 -> 2.32.3</li>
<li>See full diff in <a
href="456688f15b...7ec16f2c06">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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
There is a sparkle-related 'issue' with the previous implementation.
When you download/install in the `updateAvailable` state, if you don't
install it, then check the updates again. Sparkle loses its downloaded
stage in the delegate (it's normal when I use the sparkle source code).
This time, when you click install in the `updateAvailable` state, it
just uses the previous downloaded package and starts to install, without
calling `showReady(toInstallAndRelaunch:)`.
I think removing `readyToInstall` in our customised ui makes sense,
since the current package is pretty small and only takes a few seconds
to download for a normal network. And most of users intend to install
this update.
Also, there is no way back once you reach "Install and Restart" stage in
`SPUStandardUserDriver`, unless you force quit ofc.
~~This pr also contains some further cleanup for #9170, since
`InstallingView` is no longer visible for users.~~
#### `auto-download`
When `auto-download = check`, Sparkle will call
`UpdateDriver/showUpdateFound(with:state:reply:)`.
When `auto-download = download`, previously no update ui was presented
to the user, neither Sparkle's nor Ghostty’s.
> I tried tick&untick auto download&install in Sparkle's alert, which
doesn't seem to make any difference.
With this pr, `auto-download = check` will behave the same,
**`auto-download = download` will now prompt the user with a capsule.**
https://github.com/user-attachments/assets/f994dd29-d348-4fea-8777-df3720d6e7af
> [!NOTE]
> I used AI to proofread my comments
There is a sparkle-related 'issue' with the previous implementation. When you download/install in the `updateAvailable` state, if you don't install it, then check the updates again. Sparkle loses its downloaded stage in the delegate (it's normal when I use the sparkle source code). This time, when you click install in the `updateAvailable` state, it just uses the previous downloaded package and starts to install, without calling `showReady(toInstallAndRelaunch:)`.
I think removing `readyToInstall` in our customed ui, will reduce one step to install an update for most of the users out there, which makes sense, since the current package is pretty small, only takes a few seconds to download for a normal network, and they intended to install this update.
Resolves Issue #8357
### Implementation
Following the existing `onResize` callback pattern in
`TerminalSplitTreeView`, I added an `onEqualize` callback to
`SplitView`. When a divider is double-tapped, the callback retrieves a
surface from that `TerminalView`'s `SplitTree` and calls `splitEqualize`
to equalize the entire tree.
### Context
There is an existing PR #8364 that implements this feature but uses
`focusedSurface`, which doesn't work for unfocused windows. Since that
PR has been inactive for a few months after requested changes, I've
implemented this alternative approach.
Credit to @liby for that initial implementation!
### AI Usage
I chatted with Claude Code in Plan mode to understand the relationship
between surfaces and the split tree/split views, but I wrote all the
code myself.
### Screenshot
https://github.com/user-attachments/assets/0efd70ef-c90e-4b50-b853-b05e2ca2be67
Listen to changes to the OS preferred scroller style and use it to
trigger updates the core surface size such that the scrollbar margin
kept in sync. Otherwise, the margin update would be deferred until the
next window or surface resize/split event.
In the video I'm turning my external bluetooth mouse off and on again,
triggering these updates.
https://github.com/user-attachments/assets/15c0ee40-2c1a-419a-8b07-8270e9f7a12f
Here's a much simpler approach to fixing #9248 that doesn't involve
changes to the core or libghostty (the apprt one) at all. No safe area
API needed. All that's needed is for the content view to not clip its
subviews, such that the surface view can draw behind the scrollbar even
though it's a subview of the content view.
I haven't looked at the details on the renderer side, but since this
works it must be that it always renders background all the way to the
edges of the framebuffer, regardless of what the terminal screen size
(including window padding) happens to be. (This was already evident in
that we were able to avoid a transparent slot when the scrollbar was
hidden.) So I guess we kind of already have a safe area API through this
ability to set a screen size that's different from the framebuffer size?
_EDIT:_ I guess what's missing is the ability to shift the origin of the
screen within the framebuffer.
Fixes#9248, supersedes #9291.
Currently, close confirmation is always attached to the first tab in the
window group.
**This pr will attach the alert to the window that actually needs that
confirmation, so upon close, the window with the running process will be
focused.**
https://github.com/user-attachments/assets/8fddd4ce-a3f9-4e18-a645-80c85b5ba995
### Reproduce Steps on Tip (1.2.3 seems fine)
1. Open a new window.
2. Open a new tab.
3. **Wait for 2-3 seconds.**
4. Undo.
5. 💥
**RC is unclear for me, but dispatching it seems to fix it.**
This makes `ucs-detect` available in our Nix environment so that we can
run tests on our Unicode support. In the future, I'd like to modify our
CI to run this too.
This also adds a `./test/ucs-detect.sh` script that runs `ucs-detect`
with consistent options that match the upstream test styles.
Fwiw, I did try to write a simple libghostty-based Zig binary to run
this automatically for us in CI but while libghostty-vt is very good, we
don't yet have the proper APIs setup for actually setting up a Pty and
sub processing commands and hooking them up to a VT stream. So, I punt
that to the future.
This makes `ucs-detect` available in our Nix environment so that we can
run tests on our Unicode support. In the future, I'd like to modify our
CI to run this too.
This also adds a `./test/ucs-detect.sh` script that runs `ucs-detect`
with consistent options that match the upstream test styles.
If a UTF-8 byte order mark starts a config file, it should be ignored.
This also refactors config file loading a bit to reduce redundant code
and to make it possible to test loading config from a file.
Fixes#9490
If a UTF-8 byte order mark starts a config file, it should be ignored.
This also refactors config file loading a bit to reduce redundant code
and to make it possible to test loading config from a file.
Fixes#9490
Partially fixes#8493.
After dictating some texts, the icon still appears above, but it will
return to its right position after resizing or `\n` (saying newline, not
hitting enter).
This behaviour is better than before, where the icon always appeared
above.
> Quicklook doesn't seem to call this on Tahoe, but it still works well
anyway.
### Reference:
9e905357bb/src/nsterm.m (L7426)https://github.com/user-attachments/assets/6bf818c3-a0bb-412f-ae06-673f67cdeae4
This fixes the VS16 issues found in this test:
https://ucs-detect.readthedocs.io/sw_results/ghostty.html#ghostty
This is also a more robust way to handle VS15/16 in general.
This commit also changes our propeties to be a packed struct which
reduces its size from 4 bytes to 1 and likewise drops our unicode table
size 4x.
This updates uucode. As part of this, the wcwidth implementation was
updated (in uucode) to make emoji modifiers width ZERO. But if they're
standalone, we want them as width 2.
So this also contains a change to force them as width 2 for our width
calculation. This only matters for standalone emoji modifiers, because
when they form a valid grapheme we don't use this width calculation.
This updates uucode. As part of this, the wcwidth implementation was
updated (in uucode) to make emoji modifiers width ZERO. But if they're
standalone, we want them as width 2.
So this also contains a change to force them as width 2 for our width
calculation. This only matters for standalone emoji modifiers, because
when they form a valid grapheme we don't use this width calculation.
Partially fixes#8493.
After dictating some texts, the icon still appears above, but it will return to its right position after resizing or `\n` (saying newline, not hitting enter).
This behaviour is better than before, where the icon always appeared above.
### Reference:
9e905357bb/src/nsterm.m (L7426)
This adds the `UTF8_STRING` atom and explicit UTF-8 character set MIME
type (`text/plain;charset=utf-8`) to text content when being sent to the
clipboard under the new multipart support.
This fixes clipboard support under X11 particularly, which generally
looks for the `UTF8_STRING` atom when looking for text content. This can
be verified with `xclip -out -verbose`, or trying to do things like
paste in Firefox.
I've noted that there's a number of other older atoms that exist, but
I've refrained from adding them for now. Kitty only seems to set
`UTF8_STRING` and I've had a hard time finding consensus on what exactly
is the correct set otherwise.
This adds the UTF8_STRING atom and explicit UTF-8 character set MIME
type (text/plain;charset=utf-8) to text content when being sent to the
clipboard under the new multipart support.
This fixes clipboard support under X11 particularly, which generally
looks for the UTF8_STRING atom when looking for text content. This can be
verified with xclip -out -verbose, or trying to do things like paste in
Firefox.
I've noted that there's a number of other older atoms that exist, but
I've refrained from adding them for now. Kitty only seems to set
UTF8_STRING and I've had a hard time finding consensus on what exactly
is the correct set otherwise.
This supports the new `setClipboard` parameter that may provide data in
multiple formats, allowing us to copy rich text to/from the clipboard as
well as other types in the future.
This all fixes a memory leak on all clipboard ops that snuck through.
This supports the new `setClipboard` parameter that may provide data in
multiple formats, allowing us to copy rich text to/from the clipboard as
well as other types in the future.
Fixes#9426
Since we can't set the meta charset tag since we emit partial HTML, we
use codepoint entities like `{` for non-ASCII characters to ensure
proper rendering.
Fixes#9426
Since we can't set the meta charset tag since we emit partial HTML, we
use codepoint entities like `{` for non-ASCII characters to
ensure proper rendering.
Fixes#9420
The problem was ultimately that the padding calculations assumed that
the total vertical padding is always less than one cell height (just
doing `fmod(contentHeight, cellHeight)` instead of the more careful
`contentHeight - scrollbar.len * cellHeight`). This is not at all true,
and as a result, the calculated document height was often a cell height
short of what it should be.
For similar reasons, we shouldn't rely on `fmod`-based padding update
calculations during relayouting. This PR takes the proper approach of
saving and reusing the current scrollbar state to calculate the correct
document height on every layout. As a bonus, this removes the flickering
scrollbar during resize that I complained about in #9296.
Fixes#9397
This makes `copy_to_clipboard` take an optional parameter with the
format to copy. **The default has changed to `mixed`,** which will set
multiple content types on the clipboard allowing the OS or target
application to choose what they prefer. In this case, we set both
`text/plain` and `text/html`.
This only includes the macOS implementation. The GTK side still needs to
be done, but is likely trivial to do.
https://github.com/user-attachments/assets/b1b2f5cd-d59a-496e-bb77-86a60571ed7f
This moves the bg, fg, cursor color state into the `Terminal` struct,
including all default and override handling. I've rewritten our color
palette handling so that overrides, defaults, resets, etc are all
handled by the terminal package. I've added much more unit test
coverage.
This has various benefits:
* libghostty now handles OSC and Kitty color operations
* formatters can be aware of all of these colors (not implemented in
this PR)
* renderer has way less inter-thread messages
* renderer uses less memory
* termio stream handler uses less memory and becomes simpler
* override logic, changing defaults can all be unit tested
* we have unit tests for kitty color operations end-to-end now (cc
@jcollie heyo!)
There's a ton of risk on this PR too. There's a lot of really tiny
conditionals EVERYWHERE and I'm sure I got at least one wrong. We'll let
this bake in tip to find those, I'm sure they're minor and will show up
as just the wrong color somewhere.
**AI disclosure:** Amp wrote many of the tests. I did all the
implementation.
With its being `focusable`(default), the first responder became the text
editor instead of the paste button.
This fixes the issue where one can't confirm with the keyboard.
This doesn't affect its selection.
std.fs.makeDirAbsolute() only creates the last directory. We instead
need Dir.makePath() to make the entire path, including intermediate
directories.
This fixes the problem where a missing $XDG_STATE_HOME directory (e.g.
~/.local/state/) would prevent our ssh cache file from being created.
Fixes#9393
Bumps
[cachix/install-nix-action](https://github.com/cachix/install-nix-action)
from 31.8.1 to 31.8.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/cachix/install-nix-action/releases">cachix/install-nix-action's
releases</a>.</em></p>
<blockquote>
<h2>v31.8.2</h2>
<h2>What's Changed</h2>
<ul>
<li>nix: 2.32.1 -> 2.32.2 by <a
href="https://github.com/github-actions"><code>@github-actions</code></a>[bot]
in <a
href="https://redirect.github.com/cachix/install-nix-action/pull/259">cachix/install-nix-action#259</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/cachix/install-nix-action/compare/v31.8.1...v31.8.2">https://github.com/cachix/install-nix-action/compare/v31.8.1...v31.8.2</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="456688f15b"><code>456688f</code></a>
Merge pull request <a
href="https://redirect.github.com/cachix/install-nix-action/issues/259">#259</a>
from cachix/create-pull-request/patch</li>
<li><a
href="0cacfe0f2a"><code>0cacfe0</code></a>
nix: 2.32.1 -> 2.32.2</li>
<li>See full diff in <a
href="fd24c48048...456688f15b">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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This adds HTML formatting capabilities to the formatter package. HTML is
emitted as inline styles. For palette indexes, direct RGB is emitted if
we have access to a palette; otherwise, we fall back to CSS variables.
This isn't exposed to end users yet, but will enable copy as html
features. This is available in libghostty.
Fixes#9395
**AI disclosure:** I used AI (Amp) to help me write tests, but the
implementation was done manually. I reviewed everything.
There have been frequent reports of key encoding issues in vim and tmux
with version 1.2.3 on macOS: #9340, #9361, #9401,
https://discord.com/channels/1005603569187160125/1432413679806320772.
I think I found the culprit: the option modifier is always passed as alt
to the core, regardless of `macos-option-as-alt`. Since #9289, this
means that a key event where option was used (as option) for translation
is encoded as if it also has the alt modifier.
For example, consider the many European keyboard layouts where option+8
sends `[`. If `macos-option-as-alt = true`, Ghostty correctly intercepts
the option and encodes option+8 as alt+8 instead (that is,
`^[[27;3;56~`). But if `macos-option-as-alt = false`, Ghostty first
allows option to be used for translation, obtaining `[`, and then
encodes the key event as alt+[ (that is, `^[[27;3;91~`), rather than
just `[`.
Tweaking the test case from #9289, here's a quick way to see this: set
`macos-option-as-alt = left`, run
```
printf '\033[>4;2m'
cat
```
choose a European keyboard layout (e.g., Norwegian), and hit both
left-option+8 and right-option+8. The former inserts `^[[27;3;56~` in
all well-behaved terminals. The latter inserts `[` in other terminals,
but `^[[27;3;91~` in Ghostty.
Basically, while modify other keys 2 does require encoding consumed
modifiers, the option key is not one of the supported modifiers, and
should not be included (as alt or anything else) when
`macos-option-as-alt = false`.
This PR removes alts that were actually options when using modify other
keys 2.
This removes all existing functionality that I know of that encodes a
terminal, screen, pagelist, or page as plaintext and unifies all logic
onto the formatter system.
This replaces the logic of Screen.selectionString with calls to
ScreenFormatter.
This means that all our various selection-based features like copying to
clipboards now uses the new formatter. The formatter code is now
user-facing.
This forced us to pass all selectionString tests which revealed some
edge cases that were not handled correctly before in the formatter! The
formatter now handles:
- Plain text now emits `\n` instead of `\r\n`. VT emits `\r\n`
- Rectangular selections
- Various wide character edge cases
- Selection is now inclusive on the end, not exclusive
This adds the option `pin_map` or `point_map` (for pages) to formatter,
allowing callers to get a byte-by-byte mapping for where on the screen
each encoding maps to. This is used by search internals and hyperlinks.
I haven't hooked that all up yet. This diff was big enough I wanted this
as one.
Tests significantly cover the new feature.
Next up, we'll rip out `selectionString` and replace it with formatters!
This adds a new formatter that can be used with standard Zig `{f}`
formatting that emits any portion of the terminal screen as VT
sequences. In addition to simply styling, this can emit the entire
terminal/screen state such as cursor positions, active style, terminal
modes, etc.
To do this, I've extracted all formatting to a dedicated `formatter`
package within `terminal`. This handles all formatting types (currently
plaintext and VT formatting, but can imagine things like HTML in the
future). Presently, we have "formatting" split out across a variety of
places in Terminal, Screen, PageList, and Page. I didn't remove this
code yet but I intend to unify it all on formatter in the future.
This also doesn't expose this functionality in any user-facing way yet.
This PR just adds it to the ghostty-vt Zig module and unit tests it.
Ghostty app changes will come later.
**This also improves the readonly stream** to handle OSC color
operations for _setting_ but it doesn't emit any responses of course,
since its readonly.
Fixes a bug described in #9291, where resizing an empty window causes
the scrollbar to appear even though the window remains larger than the
total content, because the relayouting fails to account for the change
in padding around the cell grid.
To reproduce the issue:
1. Enable legacy scrollbars (System Settings -> Appearance -> Show
scroll bars -> Always)
2. Open Ghostty
3. Vertically resize the window to make it smaller. The scrollbar will
pop up, and as you drag the window edge, it will cycle between a maximum
offset and zero depending on how far the resize is from an integer
multiple of the cell height.
With this PR you'll still see the scrollbar flicker while resizing, but
when you stop it will always disappear. Haven't figured out how to get
rid of the flickering yet. I tried to condition various updates on the
window not being in a live resize, but so far no luck.
contains() checks the cache for an existing entry. It's a read-only
operation, so we can drop the write bit and fixupPermissions() call.
This is also consistent with the list() operation.
fixupPermissions() is unnecessary in this code path. It provided minimal
additional security because all of our creation and update operations
enforce 0o600 (owner-only) permissions, so anyone tampering with this
file has already gotten around that. The contents of this (ssh host
cache) file are also not sensitive enough to warrant any additional
hardening on reads.
Bumps
[actions/download-artifact](https://github.com/actions/download-artifact)
from 5.0.0 to 6.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/download-artifact/releases">actions/download-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v6.0.0</h2>
<h2>What's Changed</h2>
<p><strong>BREAKING CHANGE:</strong> this update supports Node
<code>v24.x</code>. This is not a breaking change per-se but we're
treating it as such.</p>
<ul>
<li>Update README for download-artifact v5 changes by <a
href="https://github.com/yacaovsnc"><code>@yacaovsnc</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/417">actions/download-artifact#417</a></li>
<li>Update README with artifact extraction details by <a
href="https://github.com/yacaovsnc"><code>@yacaovsnc</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/424">actions/download-artifact#424</a></li>
<li>Readme: spell out the first use of GHES by <a
href="https://github.com/danwkennedy"><code>@danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/download-artifact/pull/431">actions/download-artifact#431</a></li>
<li>Bump <code>@actions/artifact</code> to <code>v4.0.0</code></li>
<li>Prepare <code>v6.0.0</code> by <a
href="https://github.com/danwkennedy"><code>@danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/download-artifact/pull/438">actions/download-artifact#438</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/danwkennedy"><code>@danwkennedy</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/download-artifact/pull/431">actions/download-artifact#431</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/download-artifact/compare/v5...v6.0.0">https://github.com/actions/download-artifact/compare/v5...v6.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="018cc2cf5b"><code>018cc2c</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/438">#438</a>
from actions/danwkennedy/prepare-6.0.0</li>
<li><a
href="815651c680"><code>815651c</code></a>
Revert "Remove <code>github.dep.yml</code>"</li>
<li><a
href="bb3a066a8b"><code>bb3a066</code></a>
Remove <code>github.dep.yml</code></li>
<li><a
href="fa1ce46bbd"><code>fa1ce46</code></a>
Prepare <code>v6.0.0</code></li>
<li><a
href="4a24838f3d"><code>4a24838</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/431">#431</a>
from danwkennedy/patch-1</li>
<li><a
href="5e3251c4ff"><code>5e3251c</code></a>
Readme: spell out the first use of GHES</li>
<li><a
href="abefc31eaf"><code>abefc31</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/424">#424</a>
from actions/yacaovsnc/update_readme</li>
<li><a
href="ac43a6070a"><code>ac43a60</code></a>
Update README with artifact extraction details</li>
<li><a
href="de96f4613b"><code>de96f46</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/417">#417</a>
from actions/yacaovsnc/update_readme</li>
<li><a
href="7993cb44e9"><code>7993cb4</code></a>
Remove migration guide for artifact download changes</li>
<li>Additional commits viewable in <a
href="634f93cb29...018cc2cf5b">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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This exposes the SGR parser to the C and Wasm APIs. An example is shown
in c-vt-sgr.
Compressed example:
```c
#include <assert.h>
#include <stdio.h>
#include <ghostty/vt.h>
int main() {
// Create parser
GhosttySgrParser parser;
assert(ghostty_sgr_new(NULL, &parser) == GHOSTTY_SUCCESS);
// Parse: ESC[1;31m (bold + red foreground)
uint16_t params[] = {1, 31};
assert(ghostty_sgr_set_params(parser, params, NULL, 2) == GHOSTTY_SUCCESS);
printf("Parsing: ESC[1;31m\n\n");
// Iterate through attributes
GhosttySgrAttribute attr;
while (ghostty_sgr_next(parser, &attr)) {
switch (attr.tag) {
case GHOSTTY_SGR_ATTR_BOLD:
printf("✓ Bold enabled\n");
break;
case GHOSTTY_SGR_ATTR_FG_8:
printf("✓ Foreground color: %d (red)\n", attr.value.fg_8);
break;
default:
break;
}
}
ghostty_sgr_free(parser);
return 0;
}
```
**AI disclosure:** Amp wrote most of the C headers, but I verified it
all. https://ampcode.com/threads/T-d9f145cb-e6ef-48a8-ad63-e5fc85c0d43e
After `ghostty_app_update_config`, `ghostty_action_config_change_s` was
fired with the correct config. This happens synchronously, which will
update `App.config` in `App.configChange(_:target:v:)`.
Previously, after updating, `App.config` was set with the stale one,
which caused #8282.
Fixes a double-free bug when closing the inspector.
Not sure if there's a good way to add a test to check that allocation
and deallocation are sound here? To instantiate an Inspector you need a
Surface, which in turn requires an entire App/apprt. Doesn't look like
the repo has any tests with that kind of scope at the moment?
This adds a new stream handler implementation that updates terminal
state in reaction to VT sequences, but doesn't perform any of the
actions that would require responses (e.g. queries).
This is exposed in two ways: first, as a standalone `ReadonlyStream` and
`ReadonlyHandler` type that contains all the implementation. Second, as
a convenience func on `Terminal` as `vtStream` and `vtHandler` which
return their respective types preconfigured to update the calling
terminal state.
This dramatically simplifies libghostty-vt usage from Zig (and will
eventually be exposed to C, too) since a Terminal on its own is ready to
go as a full VT parser and state machine without needing to build any
custom types!
There's a second big bonus here which is that our `stream_readonly.zig`
tests are true end-to-end tests for raw bytes to terminal state. This
will let us test a wider variety of situations more broadly. To start,
there are only a handful of tests implemented here.
**AI disclosure:** Amp wrote basically this whole thing, but I reviewed
it. https://ampcode.com/threads/T-3490efd2-1137-4112-96f6-4bf8a0141ff5
### Background
> Reported from
https://discord.com/channels/1005603569187160125/1320882404717625374/1431258448439279709
Per current implementation, when `macos-titlebar-style` is `tabs` or
`transparent`, the titlebar's background is hidden to enable a blur
effect, but this could result in an incorrect appearance when the
window's appearance is different from the one based on the terminal's
background color. For instance, with the following configs:
```
window-theme = "light"
// theme will be default to Ghostty, which is dark
```
or
```
window-theme = "system"
// theme will be default to Ghostty, which is dark
// and system theme is set to light
```
### Changes
Update window theme based on the terminal's background color when using
`tabs` or `transparent`
This removes our `@hasDecl` usage from `terminal.Stream` and instead
uses a tagged union approach similar to what we already do for apprt
actions. The reasons to do this:
1. It is less magic. You don't get new functionality by magically
implementing a function.
2. It is safer. You can't typo a function name and Zig's exhaustive enum
handling will force you to handle all cases (even if most cases are
no-ops). This also helps you as at the implementor know when new
functionality pops up.
3. It is easier to integrate into C (for libghostty-vt). We can expose a
single tagged union type with a single callback rather than whatever the
previous mess was. This PR doesn't do this yet.
In addition, this PR adds in some helpers necessary to make it easier to
make C ABI compatible tagged unions. This lays the groundwork for our
libghostty-vt work but isn't exposed directly there yet. This PR has no
functional changes. Everything should behave identically as before.
I'm PRing this now because its already a huge diff, and I want to get
this in before I make more meaningful changes such as exposing some of
this to libghostty or adding a simpler Stream handler that maps to
terminal state for the Zig module and so on.
## Benchmarks
There's no meaningful impact on VT processing, I'd say all changes seen
below are noise:
<img width="2038" height="1392" alt="CleanShot 2025-10-25 at 07 10
04@2x"
src="https://github.com/user-attachments/assets/af6fa611-5b35-44d0-91ae-26955b1f980a"
/>
## One more `@hasDecl`
There is one more `hasDecl` remaining for `handleManually`. This is a
special case that's only used by our inspector. I think there is a
better way to do this but I didn't want to bloat this PR with anything
more! This doesn't impact our primary consumers of stream.
## AI Disclosure
I used AI considerably in handling the rote tasks in refactoring this. I
did the design myself manually but then prompted AI to help complete it
step by step. I did review each manually and understand it but I want to
take a careful review again...
Remove the semi-magic upper bound on the total cache key length. The
hostname and username validation routines will perform their own length
checks.
Also consolidate this function's tests. We previously had a few
redundant test cases.
Fixes#9322
SwiftUI `Text` has huge performance issues. On my maxed out MBP it hangs
for any text more than 100KB (it took ~8s to display it!). `TextEditor`
with a constant value works much better and handles scrolling for us,
too!
Fixes#9132
We were processing our window size defaults separate from our window
position and the result was that you'd get some incorrect behavior.
Unify the logic more to fix the positioning.
Note there is room to improve this further, I think that all initial
positioning could go into the controller completely. But I wanted to
minimize the diff for a backport.
This adds a set of Wasm convenience functions to ease memory management.
These are all prefixed with `ghostty_wasm` and are documented as part of
the standard Doxygen docs.
I also added a very simple single-page HTML example that demonstrates
how to use the Wasm module for key encoding.
This also adds a bunch of safety checks to the C API to verify that
valid values are actually passed to the function. This is an easy to hit
bug.
**AI disclosure:** The example is AI-written with Amp. I read through
all the code and understand it but I can't claim there isn't a better
way, I'm far from a JS expert. It is simple and works currently though.
Happy to see improvements if anyone wants to contribute.
This makes `libghostty-vt` build for freestanding wasm targets (aka a
browser) and produce a `ghostty-vt.wasm` file. This exports the same C
API that libghostty-vt does.
This commit specifically makes the changes necessary for the build to
build properly and for us to run the build in CI. We don't yet actually
try using it...
Fixes a crash when NSLocale returns nil for languageCode or countryCode
properties. This can happen when the app launches without locale
environment variables set.
The crash occurs at `src/os/locale.zig:87-88` when trying to call
`getProperty()` on a nil object. The fix adds a null check and falls
back to `en_US.UTF-8` instead of dereferencing null.
## Testing
Tested by running with locale variables unset:
```bash
unset LC_ALL && ./zig-out/Ghostty.app/Contents/MacOS/ghostty
```
Before: segmentation fault
After: launches successfully with fallback locale
Fixes#8900
Our xterm modify other keys state 2 encoding was stripped consumed mods
from the keyboard event. This doesn't match xterm or other popular
terminal emulators (but most importantly: xterm). Use the full set of
mods and add a test to verify this.
Reproduction:
```
printf '\033[>4;2m'
cat
```
Then press `ctrl+shift+h` and compare across terminals.
Closes#8430
A few questions:
* Should I set a default keybind for `toggle-mouse-reporting`? The issue
mentioned one, it's currently unset.
* Am I handling the `toggle-mouse-reporting` action properly in
`performAction` (gtk) / `action` (macos)?
Copilot was used to understand the codebase, but code was authored
manually.
This cleans up some of our termio exec code by unifying process launch
state into a single union type. This makes it easier to distinguish
between the current two mutually exclusive modes of launching a process:
fork/exec and flatpak dbus commands.
It also ensures everyplace we touch related to process launching is
forced to address every case (exhaustive switch handling). I did find
one resource cleanup bug based on this cleanup, which is also fixed
here. This just improves memory slightly so it's not a big deal.
If we add future ways to launch processes, we can add a new union case.
For example, I originally had a `posix_spawn` option while I was
experimenting with that before abandoning it (see #9274).
The host validation code previously expected IPv6 addresses to be
enclosed in [brackets], but that's not how ssh(1) expects them.
This change removes that requirement and reimplements the host
validation routine to check for valid hostnames and IP addresses (IPv4
and IPv6) using standard routines rather than custom logic.
Fixes#9251
Bumps
[flatpak/flatpak-github-actions](https://github.com/flatpak/flatpak-github-actions)
from 6.5 to 6.6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/flatpak/flatpak-github-actions/releases">flatpak/flatpak-github-actions's
releases</a>.</em></p>
<blockquote>
<h2>v6.6</h2>
<ul>
<li>Specify full URL policy when mirroring screenshots</li>
<li>Fix restore cache keys to actually include arch</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="92ae9851ad"><code>92ae985</code></a>
Fix restore cache keys to actually include arch</li>
<li><a
href="b8a638469e"><code>b8a6384</code></a>
Specify full URL policy when mirroring screenshots</li>
<li><a
href="6684584b07"><code>6684584</code></a>
Switch funding to flatpak instead</li>
<li><a
href="e5aa88fb51"><code>e5aa88f</code></a>
readme: Update flathub images list</li>
<li><a
href="b93832bada"><code>b93832b</code></a>
Sync readme from flathub's fork</li>
<li>See full diff in <a
href="10a3c29f01...92ae9851ad">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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
The host validation code previously expected IPv6 addresses to be
enclosed in [brackets], but that's not how ssh(1) expects them.
This change removes that requirement and reimplements the host
validation routine to check for valid hostnames and IP addresses (IPv4
and IPv6) using standard routines rather than custom logic.
std.net.isValidHostname is currently too generous. It considers strings
like ".example.com", "exa..mple.com", and "-example.com" to be valid
hostnames, which is incorrect according to RFC 1123 (the currently
accepted standard).
Until the standard library function is improved, we can use this local
implementation that does follow the RFC 1123 standard.
I asked Claude to perform an audit of the code based on its understand
of the RFC. It suggested some additional test cases and considers the
overall implementation to be robust (its words) and standards compliant.
Ref: https://www.rfc-editor.org/rfc/rfc1123
std.net.isValidHostname is currently too generous. It considers strings
like ".example.com", "exa..mple.com", and "-example.com" to be valid
hostnames, which is incorrect according to RFC 1123 (the currently
accepted standard).
Until the standard library function is improved, we can use this local
implementation that does follow the RFC 1123 standard.
I asked Claude to perform an audit of the code based on its understand
of the RFC. It suggested some additional test cases and considers the
overall implementation to be robust (its words) and standards compliant.
Ref: https://www.rfc-editor.org/rfc/rfc1123
`fish_add_path` by default updates the `fish_user_paths` universal
variable which makes the modification persist across shell sessions.
The integration also tries to update the `fish_user_paths` when the
desired path already appears in the `PATH` environment variable but not
in `fish_user_paths`. Because `fish_user_paths` will always be inserted
before the inherited `PATH` env. This makes the added path
unintentionally has a higher priority.
This patch fixes the above issues by adding `--global` and `--path`
options to `fish_user_paths` which limits the modification scope and
ensures that the path won't be added if it already exists in `PATH`.
Ref: https://fishshell.com/docs/current/cmds/fish_add_path.html
`fish_add_path` by default updates the `fish_user_paths` universal
variable which makes the modification persist across shell sessions.
The integration also tries to update the `fish_user_paths` when the
desired path already appears in the `PATH` environment variable but
not in `fish_user_paths`. Because `fish_user_paths` will always be
inserted before the inherited `PATH` env. This makes the added path
unintentionally has a higher priority.
This patch makes the above issues by adding `--global` and `--path`
options to `fish_user_paths` which limits the modification scope and
ensures that the path won't be added if it already exists in `PATH`.
This fixes the source of a deadlock that some tip users have hit. If our
surface mailbox is full and there is a dirty scrollbar state, then
drawFrame would block forever trying to queue to the surface mailbox.
We now fail instantly if the queue is full and keep the scrollbar state
dirty. We can try again on the next frame, it's not a critical thing to
get updated.
This fixes the source of a deadlock that some tip users have hit. If our
surface mailbox is full and there is a dirty scrollbar state, then
drawFrame would block forever trying to queue to the surface mailbox.
We now fail instantly if the queue is full and keep the scrollbar state
dirty. We can try again on the next frame, it's not a critical thing to
get updated.
NSScreen instances can be garbage collected at any time, even for
screens that remain connected, making NSMapTable with weak keys
unreliable for tracking per-screen state.
This changes the quick terminal to use CGDisplay UUIDs as stable
identifiers, keyed in a strong dictionary. Each entry stores the window
frame along with screen dimensions, scale factor, and last-seen
timestamp.
**This should make quick terminal size restore more stable than 1.2.2.**
Rules for pruning:
- Entries are invalidated when screens shrink or change scale
- Entries persist and update when screens grow (allowing cached state to
work with larger resolutions)
- Stale entries for disconnected screens expire after 14 days.
- Maximum of 10 screen entries to prevent unbounded growth
NSScreen instances can be garbage collected at any time, even for
screens that remain connected, making NSMapTable with weak keys
unreliable for tracking per-screen state.
This changes the quick terminal to use CGDisplay UUIDs as stable
identifiers, keyed in a strong dictionary. Each entry stores the
window frame along with screen dimensions, scale factor, and last-seen
timestamp.
Rules for pruning:
- Entries are invalidated when screens shrink or change scale
- Entries persist and update when screens grow (allowing cached state
to work with larger resolutions)
- Stale entries for disconnected screens expire after 14 days.
- Maximum of 10 screen entries to prevent unbounded growth
When the preferred scrollbar style is "legacy", the scrollbar takes up
space that offsets the actual terminal. To prevent reflow, we detect
this before the scrollbar becomes visible and shrink our terminal width
to prepare for it.
This doesn't account for the style changing at runtime, yet.
This changes the default FreeType load target to `FT_LOAD_TARGET_LIGHT`,
giving the hinter a lighter touch in line with the default behavior in
most other GTK apps, and adds a load flag such that the old hinting
behavior can be restored via config.
As discussed in
https://github.com/ghostty-org/ghostty/issues/8674#issuecomment-3417082534.
However, this doesn't close that issue, as it still doesn't respect
custom Fontconfig settings. It's just a stopgap solution bringing
Ghostty's defaults more in line with the typical results of not
customizing Fontconfig.
Fixes#2731 (again)
This regressed in 1.2 due to the renderer rework missing porting this. I
believe this issue is still valid even with the rework since the font
grid changes the atlas and if there are still cached cells that
reference the old atlas coordinates it will produce garbage.
Fixes#2731 (again)
This regressed in 1.2 due to the renderer rework missing porting this. I
believe this issue is still valid even with the rework since the font
grid changes the atlas and if there are still cached cells that
reference the old atlas coordinates it will produce garbage.
Fixes#111
This builds on the prior work and adds scrollbars to the GTK
application. After this PR, both the macOS and GTK applications support
scrollbars and #111 can be closed.
## TODO
- [x] Verify that with text coming out if we manually scroll to the
bottom we bind to the bottom
- [x] Valgrind checks
Fixes#9233. The issue was that the content view was effectively
truncated by `SurfaceScrollView`s safe area insets covering the titlebar
(I suppose the scroll view doesn't inherit the `ignoresSafeArea`
modifier on `TerminalSplitTreeView` because it's not actually a node in
that tree, it just contains a subview that is). I think it's OK to zero
these insets unconditionally, as I don't think the frame size passed
down from `SurfaceWrapper` will ever overlap with a visible titlebar.
This PR also fixes a somewhat related issue I discovered along the way.
In many cases, the mouse wouldn't work on the first row of text, whether
trying to select text or interact with TUI elements like neovim
bufferlines/tablines. When looking at the inspector while moving the
mouse around, you'd see the mouse position jump discontinuously from a
y-coordinate of 10-20 pixels to -3, i.e., no longer within the surface.
The problem turned out to be that the height of the document view only
included the text lines, not the window padding, while the content view
is sized to the surface including padding. If you imagine the
metaphorical sliding of the viewport up and down the document, and you
require the text to snap to a fixed grid on the viewport, it's clear
that the document must have the same top and bottom padding as the
viewport, otherwise the math breaks at the ends. Sure enough, adding
this padding fixed the problem.
To properly reproduce these issues and see the effect of the fixes, it's
helpful to change the system settings to always show scroll bars
(Appearance -> Show scroll bars -> Always). That's what connecting an
external mouse was all about. You should also remove the debug banner or
use a release build, otherwise the banner takes the place of the hidden
titlebar and conceals the issue.
Completes #111 for macOS
This builds on #9225 and adds the native, macOS GUI scrollbars for
terminals.
This doesn't do GTK, but all the groundwork is there to make this easy
on GTK, depending on how scrollviews work there. I have to look into
that still. But in theory all the information and controls we provide
out of core are generic to _any_ scrollbar drawing.
## Demo
https://github.com/user-attachments/assets/85683bf9-1117-4f32-aaec-d926edd68c39
## Details
- The scrollbars respect your macOS system settings on style, color,
visibility.
- A new configuration `scrollbar` controls whether scrollbars are
visible (defaults to `system` allowing the system to choose).
- There is a new keybind action `scroll_to_row:N` that lets you jump to
an absolute row number N. This is implemented efficiently. This is how
grabbing the knob and scrolling works.
Bumps
[cachix/install-nix-action](https://github.com/cachix/install-nix-action)
from 31.8.0 to 31.8.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/cachix/install-nix-action/releases">cachix/install-nix-action's
releases</a>.</em></p>
<blockquote>
<h2>v31.8.1</h2>
<h2>What's Changed</h2>
<ul>
<li>nix: 2.32.0 -> 2.32.1 by <a
href="https://github.com/github-actions"><code>@github-actions</code></a>[bot]
in <a
href="https://redirect.github.com/cachix/install-nix-action/pull/258">cachix/install-nix-action#258</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/cachix/install-nix-action/compare/v31...v31.8.1">https://github.com/cachix/install-nix-action/compare/v31...v31.8.1</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="fd24c48048"><code>fd24c48</code></a>
Merge pull request <a
href="https://redirect.github.com/cachix/install-nix-action/issues/258">#258</a>
from cachix/create-pull-request/patch</li>
<li><a
href="a55fd2d847"><code>a55fd2d</code></a>
nix: 2.32.0 -> 2.32.1</li>
<li>See full diff in <a
href="7ab6e7fd29...fd24c48048">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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps
[namespacelabs/nscloud-setup-buildx-action](https://github.com/namespacelabs/nscloud-setup-buildx-action)
from 0.0.19 to 0.0.20.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/namespacelabs/nscloud-setup-buildx-action/releases">namespacelabs/nscloud-setup-buildx-action's
releases</a>.</em></p>
<blockquote>
<h2>v0.0.20</h2>
<h2>What's Changed</h2>
<ul>
<li>Add wait-for-builder input to eagerly start build clusters and wait
for them by <a
href="https://github.com/annervisser"><code>@annervisser</code></a> in
<a
href="https://redirect.github.com/namespacelabs/nscloud-setup-buildx-action/pull/11">namespacelabs/nscloud-setup-buildx-action#11</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/annervisser"><code>@annervisser</code></a>
made their first contribution in <a
href="https://redirect.github.com/namespacelabs/nscloud-setup-buildx-action/pull/11">namespacelabs/nscloud-setup-buildx-action#11</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/namespacelabs/nscloud-setup-buildx-action/compare/v0...v0.0.20">https://github.com/namespacelabs/nscloud-setup-buildx-action/compare/v0...v0.0.20</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="91c2e65377"><code>91c2e65</code></a>
Merge pull request <a
href="https://redirect.github.com/namespacelabs/nscloud-setup-buildx-action/issues/11">#11</a>
from namespacelabs/add-wait-for-builder-input-to-eager...</li>
<li><a
href="459dd43ae1"><code>459dd43</code></a>
Add wait-for-builder input to eagerly start build clusters and wait for
them</li>
<li>See full diff in <a
href="7020d7d8e6...91c2e65377">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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Related to #111
This adds the necessary logic and data for the `PageList` data structure
to keep track of **total length** of the screen, **offset** into the
viewport, and **length** of the viewport. These three values are
necessary to _render_ a scrollbar. This PR updates the renderer to grab
this information but stops short of actually drawing a scrollbar (which
we'll do with native UI), in the interest of having a PR that doesn't
contain too many changes.
**This doesn't yet draw a scrollbar, these are just the internal changes
necessary to support it.**
## Background
The `PageList` structure is very core to how we represent terminal
state. It maintains a doubly linked list of "pages" (not literally
virtual memory pages, but close). Each page stores cell information,
styles, hyperlinks, etc fully self-contained in a contiguous sets of VM
pages using offset addresses rather than full pointers. **Pages are not
guaranteed to be equal sizes.** (This is where scrollbars get difficult)
Because it is a linked list structure of non-equal sized nodes, it isn't
amenable to typical scrollbar behavior. A scrollbar needs to know: full
size, offset, and length in order to draw the scrollbar properly.
Getting these values naively is `O(N)` within the data structure that is
on the hottest IO performance path in all of Ghostty.
## Implementation
### PageList
We now maintain two cached values for **total length** and **viewport
offset**.
The total length is relatively straightforward, we just have to be
careful to update it in every operation that could add or remove rows.
I've done this and ensured that every place we update it is covered with
unit test coverage.
The viewport offset is nasty, but I came up with what I believe is a
good solution. The viewport when arbitrarily scrolled is defined as a
direct pointer to the linked list node plus a row offset into that node.
The only way to calculate offset from the top is `O(N)`.
But we have a couple shortcuts:
1. If the viewport is at the bottom (most common) or top, calculating
the offset is `O(1)`: bottom is `total_rows - active_rows`, both readily
available. And top is `0` by definition.
2. Operations on the PageList typically add or remove rows. We don't do
arbitrary linked list surgery. If we instrument those areas with delta
updates to our cache, we can avoid the `O(N)` cost for most operations,
including scrolling a scrollbar. The only expensive operation is a full,
arbitrary jump (new node pointer).
Point 1 was quick to implement, so I focused all the complexity on point
2. Whenever we have an operation that adds or removes rows (for example
pruning the scroll back, adding more, erase rows within the active area,
etc.) then I do the math to calculate the delta change required for the
offset if we've already calculated it, and apply that directly.
### Renderer
The other issue was how to notify the apprts of scrollbar state. Sending
messages on any terminal change within the IO thread is a non-option
because (1) sending messages is slow (2) the terminal changes a lot and
(3) any slowness in the IO thread slows down overall terminal
throughput.
The solution was to **trigger scrollbar notifications with the renderer
vsync**. We read the scrollbar information when we render a frame,
compare it to renderer previous state, and if the scrollbar changed,
send a message to the apprt _after the frame is GPU-renderer_.
The renderer spends _most_ of its time sleeping compared to the IO
thread, and has more opportunities for optimizing its awake time.
Additionally, there's no reason to update the scrollbar information if
the renderer hasn't rendered the new frames because the user can't even
see the stuff the scrollbar wants to scroll to. We're talking about
millisecond scale stuff here at worst but it adds up.
## Performance
No noticeable performance impact for the additional metrics:
<img width="1012" height="738" alt="image"
src="https://github.com/user-attachments/assets/4ed0a3e8-6d76-40c1-b249-e34041c2f6fd"
/>
## AI Usage
I used Amp to help audit the codebase and write tests. I wrote all the
main implementation code manually. I came up with the main design
myself. Relevant threads:
-
https://ampcode.com/threads/T-95fff686-75bb-4553-a2fb-e41fe4cd4b77#message-0-block-0
-
https://ampcode.com/threads/T-48e9a288-b280-4eec-83b7-ca73d029b4ef#message-91-block-0
## Future
This is just the internal changes necessary to _draw_ a scrollbar. There
will be other changes we'll need to add to handle grabbing and actually
jumping the scrollbar. I have a good idea of how to implement those
performantly as well.
Fixes#9191
This changes our color change operations from writing to the renderer
mailbox directly to using our `rendererMailboxWriter` function which
handles the scenario where the mailbox is full by yielding the lock,
waking up the renderer, and retrying later.
This is a known deadlock scenario we've worked around since the private
beta days, but unfortunately this slipped through and I didn't catch it
in review.
What happens here is it's possible with certain escape sequences for the
IO thread to saturate other mailboxes with messages while holding the
terminal state lock. This can happen to any thread. This ultimately
leads to starvation and all threads deadlock.
Our IO thread is the only thread that produces this kind of massive
stream of events while holding the lock, so we have helpers in it to
attempt to queue (cheap, fast) and if that fails then to yield the lock,
wakeup the target thread, requeue, and grab the lock again (expensive,
slow).
This hasn't caused any known bugs but leads to selection memory
corruption and assertion failures in runtime safe modes. When the
terminal screen changes (primary to secondary) and we have an active
dragging mode going either by moving the mouse or our selection tick
timer, we should halt.
We still keep the mouse state active which lets selection continue once
the screen switches back.
- update nixpkgs now that Zig 0.15.2 is available in nixpkgs
- drop hack that worked around compile failures on systems with more
than 32 cores
- enforce patch version of Zig
This reimplements the MAC address-aware URI parsing logic used by the
OSC 7 handler and adds an additional .raw_path option that returns the
full, unencoded path string (including query and fragment values), which
is needed for compliant kitty-shell-cwd:// handling.
Notably, this implementation takes an options-based approach that allows
these additional behaviors to be enabled at runtime. It also leverages
two std.Uri.parse guarantees:
1. Return slices point into the original text string.
2. .raw components don't require unescaping (.percent_encoded does).
The implementation is in a new 'os.uri' module because its now generic
enough to not be hostname-oriented.
We use os.uri.parseUri and its parsing options to reimplement our OSC 7
file-style URI handling. This has two advantages:
First, it fixes kitty-shell-cwd scheme handling. This scheme expects the
full, unencoded path string, whereas the file scheme expects normal URI
percent encoding. This was preventing paths containing "special" URI
characters (like "path?") from working correctly in our bash, zsh, and
elvish shell integrations, which report working directories using the
kitty-shell-cwd scheme. (fish uses file URIs, which work as expected.)
Second, we can greatly simplify our hostname and path string handling
because we can now rely on the "raw" std.Uri component form to always
provide the correct representation.
Lastly, this lets us remove the previous URI-related code from the
os.hostname module, restoring its focus to hostname-related functions.
See: #5289
This mainly allows users who have a pending update but didn't install it
for some time to re-check to see if there is something newer in the mean
time.
This might fix#9191, but since I don't have a reproduction I can't be
sure. In any case, this is a bad bug that should be fixed.
The issue is that we weren't checking our scroll timer completion state.
This meant that if `start_scroll_timer` was called multiple times within
a single loop tick, we'd enqueue our completion multiple times, leading
to various undefined behaviors.
If we don't enqueue anything else in the interim, this is safe by
chance. But if we enqueue something else, then we'd hit a queue
assertion failure and honestly I'm not totally sure what would happen.
I wasn't able to trigger the "bad" case, but I was able to trigger the
benign case very easily. Our other timers such as the renderer cursor
timer already have this protection.
Let's fix this and continue looking...
Bumps
[namespacelabs/nscloud-setup-buildx-action](https://github.com/namespacelabs/nscloud-setup-buildx-action)
from 0.0.18 to 0.0.19.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="7020d7d8e6"><code>7020d7d</code></a>
support experimental flags in setup-buildx action. (<a
href="https://redirect.github.com/namespacelabs/nscloud-setup-buildx-action/issues/10">#10</a>)</li>
<li><a
href="361d7cbc2b"><code>361d7cb</code></a>
Fix error parsing.</li>
<li><a
href="4271c3f47a"><code>4271c3f</code></a>
Robust status checking.</li>
<li><a
href="77ccfe1b7f"><code>77ccfe1</code></a>
Print exec outputs.</li>
<li><a
href="5b194a9c8d"><code>5b194a9</code></a>
Update README</li>
<li><a
href="4db0d391b6"><code>4db0d39</code></a>
Merge pull request <a
href="https://redirect.github.com/namespacelabs/nscloud-setup-buildx-action/issues/9">#9</a>
from namespacelabs/n-g-patch-1</li>
<li><a
href="feb137b9d0"><code>feb137b</code></a>
Update README.md</li>
<li><a
href="f57c281382"><code>f57c281</code></a>
Merge pull request <a
href="https://redirect.github.com/namespacelabs/nscloud-setup-buildx-action/issues/8">#8</a>
from namespacelabs/pavol/docker_buildx_ls</li>
<li><a
href="4177f4c963"><code>4177f4c</code></a>
e2e: run docker buildx ls</li>
<li>See full diff in <a
href="01628ae51e...7020d7d8e6">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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps
[softprops/action-gh-release](https://github.com/softprops/action-gh-release)
from 2.4.0 to 2.4.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/softprops/action-gh-release/releases">softprops/action-gh-release's
releases</a>.</em></p>
<blockquote>
<h2>v2.4.1</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<h3>Other Changes 🔄</h3>
<ul>
<li>fix(util): support brace expansion globs containing commas in
parseInputFiles by <a
href="https://github.com/Copilot"><code>@Copilot</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/672">softprops/action-gh-release#672</a></li>
<li>fix: gracefully fallback to body when body_path cannot be read by <a
href="https://github.com/Copilot"><code>@Copilot</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/671">softprops/action-gh-release#671</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/softprops/action-gh-release/compare/v2...v2.4.1">https://github.com/softprops/action-gh-release/compare/v2...v2.4.1</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md">softprops/action-gh-release's
changelog</a>.</em></p>
<blockquote>
<h2>2.4.1</h2>
<h2>What's Changed</h2>
<h3>Other Changes 🔄</h3>
<ul>
<li>fix(util): support brace expansion globs containing commas in
parseInputFiles by <a
href="https://github.com/Copilot"><code>@Copilot</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/672">softprops/action-gh-release#672</a></li>
<li>fix: gracefully fallback to body when body_path cannot be read by <a
href="https://github.com/Copilot"><code>@Copilot</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/671">softprops/action-gh-release#671</a></li>
</ul>
<h2>2.4.0</h2>
<h2>What's Changed</h2>
<h3>Exciting New Features 🎉</h3>
<ul>
<li>feat(action): respect working_directory for files globs by <a
href="https://github.com/stephenway"><code>@stephenway</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/667">softprops/action-gh-release#667</a></li>
</ul>
<h2>2.3.4</h2>
<h2>What's Changed</h2>
<h3>Bug fixes 🐛</h3>
<ul>
<li>fix(action): handle 422 already_exists race condition by <a
href="https://github.com/stephenway"><code>@stephenway</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/665">softprops/action-gh-release#665</a></li>
</ul>
<h3>Other Changes 🔄</h3>
<ul>
<li>dependency updates</li>
</ul>
<h2>2.3.3</h2>
<h2>What's Changed</h2>
<h3>Exciting New Features 🎉</h3>
<ul>
<li>feat: add input option <code>overwrite_files</code> by <a
href="https://github.com/asfernandes"><code>@asfernandes</code></a> in
<a
href="https://redirect.github.com/softprops/action-gh-release/pull/343">softprops/action-gh-release#343</a></li>
</ul>
<h3>Other Changes 🔄</h3>
<ul>
<li>dependency updates</li>
</ul>
<h2>2.3.2</h2>
<ul>
<li>fix: revert fs <code>readableWebStream</code> change</li>
</ul>
<h2>2.3.1</h2>
<h3>Bug fixes 🐛</h3>
<ul>
<li>fix: fix file closing issue by <a
href="https://github.com/WailGree"><code>@WailGree</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/629">softprops/action-gh-release#629</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="6da8fa9354"><code>6da8fa9</code></a>
release 2.4.1</li>
<li><a
href="f38efdea4c"><code>f38efde</code></a>
fix: gracefully fallback to body when body_path cannot be read (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/671">#671</a>)</li>
<li><a
href="cec1a1113b"><code>cec1a11</code></a>
fix(util): support brace expansion globs containing commas in
parseInputFiles...</li>
<li>See full diff in <a
href="aec2ec56f9...6da8fa9354">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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps
[cachix/install-nix-action](https://github.com/cachix/install-nix-action)
from 31.7.0 to 31.8.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/cachix/install-nix-action/releases">cachix/install-nix-action's
releases</a>.</em></p>
<blockquote>
<h2>v31.8.0</h2>
<h2>What's Changed</h2>
<ul>
<li>nix: 2.31.2 -> 2.32.0 by <a
href="https://github.com/github-actions"><code>@github-actions</code></a>[bot]
in <a
href="https://redirect.github.com/cachix/install-nix-action/pull/257">cachix/install-nix-action#257</a>
Release notes: <a
href="https://discourse.nixos.org/t/nix-2-32-0-released/70528">https://discourse.nixos.org/t/nix-2-32-0-released/70528</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/cachix/install-nix-action/compare/v31.7.0...v31.8.0">https://github.com/cachix/install-nix-action/compare/v31.7.0...v31.8.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="7ab6e7fd29"><code>7ab6e7f</code></a>
Merge pull request <a
href="https://redirect.github.com/cachix/install-nix-action/issues/257">#257</a>
from cachix/create-pull-request/patch</li>
<li><a
href="a851831538"><code>a851831</code></a>
nix: 2.31.2 -> 2.32.0</li>
<li><a
href="0b2de19be5"><code>0b2de19</code></a>
docs: update the ci badge</li>
<li><a
href="b8a94d3614"><code>b8a94d3</code></a>
ci: pass correct args to the act test</li>
<li><a
href="0ef05056da"><code>0ef0505</code></a>
ci: adjust oldest supported version for macos-15</li>
<li><a
href="0b43574e96"><code>0b43574</code></a>
ci: add macos-15-intel runner</li>
<li>See full diff in <a
href="9280e7aca8...7ab6e7fd29">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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Update libvaxis. The latest commit of libvaxis includes `uucode` as the
unicode
library. `uucode` has a much cleaner API and is actively developed by a
Ghostty
maintainer (@jacobsandlund). This also has the advantage of removing the
last
transitive dependency Ghostty has that is hosted on codeberg, which
separates us
from the frequent outages.
Disclosures: I used AI to debug the import issue in `boo.zig`
---------
Co-authored-by: Mitchell Hashimoto <m@mitchellh.com>
Most of the changes seem to be Tahoe UI related and now that we have a
custom UI I don't think there is anything important here but we should
update nonetheless.
This includes multiple changes to clean up the "installing" state:
- Ghostty will not confirm quit, since the user has already confirmed
they want to restart to install the update.
- If termination fails for any reason, the popover has a button to retry
restarting.
- The copy and badge symbol have been updated to better match the
reality of the "installing" state.
<img width="1756" height="890" alt="CleanShot 2025-10-12 at 15 04 08@2x"
src="https://github.com/user-attachments/assets/1b769518-e15f-4758-be3b-c45163fa2603"
/>
AI written:
https://ampcode.com/threads/T-623d1030-419f-413f-a285-e79c86a4246b fully
understood.
Resolves#8689
For various reason, ghostty wants to have a unique file extension for
the config files. The name was settled on `config.ghostty`. This will
help with tooling. See #8438 (original discussion) for more details.
This PR introduces the preferred default of `.ghostty` while still
supporting the previous `config` file. If both files exist, a warning
log is sent.
The docs / website will need to be updated to reflect this change.
> [!NOTE]
> Only tested on macOS 26.0.
---------
Co-authored-by: Mitchell Hashimoto <m@mitchellh.com>
### Background
Been running Ghostty locally for a while now, and I use the Finder
service a lot. It often confuses me which one is the official one, until
I actually open it.
### Changes
- Use blueprint to distinguish from release app, if no custom icon
specified
- Change BundleDisplayName to Ghostty[Debug]
- Enable Info.plist preprocessing for reading
`$(INFOPLIST_KEY_CFBundleDisplayName)` for providing different services
with different configurations
> (Preprocessing was once reverted
before](https://github.com/ghostty-org/ghostty/commit/6508fec), so I'm
not sure whether this follows the 'rules' here, but for now, there are
no links in the plist file, so I think it’s
[safe](https://developer.apple.com/library/archive/technotes/tn2175/_index.html#:~:text=can%20pass%20the-,%2Dtraditional,-flag%20to%20the)
to enable it
Previously, the fish shell integration interfered with fish's builtin vi
mode cursor switching configurations such as `$fish_cursor_default` and
`$fish_cursor_insert`.
```console
$ ghostty --config-default-files=false -e fish --no-config --init-command 'source "$GHOSTTY_RESOURCES_DIR"/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish; fish_vi_key_bindings'
```
The above command starts fish in vi mode with Ghostty shell
integrations. Manually loading the integration is necessary due to
`--no-config` blocking auto injection.
1. At the prompt, fish is in insert mode, and the cursor is a blinking
beam. However, press escape and then "i" to exit then re-enter insert
mode, and the cursor will be a solid beam due to the
`$fish_cursor_unknown` setting. Without the shell integration, insert
mode always uses a solid beam cursor.
2. A similar problem shows if we start fish with `fish_vi_key_bindings
default`. The cursor ends up as a blinking beam in normal mode only due
to the shell integration interfering. This glitch can also be reset away
by entering then exiting insert mode.
3. Also, `$fish_cursor_external` has no effect when used with shell
integration. After `fish_vi_key_bindings`, set it to `line`, run cat(1),
and shell integration will give you a blinking block, not the asked for
line/beam.
I verified that this patch makes the shell integration stop interfering
in three scenarios above, and it still changes the cursor when not using
fish's vi mode.
Note that `$fish_cursor_*` variables can be set when fish isn't in vi
mode, so they're not great signals for the shell integration hooks.
As pointed out in #9156, an unintended consequence of all the work to
get icon sizing right is that `adjust-icon-height` now only applies to
the small icons you get when the next cell is not whitespace. Large
icons are unaffected.
With this PR, `adjust-icon-height` affects the maximum height of every
symbol specifying the `.icon` constraint height, regardless of
constraint width. This includes most Nerd Font icons, but excludes emoji
and other unicode symbols, and also excludes terminal graphics-oriented
Nerd Font symbols such as Powerline symbols.
In the following screenshots, **Baseline** is without
`adjust-icon-height`, while **Before** and **After** are with
`adjust-icon-height = -25%`.
**Baseline**
<img width="711" height="95" alt="Screenshot 2025-10-11 at 23 28 20"
src="https://github.com/user-attachments/assets/7499db4d-75a4-4dbd-b107-8cb5849e31a3"
/>
**Before** (only small icons affected)
<img width="711" height="95" alt="Screenshot 2025-10-11 at 23 20 12"
src="https://github.com/user-attachments/assets/9afd9fbf-ef25-44cc-9d8e-c39a69875163"
/>
**After** (both small and large icons affected, but not emoji)
<img width="711" height="95" alt="Screenshot 2025-10-11 at 23 21 05"
src="https://github.com/user-attachments/assets/90999f59-3b43-4684-9c8e-2c3c1edd6d18"
/>
### This PR depends on #9162#1691 doesn't seem to be an issue anymore, but changing appearance while
Ghosty is active will result in similar nastiness.
https://github.com/user-attachments/assets/fcd7761e-a521-4382-8d7a-9d93dc0806bc
### Changes
- [Sequoia/Ventura] Fix flickering new tab icon, and it also didn't
respond to window's key status change correctly
- [Sequoia/Ventura] Fix after changing appearance, tab bar may disappear
or have inconsistent background colour
- Fix initial tint of reset zoom button on Sequoia/Ventura with
`macos-titlebar-style=tabs` and all `native/transparent` titlebars
- Fix title alignment with custom font with `native/transparent`
titlebar
Of course #9142 would require a minor follow-up!
* Scale groups can cut across patch sets, but not across fonts. We had
some scale group mixing between Font Awesome and the weather symbols,
which is removed by this PR.[^cp_table_full]
* There's one case where a scale group includes a glyph that's not part
of any patch sets, just for padding out the group bounding box.
Previously, an unrelated glyph from a different font would be pulled in.
Now we use an appropriate stand-in. (See code comment for details.)
* I noticed overlaps weren't being split between each side of the
bounding box, they were added to both sides, resulting in twice as much
padding as specified.
Screenshots showing the extra vertical padding for progress bar elements
due to the second bullet point:
**Before**
<img width="191" height="42" alt="Screenshot 2025-10-11 at 15 33 54"
src="https://github.com/user-attachments/assets/cf288cce-86d3-46fd-ae86-18e5c274b0e4"
/>
**After**
<img width="191" height="42" alt="Screenshot 2025-10-11 at 15 33 20"
src="https://github.com/user-attachments/assets/7ac799c7-bf50-4e65-a74a-f8a2c42d2441"
/>
[^cp_table_full]: Forming and using the merged `cp_table_full` table
should have been a red flag. Such a table doesn't make sense, it would
be a one-to-many map. You need the names of the original fonts to
disambiguate.
Fixes#9076
**Before**
<img width="128" height="57" alt="Screenshot 2025-10-11 at 00 07 09"
src="https://github.com/user-attachments/assets/a6b416d5-dae1-4cea-a836-00640ceaf39b"
/>
**After**
<img width="128" height="57" alt="Screenshot 2025-10-11 at 00 07 31"
src="https://github.com/user-attachments/assets/7d2df7b1-4767-4e2d-84d2-8301da5c6602"
/>
These screenshots show the chevrons mentioned in
https://github.com/ghostty-org/ghostty/discussions/7820#discussioncomment-14617170,
which should be scaled as a group but were not until this PR.
The added code downloads each individual symbol font file from the Nerd
Fonts github repo (making sure to get the version corresponding to the
vendored `font-patcher.py`) and iterates over all of them to build the
correct and complete codepoint mapping table. The table is saved to
`nerd_font_codepoint_tables.py`, which `nerd_font_codegen.py` will reuse
if possible instead of downloading the font files again.
I'm not going to utter any famous last words or anything, but... after
this, I don't think the number of remaining issues with icon
scaling/alignment is _large._
Clicking on desktop notifications sent by Ghostty _should_ cause the
window that sent the notification to come to the top. However, because
the notification that was sent targeted the wrong surface (apprt surface
vs core surface) and the window did not call `present()` on itself the
window would never be brought to the surface, the correct tab would not
be selected, etc.
Fixes#9145
Log messages will include the problematic CSS, simplifying debugging.
Especially helpful since some of our CSS is generated at runtime so it
could be difficult to examine the CSS "source".
```
info(gtk_ghostty_application): loading gtk-custom-css path=/home/ghostty/dev/ghostty/x.css
warning(gtk_ghostty_application): css parsing failed at <data>:2:3-14: gtk-css-parser-error-quark 4 No property named "border-poop"
* {
border-poop: 0;
warning(gtk_ghostty_application): css parsing failed at <data>:1:3-3:1: gtk-css-parser-warning-quark 1 Unterminated block at end of document
* {
border-poop: 0;
```
vs:
```
info(gtk_ghostty_application): loading gtk-custom-css path=/home/ghostty/dev/ghostty/x.css
warning(glib): WARNING: Gtk: Theme parser error: <data>:2:3-14: No property named "border-poop"
warning(glib): WARNING: Gtk: Theme parser warning: <data>:1:3-3:1: Unterminated block at end of document
```
I want to see #7932 get merged, so applied the latest proposed patch.
Will close if the original PR gets some traction, as I do _not_ know Zig
nor this project.
Co-authored-by: rhodes-b <59537185+rhodes-b@users.noreply.github.com>
If an update is available, you can now trigger the full download,
install, and restart from a single command palette action. This allows
for a fully keyboard-driven update process.
While an update is being installed, an option to cancel or skip the
current update is also shown as an option, so that can also be
keyboard-driven.
This currently can't be bound to a keyboard action, but that may be
added in the future if there's demand for it.
**AI Disclosure:** Amp was used considerably. I reviewed all the code
and understand it.
## Demo
https://github.com/user-attachments/assets/df6307f8-9967-40d4-9a62-04feddf00ac2
Sorry, I'm living dangerously here and haven't started a discussion.
New ghostty user. When working interactively with SQL clients you're
often writing semi-colons at the end of statements, e.g. `select * from
table;`
It's super annoying when you double-click to select the word `table` it
actually selects `table;` Anecdotally, this behaviour disagrees with
other terminals I've tried (tho not exhaustive).
Disclosure: Claude wrote this code but, ironically, I "assisted it" by
pointing to the file and function after uncovering issue #30 and
relevant PR.
Fixes#2638Fixes#9056
This changes our update check and notification system to be fully
unobtrusive if a terminal window is open. If a terminal window is open,
update system notifications now appear in the titlebar or the bottom
right corner of the window (if the titlebar is unavailable for any
reason). Importantly, this means no more window popups!
If a terminal window is not open, then explicit update checks will open
the existing, standard, dedicated window that currently exists. This
only triggers for manual update checks while the window is not open,
though. Or at least, that is the intention, I'm not sure if I got all
the logic there 100% correct.
**AI disclosure:** I used Amp considerably on the path to this fix.
Sorry, I wanted to use Codex due to the source, but I wanted to get this
fix out quickly and used a tool I was more familiar with. I manually
modified most of the code and understand it all.
## Demo
Update flows are complex and can do many things, so I built a simulator
to test the various states. This section will show videos of this.
### Happy Path (Full Update Check and Install)
https://github.com/user-attachments/assets/0d9c3396-cad1-4f13-b247-0fcc7382b47e
### Happy Path (No titlebar)
https://github.com/user-attachments/assets/839ffc20-b2d2-459b-9558-29f0f233d7a2
### No Update Available
https://github.com/user-attachments/assets/44650a98-c39b-4119-a8d0-64c21e06b79f
### Error
https://github.com/user-attachments/assets/ab27fe8c-4dd9-48f2-ad4f-23928d6a6829
### First Launch Permission Check
Note: This would show up automatically, not manually triggered.
https://github.com/user-attachments/assets/0add869e-eea8-4600-b119-4a236e77c4bf
## TODO
- [x] Fix progress percentage causing width wiggling
- [x] Fix padding from edges to be aligned on top/bottom and right
Previous PR: #8535 (merged but problem persists)
Issues: #5934
The Ghostty window will always start with the title "Ghostty" at
startup, and then immediately change to the correct window title. This
is a problem when using compositors like Hyprland and Niri if you want
to create rules for floating windows and similar, as the window title
isn't detected at startup.
This fixes the bad behaviour both for title configured in the config
file, and for processes started with the --title argument.
In this fix I've updated the `tags.zig` `closureComputedTitle()`
function to get the title from the passed in config, and use that as a
fallback before the default `Ghostty` fallback.
Previous behaviour as logged by `niri msg event-stream`:
> Window opened or changed: Window { id: 19, title: Some("Ghostty"),
app_id: Some("com.mitchellh.ghostty-debug"), pid: Some(802495),
workspace_id: Some(1), is_focused: true, is_floating: false, is_urgent:
false, layout: WindowLayout { pos_in_scrolling_layout: Some((3, 1)),
tile_size: (2266.0, 1365.0), window_size: (2266, 1365),
tile_pos_in_workspace_view: None, window_offset_in_tile: (0.0, 0.0) } }
Window layouts changed: [(6, WindowLayout { pos_in_scrolling_layout:
Some((4, 1)), tile_size: (2266.0, 1365.0), window_size: (2266, 1365),
tile_pos_in_workspace_view: None, window_offset_in_tile: (0.0, 0.0) })]
Window opened or changed: Window { id: 19, title:
Some("pr-test-title-fix"), app_id: Some("com.mitchellh.ghostty-debug"),
pid: Some(802495), workspace_id: Some(1), is_focused: true, is_floating:
false, is_urgent: false, layout: WindowLayout { pos_in_scrolling_layout:
Some((3, 1)), tile_size: (2266.0, 1365.0), window_size: (2266, 1365),
tile_pos_in_workspace_view: None, window_offset_in_tile: (0.0, 0.0) } }
New behaviour:
> Window opened or changed: Window { id: 20, title:
Some("pr-test-title-fix"), app_id: Some("com.mitchellh.ghostty-debug"),
pid: Some(804534), workspace_id: Some(1), is_focused: true, is_floating:
false, is_urgent: false, layout: WindowLayout { pos_in_scrolling_layout:
Some((3, 1)), tile_size: (2266.0, 1365.0), window_size: (2266, 1365),
tile_pos_in_workspace_view: None, window_offset_in_tile: (0.0, 0.0) } }
Window layouts changed: [(6, WindowLayout { pos_in_scrolling_layout:
Some((4, 1)), tile_size: (2266.0, 1365.0), window_size: (2266, 1365),
tile_pos_in_workspace_view: None, window_offset_in_tile: (0.0, 0.0) })]
This fixes the problem as shown in the output. I have only tested this
on Linux (Arch with Niri).
Release notes at:
https://github.com/vancluever/z2d/blob/v0.9.0/CHANGELOG.md
This release brings our Zig 0.15.x branch into main, now that Ghostty is
on it too.
Additionally, this adds major speedups to the default path (filling with
a solid color using the default operator).
Release notes at:
https://github.com/vancluever/z2d/blob/v0.9.0/CHANGELOG.md
This release brings our Zig 0.15.x branch into main, now that Ghostty is
on it too.
Additionally, this adds major speedups to the default path (filling with a
solid color using the default operator).
Adds the Metal Toolchain as a required Xcode component for building
Ghostty. Also updates the notes about Xcode 26 now that it and Tahoe are
out of Beta.
Bumps
[softprops/action-gh-release](https://github.com/softprops/action-gh-release)
from 2.3.4 to 2.4.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/softprops/action-gh-release/releases">softprops/action-gh-release's
releases</a>.</em></p>
<blockquote>
<h2>v2.4.0</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<h3>Exciting New Features 🎉</h3>
<ul>
<li>feat(action): respect working_directory for files globs by <a
href="https://github.com/stephenway"><code>@stephenway</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/667">softprops/action-gh-release#667</a></li>
</ul>
<h3>Other Changes 🔄</h3>
<ul>
<li>chore(deps): bump the npm group with 2 updates by <a
href="https://github.com/dependabot"><code>@dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/668">softprops/action-gh-release#668</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/softprops/action-gh-release/compare/v2.3.4...v2.4.0">https://github.com/softprops/action-gh-release/compare/v2.3.4...v2.4.0</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md">softprops/action-gh-release's
changelog</a>.</em></p>
<blockquote>
<h2>2.4.0</h2>
<h2>What's Changed</h2>
<h3>Exciting New Features 🎉</h3>
<ul>
<li>feat(action): respect working_directory for files globs by <a
href="https://github.com/stephenway"><code>@stephenway</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/667">softprops/action-gh-release#667</a></li>
</ul>
<h2>2.3.4</h2>
<h2>What's Changed</h2>
<h3>Bug fixes 🐛</h3>
<ul>
<li>fix(action): handle 422 already_exists race condition by <a
href="https://github.com/stephenway"><code>@stephenway</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/665">softprops/action-gh-release#665</a></li>
</ul>
<h3>Other Changes 🔄</h3>
<ul>
<li>dependency updates</li>
</ul>
<h2>2.3.3</h2>
<h2>What's Changed</h2>
<h3>Exciting New Features 🎉</h3>
<ul>
<li>feat: add input option <code>overwrite_files</code> by <a
href="https://github.com/asfernandes"><code>@asfernandes</code></a> in
<a
href="https://redirect.github.com/softprops/action-gh-release/pull/343">softprops/action-gh-release#343</a></li>
</ul>
<h3>Other Changes 🔄</h3>
<ul>
<li>dependency updates</li>
</ul>
<h2>2.3.2</h2>
<ul>
<li>fix: revert fs <code>readableWebStream</code> change</li>
</ul>
<h2>2.3.1</h2>
<h3>Bug fixes 🐛</h3>
<ul>
<li>fix: fix file closing issue by <a
href="https://github.com/WailGree"><code>@WailGree</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/629">softprops/action-gh-release#629</a></li>
</ul>
<h2>2.3.0</h2>
<ul>
<li>Migrate from jest to vitest</li>
<li>Replace <code>mime</code> with <code>mime-types</code></li>
<li>Bump to use node 24</li>
<li>Dependency updates</li>
</ul>
<h2>2.2.2</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="aec2ec56f9"><code>aec2ec5</code></a>
release 2.4.0</li>
<li><a
href="4db716b167"><code>4db716b</code></a>
feat: respect working_directory for files globs; add input and tests (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/667">#667</a>)</li>
<li><a
href="14820f2cee"><code>14820f2</code></a>
chore(deps): bump the npm group with 2 updates (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/668">#668</a>)</li>
<li>See full diff in <a
href="62c96d0c4e...aec2ec56f9">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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
Adds the Metal Toolchain as a required Xcode component for building
Ghostty. Also updates the notes about Xcode 26 now that it and Tahoe are
out of Beta.
OSC 133;A can have:
- special_key
- click_events
OSC 133;C can have:
- cmdline
- cmdline_url
Notably, they are in use by `fish`. Not sure what other shells currently
use these options.
Note that the options are only parsed. Nothing further is done with them
at this point.
OSC 133;A can have:
- special_key
- click_events
OSC 133;C can have:
- cmdline
- cmdline_url
Notably, they are in use by `fish`. Not sure what other shells currently
use these options.
Note that the options are only parsed. Nothing further is done with them
at this point.
This fix was found by Claude Code, but I manually reviewed this change
and removed extraneous changes made by the AI tool.
Co-authored-by: moderation <michael@sooper.org>
This modernizes `KeyEncoder` to a new `std.Io.Writer`-based API.
Additionally, instead of a single struct, it is now an `encode` function
that takes a series of more focused options. This is more idiomatic Zig
while also making it easier to expose via libghostty-vt.
libghostty-vt Zig module also gains access to key encoding APIs. The C
APIs will follow another time.
Converting the KeyEncoder tests was done using AI:
https://ampcode.com/threads/T-9731bbdc-e0a9-41ad-9404-2b781a66ee39
Reviewed and understood.
This modernizes `KeyEncoder` to a new `std.Io.Writer`-based API.
Additionally, instead of a single struct, it is now an `encode` function
that takes a series of more focused options. This is more idiomatic Zig
while also making it easier to expose via libghostty-vt.
libghostty-vt also gains access to key encoding APIs.
This moves our paste logic to `src/input` in preparation for exposing
this as part of libghostty-vt. This yields an immediate benefit of unit
tests for paste encoding.
Additionally, we were able to remove one allocation on every unbracketed
paste path unless the input specifically contains a newline. Unlikely to
be noticable, but nice.
NOTE: This also includes one change in behavior: we no longer encode
`\r\n` and a single `\r`, but as a duplicate `\r\r`. This matches xterm
behavior and I don't think will result in any issues since duplicate
carriage returns should do nothing in well-behaved terminals.
This moves our paste logic to `src/input` in preparation for exposing
this as part of libghostty-vt. This yields an immediate benefit of
unit tests for paste encoding.
Additionally, we were able to remove one allocation on every unbracketed
paste path unless the input specifically contains a newline. Unlikely to
be noticable, but nice.
NOTE: This also includes one change in behavior: we no longer encode
`\r\n` and a single `\r`, but as a duplicate `\r\r`. This matches xterm
behavior and I don't think will result in any issues since duplicate
carriage returns should do nothing in well-behaved terminals.
Since #8999, `macos-custom-icon` works when its a fully expanded
absolute path like `/Users/username/dir/icon.icns`, but not when it's
abbreviated as `~/dir/icon.icns`. Users were understandably surprised
and confused by this. This PR adds tilde expansion using `NSString`s
built-in property for this.
Also removed a line from the config docs that seemed erroneous. Given
that the option has a functional default, it seems incorrect to say that
it's required.
As promised in #8990.
I opted for hardcoded metrics and bounding boxes rather than actually
loading fonts and glyphs, both to avoid backend dependence and limit the
focus to the constraint calculations themselves, and because I wanted to
test a case that isn't exhibited by any of the fonts available in the
repo.
This also fixes an error from #8990, probably due to a botched
cherry-pick or rebase.
This is my final set of fixes to the font patcher/icon scaling code. It
builds on #8563 and there's not much reason to pay attention here until
that one has been reviewed (the unique changes in this PR only touch the
two `nerd_font_*` files; the other 8 files in the diff are just #8563).
However, I wanted to make sure the full set of changes/fixes I propose
are out in the open, such that any substantial edits by maintainers
(like in #7953) can take into account the full context.
I think this and the related patches should be considered fixes, not
features, so I hope they can be considered for a 1.2.x release.
This PR fixes some bugs in the extraction of scale and alignment rules
from the `font_patcher` script. Roughly in order of importance:
* Nerd fonts apply an offset to some codepoint ranges when extracting
glyphs from their original font (e.g., Font Awesome) and placing them in
a Nerd Font. Rules are specified in terms of the former codepoints, but
must be applied to the latter. This offset was previously not taken into
account, so rules were applied to the wrong glyphs, and some glyphs that
should have rules didn't get any.
* Previously, the rules from every single patch set was included, but
the embedded Symbols Only font doesn't contain all of them. Most
importantly, there's a legacy patch set that only exists for historical
reasons and is never used anymore, which was overwriting some other
rules because of overlapping codepoint ranges. Also, the Symbols Only
font contains no box drawing characters, so those rules should not be
included. With this PR, irrelevant patch sets are filtered out.
* Some patch sets specify overlapping codepoint ranges, though in
reality the original fonts don't actually cover the full ranges and the
overlaps just imply that they're filling each other's gaps. During font
patching, the presence/absence of a glyph at each codepoint in the
original font takes care of the ambiguity. Since we don't have that
information, we need to hardcode which patch set "wins" for each case
(it's not always the latest set in the list). Luckily, there are only
two cases.
* Many glyphs belong to scale groups that should be scaled and aligned
as a unit. However, in `font_patcher`, the scale group is _not_ used for
_horizontal_ alignment, _unless_ the entire scale group has a single
advance width (remember, the original symbol fonts are not monospace).
This PR implements this rule by only setting `relative_width` and
`relative_x` if the group is monospace.
There are some additional tweaks to ensure that each codepoint actually
gets the rule it's supposed to when it belongs to multiple scale groups
or patch sets, and to avoid setting rules for codepoints that don't
exist in the embedded font.
Follow-up to #8563, which broke scaling without alignment. This change
recovers the behavior from before #8563, such that a scaled group is
clamped to the constraint width and height if necessary, and otherwise,
scaling does not shift the center of the group bounding box.
As a part of this change, horizontal alignment was rewritten to assume
the face is flush with the left edge of the cell. The cell-to-face
offset in the rendering code is then applied regardless of the value of
`align_horizontal`. This both simplifies the code and improves
consistency, as it ensures that the offset is the same for all
non-bitmap glyphs (rounded in FreeType, not rounded in CoreText). It's
the right thing to do following the align-to-face changes in #8563.
In CoreText, when thickening (font smoothing) is enabled or Ghostty is
synthesizing a bold face, the glyph bounding box is padded to make sure
the thicker glyph can fit. Currently, this happens before applying
constraints (scaling and alignment), which makes the size and position
of constrained glyphs dependent on font size, font thickening strength,
and display DPI.
With this PR, constraints are applied before any other adjustments, and
padding is applied directly to the rasterization canvas without
modifying any metrics.
For consistency, I also moved constraint application above emboldening
in the FreeType code, although under that API, the two operations are
orthogonal as far as I can tell.
Secondly, this PR moves glyph centering above bitmap quantization, as
centering is generally fractional and will therefore undo the quantizing
if done after.
Supersedes #8552.
In Freetype, measure rect after emboldening, so constraints apply to the
true glyph size like in CoreText.
In CoreText, don't let font smoothing affect the rect (only the canvas).
This adds a new script we can manually run that downloads all the files
that need to be uploaded to the mirror and updates our build.zig.zon.
The upload still happens manually [by me] but this simplifies the task
greatly.
This adds a new script we can manually run that downloads all the files
that need to be uploaded to the mirror and updates our build.zig.zon.
The upload still happens manually [by me] but this simplifies the task
greatly.
Replaces #8372
Before merge I'm going to squash this and give @pluiedev coauthor, since
I took a lot of her work. I just have to go through this myself to make
sure I learn all the changes in Zig 0.15, but as I got things, I copy
and pasted her work in. My work is probably less thorough and there are
places we can convert deprecated things, probably, but this results in
green CI.
## Benchmarks
It looks like there are some speed regressions in isolated places. I'm
not sure if this is noise or not, I'm going to keep running some tests.
If someone can check macOS that'd be helpful (my vtebench is down on
macOS atm), cc @qwerasd205 if interested.
On an x86_64 system:

Fixes#8991
Uses OSC 133 esc sequences to keep track of how long commands take to
execute. If the user chooses, commands that take longer than a user
specified limit will trigger a notification. The user can choose between
a bell notification or a desktop notification.
Signal handlers are connected to surface objects in two spots - when a
tab is added to a page and when the split tree changes. This resulted in
duplicate signal handlers being added for each surface. This was most
noticeable when copying the selection to the clipboard - you would see
two "Copied to clipboard" toasts. Ensure that there is only one signal
handler by removing any old ones before adding the new ones.
You can pretty simply reproduce a crash on `main` in `Debug` mode by
running `printf "مرحبًا \n"` with your primary font set to one that
supports Arabic such as Cascadia Code/Mono or Kawkab Mono, which will
cause CoreText to output the shaped glyphs non-monotonically which hits
the assert we have in the renderer.
In `ReleaseFast` this assert is skipped and because we already moved
ahead to the space glyph (which belongs at the end but is emitted first)
all of the glyphs up to that point are lost. I believe this is probably
the cause of #8280, I tested and this change seems to fix it at least.
Included in this PR is a little optimization: we were allocating buffers
to copy glyphs etc. from runs to every time, even though CoreText
provides `CTRunGet*Ptr` functions which get *pointers* to the internal
storage of these values- these aren't guaranteed to return a usable
pointer but in that case we can always fall back to allocating again.
Also avoided allocation while processing glyphs by ensuring capacity
beforehand immediately after creating the `CTLine`.
The performance impact of this PR is negligible on my machine and
actually seems to be positive, probably due to avoiding allocations if I
had to guess.
The solution we had before worked in most cases but there were some
which caused problems still. This is what HarfBuzz's CoreText shaper
backend does, it uses a CTTypesetter with the forced embedding level
attribute. This fixes the failure case I found that was causing non-
monotonic outputs which can have all sorts of unexpected results, and
causes a crash in Debug modes because we assert the monotonicity while
rendering.
Reduce potential allocation while processing glyphs by ensuring capacity
in the buffer ahead of time and also using CTRunGet*Ptr functions first
and only allocating for those if that didn't work (it should almost
always work in practice.)
We never used it because our minidump files on Linux didn't contain
meaningful information. With Zig's Writergate, let's drop this and
rewrite it later, we can always resurrect it from the git history.
Rejoice @pluiedev
We never used it because our minidump files on Linux didn't contain
meaningful information. With Zig's Writergate, let's drop this and
rewrite it later, we can always resurrect it from the git history.
Fixes#8991
Uses OSC 133 esc sequences to keep track of how long commands take to
execute. If the user chooses, commands that take longer than a user
specified limit will trigger a notification. The user can choose between
a bell notification or a desktop notification.
#8829 fixed the interaction of Powerline glyphs with other symbols, but
regressed in the handling of the Powerline glyphs themselves by letting
them get caught in an early exit that imposes a constraint width of 1.
This PR fixes the regression and adds corresponding tests. Tried to be
somewhat principled about why the special treatment is warranted, hence
the new helper function `isGraphicsElement`.
**Before**
<img width="270" height="44" alt="Screenshot 2025-10-02 at 00 16 54"
src="https://github.com/user-attachments/assets/9e975434-114c-44d5-a4ed-ac6a954b9d00"
/>
**After**
<img width="270" height="44" alt="Screenshot 2025-10-02 at 00 16 11"
src="https://github.com/user-attachments/assets/20545e74-c9f9-4a6b-9bf0-a7cf1d38c3a0"
/>
Zig 0.15 removed the ability to compress from the stdlib, which makes
porting our framegen tool to Zig 0.15+ more work than it's worth. We
already depend on and have the ability to build zlib, and Zig is a full
blown C compiler, so let's just use C.
The framegen C program doesn't free any memory, because it is meant to
exit quickly. It otherwise behaves pretty much the same as the old Zig
codebase.
The build scripts were modified to build the C program and run it, but
also to include the framedata in the generated source tarball so that
downstream packagers don't have to do this (although they'll have all
the deps anyways).
**AI disclosure:** The Zig to C conversion was done by Amp. I know C and
reviewed the code. The build system improvements are partially done by
Amp as well but I finished it all out. I've reviewed everything. Full
thread:
https://ampcode.com/threads/T-4e0a80af-3d2a-48d6-bf59-3e2f05eb7673
Zig 0.15 removed the ability to compress from the stdlib, which makes
porting our framegen tool to Zig 0.15+ more work than it's worth. We
already depend on and have the ability to build zlib, and Zig is a full
blown C compiler, so let's just use C.
The framegen C program doesn't free any memory, because it is meant to
exit quickly. It otherwise behaves pretty much the same as the old Zig
codebase.
The build scripts were modified to build the C program and run it, but
also to include the framedata in the generated source tarball so that
downstream packagers don't have to do this (although they'll have all
the deps anyways).
This fixes the lazyImport importing outside of the build root. The build
root for the build binary is always the root `build.zig` (which we
want), but our `src/build_config.zig` transitively imported SharedDeps
which led to issues with Zig 0.15.
This back ports to main early (outside our Zig 0.15 PR) because its
compatible and will simplify that one.
This fixes the lazyImport importing outside of the build root. The build
root for the build binary is always the root `build.zig` (which we
want), but our `src/build_config.zig` transitively imported SharedDeps
which led to issues.
Since we now use uucode, we don't need ziglyph anymore. Ziglyph was kept
around as a test-only dep so we can verify matching but this is
complicating our Zig 0.15 upgrade because ziglyph doesn't support Zig
0.15. Let's just drop it.
cc @jacobsandlund
Since we now use uucode, we don't need ziglyph anymore. Ziglyph was kept
around as a test-only dep so we can verify matching but this is
complicating our Zig 0.15 upgrade because ziglyph doesn't support Zig
0.15. Let's just drop it.
I used the new CPU counter mode in Instruments.app to track down
functions that had instruction delivery bottlenecks (indicating i-cache
misses) and picked a bunch of trivial functions to mark as inline (plus
a couple that are only used once or twice and which benefit from
inlining).
The size of `macos-arm64/libghostty-fat.a` built with `zig build
-Doptimize=ReleaseFast -Dxcframework-target=native` goes from
`145,538,856` bytes on `main` to `145,595,952` on this branch, a
negligible increase.
These changes resulted in some pretty sizable improvements in vtebench
results on my machine (Apple M3 Max):
<img width="983" height="696" alt="image"
src="https://github.com/user-attachments/assets/cac595ca-7616-48ed-983c-208c2ca2023f"
/>
With this, the only vtebench test we're slower than Alacritty in (on my
machine, at 130x51 window size) is `dense_cells` (which, IMO, is so
artificial that optimizing for it might actually negatively impact real
world performance).
I also did a pretty simple improvement to how we copy the screen in the
renderer, gave it its own page pool for less memory churn. Further
optimization in that area should be explored since in some scenarios it
seems like as much as 35% of the time on the `io-reader` thread is spent
waiting for the lock.
> [!NOTE]
> Before this is merged, someone really ought to test this on an x86
processor to see how the performance compares there, since this *is*
tuning for my processor specifically, and I know that M chips have
pretty big i-cache compared to some x86 processors which could impact
the performance characteristics of these changes.
A whole bunch of inline annotations, some of these were tracked down
with Instruments.app, others are guesses / just seemed right because
they were trivial wrapper functions.
Regardless, these changes are ultimately supported by improved vtebench
results on my machine (Apple M3 Max).
Changes it so that the renderer retains its own MemoryPool for PageList
pages so that new pages rarely need to be allocated when cloning the
screen. Also switches to using an arena allocator in `updateFrame` to
avoid having to deinit the cloned screen since instead we can just throw
out the memory.
- Provide SONAME versioned shared libraries with major version numbers
to separate ABI breaks
Adding a SemanticVersion to the shared library's module generates
symlings for `libghostty-vt.so.<major version>` and
`libghostty-vt.so.<major>.<minor>`
```
> zig build
> ls zig-out/lib/
libghostty-vt.so@ libghostty-vt.so.0@ libghostty-vt.so.0.1.0*
> readelf -d zig-out/lib/libghostty-vt.so
Dynamic section at offset 0x452858 contains 25 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x0000000000000001 (NEEDED) Shared library: [ld-linux-x86-64.so.2]
0x000000000000000e (SONAME) Library soname: [libghostty-vt.so.0]
...
```
Major versions are important for allowing multiple versions of a library to co-exist and let the SONAME section in an ELF binary request a specific major version of a shared library.[^1]
I believe the rules for updating SONAME versions match the SemVer spec for ABI changes.
> 1. MAJOR version when you make incompatible API changes
> 2. MINOR version when you add functionality in a backward compatible manner
> 3. PATCH version when you make backward compatible bug fixes
[^2]
[^1]: https://www.man7.org/conf/lca2006/shared_libraries/slide5d.html
[^2]: https://semver.org/#summary
Closes#8791
Discussion: #8657
### Summary
This adds the FocusTerminalIntent App Intent and related function
(focusSurface), allows external tools (such as Shortcuts/Siri) to
programmatically move focus to a specific terminal window or tab.
### Verification
This functionality has been tested across following scenarios,
confirming correct focus behavior for:
- Split Window
- Tab Group
- Quick Terminal
### Note
It is not supported to move focus to a split that is hidden by a zoomed
split. The same applies to the CloseTerminalIntent.
### AI Disclosure
This pull request was made with assistance from Claude Code.
I reviewed all AI-generated code and wrote the final output manually.
Some bell features should be triggered on the receipt of every BEL
character, namely `audio` and `system`. However, Ghostty was setting a
boolean to `true` upon the receipt of the first BEL. Subsequent BEL
characters would be ignored until that boolean was reset to `false`,
usually by keyboard/mouse activity.
This PR fixes the problem by ensuring that the `audio` and `system`
features are triggered every time a BEL is received. Other features
continue to be triggered only when the `bell-ringing` boolean state
changes.
Fixes#8957
Some bell features should be triggered on the receipt of every BEL
character, namely `audio` and `system`. However, Ghostty was setting a
boolean to `true` upon the receipt of the first BEL. Subsequent BEL
characters would be ignored until that boolean was reset to `false`,
usually by keyboard/mouse activity.
This PR fixes the problem by ensuring that the `audio` and `system`
features are triggered every time a BEL is received. Other features
continue to be triggered only when the `bell-ringing` boolean state
changes.
Fixes#8957
Fixes https://github.com/ghostty-org/ghostty/discussions/8697 by making
`OK` the suggested default and activating it by default.
Previously `OK` was `destructive` which imo is not a good approach for
just setting a terminal title.
Follow-up to #8720 adding
* Two improvements to FreeType glyph measurements:
- Ensuring that glyphs are measured with the same hinting as they are
rendered, ref
[#8720#issuecomment-3305408157](https://github.com/ghostty-org/ghostty/pull/8720#issuecomment-3305408157);
- For outline glyphs, using the outline bbox instead of the built-in
metrics, like `renderGlyph()`.
* Basic unit tests for face metrics and their estimators, using the
narrowest and widest fonts from the resource directory, Cozette Vector
and Geist Mono.
---
I also made one unrelated change to `freetype.zig`, replacing
`@alignCast(@ptrCast(...))` with `@ptrCast(@alignCast(...))` on line
173. Autoformatting has been making this change on every save for weeks,
and reverting the hunk before each commit is getting old, so I hope it's
OK that I use this PR to upstream this decree from the formatter.
Powerline glyphs were treated as whitespace, giving the preceding cell a
constraint width of 2 and cutting off icons in people's prompts and
statuslines. It is however correct to not treat Powerline glyphs like
other Nerd Font symbols; they should simply be treated as normal
characters, just like their relatives in the block elements unicode
block.
This resolves
https://discord.com/channels/1005603569187160125/1417236683266592798
(never promoted to an issue, but real and easy to reproduce).
**Tip**
<img width="215" height="63" alt="Screenshot 2025-09-21 at 16 57 58"
src="https://github.com/user-attachments/assets/81e770c5-d688-4d8e-839c-1f4288703c06"
/>
**This PR**
<img width="215" height="63" alt="Screenshot 2025-09-21 at 16 58 42"
src="https://github.com/user-attachments/assets/5d2dd770-0314-46f6-99b5-237a0933998e"
/>
The constraint width logic was untested but contains some quite subtle
interactions, so I wrote a suite of tests covering the cases I'm aware
of.
While working on this code I also resolved a TODO comment to add all the
box drawing/block element type characters to the set of codepoints
excluded from the minimum contrast settings.
> This PR will probably need a rebase on the final outcome of #8550 and
~#8552~ #8580, but I'm putting it out here so folks can begin taking a
look if they want.
This is a rewrite of the code that applies scaling and alignment
constraints. The main intention is to further improve how Nerd Font
icons are matched to the primary font. This PR aligns the calculations
more closely with how the Nerd Font `font-patcher` script works, except
in two cases where we can easily do something unambiguously better (one
because of what's arguably a bug in the script, and one because we do
multi-cell alignment with knowledge of the pixel-rounded cell grid).
A goal of the rewrite is to make the scaling and alignment calculations
as clear and easy to follow as possible.
I'll lead with some screenshots. First the status quo, then this PR.
<img width="505" height="357" alt="Screenshot 2025-09-07 at 17 23 51"
src="https://github.com/user-attachments/assets/8e3ff9fd-3b66-4d54-be38-d54cf3b6cc5b"
/><img width="505" height="357" alt="Screenshot 2025-09-07 at 17 20 39"
src="https://github.com/user-attachments/assets/84fbe076-2e3f-4879-b9b2-91ce86b9ef5f"
/>
Relevant specs: macOS; 1920x1080; Ghostty config:
```ini
font-family = "CommitMono"
font-size = "15"
adjust-cell-height = "+20%"
```
**Points to note**
* Icons are generally larger, making better use of the available space.
* Icons are aligned nearly a pixel lower, better matching the text. This
is because alignment is now calculated from face metrics/bearings, not
the pixel-rounded cell. (See more below.)
* Relative sizes are better matched. Note especially that tall and
narrow icons, like the git branch symbol and icons depicting sheets of
paper, look conspicuously small in the status quo. With this PR, they're
better matched to other icons.
* Look at the letter Z icon I use as prompt character for zsh. It's
_tiny_ in the status quo, but properly sized with this PR. This
demonstrates the most important and clear-cut improvement we make over
`font-patcher`. (See more below.)
* Icons wider than a single cell are now left-aligned rather than
centered across two cells. I think this is preferable and makes better
use of space in most relevant contexts.
- Consider a Neovim bufferline showing the buffer title as a filetype
icon followed by the file name. Padding on the left would be a waste of
space, but having that extra space on the right can improve legibility.
- In listings, such as in the screenshots, columns look tidier when
their left edges are straight rather than ragged.
- This is how `font-patcher` does alignment, and thus what Nerd Font
users and UI designers expect.
**Implementation details**
I won't get too deep in the weeds here; see the code and comments. In
brief:
* `size_horizontal` and `size_vertical` are combined to a single `size`,
which can be `.none, .stretch, .fit, .cover` or `.fit_cover1`. The
latter implements the `pa` rule from `font-patcher`, except it works
better for icons that are small before scaling, like the letter Z prompt
in the screenshots. In short, it preserves aspect ratio while clamping
the size such that the icon `.cover`s at least one cell and `.fit`s
within the available space. See code comments and
ryanoasis/nerd-fonts/pull/1926 for details.
* An alignment mode `.center1` is added, implementing the centering rule
from `font-patcher` that I explained/defended above. In short, we center
the icon _in the first cell_, even it's allowed to span multiple cells.
For icons wider than a single cell, the lower bound that prevents them
from protruding to the left kicks in and turns this into left-alignment.
We keep the regular `.center` rule around for use with emojis, et
cetera.
* Scaling and alignment calculations only use the unrounded face metrics
and bearings. This ensures that pixel rounding of the cell and baseline,
and `adjust-cell-{width,height}`, don't affect scaling or relative
alignment; the icons are always scaled and aligned to the _face_. (The
one place we need to use cell metrics in the calculations is when we use
`cell_width` to obtain the inter-cell padding needed to correctly center
or right-align a glyph across two cells.)
- We can do this with impunity because we're blessed with sprite glyphs
in place of the "icons" that are actually box drawing and block graphics
characters 🙌
**Guide**
The meat of the changes is 100 % in `src/font/face.zig` and
`src/font/nerd_font_codegen.py`. Changes to other files only amount to
a) adding/changing some struct fields to get numbers to where they need
to be (see `src/font/Metrics.zig`), and b) collateral updates to make
otherwise unchanged code and tests work with/take advantage of the
modified structs.
Most files should have a clear and friendly diff. The exception is the
bottom half of `src/font/face.zig`, where the diff is meaningless and
the new code should just be reviewed on its own merits. This is the part
where the `constrain` function is rewritten and refactored. Scarred by
countless hours perusing `font-patcher`, I tried hard to make the math
and logic easy to follow here. I hope I have succeeded 🤞
Fixes#8944
When we drag the only tab out of the tab overview, this triggers an
`n-pages` signal with 0 pages. If we close the window in this state, it
causes both Ghostty to exit AND the drag/drop to fail. Even if we
pre-empt Ghostty exiting by modifying the application class, the
drag/drop still fails and the application leaks memory and enters a bad
state.
The solution is to keep the window open if we go to `n-pages == 0` and
we have the tab overview open.
Interestingly, if you click to close the final tab from the tab
overview, Adwaita closes the tab overview so it still triggers the
window closing behavior (this is good, this is desired).
Fixes#8944
When we drag the only tab out of the tab overview, this triggers
an `n-pages` signal with 0 pages. If we close the window in this state,
it causes both Ghostty to exit AND the drag/drop to fail. Even if we
pre-empt Ghostty exiting by modifying the application class, the
drag/drop still fails and the application leaks memory and enters a bad
state.
The solution is to keep the window open if we go to `n-pages == 0` and
we have the tab overview open.
Interestingly, if you click to close the final tab from the tab
overview, Adwaita closes the tab overview so it still triggers the
window closing behavior (this is good).
Resolves Issue: #8670
Now precision and discrete scrolling can be scaled independently.
Supports following configuration,
```code
# Apply everywhere
mouse-scroll-multiplier = 3
# Apply separately
mouse-scroll-multiplier = precision:0.1,discrete:3 (default)
# Also it's order agnostic
mouse-scroll-multiplier = discrete:3,precision:2
# Apply one, default other
mouse-scroll-multiplier = precision:2
```
The default precision value is set 0.1, as it felt natural to me at
least on my track-pad. I've also set the min clamp value precision to
0.1 as 0.01 felt kind of useless to me but I'm unsure.
This adds functions to extra data out of OSC commands, but it is mostly
boilerplate since I only added one valid data extraction. 😄 The example
was updated to show this.
This also changes OSC strings to be null-terminated to ease lib-vt
integration. This shouldn't have any practical effect on terminal
performance, but it does lower the maximum length of OSC strings by 1
since we always reserve space for the null terminator.
This also changes OSC strings to be null-terminated to ease lib-vt
integration. This shouldn't have any practical effect on terminal
performance, but it does lower the maximum length of OSC strings by 1
since we always reserve space for the null terminator.
Fixes various issues:
- C ABI detection was faulty, which caused some Zig programs to use the
C ABI mode and some C programs not to. Let's be explicit.
- Unit tests now tests C ABI mode.
- Build binary no longer rebuilds on any terminal change (a regression).
- Zig programs can choose to depend on the C ABI version of the terminal
lib by using the `ghostty-vt-c` module.
Fixes various issues:
- C ABI detection was faulty, which caused some Zig programs to use
the C ABI mode and some C programs not to. Let's be explicit.
- Unit tests now tests C ABI mode.
- Build binary no longer rebuilds on any terminal change (a regression).
- Zig programs can choose to depend on the C ABI version of the terminal
lib by using the `ghostty-vt-c` module.
This adds more of the OSC parsing API to the C library of
`libghostty-vt`. This adds the following:
* `ghostty_osc_next` - Push a single character into the OSC parser
* `ghostty_osc_reset` - Reset the parser state and free any temporary
memory
* `ghostty_osc_end` - End a parsing sequence and return the parsed
command
* `ghostty_osc_command_type` - Return the type of command parsed
* `examples/c-vt` is updated to use these new APIs
* Our Zig `osc.Command` tagged union is updated to use a new comptime
func to generate an ABI compatible tag for the C lib.
* **Important change:** Our Zig `osc.Parser.end` function was modified
to return a _pointer_ to the command within its own structure memory
rather than a copy. This eases the C API. This impacts the Zig side but
we ultimately copy it again [for now] in the main `terminal.Parser` so
end result is the same.
* Unit tests cover even the C lib
* Shuffled some code for maintainability
The plan is to use this opaque type with getters to ease ABI
compatibility going forward.
Related to #8924
Zig currenly has a bug where it crashes when compiling Ghostty on
systems with more than 32 cpus (See the linked issue for the gory
details). As a temporary hack, use `sched_setaffinity` on Linux systems
to limit the compile to the first 32 cores. Note that this affects the
build only. The resulting Ghostty executable is not limited in any way.
This is a more general fix than wrapping the Zig compiler with
`taskset`. First of all, it requires no action from the user or
packagers. Second, it will be easier for us to remove once the upstream
Zig bug is fixed.
Related to #8924
Zig currenly has a bug where it crashes when compiling Ghostty on
systems with more than 32 cpus (See the linked issue for the gory
details). As a temporary hack, use `sched_setaffinity` on Linux systems
to limit the compile to the first 32 cores. Note that this affects the
build only. The resulting Ghostty executable is not limited in any way.
This is a more general fix than wrapping the Zig compiler with
`taskset`. First of all, it requires no action from the user or
packagers. Second, it will be easier for us to remove once the upstream
Zig bug is fixed.
Hi!
I'm a full Zig noob but [Mitchell's recent
post](https://mitchellh.com/writing/libghostty-is-coming) made me want
to clone the repo and take a look at the tooling.
My first attempt at running examples though VSCode failed because the
latest version of Zig is 0.15.1, but Ghostty requires Zig 0.14*. When
configuring the extension to use a compatible version if Zig, it
suggested pinning the version in a .zigversion file. I'm not familiar
with the pattern, but if it can help someone else's onboarding, I
figured I'd open a PR to suggest the change.
Cheers
*edit: I had a hard time figuring that out
Fix provided by @jcollie
The swift `open_config` action was triggering an allocation error
`error(gpa): Allocation size 41 bytes does not match free size 40.`.
> A string that was created as a `[:0]const u8` was cast to `[]const u8`
and then freed. The sentinel is the off-by-one.
@jcollie
For full context, see
https://discord.com/channels/1005603569187160125/1420367156071239820
Co-authored-by: Jeffrey C. Ollie <jcollie@dmacc.edu>
This is nicer because it only sets the filetype if it hasn't already
been set. :setf[iletype] has been available since vim version 6.
See: https://vimhelp.org/options.txt.html#%3Asetf
Bumps
[namespacelabs/nscloud-cache-action](https://github.com/namespacelabs/nscloud-cache-action)
from 1.2.17 to 1.2.18.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="7baedde84b"><code>7baedde</code></a>
Merge pull request <a
href="https://redirect.github.com/namespacelabs/nscloud-cache-action/issues/36">#36</a>
from namespacelabs/niklas-docs</li>
<li><a
href="72a48b9085"><code>72a48b9</code></a>
fix docs links</li>
<li><a
href="f5a0d9e059"><code>f5a0d9e</code></a>
Merge pull request <a
href="https://redirect.github.com/namespacelabs/nscloud-cache-action/issues/35">#35</a>
from namespacelabs/niklas-workflow</li>
<li><a
href="7bb48a0f26"><code>7bb48a0</code></a>
skip bundle exec</li>
<li><a
href="32d69b87dc"><code>32d69b8</code></a>
Merge pull request <a
href="https://redirect.github.com/namespacelabs/nscloud-cache-action/issues/34">#34</a>
from namespacelabs/niklas-apple-caches</li>
<li><a
href="a49d9d84e9"><code>a49d9d8</code></a>
Add experimental cache modes for Xcode, Swift, Ruby and CocoaPods</li>
<li>See full diff in <a
href="a289cf5d2f...7baedde84b">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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
This is nicer because it only sets the filetype if it hasn't already
been set. :setf[iletype] has been available since vim version 6.
See: https://vimhelp.org/options.txt.html#%3Asetf
Bumps
[cachix/install-nix-action](https://github.com/cachix/install-nix-action)
from 31.6.2 to 31.7.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/cachix/install-nix-action/releases">cachix/install-nix-action's
releases</a>.</em></p>
<blockquote>
<h2>v31.7.0</h2>
<h2>What's Changed</h2>
<ul>
<li>
<p>feat: set up the environment based on the installer shell scripts by
<a href="https://github.com/sandydoo"><code>@sandydoo</code></a> in <a
href="https://redirect.github.com/cachix/install-nix-action/pull/251">cachix/install-nix-action#251</a></p>
<p>Configures the following environment variables:</p>
<ul>
<li><code>NIX_PROFILES</code></li>
<li><code>NIX_SSL_CERT_FILE</code> (if not set)</li>
</ul>
<p>Adds the bin directory from the user's profile to
<code>$PATH</code>.</p>
</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/cachix/install-nix-action/compare/v31.6.2...v31.7.0">https://github.com/cachix/install-nix-action/compare/v31.6.2...v31.7.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="9280e7aca8"><code>9280e7a</code></a>
fix: use -e to check for certs</li>
<li><a
href="effa594a17"><code>effa594</code></a>
fix: simplify setting the user profile</li>
<li><a
href="eb0f6c7357"><code>eb0f6c7</code></a>
ci: document where to find available images to test against</li>
<li><a
href="6676c23a71"><code>6676c23</code></a>
ci: add ubuntu-22.04-arm</li>
<li><a
href="cbf4b16d11"><code>cbf4b16</code></a>
Merge pull request <a
href="https://redirect.github.com/cachix/install-nix-action/issues/251">#251</a>
from cachix/fix-env</li>
<li><a
href="29a4dac2fa"><code>29a4dac</code></a>
tests: refactor tests to run under a single matrix</li>
<li><a
href="7449e8905b"><code>7449e89</code></a>
tests: improve env tests and move to tests dir</li>
<li><a
href="d487f94a7a"><code>d487f94</code></a>
lint</li>
<li><a
href="581a134122"><code>581a134</code></a>
refactor: document ssl-cert-file vs NIX_SSL_CERT_FILE</li>
<li><a
href="d914f6d9e8"><code>d914f6d</code></a>
refactor: drop ssl handling for unsupported platforms</li>
<li>Additional commits viewable in <a
href="a809471b5c...9280e7aca8">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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
Fixes#8849
Previously, the `parseAutoStruct` function that was used to parse
generic structs for the config simply split the input value on commas
without taking into account quoting or escapes. This led to problems
because it was impossible to include a comma in the value of config
entries that were parsed by `parseAutoStruct`. This is particularly
problematic because `ghostty +show-config --default` would produce
output like the following:
```
command-palette-entry = title:Focus Split: Next,description:Focus the next split, if any.,action:goto_split:next
```
Because the `description` contains a comma, Ghostty is unable to parse
this correctly. The value would be split into four parts:
```
title:Focus Split: Next
description:Focus the next split
if any.
action:goto_split:next
```
Instead of three parts:
```
title:Focus Split: Next
description:Focus the next split, if any.
action:goto_split:next
```
Because `parseAutoStruct` simply looked for commas to split on, no
amount of quoting or escaping would allow that to be parsed correctly.
This is fixed by (1) introducing a parser that will split the input to
`parseAutoStruct` into fields while taking into account quotes and
escaping. And (2) changing the `ghostty +show-config` output to put the
values in `command-palette-entry` into quotes so that Ghostty can parse
it's own output.
`parseAutoStruct` will also now parse double quoted values as a Zig
string literal. This makes it easier to embed control codes, whitespace,
and commas in values.
Resolves#8890
If you try to create the config file when the directory already exists,
you (I) get an error that the _file_ path already exists.
```
warning(config): error creating template config file err=error.PathAlreadyExists
```
Even though the file does not exist. By changing the API entry point,
this error goes away.
I have no solid explanation for why this change works.
| State | Old Behavior | New Behavior |
|--------|--------|--------|
| A config file exists | N/A | N/A |
| No config file, no directory | create directory and config file | N/A
|
| No config file, yes directory | fail to create on config file | create
config file |
This behavior is confirmed on my macOS 26 machine. It is the least
intrusive change I could make, and in all other situations should be a
no-op.
This adds the boilerplate necessary for `libghostty-vt` the C library.
> [!IMPORTANT]
>
> This _does not expose almost any APIs_. The point of this PR is to
setup our boilerplate, build system, docs system, etc. for the
libghostty-vt C lib.
- Adds a `zig build lib-vt` target to _only_ build `libghostty-vt`
- Adds the beginning of `include/ghostty-vt.h` for the C API
- Adds a full custom allocator interface to the C API mimicking Zig
custom allocators
- Adds an example in `example/c-vt` that builds a pure C program and
links to our shared library and calls functions
- Adds the `osc_parser_new/free` C APIs just as a proof of concept that
things work
- Adds a basic Doxygen config so we have _something_ (I'm not at all
committed to Doxygen, but want us to doc from the beginning)
- Updates CI to test building the shared library for macOS, Windows, and
Linux (yes, it builds for Windows!)
**Note:** To use the `dep.artifact` function provided by Zig, we must
install the artifact. But this means that every `zig build` now includes
`libghostty-vt`. That... could be completely fine, but it's something to
consider for packagers.
## Bikeshed
We're at a pivotal point where we must define the general _style_ of our
C API.
This includes the very bike shed things such as capitalization styling,
but also general API form.
ABI compatibility will eventually be important.
I'm very much open and would love to receive feedback form more
experience C programmers on what they feel would constitute a good API.
I've consumed _many_ C APIs but I haven't provided many directly.
cc @gpanders
Fixes#8849
Previously, the `parseAutoStruct` function that was used to parse
generic structs for the config simply split the input value on commas
without taking into account quoting or escapes. This led to problems
because it was impossible to include a comma in the value of config
entries that were parsed by `parseAutoStruct`. This is particularly
problematic because `ghostty +show-config --default` would produce
output like the following:
```
command-palette-entry = title:Focus Split: Next,description:Focus the next split, if any.,action:goto_split:next
```
Because the `description` contains a comma, Ghostty is unable to
parse this correctly. The value would be split into four parts:
```
title:Focus Split: Next
description:Focus the next split
if any.
action:goto_split:next
```
Instead of three parts:
```
title:Focus Split: Next
description:Focus the next split, if any.
action:goto_split:next
```
Because `parseAutoStruct` simply looked for commas to split on, no
amount of quoting or escaping would allow that to be parsed correctly.
This is fixed by (1) introducing a parser that will split the input
to `parseAutoStruct` into fields while taking into account quotes and
escaping. And (2) changing the `ghostty +show-config` output to put the
values in `command-palette-entry` into quotes so that Ghostty can parse
it's own output.
`parseAutoStruct` will also now parse double quoted values as a Zig
string literal. This makes it easier to embed control codes, whitespace,
and commas in values.
This makes a `ghostty-vt` Zig module available from our `build.zig` that
contains a reusable Zig API version of our core terminal emulation layer
including escape sequence parsing, terminal state, and screen state.
This is the groundwork for phase one of my "libghostty" vision.
With SIMD disabled, `ghostty-vt` has no dependencies -- not even on libc
-- and can produce fully static standalone binaries. With SIMD enabled,
`ghostty-vt` only depends on libc.
The point of this PR is primarily to get the bug fixes I found in and to
get this running in CI on every commit so that we don't regress it. In
the future we'll do more (see the future section below).
> [!WARNING]
> **The API is extremely not stable and will definitely change in the
future.** The _functionality/logic_ is very stable, because it's the
same core logic used by Ghostty, but the API itself is not at all. For
this PR, we mostly just expose everything and we'll reshape the API
later.
## What is `libghostty-vt`?
I've stated my vision for a `libghostty` for some time. You can find
background on that. Recently, I've realized that the _scope_ of
`libghostty` is way too large to ship as a single unit. To that end,
`libghostty` will be split into smaller scoped sub-libraries (that may
depend on each other for higher level functionality). The exact mapping
is being worked out.
**The first library I'm extracting is `libghostty-vt` (both Zig and C,
this PR starts with Zig).** This will be a library focused only on core
terminal emulation, terminal state, and screen state. It lacks rendering
support and input handling.
**But why?** The core terminal emulation is the primary source of both
missing functionality and bugs within terminal emulators. Look at this
[simple bug in jediterm](https://github.com/JetBrains/jediterm/pull/311)
that fails to parse a trivially common sequence resulting in horrendous
misrenders. Jediterm is used by every JetBrains IDE! Literally the core
terminal in a many-millions-of-dollars business!
`libghostty-vt` is a _zero dependency_ terminal emulation layer that
exposes a C API which will let any popular language build bindings so
that we can stop reinventing the terminal emulation layer and get best
in class (or near it) terminal emulation capabilities everywhere.
## In This PR
- `ghostty-vt` Zig module
- Example usage of it in `example/zig-vt`
- CI to run Zig module tests, test that our examples build, and test
SIMD on/off
- New feature build flag `-Dsimd` (default on) that turns SIMD on or off
- Unexposed feature flag that allows building the core terminal logic
without regex support (default on right now jus for the ghostty-vt
module as I figure out what our future regex story is in a post-oni
world).
- Fixes for non-SIMD builds
## Future
There's a lot to do in the future outside of this PR:
- Define a more stable Zig API
- Define a C API at all
- Figure out our regex engine story
- Documentation improvements
This fixes test failures when Ghostty's core is run without libc.
Ghostty in the real world (all built executables) require libc so this
bug has never been hit before, but I'm working on a libc-less core and
this caused real test failures (so its already tested, as well).
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm)
from 1.2.3 to 1.2.4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vmactions/freebsd-vm/releases">vmactions/freebsd-vm's
releases</a>.</em></p>
<blockquote>
<p>add 15.0</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="487ce35b96"><code>487ce35</code></a>
Update from base vm</li>
<li><a
href="81b0e85cca"><code>81b0e85</code></a>
Generated from base-vm</li>
<li><a
href="4f22df8b8d"><code>4f22df8</code></a>
add 15</li>
<li><a
href="11ecb20fda"><code>11ecb20</code></a>
Update from base vm</li>
<li><a
href="ec67fecd18"><code>ec67fec</code></a>
Generated from base-vm</li>
<li><a
href="709b36f060"><code>709b36f</code></a>
Update version to</li>
<li>See full diff in <a
href="05856381fa...487ce35b96">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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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/install-nix-action](https://github.com/cachix/install-nix-action)
from 31.6.1 to 31.6.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/cachix/install-nix-action/releases">cachix/install-nix-action's
releases</a>.</em></p>
<blockquote>
<h2>v31.6.2</h2>
<h2>What's Changed</h2>
<ul>
<li>nix: 2.31.1 -> 2.31.2 by <a
href="https://github.com/github-actions"><code>@github-actions</code></a>[bot]
in <a
href="https://redirect.github.com/cachix/install-nix-action/pull/256">cachix/install-nix-action#256</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/cachix/install-nix-action/compare/v31...v31.6.2">https://github.com/cachix/install-nix-action/compare/v31...v31.6.2</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="a809471b5c"><code>a809471</code></a>
Merge pull request <a
href="https://redirect.github.com/cachix/install-nix-action/issues/256">#256</a>
from cachix/create-pull-request/patch</li>
<li><a
href="d5f1c043d0"><code>d5f1c04</code></a>
nix: 2.31.1 -> 2.31.2</li>
<li>See full diff in <a
href="7be5dee142...a809471b5c">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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
Replaces #8786
The author of the original PR used AI agents to create that PR. To the
extent that this PR borrows code from that PR (mostly in the tests) AI
was used in the creation of this PR.
e0d6 and e0d7 were left out.
Also collapsed everything to a single range; unlikely that the unused
gaps (e0c9, e0cb, e0d3, e0d5) would be used for something else in any
font that ships Powerline glyphs.
...not whitespace. Powerline glyphs can be considered an extension of
the Block Elements unicode block, which is neither whitespace nor
symbols (icons).
This ensures that characters immediately followed by a powerline glyph
are constrained to a single cell (unlike the current behavior where a PL
glyph is considered whitespace), while symbols (icons) immediately
preceded by a powerline glyph are not (unlike if a PL glyph were
considered a symbol). This resolves
https://discord.com/channels/1005603569187160125/1417236683266592798
This makes more dependencies lazy. This has a practical effect of
reducing the number of dependencies that need to be downloaded when
running certain zig build steps.
This is all limited because we're blocked on an upstream Zig issue:
https://github.com/ziglang/zig/issues/21525 This prevents us from fully
avoiding downloading many dependencies, but at least they're relatively
small.
One major improvement here is the usage of `lazyImport` for
`zig-wayland` that prevents downloading `zig_wayland` unconditionally on
all platforms. On macOS, we don't download this at all anymore.
Another, weirder change is that all our transitive dependencies are now
marked lazy (e.g. glslang's upstream source) even if the glslang build
always requires it. This was necessary because without this, even if we
simply referenced glslang in the root build.zig, it would force the
source package to download unconditionally. This no longer happens.
cc @pluiedev Minor improvements here, doesn't change the long term plan,
but improves things in the interim.
This makes more dependencies lazy. This has a practical effect of
reducing the number of dependencies that need to be downloaded when
running certain zig build steps.
This is all limited because we're blocked on an upstream Zig issue:
https://github.com/ziglang/zig/issues/21525 This prevents us from
fully avoiding downloading many dependencies, but at least they're
relatively small.
One major improvement here is the usage of `lazyImport` for
`zig-wayland` that prevents downloading `zig_wayland` unconditionally on
all platforms. On macOS, we don't download this at all anymore.
Another, weirder change is that all our transitive dependencies are now
marked lazy (e.g. glslang's upstream source) even if the glslang build
always requires it. This was necessary because without this, even if we
simply referenced glslang in the root build.zig, it would force the
source package to download unconditionally. This no longer happens.
This was not ported to gtk-ng before old runtime was removed, breaking
shell integration on Flatpak.
This implementation is copied verbatim from old runtime.
This makes it cleaner visually where the separation of concerns is.
There is now the generic `Properties.zig`, and then the
implementation-specific `props_<impl>.zig` files. Despite Zig's lazy
analysis, I find this is much easier to understand as a human.
Doing this resulted in finding one part in `src/terminal` where we were
still inadvertently using ziglyph directly instead of our LUTs! I
switched this out.
After this PR, `src/terminal` as a standalone module no longer depends
on `ziglyph` at all.[^1]
cc @jacobsandlund this is going to cause conflicts in your PR. I'm sorry
about that. But it should make it cleaner to bring in the uucode work by
adding a dedicated `props_uucode.zig` file!
[^1]: Why would I be talking about `src/terminal` as a standalone
module? That's interesting.
This makes it cleaner to add new sources of table generation and also
avoids inadvertently depending on different modules (despite Zig's lazy
analysis).
This also fixes up terminal to only use our look up tables which avoids
bringing ziglyph in for the terminal module.
Avoid leaking snap environment values and in particular the `$SNAP*`
values to the children that we run from the terminal.
Do this programmatically instead of the launcher, since ghostty needs
know the environment it runs in, while it must not leak the info to the
children.
We've also another leak on snap, that creates a more visible problem
(wrong matching of children applications) that is the apparmor security
profile.
I've handled it at
cc3b46f600
but that requires some love in order to fully decouple the snap option
to the build, to avoid including it in non-snap builds, so an help would
be appreciated there.
> This PR was contains code (in `filterSnapPaths`) that was improved by
DeepSeek.
When running in a snap context we need to filtering out all the SNAP_*
variables out there, but this is not enough, because it's also needed
to sanitize them by ensuring that no variable containing a path pointing
to a $SNAP folder is leaked there.
Otherwise we might have (for example) XDG_RUNTIME_DIRS containing a
"private" snap path, and that will be exposed to all the applications
that will be started from ghostty
This is stomping towards minimizing our build.zig dependencies so that
it can be cached more often. Right now, touching almost any file in the
project forces the build.zig to rebuild which is destroying my
productivity.
The first set of work is to move all our completion and syntax file
generation into a data generator exe, e.g. vim, zsh, fish, etc. This
might seem like just shuffling bits but it results in a real tangible
improvement: when you run `zig build test`, we no longer have to rebuild
our `build.zig` when you for example... modify a CLI action.
This is just a small improvement. Our build.zig depends on way too much
stuff, so this PR will be draft while I continue to use commits to
separate out scopes of change.
This is stomping towards minimizing our build.zig dependencies so that
it can be cached more often. Right now, touching almost any file in the
project forces the build.zig to rebuild which is destroying my
productivity.
Fixes#8713
This stores the last closed state of the quick terminal by screen
pointer. We use a weak mapping so if a screen is unplugged we'll clear
the memory. We will not remember the size if you unplug and replug in a
monitor.
Fixes#8713
This stores the last closed state of the quick terminal by screen
pointer. We use a weak mapping so if a screen is unplugged we'll clear
the memory. We will not remember the size if you unplug and replug in a
monitor.
Fixes#8734
This forces the app icon to be set on another event loop tick from the
main startup.
In the future, we should load and set the icon completely in another
thread. It appears that all the logic we have is totally thread-safe.
Fixes#8734
This forces the app icon to be set on another event loop tick from
the main startup.
In the future, we should load and set the icon completely in another
thread. It appears that all the logic we have is totally thread-safe.
Fixes#8785
This is the callback AppKit sends when it wants to know if our
application can handle sending and receiving certain types of data.
The prior implementaiton was incorrect and would erroneously claim
support over combinations that we couldn't handle (at least, at the
SurfaceView layer).
This corrects the implementation. The services we expect still show up
and the error in 8785 goes away.
Fixes#8785
This is the callback AppKit sends when it wants to know if our
application can handle sending and receiving certain types of data.
The prior implementaiton was incorrect and would erroneously claim
support over combinations that we couldn't handle (at least, at the
SurfaceView layer).
This corrects the implementation. The services we expect still show up
and the error in 8785 goes away.
Fixes#8783
Our new tab flow will never have a previously focused window because its
triggered by a service so we need to use the "preferred parent" logic we
have to open this in the last focused window.
Fixes#8783
Our new tab flow will never have a previously focused window because its
triggered by a service so we need to use the "preferred parent" logic we
have to open this in the last focused window.
Hello
This is a small thing I noticed when building Ghostty. The logs for
SetTitle and Pwd are unreadable due to them being shown as an array of
integers, so I added a custom formatter for them to be shows as text.
Previously we're just feeding the compiler source files generated via
capturing stdout, which all by default have the filename `stdout`. Under
some particular yet uncertain circumstances in Zig 0.14 (only factor
we've found so far is a large amount of cores/compilation shards) the
compiler will actually crash on an unreachable code path that assumes
all source files either end in .zig or .zon, causing crashes for
packagers for distros like Nixpkgs and Gentoo.
Given this has been explicitly made illegal in Zig 0.15 (see
ziglang/zig#24957) I don't really see why we shouldn't fix this for 1.2
too. We have to do this at some point no matter what anyways.
Previously we're just feeding the compiler source files generated via
capturing stdout, which all by default have the filename `stdout`.
Under some particular yet uncertain circumstances in Zig 0.14 (only
factor we've found so far is a large amount of cores/compilation shards)
the compiler will actually crash on an unreachable code path that assumes
all source files either end in .zig or .zon, causing crashes for packagers
for distros like Nixpkgs and Gentoo.
Given this has been explicitly made illegal in Zig 0.15
(see ziglang/zig#24957) I don't really see why we shouldn't fix this for
1.2 too. We have to do this at some point no matter what anyways.
Maybe fixes#8736
I thought `windowDidLoad` was early on because its before the window is
shown but apparently not. Let's try `awakeFromNib` which is called just
after the window is loaded from the nib. It is hard to get any earlier
than that.
Maybe fixes#8736
I thought `windowDidLoad` was early on because its before the window is
shown but apparently not. Let's try `awakeFromNib` which is called
just after the window is loaded from the nib. It is hard to get any
earlier than that.
The documentation shows that the enum values should be "new-window" and
"new-tab",
However, "new-window" currently fails, but "window" is still accepted.
The GLSL to MSL conversion process uses a passed-in sampler state for
the `iChannel0` parameter and we weren't providing it. This magically
worked on Apple Silicon for unknown reasons but failed on Intel GPUs.
In normal, hand-written MSL, we'd explicitly create the sampler state as
a normal variable (we do this in `shaders.metal` already!), but the
Shadertoy conversion stuff doesn't do this, probably because the exact
sampler parameters can't be safely known.
This fixes a Metal validation error when using custom shaders:
```
-[MTLDebugRenderCommandEncoder validateCommonDrawErrors:]:5970: failed
assertion `Draw Errors Validation Fragment Function(main0): missing Sampler
binding at index 0 for iChannel0Smplr[0].
```
This wasn't a simple error message, the assertion would cause Xcode 26
to halt the program at that point.
The GLSL to MSL conversion process uses a passed-in sampler state for
the `iChannel0` parameter and we weren't providing it. This magically
worked on Apple Silicon for unknown reasons but failed on Intel GPUs.
In normal, hand-written MSL, we'd explicitly create the sampler state as
a normal variable (we do this in `shaders.metal` already!), but the
Shadertoy conversion stuff doesn't do this, probably because the exact
sampler parameters can't be safely known.
This fixes a Metal validation error when using custom shaders:
```
-[MTLDebugRenderCommandEncoder validateCommonDrawErrors:]:5970: failed
assertion `Draw Errors Validation Fragment Function(main0): missing Sampler
binding at index 0 for iChannel0Smplr[0].
```
This fixes a Metal validation error in Xcode when using custom shaders.
I suspect this is one part of custom shaders not working properly on
Intel macs (probably anything with a discrete GPU).
This happens to work on Apple Silicon but this is undefined behavior and
we're just getting lucky.
There is one more issue I'm chasing down that I think is also still
blocking custom shaders working on Intel macs.
This fixes a Metal validation error in Xcode when using custom shaders.
I suspect this is one part of custom shaders not working properly on
Intel macs (probably anything with a discrete GPU).
This happens to work on Apple Silicon but this is undefined behavior and
we're just getting lucky.
There is one more issue I'm chasing down that I think is also still
blocking custom shaders working on Intel macs.
Measure the ~actual height between the ascender and descender lines as
the height of the~ overall bounding box of the font's ASCII
characters[^1], and use this to upper bound the IC width estimate. This
ensures that the scaling of fallback CJK fonts to a wide-aspect primary
font doesn't result in oversized CJK glyphs.
Fixes#8709.
I'd appreciate feedback from Chinese speakers on the suitability of this
metric across a variety of primary fonts.
Screenshots using an empty Ghostty config on macOS and the test file
from #8651. The font loaded as fallback for CJK is PingFang SC.
**1.2.0**
<img width="592" height="301" alt="Screenshot 2025-09-17 at 09 51 43"
src="https://github.com/user-attachments/assets/e553885d-009a-4205-88c9-24747b195211"
/>
**This PR**
<img width="592" height="301" alt="Screenshot 2025-09-17 at 09 51 25"
src="https://github.com/user-attachments/assets/3a8e8d95-ec0a-4d23-a5f0-85b2f47253e3"
/>
[^1]: Note that this may be different from the difference between the
nominal ascent - descent, as non-letter ASCII characters often exceed
the ascender and descnder lines, and fonts often bake the line gap into
the ascent/descent and set `line_gap` to zero, as per official
recommendations like those from Google Fonts:
https://simoncozens.github.io/gf-docs/metrics.html
Fixes#8683
The selection scrolling logic should only depend on the y value of the
cursor position, not the x value. This presents unwanted scroll
behaviors, such as reversing the scroll direction which was just a side
effect of attempting to scroll tick to begin with.
Fixes#8683
The selection scrolling logic should only depend on the y value of the
cursor position, not the x value. This presents unwanted scroll
behaviors, such as reversing the scroll direction which was just a side
effect of attempting to scroll tick to begin with.
This was a very common pitfall for users. The new logic will reload the
font-size at runtime, but only if the font wasn't manually set by the
user using actions such as `increase_font_size`, `decrease_font_size`,
or `set_font_size`. The `reset_font_size` action will reset our state to
assume the font-size wasn't manually set.
This was requested by the Omarchy project since their themes also can
adjust font size. It makes sense to me.
I also updated a comment about `font-family` not reloading at runtime;
this wasn't true even prior to this commit.
This was a very common pitfall for users. The new logic will reload the
font-size at runtime, but only if the font wasn't manually set by the
user using actions such as `increase_font_size`, `decrease_font_size`,
or `set_font_size`. The `reset_font_size` action will reset our state
to assume the font-size wasn't manually set.
I also updated a comment about `font-family` not reloading at runtime;
this wasn't true even prior to this commit.
Fixes#8667
The binding `a=text:=` didn't parse properly.
This is a band-aid solution. It works and we have test coverage for it
thankfully. Longer term we should move the parser to a fully
state-machine based parser that parses the trigger first then the
action, to avoid these kind of things.
Fixes#8667
The binding `a=text:=` didn't parse properly.
This is a band-aid solution. It works and we have test coverage for it
thankfully. Longer term we should move the parser to a fully
state-machine based parser that parses the trigger first then the
action, to avoid these kind of things.
Added some prepositions not previously added and
changed a word to be more accurate to the portuguese meaning
---------
Signed-off-by: Nilton Perim Neto <niltonperimneto@gmail.com>
Config contains the command, working directory, and environment
variables intended to be passed to the new split, but it looks like we
forgot to include it as an argument in this branch.
Discussion: https://github.com/ghostty-org/ghostty/discussions/8637
Config contains the command, working directory, and environment
variables intended to be passed to the new split, but it looks like we
forgot to include it as an argument in this branch.
Discussion: https://github.com/ghostty-org/ghostty/discussions/8637
The custom VM can then be run with a command like this:
```
nix run .#nixosConfigurations.custom-vm.config.system.build.vm
```
A file named `ghostty.qcow2` will be created that is used to persist any changes
made in the VM. To "reset" the VM to default delete the file and it will be
recreated the next time you run the VM.
### Contributing new VM definitions
#### VM Acceptance Criteria
We welcome the contribution of new VM definitions, as long as they meet the following criteria:
1. They should be different enough from existing VM definitions that they represent a distinct
user (and developer) experience.
2. There's a significant Ghostty user population that uses a similar environment.
3. The VMs can be built using only packages from the current stable NixOS release.
#### VM Definition Criteria
1. VMs should be as minimal as possible so that they build and launch quickly.
Additional software can be added at runtime with a command like `nix run nixpkgs#<package name>`.
2. VMs should not expose any services to the network, or run any remote access
software like SSH daemons, VNC or RDP.
3. VMs should auto-login using the "ghostty" user.
## Nix VM Integration Tests
Several Nix VM tests are provided by the project for testing Ghostty in a "live"
environment rather than just unit tests.
Running these requires a working Nix installation, either Nix on your
favorite Linux distribution, NixOS, or macOS with nix-darwin installed. Further
requirements for macOS are detailed below.
### Linux
1. Check out the Ghostty source and change to the directory.
2. Run `nix run .#check.<system>.<test-name>.driver`. `<system>` should be
`x86_64-linux` or `aarch64-linux` (even on macOS, this launches a Linux
VM, not a macOS one). `<test-name>` should be one of the tests defined in
`nix/tests.nix`. The test will build and then launch. Depending on the speed
of your system, this can take a while. Eventually though the test should
complete. Hopefully successfully, but if not error messages should be printed
out that can be used to diagnose the issue.
3. To run _all_ of the tests, run `nix flake check`.
### macOS
1. To run the VMs on macOS you will need to enable the Linux builder in your `nix-darwin`
config. This _should_ be as simple as adding `nix.linux-builder.enable=true` to your
configuration and then rebuilding. See [this](https://nixcademy.com/posts/macos-linux-builder/)
blog post for more information about the Linux builder and how to tune the performance.
2. Once the Linux builder has been enabled, you should be able to follow the Linux instructions
above to launch a test.
### Interactively Running Test VMs
To run a test interactively, run `nix run
.#check.<system>.<test-name>.driverInteractive`. This will load a Python console
that can be used to manage the test VMs. In this console run `start_all()` to
start the VM(s). The VMs should boot up and a window should appear showing the
VM's console.
For more information about the Nix test console, see [the NixOS manual](https://nixos.org/manual/nixos/stable/index.html#sec-call-nixos-test-outside-nixos)
### SSH Access to Test VMs
Some test VMs are configured to allow outside SSH access for debugging. To