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
@@ -14,6 +14,7 @@ ghostty-datastruct.workspace = true
|
||||
ghostty-font.workspace = true
|
||||
ghostty-inspector.workspace = true
|
||||
ghostty-input.workspace = true
|
||||
ghostty-build-info.workspace = true
|
||||
ghostty-os.workspace = true
|
||||
ghostty-renderer.workspace = true
|
||||
ghostty-terminal.workspace = true
|
||||
|
||||
@@ -116,7 +116,7 @@ pub struct GlobalState {
|
||||
static STATE: OnceLock<GlobalState> = OnceLock::new();
|
||||
|
||||
/// Options for [`init`].
|
||||
#[derive(Clone, Copy, Default)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct InitOptions {
|
||||
/// A CLI action is being executed: disable stderr logging by default
|
||||
/// since the action writes to stderr/stdout itself.
|
||||
@@ -126,6 +126,32 @@ pub struct InitOptions {
|
||||
/// `apprt.runtime.resourcesDir`, e.g. GTK's Flatpak host path lookup).
|
||||
/// `None` uses the generic `ghostty_os::resourcesdir::resources_dir`.
|
||||
pub resources_dir: Option<fn() -> std::io::Result<ResourcesDir>>,
|
||||
|
||||
/// Crash reporting metadata (version, build mode, runtime, font backend
|
||||
/// and renderer tags; Zig: `build_config`). `None` uses
|
||||
/// [`default_crash_options`].
|
||||
pub crash: Option<ghostty_crash::Options>,
|
||||
}
|
||||
|
||||
/// Crash metadata for this build when the caller supplies none: the
|
||||
/// version from the shared build info and the compile-time platform
|
||||
/// defaults (Metal + CoreText on Apple targets, fontconfig + OpenGL
|
||||
/// elsewhere).
|
||||
#[must_use]
|
||||
pub fn default_crash_options() -> ghostty_crash::Options {
|
||||
let mut o = ghostty_crash::Options {
|
||||
release: ghostty_build_info::VERSION_STRING.to_owned(),
|
||||
build_mode: ghostty_build_info::BUILD_MODE
|
||||
.trim_start_matches('.')
|
||||
.to_owned(),
|
||||
..ghostty_crash::Options::default()
|
||||
};
|
||||
if cfg!(target_vendor = "apple") {
|
||||
"none".clone_into(&mut o.app_runtime);
|
||||
"coretext".clone_into(&mut o.font_backend);
|
||||
"metal".clone_into(&mut o.renderer);
|
||||
}
|
||||
o
|
||||
}
|
||||
|
||||
/// The global state. If [`init`] was never called, a default state (no
|
||||
@@ -182,13 +208,17 @@ fn init_with(opts: InitOptions, default_logging: Logging) -> &'static GlobalStat
|
||||
let rlimits = ResourceLimits::init();
|
||||
|
||||
// Initialize our crash reporting.
|
||||
if let Err(err) = ghostty_crash::init(&ghostty_crash::Options {
|
||||
release: ghostty_termio::stream_handler::VERSION_STRING.to_owned(),
|
||||
..ghostty_crash::Options::default()
|
||||
}) {
|
||||
let crash_opts = opts.crash.clone().unwrap_or_else(default_crash_options);
|
||||
if let Err(err) = ghostty_crash::init(&crash_opts) {
|
||||
log::warn!("sentry init failed, no crash capture available err={err}");
|
||||
}
|
||||
|
||||
// We need to make sure the process locale is set properly. Locale
|
||||
// affects a lot of behaviors in a shell.
|
||||
if let Some(locale) = ghostty_os::ensure_locale() {
|
||||
log::debug!("locale set locale={locale}");
|
||||
}
|
||||
|
||||
// Find our resources directory once for the app so every launch
|
||||
// hereafter can use this cached value.
|
||||
let discover = opts
|
||||
@@ -352,6 +382,19 @@ mod tests {
|
||||
assert_eq!(log_scope("other::x"), "other");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crash_defaults_use_shared_version() {
|
||||
let o = default_crash_options();
|
||||
assert_eq!(o.release, ghostty_build_info::VERSION_STRING);
|
||||
if cfg!(target_vendor = "apple") {
|
||||
assert_eq!(o.renderer, "metal");
|
||||
assert_eq!(o.font_backend, "coretext");
|
||||
assert_eq!(o.app_runtime, "none");
|
||||
} else {
|
||||
assert_eq!(o.renderer, "opengl");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logging_parse() {
|
||||
assert!(Logging::parse("stderr").stderr);
|
||||
|
||||
@@ -424,6 +424,7 @@ pub fn write_screen_temp_file(
|
||||
loc: WriteScreenLoc,
|
||||
emit: WriteScreenFormat,
|
||||
) -> Result<String, CoreError> {
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::OpenOptionsExt as _;
|
||||
|
||||
// Create a temporary directory to store our scrollback.
|
||||
@@ -432,12 +433,11 @@ pub fn write_screen_temp_file(
|
||||
let file_path = tmp_dir.path().join(&filename);
|
||||
|
||||
let result = (|| -> Result<String, CoreError> {
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.mode(0o600)
|
||||
.open(&file_path)?;
|
||||
let mut opts = std::fs::OpenOptions::new();
|
||||
opts.write(true).create(true).truncate(true);
|
||||
#[cfg(unix)]
|
||||
opts.mode(0o600);
|
||||
let mut file = opts.open(&file_path)?;
|
||||
file.write_all(contents.as_bytes())?;
|
||||
file.flush()?;
|
||||
Ok(std::fs::canonicalize(&file_path)?
|
||||
@@ -1272,10 +1272,13 @@ mod tests {
|
||||
assert!(path.ends_with("screen.txt"));
|
||||
let read = std::fs::read_to_string(&path).unwrap();
|
||||
assert_eq!(read, s);
|
||||
let mode = std::os::unix::fs::PermissionsExt::mode(
|
||||
&std::fs::metadata(&path).unwrap().permissions(),
|
||||
);
|
||||
assert_eq!(mode & 0o777, 0o600);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let mode = std::os::unix::fs::PermissionsExt::mode(
|
||||
&std::fs::metadata(&path).unwrap().permissions(),
|
||||
);
|
||||
assert_eq!(mode & 0o777, 0o600);
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(std::path::Path::new(&path).parent().unwrap());
|
||||
}
|
||||
|
||||
|
||||
@@ -215,6 +215,12 @@ impl Surface {
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Surface::complete_clipboard_request`] for a paste of raw bytes
|
||||
/// (Zig passes `[]const u8`; e.g. dropped non-UTF-8 file names).
|
||||
pub fn complete_paste_bytes(&self, data: &[u8], confirmed: bool) -> Result<(), ClipboardError> {
|
||||
self.complete_clipboard_paste(data, confirmed)
|
||||
}
|
||||
|
||||
/// Paste text (also the target of `text_callback`).
|
||||
pub(crate) fn complete_clipboard_paste(
|
||||
&self,
|
||||
@@ -390,7 +396,13 @@ impl Surface {
|
||||
|
||||
/// `textCallback`: text input that is pasted verbatim.
|
||||
pub fn text_callback(&self, text: &str) -> Result<(), ClipboardError> {
|
||||
self.text_callback_bytes(text.as_bytes())
|
||||
}
|
||||
|
||||
/// [`Surface::text_callback`] for raw bytes (Zig takes `[]const u8`;
|
||||
/// the embedded `ghostty_surface_text` forwards the host bytes as is).
|
||||
pub fn text_callback_bytes(&self, text: &[u8]) -> Result<(), ClipboardError> {
|
||||
let _crash = self.crash_guard();
|
||||
self.complete_clipboard_paste(text.as_bytes(), true)
|
||||
self.complete_clipboard_paste(text, true)
|
||||
}
|
||||
}
|
||||
@@ -471,10 +471,15 @@ impl Surface {
|
||||
.and_then(|wd| wd.value())
|
||||
.map(str::to_owned);
|
||||
ecfg.resources_dir = global::state().resources_dir.host().map(str::to_owned);
|
||||
ecfg.rt_pre_exec_info = ghostty_termio::command::RtPreExecInfo::init(&config);
|
||||
ecfg.rt_post_fork_info = ghostty_termio::command::RtPostForkInfo::init(&config);
|
||||
ecfg.create_scope = rt_surface.create_scope_fn();
|
||||
ecfg.restore_rlimit = global::state().rlimits.nofile;
|
||||
// The Windows exec backend has no pre-exec/post-fork hooks,
|
||||
// cgroup scopes or rlimits (Zig: Linux/POSIX only).
|
||||
#[cfg(unix)]
|
||||
{
|
||||
ecfg.rt_pre_exec_info = ghostty_termio::command::RtPreExecInfo::init(&config);
|
||||
ecfg.rt_post_fork_info = ghostty_termio::command::RtPostForkInfo::init(&config);
|
||||
ecfg.create_scope = rt_surface.create_scope_fn();
|
||||
ecfg.restore_rlimit = global::state().rlimits.nofile;
|
||||
}
|
||||
|
||||
let exec = Exec::new(ecfg)?;
|
||||
let args = exec.args().to_vec();
|
||||
@@ -935,7 +940,9 @@ impl Surface {
|
||||
}
|
||||
|
||||
Message::PwdChange(w) => {
|
||||
let pwd = String::from_utf8_lossy(w.slice()).into_owned();
|
||||
// Keep the OSC 7 bytes (see `Pwd`): non-UTF-8 bytes are
|
||||
// carried with the reversible byte escape.
|
||||
let pwd = ghostty_os::osbytes::bytes_to_config_string(w.slice());
|
||||
self.perform(RtAction::Pwd(Pwd { pwd }));
|
||||
}
|
||||
|
||||
@@ -1364,7 +1371,14 @@ impl Surface {
|
||||
pub fn pwd(&self) -> Option<String> {
|
||||
let t = self.lock_terminal();
|
||||
let pwd = t.get_pwd()?;
|
||||
Some(String::from_utf8_lossy(pwd).into_owned())
|
||||
Some(ghostty_os::osbytes::bytes_to_config_string(pwd))
|
||||
}
|
||||
|
||||
/// The raw bytes of [`Surface::pwd`] (Zig returns the OSC 7 bytes as is).
|
||||
#[must_use]
|
||||
pub fn pwd_bytes(&self) -> Option<Vec<u8>> {
|
||||
let t = self.lock_terminal();
|
||||
t.get_pwd().map(<[u8]>::to_vec)
|
||||
}
|
||||
|
||||
/// Returns the x/y coordinate of where the IME (Input Method Editor)
|
||||
@@ -2055,6 +2069,10 @@ impl CoreSurface for Surface {
|
||||
Surface::complete_clipboard_request(self, req, data, confirmed)
|
||||
}
|
||||
|
||||
fn complete_paste_bytes(&self, data: &[u8], confirmed: bool) -> Result<(), ClipboardError> {
|
||||
Surface::complete_paste_bytes(self, data, confirmed)
|
||||
}
|
||||
|
||||
fn set_clipboard_read_allowed(&self, allow: bool) {
|
||||
use ghostty_config::types_b::ClipboardAccess;
|
||||
self.set_config_clipboard(
|
||||
|
||||
@@ -1664,7 +1664,7 @@ impl Surface {
|
||||
sel: link.selection,
|
||||
trim: false,
|
||||
});
|
||||
let pwd = t.get_pwd().map(|p| String::from_utf8_lossy(p).into_owned());
|
||||
let pwd = t.get_pwd().map(ghostty_os::osbytes::bytes_to_config_string);
|
||||
resolve_path_for_opening(pwd.as_deref(), &str).unwrap_or(str)
|
||||
}
|
||||
|
||||
|
||||
@@ -114,6 +114,24 @@ fn pwd_osc7() {
|
||||
assert_eq!(h.surface.pwd().as_deref(), Some("/tmp/a b"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pwd_osc7_keeps_non_utf8_bytes() {
|
||||
let h = harness!(Harness::recorder(
|
||||
r"\033]7;file://localhost/tmp/a%%FFb\033\\",
|
||||
&[]
|
||||
));
|
||||
let pwd = h.wait_action("pwd", |a| match a {
|
||||
Action::Pwd(p) if !p.pwd.is_empty() => Some(p.pwd.clone()),
|
||||
_ => None,
|
||||
});
|
||||
assert_eq!(
|
||||
ghostty_os::osbytes::config_str_to_bytes(&pwd),
|
||||
b"/tmp/a\xffb"
|
||||
);
|
||||
assert_eq!(h.surface.pwd_bytes().as_deref(), Some(&b"/tmp/a\xffb"[..]));
|
||||
assert_eq!(h.surface.pwd().as_deref(), Some(pwd.as_str()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bell_rings_once_per_burst() {
|
||||
let h = harness!(Harness::recorder(r"\007", &[]));
|
||||
|
||||
Reference in new issue
Block a user