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,145 @@
|
||||
//! `MainMenu.xib`, built programmatically from [`menu_spec`].
|
||||
//!
|
||||
//! `NSApplicationMain` loads the xib named by `NSMainNibFile` (`MainMenu`),
|
||||
//! which instantiates the `AppDelegate`, the menus, the `NSFontManager` object
|
||||
//! and wires the `@IBOutlet`s. The Rust app has no `ibtool`/nib, so
|
||||
//! [`install`] creates the same objects in code:
|
||||
//!
|
||||
//! * menus/items with the same titles, key equivalents, modifier masks and
|
||||
//! actions (selector names are registered with the runtime; items whose xib
|
||||
//! target is the First Responder proxy get a nil target, so they travel the
|
||||
//! responder chain exactly like in the nib; `AppDelegate` targets are set);
|
||||
//! * the system menus (`systemMenu` = apple/window/help) are registered with
|
||||
//! `NSApplication` (`setAppleMenu:`, `windowsMenu`, `helpMenu`);
|
||||
//! * the outlet items are returned for `AppDelegate` ([`Outlets`]).
|
||||
//!
|
||||
//! Edit-menu extras AppKit injects (Start Dictation, Emoji & Symbols) and the
|
||||
//! Window-menu tab items are added by AppKit on its own at runtime, as in the
|
||||
//! nib case.
|
||||
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::{collections::HashMap, ffi::CString};
|
||||
|
||||
use objc2::{
|
||||
MainThreadMarker,
|
||||
MainThreadOnly,
|
||||
msg_send,
|
||||
rc::Retained,
|
||||
runtime::{AnyObject, Sel},
|
||||
sel,
|
||||
};
|
||||
use objc2_app_kit::{NSApplication, NSEventModifierFlags, NSFontManager, NSMenu, NSMenuItem};
|
||||
use objc2_foundation::NSString;
|
||||
|
||||
use super::{
|
||||
app_delegate::AppDelegate,
|
||||
menu_spec::{ItemSpec, MenuSpec, Outlet, SystemMenu, Target},
|
||||
};
|
||||
|
||||
/// The items/menus the xib connects to `AppDelegate` outlets.
|
||||
#[derive(Default)]
|
||||
pub struct Outlets {
|
||||
pub items: HashMap<Outlet, Retained<NSMenuItem>>,
|
||||
/// `menuServices`, the submenu of the Services item.
|
||||
pub services_menu: Option<Retained<NSMenu>>,
|
||||
}
|
||||
|
||||
/// Build the main menu from the spec and register it (and the system menus)
|
||||
/// with `app`. Also instantiates the `NSFontManager` the xib contains.
|
||||
pub fn install(mtm: MainThreadMarker, app: &NSApplication, delegate: &AppDelegate) -> Outlets {
|
||||
// `<customObject customClass="NSFontManager"/>`: make sure the shared
|
||||
// font manager exists as the nib's instantiation would.
|
||||
let _ = NSFontManager::sharedFontManager(mtm);
|
||||
|
||||
let mut outlets = Outlets::default();
|
||||
let main = build_menu(
|
||||
mtm,
|
||||
app,
|
||||
delegate,
|
||||
&super::menu_spec::MAIN_MENU,
|
||||
&mut outlets,
|
||||
);
|
||||
app.setMainMenu(Some(&main));
|
||||
outlets
|
||||
}
|
||||
|
||||
fn build_menu(
|
||||
mtm: MainThreadMarker,
|
||||
app: &NSApplication,
|
||||
delegate: &AppDelegate,
|
||||
spec: &MenuSpec,
|
||||
outlets: &mut Outlets,
|
||||
) -> Retained<NSMenu> {
|
||||
let menu = NSMenu::initWithTitle(NSMenu::alloc(mtm), &NSString::from_str(spec.title));
|
||||
register_system_menu(app, spec.system_menu, &menu);
|
||||
if spec.outlet == Some(Outlet::MenuServices) {
|
||||
outlets.services_menu = Some(menu.clone());
|
||||
}
|
||||
|
||||
for entry in spec.items {
|
||||
match entry {
|
||||
ItemSpec::Separator => menu.addItem(&NSMenuItem::separatorItem(mtm)),
|
||||
ItemSpec::Item(item) => {
|
||||
let action: Option<Sel> = item.action.map(|a| selector(a.selector));
|
||||
// SAFETY: a valid title, selector (or none) and key equivalent.
|
||||
let ns_item = unsafe {
|
||||
NSMenuItem::initWithTitle_action_keyEquivalent(
|
||||
NSMenuItem::alloc(mtm),
|
||||
&NSString::from_str(item.title),
|
||||
action,
|
||||
&NSString::from_str(item.key_equivalent),
|
||||
)
|
||||
};
|
||||
if let Some(mods) = item.modifiers {
|
||||
ns_item.setKeyEquivalentModifierMask(NSEventModifierFlags(mods.0));
|
||||
}
|
||||
if let Some(a) = item.action {
|
||||
match a.target {
|
||||
// SAFETY: a nil target / the delegate (which outlives the menu).
|
||||
Target::FirstResponder => unsafe { ns_item.setTarget(None) },
|
||||
Target::AppDelegate => {
|
||||
let target: &AnyObject = delegate;
|
||||
// SAFETY: the delegate outlives the menu.
|
||||
unsafe { ns_item.setTarget(Some(target)) };
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(sub) = item.submenu {
|
||||
let submenu = build_menu(mtm, app, delegate, sub, outlets);
|
||||
ns_item.setSubmenu(Some(&submenu));
|
||||
}
|
||||
if let Some(o) = item.outlet {
|
||||
outlets.items.insert(o, ns_item.clone());
|
||||
}
|
||||
menu.addItem(&ns_item);
|
||||
}
|
||||
}
|
||||
}
|
||||
menu
|
||||
}
|
||||
|
||||
fn register_system_menu(app: &NSApplication, kind: Option<SystemMenu>, menu: &NSMenu) {
|
||||
match kind {
|
||||
Some(SystemMenu::Window) => app.setWindowsMenu(Some(menu)),
|
||||
Some(SystemMenu::Help) => app.setHelpMenu(Some(menu)),
|
||||
Some(SystemMenu::Apple) => {
|
||||
// `systemMenu="apple"`: Interface Builder calls the private
|
||||
// `-[NSApplication setAppleMenu:]`.
|
||||
let sel = sel!(setAppleMenu:);
|
||||
let responds: bool = unsafe { msg_send![app, respondsToSelector: sel] };
|
||||
if responds {
|
||||
// SAFETY: `setAppleMenu:` takes an NSMenu and returns void.
|
||||
let _: () = unsafe { msg_send![app, setAppleMenu: menu] };
|
||||
}
|
||||
}
|
||||
Some(SystemMenu::Main) | None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a selector by name (`#selector(name)`).
|
||||
#[must_use]
|
||||
pub fn selector(name: &str) -> Sel {
|
||||
let c = CString::new(name).expect("selector names contain no NUL");
|
||||
Sel::register(&c)
|
||||
}
|
||||
Reference in new issue
Block a user