grammargem/Sources/GrammaGem/System/AppDetector.swift
BigMarc bb096bed7b Always-on live monitoring, page blocker, universal builds
- LiveMonitor: menu-bar–resident; reads the focused field via the Accessibility
  API, runs the offline grammar engine, and shows live suggestions + a "Fix all".
- Menu-bar icon now reflects status (issue count / paused); added Pause/Resume.
- Page Blocker: exclude apps + websites (Safari domain detection) where GrammaGem
  stays out of the way; hotkeys + monitoring suppressed there.
- build.sh now produces a universal (arm64 + x86_64) bundle; sign-notarize.sh is a
  turnkey codesign→notarize→staple flow reading creds from env/keychain (no secrets).
- Clean entitlements plist (AMFI-compatible).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 15:17:08 +02:00

52 lines
1.9 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)
var winRef: CFTypeRef?
guard AXUIElementCopyAttributeValue(
axApp, kAXFocusedWindowAttribute as CFString, &winRef) == .success,
let winRef
else { return nil }
let window = winRef as! AXUIElement
var docRef: CFTypeRef?
if AXUIElementCopyAttributeValue(
window, kAXDocumentAttribute as CFString, &docRef) == .success,
let urlString = docRef as? String,
let host = URL(string: urlString)?.host {
return host
}
return nil
}
}