mirror of
https://github.com/BigMarc/grammargem.git
synced 2026-10-13 17:01:55 +02:00
Native SwiftUI menu-bar app: global hotkey → capture selected text (AX API / clipboard fallback) → grammar (Harper) / local AI (MLX) → replace in place. Free/paid entitlement gate + Lemon Squeezy device-cap licensing, $0 marginal cost per user. Heavyweight engines behind protocol stubs so it builds out of the box. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
55 lines
2.0 KiB
Swift
55 lines
2.0 KiB
Swift
import Foundation
|
|
import AppKit
|
|
import ApplicationServices
|
|
|
|
/// Tracks and requests the macOS permissions GrammaGem needs to read & replace
|
|
/// text system-wide: **Accessibility** (required) and, for the synthetic-keystroke
|
|
/// clipboard fallback, **Input Monitoring** may also be prompted by the system.
|
|
@MainActor
|
|
final class Permissions: ObservableObject {
|
|
@Published private(set) var accessibilityTrusted: Bool = false
|
|
|
|
init() {
|
|
refresh()
|
|
}
|
|
|
|
/// True when the app is trusted for the Accessibility API.
|
|
func refresh() {
|
|
accessibilityTrusted = AXIsProcessTrusted()
|
|
}
|
|
|
|
/// Prompt for Accessibility (shows the system dialog the first time).
|
|
func requestAccessibility() {
|
|
let key = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String
|
|
let options = [key: true] as CFDictionary
|
|
accessibilityTrusted = AXIsProcessTrustedWithOptions(options)
|
|
}
|
|
|
|
/// Deep-link to the relevant System Settings pane.
|
|
func openAccessibilitySettings() {
|
|
open("x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")
|
|
}
|
|
|
|
func openInputMonitoringSettings() {
|
|
open("x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent")
|
|
}
|
|
|
|
private func open(_ urlString: String) {
|
|
guard let url = URL(string: urlString) else { return }
|
|
NSWorkspace.shared.open(url)
|
|
}
|
|
|
|
/// Poll until the user grants Accessibility (used by onboarding to advance
|
|
/// automatically once permission flips on). Cancels when `accessibilityTrusted`.
|
|
func startPollingUntilGranted(interval: TimeInterval = 1.0) {
|
|
guard !accessibilityTrusted else { return }
|
|
Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] timer in
|
|
Task { @MainActor in
|
|
guard let self else { timer.invalidate(); return }
|
|
self.refresh()
|
|
if self.accessibilityTrusted { timer.invalidate() }
|
|
}
|
|
}
|
|
}
|
|
}
|