Customize HopTab’s app switcher and profile switcher shortcuts
HopTab provides configurable keyboard shortcuts for both the app switcher and profile switcher. You can choose from preset combinations or define custom shortcuts.
By default, the profile switcher uses Option + \`` (backtick). However, if you configure the app switcher to use Option + `, the profile switcher automatically falls back to Control + “ to avoid conflicts.This logic is implemented in AppState.swift:130-159:
private func applyProfileShortcut() { let modFlag: CGEventFlags let keyCode: Int64 let modName: String let keyName: String if isCustomProfileShortcut, let c = customProfileShortcut { modFlag = c.modifierFlags keyCode = c.keyCode modName = c.modifierName keyName = c.keyName } else { // Auto-fallback if app shortcut conflicts if appShortcutSelection.modifierFlags == .maskAlternate && appShortcutSelection.keyCode == Int64(kVK_ANSI_Grave) { modFlag = .maskControl keyCode = Int64(kVK_ANSI_Grave) modName = "Control" } else { modFlag = .maskAlternate keyCode = Int64(kVK_ANSI_Grave) modName = "Option" } keyName = "`" } hotkeyService.configureProfileShortcut(modifierFlag: modFlag, keyCode: keyCode) profileShortcutModifierName = modName profileShortcutKeyName = keyName checkConflicts()}
The automatic fallback prevents you from accidentally configuring identical shortcuts for both switchers.
The ShortcutSelection enum (ShortcutConfig.swift:135-157) handles persistence:
static var current: ShortcutSelection { get { let mode = UserDefaults.standard.string(forKey: modeKey) ?? "preset" if mode == "custom", let data = UserDefaults.standard.data(forKey: customDataKey), let custom = try? JSONDecoder().decode(CustomShortcut.self, from: data) { return .custom(custom) } return .preset(ShortcutPreset.current) } set { switch newValue { case .preset(let p): UserDefaults.standard.set("preset", forKey: modeKey) ShortcutPreset.current = p case .custom(let c): UserDefaults.standard.set("custom", forKey: modeKey) if let data = try? JSONEncoder().encode(c) { UserDefaults.standard.set(data, forKey: customDataKey) } } }}
Profile shortcuts are stored separately (ShortcutConfig.swift:168-180):
static var savedProfileShortcut: CustomShortcut? { get { guard let data = UserDefaults.standard.data(forKey: profileDataKey) else { return nil } return try? JSONDecoder().decode(CustomShortcut.self, from: data) } set { if let c = newValue, let data = try? JSONEncoder().encode(c) { UserDefaults.standard.set(data, forKey: profileDataKey) } else { UserDefaults.standard.removeObject(forKey: profileDataKey) } }}
HopTab uses a CGEvent tap to intercept keyboard events globally. When you press keys, the hotkeyCallback function (HotkeyService.swift:248-260) is invoked:
private func hotkeyCallback( proxy: CGEventTapProxy, type: CGEventType, event: CGEvent, userInfo: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? { guard let userInfo else { return Unmanaged.passUnretained(event) } let service = Unmanaged<HotkeyService>.fromOpaque(userInfo).takeUnretainedValue() if let result = service.handleEvent(type: type, event: event) { return Unmanaged.passUnretained(result) } return nil // swallow}
HopTab tracks modifier key state separately for the app switcher and profile switcher (HotkeyService.swift:29-32):
private(set) var isModifierHeld = falseprivate(set) var isSwitcherActive = falseprivate(set) var isProfileModifierHeld = falseprivate(set) var isProfileSwitcherActive = false
When a flagsChanged event occurs, the service updates these flags (HotkeyService.swift:139-164):
if type == .flagsChanged { // App switcher modifier tracking let appModDown = flags.contains(modifierFlag) if appModDown && !isModifierHeld { isModifierHeld = true } else if !appModDown && isModifierHeld { isModifierHeld = false if isSwitcherActive { isSwitcherActive = false onSwitcherDismissed?() return nil } } // Profile switcher modifier tracking let profileModDown = flags.contains(profileModifierFlag) if profileModDown && !isProfileModifierHeld { isProfileModifierHeld = true } else if !profileModDown && isProfileModifierHeld { isProfileModifierHeld = false if isProfileSwitcherActive { isProfileSwitcherActive = false onProfileSwitcherDismissed?() return nil } } return event}
This allows the “release to activate” behavior — when you release the modifier key, the switcher dismisses and activates the selected app or profile.