Files
ghostty-rs/docs/rewrite/notes/ghostty-config.md
T
ddidderr 67367dcaf5 feat(config): port the configuration system and CLI argument parser
Ports ZIG/src/config (minus C/WASM APIs) and cli/{args,CommaSplitter,
diagnostics}.zig into ghostty-config:
- all 202 options with exact names, types, defaults and verbatim docs,
  declared through one config_struct! macro; Zig's comptime reflection is
  replaced by the ConfigField/CliTarget traits
- loading: default XDG files, -e, config-file includes (optional '?',
  cycle detection), themes incl. light:/dark: pairs, conditional config,
  replay, finalize() derived defaults, clone/changed diffing for reload
- Linux/GTK default keybinds built on ghostty_input::binding::Set
- formatter producing +show-config output

Verified against the installed Ghostty 1.3.1: `+show-config --default`
and `--default --docs` match byte for byte (except gtk-opengl-debug,
which differs between debug/release builds), as do two complex loaded
configs.

Open items (docs/rewrite/notes/ghostty-config.md): url regex needs an
Oniguruma-compatible engine (onig) in the renderer; some font/terminal
types are local copies awaiting integration; KeyRemapSet equality is
order sensitive unlike Zig.

Test Plan:
- cargo test -p ghostty-config (255 tests)
- compare `ghostty +show-config --default --docs` with our formatter
2026-10-11 22:00:12 +02:00

4.9 KiB

ghostty-config port notes (W6)

Sub-notes from the parallel forks live in ghostty-config/ (files-a.md, types-a.md, types-b.md, keybinds.md).

What was ported (Zig -> Rust, all in crates/ghostty-config/src/)

Zig Rust
cli/args.zig cli_args.rs (ConfigField + CliTarget traits, parse, LineIterator, ArgsIterator, parse_auto_struct_with, parse_packed_struct_with, split_tagged_union, compatibility_renamed)
cli/CommaSplitter.zig comma_splitter.rs
cli/diagnostics.zig diagnostics.rs
config/Config.zig (fields + methods) config.rs (fields declared in a single config_struct! invocation: name, type, default, verbatim doc comment), keys.rs (one Key const per option)
Config.zig nested types types_a.rs, types_b.rs, keybinds.rs, font_types.rs, metric_modifier.rs, input_fields.rs (ConfigField impls for ghostty-input types)
conditional.zig, key.zig, ErrorList.zig conditional.rs, key.rs, error_list.rs
formatter.zig formatter.rs (EntryFormatter) + file_formatter.rs (FileFormatter)
command/path/url/string/io/theme/edit/file_load/RepeatableStringMap/ClipboardCodepointMap same names, snake_case
config/config-template, config/testdata src/config-template, src/testdata/
helpgen.zig (config part) macros::help_string (computed at runtime from the doc attributes)

Skipped: CApi.zig, c_get.zig, Wasm.zig, all cval/*_c members, enum(c_int) discriminants, getGObjectType.

Design

  • No comptime reflection: config_struct! (macros.rs) generates the struct, parse_field (== parseIntoField, incl. "empty value resets to default"), per-field FieldInfo (name, doc lines, deep-eq fn, format fn) used by FileFormatter, Config::changed, ChangeIterator.
  • config_enum! / config_packed! define Zig-style enums / packed bool structs.
  • Memory: all values are owned (String, Vec); there is no arena. clone is #[derive(Clone)]; Zig equal == PartialEq (custom where Zig's differs).
  • Replay (theme / conditional state): Config::load_replay replaces the Zig Replay.Iterator (which aliased the config); behaviour is the same.
  • Config::default() == Zig Config.default (default keybinds, command palette entries and the URL link are installed). Config::raw_default() == bare .{}.
  • Config::load() == Zig Config.load (default files, CLI, recursive files, finalize). finalize skips the shell/passwd lookup under cfg(test) like Zig's builtin.is_test.
  • Linux values are baked in for builtin.os.tag switches (e.g. font-size = 12, copy-on-select = true, quit-after-last-window-closed = true, linux-cgroup = single-instance, alpha-blending = linear-corrected). gtk-opengl-debug follows cfg!(debug_assertions) (Zig: mode == .Debug). auto-update-channel finalizes to stable (upstream release build value).
  • macOS-only options (macos-*) are parsed and stored but otherwise ignored.

Verification against the real binary

ghostty +show-config --default and --default --docs (Ghostty 1.3.1 installed at /usr/bin/ghostty) were diffed against FileFormatter output of Config::default(): identical except gtk-opengl-debug (debug vs release build). A loaded config (themes light/dark, config-file includes incl. ? optional and missing, keybinds, palette, font-codepoint-map, errors/diagnostics) was also diffed against ghostty +show-config; identical except auto-update-channel.

Tests

Every Zig test in the ported files is ported (names snake_cased), except:

  • args.zig: none skipped (36 Zig tests -> 40+ Rust tests incl. extras)
  • Config.zig: C-API tests ("RepeatableCommand cval", "cval cleared") skipped
  • key.zig Value (comptime type check) replaced by value_key_types
  • Allocator-failure tests: none existed. Extra tests added for config-file cycles / optional includes, docs formatting.

Deviations / notes for integration

  • ArgError::Other(name) renders as unknown error error.<name> like Zig's {} of an error.
  • Error text for IO failures in config-file diagnostics uses Rust wording except FileNotFound/AccessDenied.
  • theme::open takes the resources dir explicitly (ghostty_os::resources_dir().app()).
  • URL regex (url::REGEX) needs Oniguruma-style lookbehind: consumers (renderer link matching) must use the onig crate (added as dev-dependency only here).
  • font_types.rs holds local Variation, Descriptor, CodepointMap; clipboard_codepoint_map::Entry mirrors terminal/formatter's CodepointMap (TODO(integration): ghostty-font / ghostty-terminal should re-use these).
  • metric_modifier::MetricModifier is the font Metrics.Modifier (font crate depends on config).
  • CursorStyle is defined in types_a.rs (cannot depend on ghostty-terminal).
  • macOS app-support config paths are not implemented (Linux only).
  • KeyRemapSet::PartialEq is input's derived equality (Zig equal is order independent).