53b4f60da1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
124 lines
5.0 KiB
Swift
124 lines
5.0 KiB
Swift
import AppKit
|
|
import SwiftUI
|
|
|
|
@MainActor
|
|
final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
var statusItem: NSStatusItem?
|
|
private(set) var islandController: IslandWindowController?
|
|
|
|
func applicationWillTerminate(_ notification: Notification) {
|
|
HotKeyCenter.shared.unregisterAll()
|
|
}
|
|
|
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
// Ignore SIGPIPE — prevents crash when nb-hook closes socket before we write response
|
|
signal(SIGPIPE, SIG_IGN)
|
|
// Warm up Keychain cache on main thread BEFORE any poller or view touches it
|
|
_ = KeychainStore.shared
|
|
NSApp.setActivationPolicy(.accessory)
|
|
setupMenuBarItem()
|
|
setupIsland()
|
|
#if PHONE_LINK
|
|
CloudProbe.shared.startIfEnabled()
|
|
#endif
|
|
}
|
|
|
|
// MARK: - Menu bar
|
|
|
|
private func setupMenuBarItem() {
|
|
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
|
guard let button = statusItem?.button else { return }
|
|
button.image = NSImage(named: "MenuBarIcon") ?? NSImage(systemSymbolName: "circle.fill", accessibilityDescription: "Coucou")
|
|
button.image?.size = NSSize(width: 24, height: 18)
|
|
button.image?.accessibilityDescription = "Coucou"
|
|
button.image?.isTemplate = true
|
|
|
|
let menu = NSMenu()
|
|
menu.addItem(withTitle: "Open Coucou", action: #selector(openIsland), keyEquivalent: "")
|
|
menu.addItem(.separator())
|
|
menu.addItem(withTitle: "Settings…", action: #selector(openSettings), keyEquivalent: ",")
|
|
menu.addItem(.separator())
|
|
menu.addItem(withTitle: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
|
|
|
|
statusItem?.menu = menu
|
|
}
|
|
|
|
// MARK: - Actions
|
|
|
|
@objc private func openIsland() {
|
|
islandController?.expand(to: .overview)
|
|
}
|
|
|
|
private var settingsWindow: NSWindow?
|
|
|
|
@objc private func openSettingsFromNotification(_ notification: Notification) {
|
|
if let section = notification.object as? String {
|
|
UserDefaults.standard.set(section, forKey: "settingsSection")
|
|
}
|
|
openSettings()
|
|
}
|
|
|
|
@objc private func openSettings() {
|
|
// The island floats above every window; fold it away so it can't cover Settings.
|
|
if AppState.shared.mode == .expanded { islandController?.collapse() }
|
|
|
|
if let w = settingsWindow, w.isVisible {
|
|
placeBelowIsland(w)
|
|
w.makeKeyAndOrderFront(nil); NSApp.activate(ignoringOtherApps: true); return
|
|
}
|
|
let win = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 720, height: 560),
|
|
styleMask: [.titled, .closable, .miniaturizable, .resizable],
|
|
backing: .buffered, defer: false)
|
|
win.title = "Settings — Coucou"
|
|
let host = NSHostingView(rootView: SettingsView())
|
|
host.sizingOptions = [.minSize]
|
|
win.contentView = host
|
|
win.contentMinSize = NSSize(width: 640, height: 420)
|
|
win.isReleasedWhenClosed = false
|
|
placeBelowIsland(win)
|
|
settingsWindow = win
|
|
win.makeKeyAndOrderFront(nil)
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
}
|
|
|
|
/// Centres the window horizontally and keeps its title bar clear of the island panel
|
|
/// (320 pt tall at the top of the notch screen), shrinking it to fit if needed.
|
|
private func placeBelowIsland(_ win: NSWindow) {
|
|
let screen = IslandWindowController.notchScreen() ?? NSScreen.main ?? win.screen
|
|
guard let screen else { win.center(); return }
|
|
let visible = screen.visibleFrame
|
|
let islandBottom = screen.frame.maxY - 320 - 12 // island panel height + margin
|
|
let top = min(visible.maxY, islandBottom)
|
|
var frame = win.frame
|
|
frame.size.height = min(frame.height, max(top - visible.minY - 12, win.minSize.height))
|
|
frame.origin.x = visible.midX - frame.width / 2
|
|
frame.origin.y = max(visible.minY + 12, top - frame.height)
|
|
win.setFrame(frame, display: true)
|
|
}
|
|
|
|
// MARK: - Island setup
|
|
|
|
private func setupIsland() {
|
|
islandController = IslandWindowController()
|
|
islandController?.showWindow(nil)
|
|
islandController?.fsm.launch()
|
|
HookServer.shared.start()
|
|
N8nPoller.shared.start()
|
|
VercelPoller.shared.start()
|
|
ResendPoller.shared.start()
|
|
GithubPoller.shared.start()
|
|
StripePoller.shared.start()
|
|
CalcomPoller.shared.start()
|
|
NotionPoller.shared.start()
|
|
NotificationCenter.default.addObserver(self, selector: #selector(openSettingsFromNotification(_:)),
|
|
name: .openFullSettings, object: nil)
|
|
// After the greeting ends, fly Mochi back to the desktop if it was there at last quit
|
|
NotificationCenter.default.addObserver(forName: .greetComplete, object: nil, queue: .main) { _ in
|
|
DesktopMochiController.shared.launchFlyIfNeeded()
|
|
}
|
|
#if !APPSTORE
|
|
_ = MusicController.shared
|
|
#endif
|
|
}
|
|
}
|