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

@@ -1286,8 +1286,9 @@ impl Application {
})
}
Action::MouseOverLink(v) => {
let url = v.url.to_text_lossy();
tgt.with_surface_void("mouse over link", |s| {
s.set_mouse_hover_url(if v.url.is_empty() { None } else { Some(&v.url) });
s.set_mouse_hover_url(if url.is_empty() { None } else { Some(&url) });
});
true
}
@@ -1585,26 +1586,28 @@ impl Application {
fn desktop_notification(&self, tgt: &Tgt, n: &action::DesktopNotification) {
if let Some(s) = &tgt.surface {
s.send_desktop_notification(&n.title, &n.body);
s.send_desktop_notification(&n.title.to_text_lossy(), &n.body.to_text_lossy());
return;
}
// Set a default title if we don't already have one
let title = if n.title.is_empty() {
let n_title = n.title.to_text_lossy();
let n_body = n.body.to_text_lossy();
let title = if n_title.is_empty() {
"Ghostty"
} else {
n.title.as_str()
n_title.as_str()
};
let notification = gio::Notification::new(title);
notification.set_body(Some(&n.body));
notification.set_body(Some(&n_body));
notification.set_icon(&gio::ThemedIcon::new("com.mitchellh.ghostty"));
notification
.set_default_action_and_target_value("app.present-surface", Some(&0u64.to_variant()));
// We set the notification ID to the body content. If the content is
// the same, this notification may replace a previous notification.
self.send_notification(Some(&n.body), &notification);
self.send_notification(Some(&n_body), &notification);
}
fn goto_split(surface: &Surface, to: action::GotoSplit) -> bool {