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.