Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ WHISPER_MODEL=whisper-1
# Leave empty for auto-detection, or use language codes like 'en', 'es', 'fr', etc.
LANGUAGE=

# Optional: UI language for app interface and menus (en, es, fr, de, pt, it)
UI_LANGUAGE=en

# Anthropic API Configuration (optional - for Claude AI processing)
# Get your API key from: https://console.anthropic.com/api-keys
ANTHROPIC_API_KEY=your_anthropic_api_key_here
Expand Down
47 changes: 30 additions & 17 deletions electron-builder.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"src/dist/**/*",
"src/helpers/**/*",
"src/config/**/*",
"src/locales/**/*",
"src/hooks/**/*",
"src/models/**/*",
"src/services/localReasoningBridge.js",
Expand Down Expand Up @@ -81,18 +82,24 @@
"!**/node_modules/**/CHANGELOG*",
"!**/node_modules/**/.github/**"
],
"asarUnpack": [
"**/node_modules/ffmpeg-static/**/*",
"**/node_modules/better-sqlite3/**/*"
],
"asarUnpack": ["**/node_modules/ffmpeg-static/**/*", "**/node_modules/better-sqlite3/**/*"],
"extraResources": [
"src/assets/**/*",
"resources/bin/macos-globe-listener",
"resources/bin/macos-fast-paste",
{
"from": "resources/bin/",
"to": "bin/",
"filter": ["whisper-cpp-*", "whisper-server-*", "llama-server-*", "sherpa-onnx-*", "windows-key-listener*", "*.dylib", "*.dll", "*.so*"]
"filter": [
"whisper-cpp-*",
"whisper-server-*",
"llama-server-*",
"sherpa-onnx-*",
"windows-key-listener*",
"*.dylib",
"*.dll",
"*.so*"
]
}
],
"mac": {
Expand All @@ -112,10 +119,7 @@
}
},
"win": {
"target": [
"nsis",
"portable"
],
"target": ["nsis", "portable"],
"icon": "src/assets/icon.ico",
"extraResources": [
{
Expand All @@ -125,12 +129,7 @@
]
},
"linux": {
"target": [
"AppImage",
"deb",
"rpm",
"tar.gz"
],
"target": ["AppImage", "deb", "rpm", "tar.gz"],
"icon": "src/assets/icon.png",
"category": "Utility",
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}"
Expand All @@ -156,10 +155,24 @@
},
"deb": {
"afterRemove": "resources/linux/after-remove.sh",
"fpm": ["--deb-recommends", "xdotool", "--deb-recommends", "wtype", "--deb-recommends", "wl-clipboard"]
"fpm": [
"--deb-recommends",
"xdotool",
"--deb-recommends",
"wtype",
"--deb-recommends",
"wl-clipboard"
]
},
"rpm": {
"fpm": ["--rpm-tag", "Recommends: xdotool", "--rpm-tag", "Recommends: wtype", "--rpm-tag", "Recommends: wl-clipboard"]
"fpm": [
"--rpm-tag",
"Recommends: xdotool",
"--rpm-tag",
"Recommends: wtype",
"--rpm-tag",
"Recommends: wl-clipboard"
]
},
"nsis": {
"oneClick": false,
Expand Down
96 changes: 57 additions & 39 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,16 @@ if (process.platform === "linux") {
// and GlobalShortcutsPortal for global shortcuts via xdg-desktop-portal
if (process.platform === "linux" && process.env.XDG_SESSION_TYPE === "wayland") {
app.commandLine.appendSwitch("ozone-platform-hint", "auto");
app.commandLine.appendSwitch("enable-features", "UseOzonePlatform,WaylandWindowDecorations,GlobalShortcutsPortal");
app.commandLine.appendSwitch(
"enable-features",
"UseOzonePlatform,WaylandWindowDecorations,GlobalShortcutsPortal"
);
}

// Group all windows under single taskbar entry on Windows
if (process.platform === "win32") {
const windowsAppId =
APP_CHANNEL === "production"
? BASE_WINDOWS_APP_ID
: `${BASE_WINDOWS_APP_ID}.${APP_CHANNEL}`;
APP_CHANNEL === "production" ? BASE_WINDOWS_APP_ID : `${BASE_WINDOWS_APP_ID}.${APP_CHANNEL}`;
app.setAppUserModelId(windowsAppId);
}

Expand All @@ -87,7 +88,9 @@ function getOAuthProtocol() {
return fromEnv;
}

return DEFAULT_OAUTH_PROTOCOL_BY_CHANNEL[APP_CHANNEL] || DEFAULT_OAUTH_PROTOCOL_BY_CHANNEL.production;
return (
DEFAULT_OAUTH_PROTOCOL_BY_CHANNEL[APP_CHANNEL] || DEFAULT_OAUTH_PROTOCOL_BY_CHANNEL.production
);
}

const OAUTH_PROTOCOL = getOAuthProtocol();
Expand Down Expand Up @@ -156,6 +159,7 @@ const UpdateManager = require("./src/updater");
const GlobeKeyManager = require("./src/helpers/globeKeyManager");
const DevServerManager = require("./src/helpers/devServerManager");
const WindowsKeyManager = require("./src/helpers/windowsKeyManager");
const { i18nMain, changeLanguage } = require("./src/helpers/i18nMain");

// Manager instances - initialized after app.whenReady()
let debugLogger = null;
Expand Down Expand Up @@ -218,6 +222,9 @@ function initializeCoreManagers() {
debugLogger.ensureFileLogging();

environmentManager = new EnvironmentManager();
const uiLanguage = environmentManager.getUiLanguage();
process.env.UI_LANGUAGE = uiLanguage;
changeLanguage(uiLanguage);
debugLogger.refreshLogLevel();

windowManager = new WindowManager();
Expand All @@ -239,6 +246,7 @@ function initializeCoreManagers() {
windowManager,
updateManager,
windowsKeyManager,
getTrayManager: () => trayManager,
});
}

Expand All @@ -256,29 +264,27 @@ function initializeDeferredManagers() {
globeKeyAlertShown = true;

const detailLines = [
error?.message || "Unknown error occurred while starting the Globe listener.",
"The Globe key shortcut will remain disabled; existing keyboard shortcuts continue to work.",
error?.message || i18nMain.t("startup.globeHotkey.details.unknown"),
i18nMain.t("startup.globeHotkey.details.fallback"),
];

if (process.env.NODE_ENV === "development") {
detailLines.push(
"Run `npm run compile:globe` and rebuild the app to regenerate the listener binary."
);
detailLines.push(i18nMain.t("startup.globeHotkey.details.devHint"));
} else {
detailLines.push("Try reinstalling OpenWhispr or contact support if the issue persists.");
detailLines.push(i18nMain.t("startup.globeHotkey.details.reinstallHint"));
}

dialog.showMessageBox({
type: "warning",
title: "Globe Hotkey Unavailable",
message: "OpenWhispr could not activate the Globe key hotkey.",
title: i18nMain.t("startup.globeHotkey.title"),
message: i18nMain.t("startup.globeHotkey.message"),
detail: detailLines.join("\n\n"),
});
});
}
}

app.on('open-url', (event, url) => {
app.on("open-url", (event, url) => {
event.preventDefault();
if (!url.startsWith(`${OAUTH_PROTOCOL}://`)) return;

Expand All @@ -300,7 +306,7 @@ function navigateControlPanelWithVerifier(verifier) {
const appUrl = DevServerManager.getAppUrl(true);

if (appUrl) {
const separator = appUrl.includes('?') ? '&' : '?';
const separator = appUrl.includes("?") ? "&" : "?";
const urlWithVerifier = `${appUrl}${separator}neon_auth_session_verifier=${encodeURIComponent(verifier)}`;
windowManager.controlPanelWindow.loadURL(urlWithVerifier);
} else {
Expand All @@ -323,11 +329,11 @@ function navigateControlPanelWithVerifier(verifier) {
function handleOAuthDeepLink(deepLinkUrl) {
try {
const parsed = new URL(deepLinkUrl);
const verifier = parsed.searchParams.get('neon_auth_session_verifier');
const verifier = parsed.searchParams.get("neon_auth_session_verifier");
if (!verifier) return;
navigateControlPanelWithVerifier(verifier);
} catch (err) {
if (debugLogger) debugLogger.error('Failed to handle OAuth deep link:', err);
if (debugLogger) debugLogger.error("Failed to handle OAuth deep link:", err);
}
}

Expand Down Expand Up @@ -400,7 +406,9 @@ function startAuthBridgeServer() {
navigateControlPanelWithVerifier(verifier);

res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end("<html><body><h3>OpenWhispr sign-in complete.</h3><p>You can close this tab.</p></body></html>");
res.end(
"<html><body><h3>OpenWhispr sign-in complete.</h3><p>You can close this tab.</p></body></html>"
);
});

authBridgeServer.on("error", (error) => {
Expand Down Expand Up @@ -430,7 +438,9 @@ async function startApp() {
(details, callback) => {
try {
details.requestHeaders["Origin"] = new URL(details.url).origin;
} catch { /* malformed URL — leave Origin as-is */ }
} catch {
/* malformed URL — leave Origin as-is */
}
callback({ requestHeaders: details.requestHeaders });
}
);
Expand Down Expand Up @@ -651,7 +661,8 @@ async function startApp() {
};

// Right-side single modifiers need the native listener even in tap mode
const isRightSideMod = (hotkey) => /^Right(Control|Ctrl|Alt|Option|Shift|Super|Win|Meta|Command|Cmd)$/i.test(hotkey);
const isRightSideMod = (hotkey) =>
/^Right(Control|Ctrl|Alt|Option|Shift|Super|Win|Meta|Command|Cmd)$/i.test(hotkey);

const { isModifierOnlyHotkey } = require("./src/helpers/hotkeyManager");

Expand Down Expand Up @@ -724,12 +735,14 @@ async function startApp() {
});

windowsKeyManager.on("unavailable", () => {
debugLogger.debug("[Push-to-Talk] Windows key listener not available - falling back to toggle mode");
debugLogger.debug(
"[Push-to-Talk] Windows key listener not available - falling back to toggle mode"
);
windowManager.setWindowsPushToTalkAvailable(false);
if (isLiveWindow(windowManager.mainWindow)) {
windowManager.mainWindow.webContents.send("windows-ptt-unavailable", {
reason: "binary_not_found",
message: "Push-to-Talk native listener not available",
message: i18nMain.t("windows.pttUnavailable"),
});
}
});
Expand All @@ -751,7 +764,9 @@ async function startApp() {
debugLogger.debug("[Push-to-Talk] Current state", { activationMode, currentHotkey });

if (needsNativeListener(currentHotkey, activationMode)) {
debugLogger.debug("[Push-to-Talk] Starting Windows key listener", { hotkey: currentHotkey });
debugLogger.debug("[Push-to-Talk] Starting Windows key listener", {
hotkey: currentHotkey,
});
windowsKeyManager.start(currentHotkey);
} else {
debugLogger.debug("[Push-to-Talk] Native listener not needed for current configuration");
Expand Down Expand Up @@ -821,28 +836,31 @@ if (gotSingleInstanceLock) {
}

// Check for OAuth protocol URL in command line arguments (Windows/Linux)
const url = commandLine.find(arg => arg.startsWith(`${OAUTH_PROTOCOL}://`));
const url = commandLine.find((arg) => arg.startsWith(`${OAUTH_PROTOCOL}://`));
if (url) {
handleOAuthDeepLink(url);
}
});

app.whenReady().then(() => {
// On Linux, --enable-transparent-visuals requires a short delay before creating
// windows to allow the compositor to set up the ARGB visual correctly.
// Without this delay, transparent windows flicker on both X11 and Wayland.
const delay = process.platform === "linux" ? 300 : 0;
return new Promise((resolve) => setTimeout(resolve, delay));
}).then(() => {
startApp().catch((error) => {
console.error("Failed to start app:", error);
dialog.showErrorBox(
"OpenWhispr Startup Error",
`Failed to start the application:\n\n${error.message}\n\nPlease report this issue.`
);
app.exit(1);
app
.whenReady()
.then(() => {
// On Linux, --enable-transparent-visuals requires a short delay before creating
// windows to allow the compositor to set up the ARGB visual correctly.
// Without this delay, transparent windows flicker on both X11 and Wayland.
const delay = process.platform === "linux" ? 300 : 0;
return new Promise((resolve) => setTimeout(resolve, delay));
})
.then(() => {
startApp().catch((error) => {
console.error("Failed to start app:", error);
dialog.showErrorBox(
i18nMain.t("startup.error.title"),
i18nMain.t("startup.error.message", { error: error.message })
);
app.exit(1);
});
});
});

app.on("window-all-closed", () => {
// Don't quit on macOS when all windows are closed
Expand Down
Loading