diff --git a/main.js b/main.js index e7a5f2fd1..a573db906 100644 --- a/main.js +++ b/main.js @@ -1,3 +1,20 @@ +// KDE Wayland: force XWayland so globalShortcut works via X11. +// Must be a command-line flag because Chromium picks the display backend +// before main.js can call appendSwitch. +if ( + process.platform === "linux" && + process.env.XDG_SESSION_TYPE === "wayland" && + /kde/i.test(process.env.XDG_CURRENT_DESKTOP || "") && + !process.argv.includes("--ozone-platform=x11") +) { + const { spawn } = require("child_process"); + spawn(process.execPath, [...process.argv.slice(1), "--ozone-platform=x11"], { + stdio: "inherit", + detached: true, + }).unref(); + process.exit(0); +} + const { app, globalShortcut, @@ -62,6 +79,14 @@ function configureChannelUserDataPath() { configureChannelUserDataPath(); +// Load userData .env (contains DICTATION_KEY, API keys, etc.) so they're +// available early — before hotkey registration, which needs DICTATION_KEY +// before the renderer page finishes loading. +require("dotenv").config({ + path: path.join(app.getPath("userData"), ".env"), + override: false, +}); + // Fix transparent window flickering on Linux: --enable-transparent-visuals requires // the compositor to set up an ARGB visual before any windows are created. // --disable-gpu-compositing prevents GPU compositing conflicts with the compositor. @@ -75,17 +100,10 @@ if (process.platform === "win32") { app.commandLine.appendSwitch("disable-gpu-compositing"); } -// Enable native Wayland support: Ozone platform for native rendering. -// KDE uses XWayland for reliable clipboard access, with KGlobalAccel D-Bus -// for global shortcuts. GNOME and Hyprland run native Wayland with their -// own D-Bus shortcut managers. +// Enable native Wayland support for non-KDE desktops (GNOME, Hyprland, etc.). +// KDE is handled by the self-relaunch guard above (--ozone-platform=x11). if (process.platform === "linux" && process.env.XDG_SESSION_TYPE === "wayland") { - const desktop = (process.env.XDG_CURRENT_DESKTOP || "").toLowerCase(); - if (desktop.includes("kde")) { - app.commandLine.appendSwitch("ozone-platform-hint", "x11"); - } else { - app.commandLine.appendSwitch("ozone-platform-hint", "auto"); - } + app.commandLine.appendSwitch("ozone-platform-hint", "auto"); app.commandLine.appendSwitch("enable-features", "UseOzonePlatform,WaylandWindowDecorations"); } diff --git a/scripts/run-electron.js b/scripts/run-electron.js index 8bb7ae72e..b730f7d12 100644 --- a/scripts/run-electron.js +++ b/scripts/run-electron.js @@ -25,8 +25,24 @@ console.log("[run-electron] Electron path:", electronPath); console.log("[run-electron] App dir:", appDir); console.log("[run-electron] Args:", args); -// Spawn electron with the cleaned environment -const child = spawn(electronPath, [appDir, ...args], { +// On KDE Wayland, force XWayland so globalShortcut works via X11. +// This must be a command-line flag because Chromium picks the display +// backend before main.js runs. Adding it here avoids the self-relaunch +// in main.js which kills concurrently in dev mode. +if ( + process.platform === "linux" && + process.env.XDG_SESSION_TYPE === "wayland" && + /kde/i.test(process.env.XDG_CURRENT_DESKTOP || "") && + !args.includes("--ozone-platform=x11") +) { + args.push("--ozone-platform=x11"); + console.log("[run-electron] KDE Wayland detected, forcing XWayland"); +} + +// Chromium flags must come before the app path, app args after. +const chromiumFlags = args.filter((a) => a.startsWith("--ozone-platform=")); +const appArgs = args.filter((a) => !a.startsWith("--ozone-platform=")); +const child = spawn(electronPath, [...chromiumFlags, appDir, ...appArgs], { stdio: "inherit", env: process.env, cwd: appDir, diff --git a/src/helpers/clipboard.js b/src/helpers/clipboard.js index c436d76f2..04bc07fac 100644 --- a/src/helpers/clipboard.js +++ b/src/helpers/clipboard.js @@ -57,6 +57,8 @@ const RESTORE_DELAYS = { win32_nircmd: 80, win32_pwsh: 80, linux: 200, + // KDE Wayland + XWayland: clipboard bridge is slow, needs more time + linux_kde_wayland: 600, }; function writeClipboardInRenderer(webContents, text) { @@ -1029,13 +1031,14 @@ class ClipboardManager { const restoreClipboard = () => { if (originalClipboard == null) return; + const delay = isKde && isWayland ? RESTORE_DELAYS.linux_kde_wayland : RESTORE_DELAYS.linux; setTimeout(() => { if (isWayland) { this._writeClipboardWayland(originalClipboard, webContents); } else { clipboard.writeText(originalClipboard); } - }, RESTORE_DELAYS.linux); + }, delay); }; const terminalClasses = [ diff --git a/src/helpers/hotkeyManager.js b/src/helpers/hotkeyManager.js index 3409d600d..3e50d90da 100644 --- a/src/helpers/hotkeyManager.js +++ b/src/helpers/hotkeyManager.js @@ -559,7 +559,8 @@ class HotkeyManager { this.mainWindow = mainWindow; this.hotkeyCallback = callback; - if (process.platform === "linux" && GnomeShortcutManager.isWayland()) { + const isXWayland = process.argv.includes("--ozone-platform=x11"); + if (process.platform === "linux" && GnomeShortcutManager.isWayland() && !isXWayland) { const gnomeOk = await this.initializeGnomeShortcuts(callback); if (gnomeOk) { @@ -679,9 +680,25 @@ class HotkeyManager { globalShortcut.unregisterAll(); } - setTimeout(() => { - this.loadSavedHotkeyOrDefault(mainWindow, callback); - }, HOTKEY_REGISTRATION_DELAY_MS); + // Register hotkey immediately from env var (no need to wait for page load). + // Fall back to waiting for page if env var is empty (need localStorage). + const envHotkey = process.env.DICTATION_KEY || ""; + if (envHotkey) { + const result = this.setupShortcuts(envHotkey, callback); + if (result.success) { + debugLogger.log(`[HotkeyManager] Hotkey "${envHotkey}" registered from env`); + } else { + debugLogger.log(`[HotkeyManager] Env hotkey "${envHotkey}" failed, waiting for page`); + this.loadSavedHotkeyOrDefault(mainWindow, callback); + } + } else { + const loadHotkey = () => this.loadSavedHotkeyOrDefault(mainWindow, callback); + if (mainWindow.webContents.isLoading()) { + mainWindow.webContents.once("did-finish-load", loadHotkey); + } else { + loadHotkey(); + } + } this.isInitialized = true; } @@ -690,12 +707,19 @@ class HotkeyManager { try { // First check file-based storage (environment variable) - more reliable let savedHotkey = process.env.DICTATION_KEY || ""; + debugLogger.log(`[HotkeyManager] DICTATION_KEY from env: "${savedHotkey}"`); // Fall back to localStorage if env var is empty if (!savedHotkey) { - savedHotkey = await mainWindow.webContents.executeJavaScript(` - localStorage.getItem("dictationKey") || "" - `); + try { + savedHotkey = await mainWindow.webContents.executeJavaScript(` + localStorage.getItem("dictationKey") || "" + `); + debugLogger.log(`[HotkeyManager] dictationKey from localStorage: "${savedHotkey}"`); + } catch (jsErr) { + debugLogger.log(`[HotkeyManager] executeJavaScript failed: ${jsErr.message}`); + savedHotkey = ""; + } // If we found a hotkey in localStorage but not in env, migrate it to .env file if (savedHotkey && savedHotkey.trim() !== "") { diff --git a/src/helpers/kdeShortcut.js b/src/helpers/kdeShortcut.js index e1d511030..6b5280890 100644 --- a/src/helpers/kdeShortcut.js +++ b/src/helpers/kdeShortcut.js @@ -222,7 +222,10 @@ class KDEShortcutManager { try { // Register action then set shortcut await this.kglobalaccel.doRegister(actionId); - const result = await this.kglobalaccel.setShortcut(actionId, [qtKey], 0x02); + // Flag 0: always set the provided shortcut (overwrite any saved value). + // Flag 0x02 would keep existing shortcuts, which causes the hotkey to + // silently not work on startup if KGlobalAccel has a stale saved value. + const result = await this.kglobalaccel.setShortcut(actionId, [qtKey], 0); if (callback) this.callbacks.set(slotName, callback); this.registeredSlots.add(slotName); diff --git a/src/tsconfig.json b/src/tsconfig.json index 84f8984eb..baa91e676 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -11,6 +11,8 @@ "checkJs": false, "esModuleInterop": true, "skipLibCheck": true, + "typeRoots": ["../node_modules/@types"], + "types": ["react", "react-dom"], "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "isolatedModules": true,