From 51692b8d1ed81072a3906e5ce50524eec0981b67 Mon Sep 17 00:00:00 2001 From: r3dbars Date: Wed, 7 Oct 2026 16:27:58 -0500 Subject: [PATCH 1/3] Dictation key: Caps Lock no longer blocks the Right Option hold With Caps Lock latched, every event tap flag set carries the capsLock bit, so the modifier-key matcher compared option|capsLock against a binding with no modifiers and the default Right Option key never started dictation. Caps Lock is a toggle, so the matcher and the recorder now ignore it for modifier-key bindings, the same way duplicate detection already did. --- .../PhysicalDictationTriggerPreferences.swift | 6 ++- ...hysicalDictationTriggerCapsLockTests.swift | 46 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 Tests/PhysicalDictationTriggerCapsLockTests.swift diff --git a/Sources/Support/PhysicalDictationTriggerPreferences.swift b/Sources/Support/PhysicalDictationTriggerPreferences.swift index 13f0955ef..723f59a58 100644 --- a/Sources/Support/PhysicalDictationTriggerPreferences.swift +++ b/Sources/Support/PhysicalDictationTriggerPreferences.swift @@ -296,7 +296,7 @@ enum PhysicalDictationTriggerPreferences { return PhysicalDictationTriggerBinding( keyCode: keyCode, - modifiers: currentModifiers & ~primaryModifier + modifiers: currentModifiers & ~primaryModifier & ~PhysicalDictationTriggerModifiers.capsLock ) } @@ -417,7 +417,9 @@ enum PhysicalDictationTriggerPreferences { return false } - return (modifiers & ~primaryModifier) == binding.modifiers + // Caps Lock is latched, not held, so it never blocks a modifier chord. + let ignored = primaryModifier | PhysicalDictationTriggerModifiers.capsLock + return (modifiers & ~ignored) == (binding.modifiers & ~ignored) } private static func modifierChordEventBelongs( diff --git a/Tests/PhysicalDictationTriggerCapsLockTests.swift b/Tests/PhysicalDictationTriggerCapsLockTests.swift new file mode 100644 index 000000000..9f2a2914b --- /dev/null +++ b/Tests/PhysicalDictationTriggerCapsLockTests.swift @@ -0,0 +1,46 @@ +import Carbon +import CoreGraphics +import Foundation + +// Caps Lock is a latched toggle, not a held modifier. With it on, every event +// tap flag set carries the capsLock bit, and the shortcuts must still fire. +func testPhysicalDictationTriggerCapsLock() { + let option = PhysicalDictationTriggerModifiers.option + let capsLock = PhysicalDictationTriggerModifiers.capsLock + let rightOption = UInt32(kVK_RightOption) + + runSuite("The Right Option dictation key still presses and releases with Caps Lock on") { + let binding = PhysicalDictationTriggerPreferences.defaultPushToTalkBinding + let pressFlags = PhysicalDictationTriggerPreferences.modifiers(from: CGEventFlags([.maskAlternate, .maskAlphaShift])) + let releaseFlags = PhysicalDictationTriggerPreferences.modifiers(from: CGEventFlags([.maskAlphaShift])) + + assertTrue( + PhysicalDictationTriggerPreferences.matchesFlagsChangedPress(binding, keyCode: rightOption, modifiers: pressFlags), + "Right Option down with Caps Lock latched should start dictation" + ) + assertTrue( + PhysicalDictationTriggerPreferences.matchesFlagsChangedRelease(binding, keyCode: rightOption, modifiers: releaseFlags), + "Right Option up with Caps Lock latched should end the hold" + ) + assertFalse( + PhysicalDictationTriggerPreferences.matchesFlagsChangedPress( + binding, + keyCode: rightOption, + modifiers: option | capsLock | PhysicalDictationTriggerModifiers.shift + ), + "a real extra modifier (Shift) still blocks the bare Right Option key" + ) + } + + runSuite("A modifier chord recorded with Caps Lock on does not need Caps Lock to fire") { + let recorded = PhysicalDictationTriggerPreferences.bindingForFlagsChanged( + keyCode: rightOption, + modifierFlags: [.option, .capsLock] + ) + assertEqual( + recorded, + PhysicalDictationTriggerBinding(keyCode: rightOption), + "recording Right Option while Caps Lock is on should save plain Right Option" + ) + } +} From a2a4db1e1fde31019b56a77fbbdb6f8cacd1cc51 Mon Sep 17 00:00:00 2001 From: r3dbars Date: Wed, 7 Oct 2026 16:37:03 -0500 Subject: [PATCH 2/3] Shortcuts: chord shortcuts fire with Caps Lock on matchesKeyDown compared the binding against event modifiers that still had the Caps Lock bit, so Option-M (and any other chord) silently did nothing while Caps Lock was latched. Recording a chord with Caps Lock on also saved the bit, so that shortcut then only worked with Caps Lock on. Both now ignore Caps Lock. --- .../PhysicalDictationTriggerPreferences.swift | 6 ++-- ...hysicalDictationTriggerCapsLockTests.swift | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/Sources/Support/PhysicalDictationTriggerPreferences.swift b/Sources/Support/PhysicalDictationTriggerPreferences.swift index 723f59a58..f1ec098c0 100644 --- a/Sources/Support/PhysicalDictationTriggerPreferences.swift +++ b/Sources/Support/PhysicalDictationTriggerPreferences.swift @@ -274,7 +274,7 @@ enum PhysicalDictationTriggerPreferences { static func bindingForKeyDown(keyCode: UInt32, modifierFlags: NSEvent.ModifierFlags) -> PhysicalDictationTriggerBinding { PhysicalDictationTriggerBinding( keyCode: keyCode, - modifiers: modifiers(from: modifierFlags) + modifiers: modifiers(from: modifierFlags) & ~PhysicalDictationTriggerModifiers.capsLock ) } @@ -306,8 +306,10 @@ enum PhysicalDictationTriggerPreferences { modifiers: UInt32 ) -> Bool { guard !isModifierKey(binding.keyCode) else { return false } + // Caps Lock is latched, not held, so it never blocks a chord. + let relevant = PhysicalDictationTriggerModifiers.all & ~PhysicalDictationTriggerModifiers.capsLock return binding.keyCode == keyCode - && binding.modifiers == (modifiers & PhysicalDictationTriggerModifiers.all) + && (binding.modifiers & relevant) == (modifiers & relevant) } static func matchesFlagsChangedPress( diff --git a/Tests/PhysicalDictationTriggerCapsLockTests.swift b/Tests/PhysicalDictationTriggerCapsLockTests.swift index 9f2a2914b..ff566158d 100644 --- a/Tests/PhysicalDictationTriggerCapsLockTests.swift +++ b/Tests/PhysicalDictationTriggerCapsLockTests.swift @@ -43,4 +43,32 @@ func testPhysicalDictationTriggerCapsLock() { "recording Right Option while Caps Lock is on should save plain Right Option" ) } + + runSuite("Option-M and other chord shortcuts still fire with Caps Lock on") { + let meeting = PhysicalDictationTriggerPreferences.defaultMeetingBinding + let flags = PhysicalDictationTriggerPreferences.modifiers(from: CGEventFlags([.maskAlternate, .maskAlphaShift])) + + assertTrue( + PhysicalDictationTriggerPreferences.matchesKeyDown(meeting, keyCode: meeting.keyCode, modifiers: flags), + "Option-M with Caps Lock latched should still start a meeting" + ) + assertFalse( + PhysicalDictationTriggerPreferences.matchesKeyDown( + meeting, + keyCode: meeting.keyCode, + modifiers: option | capsLock | PhysicalDictationTriggerModifiers.command + ), + "a real extra modifier (Command) still keeps Option-M from matching" + ) + + let recorded = PhysicalDictationTriggerPreferences.bindingForKeyDown( + keyCode: meeting.keyCode, + modifierFlags: [.option, .capsLock] + ) + assertEqual(recorded, meeting, "recording Option-M while Caps Lock is on should save plain Option-M") + assertTrue( + PhysicalDictationTriggerPreferences.matchesKeyDown(recorded, keyCode: meeting.keyCode, modifiers: option), + "a chord recorded with Caps Lock on should fire after Caps Lock is turned off" + ) + } } From 1de66a4b5c470efc6811c131995a18d4a2c71011 Mon Sep 17 00:00:00 2001 From: r3dbars Date: Wed, 7 Oct 2026 17:57:01 -0500 Subject: [PATCH 3/3] Shortcuts: drop a Caps Lock bit saved by older builds on load Review follow-up. A binding recorded with Caps Lock on before this fix still carried the bit on disk, so pressing Caps Lock while holding Left Option counted as that dictation key, and Settings labelled it "Caps". Stored bindings now load with the bit stripped. --- .../PhysicalDictationTriggerPreferences.swift | 9 ++++- ...hysicalDictationTriggerCapsLockTests.swift | 39 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/Sources/Support/PhysicalDictationTriggerPreferences.swift b/Sources/Support/PhysicalDictationTriggerPreferences.swift index f1ec098c0..f4e08aa29 100644 --- a/Sources/Support/PhysicalDictationTriggerPreferences.swift +++ b/Sources/Support/PhysicalDictationTriggerPreferences.swift @@ -569,7 +569,14 @@ enum PhysicalDictationTriggerPreferences { _ binding: PhysicalDictationTriggerBinding, fallback: PhysicalDictationTriggerBinding ) -> PhysicalDictationTriggerBinding { - rejectionReason(for: binding) == nil ? binding : fallback + // Older builds saved the latched Caps Lock bit with a key. It isn't a + // held modifier, so drop it: otherwise Caps Lock itself "belongs" to + // the chord and Settings labels it "Caps". + let binding = PhysicalDictationTriggerBinding( + keyCode: binding.keyCode, + modifiers: binding.modifiers & ~PhysicalDictationTriggerModifiers.capsLock + ) + return rejectionReason(for: binding) == nil ? binding : fallback } private static func isFunctionKey(_ keyCode: UInt32) -> Bool { diff --git a/Tests/PhysicalDictationTriggerCapsLockTests.swift b/Tests/PhysicalDictationTriggerCapsLockTests.swift index ff566158d..f87bc26b8 100644 --- a/Tests/PhysicalDictationTriggerCapsLockTests.swift +++ b/Tests/PhysicalDictationTriggerCapsLockTests.swift @@ -71,4 +71,43 @@ func testPhysicalDictationTriggerCapsLock() { "a chord recorded with Caps Lock on should fire after Caps Lock is turned off" ) } + + runSuite("A key saved with the Caps Lock bit by an older build loads without it") { + let suiteName = "PhysicalDictationTriggerCapsLockTests-\(UUID().uuidString)" + let defaults = UserDefaults(suiteName: suiteName)! + defaults.removePersistentDomain(forName: suiteName) + defer { defaults.removePersistentDomain(forName: suiteName) } + + let leftOption = UInt32(kVK_Option) + // Older builds recorded the latched Caps Lock bit with the key. + PhysicalDictationTriggerPreferences.savePushToTalk( + PhysicalDictationTriggerBinding(keyCode: leftOption, modifiers: capsLock), + userDefaults: defaults + ) + let loaded = PhysicalDictationTriggerPreferences.pushToTalkBinding(userDefaults: defaults) + + assertEqual(loaded, PhysicalDictationTriggerBinding(keyCode: leftOption), "the stored Caps Lock bit should be dropped on load") + assertFalse( + PhysicalDictationTriggerPreferences.matchesFlagsChangedPress(loaded, keyCode: UInt32(kVK_CapsLock), modifiers: option | capsLock), + "pressing Caps Lock while holding Left Option must not count as the dictation key" + ) + assertTrue( + PhysicalDictationTriggerPreferences.matchesFlagsChangedPress(loaded, keyCode: leftOption, modifiers: option | capsLock), + "Left Option itself still starts dictation with Caps Lock on" + ) + assertFalse( + PhysicalDictationTriggerPreferences.displayString(for: loaded).contains("Caps"), + "Settings should not label the key with Caps" + ) + + PhysicalDictationTriggerPreferences.saveMeeting( + PhysicalDictationTriggerBinding(keyCode: UInt32(kVK_ANSI_M), modifiers: option | capsLock), + userDefaults: defaults + ) + assertEqual( + PhysicalDictationTriggerPreferences.meetingBinding(userDefaults: defaults), + PhysicalDictationTriggerPreferences.defaultMeetingBinding, + "a stored Option-Caps-M loads as plain Option-M" + ) + } }