561a547b70
Menu-bar utility that fixes cross-display keyboard focus loss: a listen-only CGEvent tap locates the window under the cursor and reasserts app activation + AX focus on it, with debounce, tap auto-recovery, and an Accessibility permission gate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
55 lines
1.6 KiB
Swift
55 lines
1.6 KiB
Swift
import Foundation
|
|
|
|
/// UserDefaults-backed settings for FocusFixer.
|
|
final class Preferences {
|
|
static let shared = Preferences()
|
|
|
|
private enum Key {
|
|
static let enabled = "enabled"
|
|
static let excludedBundleIDs = "excludedBundleIDs"
|
|
static let debounceIntervalMS = "debounceIntervalMS"
|
|
}
|
|
|
|
private static let defaultDebounceIntervalMS = 150
|
|
|
|
private let defaults: UserDefaults
|
|
|
|
init(defaults: UserDefaults = .standard) {
|
|
self.defaults = defaults
|
|
defaults.register(defaults: [
|
|
Key.enabled: true,
|
|
Key.excludedBundleIDs: [String](),
|
|
Key.debounceIntervalMS: Self.defaultDebounceIntervalMS
|
|
])
|
|
}
|
|
|
|
var isEnabled: Bool {
|
|
get { defaults.bool(forKey: Key.enabled) }
|
|
set { defaults.set(newValue, forKey: Key.enabled) }
|
|
}
|
|
|
|
var excludedBundleIDs: Set<String> {
|
|
get { Set(defaults.stringArray(forKey: Key.excludedBundleIDs) ?? []) }
|
|
set { defaults.set(Array(newValue), forKey: Key.excludedBundleIDs) }
|
|
}
|
|
|
|
var debounceInterval: TimeInterval {
|
|
get { TimeInterval(defaults.integer(forKey: Key.debounceIntervalMS)) / 1000.0 }
|
|
set { defaults.set(Int(newValue * 1000), forKey: Key.debounceIntervalMS) }
|
|
}
|
|
|
|
func isExcluded(bundleID: String) -> Bool {
|
|
excludedBundleIDs.contains(bundleID)
|
|
}
|
|
|
|
func setExcluded(_ excluded: Bool, bundleID: String) {
|
|
var ids = excludedBundleIDs
|
|
if excluded {
|
|
ids.insert(bundleID)
|
|
} else {
|
|
ids.remove(bundleID)
|
|
}
|
|
excludedBundleIDs = ids
|
|
}
|
|
}
|