Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 28 additions & 10 deletions main.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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.
Expand All @@ -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");
}

Expand Down
20 changes: 18 additions & 2 deletions scripts/run-electron.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion src/helpers/clipboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 = [
Expand Down
38 changes: 31 additions & 7 deletions src/helpers/hotkeyManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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() !== "") {
Expand Down
5 changes: 4 additions & 1 deletion src/helpers/kdeShortcut.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"checkJs": false,
"esModuleInterop": true,
"skipLibCheck": true,
"typeRoots": ["../node_modules/@types"],
"types": ["react", "react-dom"],
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
Expand Down
Loading