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
395cff8981
commit
a9f3856b15
30 files changed
+863
-80
No files matched your search
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user