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:
1 parent
feacba2d69
commit
4861015105
112 files changed
+18438
-2
No files matched your search
@@ -0,0 +1,118 @@
|
||||
//! `Helpers/Extensions/NSApplication+Extension.swift`.
|
||||
|
||||
use std::{cell::RefCell, collections::HashMap};
|
||||
|
||||
use objc2_app_kit::{
|
||||
NSApplication,
|
||||
NSApplicationPresentationOptions,
|
||||
NSRunningApplication,
|
||||
NSWorkspace,
|
||||
};
|
||||
use objc2_foundation::NSBundle;
|
||||
|
||||
thread_local! {
|
||||
/// `NSApplication.presentationOptionCounts`, keyed by the option's raw value.
|
||||
static PRESENTATION_OPTION_COUNTS: RefCell<HashMap<usize, u64>> = RefCell::new(HashMap::new());
|
||||
}
|
||||
|
||||
/// Pure ref counting behind `acquire/releasePresentationOption`.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct OptionCounts(HashMap<usize, u64>);
|
||||
|
||||
/// What the caller must do to the app's presentation options.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum OptionChange {
|
||||
Insert,
|
||||
Remove,
|
||||
Nothing,
|
||||
}
|
||||
|
||||
impl OptionCounts {
|
||||
/// `acquirePresentationOption`: always (re)inserts the option.
|
||||
pub fn acquire(&mut self, option: usize) -> OptionChange {
|
||||
*self.0.entry(option).or_insert(0) += 1;
|
||||
OptionChange::Insert
|
||||
}
|
||||
|
||||
/// `releasePresentationOption`: removes the option when the last
|
||||
/// reference is released.
|
||||
pub fn release(&mut self, option: usize) -> OptionChange {
|
||||
let Some(&value) = self.0.get(&option) else {
|
||||
return OptionChange::Nothing;
|
||||
};
|
||||
if value == 0 {
|
||||
return OptionChange::Nothing;
|
||||
}
|
||||
if value == 1 {
|
||||
self.0.remove(&option);
|
||||
OptionChange::Remove
|
||||
} else {
|
||||
self.0.insert(option, value - 1);
|
||||
OptionChange::Nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait NSApplicationExt {
|
||||
/// Add a presentation option to the application and maintain a reference
|
||||
/// count so that an equal number of releases is required to disable it.
|
||||
/// This is useful so that multiple classes can affect global app state
|
||||
/// without overriding others.
|
||||
fn acquire_presentation_option(&self, option: NSApplicationPresentationOptions);
|
||||
|
||||
/// See `acquire_presentation_option`.
|
||||
fn release_presentation_option(&self, option: NSApplicationPresentationOptions);
|
||||
|
||||
/// True if the application is frontmost. This isn't exactly the same as
|
||||
/// isActive because an app can be active but not be frontmost if the
|
||||
/// window with activity is an NSPanel.
|
||||
fn is_frontmost(&self) -> bool;
|
||||
}
|
||||
|
||||
impl NSApplicationExt for NSApplication {
|
||||
fn acquire_presentation_option(&self, option: NSApplicationPresentationOptions) {
|
||||
PRESENTATION_OPTION_COUNTS.with(|c| {
|
||||
let mut counts = OptionCounts(std::mem::take(&mut *c.borrow_mut()));
|
||||
let change = counts.acquire(option.0);
|
||||
*c.borrow_mut() = counts.0;
|
||||
debug_assert_eq!(change, OptionChange::Insert);
|
||||
});
|
||||
self.setPresentationOptions(self.presentationOptions() | option);
|
||||
}
|
||||
|
||||
fn release_presentation_option(&self, option: NSApplicationPresentationOptions) {
|
||||
let change = PRESENTATION_OPTION_COUNTS.with(|c| {
|
||||
let mut counts = OptionCounts(std::mem::take(&mut *c.borrow_mut()));
|
||||
let change = counts.release(option.0);
|
||||
*c.borrow_mut() = counts.0;
|
||||
change
|
||||
});
|
||||
if change == OptionChange::Remove {
|
||||
self.setPresentationOptions(self.presentationOptions() - option);
|
||||
}
|
||||
}
|
||||
|
||||
fn is_frontmost(&self) -> bool {
|
||||
let front: Option<_> = NSWorkspace::sharedWorkspace().frontmostApplication();
|
||||
let front_id =
|
||||
front.and_then(|a: objc2::rc::Retained<NSRunningApplication>| a.bundleIdentifier());
|
||||
let main_id = NSBundle::mainBundle().bundleIdentifier();
|
||||
front_id.map(|s| s.to_string()) == main_id.map(|s| s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ref_counts() {
|
||||
let mut c = OptionCounts::default();
|
||||
assert_eq!(c.release(1), OptionChange::Nothing);
|
||||
assert_eq!(c.acquire(1), OptionChange::Insert);
|
||||
assert_eq!(c.acquire(1), OptionChange::Insert);
|
||||
assert_eq!(c.release(1), OptionChange::Nothing);
|
||||
assert_eq!(c.release(1), OptionChange::Remove);
|
||||
assert_eq!(c.release(1), OptionChange::Nothing);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user