mirror of
https://github.com/BigMarc/grammargem.git
synced 2026-10-13 17:01:55 +02:00
Stability (the "super buggy" feel): - Move the Accessibility-tree traversal OFF the main thread with a per-message timeout, so a busy/wedged target app can't beachball or hang GrammaGem. - Replace every unchecked `as! AXUIElement` force-cast with a CFTypeID-checked safe cast (AXSupport), eliminating crashes from misbehaving AX servers. - Never scan GrammaGem's own windows (self-PID exclusion). - Generation tokens + in-flight guard so superseded scans/downloads can't clobber state. Text safety: - Live fixes now read the element's CURRENT value before writing (and only apply a span edit when the field still matches the snapshot), so edits made since the last scan are never overwritten. Cap per-field check length. - Stable per-field identity (AX id / position) so the Live Check list no longer churns. - Include focus + identity in change-detection so tabbing updates the focused field. Other audited fixes: - Page blocker: conservative skip when a browser's URL can't be read. - Model download: validate on-disk size (reject HTML/truncated 200s), clean up partials on cancel/failure, handle URLError.cancelled. - Licensing: clear a server-confirmed-invalid license instead of honoring grace; show the real device limit instead of "0 Macs". - Hotkey re-register falls back to the default if a chosen combo is taken. - Daily free-tier counter tracks the live timezone. - "Fix focused field" ignores non-text controls. Feature: auto-install the on-device model in the background on first launch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
57 lines
2.3 KiB
Swift
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)
|
|
}
|
|
}
|