From 498ead9f22d3adac6d5b0215994e0db620a81a1f Mon Sep 17 00:00:00 2001 From: tomide Date: Thu, 30 Jul 2026 14:46:03 +0200 Subject: [PATCH] add --hotkey option to pick the push-to-talk key The README documents --hotkey right-option but the code hardwired fn. Right option matches keycode 61 so the left key stays free for typing. --- Sources/parrot/Input/HotkeyMonitor.swift | 7 ++++++- Sources/parrot/Parrot.swift | 16 ++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/Sources/parrot/Input/HotkeyMonitor.swift b/Sources/parrot/Input/HotkeyMonitor.swift index b3bc132c..044b2877 100644 --- a/Sources/parrot/Input/HotkeyMonitor.swift +++ b/Sources/parrot/Input/HotkeyMonitor.swift @@ -12,14 +12,18 @@ final class HotkeyMonitor { /// Mask of the modifier we treat as the hotkey. Fn = `.maskSecondaryFn`. private let mask: CGEventFlags + /// When set, only flagsChanged events with this keycode are considered, + /// so left/right variants of a modifier can be told apart. + private let keycode: Int64? private let debug: Bool private var onEvent: ((Event) -> Void)? private var tap: CFMachPort? private var runLoopSource: CFRunLoopSource? private var isPressed = false - init(mask: CGEventFlags = .maskSecondaryFn, debug: Bool = false) { + init(mask: CGEventFlags = .maskSecondaryFn, keycode: Int64? = nil, debug: Bool = false) { self.mask = mask + self.keycode = keycode self.debug = debug } @@ -87,6 +91,7 @@ final class HotkeyMonitor { )) } guard type == .flagsChanged else { return } + if let keycode, event.getIntegerValueField(.keyboardEventKeycode) != keycode { return } let pressed = event.flags.contains(mask) guard pressed != isPressed else { return } isPressed = pressed diff --git a/Sources/parrot/Parrot.swift b/Sources/parrot/Parrot.swift index 05a69ebe..90acdfc9 100644 --- a/Sources/parrot/Parrot.swift +++ b/Sources/parrot/Parrot.swift @@ -34,6 +34,9 @@ struct Run: ParsableCommand { @Option(name: .long, help: "Model id to use. Defaults to the recommended model.") var model: String? + @Option(name: .long, help: "Push-to-talk key: fn (default) or right-option.") + var hotkey: String = "fn" + func run() throws { if !skipDoctor { let checks = DoctorReport.run() @@ -81,7 +84,16 @@ struct Run: ParsableCommand { let app = NSApplication.shared app.setActivationPolicy(.accessory) - let monitor = HotkeyMonitor(debug: debugHotkey) + let monitor: HotkeyMonitor + switch hotkey { + case "fn": + monitor = HotkeyMonitor(debug: debugHotkey) + case "right-option": + monitor = HotkeyMonitor(mask: .maskAlternate, keycode: 61, debug: debugHotkey) + default: + FileHandle.standardError.write(Data("unknown hotkey: \(hotkey) (use fn or right-option)\n".utf8)) + throw ExitCode(1) + } let capture = AudioCapture() let dumpWav = self.dumpWav let overlay: RecordingOverlay? = noOverlay ? nil : MainActor.assumeIsolated { RecordingOverlay() } @@ -169,7 +181,7 @@ struct Run: ParsableCommand { sigint.resume() signal(SIGINT, SIG_IGN) - FileHandle.standardError.write(Data("listening on fn hold · model: \(chosenModel.id) · ^C to quit\n".utf8)) + FileHandle.standardError.write(Data("listening on \(hotkey) hold · model: \(chosenModel.id) · ^C to quit\n".utf8)) app.run() } }