Files
conjure/Sources/Conjure/ConjureApp.swift
T
2026-10-15 15:08:57 +02:00

161 lines
6.5 KiB
Swift

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: "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 = SettingsWindowController(shortcuts: shortcuts)
private var startupShortcutError: String?
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
settings.retryUninstallRecovery = { [uninstaller] in await uninstaller.recover() }
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()
if !shortcutErrors.isEmpty {
startupShortcutError = shortcutErrors.joined(separator: "\n")
if !isLoginLaunch { showSettings(nil) }
}
}
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows: Bool) -> Bool {
guard !isLoginLaunch else { return false }
launcher.present()
if startupShortcutError != nil || startupRecoveryMessage != nil {
showSettings(nil)
}
return false
}
@objc func showSettings(_ sender: Any?) {
settings.present(error: startupShortcutError)
startupShortcutError = nil
startupRecoveryMessage = nil
}
func applicationWillTerminate(_ notification: Notification) {
shortcuts.stop()
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
false
}
}