import AppKit import ConjureKit @main @MainActor struct ConjureApp { static func main() { let application = NSApplication.shared let delegate = ApplicationDelegate() application.delegate = delegate application.setActivationPolicy(.accessory) application.mainMenu = makeMainMenu(delegate: delegate) // NSApplication holds its delegate weakly. withExtendedLifetime(delegate) { application.run() } } private static func makeMainMenu(delegate: ApplicationDelegate) -> NSMenu { let menu = NSMenu() let applicationItem = NSMenuItem(title: "Conjure", action: nil, keyEquivalent: "") let applicationMenu = NSMenu(title: "Conjure") applicationItem.submenu = applicationMenu menu.addItem(applicationItem) applicationMenu.addItem( withTitle: "About Conjure", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "" ) let settings = applicationMenu.addItem( withTitle: "Settings…", action: #selector(ApplicationDelegate.showSettings(_:)), keyEquivalent: "," ) settings.target = delegate applicationMenu.addItem(.separator()) applicationMenu.addItem( withTitle: "Hide Conjure", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h" ) let hideOthers = applicationMenu.addItem( withTitle: "Hide Others", action: #selector(NSApplication.hideOtherApplications(_:)), keyEquivalent: "h" ) hideOthers.keyEquivalentModifierMask = [.command, .option] applicationMenu.addItem( withTitle: "Show All", action: #selector(NSApplication.unhideAllApplications(_:)), keyEquivalent: "" ) applicationMenu.addItem(.separator()) applicationMenu.addItem( withTitle: "Quit Conjure", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q" ) let fileItem = NSMenuItem(title: "File", action: nil, keyEquivalent: "") let fileMenu = NSMenu(title: "File") fileItem.submenu = fileMenu menu.addItem(fileItem) fileMenu.addItem(withTitle: "Close Window", action: #selector(NSWindow.performClose(_:)), keyEquivalent: "w") let editItem = NSMenuItem(title: "Edit", action: nil, keyEquivalent: "") let editMenu = NSMenu(title: "Edit") editItem.submenu = editMenu menu.addItem(editItem) editMenu.addItem(withTitle: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x") editMenu.addItem(withTitle: "Undo", action: Selector(("undo:")), keyEquivalent: "z") let redo = editMenu.addItem(withTitle: "Redo", action: Selector(("redo:")), keyEquivalent: "z") redo.keyEquivalentModifierMask = [.command, .shift] editMenu.addItem(withTitle: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c") editMenu.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v") editMenu.addItem(.separator()) editMenu.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a") let windowItem = NSMenuItem(title: "Window", action: nil, keyEquivalent: "") let windowMenu = NSMenu(title: "Window") windowItem.submenu = windowMenu menu.addItem(windowItem) windowMenu.addItem(withTitle: "Minimize", action: #selector(NSWindow.performMiniaturize(_:)), keyEquivalent: "m") windowMenu.addItem(withTitle: "Zoom", action: #selector(NSWindow.performZoom(_:)), keyEquivalent: "") windowMenu.addItem(.separator()) windowMenu.addItem(withTitle: "Bring All to Front", action: #selector(NSApplication.arrangeInFront(_:)), keyEquivalent: "") NSApplication.shared.windowsMenu = windowMenu return menu } } @MainActor private final class ApplicationDelegate: NSObject, NSApplicationDelegate { private let uninstaller = ApplicationUninstaller() private lazy var launcher = LauncherWindowController(uninstaller: uninstaller) private lazy var shortcuts = GlobalShortcuts { [weak self] destination in switch destination { case .launcher: self?.launcher.toggle() case .command(let command): self?.launcher.toggle(command: command) } } private lazy var settings = makeSettings() private func makeSettings() -> SettingsWindowController { let controller = SettingsWindowController(shortcuts: shortcuts) controller.retryUninstallRecovery = { [uninstaller] in await uninstaller.recover() } return controller } private var startupError = ThemePalette.configurationError private var startupRecoveryMessage: String? private var isLoginLaunch: Bool { let event = NSAppleEventManager.shared().currentAppleEvent return event?.eventID == kAEOpenApplication && event?.paramDescriptor(forKeyword: keyAEPropData)?.enumCodeValue == keyAELaunchedAsLogInItem } func applicationDidFinishLaunching(_ notification: Notification) { let menu = NSMenu() let settingsItem = menu.addItem(withTitle: "Settings…", action: #selector(showSettings(_:)), keyEquivalent: ",") settingsItem.target = self menu.addItem(.separator()) let quitItem = menu.addItem(withTitle: "Quit Conjure", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q") quitItem.target = NSApplication.shared launcher.window?.contentView?.menu = menu let loginLaunch = isLoginLaunch Task { let report = await uninstaller.recover() guard !report.messages.isEmpty else { return } startupRecoveryMessage = report.messages.joined(separator: "\n") settings.setUninstallRecoveryReport(report) if !loginLaunch { showSettings(nil) } } if !isLoginLaunch { launcher.present() } let shortcutErrors = shortcuts.start() startupError = ([startupError].compactMap { $0 } + shortcutErrors).joined(separator: "\n") if startupError?.isEmpty == true { startupError = nil } if startupError != nil && !loginLaunch { showSettings(nil) } } func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows: Bool) -> Bool { guard !isLoginLaunch else { return false } launcher.present() if startupError != nil || startupRecoveryMessage != nil { showSettings(nil) } return false } @objc func showSettings(_ sender: Any?) { settings.present(error: startupError) startupError = nil startupRecoveryMessage = nil } func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { launcher.saveNotes() ? .terminateNow : .terminateCancel } func applicationWillTerminate(_ notification: Notification) { shortcuts.stop() } func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { false } }