fix: Windows core build, embedded init, byte-exact paths, version info
Closes the remaining core/lib/termio/GTK rows (final-Z2.md): - ghostty-core compiles for Windows (Unix-only exec fields and file modes gated); checked with cargo check --target x86_64-pc-windows-gnu. - Embedded init: ensure_locale, SIGPIPE ignored in ghostty_init for C/Swift hosts, Apple crash tags; the argv passed to ghostty_init feeds ghostty_config_load_cli_args; ghostty_config_load_file accepts non-UTF-8 paths. - OSC 7 pwd, embedded surface cwd/command/env/initial input, ghostty_surface_text and GTK file drops keep raw bytes. - XTVERSION, TERM_PROGRAM_VERSION, the About dialog and crash metadata use ghostty_build_info instead of hardcoded versions. Test Plan: - cargo test --workspace (3327 passed) - cargo clippy --workspace --all-targets -- -D warnings
This commit is contained in:
1 parent
8519f1ba1b
commit
ebefad5031
30 files changed
+863
-80
No files matched your search
@@ -23,6 +23,7 @@ ghostty-bench.workspace = true
|
||||
ghostty-cli.workspace = true
|
||||
ghostty-config.workspace = true
|
||||
ghostty-core.workspace = true
|
||||
ghostty-crash.workspace = true
|
||||
ghostty-input.workspace = true
|
||||
ghostty-inspector.workspace = true
|
||||
ghostty-os.workspace = true
|
||||
@@ -31,6 +32,9 @@ ghostty-terminal.workspace = true
|
||||
ghostty-vt.workspace = true
|
||||
log = "0.4.34"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2.190"
|
||||
|
||||
[target.'cfg(target_vendor = "apple")'.dependencies]
|
||||
block2 = "0.6.2"
|
||||
dispatch2 = "0.3.1"
|
||||
@@ -40,6 +44,7 @@ objc2-foundation = "0.3.2"
|
||||
objc2-metal = { version = "0.3.2", features = ["block2"] }
|
||||
|
||||
[dev-dependencies]
|
||||
ghostty-build-info.workspace = true
|
||||
tempfile = "3"
|
||||
|
||||
# The workspace forbids `unsafe`; this crate is the C ABI boundary, so it
|
||||
|
||||
@@ -36,7 +36,14 @@ pub struct Keep {
|
||||
impl Keep {
|
||||
/// A NUL terminated copy of `s` (interior NULs are dropped).
|
||||
fn cstr(&mut self, s: &str) -> *const c_char {
|
||||
let c = CString::new(s.replace('\0', "")).unwrap_or_default();
|
||||
self.cstr_bytes(s.as_bytes())
|
||||
}
|
||||
|
||||
/// [`Keep::cstr`] for raw bytes.
|
||||
fn cstr_bytes(&mut self, s: &[u8]) -> *const c_char {
|
||||
let mut s = s.to_vec();
|
||||
s.retain(|&b| b != 0);
|
||||
let c = CString::new(s).unwrap_or_default();
|
||||
let ptr = c.as_ptr();
|
||||
// The heap buffer of a CString doesn't move when the Vec grows.
|
||||
self.strings.push(c);
|
||||
@@ -123,7 +130,9 @@ pub fn action_to_c(action: &Action, keep: &mut Keep) -> ghostty_action_s {
|
||||
Action::SetTitle(t) => v.set_title.title = keep.cstr(&t.title),
|
||||
Action::SetTabTitle(t) => v.set_tab_title.title = keep.cstr(&t.title),
|
||||
Action::PromptTitle(p) => v.prompt_title = *p as c_int,
|
||||
Action::Pwd(p) => v.pwd.pwd = keep.cstr(&p.pwd),
|
||||
Action::Pwd(p) => {
|
||||
v.pwd.pwd = keep.cstr_bytes(&ghostty_os::osbytes::config_str_to_bytes(&p.pwd));
|
||||
}
|
||||
Action::MouseShape(s) => v.mouse_shape = *s as c_int,
|
||||
Action::MouseVisibility(m) => v.mouse_visibility = *m as c_int,
|
||||
Action::MouseOverLink(l) => {
|
||||
|
||||
@@ -249,9 +249,10 @@ pub unsafe extern "C" fn ghostty_config_load_file(ptr: ghostty_config_t, path: *
|
||||
return;
|
||||
}
|
||||
// SAFETY: `path` is NUL terminated.
|
||||
let path = unsafe { CStr::from_ptr(path) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
// Non-UTF-8 file names are valid on Unix; keep the bytes (reversible
|
||||
// escape understood by the config file loader).
|
||||
let path =
|
||||
ghostty_os::osbytes::bytes_to_config_string(unsafe { CStr::from_ptr(path) }.to_bytes());
|
||||
if let Err(err) = c.config.load_file(&path) {
|
||||
log::error!("error loading config from file path={path} err={err}");
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ use ghostty_input::{
|
||||
binding::Action as BindingAction,
|
||||
keycodes,
|
||||
};
|
||||
use ghostty_os::osbytes;
|
||||
use ghostty_terminal::{
|
||||
point::{Coordinate, Point, Tag as PointTag},
|
||||
selection::Selection,
|
||||
@@ -884,17 +885,22 @@ fn string_escape(input: &str) -> String {
|
||||
unsafe fn apply_surface_options(config: &mut Config, opts: &ghostty_surface_config_s) {
|
||||
// If we have a working directory from the options then we set it.
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(wd) =
|
||||
unsafe { cstr(opts.working_directory) }.map(|c| c.to_string_lossy().into_owned())
|
||||
if let Some(wd) = unsafe { cstr(opts.working_directory) }.map(CStr::to_bytes)
|
||||
&& !wd.is_empty()
|
||||
{
|
||||
match std::fs::metadata(&wd) {
|
||||
Err(err) => log::warn!("error opening requested working directory dir={wd} err={err}"),
|
||||
// Raw bytes are kept (reversible escape in the String based config).
|
||||
let wd_str = osbytes::bytes_to_config_string(wd);
|
||||
let wd_os = osbytes::os_from_bytes(wd.to_vec());
|
||||
let wd_disp = String::from_utf8_lossy(wd);
|
||||
match std::fs::metadata(&wd_os) {
|
||||
Err(err) => {
|
||||
log::warn!("error opening requested working directory dir={wd_disp} err={err}");
|
||||
}
|
||||
Ok(m) if !m.is_dir() => {
|
||||
log::warn!("requested working directory is not a directory dir={wd}");
|
||||
log::warn!("requested working directory is not a directory dir={wd_disp}");
|
||||
}
|
||||
Ok(_) => {
|
||||
let mut wd_val = WorkingDirectory::Path(wd);
|
||||
let mut wd_val = WorkingDirectory::Path(wd_str);
|
||||
wd_val.finalize();
|
||||
config.working_directory = Some(wd_val);
|
||||
}
|
||||
@@ -903,10 +909,10 @@ unsafe fn apply_surface_options(config: &mut Config, opts: &ghostty_surface_conf
|
||||
|
||||
// If we have a command from the options then we set it.
|
||||
// SAFETY: per the function contract.
|
||||
if let Some(cmd) = unsafe { cstr(opts.command) }.map(|c| c.to_string_lossy().into_owned())
|
||||
if let Some(cmd) = unsafe { cstr(opts.command) }.map(CStr::to_bytes)
|
||||
&& !cmd.is_empty()
|
||||
{
|
||||
config.command = Some(Command::Shell(cmd));
|
||||
config.command = Some(Command::Shell(osbytes::bytes_to_config_string(cmd)));
|
||||
config.wait_after_command = true;
|
||||
}
|
||||
|
||||
@@ -919,7 +925,10 @@ unsafe fn apply_surface_options(config: &mut Config, opts: &ghostty_surface_conf
|
||||
let (Some(k), Some(v)) = (unsafe { cstr(var.key) }, unsafe { cstr(var.value) }) else {
|
||||
continue;
|
||||
};
|
||||
config.env.put(&k.to_string_lossy(), &v.to_string_lossy());
|
||||
config.env.put(
|
||||
&osbytes::bytes_to_config_string(k.to_bytes()),
|
||||
&osbytes::bytes_to_config_string(v.to_bytes()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -929,10 +938,9 @@ unsafe fn apply_surface_options(config: &mut Config, opts: &ghostty_surface_conf
|
||||
// We need to escape the string because the "raw" field expects a
|
||||
// Zig string.
|
||||
config.input.list.clear();
|
||||
config
|
||||
.input
|
||||
.list
|
||||
.push(ReadableIO::Raw(string_escape(&input.to_string_lossy())));
|
||||
config.input.list.push(ReadableIO::Raw(string_escape(
|
||||
&osbytes::bytes_to_config_string(input.to_bytes()),
|
||||
)));
|
||||
}
|
||||
|
||||
// Wait after command
|
||||
@@ -1107,7 +1115,7 @@ pub unsafe extern "C" fn ghostty_surface_inherited_config(
|
||||
}
|
||||
|
||||
if should_inherit_working_directory(context, &config)
|
||||
&& let Some(cwd) = core.pwd()
|
||||
&& let Some(cwd) = core.pwd_bytes()
|
||||
&& let Ok(c) = CString::new(cwd)
|
||||
{
|
||||
let mut keep = s.inherited_wd.borrow_mut();
|
||||
@@ -1593,7 +1601,7 @@ pub unsafe extern "C" fn ghostty_surface_text(
|
||||
// SAFETY: `text` points to `len` readable bytes.
|
||||
unsafe { std::slice::from_raw_parts(text.cast::<u8>(), len) }
|
||||
};
|
||||
if let Err(err) = core.text_callback(&String::from_utf8_lossy(bytes)) {
|
||||
if let Err(err) = core.text_callback_bytes(bytes) {
|
||||
log::error!("error in key callback err={err}");
|
||||
}
|
||||
}
|
||||
@@ -2155,3 +2163,61 @@ mod darwin {
|
||||
fill_text(core, t, sel, result)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
use super::*;
|
||||
use crate::types::ghostty_env_var_s;
|
||||
|
||||
/// Surface options are raw C bytes in Zig; non-UTF-8 values must reach
|
||||
/// the config unchanged (reversible byte escape).
|
||||
#[test]
|
||||
fn surface_options_keep_non_utf8_bytes() {
|
||||
let td = tempfile::tempdir().unwrap();
|
||||
let mut dir = td.path().as_os_str().as_bytes().to_vec();
|
||||
dir.extend_from_slice(b"/d\xff");
|
||||
std::fs::create_dir(std::ffi::OsStr::from_bytes(&dir)).unwrap();
|
||||
|
||||
let wd = CString::new(dir.clone()).unwrap();
|
||||
let cmd = CString::new(&b"echo \xfe"[..]).unwrap();
|
||||
let key = CString::new(&b"K\xfd"[..]).unwrap();
|
||||
let val = CString::new(&b"v\xfc"[..]).unwrap();
|
||||
let input = CString::new(&b"in\xff\n"[..]).unwrap();
|
||||
let mut env = [ghostty_env_var_s {
|
||||
key: key.as_ptr(),
|
||||
value: val.as_ptr(),
|
||||
}];
|
||||
|
||||
// SAFETY: all-zero is a valid value (null pointers, 0 enums).
|
||||
let mut opts: ghostty_surface_config_s = unsafe { std::mem::zeroed() };
|
||||
opts.working_directory = wd.as_ptr();
|
||||
opts.command = cmd.as_ptr();
|
||||
opts.env_vars = env.as_mut_ptr();
|
||||
opts.env_var_count = 1;
|
||||
opts.initial_input = input.as_ptr();
|
||||
|
||||
let mut config = Config::default();
|
||||
// SAFETY: the pointers above are valid NUL terminated strings.
|
||||
unsafe { apply_surface_options(&mut config, &opts) };
|
||||
|
||||
let got_wd = config
|
||||
.working_directory
|
||||
.as_ref()
|
||||
.and_then(|w| w.value())
|
||||
.unwrap();
|
||||
assert_eq!(osbytes::config_str_to_bytes(got_wd), dir);
|
||||
let Some(Command::Shell(c)) = &config.command else {
|
||||
panic!("no command");
|
||||
};
|
||||
assert_eq!(osbytes::config_str_to_bytes(c), b"echo \xfe");
|
||||
let (k, v) = config.env.iter().next().unwrap();
|
||||
assert_eq!(osbytes::config_str_to_bytes(k), b"K\xfd");
|
||||
assert_eq!(osbytes::config_str_to_bytes(v), b"v\xfc");
|
||||
assert!(matches!(
|
||||
config.input.list.first(),
|
||||
Some(ReadableIO::Raw(_))
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,13 @@
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ffi::{CStr, CString, c_char, c_int},
|
||||
ffi::{CStr, CString, OsString, c_char, c_int},
|
||||
sync::{Mutex, OnceLock},
|
||||
};
|
||||
|
||||
use ghostty_cli::build_info::{self, BuildInfo};
|
||||
use ghostty_core::global::{self, InitOptions};
|
||||
use ghostty_os::osbytes;
|
||||
|
||||
use crate::types::{build_mode, ghostty_info_s, ghostty_string_s};
|
||||
|
||||
@@ -77,7 +78,8 @@ pub(crate) fn alloc_c_text(bytes: &[u8]) -> *const c_char {
|
||||
/// `argv` must be null or point to `argc` valid NUL terminated strings.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_init(argc: usize, argv: *mut *mut c_char) -> c_int {
|
||||
let mut args = Vec::with_capacity(argc);
|
||||
// Raw argument bytes (Zig: `std.os.argv = argv[0..argc]`).
|
||||
let mut raw: Vec<OsString> = Vec::with_capacity(argc);
|
||||
if !argv.is_null() {
|
||||
for i in 0..argc {
|
||||
// SAFETY: the caller guarantees `argv[0..argc]` are valid.
|
||||
@@ -87,10 +89,18 @@ pub unsafe extern "C" fn ghostty_init(argc: usize, argv: *mut *mut c_char) -> c_
|
||||
}
|
||||
// SAFETY: each argv entry is a NUL terminated string.
|
||||
let c = unsafe { CStr::from_ptr(p) };
|
||||
args.push(c.to_string_lossy().into_owned());
|
||||
raw.push(osbytes::os_from_bytes(c.to_bytes().to_vec()));
|
||||
}
|
||||
}
|
||||
|
||||
// Later config CLI loading (`ghostty_config_load_cli_args`) must see
|
||||
// these arguments rather than the process arguments.
|
||||
ghostty_os::args::set_argv(raw.clone());
|
||||
let args: Vec<String> = raw
|
||||
.iter()
|
||||
.map(|a| osbytes::os_to_config_string(a))
|
||||
.collect();
|
||||
|
||||
// We first try to parse any action that we may be executing.
|
||||
let action = match ghostty_cli::ghostty::detect(&args) {
|
||||
Ok(a) => a,
|
||||
@@ -101,16 +111,48 @@ pub unsafe extern "C" fn ghostty_init(argc: usize, argv: *mut *mut c_char) -> c_
|
||||
};
|
||||
let _ = ARGS.set(args);
|
||||
|
||||
// `+version` and friends report the library build.
|
||||
build_info::set(lib_build_info());
|
||||
|
||||
// Setup our signal handlers before logging (Zig: `initSignals`). A C or
|
||||
// Swift host does not get the Rust runtime's SIGPIPE ignore.
|
||||
init_signals();
|
||||
|
||||
global::init_lib(InitOptions {
|
||||
action: action.is_some(),
|
||||
resources_dir: None,
|
||||
crash: Some(lib_crash_options()),
|
||||
});
|
||||
|
||||
// `+version` and friends report the library build.
|
||||
build_info::set(lib_build_info());
|
||||
0
|
||||
}
|
||||
|
||||
/// Ignore SIGPIPE: it is common with how termio shuts down read threads
|
||||
/// (Zig: `GlobalState.initSignals`; not on Windows).
|
||||
fn init_signals() {
|
||||
#[cfg(unix)]
|
||||
// SAFETY: installing SIG_IGN for SIGPIPE has no memory safety impact.
|
||||
unsafe {
|
||||
libc::signal(libc::SIGPIPE, libc::SIG_IGN);
|
||||
}
|
||||
}
|
||||
|
||||
/// Crash metadata for the library artifact: no app runtime, CoreText +
|
||||
/// Metal on Apple targets (so the GTK / fontconfig / OpenGL defaults are
|
||||
/// not reported for the macOS app).
|
||||
fn lib_crash_options() -> ghostty_crash::Options {
|
||||
let info = build_info::get();
|
||||
let tag = |s: &str| s.trim_start_matches('.').to_owned();
|
||||
let mut o = global::default_crash_options();
|
||||
o.release.clone_from(&info.version_string);
|
||||
o.app_runtime = tag(&lib_build_info().app_runtime);
|
||||
if cfg!(target_vendor = "apple") {
|
||||
"coretext".clone_into(&mut o.font_backend);
|
||||
"metal".clone_into(&mut o.renderer);
|
||||
}
|
||||
o
|
||||
}
|
||||
|
||||
/// The build info of the library (Zig: `build_config` for the lib artifact:
|
||||
/// no app runtime, CoreText + Metal on Apple targets).
|
||||
fn lib_build_info() -> BuildInfo {
|
||||
|
||||
@@ -43,7 +43,10 @@ fn host_program_passes() {
|
||||
"host failed\nstdout:\n{stdout}\nstderr:\n{stderr}"
|
||||
);
|
||||
assert!(stdout.contains("OK"), "stdout:\n{stdout}");
|
||||
assert!(stdout.contains("version=1.3.1"), "stdout:\n{stdout}");
|
||||
assert!(
|
||||
stdout.contains(&format!("version={}", ghostty_build_info::VERSION_STRING)),
|
||||
"stdout:\n{stdout}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -55,7 +58,10 @@ fn cli_try_action_runs_version_and_exits() {
|
||||
"stderr: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
assert!(stdout.starts_with("Ghostty 1.3.1"), "stdout:\n{stdout}");
|
||||
assert!(
|
||||
stdout.starts_with(&format!("Ghostty {}", ghostty_build_info::VERSION_STRING)),
|
||||
"stdout:\n{stdout}"
|
||||
);
|
||||
// The action ran instead of the rest of the program.
|
||||
assert!(!stdout.contains("OK"));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
//! `ghostty_init(argc, argv)` semantics from `main_c.zig`: the supplied
|
||||
//! argument vector (not the process arguments) feeds the later config CLI
|
||||
//! loading, and SIGPIPE is ignored for C / Swift hosts.
|
||||
#![cfg(unix)]
|
||||
#![allow(
|
||||
unsafe_code,
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::undocumented_unsafe_blocks
|
||||
)]
|
||||
|
||||
use std::ffi::{CString, c_char};
|
||||
|
||||
use ghostty::{config_capi::*, main_c::ghostty_init, types::ghostty_config_t};
|
||||
|
||||
fn get_bool(cfg: ghostty_config_t, key: &str) -> bool {
|
||||
let mut out = false;
|
||||
let ok =
|
||||
unsafe { ghostty_config_get(cfg, (&raw mut out).cast(), key.as_ptr().cast(), key.len()) };
|
||||
assert!(ok);
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_argv_feeds_config_cli_and_ignores_sigpipe() {
|
||||
// Start from the default disposition so that the check is meaningful.
|
||||
unsafe { libc::signal(libc::SIGPIPE, libc::SIG_DFL) };
|
||||
|
||||
// Not the process argv: the test harness argv has no `--maximize`.
|
||||
let owned: Vec<CString> = [&b"ghostty"[..], b"--maximize=true"]
|
||||
.iter()
|
||||
.map(|a| CString::new(*a).unwrap())
|
||||
.collect();
|
||||
let mut argv: Vec<*mut c_char> = owned.iter().map(|c| c.as_ptr().cast_mut()).collect();
|
||||
assert_eq!(unsafe { ghostty_init(argv.len(), argv.as_mut_ptr()) }, 0);
|
||||
|
||||
let mut sa: libc::sigaction = unsafe { std::mem::zeroed() };
|
||||
unsafe { libc::sigaction(libc::SIGPIPE, std::ptr::null(), &mut sa) };
|
||||
assert_eq!(sa.sa_sigaction, libc::SIG_IGN);
|
||||
|
||||
let cfg = ghostty_config_new();
|
||||
unsafe { ghostty_config_load_cli_args(cfg) };
|
||||
assert!(get_bool(cfg, "maximize"));
|
||||
unsafe { ghostty_config_free(cfg) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_load_file_accepts_non_utf8_path() {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut name = dir.path().as_os_str().as_bytes().to_vec();
|
||||
name.extend_from_slice(b"/cfg\xff");
|
||||
let path = std::ffi::OsStr::from_bytes(&name);
|
||||
std::fs::write(path, "maximize = true\n").unwrap();
|
||||
|
||||
let cfg = ghostty_config_new();
|
||||
let c = CString::new(name).unwrap();
|
||||
unsafe { ghostty_config_load_file(cfg, c.as_ptr()) };
|
||||
assert!(get_bool(cfg, "maximize"));
|
||||
unsafe { ghostty_config_free(cfg) };
|
||||
}
|
||||
Reference in new issue
Block a user