diff --git a/Sources/Prompt/App.swift b/Sources/Prompt/App.swift index 389b133..415a934 100644 --- a/Sources/Prompt/App.swift +++ b/Sources/Prompt/App.swift @@ -47,6 +47,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { HotkeyManager.shared.start() setupStatusItem() + setupMainMenu() // Show the panel on launch so the app is discoverable (it has no Dock // icon and the menu bar icon can be hard to spot on a full bar). @@ -56,6 +57,52 @@ final class AppDelegate: NSObject, NSApplicationDelegate { } } + /// A launched-again app must reveal itself. Without this, `open` (or + /// clicking it in Finder/Spotlight) hits the running instance, which does + /// nothing, so the app looks like it never started. + @MainActor + func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { + PanelController.shared.show() + return true + } + + @MainActor + func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { + // Stop the poll timer, any queued push, the event tap and the Carbon + // hotkeys so termination cannot be held up by a live callback. + SyncService.shared.stop() + HotkeyManager.shared.stop() + return .terminateNow + } + + /// Accessory apps have no visible menu bar, but the main menu still + /// services key equivalents — this is what makes Cmd+Q and Cmd+W work + /// while the panel has focus. + private func setupMainMenu() { + let mainMenu = NSMenu() + let appMenuItem = NSMenuItem() + mainMenu.addItem(appMenuItem) + + let appMenu = NSMenu() + appMenuItem.submenu = appMenu + + let hide = NSMenuItem(title: "Hide Panel", action: #selector(hidePanel), keyEquivalent: "w") + hide.target = self + appMenu.addItem(hide) + + appMenu.addItem(.separator()) + + let quit = NSMenuItem(title: "Quit Prompt", action: #selector(quit), keyEquivalent: "q") + quit.target = self + appMenu.addItem(quit) + + NSApp.mainMenu = mainMenu + } + + @MainActor @objc private func hidePanel() { + PanelController.shared.hide() + } + private func setupStatusItem() { let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) item.autosaveName = "Prompt" diff --git a/Sources/Prompt/HotkeyManager.swift b/Sources/Prompt/HotkeyManager.swift index d608ef0..e723cfa 100644 --- a/Sources/Prompt/HotkeyManager.swift +++ b/Sources/Prompt/HotkeyManager.swift @@ -34,6 +34,22 @@ final class HotkeyManager { registerFallbackHotkeys() } + func stop() { + if let tap = eventTap { + CGEvent.tapEnable(tap: tap, enable: false) + CFMachPortInvalidate(tap) + } + if let source = runLoopSource { + CFRunLoopRemoveSource(CFRunLoopGetMain(), source, .commonModes) + } + for ref in hotKeyRefs { + if let ref { UnregisterEventHotKey(ref) } + } + hotKeyRefs.removeAll() + eventTap = nil + runLoopSource = nil + } + // MARK: - Double-Shift event tap private func startEventTap() { diff --git a/Sources/Prompt/PanelController.swift b/Sources/Prompt/PanelController.swift index 451fb49..a5e4a4e 100644 --- a/Sources/Prompt/PanelController.swift +++ b/Sources/Prompt/PanelController.swift @@ -31,12 +31,14 @@ final class PanelController: NSObject { } NSApp.activate(ignoringOtherApps: true) panel.makeKeyAndOrderFront(nil) + NSLog("Prompt: panel shown") NotificationCenter.default.post(name: .promptPanelDidShow, object: nil) } func hide() { panel?.orderOut(nil) NSApp.deactivate() + NSLog("Prompt: panel hidden (app keeps running for the Shift-Shift hotkeys)") } private func makePanel() -> KeyablePanel { diff --git a/Sources/Prompt/PanelView.swift b/Sources/Prompt/PanelView.swift index 6a3ff3b..dde0ee1 100644 --- a/Sources/Prompt/PanelView.swift +++ b/Sources/Prompt/PanelView.swift @@ -76,7 +76,8 @@ struct PanelView: View { Button { addPhotos() } label: { Image(systemName: "photo.on.rectangle.angled") } .buttonStyle(.plain).foregroundStyle(.secondary).help("Add photos") Button { PanelController.shared.hide() } label: { Image(systemName: "xmark") } - .buttonStyle(.plain).foregroundStyle(.secondary).help("Close (Esc)") + .buttonStyle(.plain).foregroundStyle(.secondary) + .help("Hide the panel (Cmd+W) — Prompt keeps running so Shift-Shift capture still works. Quit from the menu bar icon or Cmd+Q.") } .padding(.horizontal, 12) .padding(.vertical, 10) diff --git a/Sources/Prompt/SyncService.swift b/Sources/Prompt/SyncService.swift index d06008e..50f3397 100644 --- a/Sources/Prompt/SyncService.swift +++ b/Sources/Prompt/SyncService.swift @@ -67,6 +67,14 @@ final class SyncService: ObservableObject { restartPolling() } + func stop() { + pollTimer?.invalidate() + pollTimer = nil + pushWork?.cancel() + pushWork = nil + Store.shared.onDidChange = nil + } + // MARK: - Group membership func generateCode() {