diff --git a/apps/desktop/forge.config.ts b/apps/desktop/forge.config.ts index 5fddb37..0d4c49e 100644 --- a/apps/desktop/forge.config.ts +++ b/apps/desktop/forge.config.ts @@ -175,7 +175,10 @@ const config: ForgeConfig = { // Read where the symlink points to const symlinkTarget = readlinkSync(localDepPath); - const absoluteTarget = join(localDepPath, "..", symlinkTarget); + let absoluteTarget = symlinkTarget; + if (process.platform !== "win32") { + absoluteTarget = join(localDepPath, "..", symlinkTarget); + } const sourcePath = normalize(absoluteTarget); console.log(` Symlink points to: ${sourcePath}`); diff --git a/apps/desktop/scripts/download-node-binaries.js b/apps/desktop/scripts/download-node-binaries.js deleted file mode 100644 index 56c22e9..0000000 --- a/apps/desktop/scripts/download-node-binaries.js +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env node - -const https = require('https'); -const fs = require('fs'); -const path = require('path'); -const { execSync } = require('child_process'); -const { createWriteStream, mkdirSync, chmodSync } = fs; - -// Node.js version to download -const NODE_VERSION = '24.4.0'; - -// Platform configurations -const PLATFORMS = [ - { - platform: 'darwin', - arch: 'arm64', - url: `https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-darwin-arm64.tar.gz`, - binary: 'bin/node' - }, - { - platform: 'darwin', - arch: 'x64', - url: `https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-darwin-x64.tar.gz`, - binary: 'bin/node' - }, - { - platform: 'win32', - arch: 'x64', - url: `https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-win-x64.zip`, - binary: 'node.exe' - }, - { - platform: 'linux', - arch: 'x64', - url: `https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.gz`, - binary: 'bin/node' - } -]; - -// Base directory for binaries -const RESOURCES_DIR = path.join(__dirname, '..', 'node-binaries'); - -async function downloadFile(url, dest) { - return new Promise((resolve, reject) => { - const file = createWriteStream(dest); - - https.get(url, (response) => { - if (response.statusCode === 302 || response.statusCode === 301) { - // Handle redirect - https.get(response.headers.location, (redirectResponse) => { - redirectResponse.pipe(file); - file.on('finish', () => { - file.close(resolve); - }); - }).on('error', reject); - } else { - response.pipe(file); - file.on('finish', () => { - file.close(resolve); - }); - } - }).on('error', reject); - }); -} - -async function extractArchive(archivePath, platform) { - const tempDir = path.join(path.dirname(archivePath), 'temp'); - mkdirSync(tempDir, { recursive: true }); - - if (platform === 'win32') { - // Use unzip command (available on macOS) to extract zip files - execSync(`unzip -q "${archivePath}" -d "${tempDir}"`, { stdio: 'inherit' }); - } else { - // Use tar for Unix-like systems - execSync(`tar -xzf "${archivePath}" -C "${tempDir}"`, { stdio: 'inherit' }); - } - - return tempDir; -} - -async function downloadNodeBinary(config) { - const { platform, arch, url, binary } = config; - const platformDir = path.join(RESOURCES_DIR, `${platform}-${arch}`); - const binaryPath = path.join(platformDir, platform === 'win32' ? 'node.exe' : 'node'); - - // Skip if already exists - if (fs.existsSync(binaryPath)) { - console.log(`✓ ${platform}-${arch} binary already exists`); - return; - } - - console.log(`Downloading Node.js for ${platform}-${arch}...`); - - // Create directory - mkdirSync(platformDir, { recursive: true }); - - // Download archive - const archiveExt = platform === 'win32' ? '.zip' : '.tar.gz'; - const archivePath = path.join(platformDir, `node-v${NODE_VERSION}${archiveExt}`); - - try { - await downloadFile(url, archivePath); - console.log(`Downloaded archive for ${platform}-${arch}`); - - // Extract archive - const tempDir = await extractArchive(archivePath, platform); - - // Find the node binary in extracted files - // Windows uses different directory naming convention (win instead of win32) - const extractedDirName = platform === 'win32' - ? `node-v${NODE_VERSION}-win-${arch}` - : `node-v${NODE_VERSION}-${platform}-${arch}`; - const extractedBinaryPath = path.join(tempDir, extractedDirName, binary); - - // Copy binary to final location - fs.copyFileSync(extractedBinaryPath, binaryPath); - - // Make executable on Unix-like systems - if (platform !== 'win32') { - chmodSync(binaryPath, '755'); - } - - // Clean up - fs.rmSync(tempDir, { recursive: true, force: true }); - fs.unlinkSync(archivePath); - - console.log(`✓ Successfully installed ${platform}-${arch} binary`); - } catch (error) { - console.error(`✗ Failed to download ${platform}-${arch}:`, error.message); - // Clean up on failure - if (fs.existsSync(archivePath)) { - fs.unlinkSync(archivePath); - } - } -} - -async function main() { - console.log(`Downloading Node.js v${NODE_VERSION} binaries for all platforms...\n`); - - // Create base directory - mkdirSync(RESOURCES_DIR, { recursive: true }); - - // Download binaries for all platforms - for (const platform of PLATFORMS) { - await downloadNodeBinary(platform); - } - - console.log('\nDone! Node.js binaries downloaded to:', RESOURCES_DIR); -} - -// Run if called directly -if (require.main === module) { - main().catch(console.error); -} - -module.exports = { downloadNodeBinary, PLATFORMS, NODE_VERSION }; \ No newline at end of file diff --git a/apps/desktop/scripts/download-node-binaries.ts b/apps/desktop/scripts/download-node-binaries.ts index 9adb632..ba4a07f 100644 --- a/apps/desktop/scripts/download-node-binaries.ts +++ b/apps/desktop/scripts/download-node-binaries.ts @@ -142,7 +142,10 @@ async function extractArchive( if (platform === "win32") { // Use unzip command (available on macOS) to extract zip files - execSync(`unzip -q "${archivePath}" -d "${tempDir}"`, { stdio: "inherit" }); + execSync( + `powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${tempDir}' -Force"`, + { stdio: "inherit" }, + ); } else { // Use tar for Unix-like systems execSync(`tar -xzf "${archivePath}" -C "${tempDir}"`, { stdio: "inherit" }); diff --git a/apps/desktop/src/components/shortcut-input.tsx b/apps/desktop/src/components/shortcut-input.tsx index 1eec8cc..3da3d88 100644 --- a/apps/desktop/src/components/shortcut-input.tsx +++ b/apps/desktop/src/components/shortcut-input.tsx @@ -12,7 +12,7 @@ interface ShortcutInputProps { onRecordingShortcutChange: (recording: boolean) => void; } -const MODIFIER_KEYS = ["Cmd", "Ctrl", "Alt", "Shift", "Fn"]; +const MODIFIER_KEYS = ["Cmd", "Win", "Ctrl", "Alt", "Shift", "Fn"]; const MAX_KEY_COMBINATION_LENGTH = 3; type ValidationResult = { @@ -60,7 +60,7 @@ function validateShortcut(keys: string[]): ValidationResult { return { valid: false, error: - "Multiple keys require at least one modifier (Cmd, Ctrl, Alt, Shift, or Fn)", + "Multiple keys require at least one modifier (Cmd, Win, Ctrl, Alt, Shift, or Fn)", }; } diff --git a/apps/desktop/src/db/app-settings.ts b/apps/desktop/src/db/app-settings.ts index 71a2132..70cc9ee 100644 --- a/apps/desktop/src/db/app-settings.ts +++ b/apps/desktop/src/db/app-settings.ts @@ -19,10 +19,27 @@ import { type NewAppSettings, type AppSettingsData, } from "./schema"; +import { isWindows, isMacOS } from "../utils/platform"; // Singleton ID for app settings (we only have one settings record) const SETTINGS_ID = 1; +// Platform-specific default shortcuts +const getDefaultShortcuts = () => { + if (isMacOS()) { + return { + pushToTalk: "Fn", + toggleRecording: "Fn+Space", + }; + } else { + // Windows and Linux + return { + pushToTalk: "Ctrl+Win", + toggleRecording: "Ctrl+Win+Space", + }; + } +}; + // Default settings const defaultSettings: AppSettingsData = { formatterConfig: { @@ -48,10 +65,7 @@ const defaultSettings: AppSettingsData = { silenceThreshold: 3, maxRecordingDuration: 60, }, - shortcuts: { - pushToTalk: "Fn", - toggleRecording: "", - }, + shortcuts: getDefaultShortcuts(), modelProvidersConfig: { defaultSpeechModel: "", defaultLanguageModel: "", diff --git a/apps/desktop/src/main/services/shortcut-manager.ts b/apps/desktop/src/main/services/shortcut-manager.ts index 477f141..ab18623 100644 --- a/apps/desktop/src/main/services/shortcut-manager.ts +++ b/apps/desktop/src/main/services/shortcut-manager.ts @@ -3,6 +3,7 @@ import { globalShortcut } from "electron"; import { SettingsService } from "@/services/settings-service"; import { NativeBridge } from "@/services/platform/native-bridge-service"; import { getKeyNameFromPayload } from "@/utils/keycode-map"; +import { isWindows } from "@/utils/platform"; import { KeyEventPayload, HelperEvent } from "@amical/types"; import { logger } from "@/main/logger"; @@ -89,9 +90,9 @@ export class ShortcutManager extends EventEmitter { } } - // Track modifier keys + // Track modifier keys with platform-aware names const modifiers = [ - { flag: payload.metaKey, name: "Cmd" }, + { flag: payload.metaKey, name: isWindows() ? "Win" : "Cmd" }, { flag: payload.ctrlKey, name: "Ctrl" }, { flag: payload.altKey, name: "Alt" }, { flag: payload.shiftKey, name: "Shift" }, diff --git a/apps/desktop/src/services/settings-service.ts b/apps/desktop/src/services/settings-service.ts index 330957a..4525267 100644 --- a/apps/desktop/src/services/settings-service.ts +++ b/apps/desktop/src/services/settings-service.ts @@ -7,6 +7,7 @@ import { updateAppSettings, } from "../db/app-settings"; import type { AppSettingsData } from "../db/schema"; +import { isWindows, isMacOS } from "../utils/platform"; /** * Database-backed settings service with typed configuration @@ -123,10 +124,14 @@ export class SettingsService { */ async getShortcuts(): Promise { const shortcuts = await getSettingsSection("shortcuts"); - // Return defaults if not set + // Return platform-specific defaults if not set + const defaults = isMacOS() + ? { pushToTalk: "Fn", toggleRecording: "Fn+Space" } + : { pushToTalk: "Ctrl+Win", toggleRecording: "Ctrl+Win+Space" }; + return { - pushToTalk: shortcuts?.pushToTalk || "Fn", - toggleRecording: shortcuts?.toggleRecording || "Fn+Space", + pushToTalk: shortcuts?.pushToTalk || defaults.pushToTalk, + toggleRecording: shortcuts?.toggleRecording || defaults.toggleRecording, }; } diff --git a/apps/desktop/src/utils/keycode-map.ts b/apps/desktop/src/utils/keycode-map.ts index de6334f..4a144d2 100644 --- a/apps/desktop/src/utils/keycode-map.ts +++ b/apps/desktop/src/utils/keycode-map.ts @@ -1,5 +1,7 @@ +import { isWindows } from "./platform"; + // macOS keycode mappings -export const keycodeToKey: Record = { +const macOSKeycodeToKey: Record = { // Letters 0: "A", 1: "S", @@ -81,8 +83,202 @@ export const keycodeToKey: Record = { 50: "`", }; +// Windows Virtual Key code mappings +const windowsVKToKey: Record = { + // Mouse buttons (0x01-0x06) + 0x01: "LButton", + 0x02: "RButton", + 0x04: "MButton", + 0x05: "XButton1", + 0x06: "XButton2", + + // Standard keys + 0x08: "Backspace", + 0x09: "Tab", + 0x0c: "Clear", + 0x0d: "Enter", + 0x10: "Shift", + 0x11: "Ctrl", + 0x12: "Alt", + 0x13: "Pause", + 0x14: "CapsLock", + 0x1b: "Escape", + 0x20: "Space", + 0x21: "PageUp", + 0x22: "PageDown", + 0x23: "End", + 0x24: "Home", + 0x25: "Left", + 0x26: "Up", + 0x27: "Right", + 0x28: "Down", + 0x29: "Select", + 0x2a: "Print", + 0x2b: "Execute", + 0x2c: "PrintScreen", + 0x2d: "Insert", + 0x2e: "Delete", + 0x2f: "Help", + + // Numbers (0-9) + 0x30: "0", + 0x31: "1", + 0x32: "2", + 0x33: "3", + 0x34: "4", + 0x35: "5", + 0x36: "6", + 0x37: "7", + 0x38: "8", + 0x39: "9", + + // Letters (A-Z) + 0x41: "A", + 0x42: "B", + 0x43: "C", + 0x44: "D", + 0x45: "E", + 0x46: "F", + 0x47: "G", + 0x48: "H", + 0x49: "I", + 0x4a: "J", + 0x4b: "K", + 0x4c: "L", + 0x4d: "M", + 0x4e: "N", + 0x4f: "O", + 0x50: "P", + 0x51: "Q", + 0x52: "R", + 0x53: "S", + 0x54: "T", + 0x55: "U", + 0x56: "V", + 0x57: "W", + 0x58: "X", + 0x59: "Y", + 0x5a: "Z", + + // Windows keys + 0x5b: "LWin", + 0x5c: "RWin", + 0x5d: "Apps", + 0x5f: "Sleep", + + // Numpad + 0x60: "Numpad0", + 0x61: "Numpad1", + 0x62: "Numpad2", + 0x63: "Numpad3", + 0x64: "Numpad4", + 0x65: "Numpad5", + 0x66: "Numpad6", + 0x67: "Numpad7", + 0x68: "Numpad8", + 0x69: "Numpad9", + 0x6a: "Multiply", + 0x6b: "Add", + 0x6c: "Separator", + 0x6d: "Subtract", + 0x6e: "Decimal", + 0x6f: "Divide", + + // Function keys (F1-F24) + 0x70: "F1", + 0x71: "F2", + 0x72: "F3", + 0x73: "F4", + 0x74: "F5", + 0x75: "F6", + 0x76: "F7", + 0x77: "F8", + 0x78: "F9", + 0x79: "F10", + 0x7a: "F11", + 0x7b: "F12", + 0x7c: "F13", + 0x7d: "F14", + 0x7e: "F15", + 0x7f: "F16", + 0x80: "F17", + 0x81: "F18", + 0x82: "F19", + 0x83: "F20", + 0x84: "F21", + 0x85: "F22", + 0x86: "F23", + 0x87: "F24", + + // Other keys + 0x90: "NumLock", + 0x91: "ScrollLock", + 0xa0: "LShift", + 0xa1: "RShift", + 0xa2: "LCtrl", + 0xa3: "RCtrl", + 0xa4: "LAlt", + 0xa5: "RAlt", + + // Browser control keys + 0xa6: "BrowserBack", + 0xa7: "BrowserForward", + 0xa8: "BrowserRefresh", + 0xa9: "BrowserStop", + 0xaa: "BrowserSearch", + 0xab: "BrowserFavorites", + 0xac: "BrowserHome", + + // Volume control keys + 0xad: "VolumeMute", + 0xae: "VolumeDown", + 0xaf: "VolumeUp", + + // Media control keys + 0xb0: "MediaNextTrack", + 0xb1: "MediaPrevTrack", + 0xb2: "MediaStop", + 0xb3: "MediaPlayPause", + + // Launch keys + 0xb4: "LaunchMail", + 0xb5: "LaunchMediaSelect", + 0xb6: "LaunchApp1", + 0xb7: "LaunchApp2", + + // OEM keys (punctuation and symbols) + 0xba: ";", // OEM_1 + 0xbb: "=", // OEM_PLUS + 0xbc: ",", // OEM_COMMA + 0xbd: "-", // OEM_MINUS + 0xbe: ".", // OEM_PERIOD + 0xbf: "/", // OEM_2 + 0xc0: "`", // OEM_3 + 0xdb: "[", // OEM_4 + 0xdc: "\\", // OEM_5 + 0xdd: "]", // OEM_6 + 0xde: "'", // OEM_7 + 0xdf: "OEM_8", + + // Additional keys + 0xe1: "OEM_AX", + 0xe2: "OEM_102", + 0xe3: "ICOHelp", + 0xe4: "ICO00", + 0xe5: "ProcessKey", + 0xe6: "ICOClear", + 0xe7: "Packet", +}; + +// Export the appropriate mapping based on platform +export const keycodeToKey: Record = isWindows() + ? windowsVKToKey + : macOSKeycodeToKey; + export function getKeyFromKeycode(keycode: number): string | undefined { - return keycodeToKey[keycode]; + // Use the appropriate mapping based on platform + const mapping = isWindows() ? windowsVKToKey : macOSKeycodeToKey; + return mapping[keycode]; } export function matchesShortcutKey( @@ -91,17 +287,46 @@ export function matchesShortcutKey( ): boolean { if (keycode === undefined) return false; - const mappedKey = keycodeToKey[keycode]; + const mappedKey = getKeyFromKeycode(keycode); if (!mappedKey) return false; - return mappedKey.toUpperCase() === keyName.toUpperCase(); + // Normalize Windows modifier key names for comparison + const normalizedMappedKey = normalizeKeyName(mappedKey); + const normalizedKeyName = normalizeKeyName(keyName); + + return normalizedMappedKey.toUpperCase() === normalizedKeyName.toUpperCase(); } -export function getKeyNameFromPayload(payload: any): string | undefined { +export function getKeyNameFromPayload(payload: { + key?: string; + keyCode?: number; +}): string | undefined { // Try to get key name from various sources if (payload.key) return payload.key; - if (payload.keyCode !== undefined && keycodeToKey[payload.keyCode]) { - return keycodeToKey[payload.keyCode]; + if (payload.keyCode !== undefined) { + const keyName = getKeyFromKeycode(payload.keyCode); + if (keyName) { + // Normalize key names for consistency across platforms + return normalizeKeyName(keyName); + } } return undefined; } + +// Helper function to normalize key names across platforms +function normalizeKeyName(keyName: string): string { + // Normalize left/right variants to single names + const normalizations: Record = { + LWin: "Win", + RWin: "Win", + LShift: "Shift", + RShift: "Shift", + LCtrl: "Ctrl", + RCtrl: "Ctrl", + LAlt: "Alt", + RAlt: "Alt", + // Keep other keys as-is + }; + + return normalizations[keyName] || keyName; +}