feat(cli): port ghostty +action CLI commands

Ports ZIG/src/cli into ghostty-cli: action detection/dispatch with
per-action --help, and the actions help, version, list-actions,
list-colors, list-keybinds, list-themes (plain listing plus an
interactive ratatui browser mirroring Ghostty's libvaxis UI),
show-config, validate-config, edit-config, crash-report (listing),
ssh-cache and boo (235 embedded frames).

Verified byte-for-byte against the installed Ghostty 1.3.1 for stdout,
stderr and exit codes of most actions, incl. tty output in a pty at
widths 20-200. Known differences: +list-colors --plain prints all 782
colors (Ghostty truncates at 617 because it never flushes), +version
has placeholder build info until build_info::set is wired up.

Deferred: list-fonts, show-face (need font discovery integration) and
new-window (needs GTK IPC); they return NotYetAvailable for now.

Test Plan:
- cargo test -p ghostty-cli (86 tests)
- cargo run -p ghostty-cli --example cli -- +list-keybinds --plain
  and diff against `ghostty +list-keybinds --plain`
This commit is contained in:
ddidderr committed 2026-10-11 22:22:01 +02:00
1 parent a97ae04f1d
commit 6286e02f07
46 files changed
+7135 -6

No files matched your search

+108
View File
@@ -0,0 +1,108 @@
//! `+list-actions` (ZIG/src/cli/list_actions.zig).
use std::io::Write;
use ghostty_config::config_struct;
use ghostty_input::binding::ActionKey;
use crate::error::Error;
config_struct! {
pub struct Options {
/// If `true`, print out documentation about the action associated with the
/// keybinds.
"docs" => docs: bool = false,
}
}
cli_options!(Options, help);
/// 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.
pub fn run(args: &[String]) -> Result<u8, Error> {
let mut opts = Options::default();
crate::parse_options(&mut opts, args)?;
let out = generate_plaintext(opts.docs);
let mut stdout = std::io::stdout().lock();
stdout.write_all(out.as_bytes())?;
stdout.flush()?;
Ok(0)
}
/// `helpgen_actions.generate(.plaintext, show_docs)`.
///
/// TODO(integration): this is a local copy of
/// `ghostty_input::helpgen_actions::generate`. The embedded docs of a few
/// actions in ghostty-input (`adjust_selection`, ...) end with an extra blank
/// line compared to Ghostty's `help_strings`, which adds a stray `" "` line
/// to `--docs` output. Trailing newlines are normalised here; once the data
/// is fixed in ghostty-input this should call `helpgen_actions::generate`.
#[must_use]
pub fn generate_plaintext(show_docs: bool) -> String {
let mut writer = String::new();
let mut stream = String::new();
for &key in ActionKey::ALL {
let name = key.name();
if name.starts_with('_') {
continue;
}
// Write previously stored doc comment below all related actions
if show_docs {
writer.push_str(&stream);
writer.push('\n');
stream.clear();
writer.push_str(name);
writer.push_str(":\n");
for line in key.docs().trim_end_matches('\n').split('\n') {
stream.push_str(" ");
stream.push_str(line);
stream.push('\n');
}
} else {
writer.push_str(name);
writer.push('\n');
}
}
// Write any remaining buffered documentation
writer.push_str(&stream);
writer
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn names_only() {
let s = generate_plaintext(false);
assert!(s.starts_with("ignore\nunbind\ncsi\n"));
assert!(!s.contains(':'));
}
#[test]
fn docs_have_no_stray_blank_lines_between_actions() {
let s = generate_plaintext(true);
assert!(s.starts_with("\nignore:\n Ignore this key combination.\n"));
assert!(s.contains(" respectively.\n\njump_to_prompt:\n"));
assert!(s.ends_with('\n'));
}
#[test]
fn matches_input_crate_when_docs_are_clean() {
// The first actions have well-formed docs.
let ours = generate_plaintext(true);
let theirs = ghostty_input::helpgen_actions::generate_string(
ghostty_input::helpgen_actions::Format::Plaintext,
true,
);
assert!(ours.lines().take(100).eq(theirs.lines().take(100)));
}
}