feat(macos): port Ghostty's macOS code paths (Metal, CoreText, Darwin)
Ports the macOS-specific Zig code behind cfg(target_os = "macos"): - renderer: a GraphicsApi trait with generic::Renderer<G>; Metal backend (all renderer/metal/* files, shaders.metal verbatim, runtime library compile, IOSurface layer, CVDisplayLink), shadertoy -> MSL via naga; OpenGL remains the backend elsewhere. - font: CoreText face, shaper and discovery with build.rs backend selection (coretext default; coretext-freetype / coretext-harfbuzz / coretext-noshape features), no fontconfig/freetype/harfbuzz on Apple. - input: KeymapDarwin via UCKeyTranslate, option-as-alt, mac keycodes. - os: macos, mach, cf_release_thread, NSProcessInfo args and macOS branches; config: macOS defaults, keybinds, Application Support paths. - termio/core: login(1) wrapping, openpty/execvpe, Darwin child exit handling, os_log logging. Compile-checked for aarch64/x86_64-apple-darwin (clippy -D warnings clean) using the cross setup in docs/rewrite/notes/macos-M1.md; cannot run here. The embedded runtime/C API (embedded.zig) is the next step. Also includes the cross-platform integration edits shared by the crash, verification, Windows and macOS work (Cargo target-gating, thread crash-context hooks, apprt-gtk surface/IME refactor, termio/os cfg gates), a cfg fix so ghostty-config builds on wasm32, and an allow for the intentional recursion in the crash overflow test. Test Plan: - cargo test --workspace (3224 passed) - cargo clippy --workspace --all-targets -- -D warnings - cargo check --target aarch64-apple-darwin -p ghostty-renderer (with the documented cross env)
This commit is contained in:
1 parent
3c8fae1089
commit
452b782149
124 files changed
+14936
-1242
No files matched your search
@@ -16,7 +16,10 @@
|
||||
//! (try `setlocale(LC_ALL, "")`, then with `LANG` unset, then
|
||||
//! `en_US.UTF-8` and export `LANG=en_US.UTF-8`).
|
||||
//!
|
||||
//! The macOS-specific `LANG` population from Cocoa is not applicable.
|
||||
//! On Darwin (`target_vendor = "apple"`) the full Zig behaviour is
|
||||
//! implemented in [`darwin`] (it needs `unsafe` for `setlocale`/`setenv`):
|
||||
//! `LANG`/`LANGUAGE` are pre-populated from Cocoa when unset and
|
||||
//! `setlocale(LC_ALL, "")` is called with the Zig fallback chain.
|
||||
|
||||
/// The locale Ghostty falls back to (and exports as `LANG`) when the
|
||||
/// environment's locale can't be set.
|
||||
@@ -27,15 +30,179 @@ pub const FALLBACK_LOCALE: &str = "en_US.UTF-8";
|
||||
/// DEFERRED (see module docs): this is currently a no-op that only logs.
|
||||
/// Returns `Some(value)` when `LANG` should be set to `value` for child
|
||||
/// processes; always `None` for now.
|
||||
#[cfg(not(target_vendor = "apple"))]
|
||||
pub fn ensure_locale() -> Option<String> {
|
||||
log::debug!("ensure_locale: setlocale is not available from safe Rust; relying on the toolkit");
|
||||
None
|
||||
}
|
||||
|
||||
/// Ensure that the locale is set (Darwin: full port of `ensureLocale`).
|
||||
/// Returns `Some("en_US.UTF-8")` if we had to fall back to it (and exported
|
||||
/// it as `LANG`), otherwise `None`.
|
||||
#[cfg(target_vendor = "apple")]
|
||||
pub fn ensure_locale() -> Option<String> {
|
||||
darwin::ensure_locale()
|
||||
}
|
||||
|
||||
/// Format the system locale as `<lang>_<country>.UTF-8` (the value Zig's
|
||||
/// `setLangFromCocoa` exports as `LANG`).
|
||||
pub fn format_lang(lang: &str, country: &str) -> String {
|
||||
format!("{lang}_{country}.UTF-8")
|
||||
}
|
||||
|
||||
/// Build the `LANGUAGE` value from Apple's preferred languages
|
||||
/// (`preferredLanguageFromCocoa`): each BCP-47 tag is canonicalized to the
|
||||
/// POSIX format, gets `.UTF-8` appended and they are joined with `:`.
|
||||
/// Returns `None` if there are no preferred languages.
|
||||
///
|
||||
/// gettext uses `LANGUAGE` to override only translations, with a priority
|
||||
/// order given by colon-separated entries (e.g. `en:fr`).
|
||||
pub fn preferred_language_value<S: AsRef<str>>(preferred: &[S]) -> Option<String> {
|
||||
let mut out = String::new();
|
||||
for lang in preferred {
|
||||
if !out.is_empty() {
|
||||
out.push(':');
|
||||
}
|
||||
out.push_str(&crate::i18n::canonicalize_locale(lang.as_ref()));
|
||||
// The canonicalized locale never contains the encoding and all of
|
||||
// our translations require UTF-8 so we add that.
|
||||
out.push_str(".UTF-8");
|
||||
}
|
||||
(!out.is_empty()).then_some(out)
|
||||
}
|
||||
|
||||
#[cfg(target_vendor = "apple")]
|
||||
#[allow(unsafe_code)]
|
||||
mod darwin {
|
||||
use std::ffi::{CStr, CString};
|
||||
|
||||
use super::{FALLBACK_LOCALE, format_lang, preferred_language_value};
|
||||
|
||||
fn setlocale_all(locale: &str) -> Option<String> {
|
||||
let c = CString::new(locale).ok()?;
|
||||
// SAFETY: valid NUL-terminated string; setlocale returns a pointer to
|
||||
// a static string (or null) which we copy immediately.
|
||||
unsafe {
|
||||
let r = libc::setlocale(libc::LC_ALL, c.as_ptr());
|
||||
(!r.is_null()).then(|| CStr::from_ptr(r).to_string_lossy().into_owned())
|
||||
}
|
||||
}
|
||||
|
||||
fn set_env(key: &str, value: &str) {
|
||||
// SAFETY: called during early process startup (locale setup) before
|
||||
// other threads read the environment, mirroring the Zig code.
|
||||
unsafe { std::env::set_var(key, value) };
|
||||
}
|
||||
|
||||
fn unset_env(key: &str) {
|
||||
// SAFETY: see `set_env`.
|
||||
unsafe { std::env::remove_var(key) };
|
||||
}
|
||||
|
||||
/// Sets LANG (and LANGUAGE) from the system preferences.
|
||||
fn set_lang_from_cocoa() {
|
||||
use objc2_foundation::NSLocale;
|
||||
|
||||
let locale = NSLocale::currentLocale();
|
||||
let lang = locale.languageCode().to_string();
|
||||
// `countryCode` is deprecated by Apple but is what the Zig code uses.
|
||||
#[allow(deprecated)]
|
||||
let country = locale.countryCode();
|
||||
let Some(country) = country.map(|c| c.to_string()) else {
|
||||
log::warn!("languageCode or countryCode not found. Locale may be incorrect.");
|
||||
return;
|
||||
};
|
||||
|
||||
let env_value = format_lang(&lang, &country);
|
||||
log::info!("detected system locale={env_value}");
|
||||
set_env("LANG", &env_value);
|
||||
|
||||
// Get our preferred languages and set that to the LANGUAGE env var
|
||||
// in case our language differs from our locale.
|
||||
let preferred: Vec<String> = NSLocale::preferredLanguages()
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
if let Some(pref) = preferred_language_value(&preferred) {
|
||||
log::debug!("setting LANGUAGE from preferred languages value={pref}");
|
||||
set_env("LANGUAGE", &pref);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn ensure_locale() -> Option<String> {
|
||||
// On macOS, pre-populate the LANG env var with system preferences.
|
||||
// When launching the .app, LANG is not set so we must query it from
|
||||
// the OS. When launching from the CLI, LANG is usually set by the
|
||||
// parent process.
|
||||
let lang = std::env::var_os("LANG");
|
||||
if lang.as_ref().is_none_or(|v| v.is_empty()) {
|
||||
set_lang_from_cocoa();
|
||||
}
|
||||
|
||||
// Set the locale to whatever is set in env vars.
|
||||
if let Some(v) = setlocale_all("") {
|
||||
log::info!("setlocale from env result={v}");
|
||||
return None;
|
||||
}
|
||||
|
||||
// setlocale failed. This is probably because the LANG env var is
|
||||
// invalid. Try to set it without the LANG var set to use the system
|
||||
// default.
|
||||
if std::env::var_os("LANG").is_some_and(|v| !v.is_empty()) {
|
||||
// We don't need to do both of these things but we do them both
|
||||
// to be sure that lang is either empty or unset completely.
|
||||
set_env("LANG", "");
|
||||
unset_env("LANG");
|
||||
|
||||
if let Some(v) = setlocale_all("") {
|
||||
log::info!("setlocale after unset lang result={v}");
|
||||
|
||||
// If we try to setlocale to an unsupported locale it'll
|
||||
// return "C" as the POSIX/C fallback, if that's the case we
|
||||
// want to not use it and move to our fallback.
|
||||
if v != "C" {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Failure again... fallback to en_US.UTF-8
|
||||
log::warn!(
|
||||
"setlocale failed with LANG and system default. Falling back to {FALLBACK_LOCALE}"
|
||||
);
|
||||
if let Some(v) = setlocale_all(FALLBACK_LOCALE) {
|
||||
set_env("LANG", FALLBACK_LOCALE);
|
||||
log::info!("setlocale default result={v}");
|
||||
return Some(FALLBACK_LOCALE.to_owned());
|
||||
}
|
||||
log::warn!("setlocale failed even with the fallback, uncertain results");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn format_lang_value() {
|
||||
assert_eq!(format_lang("en", "US"), "en_US.UTF-8");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preferred_languages_to_language_var() {
|
||||
assert_eq!(preferred_language_value::<&str>(&[]), None);
|
||||
assert_eq!(
|
||||
preferred_language_value(&["en-US"]).as_deref(),
|
||||
Some("en_US.UTF-8")
|
||||
);
|
||||
assert_eq!(
|
||||
preferred_language_value(&["zh-Hans-CN", "en", "fr-CA"]).as_deref(),
|
||||
Some("zh_CN.UTF-8:en.UTF-8:fr_CA.UTF-8")
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(target_vendor = "apple"))]
|
||||
#[test]
|
||||
fn ensure_locale_is_noop() {
|
||||
assert_eq!(ensure_locale(), None);
|
||||
|
||||
Reference in new issue
Block a user