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
13 KiB
macOS app (Rust/AppKit port of macos/Sources): design
Crate crates/ghostty-macos (lib ghostty_macos + bin ghostty-macos-app). Written by MA1 (foundation); the
feature areas are ported by MA2..MA5 on top of it (split at the end). Nothing here has ever run on a Mac: it is
compile-checked with scripts/macos-crosscheck.sh (just check-macos, both Apple targets, -D warnings).
Principles
- Same API surface as the Swift app. The Swift app is a client of
libghosttythroughghostty.h. This app calls the same entry points in-process:ghostty-libis a normal dependency (renamedlibghostty) andghostty::capire-exports itsextern "C"functions (ghostty_app_new,ghostty_surface_key, ...),#[repr(C)]types and the header's enumerators (capi::consts, generated fromghostty.hbyscripts/gen-macos-capi-consts.py). The runtime config (wakeup,action,read/confirm_read/write_clipboard,close_surfacecallbacks, userdata) is built exactly likeGhostty.App.init. libghostty is not embedded or linked as a separate library: no XCFramework needed. - Programmatic AppKit. No
ibtool/Xcode on Linux, so xibs/SwiftUI become code (below). The crate compiles to an empty stub off Apple; platform independent logic is not gated, so it is unit-tested on Linux. - Faithful port. Swift logic is ported 1:1 (same names in snake_case, doc comments kept). Deviations:
DEVIANT_FIXES.md("Other deliberate deviations") andmacos-app-MA1.md.
Module tree (mirrors Sources/)
| Swift | Rust (crates/ghostty-macos/src) |
|---|---|
App/macOS/main.swift |
app/entry.rs (run(), called by main.rs) |
App/macOS/AppDelegate.swift, AppDelegate+Ghostty.swift |
app/app_delegate.rs, app/app_delegate_ghostty.rs |
App/macOS/MainMenu.xib |
app/menu_spec/ (generated tables + pure logic) and app/main_menu.rs (builds the NSMenus) |
Ghostty/Ghostty.App.swift |
ghostty/app.rs (App) |
Ghostty/Ghostty.Config*.swift, Ghostty.Command/Action/Error/Event/Shell, GhosttyPackage*.swift |
ghostty/{config,config_types,command,action,error,event,shell,package,notification}.rs |
Ghostty/Ghostty.Surface.swift, Surface View/* |
ghostty/{surface,surface_view,surface_configuration}.rs (+ MA2) |
Ghostty/Ghostty.Input.swift, NSEvent+Extension.swift |
ghostty/input/, ghostty/ns_event.rs |
Helpers/** |
helpers/** (extensions: helpers/extensions/<ns_type>.rs, one extension trait XExt per Swift extension) |
Features/<Area>/** |
features/<area>/** |
Platform independent modules (tested on Linux): ghostty::{capi, config, config_types, command, action, shell, input, notification, package, error, surface_configuration}, helpers::{keyboard_shortcut, color, notification_center, published, ...}, app::menu_spec, features::quick_terminal::{position,size,screen,space_behavior}.
Swift type -> Rust type
| Swift | Rust |
|---|---|
class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate |
define_class! AppDelegate (#[thread_kind = MainThreadOnly]); Swift stored properties live in the Ivars struct (RefCell/Cell); @IBAction/delegate methods are #[unsafe(method(sel:))]s with the same selectors |
@IBOutlet menuXxx |
menu_spec::Outlet enum + HashMap<Outlet, Retained<NSMenuItem>> filled by main_menu::install |
Ghostty.App (ObservableObject) |
Rc<ghostty::app::App>; @Published -> helpers::published::Published<T> |
Ghostty.Config (owns ghostty_config_t) |
ghostty::config::Config (Drop frees; shared as Rc<Config>) |
Ghostty.SurfaceView: NSView |
define_class! SurfaceView (ghostty/surface_view.rs; MA2) |
Ghostty.Surface |
ghostty::surface::Surface |
BaseTerminalController/TerminalController: NSWindowController |
define_class! classes in features/terminal (MA3) |
Weak<T> |
objc2::rc::Weak<T> (ObjC objects) / std::rc::Weak |
Swift enums with raw strings (init?(rawValue:)) |
Rust enums with from_raw_value(&str) |
OptionSet |
bitflags! |
SwiftUI.Color, KeyboardShortcut, KeyEquivalent, EventModifiers |
helpers::{color::Color, keyboard_shortcut::*} (same raw values, so menu key equivalents are identical) |
Duration |
std::time::Duration |
UserDefaults.ghostty |
helpers::extensions::user_defaults::ghostty() |
Logger (OSLog) |
log crate (ghostty_init installs the os_log logger like Zig's lib mode) |
Threading
Everything that touches AppKit/Rust-side state runs on the main thread (the Swift code is @MainActor/main queue):
objects are !Send (Rc, Retained<..> of main-thread classes), MainThreadMarkers come from helpers::main_thread().
The one cross-thread entry from libghostty is the wakeup callback: it only forwards the address of the App allocation to
DispatchQueue.main.async, where it is resolved through a main-thread registry of live apps and ghostty_app_tick runs
(ghostty/app.rs). UNUserNotificationCenter callbacks arrive on arbitrary queues: they only capture Send data and hop to
the main queue (main_async_local for closures with main-only captures; AppDelegate::shared(mtm) re-resolves the delegate).
DispatchQueue.main.async -> dispatch2::DispatchQueue::main().exec_async, asyncAfter -> exec_after, signal sources
(SIGUSR2 config reload) -> dispatch2::DispatchSource with _dispatch_source_type_signal.
Notifications, observers, Combine
- AppKit/Foundation notifications (
NSWindow.didBecomeKeyNotification, ...): block based observers (helpers::extensions::ns_notification::observe) returning an RAII token (dropping it =removeObserver). - Ghostty's own notifications (
Notification.Name.ghosttyConfigDidChange,Ghostty.Notification.ghosttyNewWindow, ...):helpers::notification_center::NotificationCenter(names/keys with the same strings inghostty::notification). Payloads stay typed:UserInfois a string-keyed map ofRc<dyn Any>, the sender (object) is aSender(identity + downcastable handle, e.g.Retained<SurfaceView>). Delivery is synchronous on the main thread, observer semantics follow Foundation (filter by sender, tokens, snapshot on post).Ghostty.Appposts these exactly where the Swift code does. - Combine
$property.sink/@Published/ObservableObject:Published<T>(get/set/update/sink,Subscription= cancellable). - KVO (
NSApplication.observe(\.effectiveAppearance)): the delegate registers itself viaaddObserver:forKeyPath:and implementsobserveValueForKeyPath:ofObject:change:context:. - Distributed notifications (icon change):
NSDistributedNotificationCenterdirectly.
SwiftUI views and xibs
- xib (
MainMenu.xib,About.xib,ConfigurationErrors.xib,ClipboardConfirmation.xib,QuickTerminal.xib,Terminal*.xib): reproduced in code. For the main menu the xib is parsed by a script (scripts/gen-macos-menu-spec.py->app/menu_spec/generated.rs: titles, key equivalents, modifier masks, selectors, targets, outlets, system menus), andmain_menu.rsinstantiates the objects; a unit test pins the structure. Window xibs are small (a window, style mask, content rect, connections): port them by hand asNSWindowController::initcode with the same style mask/frame/flags (read the XML;scripts/gen-macos-menu-spec.pyshows how to parse it). - SwiftUI views (
AboutView,ConfigurationErrorsView,ClipboardConfirmationView, update pill/popover,TerminalView, split views, command palette,SurfaceViewoverlays): rebuilt with AppKit views (NSStackView/Auto Layout,NSTextField,NSVisualEffectView,NSScrollView,NSPopover,NSHostingViewis not available).@State/@PublishedbecomePublished<T>subscriptions that update the views;EnvironmentObjectbecomes explicit references. Visual fidelity target: same layout, text, spacing and behaviour as far as AppKit allows; record gaps in the owner's notes. - Unportable SwiftUI-only helpers (no AppKit equivalent, not ported):
Backport(view modifiers),HostingWindow,NonDraggableHostingView,MetalView/MTKViewwrapper,View+Extension,Transferable+Extension/drag-and-drop viaTransferable(the AppKitNSDraggingSource/Destinationequivalent is implemented by MA2/MA3 where needed).
Menu bar
menu_spec holds MAIN_MENU (generated from the xib), Outlet, SYNCED_SHORTCUTS (the syncMenuShortcut(config, action:, menuItem:) list), MENU_IMAGES (SF Symbols), DOCK_MENU and MenuShortcutKey (the shortcut lookup key). AppDelegate ports
syncMenuShortcuts, performGhosttyBindingMenuKeyEquivalent, validateMenuItem, the dock menu and float-on-top handling.
Upstream quirk kept: the xib connects "Use Selection for Find" to a non-existent outlet menuSelectionForSearch and "Jump to
Selection" to none, so those two items never get a config shortcut (see the menu_spec module docs).
Bundle (scripts/macos-bundle.sh)
Builds ghostty-macos-app, creates Ghostty.app (executable ghostty), expands Ghostty-Info.plist plus the Xcode
INFOPLIST_KEY_* settings with PlistBuddy, copies Ghostty.sdef, builds AppIcon.icns from Assets.xcassets with
sips/iconutil and copies the alternate/custom icon PNGs and ResetZoom.pdf, installs shell-integration/themes/terminfo where
ghostty_os::resources_dir() looks on macOS (Contents/Resources/ghostty, Contents/Resources/terminfo/78/xterm-ghostty),
signs with Ghostty.entitlements (GhosttyDebug.entitlements with --debug). Runs for real only on macOS; elsewhere it is a dry
run that prints every step. NSMainNibFile is not set (menu built in code); the DockTilePlugin is a separate bundle (MA4,
--dock-tile-plugin); Sparkle.framework would be copied to Contents/Frameworks by MA5.
Assessments
- App Intents (
Features/App/Intents,Ghostty.Input.*: AppEnum): the AppIntents framework is Swift-only. Intents, entities and enums are Swift types conforming to generic Swift protocols (AppIntent,AppEntity,AppEnum,EntityQuery,@Parameterproperty wrappers); the system discovers them fromMetadata.appintentsthat the Swift compiler toolchain extracts (appintentsmetadataprocessor) from the module. There is no Objective-C surface and no runtime registration API, so a Rust binary cannot declare intents. Verdict: unportable from Rust (would need a Swift shim, i.e. not a Rust port); MA5 documents it, ports the non-framework logic that is useful elsewhere (e.g. theGhostty.Input.Action/MouseButton/...enums, already needed by AppleScript) and records the Shortcuts/Siri loss in DEVIANT_FIXES. - Sparkle (
Features/Update):Sparkle.frameworkis Objective-C; load it at runtime (NSBundle(path:).load()fromContents/Frameworks), look classes/protocols up withAnyClass::get/AnyProtocol::get, createSPUStandardUpdaterController-style objects withmsg_send!, and declare theSPUUserDriver/SPUUpdaterDelegateimplementations withobjc2::runtime::ClassBuilder+add_protocol(adefine_class!cannot name a protocol that is not linked at build time). Weak linking = the lookup returnsNonewhen the framework is absent.UpdateController's surface used byAppDelegateis already in place (start_updater,check_for_updates,is_installing, the two automatic-update flags). - AppleScript (
Features/AppleScript):Ghostty.sdef+OSAScriptingDefinitionin Info.plist (handled by the bundle script); Cocoa scripting needsNSObjectsubclasses whose KVC keys match the sdef (define_class!with property getters as methods named after the keys),NSScriptCommandsubclasses (performDefaultImplementation), and theNSApplicationscripting hooks (application:delegateHandlesKey:etc.) on the delegate.
Work split
| Agent | Area (Swift) | Notes |
|---|---|---|
| MA2 | Ghostty/Surface View/* (SurfaceView AppKit: rendering host, input, IME, mouse, drag source, scroll view, progress bar, grab handle, search overlay, inspector view), rest of Ghostty.Input.swift (KeyEvent, Mouse*, Mods, Key enum), Ghostty.Inspector, GhosttyPackage remaining pieces |
fills the TODO(MA2) in ghostty/surface_view.rs; the model fields App.swift needs already exist |
| MA3 | Features/Terminal/* (BaseTerminalController, TerminalController, TerminalView, windows + Styles/*.xib variants, restorable state, tab color), Features/Splits/* (SplitTree, split views), Helpers TabTitleEditor, TabGroupCloseCoordinator |
replaces the stubs in features/terminal/mod.rs (API already used by AppDelegate/Ghostty.App) |
| MA4 | Features/QuickTerminal, Command Palette, Settings (ConfigurationErrors window + SettingsView), About, ClipboardConfirmation, Global Keybinds (event tap, Helpers PermissionRequest), Secure Input (+overlay), Services (ServiceProvider), Custom App Icon (AppIcon, ColorizedGhosttyIcon, DockTilePlugin, UserDefaults+AppIcon) |
replace the surface stubs in each features/<area>/mod.rs; pure value types of quick terminal are already ported |
| MA5 | Features/AppleScript (sdef + scripting classes), Features/Update (Sparkle via the ObjC runtime, weak-linked), Features/App App Intents (assess: unportable, see above) |
Each owner: port tests of macos/Tests/* for the area (pure logic portable and tested on Linux, AppKit logic compile-checked),
write docs/rewrite/notes/macos-app-<MAn>.md, keep just check-macos green, list deviations.