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:
1 parent
95fe02b55a
commit
f2ccb04aa4
21 files changed
+479
-133
No files matched your search
@@ -3818,6 +3818,47 @@ fn shell_from_env(value: &std::ffi::OsStr) -> Command {
|
||||
Command::Shell(ghostty_os::osbytes::EncodedOs::from_os(value))
|
||||
}
|
||||
|
||||
/// The application runtime (Zig `build_config.app_runtime`). It only
|
||||
/// affects finalization (GTK-only defaults).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AppRuntime {
|
||||
/// No apprt: the embedded library (and wasm).
|
||||
None,
|
||||
/// GTK, the default on Linux and BSD.
|
||||
Gtk,
|
||||
}
|
||||
|
||||
const RUNTIME_DEFAULT: u8 = if cfg!(all(
|
||||
unix,
|
||||
not(target_vendor = "apple"),
|
||||
not(target_family = "wasm")
|
||||
)) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
static APP_RUNTIME: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(RUNTIME_DEFAULT);
|
||||
|
||||
/// The process-wide runtime used by [`Config::finalize`] (and by the
|
||||
/// load/reload paths which finalize internally).
|
||||
#[must_use]
|
||||
pub fn app_runtime() -> AppRuntime {
|
||||
match APP_RUNTIME.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
1 => AppRuntime::Gtk,
|
||||
_ => AppRuntime::None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Select the process-wide runtime. The embedded library selects
|
||||
/// [`AppRuntime::None`] so `gtk-single-instance = detect` stays `detect`.
|
||||
pub fn set_app_runtime(runtime: AppRuntime) {
|
||||
APP_RUNTIME.store(
|
||||
u8::from(runtime == AppRuntime::Gtk),
|
||||
std::sync::atomic::Ordering::Relaxed,
|
||||
);
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// The value of the config key in the form the C API exposes it (Zig:
|
||||
/// `c_get.get`). `None` if there is no such key.
|
||||
@@ -4478,6 +4519,12 @@ impl Config {
|
||||
/// Call this once after you are done setting configuration. This
|
||||
/// is idempotent but will waste memory if called multiple times.
|
||||
pub fn finalize(&mut self) -> Result<(), Error> {
|
||||
self.finalize_for(app_runtime())
|
||||
}
|
||||
|
||||
/// [`Config::finalize`] for an explicit application runtime (Zig's
|
||||
/// `build_config.app_runtime`, a build option there).
|
||||
pub fn finalize_for(&mut self, runtime: AppRuntime) -> Result<(), Error> {
|
||||
// We always load the theme first because it may set other fields
|
||||
// in our config.
|
||||
if let Some(theme) = self.theme.clone() {
|
||||
@@ -4543,19 +4590,22 @@ impl Config {
|
||||
wd.finalize();
|
||||
self.working_directory = Some(wd);
|
||||
|
||||
// Apprt-specific defaults (GTK)
|
||||
match self.gtk_single_instance {
|
||||
GtkSingleInstance::True | GtkSingleInstance::False => {}
|
||||
// Apprt-specific defaults
|
||||
match runtime {
|
||||
AppRuntime::None => {}
|
||||
AppRuntime::Gtk => match self.gtk_single_instance {
|
||||
GtkSingleInstance::True | GtkSingleInstance::False => {}
|
||||
|
||||
// For detection, we assume single instance unless we're
|
||||
// in a CLI environment, then we disable single instance.
|
||||
GtkSingleInstance::Detect => {
|
||||
self.gtk_single_instance = if probable_cli {
|
||||
GtkSingleInstance::False
|
||||
} else {
|
||||
GtkSingleInstance::True
|
||||
};
|
||||
}
|
||||
// For detection, we assume single instance unless we're
|
||||
// in a CLI environment, then we disable single instance.
|
||||
GtkSingleInstance::Detect => {
|
||||
self.gtk_single_instance = if probable_cli {
|
||||
GtkSingleInstance::False
|
||||
} else {
|
||||
GtkSingleInstance::True
|
||||
};
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Default our click interval
|
||||
@@ -5433,6 +5483,19 @@ mod tests {
|
||||
assert!(cfg.conditional_set.contains(&conditional::Key::Theme));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gtk_single_instance_detect_only_finalizes_for_gtk() {
|
||||
let mut cfg = Config::default();
|
||||
cfg.load_iter(&mut it(&["--gtk-single-instance=detect"]))
|
||||
.unwrap();
|
||||
let mut none = cfg.clone();
|
||||
none.finalize_for(AppRuntime::None).unwrap();
|
||||
assert_eq!(none.gtk_single_instance, GtkSingleInstance::Detect);
|
||||
let mut gtk = cfg;
|
||||
gtk.finalize_for(AppRuntime::Gtk).unwrap();
|
||||
assert_ne!(gtk.gtk_single_instance, GtkSingleInstance::Detect);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compatibility_gtk_single_instance_desktop() {
|
||||
let mut cfg = Config::default();
|
||||
|
||||
@@ -4,11 +4,16 @@
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum StringError {
|
||||
InvalidString,
|
||||
/// `std.unicode.utf8Encode` of an escaped surrogate half.
|
||||
Utf8CannotEncodeSurrogateHalf,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for StringError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("InvalidString")
|
||||
f.write_str(match self {
|
||||
StringError::InvalidString => "InvalidString",
|
||||
StringError::Utf8CannotEncodeSurrogateHalf => "Utf8CannotEncodeSurrogateHalf",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +93,11 @@ pub fn parse_bytes(input: impl AsRef<[u8]>) -> Result<Vec<u8>, StringError> {
|
||||
continue;
|
||||
}
|
||||
let cp = parse_escape_sequence(bytes, &mut i).ok_or(StringError::InvalidString)?;
|
||||
let ch = char::from_u32(cp).ok_or(StringError::InvalidString)?;
|
||||
let ch = char::from_u32(cp).ok_or(if (0xd800..=0xdfff).contains(&cp) {
|
||||
StringError::Utf8CannotEncodeSurrogateHalf
|
||||
} else {
|
||||
StringError::InvalidString
|
||||
})?;
|
||||
out.extend_from_slice(ch.encode_utf8(&mut [0; 4]).as_bytes());
|
||||
}
|
||||
Ok(out)
|
||||
@@ -200,6 +209,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parse_invalid() {
|
||||
assert_eq!(
|
||||
parse("\\u{d800}"),
|
||||
Err(StringError::Utf8CannotEncodeSurrogateHalf)
|
||||
);
|
||||
assert_eq!(
|
||||
crate::cli_args::ArgError::from(StringError::Utf8CannotEncodeSurrogateHalf),
|
||||
crate::cli_args::ArgError::other("Utf8CannotEncodeSurrogateHalf")
|
||||
);
|
||||
assert_eq!(parse("\\q"), Err(StringError::InvalidString));
|
||||
assert_eq!(parse("\\"), Err(StringError::InvalidString));
|
||||
assert_eq!(parse("\\u{}"), Err(StringError::InvalidString));
|
||||
|
||||
@@ -738,7 +738,7 @@ impl ConfigField for RepeatableFontVariation {
|
||||
&format!(
|
||||
"{}={}",
|
||||
ghostty_os::osbytes::bytes_to_config_string(&id),
|
||||
v.value
|
||||
ghostty_os::zig_fmt::format_float_d(v.value)
|
||||
),
|
||||
)?;
|
||||
}
|
||||
@@ -1508,6 +1508,13 @@ mod tests {
|
||||
assert_eq!(list.list, vec![v(b"wght", 200.0), v(b"slnt", -15.0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_nan_like_zig() {
|
||||
let mut list = RepeatableFontVariation::default();
|
||||
parse(&mut list, "wght=NaN").unwrap();
|
||||
assert_eq!(fe(&list), "a = wght=nan\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_config_single() {
|
||||
let mut list = RepeatableFontVariation::default();
|
||||
|
||||
@@ -568,7 +568,9 @@ impl QuickTerminalSizeValue {
|
||||
|
||||
fn format(self, out: &mut String) {
|
||||
match self {
|
||||
Self::Percentage(v) => out.push_str(&format!("{v}%")),
|
||||
Self::Percentage(v) => {
|
||||
out.push_str(&format!("{}%", ghostty_os::zig_fmt::format_float_d(v)));
|
||||
}
|
||||
Self::Pixels(v) => out.push_str(&format!("{v}px")),
|
||||
}
|
||||
}
|
||||
@@ -1287,6 +1289,13 @@ config_packed! {
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used, clippy::approx_constant)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn quick_terminal_size_value_formats_nan_like_zig() {
|
||||
let mut out = String::new();
|
||||
QuickTerminalSizeValue::Percentage(f32::NAN).format(&mut out);
|
||||
assert_eq!(out, "nan%");
|
||||
}
|
||||
|
||||
use super::*;
|
||||
use crate::formatter::format_entry;
|
||||
|
||||
|
||||
Reference in new issue
Block a user