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:
1 parent
5462bd8e3e
commit
f2c4b40062
46 files changed
+7135
-6
No files matched your search
@@ -0,0 +1,113 @@
|
||||
//! `+validate-config` (ZIG/src/cli/validate_config.zig).
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use ghostty_config::config::Config;
|
||||
use ghostty_config::config_struct;
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
config_struct! {
|
||||
pub struct Options {
|
||||
/// The path of the config file to validate. If this isn't specified,
|
||||
/// then the default config file paths will be validated.
|
||||
"config-file" => config_file: Option<String> = None,
|
||||
}
|
||||
}
|
||||
cli_options!(Options, help);
|
||||
|
||||
/// 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
|
||||
pub fn run(args: &[String]) -> Result<u8, Error> {
|
||||
let mut opts = Options::default();
|
||||
crate::parse_options(&mut opts, args)?;
|
||||
|
||||
let mut stdout = std::io::BufWriter::new(std::io::stdout().lock());
|
||||
let result = run_inner(&opts, &mut stdout);
|
||||
stdout.flush()?;
|
||||
result
|
||||
}
|
||||
|
||||
fn run_inner(opts: &Options, stdout: &mut dyn Write) -> Result<u8, Error> {
|
||||
let mut cfg = Config::new_default();
|
||||
|
||||
// If a config path is passed, validate it, otherwise validate default configs
|
||||
if let Some(config_path) = &opts.config_file {
|
||||
let abs_path = std::fs::canonicalize(config_path)?;
|
||||
cfg.load_file(&abs_path.to_string_lossy())?;
|
||||
cfg.load_recursive_files()?;
|
||||
} else {
|
||||
cfg = Config::load()?;
|
||||
}
|
||||
|
||||
cfg.finalize()?;
|
||||
|
||||
if !cfg.diagnostics.items().is_empty() {
|
||||
for diag in cfg.diagnostics.items() {
|
||||
writeln!(stdout, "{diag}")?;
|
||||
}
|
||||
return Ok(1);
|
||||
}
|
||||
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::unwrap_used)]
|
||||
use super::*;
|
||||
|
||||
fn write_tmp(name: &str, content: &str) -> String {
|
||||
let dir =
|
||||
std::env::temp_dir().join(format!("ghostty-cli-vc-{}-{name}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let p = dir.join("config");
|
||||
std::fs::write(&p, content).unwrap();
|
||||
p.to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_good_file() {
|
||||
let p = write_tmp("good", "font-size = 14\n");
|
||||
let opts = Options {
|
||||
config_file: Some(p),
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
assert_eq!(0, run_inner(&opts, &mut out).unwrap());
|
||||
assert_eq!(0, out.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_bad_file_prints_diagnostics() {
|
||||
let p = write_tmp("bad", "not-a-key = 1\nfont-size = abc\n");
|
||||
let opts = Options {
|
||||
config_file: Some(p.clone()),
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
assert_eq!(1, run_inner(&opts, &mut out).unwrap());
|
||||
let s = String::from_utf8(out).unwrap();
|
||||
assert!(
|
||||
s.contains(&format!("{p}:1:not-a-key: unknown field\n")),
|
||||
"{s}"
|
||||
);
|
||||
assert!(
|
||||
s.contains(&format!("{p}:2:font-size: invalid value \"abc\"\n")),
|
||||
"{s}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_missing_file_is_error() {
|
||||
let opts = Options {
|
||||
config_file: Some("/nonexistent/ghostty/config".into()),
|
||||
};
|
||||
assert!(run_inner(&opts, &mut Vec::new()).is_err());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user