diff --git a/AGENTS.md b/AGENTS.md index dd4fa72..767e096 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,13 @@ clicking an item copies it back to the clipboard. `NSStatusItem Preferred Position Prompt`). Left click toggles the panel, right click shows the menu (accessibility state, launch at login, open data folder, quit). Contains the single-instance guard and `LoginItem` - (`SMAppService`). + (`SMAppService`). Also sets `NSApp.mainMenu` with Hide Panel (Cmd+W) and Quit + (Cmd+Q) — an accessory app shows no menu bar, but the main menu still + services key equivalents, which is the only way to quit from the keyboard + while the panel has focus. `applicationShouldHandleReopen` shows the panel so + launching an already-running app does something. Supports `--sync-check` and + `--diag` (writes the status item's frame and permission state to + `/tmp/prompt-diag.txt`, then exits). - `Sources/Prompt/Store.swift` — `@MainActor` `ObservableObject` singleton. Data model: `Section` → `[PromptItem]`, item content is `.text(String)` or `.image(String)` (filename). Persists to @@ -167,7 +173,10 @@ brew upgrade --cask prompt the main thread (the hotkey handler already dispatches to a global queue). - The double-Shift tap needs Accessibility; without it the event tap silently fails and only the Carbon fallbacks work. The panel banner and the status - menu both surface the permission state. + menu both surface the permission state, and `AppDelegate` shows a launch + alert when it is missing — otherwise a user who hides the panel is locked out + (no hotkey, no Dock icon) and reports it as "the app won't come back". Check + with `Prompt --diag` (`accessibility: false` is the tell). - `Info.plist` sets `NSAllowsArbitraryLoads` because the sync server URL is user-supplied and is often plain HTTP (LAN address, self-hosted box). ATS would otherwise block it with a confusing error. diff --git a/README.md b/README.md index de3e0a9..72c82d0 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,11 @@ to your work. Click anything to copy it back to your clipboard. | Close panel | Esc | Double-Shift gestures require **Accessibility access** (Prompt reads the -selection by simulating Cmd+C, then restores your clipboard untouched). The -Cmd+Shift fallbacks work without it. Bare keys like Space and Cmd+C are -deliberately not bound inside the panel so they never conflict with typing in -the composer. +selection by simulating Cmd+C, then restores your clipboard untouched). Until +it is granted, use Cmd+Shift+Space and Cmd+Shift+C, or click the menu bar icon +— and launching the app again always brings the panel back. Bare keys like +Space and Cmd+C are deliberately not bound inside the panel so they never +conflict with typing in the composer. ## Install diff --git a/Sources/Prompt/App.swift b/Sources/Prompt/App.swift index 415a934..5dd83b0 100644 --- a/Sources/Prompt/App.swift +++ b/Sources/Prompt/App.swift @@ -27,6 +27,18 @@ final class AppDelegate: NSObject, NSApplicationDelegate { return } + // --diag reports where the status item landed. On a notched Mac a + // brand-new item can be placed under the notch and be unclickable, + // which locks the user out once the panel is hidden. + if CommandLine.arguments.contains("--diag") { + setupStatusItem() + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.writeDiagnostics() + NSApp.terminate(nil) + } + return + } + // Single instance: stale copies keep their hotkeys alive after an // update and eat the events meant for the new instance. let myPid = ProcessInfo.processInfo.processIdentifier @@ -48,6 +60,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { setupStatusItem() setupMainMenu() + promptForAccessibilityIfNeeded() // 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). @@ -57,6 +70,28 @@ final class AppDelegate: NSObject, NSApplicationDelegate { } } + /// Without Accessibility the Shift-Shift gestures fail silently, so the + /// panel can be hidden with no obvious way back. Say so on launch and + /// point at the shortcut that works without the permission. + private func promptForAccessibilityIfNeeded() { + guard !AXIsProcessTrusted() else { return } + let alert = NSAlert() + alert.messageText = "Prompt needs Accessibility access" + alert.informativeText = """ + Capturing with Shift-Shift (and showing the panel with right Shift twice) \ + only works with Accessibility access. + + Until it is granted, use Cmd+Shift+Space to show or hide the panel and \ + Cmd+Shift+C to capture, or click the menu bar icon. + """ + alert.addButton(withTitle: "Open Settings") + alert.addButton(withTitle: "Later") + if alert.runModal() == .alertFirstButtonReturn { + let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue(): true] as CFDictionary + _ = AXIsProcessTrustedWithOptions(options) + } + } + /// 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. @@ -103,6 +138,26 @@ final class AppDelegate: NSObject, NSApplicationDelegate { PanelController.shared.hide() } + @MainActor + private func writeDiagnostics() { + var lines = ["Prompt diagnostics", "accessibility: \(AXIsProcessTrusted())"] + if let window = statusItem?.button?.window { + lines.append("status item frame: \(window.frame)") + if let screen = window.screen ?? NSScreen.main { + lines.append("screen: \(screen.frame) visible: \(screen.visibleFrame)") + lines.append("notch inset: \(screen.safeAreaInsets)") + let underNotch = window.frame.maxX > screen.frame.maxX - screen.safeAreaInsets.right + lines.append("under the notch: \(underNotch)") + } + } else { + lines.append("status item has no window (not in the menu bar)") + } + lines.append("preferred position: \(UserDefaults.standard.object(forKey: "NSStatusItem Preferred Position Prompt") ?? "unset")") + try? lines.joined(separator: "\n").write( + to: URL(fileURLWithPath: "/tmp/prompt-diag.txt"), atomically: true, encoding: .utf8 + ) + } + private func setupStatusItem() { let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) item.autosaveName = "Prompt" diff --git a/Sources/Prompt/PanelView.swift b/Sources/Prompt/PanelView.swift index dde0ee1..eb146b0 100644 --- a/Sources/Prompt/PanelView.swift +++ b/Sources/Prompt/PanelView.swift @@ -87,8 +87,13 @@ struct PanelView: View { HStack(spacing: 8) { Image(systemName: "exclamationmark.triangle.fill") .foregroundStyle(.yellow) - Text("Shift-tap capture needs Accessibility access.") - .font(.caption) + VStack(alignment: .leading, spacing: 2) { + Text("Shift-tap capture needs Accessibility access.") + .font(.caption) + Text("Meanwhile: Cmd+Shift+Space toggles this panel.") + .font(.caption2) + .foregroundStyle(.secondary) + } Spacer() Button("Grant…") { let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue(): true] as CFDictionary