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, and IndicatorWindowManager.

  • KeyboardShortcuts: A third-party library (declared in Package.swift) that watches for the named shortcut .toggleRecord and delivers callbacks on key down and key up events. This is configured in setupKeyboardShortcuts() 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) and handleKeyUp() to show and hide the recording interface.

  • AppPreferences: Central storage for user settings including holdToRecord, modifierOnlyHotkey, and mouseButtonHotkey. Read in setupRecordingTrigger() at lines 75-78 and within handleKeyDown/Up at line 26.

Execution Flow from User Input to Recording

The typical execution path through ShortcutManager.swift follows six distinct stages:

  1. Singleton Initialization: ShortcutManager.shared is lazily instantiated once, logging "ShortcutManager init" to confirm activation.

  2. Keyboard Registration: setupKeyboardShortcuts() configures KeyboardShortcuts.onKeyDown and onKeyUp for the .toggleRecord identifier.

  3. Trigger Mode Selection: setupRecordingTrigger() reads AppPreferences.shared and 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.

  4. Key Down Handling: handleKeyDown() creates an IndicatorWindowManager view model if none exists, captures the current caret/cursor position, and initiates recording. If hold-to-record is enabled, it queues a DispatchWorkItem to set holdMode after the holdThreshold (0.3 seconds).

  5. Key Up Handling: handleKeyUp() cancels any pending holdWorkItem. 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 holdWorkItem DispatchWorkItem and holdMode state transitions in handleKeyDown() and handleKeyUp().
  • Identify active trigger modes by logging useMouseButtonHotkey and useModifierOnlyHotkey flags at the start of setupRecordingTrigger().
  • Fix indicator window issues by validating that IndicatorWindowManager.shared.show() returns a non-nil view model and that FocusUtils.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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →