This change primarily focused on a revised +ssh-cache user interface,
but it also reworks a bunch of the internals.
The primary CLI improvement is support for positional arguments and a
consistent list output format that includes both the ISO-formatted
timestamp and relative age.
ghostty +ssh-cache # List all cached destinations
ghostty +ssh-cache user@example.com # Show that destination
ghostty +ssh-cache example.com # Show all users on that host
ghostty +ssh-cache --add=user@example.com # Manually add a destination
ghostty +ssh-cache --remove=user@example.com # Remove a destination
ghostty +ssh-cache --prune=30d # Remove entries older than 30 days
ghostty +ssh-cache --clear # Clear entire cache
Notable, we now support a --prune operation that replaces the previous
--expire-days flag that was never actually hooked up to anything (!!).
--prune also supports a wider range of Duration-based values.
We're also much more consistent with error codes: 0=success, 1=failure,
2=usage.
While working on those changes, I also reworked the cache internals,
particularly the code around timestamp handling and errors. For example,
I dropped the explicit error sets because they were growing unwieldy,
and in practice we only matched on a subset of those errors.
Lastly, overall test coverage should be much improved, especially around
the time- and allocation-related operations.
readEntries had two memory bugs on the allocation failure path, both
only reachable under OOM:
- The map itself was never freed if we ran into an allocation failure
- The unconditional `errdefer`s for the dupe'd hostname and terminfo
values could double-free if there was a later allocation failure.
This change restructures this function so that these values are dupe'd
up-front, and then their ownership is tracked using optionals that can
be null'ed out once their ownership is transferred into the map.
Both of these cases are now covered by unit tests.
Add a drop-in `ssh` wrapper that sets up the remote environment for
Ghostty. Anything not consumed as one of our own flags is forwarded to
the real, wrapped `ssh` binary. It can be used directly (`ghostty +ssh
user@host`), aliased (`alias ssh='ghostty +ssh --'`), or invoked through
Ghostty's shell integration.
Before exec'ing ssh, `+ssh`:
- Forwards Ghostty environment to the remote (`--forward-env`): sets
TERM=xterm-256color and requests SendEnv forwarding of COLORTERM,
TERM_PROGRAM, and TERM_PROGRAM_VERSION.
- Installs Ghostty's terminfo on the remote (`--terminfo`), informed by
our existing `ssh-cache` system and using our internal xterm-ghostty
terminfo representation.
A third flag, `--cache`, controls cache use; `--cache=false` bypasses
both read and write, which is useful for scripting and for debugging
install failures without polluting the cache.
For shell integration, this replaces the per-shell logic (which made up
roughly a third of our shell integration scripts) with a simple wrapper
function that translates GHOSTTY_SHELL_FEATURES into a `ghostty +ssh`
command line.
Expose toggle-quick-terminal as a proper IPC action so it can be
triggered via 'ghostty +toggle-quick-terminal' from the command line,
instead of calling the raw D-Bus org.gtk.Actions.Activate interface.
This follows the same pattern as the existing +new-window IPC command:
- Add toggle_quick_terminal to apprt.ipc.Action enum (Zig + C ABI)
- Create apprt/gtk/ipc/toggle_quick_terminal.zig (GTK D-Bus handler)
- Route .toggle_quick_terminal in apprt/gtk/App.zig performIpc
- Register toggle-quick-terminal GAction in application.zig
- Add +toggle-quick-terminal CLI handler in cli/
- Register in cli/ghostty.zig Action enum, runMain, and options
- Add stub in apprt/embedded.zig
- Update include/ghostty.h C header enum
Usage:
ghostty +toggle-quick-terminal
Closes: #12618
Add a drop-in `ssh` wrapper that sets up the remote environment for
Ghostty. Anything not consumed as one of our own flags is forwarded to
the real, wrapped `ssh` binary. It can be used directly (`ghostty +ssh
user@host`), aliased (`alias ssh='ghostty +ssh --'`), or invoked through
Ghostty's shell integration.
Before exec'ing ssh, `+ssh`:
- Forwards Ghostty environment to the remote (`--forward-env`): sets
TERM=xterm-256color and requests SendEnv forwarding of COLORTERM,
TERM_PROGRAM, and TERM_PROGRAM_VERSION.
- Installs Ghostty's terminfo on the remote (`--terminfo`), informed by
our existing `ssh-cache` system and using our internal xterm-ghostty
terminfo representation.
A third flag, `--cache`, controls cache use; `--cache=false` bypasses
both read and write, which is useful for scripting and for debugging
install failures without polluting the cache.
For shell integration, this replaces the per-shell logic (which made up
roughly a third of our shell integration scripts) with a simple wrapper
function that translates GHOSTTY_SHELL_FEATURES into a `ghostty +ssh`
command line. This commit only migrates the bash integration; the other
shells will follow separately.
Per review feedback, drop the `if (Discover == Windows)` comptime
branches in SharedGridSet and list_fonts by making every backend's
`init` take a Library and ignore it when unused. Call sites just do
`Discover.init(self.font_lib)` now.
Also adds a discovery test for the Windows backend that looks up
Arial and checks the returned face has the 'A' codepoint.
Co-authored-by: Claude <noreply@anthropic.com>
Adds a FreeType-based Discover implementation for Windows that walks
the system (C:\Windows\Fonts) and per-user
(%LOCALAPPDATA%\Microsoft\Windows\Fonts) font directories, matching
descriptors via family_name / SFNT name table and optionally codepoint
presence.
Wired up as a new .freetype_windows backend which Backend.default() now
returns on Windows. Existing freetype-only paths are untouched.
With this in place, standard code paths -- +list-fonts, SharedGridSet
font-family lookup, CodepointResolver fallback -- work on Windows
without any os.tag == .windows branches in the caller.
Co-authored-by: Claude <noreply@anthropic.com>
Add a new Pager type that wraps output to an external pager program when
stdout is a TTY, following the same conventions as git. The pager
command is resolved from $PAGER, falling back to `less`. An empty $PAGER
disables paging. If the pager fails to spawn, we fall back to stdout.
Previously, +explain-config wrote directly to stdout with no paging,
which meant long help text would scroll by. Now output is automatically
piped through the user's preferred pager when running interactively. A
--no-pager flag is available to disable this.
The argument iterator's .next() method returns a transient slice of the
command line buffer so we need to make our own copies of these values to
avoid referencing stale memory.
The argument iterator's .next() method returns a transient slice of the
command line buffer so we need to make our own copies of these values to
avoid referencing stale memory.
If `$EDITOR` or `$VISUAL` contained arguments, not just the path to
an editor (e.g. `zed --new`) `+edit-config` would fail because we were
treating the whole command as a path. Instead, wrap the command with
`/bin/sh -c <command>` so that the shell can separate the path from
the arguments.
Fixes#11897
CommaSplitter treats backslash as an escape character, which breaks
Windows paths like C:\Users\foo since \U is not a valid escape. On
Windows, treat backslash as a literal character outside of quoted
strings. Inside quotes, escape sequences still work as before.
The platform behavior is controlled by a single comptime constant
(escape_outside_quotes) so the logic lives in one place. Escape-specific
tests are skipped on Windows with SkipZigTest, and Windows-specific
tests are added separately.
Also fix Theme.parseCLI to not mistake the colon in a Windows drive
letter (C:\...) for a light/dark theme pair separator.
Note: other places in the config parsing also use colon as a delimiter
without accounting for Windows drive letters (command.zig prefix
parsing, keybind parsing). Those are tracked separately.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This is a new CLI action that prints an option or keybind's help
documentation to stdout.
ghostty +explain-config font-size
ghostty +explain-config copy_to_clipboard
ghostty +explain-config --option=font-size
ghostty +explain-config --keybind=copy_to_clipboard
The --option and --keybind flags perform a specific lookup. A string
passed as a positional argument attempts to look up the name first as an
option and then as a keybind.
Our vim plugin uses this with &keywordprg, which allows you to look up
the documentation for the config option or keybind under the cursor (K).
If the CLI argument `--working-directory` is not used with
`+new-window`, the current working directory that `ghostty +new-window`
is run from will be appended to the list of configuration data sent
to the main Ghostty process. If `-e` _was_ used on the CLI, the
`--working-directory` that was appended will be interpreted as part of
the command to be executed, likely causing it to fail.
Instead, insert `--working-directory` at the beginning of the list of
configuration that it sent to the main Ghostty process.
Fixes#11356
As discussed in Discord, this commit drops the `ConfigOverride` object
in favor of a simpler method of passing the overrides around. Completely
avoiding changes to the core wasn't possible but it's very minimal now.
Fixes: #8862Fixes: #10716
This adds the machinery to pass configuration settings received over
DBus down to the GObject Surface so that that configuration information
can be used to override some settings from the current "live" config
when creating a new window. Currently it's only possible to override
`--working-directory` and `--command`. `-e` on the `ghostty +new-window`
CLI works as well.
Adding more overridable settings is possible, but being able to fully
override any possible setting would better be served with a major
revamp of how Ghostty handles configs, which I is way out of scope at
the moment.
We previously wrote our new cache file into a temporary directory and
the (atomically) renamed it to the canonical cache file path. This
rename operation unfortunately only works when both files are on the
same file system, and that's not always the case (e.g. when $TMPDIR is
on its own file system).
Instead, we can use Zig's AtomicFile to safely perform this operation
inside of the cache directory.
There's a new risk of a crash leaving the temporary file around in this
directory (and not getting cleaned up like $TMPDIR-based files), but the
probability is low and those files will only be readable by the creating
user (mode 0o600).
There's a new test cash that verifies the expected AtomicFile clean up
behavior. I also switched the file-oriented tests to use testing.tmpDir
rather than using our application-level TempDir type.
- Display keybindings grouped by their source table, with table name as prefix
- Sort default bindings before table bindings, maintaining visual hierarchy
- Support keybindings defined in key tables alongside default bindings
- Enable users to discover all available keybindings across the entire config
Part of #9963
This adds a new special key `catch_all` that can be used in keybinding
definitions to match any key that is not explicitly bound. For example:
`keybind = catch_all=new_window` (chaos!).
`catch_all` can be used in combination with modifiers, so if you want to
catch any non-bound key with Ctrl held down, you can do:
`keybind = ctrl+catch_all=new_window`.
`catch_all` can also be used with trigger sequences, so you can do:
`keybind = ctrl+a>catch_all=new_window` to catch any key pressed after
`ctrl+a` that is not explicitly bound and make a new window!
And if you want to remove the catch all binding, it is like any other:
`keybind = catch_all=unbind`.
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
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
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.
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.
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.
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>
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>