feat: port GTK version fallbacks, helpgen, bench modes, CLI leftovers

- apprt-gtk: Ghostty's runtime+build-time version gating (at_least)
  with cargo features adw_1_3/adw_1_4/adw_1_5/gtk_4_16 (default on) and
  the fallbacks for older libraries: MessageDialog (adw < 1.5),
  clipboard dialog without SwitchRow, pre-Banner debug warning and
  child-exited widgets, the old about dialog, GTK 4.16 CSS error source
  bytes. The minimum stays GTK 4.14 / libadwaita 1.5 (Ghostty's floor).
  The root build links with -z lazy so version-gated symbols don't keep
  older libraries from loading.
- ghostty-helpgen (port of helpgen.zig) generates CLI action help and
  keybinding action docs from doc comments at build time, replacing the
  checked-in help/*.txt; +help and +list-actions --docs stay
  byte-identical to Ghostty 1.3.1.
- ghostty-bench: libc wcwidth mode and separate simd/table and
  uucode/table code paths like Zig.
- ghostty-cli: Windows branches of edit-config, list-colors,
  list-keybinds and boo; ssh-cache DiskCache flock locking (no-op on
  Windows, as in Zig) with an exclusivity test.

Test Plan:
- cargo test --workspace
- diff <(cargo run -q -- +list-actions --docs) \
  <(ghostty +list-actions --docs)
This commit is contained in:
ddidderr committed 2026-10-12 06:38:33 +02:00
1 parent 020beb2d75
commit 4787565397
51 files changed
+2426 -1621

No files matched your search

+3
View File
@@ -19,6 +19,9 @@ termina = "0.3.3"
thiserror = "2.0.21"
unicode-width = "0.2.2"
[build-dependencies]
ghostty-helpgen.workspace = true
[lints]
workspace = true
+37
View File
@@ -125,8 +125,45 @@ fn build_mode() -> &'static str {
}
}
/// Generates the per-action help table (Zig: `helpgen.zig` `genActions`):
/// the doc comment of each action's `run` function becomes
/// `Action::help_text`.
fn gen_action_help(manifest_dir: &Path) {
let ghostty_rs = manifest_dir.join("src/ghostty.rs");
println!("cargo:rerun-if-changed={}", ghostty_rs.display());
let src = std::fs::read_to_string(&ghostty_rs).expect("read ghostty.rs");
let mut table = Vec::new();
for (variant, _) in ghostty_helpgen::enum_variant_docs(&src, "Action") {
// `ListFonts` -> `list_fonts` (file name, as Zig `Action.file`).
let mut file = String::new();
for (i, c) in variant.chars().enumerate() {
if c.is_ascii_uppercase() && i > 0 {
file.push('_');
}
file.push(c.to_ascii_lowercase());
}
let path = manifest_dir.join(format!("src/{file}.rs"));
println!("cargo:rerun-if-changed={}", path.display());
let text = std::fs::read_to_string(&path).expect("read action source");
let help = ghostty_helpgen::run_fn_doc(&text).unwrap_or_else(|| {
panic!("doc comment must be present on run function of the {file} action!")
});
table.push((variant, help));
}
assert!(!table.is_empty(), "no actions found");
let out = PathBuf::from(std::env::var_os("OUT_DIR").expect("OUT_DIR"));
std::fs::write(
out.join("action_help.rs"),
ghostty_helpgen::cli_help_match(&table),
)
.expect("write action_help.rs");
}
fn main() {
println!("cargo:rerun-if-changed=build.rs");
gen_action_help(&PathBuf::from(
std::env::var_os("CARGO_MANIFEST_DIR").unwrap_or_default(),
));
println!("cargo:rerun-if-env-changed=RUSTC");
let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".into());
let version = Command::new(rustc)
+10
View File
@@ -180,6 +180,16 @@ impl Boo {
/// The `boo` command is used to display the animation from the Ghostty website in the terminal
pub fn run(args: &[String]) -> Result<u8, Error> {
// Disable on non-desktop systems.
if cfg!(not(any(
windows,
target_os = "macos",
target_os = "linux",
target_os = "freebsd"
))) {
return Ok(1);
}
let mut opts = Options;
crate::parse_options(&mut opts, args)?;
+29 -2
View File
@@ -1,6 +1,8 @@
//! `+edit-config` (ZIG/src/cli/edit_config.zig).
use std::{io::Write, os::unix::process::CommandExt};
use std::io::Write;
#[cfg(unix)]
use std::os::unix::process::CommandExt;
use ghostty_config::config::Config;
@@ -62,6 +64,31 @@ fn run_inner(stderr: &mut dyn Write) -> Result<u8, Error> {
let path = ghostty_config::file_load::preferred_default_file_path()
.map_err(|e| Error::Other(e.to_string()))?;
// We don't currently support Windows because we use the exec syscall.
#[cfg(windows)]
{
write_windows_unsupported(stderr, &path)
}
#[cfg(unix)]
exec_editor(stderr, &path)
}
/// Windows branch of `run`: print the path and fail.
#[cfg(windows)]
fn write_windows_unsupported(stderr: &mut dyn Write, path: &str) -> Result<u8, Error> {
write!(
stderr,
"The `ghostty +edit-config` command is not supported on Windows.\n\
Please edit the configuration file manually at the following path:\n\
\n\
{path}\n"
)?;
Ok(1)
}
#[cfg(unix)]
fn exec_editor(stderr: &mut dyn Write, path: &str) -> Result<u8, Error> {
// Get our editor
let Some(editor) = find_editor() else {
// If we don't have `$EDITOR` set then we can't do anything
@@ -87,7 +114,7 @@ fn run_inner(stderr: &mut dyn Write) -> Result<u8, Error> {
// Flush before exec so no output is lost.
let _ = stderr.flush();
let err = std::process::Command::new(&editor).arg(&path).exec();
let err = std::process::Command::new(&editor).arg(path).exec();
// If we reached this point then exec failed.
let err = match err.kind() {
+3 -17
View File
@@ -148,23 +148,9 @@ impl Action {
/// function, i.e. `help_strings.Action.<name>`), without trailing newline.
#[must_use]
pub fn help_text(self) -> &'static str {
match self {
Action::Version => include_str!("help/version.txt"),
Action::Help => include_str!("help/help.txt"),
Action::ListFonts => include_str!("help/list-fonts.txt"),
Action::ListKeybinds => include_str!("help/list-keybinds.txt"),
Action::ListThemes => include_str!("help/list-themes.txt"),
Action::ListColors => include_str!("help/list-colors.txt"),
Action::ListActions => include_str!("help/list-actions.txt"),
Action::SshCache => include_str!("help/ssh-cache.txt"),
Action::EditConfig => include_str!("help/edit-config.txt"),
Action::ShowConfig => include_str!("help/show-config.txt"),
Action::ValidateConfig => include_str!("help/validate-config.txt"),
Action::ShowFace => include_str!("help/show-face.txt"),
Action::CrashReport => include_str!("help/crash-report.txt"),
Action::Boo => include_str!("help/boo.txt"),
Action::NewWindow => include_str!("help/new-window.txt"),
}
// Generated by `build.rs` (via `ghostty-helpgen`) from the doc
// comment of each action's `run` function.
include!(concat!(env!("OUT_DIR"), "/action_help.rs"))
}
/// Run the action. `args` is the full argument vector (including
-1
View File
@@ -1 +0,0 @@
The `boo` command is used to display the animation from the Ghostty website in the terminal
@@ -1,6 +0,0 @@
The `crash-report` command is used to inspect and send crash reports.
When executed without any arguments, this will list existing crash reports.
This command currently only supports listing crash reports. Viewing
and sending crash reports is unimplemented and will be added in the future.
@@ -1,20 +0,0 @@
The `edit-config` command opens the Ghostty configuration file in the
editor specified by the `$VISUAL` or `$EDITOR` environment variables.
IMPORTANT: This command will not reload the configuration after
editing. You will need to manually reload the configuration using the
application menu, configured keybind, or by restarting Ghostty. We
plan to auto-reload in the future, but Ghostty isn't capable of
this yet.
The filepath opened is the default user-specific configuration
file, which is typically located at `$XDG_CONFIG_HOME/ghostty/config.ghostty`.
On macOS, this may also be located at
`~/Library/Application Support/com.mitchellh.ghostty/config.ghostty`.
On macOS, whichever path exists and is non-empty will be prioritized,
prioritizing the Application Support directory if neither are
non-empty.
This command prefers the `$VISUAL` environment variable over `$EDITOR`,
if both are set. If neither are set, it will print an error
and exit.
-5
View File
@@ -1,5 +0,0 @@
The `help` command shows general help about Ghostty. Recognized as either
`-h, `--help`, or like other actions `+help`.
You can also specify `--help` or `-h` along with any action such as
`+list-themes` to see help for a specific action.
@@ -1,7 +0,0 @@
The `list-actions` command is used to list all the available keybind
actions for Ghostty. These are distinct from the CLI Actions which can
be listed via `+help`
Flags:
* `--docs`: will print out the documentation for each action.
@@ -1,7 +0,0 @@
The `list-colors` command is used to list all the named RGB colors in
Ghostty.
Flags:
* `--plain`: will disable formatting and make the output more
friendly for Unix tooling. This is default when not printing to a tty.
@@ -1,24 +0,0 @@
The `list-fonts` command is used to list all the available fonts for
Ghostty. This uses the exact same font discovery mechanism Ghostty uses to
find fonts to use.
When executed with no arguments, this will list all available fonts, sorted
by family name, then font name. If a family name is given with `--family`,
the sorting will be disabled and the results instead will be shown in the
same priority order Ghostty would use to pick a font.
Flags:
* `--bold`: Filter results to specific bold styles. It is not guaranteed
that only those styles are returned. They are only prioritized.
* `--italic`: Filter results to specific italic styles. It is not guaranteed
that only those styles are returned. They are only prioritized.
* `--style`: Filter results based on the style string advertised by a font.
It is not guaranteed that only those styles are returned. They are only
prioritized.
* `--family`: Filter results to a specific font family. The family handling
is identical to the `font-family` set of Ghostty configuration values, so
this can be used to debug why your desired font may not be loading.
@@ -1,17 +0,0 @@
The `list-keybinds` command is used to list all the available keybinds for
Ghostty.
When executed without any arguments this will list the current keybinds
loaded by the config file. If no config file is found or there aren't any
changes to the keybinds it will print out the default ones configured for
Ghostty
Flags:
* `--default`: will print out all the default keybinds
* `--docs`: currently does nothing, intended to print out documentation
about the action associated with the keybinds
* `--plain`: will disable formatting and make the output more
friendly for Unix tooling. This is default when not printing to a tty.
@@ -1,37 +0,0 @@
The `list-themes` command is used to preview or list all the available
themes for Ghostty.
If this command is run from a TTY, a TUI preview of the themes will be
shown. While in the preview, `F1` will bring up a help screen and `ESC` will
exit the preview. Other keys that can be used to navigate the preview are
listed in the help screen.
If this command is not run from a TTY, or the output is piped to another
command, a plain list of theme names will be printed to the screen. A plain
list can be forced using the `--plain` CLI flag.
Two different directories will be searched for themes.
The first directory is the `themes` subdirectory of your Ghostty
configuration directory. This is `$XDG_CONFIG_HOME/ghostty/themes` or
`~/.config/ghostty/themes`.
The second directory is the `themes` subdirectory of the Ghostty resources
directory. Ghostty ships with a multitude of themes that will be installed
into this directory. On macOS, this directory is the
`Ghostty.app/Contents/Resources/ghostty/themes`. On Linux, this directory
is the `share/ghostty/themes` (wherever you installed the Ghostty "share"
directory). If you're running Ghostty from the source, this is the
`zig-out/share/ghostty/themes` directory.
You can also set the `GHOSTTY_RESOURCES_DIR` environment variable to point
to the resources directory.
Flags:
* `--path`: Show the full path to the theme.
* `--plain`: Force a plain listing of themes.
* `--color`: Specify the color scheme of the themes included in the list.
This can be `dark`, `light`, or `all`. The default is `all`.
@@ -1,62 +0,0 @@
The `new-window` will use native platform IPC to open up a new window in a
running instance of Ghostty.
If the `--class` flag is not set, the `new-window` command will try and
connect to a running instance of Ghostty based on what optimizations the
Ghostty CLI was compiled with. Otherwise the `new-window` command will try
and contact a running Ghostty instance that was configured with the same
`class` as was given on the command line.
All of the arguments after the `+new-window` argument (except for the
`--class` flag) will be sent to the remote Ghostty instance and will be
parsed as command line flags. These flags will override certain settings
when creating the first surface in the new window. Currently, only
`--working-directory`, `--command`, and `--title` are supported. `-e` will
also work as an alias for `--command`, except that if `-e` is found on the
command line all following arguments will become part of the command and no
more arguments will be parsed for configuration settings.
If `--working-directory` is found on the command line and is a relative
path (i.e. doesn't start with `/`) it will be resolved to an absolute path
relative to the current working directory that the `ghostty +new-window`
command is run from. `~/` prefixes will also be expanded to the user's home
directory.
If `--working-directory` is _not_ found on the command line, the working
directory that `ghostty +new-window` is run from will be passed to Ghostty.
GTK uses an application ID to identify instances of applications. If Ghostty
is compiled with release optimizations, the default application ID will be
`com.mitchellh.ghostty`. If Ghostty is compiled with debug optimizations,
the default application ID will be `com.mitchellh.ghostty-debug`. The
`class` configuration entry can be used to set up a custom application
ID. The class name must follow the requirements defined [in the GTK
documentation](https://docs.gtk.org/gio/type_func.Application.id_is_valid.html)
or it will be ignored and Ghostty will use the default as defined above.
On GTK, D-Bus activation must be properly configured. Ghostty does not need
to be running for this to open a new window, making it suitable for binding
to keys in your window manager (if other methods for configuring global
shortcuts are unavailable). D-Bus will handle launching a new instance
of Ghostty if it is not already running. See the Ghostty website for
information on properly configuring D-Bus activation.
Only supported on GTK.
Flags:
* `--class=<class>`: If set, open up a new window in a custom instance of
Ghostty. The class must be a valid GTK application ID.
* `--command`: The command to be executed in the first surface of the new window.
* `--working-directory=<directory>`: The working directory to pass to Ghostty.
* `--title`: A title that will override the title of the first surface in
the new window. The title override may be edited or removed later.
* `-e`: Any arguments after this will be interpreted as a command to
execute inside the first surface of the new window instead of the
default command.
Available since: 1.2.0
@@ -1,27 +0,0 @@
The `show-config` command shows the current configuration in a valid Ghostty
configuration file format.
When executed without any arguments this will output the current
configuration that is different from the default configuration. If you're
using the default configuration this will output nothing.
If you are a new user and want to see all available options with
documentation, run `ghostty +show-config --default --docs`.
The output is not in any specific order, but the order should be consistent
between runs. The output is not guaranteed to be exactly match the input
configuration files, but it will result in the same behavior. Comments,
whitespace, and other formatting is not preserved from user configuration
files.
Flags:
* `--default`: Show the default configuration instead of loading
the user configuration.
* `--changes-only`: Only show the options that have been changed
from the default. This has no effect if `--default` is specified.
* `--docs`: Print the documentation above each option as a comment,
This is very noisy but is very useful to learn about available
options, especially paired with `--default`.
-21
View File
@@ -1,21 +0,0 @@
The `show-face` command shows what font face Ghostty will use to render a
specific codepoint. Note that this command does not take into consideration
grapheme clustering or any other Unicode features that might modify the
presentation of a codepoint, so this may show a different font face than
Ghostty uses to render a codepoint in a terminal session.
Flags:
* `--cp`: Find the face for a single codepoint. The codepoint may be specified
in decimal (`--cp=65`), hexadecimal (`--cp=0x41`), octal (`--cp=0o101`), or
binary (`--cp=0b1000001`).
* `--string`: Find the face for all of the codepoints in a string. The
string must be a valid UTF-8 sequence.
* `--style`: Search for a specific style. Valid options are `regular`, `bold`,
`italic`, and `bold_italic`.
* `--presentation`: If set, force searching for a specific presentation
style. Valid options are `text` and `emoji`. If unset, the presentation
style of a codepoint will be inferred from the Unicode standard.
-23
View File
@@ -1,23 +0,0 @@
Manage the SSH terminfo cache for automatic remote host setup.
When SSH integration is enabled with `shell-integration-features = ssh-terminfo`,
Ghostty automatically installs its terminfo on remote hosts. This command
manages the cache of successful installations to avoid redundant uploads.
The cache stores hostnames (or user@hostname combinations) along with timestamps.
Entries older than the expiration period are automatically removed during cache
operations. By default, entries never expire.
Only one of `--clear`, `--add`, `--remove`, or `--host` can be specified.
If multiple are specified, one of the actions will be executed but
it isn't guaranteed which one. This is entirely unsafe so you should split
multiple actions into separate commands.
Examples:
ghostty +ssh-cache # List all cached hosts
ghostty +ssh-cache --host=example.com # Check if host is cached
ghostty +ssh-cache --add=example.com # Manually add host to cache
ghostty +ssh-cache --add=user@example.com # Add user@host combination
ghostty +ssh-cache --remove=example.com # Remove host from cache
ghostty +ssh-cache --clear # Clear entire cache
ghostty +ssh-cache --expire-days=30 # Set custom expiration period
@@ -1,9 +0,0 @@
The `validate-config` command is used to validate a Ghostty config file.
When executed without any arguments, this will load the config from the default
location.
Flags:
* `--config-file`: can be passed to validate a specific target config file in
a non-default location
-2
View File
@@ -1,2 +0,0 @@
The `version` command is used to display information about Ghostty. Recognized as
either `+version` or `--version`.
+76 -15
View File
@@ -2,12 +2,13 @@
//! (ZIG/src/cli/ssh-cache/DiskCache.zig). The cache only stores metadata
//! (hostname, terminfo value, etc.) and does not store any sensitive data.
#[cfg(unix)]
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
use std::{
collections::BTreeMap,
fs::{File, OpenOptions},
io::{Read, Write},
net::Ipv6Addr,
os::unix::fs::{OpenOptionsExt, PermissionsExt},
path::PathBuf,
};
@@ -87,13 +88,11 @@ impl DiskCache {
}
// Open or create cache file with secure permissions
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.mode(0o600)
.open(&self.path)?;
let mut open = OpenOptions::new();
open.read(true).write(true).create(true).truncate(false);
#[cfg(unix)]
open.mode(0o600);
let file = open.open(&self.path)?;
fixup_permissions(&file)?;
// Lock
@@ -196,12 +195,11 @@ impl DiskCache {
let tmp_path = cache_dir.join(tmp_name);
let result = (|| -> std::io::Result<()> {
let mut f = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(&tmp_path)?;
let mut open = OpenOptions::new();
open.write(true).create(true).truncate(true);
#[cfg(unix)]
open.mode(0o600);
let mut f = open.open(&tmp_path)?;
f.write_all(data.as_bytes())?;
f.sync_all()?;
std::fs::rename(&tmp_path, &self.path)
@@ -227,6 +225,18 @@ impl Drop for Lock<'_> {
/// Zig: `_ = file.tryLock(.exclusive) catch return error.CacheIsLocked;`
/// The boolean result is ignored upstream, so a lock held by someone else
/// does not fail; only a real error does.
///
/// Zig skips locking entirely on Windows
/// (`if (comptime builtin.os.tag != .windows)`), so we do too. On unix
/// `File::try_lock` is `flock(LOCK_EX | LOCK_NB)`, the same call as Zig's
/// `File.tryLock(.exclusive)`.
#[cfg(windows)]
#[allow(clippy::unnecessary_wraps)]
fn lock(_file: &File) -> Result<Lock<'_>, Error> {
Ok(Lock(None))
}
#[cfg(not(windows))]
fn lock(file: &File) -> Result<Lock<'_>, Error> {
match file.try_lock() {
Ok(()) => Ok(Lock(Some(file))),
@@ -235,7 +245,20 @@ fn lock(file: &File) -> Result<Lock<'_>, Error> {
}
}
#[cfg_attr(windows, allow(clippy::unnecessary_wraps))]
fn fixup_permissions(file: &File) -> Result<(), Error> {
// Windows does not support chmod
#[cfg(windows)]
{
let _ = file;
Ok(())
}
#[cfg(unix)]
fixup_permissions_unix(file)
}
#[cfg(unix)]
fn fixup_permissions_unix(file: &File) -> Result<(), Error> {
// Ensure file has correct permissions (readable/writable by
// owner only)
let meta = file.metadata()?;
@@ -323,6 +346,34 @@ fn is_valid_user(user: &str) -> bool {
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#[cfg(not(windows))]
#[test]
fn lock_is_exclusive_until_dropped() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("lock");
let a = File::create(&path).unwrap();
let b = File::options().read(true).write(true).open(&path).unwrap();
let held = super::lock(&a).unwrap();
assert!(held.0.is_some());
// A second handle can't take the lock; Zig ignores that result.
assert!(matches!(
b.try_lock(),
Err(std::fs::TryLockError::WouldBlock)
));
assert!(super::lock(&b).unwrap().0.is_none());
drop(held);
// Unlocked on drop.
assert!(b.try_lock().is_ok());
}
#[cfg(windows)]
#[test]
fn lock_is_noop_on_windows() {
let dir = tempfile::tempdir().unwrap();
let a = File::create(dir.path().join("lock")).unwrap();
assert!(super::lock(&a).unwrap().0.is_none());
}
use super::*;
#[test]
@@ -389,8 +440,9 @@ mod tests {
assert_eq!(vec![std::ffi::OsString::from("cache")], names);
}
#[cfg(unix)]
#[test]
fn disk_cache_file_permissions_and_invalid_host() {
fn disk_cache_file_permissions() {
let tmp = tempfile::tempdir().unwrap();
let cache = DiskCache {
path: tmp.path().join("sub/cache"),
@@ -398,6 +450,15 @@ mod tests {
cache.add("example.com").unwrap();
let mode = std::fs::metadata(&cache.path).unwrap().permissions().mode();
assert_eq!(0o600, mode & 0o777);
}
#[test]
fn disk_cache_invalid_host() {
let tmp = tempfile::tempdir().unwrap();
let cache = DiskCache {
path: tmp.path().join("sub/cache"),
};
cache.add("example.com").unwrap();
assert!(matches!(
cache.add("bad host"),
Err(Error::HostnameIsInvalid)
+11
View File
@@ -264,9 +264,20 @@ fn write_style_change(w: &mut dyn Write, cursor: &Style, next: &Style) -> std::i
Ok(())
}
/// Fixed `(cols, rows)` used on Windows instead of querying the tty.
#[cfg(windows)]
const WINDOWS_WINSIZE: (u16, u16) = (120, 24);
/// The terminal size in columns and rows (`vaxis.Tty.getWinsize`).
#[must_use]
pub fn window_size() -> (u16, u16) {
// On Windows we use some default; it doesn't really matter for what we're
// doing because we don't do any wrapping (rows = 24, cols = 120).
#[cfg(windows)]
{
WINDOWS_WINSIZE
}
#[cfg(not(windows))]
ratatui::crossterm::terminal::size().unwrap_or((80, 24))
}