fix: URL opener and +show-face output match Ghostty, register fixes
Resolves the parity items of docs/rewrite/notes/review-rv15-codex.md (final-Z18.md): - os::open inherits stdin like Zig's Child.init, strips LD_LIBRARY_PATH only in Snap builds (new `snap` cargo feature, off by default, like Zig's snap build option) and bounds output collection to 50 KiB per stream (StdoutStreamTooLong/StderrStreamTooLong). - +show-face writes diagnostics and lookup lines in the exact fragments Zig's formatter produces, so buffer drains happen at the same byte positions (0 diffs vs /usr/bin/ghostty over 0-44 bad options for three codepoints); its tests no longer parse the test runner's argv. - DEVIANT_FIXES.md: five entries reworded and four removed that were no longer deviations (verified against code and oracle). Test Plan: - cargo test -p ghostty-os open; cargo test -p ghostty-cli show_face
This commit is contained in:
1 parent
0dffc62bdf
commit
465a85b6d5
6 files changed
+296
-53
No files matched your search
@@ -7,7 +7,7 @@ use ghostty_config::{
|
||||
config::Config,
|
||||
config_enum,
|
||||
config_struct,
|
||||
diagnostics::{DiagnosticList, Location},
|
||||
diagnostics::{Diagnostic, DiagnosticList, Location},
|
||||
};
|
||||
use ghostty_font::{
|
||||
self as font,
|
||||
@@ -94,7 +94,7 @@ pub fn run(args: &[impl AsRef<[u8]>]) -> Result<u8, Error> {
|
||||
let sink: Sink = stdout_sink();
|
||||
let mut stdout = ZigWriter::new(sink.clone(), 1024);
|
||||
let mut stderr = ZigWriter::new(sink, 1024);
|
||||
let result = run_args(&opts, &mut stdout, &mut stderr);
|
||||
let result = run_args(&opts, &mut stdout, &mut stderr, &Config::load);
|
||||
let _ = stdout.flush();
|
||||
let _ = stderr.flush();
|
||||
result
|
||||
@@ -104,6 +104,7 @@ fn run_args(
|
||||
opts: &Options,
|
||||
stdout: &mut dyn std::io::Write,
|
||||
stderr: &mut dyn std::io::Write,
|
||||
load_config: &dyn Fn() -> Result<Config, ghostty_config::config::Error>,
|
||||
) -> Result<u8, Error> {
|
||||
// Print out any diagnostics, unless it's likely that the diagnostic was
|
||||
// generated trying to parse a "normal" configuration setting. Exit with an
|
||||
@@ -115,9 +116,7 @@ fn run_args(
|
||||
continue;
|
||||
}
|
||||
if FIELDS.contains(&d.key.as_str()) {
|
||||
stderr.write_all(&ghostty_os::osbytes::config_str_to_bytes(&format!(
|
||||
"config error: {d}\n"
|
||||
)))?;
|
||||
write_diagnostic(stderr, d)?;
|
||||
exit = true;
|
||||
}
|
||||
}
|
||||
@@ -133,7 +132,7 @@ fn run_args(
|
||||
return Ok(1);
|
||||
}
|
||||
|
||||
let config = match Config::load() {
|
||||
let config = match load_config() {
|
||||
Ok(c) => c,
|
||||
Err(err) => {
|
||||
write!(stderr, "Unable to load config: {err}")?;
|
||||
@@ -150,9 +149,7 @@ fn run_args(
|
||||
{
|
||||
continue;
|
||||
}
|
||||
stderr.write_all(&ghostty_os::osbytes::config_str_to_bytes(&format!(
|
||||
"config error: {d}\n"
|
||||
)))?;
|
||||
write_diagnostic(stderr, d)?;
|
||||
}
|
||||
|
||||
let font_grid_set = match SharedGridSet::new() {
|
||||
@@ -227,7 +224,7 @@ fn lookup(
|
||||
) -> Result<Option<u8>, Error> {
|
||||
let ch = char::from_u32(cp).unwrap_or('\u{FFFD}');
|
||||
let Ok(Some(idx)) = grid.get_index(cp, style, presentation) else {
|
||||
writeln!(stdout, "U+{cp:02X} \u{ab} {ch} \u{bb} not found.")?;
|
||||
write_lookup(stdout, cp, ch, " \u{bb} not found.\n", None)?;
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
@@ -235,9 +232,12 @@ fn lookup(
|
||||
let face = match st.resolver.collection.get_face(idx) {
|
||||
Ok(f) => f,
|
||||
Err(GetFaceError::Entry(EntryError::SpecialHasNoFace)) => {
|
||||
writeln!(
|
||||
write_lookup(
|
||||
stdout,
|
||||
"U+{cp:02X} \u{ab} {ch} \u{bb} is handled by Ghostty's internal sprites."
|
||||
cp,
|
||||
ch,
|
||||
" \u{bb} is handled by Ghostty's internal sprites.\n",
|
||||
None,
|
||||
)?;
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -248,18 +248,100 @@ fn lookup(
|
||||
};
|
||||
|
||||
let name = face.name();
|
||||
writeln!(
|
||||
write_lookup(
|
||||
stdout,
|
||||
"U+{cp:02X} \u{ab} {ch} \u{bb} found in face \u{201c}{name}\u{201d}."
|
||||
cp,
|
||||
ch,
|
||||
" \u{bb} found in face \u{201c}",
|
||||
Some(name.as_bytes()),
|
||||
)?;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Zig's `Writer.print` issues one `writeAll` per literal segment and per
|
||||
/// formatted argument, and an aligned integer writes its padding separately
|
||||
/// from its digits. Where the fixed-size buffer drains depends on those
|
||||
/// fragments, so every message below is written in Zig's exact fragments.
|
||||
fn write_parts(w: &mut dyn std::io::Write, parts: &[&[u8]]) -> std::io::Result<()> {
|
||||
for part in parts {
|
||||
w.write_all(part)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `stderr.print("config error: {f}\n", .{diagnostic})` in Zig
|
||||
/// (`cli/diagnostics.zig:19-37` formats through nested `print` calls).
|
||||
fn write_diagnostic(w: &mut dyn std::io::Write, d: &Diagnostic) -> std::io::Result<()> {
|
||||
use ghostty_os::osbytes::config_str_to_bytes as enc;
|
||||
let mut parts: Vec<Vec<u8>> = vec![b"config error: ".to_vec()];
|
||||
match &d.location {
|
||||
Location::None => {}
|
||||
Location::Cli(index) => {
|
||||
parts.push(b"cli:".to_vec());
|
||||
parts.push(index.to_string().into_bytes());
|
||||
parts.push(b":".to_vec());
|
||||
}
|
||||
Location::File { path, line } => {
|
||||
parts.push(enc(path));
|
||||
parts.push(b":".to_vec());
|
||||
parts.push(line.to_string().into_bytes());
|
||||
parts.push(b":".to_vec());
|
||||
}
|
||||
}
|
||||
if !d.key.is_empty() {
|
||||
parts.push(enc(&d.key));
|
||||
parts.push(b": ".to_vec());
|
||||
} else if d.location != Location::None {
|
||||
parts.push(b" ".to_vec());
|
||||
}
|
||||
parts.push(enc(&d.message));
|
||||
parts.push(b"\n".to_vec());
|
||||
let refs: Vec<&[u8]> = parts.iter().map(Vec::as_slice).collect();
|
||||
write_parts(w, &refs)
|
||||
}
|
||||
|
||||
/// `U+{0X:0>2} \u{ab} {0u} <tail>` (optionally followed by `name` and the
|
||||
/// closing `\u{201d}.\n` for the "found in face" line).
|
||||
fn write_lookup(
|
||||
w: &mut dyn std::io::Write,
|
||||
cp: u32,
|
||||
ch: char,
|
||||
tail: &str,
|
||||
name: Option<&[u8]>,
|
||||
) -> std::io::Result<()> {
|
||||
let hex = format!("{cp:X}");
|
||||
let mut buf = [0u8; 4];
|
||||
let mut parts: Vec<&[u8]> = vec![b"U+"];
|
||||
if hex.len() < 2 {
|
||||
parts.push(b"0");
|
||||
}
|
||||
parts.push(hex.as_bytes());
|
||||
parts.push(" \u{ab} ".as_bytes());
|
||||
parts.push(ch.encode_utf8(&mut buf).as_bytes());
|
||||
parts.push(tail.as_bytes());
|
||||
if let Some(name) = name {
|
||||
parts.push(name);
|
||||
parts.push("\u{201d}.\n".as_bytes());
|
||||
}
|
||||
write_parts(w, &parts)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::unwrap_used)]
|
||||
use super::*;
|
||||
|
||||
/// Load the config from explicit args instead of the test runner's argv.
|
||||
fn test_config() -> Result<Config, ghostty_config::config::Error> {
|
||||
let mut config = Config::new_default();
|
||||
let args: [&[u8]; 1] = [b"--config-default-files=false"];
|
||||
config
|
||||
.load_iter(&mut ghostty_config::cli_args::slice_iterator(&args))
|
||||
.map_err(|e| ghostty_config::config::Error::Other(e.to_string()))?;
|
||||
config.finalize()?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn parse(args: &[&[u8]]) -> Options {
|
||||
let mut full: Vec<&[u8]> = vec![b"ghostty"];
|
||||
full.extend_from_slice(args);
|
||||
@@ -287,7 +369,7 @@ mod tests {
|
||||
let o = parse(&[b"--cp=4294967295"]);
|
||||
let mut out = Vec::new();
|
||||
let mut err = Vec::new();
|
||||
assert_eq!(run_args(&o, &mut out, &mut err).unwrap(), 1);
|
||||
assert_eq!(run_args(&o, &mut out, &mut err, &test_config).unwrap(), 1);
|
||||
let text = String::from_utf8(err).unwrap();
|
||||
assert!(
|
||||
text.starts_with("config error: cli:") && text.contains("cp: invalid value"),
|
||||
@@ -309,7 +391,7 @@ mod tests {
|
||||
let mut err = Vec::new();
|
||||
// Without a usable font setup this can fail earlier; only assert the
|
||||
// Zig message when the grid was available.
|
||||
let rc = run_args(&o, &mut out, &mut err).unwrap();
|
||||
let rc = run_args(&o, &mut out, &mut err, &test_config).unwrap();
|
||||
let err = String::from_utf8(err).unwrap();
|
||||
if err.contains("Unable to") && !err.contains("as unicode") {
|
||||
return;
|
||||
@@ -317,4 +399,47 @@ mod tests {
|
||||
assert_eq!(rc, 1);
|
||||
assert_eq!(err, "Unable to parse string as unicode: error.InvalidUtf8");
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct Mem(std::rc::Rc<std::cell::RefCell<Vec<u8>>>);
|
||||
impl std::io::Write for Mem {
|
||||
fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
|
||||
self.0.borrow_mut().extend_from_slice(data);
|
||||
Ok(data.len())
|
||||
}
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagnostics_drain_at_zig_fragment_boundaries() {
|
||||
use crate::zig_writer::stream_sink;
|
||||
let mem = Mem::default();
|
||||
let mut w = ZigWriter::new(stream_sink(mem.clone()), 20);
|
||||
let d = Diagnostic {
|
||||
location: Location::Cli(0),
|
||||
key: "cp".into(),
|
||||
message: "invalid value".into(),
|
||||
};
|
||||
write_diagnostic(&mut w, &d).unwrap();
|
||||
// Zig writes "config error: ", "cli:", "0", ":", "cp" (overflows: the
|
||||
// buffer drains together with "cp"), then ": ", message and "\n".
|
||||
assert_eq!(&*mem.0.borrow(), b"config error: cli:0:cp");
|
||||
w.flush().unwrap();
|
||||
assert_eq!(&*mem.0.borrow(), b"config error: cli:0:cp: invalid value\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lookup_line_pads_hex_and_drains_per_fragment() {
|
||||
use crate::zig_writer::stream_sink;
|
||||
let mem = Mem::default();
|
||||
let mut w = ZigWriter::new(stream_sink(mem.clone()), 1024);
|
||||
write_lookup(&mut w, 0xA, '\n', " \u{bb} not found.\n", None).unwrap();
|
||||
w.flush().unwrap();
|
||||
assert_eq!(
|
||||
String::from_utf8(mem.0.borrow().clone()).unwrap(),
|
||||
"U+0A \u{ab} \n \u{bb} not found.\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user