grammargem/Sources/GrammaGem/System/Permissions.swift
BigMarc f5faa27f88 GrammaGem — open-source, on-device macOS writing assistant
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>
2026-06-16 13:56:26 +02:00

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() }
}
}
}
}