Files
ghostty-rs/crates/ghostty-macos/src/helpers/extensions/ns_application.rs
T
ddidderr 4861015105 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
2026-10-12 19:25:44 +02:00

119 lines
4.0 KiB
Rust

//! `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);
}
}