fix: C action buffer lengths, byte-faithful callbacks, runtime finalize

Resolves the 8 gaps in docs/rewrite/notes/review-rv10-codex.md
(final-Z13.md):
- P1 memory safety: the C key-table action (and hovered-link/open-URL
  payloads) could advertise a length larger than the NUL-truncated
  allocation. A counted_bytes helper now passes the full bytes plus a
  trailing NUL with len == byte count, like Zig's backing slices; all
  C pointer+len structs were audited and tested to read exactly len.
- ghostty-bench args follow cli/args.zig (ordered duplicates, empty
  value resets, first bad value errors, parseBool spellings); the C
  benchmark tokenizer uses the ArgIteratorGeneral port.
- OSC 7 derived titles, desktop notifications and OSC 8 hover links
  forward raw bytes to the runtime; GTK converts only for display.
- C diagnostic getter exports original bytes; escaped surrogates report
  Utf8CannotEncodeSurrogateHalf; config float formatters print `nan`.
- Config::finalize_for(AppRuntime): GTK-only finalization (gtk-single-
  instance=detect) and termio's cgroup hooks apply only to the GTK
  runtime, so libghostty keeps `detect` like Zig.

Test Plan:
- cargo test --workspace --no-fail-fast (exit 0)
- cargo test -p ghostty-lib --features test-platform
- just check-wasm (macOS/Windows clippy re-run after the concurrent
  terminal changes land)
This commit is contained in:
ddidderr committed 2026-10-12 13:09:04 +02:00
1 parent 95fe02b55a
commit f2ccb04aa4
21 files changed
+479 -133

No files matched your search

+14 -9
View File
@@ -1066,6 +1066,7 @@ impl Subprocess {
let pre_info = self.rt_pre_exec_info;
let post_info = self.rt_post_fork_info;
let create_scope = self.create_scope.clone();
let gtk = ghostty_config::config::app_runtime() == ghostty_config::config::AppRuntime::Gtk;
let mut cmd = Command {
path: self.args[0].clone(),
args: self.args.clone(),
@@ -1081,15 +1082,19 @@ impl Subprocess {
let _ = pty::child_pre_exec(master_raw, slave_raw);
None
})),
rt_pre_exec: Some(Box::new(move || linux_cgroup_pre_exec(&pre_info))),
rt_post_fork: Some(Box::new(move |c: &Command| {
let create = |pid, mem, tasks| match &create_scope {
Some(f) => f(pid, mem, tasks),
None => None,
};
linux_cgroup_post_fork(c, &post_info, create)
.map_err(|_: PostForkError| PostForkError)
})),
// Zig installs these only when the apprt declares them
// (`@hasDecl(apprt.runtime, "pre_exec")`): GTK.
rt_pre_exec: gtk.then(|| Box::new(move || linux_cgroup_pre_exec(&pre_info)) as Box<_>),
rt_post_fork: gtk.then(|| {
Box::new(move |c: &Command| {
let create = |pid, mem, tasks| match &create_scope {
Some(f) => f(pid, mem, tasks),
None => None,
};
linux_cgroup_post_fork(c, &post_info, create)
.map_err(|_: PostForkError| PostForkError)
}) as Box<_>
}),
pid: None,
};
+15 -6
View File
@@ -839,9 +839,8 @@ impl StreamHandler<'_> {
//-------------------------------------------------------------------------
// OSC
fn window_title(&mut self, title: &str) {
fn window_title(&mut self, title: &[u8]) {
let mut buf = [0u8; SET_TITLE_LEN];
let title = title.as_bytes();
if title.len() >= buf.len() {
log::warn!("change title requested larger than our buffer size, ignoring");
return;
@@ -949,7 +948,7 @@ impl StreamHandler<'_> {
// If we haven't seen a title, we're using the pwd as our title.
// Set it to blank which will reset our title behavior.
if !self.st.seen_title {
self.window_title("");
self.window_title(b"");
debug_assert!(!self.st.seen_title);
}
@@ -1015,8 +1014,7 @@ impl StreamHandler<'_> {
// If we haven't seen a title, use our pwd as the title.
if !self.st.seen_title {
// Titles are text; only the title is lossy.
self.window_title(&String::from_utf8_lossy(&path));
self.window_title(&path);
self.st.seen_title = false;
}
Ok(())
@@ -1501,7 +1499,7 @@ impl stream::Handler for StreamHandler<'_> {
Action::EndHyperlink => self.terminal.screen_mut().end_hyperlink(),
Action::ActiveStatusDisplay(v) => self.terminal.status_display = v,
Action::Decaln => self.terminal.decaln()?,
Action::WindowTitle(v) => self.window_title(v.title),
Action::WindowTitle(v) => self.window_title(v.title.as_bytes()),
Action::ReportPwd(v) => self.report_pwd(v.url)?,
Action::ShowDesktopNotification(v) => self.show_desktop_notification(v.title, v.body),
Action::ProgressReport(v) => self.progress_report(v),
@@ -1984,6 +1982,17 @@ mod tests {
assert!(!h.stream.state.seen_title);
}
#[test]
fn report_pwd_title_keeps_raw_bytes() {
let mut h = Harness::new();
h.feed(b"\x1b]7;file://localhost/tmp/%FF\x1b\\");
let msgs = h.surface_msgs();
assert!(
matches!(msgs.last(), Some(SurfaceMessage::SetTitle(t)) if t.starts_with(b"/tmp/\xff\0")),
"{msgs:?}"
);
}
#[test]
fn report_pwd_remote_host_ignored_and_reset() {
let mut h = Harness::new();