Skip to content

Commit 4ac1db2

Browse files
authored
Merge pull request #43 from ohueter/fix/observe-settings-change-from-other-process
Fix: Observe settings changes from other process (e.g. via CLI)
2 parents 0a06a7e + 2f3eca3 commit 4ac1db2

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 >= DEBUG {
175185
print("change: keyboard changed from \(lastActiveKeyboard ?? "nil") to \(keyboard)")
@@ -201,52 +211,6 @@ public extension IOKeyEventMonitor {
201211
}
202212
}
203213

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

252216
public extension IOKeyEventMonitor {
@@ -305,6 +269,126 @@ public extension IOKeyEventMonitor {
305269
}
306270
}
307271

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

310394
extension IOKeyEventMonitor {

0 commit comments

Comments
 (0)