From a2be6ed47f3b0fbb32d8671c486793cdcd6d1d3e Mon Sep 17 00:00:00 2001 From: altair823 Date: Sat, 25 Apr 2026 12:11:44 +0900 Subject: [PATCH] feat(hotkey): HotkeyService wrapping globalShortcut MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 17 of the slice plan. register() pre-checks globalShortcut.isRegistered to detect collisions with other apps (Failure #1 in spec ยง6.1) and reports the conflict in the return object so the OllamaBanner-style failure surface in Task 29 can report it. unregisterAll() runs on app quit. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/main/services/HotkeyService.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/main/services/HotkeyService.ts diff --git a/src/main/services/HotkeyService.ts b/src/main/services/HotkeyService.ts new file mode 100644 index 0000000..a7eed4d --- /dev/null +++ b/src/main/services/HotkeyService.ts @@ -0,0 +1,26 @@ +import { globalShortcut } from 'electron'; + +export interface HotkeyBinding { + accelerator: string; + onTrigger: () => void; +} + +export class HotkeyService { + private registered: string[] = []; + + register(binding: HotkeyBinding): { ok: boolean; reason?: string } { + const accel = binding.accelerator; + if (globalShortcut.isRegistered(accel)) { + return { ok: false, reason: `${accel} already registered by another app` }; + } + const ok = globalShortcut.register(accel, binding.onTrigger); + if (!ok) return { ok: false, reason: `failed to register ${accel}` }; + this.registered.push(accel); + return { ok: true }; + } + + unregisterAll(): void { + for (const a of this.registered) globalShortcut.unregister(a); + this.registered = []; + } +}