Commit Graph

17257 Commits

Author SHA1 Message Date
ghostty-vouch[bot]
3924a32556 Update VOUCHED list (#13870)
Triggered by [discussion
comment](https://github.com/ghostty-org/ghostty/discussions/13838#discussioncomment-18052745)
from @mitchellh.

Vouch: @basteez

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-17 13:22:13 +00:00
trag1c
b97b17f06b i18n: translate v1.4 strings (be) (#13771)
Continues the Belarusian (`be`) translation for v1.4 per #13766.
Translates the remaining 181 strings (mostly the newly-localized command
palette), bringing `be` to 253/253.
2026-08-17 09:13:53 +02:00
Mitchell Hashimoto
e9db8d2b0b libghostty: use custom memory pool for Wasm, reduce terminal memory by ~75% (#13865)
A custom memory pool for Wasm that grows by exactly one item size per
growth and shares the pool across the entire Wasm-module instead of
per-terminal.

Some background on why `std.heap.MemoryPool` is considered harmful for
WebAssembly:

First, the std.heap.MemoryPool grows 1.5x at each growth point. The
backing allocator for that is usually a GPA which is the BrkAllocator
for wasm. This grows by power-of-two big-allocation slots. If you pair
these together you get a massive permanent linear memory growth.

On non-wasm targets, the memory growth doesn't matter because these are
virtual memory mappings that don't cost physical memory, but wasm
doesn't work that way. Also on native targets, the syscalls to allocate
memory are very expensive (relatively), so it makes sense to allocate
large virtual memory chunks and avoid them. Again, wasm doesn't work
this way.

Second, we were using one pool per terminal. On wasm, this meant that we
paid for the free list N times. On non-wasm, this makes sense because
the synchronization overhead has so far been measurable enough under
load to be prohibitive (although, I'm still skeptical about this and
want to look into it). On wasm, we build single-threaded modules, so we
can use a global free list without any extra overhead.

## Benchmarks

80x24 terminal with 1000-line scrollack processing 16MB of plain ASCII.

| Scenario                        |    Before |     After |
| ------------------------------- | --------: | --------: |
| Fresh instance                  |  0.56 MiB |  0.56 MiB |
| First `terminal_new` (delta)    | +3.44 MiB | +0.88 MiB |
| One filled terminal (total)     |  4.00 MiB |  1.88 MiB |
| Each additional filled terminal | +3.00 MiB | +0.44 MiB |
| 5 filled terminals (total)      | 16.00 MiB |  4.06 MiB |

Throughput numbers are unchanged on wasm and native (to be expected in
the latter because this is all gated on
wasm).

Note I'm still very much optimizing the above numbers! This is just my
first big win.

**AI usage:** None used except to validate and judge.
2026-08-16 21:12:59 -07:00
Mitchell Hashimoto
492c26067c libghostty: use custom memory pool for Wasm
A custom memory pool for Wasm that grows by exactly one item size per
growth and shares the pool across the entire Wasm-module instead of 
per-terminal.

Some background on why `std.heap.MemoryPool` is considered harmful for 
WebAssembly:

First, the std.heap.MemoryPool grows 1.5x at each growth point. The backing 
allocator for that is usually a GPA which is the BrkAllocator for wasm. 
This grows by power-of-two big-allocation slots. If you pair these together 
you get a massive permanent linear memory growth. On non-wasm targets,
this doesn't matter because these are virtual memory mappings that don't
cost physical memory, but wasm doesn't work that way.

Second, we were using one pool per terminal. On wasm, this meant that 
we paid for the free list N times. On non-wasm, this makes sense because
the synchronization overhead has so far been measurable enough under
load to be prohibitive (although, I'm still skeptical about this and want
to look into it). On wasm, we build single-threaded modules, so we can use
a global free list without any extra overhead.

## Benchmarks

80x24 terminal with 1000-line scrollack processing 16MB of plain ASCII.

| Scenario                        |    Before |     After |
| ------------------------------- | --------: | --------: |
| Fresh instance                  |  0.56 MiB |  0.56 MiB |
| First `terminal_new` (delta)    | +3.44 MiB | +0.88 MiB |
| One filled terminal (total)     |  4.00 MiB |  1.88 MiB |
| Each additional filled terminal | +3.00 MiB | +0.44 MiB |
| 5 filled terminals (total)      | 16.00 MiB |  4.06 MiB |

Throughput numbers are unchanged on wasm and native (to be expected in
the latter because this is all gated on wasm).
2026-08-16 20:55:09 -07:00
Mitchell Hashimoto
8d70c5dca0 libghostty: reduce Wasm stack reservation (#13864)
Zig default's Wasm stacks to 1MB. Change it to 128 KB instead.

This removes 896 KiB from every Wasm instance's initial linear memory
reservation. That means that simply _loading_ `ghostty-vt.wasm` is down
this much.

Through various workload benchmarks of real terminal snapshots,
artificial worst case full ascii, full styled, full emoji, full mixed,
etc. workloads, I wasn't able to get a stack to go above 17 KB, so 128
KB is VERY generous. Lets start here.
2026-08-16 20:24:36 -07:00
Mitchell Hashimoto
5364e5158f libghostty: reduce Wasm stack reservation
Zig default's Wasm stacks to 1MB. Change it to 128 KB instead.

This removes 896 KiB from every Wasm instance's initial linear memory
reservation. That means that simply _loading_ `ghostty-vt.wasm` is down
this much.

Through various workload benchmarks of real terminal snapshots,
artificial worst case full ascii, full styled, full emoji, full mixed,
etc. workloads, I wasn't able to get a stack to go above 17 KB, so 128 KB
is VERY generous. Lets start here.
2026-08-16 19:42:24 -07:00
Jeffrey C. Ollie
9a770be61c build(deps): bump cachix/install-nix-action from 31.11.0 to 31.11.1 (#13863)
Bumps
[cachix/install-nix-action](https://github.com/cachix/install-nix-action)
from 31.11.0 to 31.11.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.11.1</h2>
<h2>What's Changed</h2>
<ul>
<li>nix: 2.35.1 -&gt; 2.35.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/281">cachix/install-nix-action#281</a>
Fixes a crash (<a
href="https://redirect.github.com/NixOS/nix/issues/16005"><code>Assertion
'!awake.empty()' failed</code></a>) that could abort builds.</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/cachix/install-nix-action/compare/v31.11.0...v31.11.1">https://github.com/cachix/install-nix-action/compare/v31.11.0...v31.11.1</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="13d8dd58da"><code>13d8dd5</code></a>
fix(ci): skip latest installer on x86_64-darwin</li>
<li><a
href="875018fe55"><code>875018f</code></a>
Merge pull request <a
href="https://redirect.github.com/cachix/install-nix-action/issues/281">#281</a>
from cachix/create-pull-request/patch</li>
<li><a
href="6624a11f6c"><code>6624a11</code></a>
nix: 2.35.1 -&gt; 2.35.2</li>
<li>See full diff in <a
href="630ae543ea...13d8dd58da">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cachix/install-nix-action&package-manager=github_actions&previous-version=31.11.0&new-version=31.11.1)](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>
2026-08-16 19:49:04 -05:00
dependabot[bot]
7a966438fc build(deps): bump cachix/install-nix-action from 31.11.0 to 31.11.1
Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 31.11.0 to 31.11.1.
- [Release notes](https://github.com/cachix/install-nix-action/releases)
- [Changelog](https://github.com/cachix/install-nix-action/blob/master/RELEASE.md)
- [Commits](630ae543ea...13d8dd58da)

---
updated-dependencies:
- dependency-name: cachix/install-nix-action
  dependency-version: 31.11.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-17 00:16:28 +00:00
Ilia Kravchenko
820ed688ef i18n(be): fix translation issues per review
- Drop redundant 'было' copula with short-form participles (lines 1767, 1769)
- Use short predicative form 'недаступна' (line 1780)
- 'у двух фарматах:' instead of dash-construction (line 156)
- 'Аднавіць' instead of 'Паўтарыць' for Redo (semantic pair with Undo)
- Fix 'у' → 'ў' after 'ANSI' (7 places)
- Align label/description wording for Split Zoom, Read-Only, Float on Top, Secure Input
- 'усе акны' instead of 'усе вокны' (consistent with 'акно')
- 'калі яна ёсць' instead of 'даступная' (if present ≠ available)
- Infinitive 'Дадаць' instead of imperative 'Дадайце'
2026-08-16 20:33:57 -03:00
Jeffrey C. Ollie
602497e9b9 input: skip text fallback for kitty key releases (#13861)
Key events without a kitty entry fall back to writing their UTF-8 text
directly. On GTK, keys whose unshifted keysym is a dead key or level 5
latch have no unshifted codepoint and take this path. With event type
reporting enabled, releases therefore emitted the same text as presses
and duplicated characters in applications such as Neovim.

Skip the raw text fallback for release events while retaining it for
presses and repeats. Keep the guard in the shared encoder so release
events for identified keys still retain the UTF-8 data used to derive
alternate keys.

Cover releases with and without report-all mode, and verify that repeat
events continue to emit fallback text.

  - https://github.com/ghostty-org/ghostty/discussions/12192
  - https://github.com/ghostty-org/ghostty/discussions/12084
  - https://github.com/ghostty-org/ghostty/discussions/12433
  - https://github.com/ghostty-org/ghostty/discussions/13816

## Testing

  - `zig build test-lib-vt -Dtarget=x86_64-linux-gnu`
  - `zig build -Demit-lib-vt -Dtarget=x86_64-linux-gnu`
  - `zig build`
  - Verified the regression test fails without the release guard
- Manually tested the GTK backend under Wayland/Sway and X11/XWayland,
with the GTK simple input context and ibus 1.5.34:
    - Ergo-L `!` and `'`
    - Spanish `[`, `{`, `]`, and `}`
- Presses, repeats, and both modifier-release orders in `nvim --clean`
    - Dead-key composition and cancellation
    - Unicode hexadecimal input
- Full kitty keyboard mode with `kitty +kitten show_key -m kitty`,
including composed text

## AI disclosure

OpenAI Codex assisted with investigating the reports, reviewing the GTK
and kitty input paths, extending the regression tests, running
validation, and drafting this description. I reviewed the final code,
edited this description, manually performed the tests listed above, and
understand how the change interacts with the input encoder.
2026-08-16 16:32:35 -05:00
Mitchell Hashimoto
f8856a78a2 config: preserve bytes in hex escapes (#13862)
Fixes #13855

Make the config string parser preserve hexadecimal escapes as bytes.
Previously all escaped values were encoded as Unicode codepoints.
2026-08-16 14:23:09 -07:00
Mitchell Hashimoto
29b82dd80c config: preserve bytes in hex escapes
Fixes #13855

Make the config string parser preserve hexadecimal escapes as bytes.
Previously all escaped values were encoded as Unicode codepoints.
2026-08-16 14:14:26 -07:00
Jeffrey C. Ollie
7557944b4f feat: expand tildes in config theme path to HOME (#13840)
When loading a theme from a path that includes a tilde:
```
theme="~/.cache/wal/colors-ghostty"
```
ghostty currently fails with the following error:
```
cannot include path separators unless it is an absolute path
```

This PR tries to expand the ~ of the path. If there is no ~ or expansion
fails, it falls back to the unexpanded value.
2026-08-16 15:19:16 -05:00
Mitchell Hashimoto
3790fb78fe libghostty: simplify Wasm allocation API (#13860)
Replace a bunch of type-specific Wasm allocation functions with a
generic byte allocator and reusable opaque out-parameters for pointers.
This makes it a lot more ergonomic (relatively) to use the Wasm
interface and removes a dozen or so exports.

This also updates the `ghostty_type_json` `abi` field with a maximum
alignment value that host sides can use to keep every allocation aligned
properly, easily, without hardcoding numbers.

This adds a test to verify this all works as intended and runs in CI.
2026-08-16 12:44:01 -07:00
Mitchell Hashimoto
a8e9b413f1 libghostty: simplify Wasm allocation API
Replace a bunch of type-specific Wasm allocation functions with a generic
byte allocator and reusable opaque out-parameters for pointers. This
makes it a lot more ergonomic (relatively) to use the Wasm interface
and removes a dozen or so exports.

This also updates the `ghostty_type_json` `abi` field with a maximum
alignment value that host sides can use to keep every allocation aligned
properly, easily, without hardcoding numbers.

This adds a test to verify this all works as intended and runs in CI.
2026-08-16 12:39:51 -07:00
ghostty-vouch[bot]
7b57c0a029 Update VOUCHED list (#13859)
Triggered by [discussion
comment](https://github.com/ghostty-org/ghostty/discussions/13858#discussioncomment-18043290)
from @jcollie.

Vouch: @tsacha

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-16 19:25:55 +00:00
Sacha Trémoureux
bd647035e9 input: don't emit fallback text on key release
Key events without a kitty entry fall back to writing their UTF-8 text
directly. On GTK, keys whose unshifted keysym is a dead key or level 5
latch have no unshifted codepoint and take this path. With event type
reporting enabled, releases therefore emitted the same text as presses
and duplicated characters in applications such as Neovim.

Skip the raw text fallback for release events while retaining it for
presses and repeats. Keep the guard in the shared encoder so release
events for identified keys still retain the UTF-8 data used to derive
alternate keys.

Cover releases with and without report-all mode, and verify that repeat
events continue to emit fallback text.
2026-08-16 20:42:19 +02:00
phl
68a2008b34 fix: format 2026-08-16 20:16:31 +02:00
Mitchell Hashimoto
0ba6250388 libghostty: ghostty_type_json expanded with more metadata, every enum member, packed layouts, etc. (#13856)
This PR makes `ghostty_type_json` contain more metadata necessary for
FFI without access to the C header to produce safe, ABI compliant field
access.

The existing `ghostty_type_json` didn't expose enough information:
embedders still had to hardcode enum values, tagged union values, and
packed bit layouts. For wasm this meant copying offsets and masks from
Zig internals and hoping they didn't drift. This isn't an ABI I want to
promise.

This also includes a formal JSON schema included in Doxygen docs and
used in CI to continuously validate our output structure. I'd like to
expand in the future comparing to actual C headers too.

## Examples

### Named References, Arrays

```json
"GhosttyRenderStateColors": {
  "kind": "struct",
  "size": 792,
  "align": 8,
  "fields": {
    "background": {
      "offset": 8,
      "size": 3,
      "type": "GhosttyColorRgb"
    },
    "palette": {
      "offset": 18,
      "size": 768,
      "type": "array",
      "elem": "GhosttyColorRgb",
      "count": 256
    }
  }
}
```

### Pointers

```json
"GhosttyCellsView": {
  "kind": "struct",
  "size": 16,
  "align": 8,
  "fields": {
    "ptr": {
      "offset": 0,
      "size": 8,
      "type": "pointer",
      "elem": "GhosttyCell",
      "const": true,
      "nullable": true
    },
    "len": {
      "offset": 8,
      "size": 8,
      "type": "u64"
    }
  }
}
```

### Enums

```json
"GhosttyStyleColorTag": {
  "kind": "enum",
  "size": 4,
  "align": 4,
  "underlying": "i32",
  "prefix": "GHOSTTY_STYLE_COLOR_",
  "values": {
    "NONE": 0,
    "PALETTE": 1,
    "RGB": 2,
    "TAG_MAX_VALUE": 2147483647
  }
}
```

### Packed Struct

```json
"GhosttyCell": {
  "kind": "packed",
  "size": 8,
  "align": 8,
  "underlying": "u64",
  "bits": {
    "content_tag": {
      "lsb": 0,
      "width": 2,
      "type": "GhosttyCellContentTag"
    },
    "content": {
      "lsb": 2,
      "width": 24,
      "kind": "union",
      "tag": "content_tag",
      "arms": {
        "BG_COLOR_RGB": {
          "kind": "packed",
          "width": 24,
          "bits": {
            "r": {"lsb": 0, "width": 8, "type": "u8"},
            "g": {"lsb": 8, "width": 8, "type": "u8"},
            "b": {"lsb": 16, "width": 8, "type": "u8"}
          }
        }
      }
    }
  }
}
```
2026-08-16 06:43:14 -07:00
trag1c
26df373ec8 i18n: update Hebrew translations for v1.4 (#13785)
Update the Hebrew translations in po/he.po with the 181 new strings for
v1.4, as requested in issue #13766 :)
2026-08-16 11:28:58 +02:00
phl
37174c73b0 add helper and expand also if light/dark is specified 2026-08-16 09:50:35 +02:00
Mitchell Hashimoto
0e8b7bea63 vt: expose packed cell layout
GhosttyCell was exposed as a raw integer while its manifest entry was only an alias, forcing bulk-read consumers to duplicate the internal cell bit layout.\n\nAdd reflection helpers for packed structs and tagged unions, and keep the C-facing layout metadata next to Cell itself. Extend the ABI manifest and schema with recursive bit descriptors so every content arm, including palette and RGB backgrounds, can be decoded without hardcoded masks.\n\nDocument manifest-driven cell decoding and test the metadata against Zig reflection and real cell values.
2026-08-15 21:16:34 -07:00
Mitchell Hashimoto
c75559589e libghostty: add ABI manifest schema
The ABI manifest previously had no machine-readable grammar or test that
the public export conformed to it.

Define a Draft 2020-12 schema and add a build check that executes
ghostty_type_json for native and wasm libraries before validation. Run
both forms in CI and publish the schema with the generated API docs.
2026-08-15 21:16:34 -07:00
Mitchell Hashimoto
9673a22b01 libghostty: expand ABI type metadata
The type metadata export only described extern struct layouts, leaving embedders to mirror enum values and tagged union relationships.

Describe every public C type in a versioned manifest with target and build metadata. Keep union field renames alongside their source tagged unions so the manifest uses public C names without changing Zig value layouts.
2026-08-15 21:16:34 -07:00
Mitchell Hashimoto
ad6e72ddc4 libghostty: add dedicated dirty row iteration + clear functions (#13852)
Add render state C APIs for iterating only rows that require a redraw
and for marking a completed frame clean in one call.

A one-row update in a 24-row viewport reduces dirty-row discovery from
50 calls to two, while cleanup becomes one call instead of O(N) of rows.

This lower call count is massive for environments where FFI is expensive
(Go, wasm).

The dirty next API outputs the viewport y because it jumps, unlike the
normal sequential next where its trivial for a caller to keep track.
2026-08-15 21:11:21 -07:00
Mitchell Hashimoto
16c833c5f1 libghostty: add render state structured cursor read (#13851)
A normal renderer would have to call `ghostty_render_state_get` _eight
times_ to reconstruct the cursor. In languages where FFI is expensive
(Go, wasm, etc.), this showed up in profiles of every frame.

Add a sized cursor snapshot and expose it. Also expose the existing
color snapshot through ghostty_render_state_get and remove the older
dedicated color getter.

Found during my normal Go/wasm adventures.
2026-08-15 21:08:27 -07:00
Mitchell Hashimoto
0d37f2d34d libghostty: add dedicated dirty row iteration + clear functions
Add render state C APIs for iterating only rows that require a redraw
and for marking a completed frame clean in one call.  

A one-row update in a 24-row viewport reduces dirty-row discovery from 
50 calls to two, while cleanup becomes one call instead of O(N) of rows.

This lower call count is massive for environments where FFI is expensive
(Go, wasm).

The dirty next API outputs the viewport y because it jumps, unlike the
normal sequential next where its trivial for a caller to keep track.
2026-08-15 20:59:01 -07:00
Mitchell Hashimoto
b4079f00c8 libghostty: add render state structured cursor read
A normal renderer would have to call `ghostty_render_state_get`
_eight times_ to reconstruct the cursor. In languages where FFI is
expensive (Go, wasm, etc.), this showed up in profiles of every frame.

Add a sized cursor snapshot and expose it. Also expose the existing color
snapshot through ghostty_render_state_get and remove the older
dedicated color getter.
2026-08-15 20:54:40 -07:00
Jeffrey C. Ollie
02436fd4eb Update iTerm2 colorschemes (#13850)
Upstream release:
https://github.com/mbadolato/iTerm2-Color-Schemes/releases/tag/release-20260810-152212-0173c3c
2026-08-15 20:52:04 -05:00
mitchellh
f2897f31de deps: Update iTerm2 color schemes 2026-08-16 00:25:02 +00:00
Mitchell Hashimoto
9009122953 macos: send all insertText commits as key events (#13817)
Partially addresses #13796. Extends #13222.

Previously, `insertText` commits without marked text were delivered via
`sendText`, which applies paste semantics and wraps the text in
bracketed
paste when the program enables it. macOS dictation and other input
methods often commit without marked text, so programs treated dictated
text as a paste: opencode collapsed it into a `"[Pasted ~N lines]"` chip
and Neovim applied paste-mode handling.

`insertText` is only invoked by input methods (IME, dictation, emoji
picker, character viewer); real paste operations use a separate path.
Every non-empty commit is now sent as a key event — the same path
already used for preedit commits since #13222 — so input method text
always arrives as typed input.

Typing is unaffected (the accumulator path returns earlier) and Cmd+V
pastes are unaffected. `committedPreeditTextAction` is renamed to
`committedTextAction` since it no longer only handles preedit commits.

Testing:

- 311 macOS unit tests pass.
- Manually verified on macOS 26: dictation into Opencode and Neovim
  arrives inline with no paste handling; emoji picker inserts inline;
  Chinese IME composition unchanged; dictation in Neovim normal mode now
  behaves as keystrokes, matching Terminal.app.

Notes:

- Dictated "new line" now matches Terminal.app behavior (no newline
  with typed-text semantics). The previous behavior came from the paste
  path preserving the newline; a follow-up could deliver it as an
  Enter keypress if desired.

AI usage: drafted with OMO + OpenCode + DeepSeek V4 Pro assistance;
reviewed, edited, and manually tested by the author.
2026-08-15 14:26:19 -07:00
Mitchell Hashimoto
cbe8a0ed4e cli: keep cached SSH terminfo installs current (#13844)
On a local cache miss, always send our embedded terminfo source to the
remote `tic` instead of accepting any existing entry reported by
`infocmp`.

We also version cache entries using a content-derived hash of our
embedded terminfo. Non-matching entries produce a cache miss and trigger
(re)installation.
2026-08-15 14:24:37 -07:00
Mitchell Hashimoto
e524df6c82 terminal/apc: limit glyf decode allocations (#13849)
Limit individual allocations made while decoding registered glyf
outlines to 64 KB.

Carefully crafted glyf outlines could expand into ~768KB of memory per
glossary entry, which adds up to hundreds of MB per terminal surface.
Across many terminals this could cause issues.

The 64KB number was chosen by inspecting every glyph across Apple
symbols and Noto emoji and the largest single glyph found was 40KB. So,
64KB is generous while limiting each terminal to ~68MB of RAM for max
glyph glossaries.

AI was used only to write initial tests, I rewrote em.
2026-08-15 14:24:05 -07:00
Mitchell Hashimoto
746a11c721 libghostty: much faster terminal snapshot encode and decode for wasm (#13848)
Snapshot encode is now 4-7x faster, decode is 3x faster for wasm builds.

Snapshot decode is particularly important for wasm builds because
libghostty is mainly used on web as a terminal _viewer_ and snapshots
are the best, most efficient way to ship down full terminal state.

The biggest change here is a totally custom software CRC32
implementation, which accounted for ~70% of total decode time. Native
builds on aarch64/x86_64 use dedicated hardware instructions that wasm
doesn't have. We've written a custom CRC32 impl (verified against Zig
stdlib through randomized unit tests) that goes from 0.3 GB/s to 5 GB/s
throughput in V8.

## Benchmarks

Wasm on V8:

| Workload | Encode Before | Encode After | Speedup | Decode Before |
Decode After | Speedup |
|---|---|---|---|---|---|---|
| ascii | 290 MB/s | 1182 MB/s | 4.1x | 318 MB/s | 946 MB/s | 3.0x |
| styled (sgr16) | 387 MB/s | 2771 MB/s | 7.2x | 284 MB/s | 758 MB/s |
2.7x |
| sgr-truecolor | 361 MB/s | 2382 MB/s | 6.6x | 252 MB/s | 766 MB/s |
3.0x |
| cjk | 411 MB/s | 2686 MB/s | 6.5x | 339 MB/s | 1100 MB/s | 3.2x |
| grapheme | 280 MB/s | 1117 MB/s | 4.0x | 287 MB/s | 839 MB/s | 2.9x |

Native on aarch64:

| Corpus | Mode | Before | After |
|---|---|---|---|
| ascii | encode | 40.6 ms | 24.8 ms |
| ascii | decode | 51.2 ms | 49.2 ms |
| utf8 | encode | 45.2 ms | 42.4 ms |
| utf8 | decode | 59.8 ms | 59.5 ms |

**AI usage:** Fable did everything here except write this PR and the
comments. It also wrote the commit messages in this case. I reviewed
everything.
2026-08-15 14:22:24 -07:00
Mitchell Hashimoto
433b16bcb1 terminal/apc: limit glyf decode allocations
Limit individual allocations made while decoding registered glyf
outlines to 64 KB.

Carefully crafted glyf outlines could expand into ~768KB of memory per
glossary entry, which adds up to hundreds of MB per terminal surface.
Across many terminals this could cause issues.

The 64KB number was chosen by inspecting every glyph across Apple
symbols and Noto emoji and the largest single glyph found was 40KB. So,
64KB is generous while limiting each terminal to ~68MB of RAM for max
glyph glossaries.
2026-08-15 14:18:24 -07:00
Mitchell Hashimoto
eb09bf8291 terminal/snapshot: interleave software CRC32C streams
Slicing tables removed the byte-at-a-time dependency chain, but each
16-byte fold still depends serially on the previous one, leaving the
software CRC latency-bound at roughly 2.5-3 GB/s in V8 while snapshot
payloads run through it once per direction. wasm has no carry-less
multiply, so wider tables are the only classic escape — and measuring
slicing-by-32 against interleaving showed the extra 16 KB of tables buys
nothing once the chain is hidden.

Instead, inputs of 4 KiB and up split into thirds processed as three
independent fold chains in one loop, then merge with the GF(2) zero-shift
operator: crc(A ++ B, s) = crc(B, 0) XOR zeroShift(crc(A, s), |B|). The
shift matrices are comptime, storing only even powers of two (an odd
power applies the preceding matrix twice), 4 KB total. Software CRC
throughput roughly doubles; hardware backends are untouched, so native
is unaffected (tables below are noise).

Benchmarks ("prev" is the parent commit):

| wasm      | encode prev | encode   | decode prev | decode   |
|-----------|------------:|---------:|------------:|---------:|
| ascii     |     2.12 ms |  1.73 ms |     2.59 ms |  2.17 ms |
| styled    |     2.23 ms |  1.47 ms |     6.43 ms |  5.38 ms |
| truecolor |     3.44 ms |  2.30 ms |     8.35 ms |  7.17 ms |
| cjk       |     5.99 ms |  3.89 ms |    11.39 ms |  9.50 ms |
| grapheme  |     8.33 ms |  6.94 ms |    10.89 ms |  9.24 ms |

| native | mode   | prev    | this    |
|--------|--------|--------:|--------:|
| ascii  | encode | 24.7 ms | 24.8 ms |
| ascii  | decode | 47.5 ms | 49.2 ms |
| utf8   | encode | 41.9 ms | 42.4 ms |
| utf8   | decode | 58.8 ms | 59.5 ms |
2026-08-15 14:08:19 -07:00
Mitchell Hashimoto
1359973aef terminal/snapshot: single-pass style entry codec
Style entries went through roughly five writer or reader vtable calls
each: encode wrote three 4-byte colors and two u16s separately, and
decode read 16 bytes into a stack buffer only to re-parse it through a
nested fixed reader, one small read per field. Style-heavy pages carry
hundreds of entries per page, so encode now assembles each entry in a
16-byte buffer with a single write, and decode parses the fixed-size
entry directly from a byte array. Entries additionally parse straight
from the buffered payload (ID and value together) when it is contiguous.

Inserting a decoded style also hashed twice: an explicit `lookup` before
`add`, even though `add` already returns the existing entry for repeated
values. Insert with `add` alone, taking one reference per accepted table
entry, and surrender those references through the encoded-ID remap after
grid decoding, the same scheme hyperlink entries already use. Refcount
outcomes are identical: each distinct style nets its cell references.

Benchmarks ("prev" is the parent commit):

| wasm      | encode prev | encode   | decode prev | decode   |
|-----------|------------:|---------:|------------:|---------:|
| ascii     |     2.13 ms |  2.12 ms |     2.54 ms |  2.59 ms |
| styled    |     2.29 ms |  2.23 ms |     6.75 ms |  6.43 ms |
| truecolor |     4.95 ms |  3.44 ms |    12.05 ms |  8.35 ms |
| cjk       |     6.23 ms |  5.99 ms |    11.39 ms | 11.39 ms |
| grapheme  |     8.72 ms |  8.33 ms |    11.27 ms | 10.89 ms |

| native | mode   | prev    | this    |
|--------|--------|--------:|--------:|
| ascii  | encode | 24.4 ms | 24.7 ms |
| ascii  | decode | 45.1 ms | 47.5 ms |
| utf8   | encode | 41.9 ms | 41.9 ms |
| utf8   | decode | 58.6 ms | 58.8 ms |
2026-08-15 14:04:05 -07:00
Mitchell Hashimoto
47a5182621 terminal/snapshot: skip remap tables for pages without styles
Decoding a page allocated and zeroed two full remap tables (a 128 KB
entries array plus an 8 KB seen bitmap each for styles and hyperlinks)
even when the page declared no table entries at all, which is every page
of plain scrollback. Empty tables now use a shared `.empty` remap that
allocates nothing; `get` reads it as all-unmapped through a length check.
Pages that do declare entries are unchanged.

(Leaving the entries array unzeroed behind a seen-bitmap-gated `get` was
also tried and measured no better than the plain memset, so the table
keeps its simple zero-means-unmapped representation.)

Benchmarks ("prev" is the parent commit):

| wasm      | encode prev | encode   | decode prev | decode   |
|-----------|------------:|---------:|------------:|---------:|
| ascii     |     2.06 ms |  2.13 ms |     2.70 ms |  2.54 ms |
| styled    |     2.41 ms |  2.29 ms |     6.56 ms |  6.75 ms |
| truecolor |     5.02 ms |  4.95 ms |    11.86 ms | 12.05 ms |
| cjk       |     6.03 ms |  6.23 ms |    11.60 ms | 11.39 ms |
| grapheme  |     8.36 ms |  8.72 ms |    11.39 ms | 11.27 ms |

| native | mode   | prev    | this    |
|--------|--------|--------:|--------:|
| ascii  | encode | 25.6 ms | 24.4 ms |
| ascii  | decode | 48.1 ms | 45.1 ms |
| utf8   | encode | 41.7 ms | 41.9 ms |
| utf8   | decode | 58.5 ms | 58.6 ms |
2026-08-15 14:04:05 -07:00
Mitchell Hashimoto
7c1014ef66 terminal/snapshot: resolve wide pairs in a per-row pass
Cell decoding ran wide-pair normalization inline for every decoded cell:
two neighbor loads and a switch per cell, even though the overwhelming
majority of rows contain no wide cells at all. Normalization is defined
against already-stored predecessors, so running it as an ordered pass
over the stored row afterward is exactly equivalent to interleaving it.

The word-cell decoders now accumulate the bitwise OR of the row's wire
words as they apply cells, and the pass is gated on it: rows without
wide bits are already normalized (every cell narrow), and width-four and
narrower transports cannot encode wide bits at all, so their rows skip
the check at comptime. That removes the per-cell neighbor traffic from
all styled text, which decodes through the four-byte width.

Benchmarks ("prev" is the parent commit):

| wasm      | encode prev | encode   | decode prev | decode   |
|-----------|------------:|---------:|------------:|---------:|
| ascii     |     2.18 ms |  2.06 ms |     2.68 ms |  2.70 ms |
| styled    |     2.28 ms |  2.41 ms |     6.93 ms |  6.56 ms |
| truecolor |     4.93 ms |  5.02 ms |    11.91 ms | 11.86 ms |
| cjk       |     6.11 ms |  6.03 ms |    11.72 ms | 11.60 ms |
| grapheme  |     8.28 ms |  8.36 ms |    11.38 ms | 11.39 ms |

| native | mode   | prev    | this    |
|--------|--------|--------:|--------:|
| ascii  | encode | 24.3 ms | 25.6 ms |
| ascii  | decode | 46.7 ms | 48.1 ms |
| utf8   | encode | 41.0 ms | 41.7 ms |
| utf8   | decode | 59.5 ms | 58.5 ms |
2026-08-15 14:04:05 -07:00
Mitchell Hashimoto
2aaad3ca99 terminal/snapshot: vectorize grid cell decoding
Decoding one- and two-byte cells widened them to their 8-byte words one
scalar store at a time. The bulk codec now widens sixteen transported
bytes per step with byte shuffles against a zero vector, degrading
width-two surrogate lanes to U+FFFD with a vector select, exactly
matching the scalar path. Short row tails reprocess the final full
window with overlapping stores that rewrite identical bytes.

This is a native win: the shuffles lower to NEON and take ascii decode
from 38.9 ms to 34.0 ms (measured by toggling this path at the tip of
this series). On wasm, V8 runs the scalar fallback at the same speed as
the shuffle version — the loop is store-bound either way — so the wasm
deltas below are flat.

Row decoding also drops per-field packed-struct read-modify-writes in
favor of one load and one store per row header, and rows that are fully
default (zero header byte, zero encoded cells) skip all work: decoded
pages start zeroed, which is exactly the default row and cell state.

Benchmarks ("prev" is the parent commit):

| wasm      | encode prev | encode   | decode prev | decode   |
|-----------|------------:|---------:|------------:|---------:|
| ascii     |     2.07 ms |  2.18 ms |     2.69 ms |  2.68 ms |
| styled    |     2.29 ms |  2.28 ms |     7.06 ms |  6.93 ms |
| truecolor |     4.91 ms |  4.93 ms |    11.99 ms | 11.91 ms |
| cjk       |     6.14 ms |  6.11 ms |    11.83 ms | 11.72 ms |
| grapheme  |     8.26 ms |  8.28 ms |    11.18 ms | 11.38 ms |

| native | mode   | prev    | this    |
|--------|--------|--------:|--------:|
| ascii  | encode | 24.3 ms | 24.3 ms |
| ascii  | decode | 50.9 ms | 46.7 ms |
| utf8   | encode | 41.2 ms | 41.0 ms |
| utf8   | decode | 59.5 ms | 59.5 ms |
2026-08-15 14:04:05 -07:00
Mitchell Hashimoto
593762cfa1 terminal/snapshot: batch grapheme suffix codec
Grapheme suffix encoding made two full passes over the grid (a counting
pass, then an emit pass) and issued three writer calls per entry plus one
per codepoint. Every grapheme cell owns exactly one entry in the page's
grapheme map, so the section count now comes straight from
page.graphemeCount() with no counting pass, and entries are batched
through a 4 KB buffer with one writer call per flush. The per-entry size
check moved into the emit loop; an error still cancels the whole record
before any of it is emitted, so error behavior is unchanged.

Decoding similarly parsed entry headers and codepoints with one reader
call per integer. Fully buffered payloads (the common case after the
borrowed-payload commit) now parse entry headers and codepoint runs
directly from the buffered bytes, and entries whose target cell cannot
carry a suffix discard their codepoints in bulk.

Benchmarks ("prev" is the parent commit):

| wasm      | encode prev | encode   | decode prev | decode   |
|-----------|------------:|---------:|------------:|---------:|
| ascii     |     2.09 ms |  2.07 ms |     2.67 ms |  2.69 ms |
| styled    |     2.23 ms |  2.29 ms |     7.00 ms |  7.06 ms |
| truecolor |     4.95 ms |  4.91 ms |    11.85 ms | 11.99 ms |
| cjk       |     6.03 ms |  6.14 ms |    11.67 ms | 11.83 ms |
| grapheme  |    13.08 ms |  8.26 ms |    13.28 ms | 11.18 ms |

| native | mode   | prev    | this    |
|--------|--------|--------:|--------:|
| ascii  | encode | 24.5 ms | 24.3 ms |
| ascii  | decode | 53.4 ms | 50.9 ms |
| utf8   | encode | 47.6 ms | 41.2 ms |
| utf8   | decode | 60.9 ms | 59.5 ms |
2026-08-15 14:04:05 -07:00
Mitchell Hashimoto
973f619a23 terminal/snapshot: vectorize grid row encoding
Row encoding previously made two scalar passes over every row (a backward
scan for the encoded cell count and a validation pass accumulating the
width-selection OR), then wrote a 3-byte header and per-width chunked
cells through separate writer calls.

Three changes, all bulk-codec only with the portable path unchanged:

  - scanRow computes the count and word-OR in @Vector(4, u64) strides.
    Trailing default cells are all-zero words, so the OR over the whole
    row equals the OR over the encoded prefix.
  - The per-cell wide-pair validation loop is skipped entirely when the
    OR carries no wide bits, which is every row of plain text.
  - Rows are emitted with a single reservation in the destination's spare
    buffer capacity (header plus cells, no writer calls), using explicit
    i8x16.shuffle truncation for the 1/2/4-byte cell widths. Zig 0.16
    disables loop auto-vectorization, so the previous "vectorizable"
    truncating loop was actually scalar. Destinations without buffered
    capacity (counting writers, a still-growing scratch) fall through to
    the streaming path.

Benchmarks ("prev" is the parent commit):

| wasm      | encode prev | encode   | decode prev | decode   |
|-----------|------------:|---------:|------------:|---------:|
| ascii     |     3.50 ms |  2.09 ms |     2.68 ms |  2.67 ms |
| styled    |     3.15 ms |  2.23 ms |     7.04 ms |  7.00 ms |
| truecolor |     5.27 ms |  4.95 ms |    12.04 ms | 11.85 ms |
| cjk       |     5.92 ms |  6.03 ms |    11.89 ms | 11.67 ms |
| grapheme  |    14.33 ms | 13.08 ms |    13.14 ms | 13.28 ms |

| native | mode   | prev    | this    |
|--------|--------|--------:|--------:|
| ascii  | encode | 41.8 ms | 24.5 ms |
| ascii  | decode | 52.4 ms | 53.4 ms |
| utf8   | encode | 47.4 ms | 47.6 ms |
| utf8   | decode | 61.2 ms | 60.9 ms |
2026-08-15 14:04:05 -07:00
Mitchell Hashimoto
c1a61fddda terminal/snapshot: borrow fully buffered record payloads
When the record source already has the complete payload buffered — always
the case for in-memory snapshots such as ghostty_snapshot_decoder_new_buf
— the record reader now borrows the payload straight out of the source
buffer instead of streaming it through the limited and hashing reader
adapters. Payload decoders parse a fixed reader over the borrowed bytes,
`finish` validates the CRC with a single bulk update, and the source
advances only after validation.

The page decoder takes a matching fast path: a fully buffered payload is
parsed in place, skipping the staging allocation and copy it previously
made per PAGE record. Streaming sources are unchanged.

This is a modest win on its own; it is also the foundation for later
commits whose buffered fast paths rely on the payload being contiguous.

Benchmarks (see the first commit in this series for methodology; "prev"
is the parent commit):

| wasm      | encode prev | encode   | decode prev | decode   |
|-----------|------------:|---------:|------------:|---------:|
| ascii     |     3.43 ms |  3.50 ms |     2.81 ms |  2.68 ms |
| styled    |     3.14 ms |  3.15 ms |     7.12 ms |  7.04 ms |
| truecolor |     5.33 ms |  5.27 ms |    12.11 ms | 12.04 ms |
| cjk       |     5.95 ms |  5.92 ms |    12.04 ms | 11.89 ms |
| grapheme  |    14.18 ms | 14.33 ms |    13.16 ms | 13.14 ms |

| native | mode   | prev    | this    |
|--------|--------|--------:|--------:|
| ascii  | encode | 41.7 ms | 41.8 ms |
| ascii  | decode | 53.2 ms | 52.4 ms |
| utf8   | encode | 47.2 ms | 47.4 ms |
| utf8   | decode | 61.6 ms | 61.2 ms |
2026-08-15 14:04:05 -07:00
Mitchell Hashimoto
36d8e3f777 terminal/snapshot: slicing-by-16 software CRC32C
The software CRC32C fallback (WebAssembly and any other target without a
dedicated instruction) was the std byte-at-a-time table walk, which
profiled at ~65% of snapshot encode and ~70% of decode self-time in V8.
Replace it with slicing-by-16: sixteen bytes fold per iteration through
comptime per-position tables, so the serial dependency advances one block
at a time instead of one byte.

The hardware backends (aarch64 CRC, x86_64 SSE4.2) are unchanged, so
native is expected to be unaffected; its deltas below are run-to-run
noise.

Benchmarks: wasm is V8 (node 25), ReleaseFast + wasm-opt -O3, 80x24
terminal, 2 MiB VT corpus per workload, complete snapshot including
scrollback, best-of-5. Native is aarch64 macOS, hyperfine mean,
ghostty-bench +terminal-snapshot --loops=20. "base" is the parent commit.

| wasm      | encode base | encode   | decode base | decode   |
|-----------|------------:|---------:|------------:|---------:|
| ascii     |     7.07 ms |  3.43 ms |     6.45 ms |  2.81 ms |
| styled    |    10.54 ms |  3.14 ms |    14.37 ms |  7.12 ms |
| truecolor |    15.23 ms |  5.33 ms |    21.76 ms | 12.11 ms |
| cjk       |    25.41 ms |  5.95 ms |    30.83 ms | 12.04 ms |
| grapheme  |    27.67 ms | 14.18 ms |    27.00 ms | 13.16 ms |

| native | mode   | base    | this    |
|--------|--------|--------:|--------:|
| ascii  | encode | 40.6 ms | 41.7 ms |
| ascii  | decode | 51.2 ms | 53.2 ms |
| utf8   | encode | 45.2 ms | 47.2 ms |
| utf8   | decode | 59.8 ms | 61.6 ms |
2026-08-15 14:04:05 -07:00
phl
c121734295 move shell expand of theme from theme.zig to config.zig 2026-08-15 20:10:10 +02:00
Jon Parise
69b9abf09e cli: version SSH terminfo cache entries
Derive a version from the encoded Ghostty terminfo and require callers
to pass it explicitly when reading or writing the SSH cache. Cache
entries created for older or different payloads no longer suppress a
required installation.
2026-08-15 12:18:56 -04:00
Sl (Shahaf Levi)
06b63c4859 i18n: update Hebrew translations for v1.4 2026-08-15 19:13:27 +03:00
Jon Parise
fff9f62f38 cli: always install embedded SSH terminfo
Remove the remote infocmp short-circuit from +ssh setup. The command
already sends Ghostty's embedded terminfo source to tic, so accepting
any existing entry leaves the payload unused and can preserve stale
data.
2026-08-15 12:12:46 -04:00
ghostty-vouch[bot]
cecf81678e Update VOUCHED list (#13843)
Triggered by [discussion
comment](https://github.com/ghostty-org/ghostty/discussions/13841#discussioncomment-18031969)
from @jcollie.

Vouch: @preiter93

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-15 16:07:28 +00:00
phl
f748b17e27 feat: expand tildes in config theme path to HOME
When loading a theme from a path that includes a tilde:
```
theme="~/.cache/wal/colors-ghostty"
```
currently fails with the following error:
```
cannot include path separators unless it is an absolute path
```

This PR tries to expand the ~ of the path. If there is no ~
or expansion fails, it falls back to the unexpanded value.
2026-08-15 16:18:30 +02:00