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
@@ -0,0 +1,191 @@
|
||||
//! macOS-specific helpers. Port of `os/macos.zig`.
|
||||
//!
|
||||
//! The platform-independent items ([`QosClass`], [`join_dir`]) compile
|
||||
//! everywhere so they can be unit tested on any host; everything that talks
|
||||
//! to Foundation / libpthread is gated on `target_vendor = "apple"`.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Quality-of-service classes (`pthread_set_qos_class_self_np`).
|
||||
///
|
||||
/// <https://developer.apple.com/library/archive/documentation/Performance/Conceptual/power_efficiency_guidelines_osx/PrioritizeWorkAtTheTaskLevel.html>
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u32)]
|
||||
pub enum QosClass {
|
||||
UserInteractive = 0x21,
|
||||
UserInitiated = 0x19,
|
||||
Default = 0x15,
|
||||
Utility = 0x11,
|
||||
Background = 0x09,
|
||||
Unspecified = 0x00,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
||||
pub enum SetQosClassError {
|
||||
/// The thread can't have its QoS class changed usually because a
|
||||
/// different pthread API was called that makes it an invalid target.
|
||||
#[error("thread incompatible")]
|
||||
ThreadIncompatible,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
||||
pub enum DirError {
|
||||
#[error("apple api failed")]
|
||||
AppleApiFailed,
|
||||
}
|
||||
|
||||
/// `NSSearchPathDirectory` values we use.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u64)]
|
||||
pub enum SearchPathDirectory {
|
||||
CachesDirectory = 13,
|
||||
ApplicationSupportDirectory = 14,
|
||||
}
|
||||
|
||||
/// Join `base_dir` with the given sub paths like `std.fs.path.join`
|
||||
/// (`commonDir`'s tail): empty components are skipped.
|
||||
pub fn join_dir(base_dir: &str, sub_paths: &[&str]) -> PathBuf {
|
||||
let mut parts = vec![base_dir];
|
||||
parts.extend_from_slice(sub_paths);
|
||||
PathBuf::from(crate::path_join(&parts))
|
||||
}
|
||||
|
||||
/// Return the path to the application support directory for Ghostty
|
||||
/// with the given sub path joined.
|
||||
#[cfg(target_vendor = "apple")]
|
||||
pub fn app_support_dir(sub_path: &str) -> Result<PathBuf, DirError> {
|
||||
common_dir(
|
||||
SearchPathDirectory::ApplicationSupportDirectory,
|
||||
&[crate::i18n::BUNDLE_ID, sub_path],
|
||||
)
|
||||
}
|
||||
|
||||
/// Return the path to the system cache directory with the given sub path
|
||||
/// joined.
|
||||
#[cfg(target_vendor = "apple")]
|
||||
pub fn cache_dir(sub_path: &str) -> Result<PathBuf, DirError> {
|
||||
common_dir(
|
||||
SearchPathDirectory::CachesDirectory,
|
||||
&[crate::i18n::BUNDLE_ID, sub_path],
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(target_vendor = "apple")]
|
||||
fn common_dir(directory: SearchPathDirectory, sub_paths: &[&str]) -> Result<PathBuf, DirError> {
|
||||
use objc2_foundation::{NSFileManager, NSSearchPathDirectory, NSSearchPathDomainMask};
|
||||
|
||||
let manager = NSFileManager::defaultManager();
|
||||
let url = manager
|
||||
.URLForDirectory_inDomain_appropriateForURL_create_error(
|
||||
NSSearchPathDirectory(directory as usize),
|
||||
NSSearchPathDomainMask::UserDomainMask,
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.map_err(|_| DirError::AppleApiFailed)?;
|
||||
let path = url.path().ok_or(DirError::AppleApiFailed)?;
|
||||
Ok(join_dir(&path.to_string(), sub_paths))
|
||||
}
|
||||
|
||||
/// Verifies that the running macOS system version is at least the given
|
||||
/// version.
|
||||
#[cfg(target_vendor = "apple")]
|
||||
pub fn is_at_least_version(major: isize, minor: isize, patch: isize) -> bool {
|
||||
use objc2_foundation::{NSOperatingSystemVersion, NSProcessInfo};
|
||||
|
||||
NSProcessInfo::processInfo().isOperatingSystemAtLeastVersion(NSOperatingSystemVersion {
|
||||
majorVersion: major,
|
||||
minorVersion: minor,
|
||||
patchVersion: patch,
|
||||
})
|
||||
}
|
||||
|
||||
/// Set the QoS class of the running thread.
|
||||
///
|
||||
/// <https://developer.apple.com/documentation/apple-silicon/tuning-your-code-s-performance-for-apple-silicon?preferredLanguage=occ>
|
||||
#[cfg(target_vendor = "apple")]
|
||||
#[allow(unsafe_code)]
|
||||
pub fn set_qos_class(class: QosClass) -> Result<(), SetQosClassError> {
|
||||
// SAFETY: plain libpthread call with a valid QoS value and no pointers.
|
||||
let rc = unsafe { libc::pthread_set_qos_class_self_np(class.to_libc(), 0) };
|
||||
match rc {
|
||||
0 => Ok(()),
|
||||
libc::EPERM => Err(SetQosClassError::ThreadIncompatible),
|
||||
// EPERM is the only known error that can happen based on the man
|
||||
// pages for pthread_set_qos_class_self_np.
|
||||
e => panic!("unexpected pthread_set_qos_class_self_np error: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_vendor = "apple")]
|
||||
impl QosClass {
|
||||
fn to_libc(self) -> libc::qos_class_t {
|
||||
match self {
|
||||
QosClass::UserInteractive => libc::qos_class_t::QOS_CLASS_USER_INTERACTIVE,
|
||||
QosClass::UserInitiated => libc::qos_class_t::QOS_CLASS_USER_INITIATED,
|
||||
QosClass::Default => libc::qos_class_t::QOS_CLASS_DEFAULT,
|
||||
QosClass::Utility => libc::qos_class_t::QOS_CLASS_UTILITY,
|
||||
QosClass::Background => libc::qos_class_t::QOS_CLASS_BACKGROUND,
|
||||
QosClass::Unspecified => libc::qos_class_t::QOS_CLASS_UNSPECIFIED,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Name the *current* thread (`pthread_setname_np` on Darwin can only name
|
||||
/// the calling thread).
|
||||
#[cfg(target_vendor = "apple")]
|
||||
#[allow(unsafe_code)]
|
||||
pub fn pthread_setname_np(name: &std::ffi::CStr) {
|
||||
// SAFETY: `name` is a valid NUL-terminated string for the duration of
|
||||
// the call; Darwin copies it.
|
||||
unsafe {
|
||||
libc::pthread_setname_np(name.as_ptr());
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience for non-Darwin callers/tests: is `p` below a `Caches`
|
||||
/// directory.
|
||||
pub fn is_cache_path(p: &Path) -> bool {
|
||||
p.components().any(|c| c.as_os_str() == "Caches")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn qos_class_values() {
|
||||
assert_eq!(QosClass::UserInteractive as u32, 0x21);
|
||||
assert_eq!(QosClass::UserInitiated as u32, 0x19);
|
||||
assert_eq!(QosClass::Default as u32, 0x15);
|
||||
assert_eq!(QosClass::Utility as u32, 0x11);
|
||||
assert_eq!(QosClass::Background as u32, 0x09);
|
||||
assert_eq!(QosClass::Unspecified as u32, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_path_values() {
|
||||
assert_eq!(SearchPathDirectory::CachesDirectory as u64, 13);
|
||||
assert_eq!(SearchPathDirectory::ApplicationSupportDirectory as u64, 14);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_dir_paths() {
|
||||
let id = crate::i18n::BUNDLE_ID;
|
||||
// Test base path.
|
||||
let p = join_dir("/Users/x/Library/Caches", &[id, ""]);
|
||||
assert!(is_cache_path(&p));
|
||||
assert!(p.to_string_lossy().contains(id));
|
||||
// Test with subdir.
|
||||
let p = join_dir("/Users/x/Library/Caches", &[id, "test"]);
|
||||
assert!(is_cache_path(&p));
|
||||
assert!(p.to_string_lossy().contains(&format!("{id}/test")));
|
||||
}
|
||||
|
||||
#[cfg(target_vendor = "apple")]
|
||||
#[test]
|
||||
fn cache_dir_real() {
|
||||
let p = cache_dir("test").unwrap();
|
||||
assert!(is_cache_path(&p));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user