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:
ddidderr committed 2026-10-12 08:14:02 +02:00
1 parent 8519f1ba1b
commit ebefad5031
30 files changed
+863 -80

No files matched your search

+37 -5
View File
@@ -18,7 +18,7 @@ use std::{
panic::{AssertUnwindSafe, catch_unwind},
};
use ghostty_os::file::Rlimit;
use ghostty_os::{file::Rlimit, osbytes::config_str_to_bytes};
use rustix::{
io::Errno,
process::{self, WaitOptions},
@@ -264,13 +264,13 @@ impl Command {
}
fn prepare(&self) -> Result<Prepared, NulError> {
let path = CString::new(self.path.as_str())?;
let path = CString::new(config_str_to_bytes(&self.path))?;
let argv = if self.args.is_empty() {
vec![path.clone()]
} else {
self.args
.iter()
.map(|a| CString::new(a.as_str()))
.map(|a| CString::new(config_str_to_bytes(a)))
.collect::<Result<_, _>>()?
};
let envp = match &self.env {
@@ -297,7 +297,7 @@ impl Command {
#[cfg(target_vendor = "apple")]
let candidates = exec_candidates(&self.path, std::env::var("PATH").ok().as_deref())
.into_iter()
.map(CString::new)
.map(|c| CString::new(config_str_to_bytes(&c)))
.collect::<Result<Vec<_>, _>>()?;
Ok(Prepared {
path,
@@ -315,7 +315,10 @@ impl Command {
pub fn start(&mut self) -> Result<(), StartError> {
// IMPORTANT: do all allocation prior to the fork().
let prep = self.prepare()?;
let cwd = self.cwd.as_deref().and_then(|c| CString::new(c).ok());
let cwd = self
.cwd
.as_deref()
.and_then(|c| CString::new(config_str_to_bytes(c)).ok());
let mut argv_ptrs: Vec<*const c_char> = prep.argv.iter().map(|c| c.as_ptr()).collect();
argv_ptrs.push(std::ptr::null());
@@ -876,6 +879,35 @@ mod tests {
assert_eq!("/tmp\n", read_all(stdout));
}
#[test]
fn non_utf8_cwd_and_args_are_preserved() {
use ghostty_os::osbytes::{bytes_to_config_string, os_from_bytes};
let td = TempDir::init().unwrap();
let stdout = test_file(&td, "stdout.txt");
// A directory whose name is not valid UTF-8, handed over with the
// reversible byte escape used by the String based layers.
let mut raw = ghostty_os::osbytes::os_bytes(td.path().as_os_str());
raw.extend_from_slice(b"/d\xff");
std::fs::create_dir(os_from_bytes(raw.clone())).unwrap();
let mut cmd = Command::new("/bin/sh", &["/bin/sh", "-c", "pwd -P; printf %s \"$0\""]);
cmd.args.push(bytes_to_config_string(b"a\xffb"));
cmd.stdout = Some(stdout.try_clone().unwrap().into());
cmd.cwd = Some(bytes_to_config_string(&raw));
cmd.start().unwrap();
assert_eq!(cmd.wait(true).unwrap(), Exit::Exited(0));
let mut bytes = Vec::new();
std::io::Read::read_to_end(
&mut std::fs::File::open(td.path().join("stdout.txt")).unwrap(),
&mut bytes,
)
.unwrap();
assert!(bytes.windows(2).any(|w| w == b"d\xff"), "{bytes:?}");
assert!(bytes.ends_with(b"a\xffb"), "{bytes:?}");
}
#[test]
fn invalid_cwd_is_ignored() {
let td = TempDir::init().unwrap();
+7 -4
View File
@@ -181,7 +181,7 @@ impl Exec {
// set that on the terminal now. This allows rapidly initializing
// new surfaces to use the proper pwd.
if let Some(cwd) = &self.subprocess.cwd
&& let Err(err) = term.set_pwd(cwd.as_bytes())
&& let Err(err) = term.set_pwd(&ghostty_os::osbytes::config_str_to_bytes(cwd))
{
log::warn!("error setting initial pwd err={err:?}");
}
@@ -907,7 +907,10 @@ impl Subprocess {
// Add the environment variables that override any others.
for (k, v) in cfg.env_override.iter() {
env.insert(k, v);
env.insert(
ghostty_os::osbytes::config_str_to_os(k),
ghostty_os::osbytes::config_str_to_os(v),
);
}
// Build our args list
@@ -925,7 +928,7 @@ impl Subprocess {
// https://github.com/kovidgoyal/kitty/issues/1595
// https://github.com/ghostty-org/ghostty/discussions/7769
if let Some(pwd) = &cwd {
env.insert("PWD", pwd.clone());
env.insert("PWD", ghostty_os::osbytes::config_str_to_os(pwd));
}
Ok(Subprocess {
@@ -1037,7 +1040,7 @@ impl Subprocess {
}
}
Some(proposed) => {
if std::path::Path::new(proposed).exists() {
if std::path::Path::new(&ghostty_os::osbytes::config_str_to_os(proposed)).exists() {
Some(proposed.clone())
} else {
log::warn!("cannot access cwd, ignoring: {proposed}");
+6 -2
View File
@@ -86,7 +86,7 @@ use crate::{
};
/// `build_config.version_string`.
pub const VERSION_STRING: &str = "1.3.1";
pub const VERSION_STRING: &str = ghostty_build_info::VERSION_STRING;
/// The long-lived state of the handler (everything but the terminal).
pub struct HandlerState {
@@ -1693,7 +1693,11 @@ mod tests {
fn xtversion() {
let mut h = Harness::new();
h.feed(b"\x1b[>q");
assert_eq!(b"\x1bP>|ghostty 1.3.1\x1b\\".as_slice(), h.written());
let want = format!(
"\x1bP>|ghostty {}\x1b\\",
ghostty_build_info::VERSION_STRING
);
assert_eq!(want.as_bytes(), h.written());
}
#[test]
+1 -1
View File
@@ -173,7 +173,7 @@ impl ThreadEnterState {
let mut out = Vec::with_capacity(self.input.len());
for item in &self.input {
match item {
ReadableIO::Raw(v) => out.push(v.clone().into_bytes()),
ReadableIO::Raw(v) => out.push(ghostty_os::osbytes::config_str_to_bytes(v)),
ReadableIO::Path(path) => {
let f = std::fs::File::open(path).map_err(|err| {
log::warn!("failed to open input file={path} err={err}");