feat(macos): foundation of the macOS app ported from Swift to Rust

Phase 9, MA1: crates/ghostty-macos (AppKit via objc2; empty stub on
non-Apple targets) ports the foundation of Ghostty 1.3.1's Swift macOS
app (ghostty/macos): main, AppDelegate (+Ghostty) with lifecycle, dock
menu, open-file, notifications, shortcut sync and quit/reopen paths;
MainMenu.xib as generated menu tables built into NSMenus; the
Ghostty.App/Config/Surface wrappers driving libghostty in-process via
ghostty-lib; the generic helpers; scripts/macos-bundle.sh to assemble
Ghostty.app (dry-run on Linux). Feature areas are stubbed for MA2-MA5.
Design and type mapping: docs/rewrite/notes/macos-app-design.md
(App Intents is a Swift-only framework and cannot be ported).

Compile-checked only (no macOS here): just check-macos passes for
aarch64/x86_64-apple-darwin with -D warnings; 125 platform-independent
tests run on Linux.

Test Plan:
- just check-macos
- cargo test -p ghostty-macos
This commit is contained in:
ddidderr committed 2026-10-12 19:25:44 +02:00
1 parent feacba2d69
commit 4861015105
112 files changed
+18438 -2

No files matched your search

@@ -0,0 +1,127 @@
//! `Helpers/Private/CGS.swift`: private CoreGraphics Spaces API.
// SAFETY: every `unsafe` block in this module carries its own justification (FFI / objc2 calls).
#![allow(unsafe_code)]
use objc2_core_foundation::{CFArray, CFNumber, CFRetained};
pub type CGSConnectionID = i32;
pub type CGSSpaceID = usize;
#[link(name = "CoreGraphics", kind = "framework")]
unsafe extern "C" {
fn CGSMainConnectionID() -> CGSConnectionID;
fn CGSGetActiveSpace(cid: CGSConnectionID) -> CGSSpaceID;
fn CGSSpaceGetType(cid: CGSConnectionID, space_id: CGSSpaceID) -> u32;
/// Returns a +1 retained CFArray (CFNumbers) or null.
fn CGSCopySpacesForWindows(
cid: CGSConnectionID,
mask: u32,
window_ids: &CFArray<CFNumber>,
) -> *const CFArray<CFNumber>;
}
/// <https://github.com/NUIKit/CGSInternal/blob/c4f6f559d624dc1cfc2bf24c8c19dbf653317fcf/CGSSpace.h#L40>
/// converted to Rust.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct CGSSpaceMask(pub u32);
impl CGSSpaceMask {
pub const INCLUDES_CURRENT: Self = Self(1 << 0);
pub const INCLUDES_OTHERS: Self = Self(1 << 1);
pub const INCLUDES_USER: Self = Self(1 << 2);
pub const INCLUDES_VISIBLE: Self = Self(1 << 16);
pub const CURRENT_SPACE: Self = Self(Self::INCLUDES_USER.0 | Self::INCLUDES_CURRENT.0);
pub const OTHER_SPACES: Self = Self(Self::INCLUDES_OTHERS.0 | Self::INCLUDES_CURRENT.0);
pub const ALL_SPACES: Self =
Self(Self::INCLUDES_USER.0 | Self::INCLUDES_OTHERS.0 | Self::INCLUDES_CURRENT.0);
pub const ALL_VISIBLE_SPACES: Self = Self(Self::INCLUDES_VISIBLE.0 | Self::ALL_SPACES.0);
}
/// `CGSSpaceType`.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CGSSpaceType {
User = 0,
System = 2,
Fullscreen = 4,
}
impl CGSSpaceType {
#[must_use]
pub fn from_raw(raw: u32) -> Option<Self> {
match raw {
0 => Some(Self::User),
2 => Some(Self::System),
4 => Some(Self::Fullscreen),
_ => None,
}
}
}
/// Represents a unique identifier for a macOS Space (Desktop, Fullscreen, etc).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct CGSSpace {
pub raw_value: CGSSpaceID,
}
impl std::fmt::Display for CGSSpace {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "SpaceID({})", self.raw_value)
}
}
impl CGSSpace {
/// Returns the currently active space.
#[must_use]
#[allow(unsafe_code)]
pub fn active() -> Self {
// SAFETY: private but stable CoreGraphics calls taking/returning integers.
let space = unsafe { CGSGetActiveSpace(CGSMainConnectionID()) };
Self { raw_value: space }
}
/// List the spaces for the given window.
#[must_use]
#[allow(unsafe_code)]
pub fn list(window_id: u32, mask: CGSSpaceMask) -> Vec<Self> {
let ids = CFArray::from_retained_objects(&[CFNumber::new_i64(i64::from(window_id))]);
// SAFETY: `ids` is a valid CFArray of CFNumbers for the call; the
// returned array (if non-null) is +1 retained and adopted below.
let spaces = unsafe { CGSCopySpacesForWindows(CGSMainConnectionID(), mask.0, &ids) };
let Some(spaces) = std::ptr::NonNull::new(spaces.cast_mut()) else {
return Vec::new();
};
// SAFETY: ownership of the +1 reference is transferred.
let spaces: CFRetained<CFArray<CFNumber>> = unsafe { CFRetained::from_raw(spaces) };
spaces
.iter()
.filter_map(|n| n.as_i64())
.map(|v| Self {
raw_value: v as CGSSpaceID,
})
.collect()
}
/// `type`.
#[must_use]
#[allow(unsafe_code)]
pub fn space_type(&self) -> Option<CGSSpaceType> {
// SAFETY: integer arguments only.
let raw = unsafe { CGSSpaceGetType(CGSMainConnectionID(), self.raw_value) };
CGSSpaceType::from_raw(raw)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn masks() {
assert_eq!(CGSSpaceMask::ALL_SPACES.0, 0b111);
assert_eq!(CGSSpaceMask::ALL_VISIBLE_SPACES.0, (1 << 16) | 0b111);
assert_eq!(CGSSpaceMask::CURRENT_SPACE.0, 0b101);
assert_eq!(CGSSpaceType::from_raw(4), Some(CGSSpaceType::Fullscreen));
}
}
@@ -0,0 +1,75 @@
//! `Helpers/Private/Dock.swift`: private APIs to get/set the Dock state.
// SAFETY: every `unsafe` block in this module carries its own justification (FFI / objc2 calls).
#![allow(unsafe_code)]
#[link(name = "ApplicationServices", kind = "framework")]
unsafe extern "C" {
// Private API to get Dock location
fn CoreDockGetOrientationAndPinning(out_orientation: *mut i32, out_pinning: *mut i32);
// Private API to get the current Dock auto-hide state
fn CoreDockGetAutoHideEnabled() -> bool;
// Toggles the Dock's auto-hide state
fn CoreDockSetAutoHideEnabled(flag: bool);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DockOrientation {
Top = 1,
Bottom = 2,
Left = 3,
Right = 4,
}
impl DockOrientation {
#[must_use]
pub fn from_raw(raw: i32) -> Option<Self> {
match raw {
1 => Some(Self::Top),
2 => Some(Self::Bottom),
3 => Some(Self::Left),
4 => Some(Self::Right),
_ => None,
}
}
}
pub struct Dock;
impl Dock {
/// Returns the orientation of the dock or None if it can't be determined.
#[must_use]
#[allow(unsafe_code)]
pub fn orientation() -> Option<DockOrientation> {
let mut orientation = 0i32;
let mut pinning = 0i32;
// SAFETY: both out-pointers are valid for writes for the call.
unsafe { CoreDockGetOrientationAndPinning(&raw mut orientation, &raw mut pinning) };
DockOrientation::from_raw(orientation)
}
/// The dock autohide.
#[must_use]
#[allow(unsafe_code)]
pub fn auto_hide_enabled() -> bool {
// SAFETY: no arguments; plain getter.
unsafe { CoreDockGetAutoHideEnabled() }
}
#[allow(unsafe_code)]
pub fn set_auto_hide_enabled(value: bool) {
// SAFETY: plain setter taking a bool.
unsafe { CoreDockSetAutoHideEnabled(value) }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn orientation_raw() {
assert_eq!(DockOrientation::from_raw(3), Some(DockOrientation::Left));
assert_eq!(DockOrientation::from_raw(0), None);
}
}
@@ -0,0 +1,4 @@
//! `Helpers/Private/{CGS,Dock}.swift`: private CoreGraphics / CoreDock APIs.
pub mod cgs;
pub mod dock;