The generation counter added in bdc0b6c19 is a process-global
std.atomic.Value(u64). The Zig compiler rejects 64-bit atomic
operations on targets whose largest supported atomic size is 32 bits
(e.g. arm-linux-androideabi), which broke the libghostty-vt Android
build. This slipped past other CI targets because they're either
64-bit or compile kitty graphics out entirely (wasm32-freestanding).
The counter backing nextGeneration is now comptime-selected: 64-bit
targets keep the lock-free atomic counter, while 32-bit targets fall
back to a mutex-protected u64. This preserves the process-wide
uniqueness and monotonicity guarantees of generation stamps
everywhere. The mutex cost is irrelevant since this is a cold path,
only invoked on content mutations.
Add a generation counter to the kitty graphics image storage. Every
content mutation (image transmit/replace, placement add, delete) assigns
the storage a fresh stamp, and every image is stamped when it is added
or replaced.
This solves two problems:
First, a retransmission of the same image ID with identical dimensions
was previously undetectable by anything comparing width, height, format,
and data length; the per-image stamp changes on every add/replace, so
caches keyed on it always see the change. Second, the dirty flag was the
only storage-wide signal, and it is also set by scrolling and resizing,
which move placements without changing contents. The generation is only
bumped by content mutations, so an unchanged value means the placement
set and all image data are identical and consumers can skip re-reading
them, recomputing only placement geometry.
The generation replaces Image.transmit_time entirely: newest-image
lookup by number, eviction ordering, and the renderer's texture
staleness checks all key on it now. A monotonic counter strictly orders
transmissions where Instant-based times could collide within clock
resolution (the renderer previously assumed equal timestamps meant
identical images), and this removes a syscall and an error path per
transmission.
Delete commands now only mark a mutation (dirty flag and generation)
when they actually remove something. A delete-all runs on every screen
clear, so previously every ESC [ 2 J dirtied the image state even with
no images stored. Eviction via setLimit also now marks the state dirty,
which it previously did not.
Add a generation counter to the kitty graphics image storage. Every
content mutation (image transmit/replace, placement add, delete)
assigns the storage a fresh stamp, and every image is stamped when
it is added or replaced.
This solves two problems:
First, a retransmission of the same image ID with identical dimensions
was previously undetectable by anything comparing width, height, format,
and data length; the per-image stamp changes on every add/replace, so caches
keyed on it always see the change. Second, the dirty flag was the only
storage-wide signal, and it is also set by scrolling and resizing, which move
placements without changing contents. The generation is only bumped by content
mutations, so an unchanged value means the placement set and all
image data are identical and consumers can skip re-reading them,
recomputing only placement geometry.
The generation replaces Image.transmit_time entirely: newest-image
lookup by number, eviction ordering, and the renderer's texture
staleness checks all key on it now. A monotonic counter strictly
orders transmissions where Instant-based times could collide within
clock resolution (the renderer previously assumed equal timestamps
meant identical images), and this removes a syscall and an error
path per transmission.
Delete commands now only mark a mutation (dirty flag and
generation) when they actually remove something. A delete-all runs
on every screen clear, so previously every ESC [ 2 J dirtied the
image state even with no images stored. Eviction via setLimit also
now marks the state dirty, which it previously did not.
Both generations are exposed through libghostty-vt as
GHOSTTY_KITTY_GRAPHICS_DATA_GENERATION and
GHOSTTY_KITTY_IMAGE_DATA_GENERATION (uint64_t), and the headers now
document that stored image data is always post-inflate/post-decode:
COMPRESSION always reports NONE, FORMAT is never PNG, and DATA_PTR
is raw pixels ready for GPU upload.
uint64_t gen = 0;
ghostty_kitty_graphics_get(
graphics, GHOSTTY_KITTY_GRAPHICS_DATA_GENERATION, &gen);
if (gen == last_gen) return; // nothing changed, skip re-reads
Selection gestures are already part of the libghostty-vt C API, but the
native Zig module did not re-export the underlying terminal type. Zig
consumers that implement mouse selection had to reach into terminal
internals instead of using @import("ghostty-vt").
Re-export SelectionGesture from lib_vt alongside the other terminal
selection and screen types.
AI Disclosure: I used AI. Yes, for +1
Selection gestures are already part of the libghostty-vt C API, but the
native Zig module did not re-export the underlying terminal type. Zig
consumers that implement mouse selection had to reach into terminal
internals instead of using @import("ghostty-vt").
Re-export SelectionGesture from lib_vt alongside the other terminal
selection and screen types.
The GRAPHEMES_UTF8 row-cells getter inferred its required byte
accumulator from utf8CodepointSequenceLength, which stores the value in
u3. Multi-scalar clusters longer than seven UTF-8 bytes could overflow
that accumulator before the capacity check, causing wrong probe sizes
and allowing optimized builds to write past a caller-provided buffer.
Use usize for the required byte count so probing and capacity checks
match the later encode loop. Extend the render C API test to cover the
short combining cluster, an eight-byte flag cluster, a longer family
emoji, exact-size success, and the cap == needed - 1 no-write boundary.
The GRAPHEMES_UTF8 row-cells getter inferred its required byte
accumulator from utf8CodepointSequenceLength, which stores the
value in u3. Multi-scalar clusters longer than seven UTF-8 bytes
could overflow that accumulator before the capacity check, causing
wrong probe sizes and allowing optimized builds to write past a
caller-provided buffer.
Use usize for the required byte count so probing and capacity
checks match the later encode loop. Extend the render C API test
to cover the short combining cluster, an eight-byte flag cluster,
a longer family emoji, exact-size success, and the
cap == needed - 1 no-write boundary.
Ignore more files when releasing tip. Moved release-tip run to a
separate script so we can test against it.
### AI Disclosure
Claude did this, I reviewed the changes and asked it run the tests
locally before creating the pr.
Detect changes since the last tip with dorny/paths-filter (base: tip)
and skip the build when a push touches only files that never reach the
built artifact: all of .github (except release-tip.yml, which defines the
build/tag/publish jobs) plus docs and repo/lint/editor metadata.
Bumps
[mitchellh/vouch/action/check-issue](https://github.com/mitchellh/vouch)
from 52aec3d64655edf2fdb58f298e02da754a056daf to
baeb3bf7c7e6c12d6e33bab3870b7e936580a934.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="baeb3bf7c7"><code>baeb3bf</code></a>
Merge pull request <a
href="https://redirect.github.com/mitchellh/vouch/issues/90">#90</a>
from trag1c/lock-issues</li>
<li>See full diff in <a
href="52aec3d646...baeb3bf7c7">compare
view</a></li>
</ul>
</details>
<br />
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
</details>
I've implemented a GTK toggle (gtk-horizontal-tab-scroll) for the
2-finger tab swiping introduced in #10575.
This resolves the issue presented in #11566. Simply put, this allows
users to decide whether or not they want to use horizontal tab scrolling
or just have the events passed through. Passing through the horizontal
scroll events allows programs like Neovim to use them for horizontal
scrolling.
This PR was largely generated by Claude Code and fully reviewed/refined
by me.
Fixes#13075 where GTK app will crash if a D-Bus connection can't be
opened. If you have global keybinds set in your config, Ghostty will
crash immediately in both debug and release builds. With no global
keybinds it still crashes when you reload the config, but only in builds
with safety checks enabled, due to a failed assertion.
This problem is rooted in `GlobalShortcuts`, which implements the XDG
global shortcuts protocol. The `refresh` function is triggered every
time the config changes and once on startup. If there are global
keybinds in the config but no D-Bus connection, `refresh` will still try
to setup the global keybinds by calling the `request` method, which will
use `priv.dbus_connection.?` while the field is null. Depending on the
build mode this either fails the Zig runtime safety check immediately or
eventually causes a segmentation fault somewhere in `gio/glib` when the
null pointer is used.
Additionally, even if there are no global keybinds set, Ghostty will
still crash when the config is reloaded, because the `close` function
exits early if `dbus_connection` is null and doesn't clean up the arena
that was created in the first call to `refresh` on startup. The next
call to `refresh` will then fail the `priv.arena == null` assertion.
This only happens if built with safety checks enabled.
As a fix `close` will now always clean up the arena and `refresh` will
exit early if there is no D-Bus connection.
To easily reproduce the crash, change
`Application.startupGlobalShortcuts` (in
`src/apprt/gtk/class/application.zig`) to set the D-Bus connection to
null with `priv.global_shortcuts.setDbusConnection(null)`. Then run with
a global keybind e.g. `ghostty
--keybind="global:ctrl+o=toggle_quick_terminal"`.
#### AI Disclosure
No AI was used.
Turns out combine's `publisher(for:,object:)` retains the object! We
verified this with a test script shown below. Fix this with a manual
filter. Found by @mustafa0x.
```
import Combine
import Foundation
final class Token {
deinit { print("Token deinitialized") }
}
weak var weakToken: Token?
var publisher: NotificationCenter.Publisher?
// Create scope that will free token.
do {
let token = Token()
weakToken = token
publisher = NotificationCenter.default.publisher(
for: Notification.Name("TestNotification"),
object: token
)
}
print("Retained:", weakToken != nil)
publisher = nil
print("Released:", weakToken == nil)
```
Turns out combine's `publisher(for:,object:)` retains the object!
We verified this with a test script shown below. Fix this with a
manual filter. Found by @mustafa0x.
```
import Combine
import Foundation
final class Token {
deinit { print("Token deinitialized") }
}
weak var weakToken: Token?
var publisher: NotificationCenter.Publisher?
// Create scope that will free token.
do {
let token = Token()
weakToken = token
publisher = NotificationCenter.default.publisher(
for: Notification.Name("TestNotification"),
object: token
)
}
print("Retained:", weakToken != nil)
publisher = nil
print("Released:", weakToken == nil)
```
Changes `PageList.scroll` to use `usize` as the type for the number of
remaining rows when trying to find a specific row in the list of pages.
Previously `CellCountInt=u16` was used, but the row offset can be larger
than `maxInt(u16)=65535`, in which case the scroll operation would fail.
Fixes#13000 where scrolling does not work correctly in the GTK app with
a large scrollback history. E.g. nothing happens when you click in the
middle of the scrollbar. Note that this problem only manifests at
millions of lines of history instead of already when the row offset
overflows `u16`. The reason for that is that GTK often emits multiple
scroll adjustments, the first of which is usually close enough to the
current position to not overflow `u16`. The other scroll adjustments are
then handled by the fast path in `PageList.scroll` that works correctly.
#### AI Disclosure
No AI was used.
Bumps
[softprops/action-gh-release](https://github.com/softprops/action-gh-release)
from 3.0.0 to 3.0.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>v3.0.1</h2>
<h2>3.0.1</h2>
<ul>
<li>maintenance release with updated dependencies</li>
</ul>
</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>3.0.1</h2>
<ul>
<li>maintenance release with updated dependencies</li>
</ul>
<h2>3.0.0</h2>
<p><code>3.0.0</code> is a major release that moves the action runtime
from Node 20 to Node 24.
Use <code>v3</code> on GitHub-hosted runners and self-hosted fleets that
already support the
Node 24 Actions runtime. If you still need the last Node 20-compatible
line, stay on
<code>v2.6.2</code>.</p>
<h2>What's Changed</h2>
<h3>Other Changes 🔄</h3>
<ul>
<li>Move the action runtime and bundle target to Node 24</li>
<li>Update <code>@types/node</code> to the Node 24 line and allow future
Dependabot updates</li>
<li>Keep the floating major tag on <code>v3</code>; <code>v2</code>
remains pinned to the latest <code>2.x</code> release</li>
</ul>
<h2>2.6.2</h2>
<h2>What's Changed</h2>
<h3>Other Changes 🔄</h3>
<ul>
<li>chore(deps): bump picomatch from 4.0.3 to 4.0.4 by <a
href="https://github.com/dependabot"><code>@dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/775">softprops/action-gh-release#775</a></li>
<li>chore(deps): bump brace-expansion from 5.0.4 to 5.0.5 by <a
href="https://github.com/dependabot"><code>@dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/777">softprops/action-gh-release#777</a></li>
<li>chore(deps): bump vite from 8.0.0 to 8.0.5 by <a
href="https://github.com/dependabot"><code>@dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/781">softprops/action-gh-release#781</a></li>
</ul>
<h2>2.6.1</h2>
<p><code>2.6.1</code> is a patch release focused on restoring linked
discussion thread creation when
<code>discussion_category_name</code> is set. It fixes
<code>[#764](https://github.com/softprops/action-gh-release/issues/764)</code>,
where the draft-first publish flow
stopped carrying the discussion category through the final publish
step.</p>
<p>If you still hit an issue after upgrading, please open a report with
the bug template and include a minimal repro or sanitized workflow
snippet where possible.</p>
<h2>What's Changed</h2>
<h3>Bug fixes 🐛</h3>
<ul>
<li>fix: preserve discussion category on publish by <a
href="https://github.com/chenrui333"><code>@chenrui333</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/765">softprops/action-gh-release#765</a></li>
</ul>
<h2>2.6.0</h2>
<p><code>2.6.0</code> is a minor release centered on
<code>previous_tag</code> support for
<code>generate_release_notes</code>,
which lets workflows pin GitHub's comparison base explicitly instead of
relying on the default range.
It also includes the recent concurrent asset upload recovery fix, a
<code>working_directory</code> docs sync,
a checked-bundle freshness guard for maintainers, and clearer
immutable-prerelease guidance where
GitHub platform behavior imposes constraints on how prerelease asset
uploads can be published.</p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="718ea10b13"><code>718ea10</code></a>
release 3.0.1</li>
<li><a
href="f1a938b9d8"><code>f1a938b</code></a>
chore(deps): bump esbuild from 0.28.0 to 0.28.1 (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/802">#802</a>)</li>
<li><a
href="0066ead0de"><code>0066ead</code></a>
chore(deps): bump vite from 8.0.14 to 8.0.16 (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/806">#806</a>)</li>
<li><a
href="dc643cac62"><code>dc643ca</code></a>
chore(deps): bump the npm group with 3 updates (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/805">#805</a>)</li>
<li><a
href="85ee99b6b2"><code>85ee99b</code></a>
chore(deps): bump actions/checkout in the github-actions group (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/804">#804</a>)</li>
<li><a
href="9ed3cf9a68"><code>9ed3cf9</code></a>
chore(deps): bump the npm group with 2 updates (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/800">#800</a>)</li>
<li><a
href="3efcac8951"><code>3efcac8</code></a>
chore(deps): bump the npm group with 3 updates (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/798">#798</a>)</li>
<li><a
href="05d6b9164a"><code>05d6b91</code></a>
chore(deps): bump brace-expansion from 5.0.5 to 5.0.6 (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/797">#797</a>)</li>
<li><a
href="403a5240f3"><code>403a524</code></a>
chore(deps): bump <code>@types/node</code> from 24.12.2 to 24.12.3 in
the npm group (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/796">#796</a>)</li>
<li><a
href="437e073e78"><code>437e073</code></a>
chore(deps): bump the npm group with 4 updates (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/792">#792</a>)</li>
<li>Additional commits viewable in <a
href="b430933298...718ea10b13">compare
view</a></li>
</ul>
</details>
<br />
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
</details>