Files
prompt/Sources/Prompt/App.swift
T
TZGyn 5db9a3b9f7 Add optional sync between Macs with a shared 8-digit code
Notes and photos were stuck on one machine. A small Go/Postgres backend
stores each group's payload under a code that doubles as the shared
secret, so a second Mac just needs the code — no account or login.

Applying a remote change must not push it straight back, otherwise the
two Macs would bump the version back and forth forever, so payloads are
fingerprinted and identical ones are skipped.

💘 Generated with Crush

Assisted-by: Crush:kimi-k3
2026-10-08 09:55:34 +08:00

173 lines
5.8 KiB
Swift

import SwiftUI
import AppKit
import ServiceManagement
@main
struct PromptApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
var body: some Scene {
// The app lives in a floating panel + status item, so there are no
// real windows; a bare Settings scene keeps SwiftUI happy.
Settings { EmptyView() }
}
}
final class AppDelegate: NSObject, NSApplicationDelegate {
private var statusItem: NSStatusItem?
func applicationDidFinishLaunching(_ notification: Notification) {
NSApp.setActivationPolicy(.accessory)
if CommandLine.arguments.contains("--sync-check") {
Task { @MainActor in
await SyncService.shared.runDiagnostic()
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
let others = NSRunningApplication.runningApplications(withBundleIdentifier: "dev.tzgyn.prompt")
.filter { $0.processIdentifier != myPid }
for other in others {
other.forceTerminate()
}
HotkeyManager.shared.onCapture = {
DispatchQueue.global(qos: .userInitiated).async {
Capture.captureSelectionIntoStore()
}
}
HotkeyManager.shared.onTogglePanel = {
Task { @MainActor in PanelController.shared.toggle() }
}
HotkeyManager.shared.start()
setupStatusItem()
// 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).
Task { @MainActor in
SyncService.shared.start()
PanelController.shared.show()
}
}
private func setupStatusItem() {
let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
item.autosaveName = "Prompt"
// Seed a visible preferred position on first run so a new item does
// not land under the notch on a full menu bar; user drags win after.
if UserDefaults.standard.object(forKey: "NSStatusItem Preferred Position Prompt") == nil {
UserDefaults.standard.set(100.0, forKey: "NSStatusItem Preferred Position Prompt")
}
if let button = item.button {
button.image = NSImage(systemSymbolName: "text.bubble", accessibilityDescription: "Prompt")
button.action = #selector(statusItemClicked(_:))
button.target = self
button.sendAction(on: [.leftMouseUp, .rightMouseUp])
}
statusItem = item
}
@MainActor @objc private func statusItemClicked(_ sender: NSStatusBarButton) {
guard let event = NSApp.currentEvent else { return }
if event.type == .rightMouseUp {
let menu = buildMenu()
statusItem?.menu = menu
statusItem?.button?.performClick(nil)
statusItem?.menu = nil
} else {
PanelController.shared.toggle()
}
}
private func buildMenu() -> NSMenu {
let menu = NSMenu()
let toggle = NSMenuItem(title: "Show/Hide Prompt", action: #selector(togglePanel), keyEquivalent: "")
toggle.target = self
menu.addItem(toggle)
menu.addItem(.separator())
if AXIsProcessTrusted() {
let granted = NSMenuItem(title: "Accessibility: Granted", action: nil, keyEquivalent: "")
granted.isEnabled = false
menu.addItem(granted)
} else {
let grant = NSMenuItem(title: "Grant Accessibility Access…", action: #selector(requestAccessibility), keyEquivalent: "")
grant.target = self
menu.addItem(grant)
}
let loginTitle = LoginItem.isEnabled ? "Disable Launch at Login" : "Launch at Login"
let login = NSMenuItem(title: loginTitle, action: #selector(toggleLoginItem), keyEquivalent: "")
login.target = self
menu.addItem(login)
let folder = NSMenuItem(title: "Open Data Folder", action: #selector(openDataFolder), keyEquivalent: "")
folder.target = self
menu.addItem(folder)
menu.addItem(.separator())
let quit = NSMenuItem(title: "Quit Prompt", action: #selector(quit), keyEquivalent: "q")
quit.target = self
menu.addItem(quit)
return menu
}
@MainActor @objc private func togglePanel() {
PanelController.shared.toggle()
}
@objc private func requestAccessibility() {
let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue(): true] as CFDictionary
_ = AXIsProcessTrustedWithOptions(options)
}
@objc private func toggleLoginItem() {
if !LoginItem.toggle() {
let alert = NSAlert()
alert.messageText = "Could not update Launch at Login"
alert.informativeText = "Launch at Login requires a signed app bundle in /Applications. It is not available when running via `swift run`."
alert.alertStyle = .warning
alert.addButton(withTitle: "OK")
alert.runModal()
}
}
@MainActor @objc private func openDataFolder() {
NSWorkspace.shared.open(Store.shared.directory)
}
@objc private func quit() {
NSApp.terminate(nil)
}
}
enum LoginItem {
static var isEnabled: Bool {
SMAppService.mainApp.status == .enabled
}
static func toggle() -> Bool {
let service = SMAppService.mainApp
do {
if service.status == .enabled {
try service.unregister()
} else {
try service.register()
}
return true
} catch {
return false
}
}
}