How to Debug Issues with ShortcutManager.swift in OpenSuperWhisper: A Complete Guide
To debug ShortcutManager.swift effectively, verify which trigger mode is active (regular shortcut, modifier-only, or mouse button), add strategic logging to handleKeyDown() and handleKeyUp(), and confirm that the KeyboardShortcuts library properly registers the .toggleRecord name in setupKeyboardShortcuts().
ShortcutManager.swift serves as the central command hub for hot-key handling in the OpenSuperWhisper macOS application, bridging user preferences defined in AppPreferences with low-level system monitors. When shortcuts fail to fire, hold-to-record behaves unexpectedly, or the indicator window never appears, developers need targeted debugging strategies to isolate the root cause. This guide provides specific techniques for troubleshooting OpenSuperWhisper/ShortcutManager.swift based on the actual source implementation and its dependencies.
Understanding the ShortcutManager.swift Architecture
Core Components and Their Interactions
The ShortcutManager class operates as a singleton that coordinates between multiple specialized monitors and the UI layer. Understanding these relationships is critical for effective debugging:
-
ShortcutManager: The singleton coordinator that registers hot-keys and manages three distinct trigger modes. It initializes with a log statement at line 25 and directly instantiates
KeyboardShortcuts,ModifierKeyMonitor,MouseButtonMonitor, andIndicatorWindowManager. -
KeyboardShortcuts: A third-party library (declared in
Package.swift) that watches for the named shortcut.toggleRecordand delivers callbacks on key down and key up events. This is configured insetupKeyboardShortcuts()at lines 54-71. -
ModifierKeyMonitor: A low-level Carbon monitor that watches for standalone modifier key presses (e.g., ⌥ Option). This activates when users select modifier-only hot-key mode, with callbacks wired in
setupRecordingTrigger()at lines 99-112. -
MouseButtonMonitor: Similar to the modifier monitor but tracks mouse button events. Enabled when mouse button hot-key is selected, with implementation at lines 84-98 in
setupRecordingTrigger(). -
IndicatorWindowManager: Creates the floating recording window and controls audio capture lifecycle. Called from
handleKeyDown()(lines 28-44) andhandleKeyUp()to show and hide the recording interface. -
AppPreferences: Central storage for user settings including
holdToRecord,modifierOnlyHotkey, andmouseButtonHotkey. Read insetupRecordingTrigger()at lines 75-78 and withinhandleKeyDown/Upat line 26.
Execution Flow from User Input to Recording
The typical execution path through ShortcutManager.swift follows six distinct stages:
-
Singleton Initialization:
ShortcutManager.sharedis lazily instantiated once, logging "ShortcutManager init" to confirm activation. -
Keyboard Registration:
setupKeyboardShortcuts()configuresKeyboardShortcuts.onKeyDownandonKeyUpfor the.toggleRecordidentifier. -
Trigger Mode Selection:
setupRecordingTrigger()readsAppPreferences.sharedand enables the appropriate monitor while disabling others. The system prioritizes mouse button > modifier key > regular shortcut, with monitors being mutually exclusive as noted in the comment at line 78. -
Key Down Handling:
handleKeyDown()creates anIndicatorWindowManagerview model if none exists, captures the current caret/cursor position, and initiates recording. If hold-to-record is enabled, it queues aDispatchWorkItemto setholdModeafter theholdThreshold(0.3 seconds). -
Key Up Handling:
handleKeyUp()cancels any pendingholdWorkItem. If operating in hold mode, it stops the recording and cleans up the view model.
Common Debugging Scenarios and Solutions
Hot-Key Not Firing at All
When no shortcuts respond, the issue typically stems from registration failures in the KeyboardShortcuts library or disabled monitors.
First, verify the singleton initialized by checking for the "ShortcutManager init" log. If absent, the manager never instantiated. Next, confirm registration succeeded by adding a print statement immediately after KeyboardShortcuts.enable(.toggleRecord) in setupKeyboardShortcuts().
If using modifier-only or mouse button modes, ensure setupRecordingTrigger() actually enabled the correct monitor. The three modes are mutually exclusive, so enabling one disables others.
Hold-to-Record Mode Malfunctioning
Hold-to-record logic depends on the holdWorkItem DispatchWorkItem and the holdMode boolean flag. Common bugs include the work item not cancelling properly, leaving holdMode stuck in the wrong state.
Add logging to track state transitions:
print("holdMode before: \(holdMode), workItem exists: \(holdWorkItem != nil)")
If holdToRecordEnabled reads false from AppPreferences, verify the preference key exists and holdThreshold hasn't been set to an excessively high value.
Wrong Trigger Mode Active
Because ShortcutManager.swift only allows one active monitor at a time, configuration issues in AppPreferences can cause unexpected behavior.
Determine the active mode by logging the preference flags at the start of setupRecordingTrigger():
print("Active mode: mouse=\(useMouseButtonHotkey) modifier=\(useModifierOnlyHotkey) regular=\(!useMouseButtonHotkey && !useModifierOnlyHotkey)")
If these flags don't match your expectation, the hotkeySettingsChanged notification likely failed to trigger after preference changes, preventing setupRecordingTrigger() from re-evaluating the configuration.
Indicator Window Not Appearing or Positioned Incorrectly
When the indicator window fails to display, verify that IndicatorWindowManager.shared.show(nearPoint:) returns a valid view model. Add this check after line 38 in handleKeyDown():
guard let vm = IndicatorWindowManager.shared.show(nearPoint: cursorPosition ?? .zero) else {
print("Failed to create indicator window")
return
}
print("Created view model: \(vm)")
For positioning issues, inspect the values returned by FocusUtils.getCaretRect() before passing them to show(nearPoint:). Nil or stale coordinates indicate the focus utility failed to detect the current text field.
Practical Debugging Code Examples
Logging the Full Hot-Key Lifecycle
Add temporary logging without altering core logic by extending ShortcutManager:
extension ShortcutManager {
private func logEvent(_ name: String) {
print("[ShortcutManager] \(name) – vm: \(String(describing: activeVm)) holdMode: \(holdMode)")
}
private func handleKeyDown() {
logEvent("keyDown start")
// original implementation...
logEvent("keyDown end")
}
private func handleKeyUp() {
logEvent("keyUp start")
// original implementation...
logEvent("keyUp end")
}
}
This trace helps identify whether key events are being swallowed or if the view model lifecycle is failing.
Forcing a Specific Trigger Mode
To isolate monitor-specific bugs, temporarily override preferences in a debug build:
// Force modifier-only mode
AppPreferences.shared.modifierOnlyHotkey = ModifierKey.control.rawValue
AppPreferences.shared.mouseButtonHotkey = MouseButton.none.rawValue
NotificationCenter.default.post(name: .hotkeySettingsChanged, object: nil)
This triggers the code path at lines 99-114, allowing you to verify ModifierKeyMonitor behavior independently of the regular shortcut system.
Simulating Key Presses for Unit Testing
For automated testing or manual debugging without hardware input:
func simulateToggleRecord() {
ShortcutManager.shared.handleKeyDown()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
ShortcutManager.shared.handleKeyUp()
}
}
Note: This bypasses the actual KeyboardShortcuts layer and should only be used for debugging the downstream logic in handleKeyDown() and handleKeyUp().
Summary
- ShortcutManager.swift acts as a singleton coordinator between user preferences and three mutually exclusive trigger modes (regular shortcut, modifier-only, or mouse button).
- Debug registration issues by verifying the
KeyboardShortcuts.enable(.toggleRecord)call succeeds and checking for the "ShortcutManager init" log. - Trace hold-to-record problems by monitoring the
holdWorkItemDispatchWorkItem andholdModestate transitions inhandleKeyDown()andhandleKeyUp(). - Identify active trigger modes by logging
useMouseButtonHotkeyanduseModifierOnlyHotkeyflags at the start ofsetupRecordingTrigger(). - Fix indicator window issues by validating that
IndicatorWindowManager.shared.show()returns a non-nil view model and thatFocusUtils.getCaretRect()provides valid coordinates.
Frequently Asked Questions
Why does my shortcut only work once and then stop responding?
This typically occurs when the holdWorkItem DispatchWorkItem fails to cancel in handleKeyUp(), leaving holdMode stuck in an inconsistent state. Add logging to verify holdWorkItem?.cancel() executes and that holdMode resets properly after each key cycle. Also ensure AppPreferences.shared.holdToRecord hasn't been inadvertently disabled.
How do I know which trigger mode is currently active?
Insert a debug print statement at the beginning of setupRecordingTrigger() to log the boolean flags: useMouseButtonHotkey, useModifierOnlyHotkey, and the negated combination for regular shortcuts. Since the monitors are mutually exclusive as implemented at line 78, only one flag should be true at any given time.
Can I debug ShortcutManager.swift without rebuilding the entire app?
Yes. Since ShortcutManager is a singleton with public methods, you can create a temporary debug extension or add logging statements directly to handleKeyDown() and handleKeyUp(). You can also programmatically trigger these handlers using the simulation code shown above to test the recording logic without physically pressing keys.
Where does ShortcutManager.swift store its configuration?
The class reads all settings from AppPreferences.shared, which persists user choices for modifier keys, mouse buttons, and hold-to-record preferences. When debugging, verify that AppPreferences returns expected values at lines 75-78, and ensure the hotkeySettingsChanged notification posts correctly after preference changes to trigger setupRecordingTrigger() reconfiguration.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →