mirror of
https://github.com/BigMarc/grammargem.git
synced 2026-10-13 17:01:55 +02:00
- 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.
89 lines
3.3 KiB
Swift
89 lines
3.3 KiB
Swift
import Foundation
|
|
import AppKit
|
|
|
|
/// The "page blocker": apps and domains where GrammaGem should stay completely
|
|
/// out of the way — password managers, banking sites, anything sensitive.
|
|
/// When the frontmost context is excluded, live monitoring and the hotkeys are
|
|
/// suppressed.
|
|
@MainActor
|
|
final class Exclusions: ObservableObject {
|
|
@Published private(set) var blockedApps: [String] // bundle identifiers
|
|
@Published private(set) var blockedDomains: [String] // hostnames, e.g. "chase.com"
|
|
|
|
private let appsKey = "GrammaGem.blockedApps"
|
|
private let domainsKey = "GrammaGem.blockedDomains"
|
|
private let d = UserDefaults.standard
|
|
|
|
/// Common sensitive apps offered as one-tap suggestions in the UI.
|
|
static let suggestedApps: [(name: String, bundleID: String)] = [
|
|
("1Password", "com.1password.1password"),
|
|
("Bitwarden", "com.bitwarden.desktop"),
|
|
("Keychain Access", "com.apple.keychainaccess"),
|
|
("Terminal", "com.apple.Terminal"),
|
|
]
|
|
|
|
init() {
|
|
blockedApps = d.stringArray(forKey: appsKey) ?? []
|
|
blockedDomains = d.stringArray(forKey: domainsKey) ?? []
|
|
}
|
|
|
|
/// True if the user has any domain rules (so we should be conservative when
|
|
/// a browser's URL can't be read).
|
|
var hasDomainRules: Bool { !blockedDomains.isEmpty }
|
|
|
|
/// Whether GrammaGem should be suppressed for this frontmost context.
|
|
func isBlocked(bundleID: String?, domain: String?) -> Bool {
|
|
if let bundleID, blockedApps.contains(bundleID) { return true }
|
|
if let host = domain?.lowercased(), !host.isEmpty {
|
|
return blockedDomains.contains { dom in
|
|
let d = dom.lowercased()
|
|
return host == d || host.hasSuffix("." + d)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func addApp(_ bundleID: String) {
|
|
let b = bundleID.trimmingCharacters(in: .whitespaces)
|
|
guard !b.isEmpty, !blockedApps.contains(b) else { return }
|
|
blockedApps.append(b)
|
|
d.set(blockedApps, forKey: appsKey)
|
|
}
|
|
|
|
func removeApp(_ bundleID: String) {
|
|
blockedApps.removeAll { $0 == bundleID }
|
|
d.set(blockedApps, forKey: appsKey)
|
|
}
|
|
|
|
func addDomain(_ domain: String) {
|
|
let host = normalizedDomain(domain)
|
|
guard !host.isEmpty, !blockedDomains.contains(host) else { return }
|
|
blockedDomains.append(host)
|
|
d.set(blockedDomains, forKey: domainsKey)
|
|
}
|
|
|
|
func removeDomain(_ domain: String) {
|
|
blockedDomains.removeAll { $0 == domain }
|
|
d.set(blockedDomains, forKey: domainsKey)
|
|
}
|
|
|
|
/// Friendly app name for a bundle id, if installed.
|
|
func appName(for bundleID: String) -> String {
|
|
if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID),
|
|
let bundle = Bundle(url: url),
|
|
let name = (bundle.infoDictionary?["CFBundleName"] as? String) {
|
|
return name
|
|
}
|
|
return bundleID
|
|
}
|
|
|
|
private func normalizedDomain(_ raw: String) -> String {
|
|
var s = raw.trimmingCharacters(in: .whitespaces).lowercased()
|
|
if let url = URL(string: s), let host = url.host { return host }
|
|
s = s.replacingOccurrences(of: "https://", with: "")
|
|
.replacingOccurrences(of: "http://", with: "")
|
|
if let slash = s.firstIndex(of: "/") { s = String(s[..<slash]) }
|
|
return s
|
|
}
|
|
}
|