From 063318fc432eba27a133af0bc63b637293c939f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 Aug 2025 00:17:14 +0000 Subject: [PATCH 1/3] Initial plan From 5f92c970ac572e3512d028df62d75f3d71169729 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 Aug 2025 00:26:24 +0000 Subject: [PATCH 2/3] Implement shortcut customization functionality Co-authored-by: hellotaotao <1796860+hellotaotao@users.noreply.github.com> --- src/main.js | 196 +++++++++++++-------- src/views/settings.html | 379 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 500 insertions(+), 75 deletions(-) diff --git a/src/main.js b/src/main.js index 6c3577a..83a3421 100644 --- a/src/main.js +++ b/src/main.js @@ -64,10 +64,46 @@ let tray; let hookStarted = false; // Track if hook is started let accessibilityWatchdog = null; // Low-frequency permission watchdog (macOS only) -// Key state tracking for hotkey combination -let ctrlPressed = false; -let shiftPressed = false; -let altPressed = false; +// Helper function to map key names to UiohookKey values +function getUiohookKey(keyName) { + const keyMap = { + 'Ctrl': [UiohookKey.Ctrl, UiohookKey.CtrlR], + 'Shift': [UiohookKey.Shift, UiohookKey.ShiftR], + 'Alt': [UiohookKey.Alt, UiohookKey.AltR], + 'Cmd': [UiohookKey.Cmd, UiohookKey.CmdR], + 'Meta': [UiohookKey.Cmd, UiohookKey.CmdR], // Alias for Cmd + 'Space': [UiohookKey.Space], + 'Tab': [UiohookKey.Tab], + 'Enter': [UiohookKey.Enter], + 'Escape': [UiohookKey.Escape], + 'Backspace': [UiohookKey.Backspace], + 'Delete': [UiohookKey.Delete], + 'F1': [UiohookKey.F1], + 'F2': [UiohookKey.F2], + 'F3': [UiohookKey.F3], + 'F4': [UiohookKey.F4], + 'F5': [UiohookKey.F5], + 'F6': [UiohookKey.F6], + 'F7': [UiohookKey.F7], + 'F8': [UiohookKey.F8], + 'F9': [UiohookKey.F9], + 'F10': [UiohookKey.F10], + 'F11': [UiohookKey.F11], + 'F12': [UiohookKey.F12], + }; + return keyMap[keyName] || []; +} + +// Helper function to check if a shortcut combination is currently pressed +function isShortcutPressed(shortcutKeys, pressedKeys) { + return shortcutKeys.every(keyName => { + const uiohookKeys = getUiohookKey(keyName); + return uiohookKeys.some(uiohookKey => pressedKeys.has(uiohookKey)); + }); +} + +// Key state tracking for custom hotkey combinations +let pressedKeys = new Set(); let isRecording = false; // Set up permission manager event listeners @@ -292,54 +328,47 @@ async function setupGlobalHotkeys() { // Register keyboard event listeners (with defensive try/catch) uIOhook.on("keydown", (e) => { try { - // Ctrl key (left or right) - if (e.keycode === UiohookKey.Ctrl || e.keycode === UiohookKey.CtrlR) { - ctrlPressed = true; - } - // Shift key (left or right) - if (e.keycode === UiohookKey.Shift || e.keycode === UiohookKey.ShiftR) { - shiftPressed = true; - } - // Alt key (left or right) - if (e.keycode === UiohookKey.Alt || e.keycode === UiohookKey.AltR) { - altPressed = true; - } - - // Start recording when Ctrl+Shift OR Shift+Alt are pressed - if (!isRecording) { - let shouldStartRecording = false; - let translateMode = false; - - // Ctrl+Shift for normal transcription - if (ctrlPressed && shiftPressed) { - shouldStartRecording = true; - translateMode = false; - } - // Shift+Alt for English translation - else if (shiftPressed && altPressed) { - shouldStartRecording = true; - translateMode = true; - } - - if (shouldStartRecording) { - // Check microphone permission before starting recording - permissionManager.checkAndRequestMicrophonePermission().then(hasPermission => { - if (hasPermission) { - isRecording = true; - if (inputPromptWindow) { - // Reposition to the active display before showing - positionInputPromptOnActiveDisplay(100); - inputPromptWindow.showInactive(); - inputPromptWindow.webContents.send("start-recording", translateMode); + // Track all pressed keys + pressedKeys.add(e.keycode); + + // Get current shortcuts configuration + const shortcuts = store.get("shortcuts", DEFAULT_SHORTCUTS); + + // Check if we should start recording + if (!isRecording) { + let shouldStartRecording = false; + let translateMode = false; + + // Check transcription shortcut + if (isShortcutPressed(shortcuts.transcription.keys, pressedKeys)) { + shouldStartRecording = true; + translateMode = false; + } + // Check translation shortcut + else if (isShortcutPressed(shortcuts.translation.keys, pressedKeys)) { + shouldStartRecording = true; + translateMode = true; + } + + if (shouldStartRecording) { + // Check microphone permission before starting recording + permissionManager.checkAndRequestMicrophonePermission().then(hasPermission => { + if (hasPermission) { + isRecording = true; + if (inputPromptWindow) { + // Reposition to the active display before showing + positionInputPromptOnActiveDisplay(100); + inputPromptWindow.showInactive(); + inputPromptWindow.webContents.send("start-recording", translateMode); + } + } else { + console.log("Recording cancelled due to lack of microphone permission"); } - } else { - console.log("Recording cancelled due to lack of microphone permission"); - } - }).catch(error => { - console.error("Error checking microphone permission:", error); - }); + }).catch(error => { + console.error("Error checking microphone permission:", error); + }); + } } - } } catch (handlerErr) { console.error("uIOhook keydown handler error:", handlerErr); } @@ -347,24 +376,20 @@ async function setupGlobalHotkeys() { uIOhook.on("keyup", (e) => { try { - // Ctrl key released - if (e.keycode === UiohookKey.Ctrl || e.keycode === UiohookKey.CtrlR) { - ctrlPressed = false; - } - // Shift key released - if (e.keycode === UiohookKey.Shift || e.keycode === UiohookKey.ShiftR) { - shiftPressed = false; - } - // Alt key released - if (e.keycode === UiohookKey.Alt || e.keycode === UiohookKey.AltR) { - altPressed = false; - } - - // Stop recording when neither Ctrl+Shift nor Shift+Alt is pressed - if (isRecording && !( (ctrlPressed && shiftPressed) || (shiftPressed && altPressed) )) { - isRecording = false; - inputPromptWindow?.webContents.send("stop-recording"); - } + // Remove key from pressed keys set + pressedKeys.delete(e.keycode); + + // Stop recording when neither shortcut combination is pressed + if (isRecording) { + const shortcuts = store.get("shortcuts", DEFAULT_SHORTCUTS); + const transcriptionPressed = isShortcutPressed(shortcuts.transcription.keys, pressedKeys); + const translationPressed = isShortcutPressed(shortcuts.translation.keys, pressedKeys); + + if (!transcriptionPressed && !translationPressed) { + isRecording = false; + inputPromptWindow?.webContents.send("stop-recording"); + } + } } catch (handlerErr) { console.error("uIOhook keyup handler error:", handlerErr); } @@ -447,6 +472,9 @@ function stopGlobalHotkeys() { try { console.log("Stopping global hotkey listener..."); + // Clear pressed keys state + pressedKeys.clear(); + // Remove all listeners first uIOhook.removeAllListeners(); @@ -467,6 +495,9 @@ function stopGlobalHotkeys() { console.error("Failed to stop global hotkeys:", error); hookStarted = false; + // Clear pressed keys state even on failure + pressedKeys.clear(); + // Clear watchdog even on failure path if (accessibilityWatchdog) { clearInterval(accessibilityWatchdog); @@ -669,13 +700,27 @@ process.on("SIGTERM", () => { process.exit(0); }); +// Default shortcut configurations +const DEFAULT_SHORTCUTS = { + transcription: { + keys: ['Ctrl', 'Shift'], + label: 'Ctrl+Shift' + }, + translation: { + keys: ['Shift', 'Alt'], + label: 'Shift+Alt' + } +}; + // IPC handlers ipcMain.handle("get-settings", () => { + const shortcuts = store.get("shortcuts", DEFAULT_SHORTCUTS); return { apiKey: store.get("apiKey", ""), apiKeyGroq: store.get("apiKeyGroq", store.get("apiKey", "")), apiKeyOpenAI: store.get("apiKeyOpenAI", ""), - shortcut: "Ctrl+Shift (hold down)", // Fixed hotkey, not customizable + shortcut: `${shortcuts.transcription.label} (transcription), ${shortcuts.translation.label} (translation)`, + shortcuts: shortcuts, language: store.get("language", "auto"), model: store.get("model", "whisper-large-v3-turbo"), microphone: store.get("microphone", "default"), @@ -703,6 +748,16 @@ ipcMain.handle("save-settings", async (event, settings) => { store.set("startMinimized", settings.startMinimized); store.set("provider", settings.provider || "groq"); + // Store shortcuts if provided + if (settings.shortcuts) { + store.set("shortcuts", settings.shortcuts); + console.log("Shortcuts updated:", settings.shortcuts); + + // Restart hotkeys with new shortcuts + await stopGlobalHotkeys(); + await setupGlobalHotkeys(); + } + // Clear transcription service cache when settings change (especially API keys) clearTranscriptionServiceCache(); @@ -719,9 +774,6 @@ ipcMain.handle("save-settings", async (event, settings) => { console.error("Failed to update auto-launch setting:", error); } - // Note: uiohook doesn't need re-registration like globalShortcut - // The hotkey combination is hardcoded to Ctrl+Shift - return true; }); diff --git a/src/views/settings.html b/src/views/settings.html index 3ed120b..c4014b6 100644 --- a/src/views/settings.html +++ b/src/views/settings.html @@ -231,6 +231,156 @@ color: #856404; } .hidden { display: none; } + + /* Shortcut Modal Styles */ + .modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + } + + .modal.active { + display: flex; + align-items: center; + justify-content: center; + } + + .modal-content { + background: white; + padding: 30px; + border-radius: 12px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + width: 500px; + max-width: 90vw; + } + + .modal-header { + margin-bottom: 20px; + } + + .modal-title { + font-size: 20px; + font-weight: 600; + color: #333; + margin-bottom: 8px; + } + + .modal-description { + font-size: 14px; + color: #666; + } + + .shortcut-recorder { + background: #f8f9fa; + border: 2px solid #e9ecef; + border-radius: 8px; + padding: 20px; + margin: 15px 0; + text-align: center; + } + + .shortcut-recorder.recording { + border-color: #1976d2; + background: #e3f2fd; + } + + .shortcut-label { + font-weight: 500; + margin-bottom: 10px; + color: #333; + } + + .shortcut-display { + font-family: monospace; + font-size: 16px; + background: white; + border: 1px solid #ddd; + border-radius: 4px; + padding: 8px 12px; + margin: 8px 0; + min-height: 20px; + color: #333; + } + + .shortcut-display.empty { + color: #999; + font-style: italic; + } + + .modal-actions { + display: flex; + gap: 12px; + justify-content: flex-end; + margin-top: 20px; + } + + .btn-record { + background: #28a745; + color: white; + } + + .btn-record:hover { + background: #218838; + } + + .btn-record.recording { + background: #dc3545; + } + + .btn-record.recording:hover { + background: #c82333; + } + + /* Switch styles for checkboxes */ + .switch { + position: relative; + display: inline-block; + width: 50px; + height: 24px; + } + + .switch input { + opacity: 0; + width: 0; + height: 0; + } + + .slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: 0.3s; + border-radius: 24px; + } + + .slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + transition: 0.3s; + border-radius: 50%; + } + + input:checked + .slider { + background-color: #1976d2; + } + + input:checked + .slider:before { + transform: translateX(26px); + }
@@ -253,14 +403,20 @@