Files
ghostty-rs/crates/ghostty-wasm/src/config.rs
T
ddidderr da9599140b 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)
2026-10-12 13:09:04 +02:00

70 lines
2.5 KiB
Rust

//! `config/Wasm.zig`: the wasm-compatible config API.
use std::io::Cursor;
use ghostty_config::{
cli_args::{self, LineIterator},
config::Config,
file_formatter::FileFormatter,
};
use crate::{ensure_init, host_bytes};
/// Create a new configuration filled with the initial default values.
#[unsafe(no_mangle)]
pub extern "C" fn config_new() -> *mut Config {
ensure_init();
ghostty_config::config::set_app_runtime(ghostty_config::config::AppRuntime::None);
Box::into_raw(Box::new(Config::new_default()))
}
#[unsafe(no_mangle)]
pub extern "C" fn config_free(ptr: *mut Config) {
if !ptr.is_null() {
// SAFETY: pointer came from `config_new` and is freed once.
drop(unsafe { Box::from_raw(ptr) });
}
}
/// Load the configuration from a string in the same format as the file-based
/// syntax for the desktop version of the terminal.
#[unsafe(no_mangle)]
pub extern "C" fn config_load_string(config: *mut Config, s: *const u8, len: usize) {
// SAFETY: live pointer from `config_new`; `s` is valid for `len` bytes.
let (config, s) = unsafe { (&mut *config, host_bytes(s, len)) };
let mut iter = LineIterator::new(Cursor::new(s));
if let Err(err) = cli_args::parse(config, &mut iter) {
log::error!("error loading config err={err}");
}
}
#[unsafe(no_mangle)]
pub extern "C" fn config_finalize(config: *mut Config) {
// SAFETY: live pointer from `config_new`.
if let Err(err) = unsafe { &mut *config }.finalize() {
log::error!("error finalizing config err={err}");
}
}
/// Extra (not in Zig): writes the config in file format (like
/// `+show-config`, only changed values) to `out` (capacity `cap`) and returns
/// the full length; the output is truncated if it does not fit.
#[unsafe(no_mangle)]
pub extern "C" fn config_format(config: *const Config, out: *mut u8, cap: usize) -> usize {
// SAFETY: live pointer from `config_new`; `out` is valid for `cap` bytes.
let (config, out) = unsafe { (&*config, std::slice::from_raw_parts_mut(out, cap)) };
let mut fmt = FileFormatter::new(config);
fmt.changed = true;
let s = fmt.to_string();
let n = s.len().min(cap);
out[..n].copy_from_slice(&s.as_bytes()[..n]);
s.len()
}
/// Extra (not in Zig): the number of diagnostics (parse errors) recorded.
#[unsafe(no_mangle)]
pub extern "C" fn config_diagnostics_count(config: *const Config) -> usize {
// SAFETY: live pointer from `config_new`.
unsafe { &*config }.diagnostics.items().len()
}