Skip to content

Commit 2f3eca3

Browse files
committed
observe settings change from other process (e.g. via CLI)
1 parent 32c6288 commit 2f3eca3

File tree

1 file changed

+131
-47
lines changed

1 file changed

+131
-47
lines changed

Sources/AutokbiswCore/IOKeyEventMonitor.swift

Lines changed: 131 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,14 @@ public final class IOKeyEventMonitor {
4848
notificationCenter = CFNotificationCenterGetDistributedCenter()
4949
let deviceMatch: CFMutableDictionary = [kIOHIDDeviceUsageKey: usage, kIOHIDDeviceUsagePageKey: usagePage] as NSMutableDictionary
5050
IOHIDManagerSetDeviceMatching(hidManager, deviceMatch)
51+
5152
loadMappings()
5253
}
5354

5455
deinit {
5556
self.saveMappings()
57+
stopObservingSettingsChanges()
58+
5659
let context = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
5760
IOHIDManagerRegisterInputValueCallback(hidManager, Optional.none, context)
5861
CFNotificationCenterRemoveObserver(notificationCenter, context, CFNotificationName(kTISNotifySelectedKeyboardInputSourceChanged), nil)
@@ -66,6 +69,8 @@ public final class IOKeyEventMonitor {
6669

6770
IOHIDManagerScheduleWithRunLoop(hidManager, CFRunLoopGetMain(), CFRunLoopMode.defaultMode!.rawValue)
6871
IOHIDManagerOpen(hidManager, IOOptionBits(kIOHIDOptionsTypeNone))
72+
73+
startObservingSettingsChanges()
6974
}
7075

7176
private func observeIputSourceChangedNotification(context: UnsafeMutableRawPointer) {
@@ -169,7 +174,12 @@ public extension IOKeyEventMonitor {
169174
}
170175

171176
func onKeyboardEvent(keyboard: String, conformsToKeyboard: Bool? = nil) {
172-
guard lastActiveKeyboard != keyboard else { return }
177+
guard lastActiveKeyboard != keyboard else {
178+
if verbosity >= TRACE {
179+
print("change: ignoring event from keyboard \(keyboard) because active device hasn't changed")
180+
}
181+
return
182+
}
173183

174184
if verbosity >= TRACE {
175185
print("change: keyboard changed from \(lastActiveKeyboard ?? "nil") to \(keyboard)")
@@ -200,52 +210,6 @@ public extension IOKeyEventMonitor {
200210
}
201211
}
202212

203-
// MARK: - Persistence
204-
205-
extension IOKeyEventMonitor {
206-
func loadMappings() {
207-
let selectableIsProperties = [
208-
kTISPropertyInputSourceIsEnableCapable: true,
209-
kTISPropertyInputSourceCategory: kTISCategoryKeyboardInputSource ?? "" as CFString,
210-
] as CFDictionary
211-
let inputSources = TISCreateInputSourceList(selectableIsProperties, false).takeUnretainedValue() as! [TISInputSource]
212-
213-
let inputSourcesById = inputSources.reduce([String: TISInputSource]()) {
214-
dict, inputSource -> [String: TISInputSource] in
215-
var dict = dict
216-
if let id = unmanagedStringToString(TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceID)) {
217-
dict[id] = inputSource
218-
}
219-
return dict
220-
}
221-
222-
if let mappings = defaults.dictionary(forKey: MAPPINGS_DEFAULTS_KEY) {
223-
for (keyboardId, inputSourceId) in mappings {
224-
kb2is[keyboardId] = inputSourcesById[String(describing: inputSourceId)]
225-
}
226-
}
227-
228-
if let enabledMappings = defaults.dictionary(forKey: MAPPING_ENABLED_KEY) as? [String: Bool] {
229-
deviceEnabled = enabledMappings
230-
}
231-
}
232-
233-
func saveMappings() {
234-
let mappings = kb2is.mapValues(is2Id)
235-
defaults.set(mappings, forKey: MAPPINGS_DEFAULTS_KEY)
236-
defaults.set(deviceEnabled, forKey: MAPPING_ENABLED_KEY)
237-
}
238-
239-
public func clearAllSettings() {
240-
kb2is.removeAll()
241-
deviceEnabled.removeAll()
242-
lastActiveKeyboard = nil
243-
defaults.removeObject(forKey: MAPPINGS_DEFAULTS_KEY)
244-
defaults.removeObject(forKey: MAPPING_ENABLED_KEY)
245-
defaults.synchronize()
246-
}
247-
}
248-
249213
// MARK: - Device Management
250214

251215
public extension IOKeyEventMonitor {
@@ -304,6 +268,126 @@ public extension IOKeyEventMonitor {
304268
}
305269
}
306270

271+
// MARK: - Persistence
272+
273+
extension IOKeyEventMonitor {
274+
func loadMappings() {
275+
let selectableIsProperties = [
276+
kTISPropertyInputSourceIsEnableCapable: true,
277+
kTISPropertyInputSourceCategory: kTISCategoryKeyboardInputSource ?? "" as CFString,
278+
] as CFDictionary
279+
let inputSources = TISCreateInputSourceList(selectableIsProperties, false).takeUnretainedValue() as! [TISInputSource]
280+
281+
let inputSourcesById = inputSources.reduce([String: TISInputSource]()) {
282+
dict, inputSource -> [String: TISInputSource] in
283+
var dict = dict
284+
if let id = unmanagedStringToString(TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceID)) {
285+
dict[id] = inputSource
286+
}
287+
return dict
288+
}
289+
290+
if let mappings = defaults.dictionary(forKey: MAPPINGS_DEFAULTS_KEY) {
291+
for (keyboardId, inputSourceId) in mappings {
292+
kb2is[keyboardId] = inputSourcesById[String(describing: inputSourceId)]
293+
}
294+
}
295+
296+
if let enabledMappings = defaults.dictionary(forKey: MAPPING_ENABLED_KEY) as? [String: Bool] {
297+
deviceEnabled = enabledMappings
298+
}
299+
}
300+
301+
func saveMappings() {
302+
withPausedSettingsObserver {
303+
let mappings = kb2is.mapValues(is2Id)
304+
defaults.set(mappings, forKey: MAPPINGS_DEFAULTS_KEY)
305+
defaults.set(deviceEnabled, forKey: MAPPING_ENABLED_KEY)
306+
defaults.synchronize()
307+
308+
postSettingsChangedNotification()
309+
}
310+
311+
if verbosity >= TRACE {
312+
print("Saved keyboard mappings to UserDefaults")
313+
}
314+
}
315+
316+
public func clearAllSettings() {
317+
kb2is.removeAll()
318+
deviceEnabled.removeAll()
319+
lastActiveKeyboard = nil
320+
defaults.removeObject(forKey: MAPPINGS_DEFAULTS_KEY)
321+
defaults.removeObject(forKey: MAPPING_ENABLED_KEY)
322+
defaults.synchronize()
323+
}
324+
}
325+
326+
// MARK: - Settings Change Notifications
327+
328+
private extension IOKeyEventMonitor {
329+
private func startObservingSettingsChanges() {
330+
if verbosity >= TRACE {
331+
print("Starting UserDefaults observation")
332+
}
333+
334+
let context = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
335+
let callback: CFNotificationCallback = { _, observer, _, _, _ in
336+
let selfPtr = Unmanaged<IOKeyEventMonitor>.fromOpaque(observer!).takeUnretainedValue()
337+
if selfPtr.verbosity >= TRACE {
338+
print("Received settings change notification")
339+
}
340+
selfPtr.onSettingsChanged()
341+
}
342+
343+
CFNotificationCenterAddObserver(
344+
notificationCenter,
345+
context,
346+
callback,
347+
"com.autokbisw.settingsChanged" as CFString,
348+
nil,
349+
.deliverImmediately
350+
)
351+
}
352+
353+
private func stopObservingSettingsChanges() {
354+
let context = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
355+
CFNotificationCenterRemoveObserver(
356+
notificationCenter,
357+
context,
358+
CFNotificationName("com.autokbisw.settingsChanged" as CFString),
359+
nil
360+
)
361+
}
362+
363+
private func postSettingsChangedNotification() {
364+
if verbosity >= TRACE {
365+
print("Posting settings changed notification")
366+
}
367+
368+
CFNotificationCenterPostNotification(
369+
notificationCenter,
370+
CFNotificationName("com.autokbisw.settingsChanged" as CFString),
371+
nil,
372+
nil,
373+
true
374+
)
375+
}
376+
377+
private func withPausedSettingsObserver<T>(_ operation: () -> T) -> T {
378+
stopObservingSettingsChanges()
379+
defer { startObservingSettingsChanges() }
380+
return operation()
381+
}
382+
383+
private func onSettingsChanged() {
384+
loadMappings()
385+
if verbosity >= TRACE {
386+
print("Reloaded mappings due to UserDefaults change")
387+
}
388+
}
389+
}
390+
307391
// MARK: - Utilities
308392

309393
extension IOKeyEventMonitor {

0 commit comments

Comments
 (0)