Files
prompt/Sources/Prompt/HotkeyManager.swift
T
TZGyn c074be3378 Initial commit: a Copper-style panel for prompts and photos
Keeps text prompts and photos in a floating always-on-top panel and
copies them back to the clipboard on click. Double-Shift captures the
selection from any app; sections, search, copy-as-list, merge, and
local JSON persistence mirror shadcn's Copper workflow.

💘 Generated with Crush

Assisted-by: Crush:kimi-k3
2026-10-07 20:54:49 +08:00

144 lines
5.2 KiB
Swift

import Cocoa
import Carbon
// Global shortcuts:
// Left Shift x2 — capture the current selection into the active section
// Right Shift x2 — show/hide the panel
// Cmd+Shift+C — capture (fallback, no Accessibility needed)
// Cmd+Shift+Space — show/hide the panel (fallback)
//
// The double-Shift gestures come from a CGEvent tap on flagsChanged, which
// needs Accessibility access. The Carbon hotkeys work without it.
final class HotkeyManager {
static let shared = HotkeyManager()
var onCapture: (() -> Void)?
var onTogglePanel: (() -> Void)?
private var eventTap: CFMachPort?
private var runLoopSource: CFRunLoopSource?
private var eventHandlerRef: EventHandlerRef?
private var hotKeyRefs: [EventHotKeyRef?] = []
private let signature: OSType = 0x50524D50 // "PRMP"
private var lastLeftShiftRelease: TimeInterval = 0
private var lastRightShiftRelease: TimeInterval = 0
private let doubleTapInterval: TimeInterval = 0.35
private init() {}
var isListening: Bool { eventTap != nil }
func start() {
startEventTap()
registerFallbackHotkeys()
}
// MARK: - Double-Shift event tap
private func startEventTap() {
let mask = CGEventMask(1 << CGEventType.flagsChanged.rawValue)
guard let tap = CGEvent.tapCreate(
tap: .cgSessionEventTap,
place: .headInsertEventTap,
options: .defaultTap,
eventsOfInterest: mask,
callback: hotkeyEventCallback,
userInfo: Unmanaged.passUnretained(self).toOpaque()
) else { return }
eventTap = tap
let source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
runLoopSource = source
CFRunLoopAddSource(CFRunLoopGetMain(), source, .commonModes)
CGEvent.tapEnable(tap: tap, enable: true)
}
fileprivate func handle(proxy: CGEventTapProxy, type: CGEventType, event: CGEvent) -> Unmanaged<CGEvent>? {
if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput {
if let tap = eventTap { CGEvent.tapEnable(tap: tap, enable: true) }
return Unmanaged.passUnretained(event)
}
guard type == .flagsChanged else { return Unmanaged.passUnretained(event) }
let keyCode = event.getIntegerValueField(.keyboardEventKeycode)
let shiftIsDown = event.flags.contains(.maskShift)
guard !shiftIsDown else { return Unmanaged.passUnretained(event) } // only count releases
let now = ProcessInfo.processInfo.systemUptime
switch keyCode {
case 56: // left shift
if now - lastLeftShiftRelease < doubleTapInterval {
lastLeftShiftRelease = 0
fire { $0.onCapture?() }
} else {
lastLeftShiftRelease = now
}
case 60: // right shift
if now - lastRightShiftRelease < doubleTapInterval {
lastRightShiftRelease = 0
fire { $0.onTogglePanel?() }
} else {
lastRightShiftRelease = now
}
default:
break
}
return Unmanaged.passUnretained(event)
}
private func fire(_ action: @escaping (HotkeyManager) -> Void) {
DispatchQueue.main.async { action(self) }
}
// MARK: - Carbon fallback hotkeys
private func registerFallbackHotkeys() {
var eventType = EventTypeSpec(
eventClass: OSType(kEventClassKeyboard),
eventKind: UInt32(kEventHotKeyPressed)
)
InstallEventHandler(GetApplicationEventTarget(), { _, event, _ in
var hotKeyID = EventHotKeyID()
let status = GetEventParameter(
event,
EventParamName(kEventParamDirectObject),
EventParamType(typeEventHotKeyID),
nil,
MemoryLayout<EventHotKeyID>.size,
nil,
&hotKeyID
)
guard status == noErr else { return noErr }
DispatchQueue.main.async {
switch hotKeyID.id {
case 1: HotkeyManager.shared.onTogglePanel?()
case 2: HotkeyManager.shared.onCapture?()
default: break
}
}
return noErr
}, 1, &eventType, nil, &eventHandlerRef)
registerHotkey(id: 1, keyCode: UInt32(kVK_Space), modifiers: UInt32(cmdKey | shiftKey))
registerHotkey(id: 2, keyCode: UInt32(kVK_ANSI_C), modifiers: UInt32(cmdKey | shiftKey))
}
private func registerHotkey(id: UInt32, keyCode: UInt32, modifiers: UInt32) {
var ref: EventHotKeyRef?
let hotKeyID = EventHotKeyID(signature: signature, id: id)
RegisterEventHotKey(keyCode, modifiers, hotKeyID, GetApplicationEventTarget(), 0, &ref)
hotKeyRefs.append(ref)
}
}
private func hotkeyEventCallback(
proxy: CGEventTapProxy,
type: CGEventType,
event: CGEvent,
refcon: UnsafeMutableRawPointer?
) -> Unmanaged<CGEvent>? {
guard let refcon else { return Unmanaged.passUnretained(event) }
let manager = Unmanaged<HotkeyManager>.fromOpaque(refcon).takeUnretainedValue()
return manager.handle(proxy: proxy, type: type, event: event)
}