feat: add configurable global shortcut to open app

This commit is contained in:
Yauheni Bandziukevich
2026-05-28 21:50:53 +02:00
parent 54677960d0
commit 480890bcce
19 changed files with 97 additions and 2 deletions
+6
View File
@@ -248,6 +248,12 @@ interface Window {
) => Promise<{ success: boolean; error?: string; message?: string }>;
getShortcuts: () => Promise<Record<string, unknown> | null>;
saveShortcuts: (shortcuts: unknown) => Promise<{ success: boolean; error?: string }>;
updateGlobalShortcut: (binding: {
key: string;
ctrl?: boolean;
shift?: boolean;
alt?: boolean;
}) => Promise<void>;
hudOverlayHide: () => void;
hudOverlayClose: () => void;
setHudOverlayIgnoreMouseEvents: (ignore: boolean) => void;
+51
View File
@@ -0,0 +1,51 @@
import fs from "node:fs/promises";
import { globalShortcut } from "electron";
import { type ShortcutBinding } from "../src/lib/shortcuts";
import { SHORTCUTS_FILE } from "./ipc/handlers";
const DEFAULT_OPEN_APP_BINDING: ShortcutBinding = { key: "o", ctrl: true, shift: true };
function bindingToAccelerator(binding: ShortcutBinding): string {
const parts: string[] = [];
if (binding.ctrl) parts.push("CommandOrControl");
if (binding.shift) parts.push("Shift");
if (binding.alt) parts.push("Alt");
parts.push(binding.key.toUpperCase());
return parts.join("+");
}
let currentAccelerator: string | null = null;
export function registerOpenAppShortcut(binding: ShortcutBinding, onTrigger: () => void): boolean {
if (currentAccelerator) {
globalShortcut.unregister(currentAccelerator);
}
const accelerator = bindingToAccelerator(binding);
const success = globalShortcut.register(accelerator, onTrigger);
if (success) {
currentAccelerator = accelerator;
console.log(`Global shortcut registered: ${accelerator}`);
} else {
console.warn(`Failed to register global shortcut: ${accelerator}`);
}
return success;
}
export async function loadAndRegisterGlobalShortcut(onTrigger: () => void): Promise<void> {
try {
const data = await fs.readFile(SHORTCUTS_FILE, "utf-8");
const shortcuts = JSON.parse(data);
const binding = shortcuts.openApp || DEFAULT_OPEN_APP_BINDING;
registerOpenAppShortcut(binding, onTrigger);
} catch {
// File doesn't exist or parse error, use default
registerOpenAppShortcut(DEFAULT_OPEN_APP_BINDING, onTrigger);
}
}
export function unregisterAllGlobalShortcuts(): void {
globalShortcut.unregisterAll();
}
+1 -1
View File
@@ -43,7 +43,7 @@ import type { CursorRecordingSession } from "../native-bridge/cursor/recording/s
import { registerNativeBridgeHandlers } from "./nativeBridge";
const PROJECT_FILE_EXTENSION = "openscreen";
const SHORTCUTS_FILE = path.join(app.getPath("userData"), "shortcuts.json");
export const SHORTCUTS_FILE = path.join(app.getPath("userData"), "shortcuts.json");
const RECORDING_FILE_PREFIX = "recording-";
const RECORDING_SESSION_SUFFIX = ".session.json";
const ALLOWED_IMPORT_VIDEO_EXTENSIONS = new Set([".webm", ".mp4", ".mov", ".avi", ".mkv"]);
+17
View File
@@ -11,6 +11,12 @@ import {
systemPreferences,
Tray,
} from "electron";
import {
loadAndRegisterGlobalShortcut,
registerOpenAppShortcut,
ShortcutBinding,
unregisterAllGlobalShortcuts,
} from "./globalShortcut";
import { mainT, setMainLocale } from "./i18n";
import { getSelectedDesktopSource, registerIpcHandlers } from "./ipc/handlers";
import {
@@ -440,6 +446,10 @@ app.on("activate", () => {
}
});
app.on("will-quit", () => {
unregisterAllGlobalShortcuts();
});
// Register all IPC handlers when app is ready
app.whenReady().then(async () => {
// Force the app into "regular" activation policy so the Dock icon appears.
@@ -512,6 +522,10 @@ app.whenReady().then(async () => {
updateTrayMenu();
});
ipcMain.handle("update-global-shortcut", (_, binding: ShortcutBinding) => {
registerOpenAppShortcut(binding, showMainWindow);
});
createTray();
updateTrayMenu();
setupApplicationMenu();
@@ -545,5 +559,8 @@ app.whenReady().then(async () => {
},
switchToHudWrapper,
);
await loadAndRegisterGlobalShortcut(showMainWindow);
createWindow();
});
+4
View File
@@ -3,6 +3,7 @@ import type { NativeMacRecordingRequest } from "../src/lib/nativeMacRecording";
import type { NativeWindowsRecordingRequest } from "../src/lib/nativeWindowsRecording";
import type { RecordingSession, StoreRecordedSessionInput } from "../src/lib/recordingSession";
import { NATIVE_BRIDGE_CHANNEL, type NativeBridgeRequest } from "../src/native/contracts";
import type { ShortcutBinding } from "./globalShortcut";
// Asset base URL is passed from the main process via webPreferences.additionalArguments
// (see windows.ts). Sandboxed preloads cannot import node:path / node:url, so we
@@ -193,6 +194,9 @@ contextBridge.exposeInMainWorld("electronAPI", {
saveShortcuts: (shortcuts: unknown) => {
return ipcRenderer.invoke("save-shortcuts", shortcuts);
},
updateGlobalShortcut: (binding: ShortcutBinding) => {
return ipcRenderer.invoke("update-global-shortcut", binding);
},
setLocale: (locale: string) => {
return ipcRenderer.invoke("set-locale", locale);
},
+3 -1
View File
@@ -54,7 +54,9 @@ export function ShortcutsProvider({ children }: { children: ReactNode }) {
const persistShortcuts = useCallback(
async (config?: ShortcutsConfig) => {
await window.electronAPI.saveShortcuts?.(config ?? shortcuts);
const configToSave = config ?? shortcuts;
await window.electronAPI.saveShortcuts?.(configToSave);
await window.electronAPI.updateGlobalShortcut?.(configToSave.openApp);
},
[shortcuts],
);
+1
View File
@@ -14,6 +14,7 @@
"savedToast": "تم حفظ اختصارات لوحة المفاتيح",
"resetToast": "إعادة تعيين إلى الاختصارات الافتراضية — انقر فوق حفظ للتطبيق",
"actions": {
"openApp": "فتح التطبيق",
"addZoom": "إضافة تكبير",
"addTrim": "إضافة قص",
"addSpeed": "إضافة سرعة",
+1
View File
@@ -14,6 +14,7 @@
"savedToast": "Keyboard shortcuts saved",
"resetToast": "Reset to default shortcuts — click Save to apply",
"actions": {
"openApp": "Open App",
"addZoom": "Add Zoom",
"addTrim": "Add Trim",
"addSpeed": "Add Speed",
+1
View File
@@ -14,6 +14,7 @@
"savedToast": "Atajos de teclado guardados",
"resetToast": "Restablecido a los atajos predeterminados — haz clic en Guardar para aplicar",
"actions": {
"openApp": "Abrir aplicación",
"addZoom": "Agregar zoom",
"addTrim": "Agregar recorte",
"addSpeed": "Agregar velocidad",
+1
View File
@@ -14,6 +14,7 @@
"savedToast": "Raccourcis clavier enregistrés",
"resetToast": "Réinitialisé aux raccourcis par défaut — cliquez sur Enregistrer pour appliquer",
"actions": {
"openApp": "Ouvrir l'application",
"addZoom": "Ajouter un zoom",
"addTrim": "Ajouter une coupe",
"addSpeed": "Ajouter une vitesse",
+1
View File
@@ -14,6 +14,7 @@
"savedToast": "Scorciatoie tastiera salvate",
"resetToast": "Ripristino alle scorciatoie predefinite — clicca Salva per applicare",
"actions": {
"openApp": "Apri applicazione",
"addZoom": "Aggiungi zoom",
"addTrim": "Aggiungi taglio",
"addSpeed": "Aggiungi velocità",
+1
View File
@@ -14,6 +14,7 @@
"savedToast": "キーボードショートカットが保存されました",
"resetToast": "デフォルトのショートカットにリセット — 保存をクリックして適用",
"actions": {
"openApp": "アプリを開く",
"addZoom": "ズームを追加",
"addTrim": "トリムを追加",
"addSpeed": "速度を追加",
+1
View File
@@ -14,6 +14,7 @@
"savedToast": "키보드 단축키가 저장되었습니다",
"resetToast": "기본 단축키로 초기화되었습니다 — 저장을 클릭해 적용하세요",
"actions": {
"openApp": "앱 열기",
"addZoom": "줌 추가",
"addTrim": "트림 추가",
"addSpeed": "속도 추가",
+1
View File
@@ -14,6 +14,7 @@
"savedToast": "Горячие клавиши сохранены",
"resetToast": "Сброс к горячим клавишам по умолчанию — нажмите Сохранить для применения",
"actions": {
"openApp": "Открыть приложение",
"addZoom": "Добавить масштабирование",
"addTrim": "Добавить обрезку",
"addSpeed": "Изменить скорость",
+1
View File
@@ -14,6 +14,7 @@
"savedToast": "Klavye kısayolları kaydedildi",
"resetToast": "Varsayılan kısayollara sıfırlandı — uygulamak için Kaydet'e tıklayın",
"actions": {
"openApp": "Uygulamayı Aç",
"addZoom": "Yakınlaştırma Ekle",
"addTrim": "Kırpma Ekle",
"addSpeed": "Hız Ekle",
+1
View File
@@ -14,6 +14,7 @@
"savedToast": "Đã lưu phím tắt",
"resetToast": "Đã đặt lại về phím tắt mặc định — nhấp Lưu để áp dụng",
"actions": {
"openApp": "Mở ứng dụng",
"addZoom": "Thêm Thu phóng",
"addTrim": "Thêm Cắt",
"addSpeed": "Thêm Tốc độ",
+1
View File
@@ -14,6 +14,7 @@
"savedToast": "键盘快捷键已保存",
"resetToast": "已恢复默认快捷键 — 点击保存以应用",
"actions": {
"openApp": "打开应用",
"addZoom": "添加缩放",
"addTrim": "添加剪辑",
"addSpeed": "添加速度",
+1
View File
@@ -14,6 +14,7 @@
"savedToast": "鍵盤快捷鍵已儲存",
"resetToast": "已還原預設快捷鍵 — 點擊儲存以套用",
"actions": {
"openApp": "開啟應用程式",
"addZoom": "新增縮放",
"addTrim": "新增剪輯",
"addSpeed": "新增速度",
+3
View File
@@ -1,4 +1,5 @@
export const SHORTCUT_ACTIONS = [
"openApp",
"addZoom",
"addTrim",
"addSpeed",
@@ -105,6 +106,7 @@ export function findConflict(
}
export const DEFAULT_SHORTCUTS: ShortcutsConfig = {
openApp: { key: "o", ctrl: true, shift: true },
addZoom: { key: "z" },
addTrim: { key: "t" },
addSpeed: { key: "s" },
@@ -116,6 +118,7 @@ export const DEFAULT_SHORTCUTS: ShortcutsConfig = {
};
export const SHORTCUT_LABELS: Record<ShortcutAction, string> = {
openApp: "Open App",
addZoom: "Add Zoom",
addTrim: "Add Trim",
addSpeed: "Add Speed",