grammargem/Sources/GrammarGem/System/AppDetector.swift
BigMarc 3d749e8139 feat: Implement LiveMonitor for real-time grammar and spelling checks across applications
- Added LiveMonitor class to scan text areas in the frontmost window for grammar and spelling issues.
- Introduced DetectedField struct to represent text areas with issues, including suggestions for corrections.
- Implemented functionality to apply corrections and dismiss suggestions.
- Added Permissions class to manage Accessibility permissions required for monitoring.
- Created SnippetStore for managing text-expansion snippets, including persistence to UserDefaults.
- Developed TextCapture class to read and write text from the frontmost application using Accessibility API and clipboard fallback.
- Implemented TextFieldScanner to traverse the Accessibility tree and collect editable text elements.
- Added TextReplacementCoordinator to orchestrate the text correction process.
- Integrated UpdaterManager for handling application updates using Sparkle.
- Created comprehensive unit tests for various components, including grammar corrections and entitlement checks.
2026-06-18 16:49:55 +02:00

57 lines
2.3 KiB
Swift

import Foundation
import AppKit
import ApplicationServices
/// Reports the frontmost application so App-Aware mode switching can pick the
/// right Writing Mode. Pure local introspection — no network, no logging of content.
@MainActor
final class AppDetector {
struct FrontApp: Equatable {
let bundleID: String
let name: String
}
/// The app currently in the foreground (the one the hotkey will act on).
func frontmost() -> FrontApp? {
guard let app = NSWorkspace.shared.frontmostApplication else { return nil }
return FrontApp(
bundleID: app.bundleIdentifier ?? "unknown",
name: app.localizedName ?? "App")
}
/// The Writing Mode App-Aware would auto-apply for the current frontmost app.
func suggestedMode() -> WritingMode? {
guard let front = frontmost() else { return nil }
return ModeRegistry.mode(forBundleID: front.bundleID)
}
/// Best-effort hostname of the frontmost browser tab (used by the page blocker).
/// Reads the window's AX document URL — works in Safari and some others with no
/// extra permission beyond Accessibility. Returns nil when it can't tell.
func frontmostDomain() -> String? {
guard AXIsProcessTrusted(),
let app = NSWorkspace.shared.frontmostApplication else { return nil }
let axApp = AXUIElementCreateApplication(app.processIdentifier)
AX.setMessagingTimeout(axApp, 0.3)
guard let window = AX.copyElement(axApp, kAXFocusedWindowAttribute) else { return nil }
if let urlString = AX.copyString(window, kAXDocumentAttribute),
let host = URL(string: urlString)?.host {
return host
}
return nil
}
/// Known web browsers, where a nil domain means "URL undeterminable" (so the
/// page blocker should be conservative) rather than "not a web page".
private static let browserBundleIDs: Set<String> = [
"com.apple.Safari", "com.google.Chrome", "com.google.Chrome.canary",
"company.thebrowser.Browser", "com.microsoft.edgemac", "com.brave.Browser",
"org.mozilla.firefox", "com.operasoftware.Opera", "com.vivaldi.Vivaldi",
]
func isBrowser(_ bundleID: String?) -> Bool {
guard let bundleID else { return false }
return Self.browserBundleIDs.contains(bundleID)
}
}