From 5b193b28e9b57e58d0dfa88a83f655937ce33a6d Mon Sep 17 00:00:00 2001 From: yslee Date: Thu, 19 Mar 2026 19:14:41 +0900 Subject: [PATCH 1/3] Add persisted customizable overlay shortcuts The overlay shortcuts were hard-coded to Alt+Space and Cmd+Shift+K. This adds a small settings layer for loading, saving, validating, and re-registering primary/secondary shortcuts at runtime. Changes included here: - add shortcut settings types and IPC channels - persist shortcut settings under the app userData directory - normalize/validate accelerators before registration - support nullable primary/secondary bindings so shortcuts can be removed - expose load/save actions to the renderer store This keeps shortcut behavior configurable without changing the default UX for users who never touch settings. --- src/main/index.ts | 58 +++++++-- src/main/shortcut-settings.ts | 181 ++++++++++++++++++++++++++++ src/preload/index.ts | 17 ++- src/renderer/stores/sessionStore.ts | 48 +++++++- src/shared/types.ts | 22 ++++ 5 files changed, 315 insertions(+), 11 deletions(-) create mode 100644 src/main/shortcut-settings.ts diff --git a/src/main/index.ts b/src/main/index.ts index 10472226..aaa63a85 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,6 +1,6 @@ import { app, BrowserWindow, ipcMain, dialog, screen, globalShortcut, Tray, Menu, nativeImage, nativeTheme, shell, systemPreferences } from 'electron' import { join } from 'path' -import { existsSync, readdirSync, statSync, createReadStream } from 'fs' +import { existsSync, readdirSync, statSync, createReadStream, readFileSync, writeFileSync } from 'fs' import { createInterface } from 'readline' import { homedir } from 'os' import { ControlPlane } from './claude/control-plane' @@ -8,8 +8,9 @@ import { ensureSkills, type SkillStatus } from './skills/installer' import { fetchCatalog, listInstalled, installPlugin, uninstallPlugin } from './marketplace/catalog' import { log as _log, LOG_FILE, flushLogs } from './logger' import { getCliEnv } from './cli-env' -import { IPC } from '../shared/types' -import type { RunOptions, NormalizedEvent, EnrichedError } from '../shared/types' +import { DEFAULT_SHORTCUT_SETTINGS, IPC } from '../shared/types' +import type { RunOptions, NormalizedEvent, EnrichedError, ShortcutSettings } from '../shared/types' +import { loadShortcutSettings, registerShortcutSettings, saveShortcutSettings } from './shortcut-settings' const DEBUG_MODE = process.env.CLUI_DEBUG === '1' const SPACES_DEBUG = DEBUG_MODE || process.env.CLUI_SPACES_DEBUG === '1' @@ -22,6 +23,7 @@ let mainWindow: BrowserWindow | null = null let tray: Tray | null = null let screenshotCounter = 0 let toggleSequence = 0 +let currentShortcutSettings: ShortcutSettings = DEFAULT_SHORTCUT_SETTINGS // Feature flag: enable PTY interactive permissions transport const INTERACTIVE_PTY = process.env.CLUI_INTERACTIVE_PERMISSIONS_PTY === '1' @@ -864,6 +866,38 @@ ipcMain.handle(IPC.GET_THEME, () => { return { isDark: nativeTheme.shouldUseDarkColors } }) +ipcMain.handle(IPC.GET_SHORTCUT_SETTINGS, () => currentShortcutSettings) + +ipcMain.handle(IPC.SET_SHORTCUT_SETTINGS, (_event, settings: ShortcutSettings) => { + const previousSettings = currentShortcutSettings + + try { + const registration = registerShortcutSettings(settings, toggleWindow) + if (!registration.ok) { + registerShortcutSettings(previousSettings, toggleWindow) + return { + ok: false, + error: registration.error, + settings: previousSettings, + } + } + + currentShortcutSettings = registration.settings + saveShortcutSettings(currentShortcutSettings) + return { + ok: true, + settings: currentShortcutSettings, + } + } catch (err) { + registerShortcutSettings(previousSettings, toggleWindow) + return { + ok: false, + error: err instanceof Error ? err.message : String(err), + settings: previousSettings, + } + } +}) + nativeTheme.on('updated', () => { broadcast(IPC.THEME_CHANGED, nativeTheme.shouldUseDarkColors) }) @@ -940,13 +974,19 @@ app.whenReady().then(async () => { } - // Primary: Option+Space (2 keys, doesn't conflict with shell) - // Fallback: Cmd+Shift+K kept as secondary shortcut - const registered = globalShortcut.register('Alt+Space', () => toggleWindow('shortcut Alt+Space')) - if (!registered) { - log('Alt+Space shortcut registration failed — macOS input sources may claim it') + currentShortcutSettings = loadShortcutSettings() + let shortcutRegistration = registerShortcutSettings(currentShortcutSettings, toggleWindow) + if (!shortcutRegistration.ok) { + log(`Shortcut registration failed for saved settings — ${shortcutRegistration.error}`) + currentShortcutSettings = DEFAULT_SHORTCUT_SETTINGS + shortcutRegistration = registerShortcutSettings(currentShortcutSettings, toggleWindow) + if (shortcutRegistration.ok) { + saveShortcutSettings(currentShortcutSettings) + } + } + if (!shortcutRegistration.ok) { + log(`Default shortcut registration failed — ${shortcutRegistration.error}`) } - globalShortcut.register('CommandOrControl+Shift+K', () => toggleWindow('shortcut Cmd/Ctrl+Shift+K')) const trayIconPath = join(__dirname, '../../resources/trayTemplate.png') const trayIcon = nativeImage.createFromPath(trayIconPath) diff --git a/src/main/shortcut-settings.ts b/src/main/shortcut-settings.ts new file mode 100644 index 00000000..b3893365 --- /dev/null +++ b/src/main/shortcut-settings.ts @@ -0,0 +1,181 @@ +import { app, globalShortcut } from 'electron' +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs' +import { dirname, join } from 'path' +import { DEFAULT_SHORTCUT_SETTINGS, type ShortcutSettings } from '../shared/types' + +const MODIFIER_ALIASES: Record = { + alt: 'Alt', + option: 'Alt', + opt: 'Alt', + '⌥': 'Alt', + shift: 'Shift', + '⇧': 'Shift', + ctrl: 'Control', + control: 'Control', + '^': 'Control', + cmd: 'CommandOrControl', + command: 'CommandOrControl', + commandorcontrol: 'CommandOrControl', + cmdorctrl: 'CommandOrControl', + cmdctrl: 'CommandOrControl', + '⌘': 'CommandOrControl', + super: 'Super', + meta: 'Super', + win: 'Super', + windows: 'Super', +} + +const KEY_ALIASES: Record = { + space: 'Space', + spacebar: 'Space', + enter: 'Enter', + return: 'Enter', + esc: 'Escape', + escape: 'Escape', + tab: 'Tab', + backspace: 'Backspace', + delete: 'Delete', + del: 'Delete', + insert: 'Insert', + ins: 'Insert', + home: 'Home', + end: 'End', + pageup: 'PageUp', + pagedown: 'PageDown', + pgup: 'PageUp', + pgdn: 'PageDown', + up: 'Up', + down: 'Down', + left: 'Left', + right: 'Right', +} + +function getShortcutSettingsPath(): string { + return join(app.getPath('userData'), 'shortcut-settings.json') +} + +function normalizeShortcut(input: string): string { + const rawParts = input + .split('+') + .map((part) => part.trim()) + .filter(Boolean) + + if (rawParts.length === 0) return '' + + const modifiers = new Set() + let key = '' + + for (const rawPart of rawParts) { + const lower = rawPart.toLowerCase() + const modifier = MODIFIER_ALIASES[lower] + if (modifier) { + modifiers.add(modifier) + continue + } + + if (key) { + throw new Error('Only one non-modifier key is allowed per shortcut.') + } + + if (/^f([1-9]|1\d|2[0-4])$/i.test(rawPart)) { + key = rawPart.toUpperCase() + continue + } + + if (/^[a-z]$/i.test(rawPart)) { + key = rawPart.toUpperCase() + continue + } + + if (/^\d$/.test(rawPart)) { + key = rawPart + continue + } + + const aliasKey = KEY_ALIASES[lower] + if (aliasKey) { + key = aliasKey + continue + } + + throw new Error(`Unsupported key token: "${rawPart}"`) + } + + if (!key) { + throw new Error('A shortcut must include one non-modifier key.') + } + + const orderedModifiers = ['CommandOrControl', 'Control', 'Alt', 'Shift', 'Super'] + .filter((modifier) => modifiers.has(modifier)) + + return [...orderedModifiers, key].join('+') +} + +function normalizeShortcutSettings(settings: ShortcutSettings): ShortcutSettings { + const primaryShortcut = settings.primaryShortcut?.trim() + ? normalizeShortcut(settings.primaryShortcut) + : null + const secondaryShortcut = settings.secondaryShortcut?.trim() + ? normalizeShortcut(settings.secondaryShortcut) + : null + + if (primaryShortcut && secondaryShortcut && secondaryShortcut === primaryShortcut) { + throw new Error('Primary and secondary shortcuts must be different.') + } + + return { primaryShortcut, secondaryShortcut } +} + +export function loadShortcutSettings(): ShortcutSettings { + try { + const filePath = getShortcutSettingsPath() + if (!existsSync(filePath)) return DEFAULT_SHORTCUT_SETTINGS + + const raw = JSON.parse(readFileSync(filePath, 'utf-8')) as Partial + return normalizeShortcutSettings({ + primaryShortcut: raw.primaryShortcut ?? DEFAULT_SHORTCUT_SETTINGS.primaryShortcut, + secondaryShortcut: raw.secondaryShortcut ?? DEFAULT_SHORTCUT_SETTINGS.secondaryShortcut, + }) + } catch { + return DEFAULT_SHORTCUT_SETTINGS + } +} + +export function saveShortcutSettings(settings: ShortcutSettings): void { + const filePath = getShortcutSettingsPath() + mkdirSync(dirname(filePath), { recursive: true }) + writeFileSync(filePath, JSON.stringify(settings, null, 2) + '\n', 'utf-8') +} + +export function registerShortcutSettings( + settings: ShortcutSettings, + onToggle: (source: string) => void, +): { ok: boolean; error?: string; settings: ShortcutSettings } { + const normalized = normalizeShortcutSettings(settings) + globalShortcut.unregisterAll() + + if ( + normalized.primaryShortcut && + !globalShortcut.register(normalized.primaryShortcut, () => onToggle(`shortcut ${normalized.primaryShortcut}`)) + ) { + return { + ok: false, + error: `Could not register primary shortcut "${normalized.primaryShortcut}". It may be invalid or already in use.`, + settings: normalized, + } + } + + if ( + normalized.secondaryShortcut && + !globalShortcut.register(normalized.secondaryShortcut, () => onToggle(`shortcut ${normalized.secondaryShortcut}`)) + ) { + globalShortcut.unregisterAll() + return { + ok: false, + error: `Could not register secondary shortcut "${normalized.secondaryShortcut}". It may be invalid or already in use.`, + settings: normalized, + } + } + + return { ok: true, settings: normalized } +} diff --git a/src/preload/index.ts b/src/preload/index.ts index 81344d61..2b16755c 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1,6 +1,17 @@ import { contextBridge, ipcRenderer } from 'electron' import { IPC } from '../shared/types' -import type { RunOptions, NormalizedEvent, HealthReport, EnrichedError, Attachment, SessionMeta, CatalogPlugin, SessionLoadMessage } from '../shared/types' +import type { + RunOptions, + NormalizedEvent, + HealthReport, + EnrichedError, + Attachment, + SessionMeta, + CatalogPlugin, + SessionLoadMessage, + ShortcutSettings, + ShortcutSettingsUpdateResult, +} from '../shared/types' export interface CluiAPI { // ─── Request-response (renderer → main) ─── @@ -33,6 +44,8 @@ export interface CluiAPI { setPermissionMode(mode: string): void getTheme(): Promise<{ isDark: boolean }> onThemeChange(callback: (isDark: boolean) => void): () => void + getShortcutSettings(): Promise + setShortcutSettings(settings: ShortcutSettings): Promise // ─── Window management ─── resizeHeight(height: number): void @@ -84,6 +97,8 @@ const api: CluiAPI = { ipcRenderer.invoke(IPC.MARKETPLACE_UNINSTALL, { pluginName }), setPermissionMode: (mode) => ipcRenderer.send(IPC.SET_PERMISSION_MODE, mode), getTheme: () => ipcRenderer.invoke(IPC.GET_THEME), + getShortcutSettings: () => ipcRenderer.invoke(IPC.GET_SHORTCUT_SETTINGS), + setShortcutSettings: (settings) => ipcRenderer.invoke(IPC.SET_SHORTCUT_SETTINGS, settings), onThemeChange: (callback) => { const handler = (_e: Electron.IpcRendererEvent, isDark: boolean) => callback(isDark) ipcRenderer.on(IPC.THEME_CHANGED, handler) diff --git a/src/renderer/stores/sessionStore.ts b/src/renderer/stores/sessionStore.ts index 3bd31893..39854bc2 100644 --- a/src/renderer/stores/sessionStore.ts +++ b/src/renderer/stores/sessionStore.ts @@ -1,5 +1,15 @@ import { create } from 'zustand' -import type { TabStatus, NormalizedEvent, EnrichedError, Message, TabState, Attachment, CatalogPlugin, PluginStatus } from '../../shared/types' +import type { + TabStatus, + NormalizedEvent, + EnrichedError, + Message, + TabState, + Attachment, + CatalogPlugin, + PluginStatus, + ShortcutSettings, +} from '../../shared/types' import { useThemeStore } from '../theme' import notificationSrc from '../../../resources/notification.mp3' @@ -32,6 +42,9 @@ interface State { preferredModel: string | null /** Global permission mode: 'ask' shows cards, 'auto' auto-approves all tool calls */ permissionMode: 'ask' | 'auto' + shortcutSettings: ShortcutSettings | null + shortcutSettingsSaving: boolean + shortcutSettingsError: string | null // Marketplace state marketplaceOpen: boolean @@ -45,6 +58,8 @@ interface State { // Actions initStaticInfo: () => Promise + loadShortcutSettings: () => Promise + saveShortcutSettings: (settings: ShortcutSettings) => Promise setPreferredModel: (model: string | null) => void setPermissionMode: (mode: 'ask' | 'auto') => void createTab: () => Promise @@ -128,6 +143,9 @@ export const useSessionStore = create((set, get) => ({ staticInfo: null, preferredModel: null, permissionMode: 'ask', + shortcutSettings: null, + shortcutSettingsSaving: false, + shortcutSettingsError: null, // Marketplace marketplaceOpen: false, @@ -154,6 +172,34 @@ export const useSessionStore = create((set, get) => ({ } catch {} }, + loadShortcutSettings: async () => { + try { + const settings = await window.clui.getShortcutSettings() + set({ shortcutSettings: settings, shortcutSettingsError: null }) + } catch (err) { + set({ shortcutSettingsError: err instanceof Error ? err.message : String(err) }) + } + }, + + saveShortcutSettings: async (settings) => { + set({ shortcutSettingsSaving: true, shortcutSettingsError: null }) + try { + const result = await window.clui.setShortcutSettings(settings) + set({ + shortcutSettingsSaving: false, + shortcutSettingsError: result.ok ? null : (result.error || 'Failed to update shortcuts'), + shortcutSettings: result.settings, + }) + return result.ok + } catch (err) { + set({ + shortcutSettingsSaving: false, + shortcutSettingsError: err instanceof Error ? err.message : String(err), + }) + return false + } + }, + setPreferredModel: (model) => { set({ preferredModel: model }) }, diff --git a/src/shared/types.ts b/src/shared/types.ts index 9745613e..1bb4dedf 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -294,6 +294,24 @@ export interface CatalogPlugin { isSkillMd: boolean // true = individual SKILL.md (direct install), false = CLI plugin (bundle install) } +// ─── App Settings ─── + +export interface ShortcutSettings { + primaryShortcut: string | null + secondaryShortcut: string | null +} + +export interface ShortcutSettingsUpdateResult { + ok: boolean + settings: ShortcutSettings + error?: string +} + +export const DEFAULT_SHORTCUT_SETTINGS: ShortcutSettings = { + primaryShortcut: 'Alt+Space', + secondaryShortcut: 'CommandOrControl+Shift+K', +} + // ─── IPC Channel Names ─── export const IPC = { @@ -349,6 +367,10 @@ export const IPC = { GET_THEME: 'clui:get-theme', THEME_CHANGED: 'clui:theme-changed', + // Shortcut settings + GET_SHORTCUT_SETTINGS: 'clui:get-shortcut-settings', + SET_SHORTCUT_SETTINGS: 'clui:set-shortcut-settings', + // Marketplace MARKETPLACE_FETCH: 'clui:marketplace-fetch', MARKETPLACE_INSTALLED: 'clui:marketplace-installed', From c9552e9f228caae0ab3795be3b05d767b571bbe8 Mon Sep 17 00:00:00 2001 From: yslee Date: Thu, 19 Mar 2026 19:17:30 +0900 Subject: [PATCH 2/3] Add shortcut controls to the settings popover The new shortcut persistence layer needed a minimal in-app surface so users can actually change bindings without editing source. Add a shortcuts section to the existing settings popover with: - direct text entry for primary and secondary bindings - key-recording controls for capturing accelerators - clear actions to remove either binding entirely - small icon-only action buttons to avoid layout breakage in the compact UI This keeps the feature lightweight and aligned with the existing overlay UX instead of introducing a separate preferences window. Refs #22 --- src/renderer/components/SettingsPopover.tsx | 245 +++++++++++++++++++- 1 file changed, 242 insertions(+), 3 deletions(-) diff --git a/src/renderer/components/SettingsPopover.tsx b/src/renderer/components/SettingsPopover.tsx index f184fb0a..0fd28067 100644 --- a/src/renderer/components/SettingsPopover.tsx +++ b/src/renderer/components/SettingsPopover.tsx @@ -1,11 +1,45 @@ import React, { useState, useRef, useEffect, useCallback } from 'react' import { createPortal } from 'react-dom' import { motion } from 'framer-motion' -import { DotsThree, Bell, ArrowsOutSimple, Moon } from '@phosphor-icons/react' +import { DotsThree, Bell, ArrowsOutSimple, Moon, Keyboard, Target, X } from '@phosphor-icons/react' import { useThemeStore } from '../theme' import { useSessionStore } from '../stores/sessionStore' import { usePopoverLayer } from './PopoverLayer' import { useColors } from '../theme' +import { DEFAULT_SHORTCUT_SETTINGS } from '../../shared/types' + +const MODIFIER_KEYS = new Set(['Shift', 'Control', 'Alt', 'Meta']) + +function eventToShortcut(e: React.KeyboardEvent): { value: string | null; preview: string } { + const modifiers: string[] = [] + if (e.metaKey) modifiers.push('CommandOrControl') + if (e.ctrlKey) modifiers.push('Control') + if (e.altKey) modifiers.push('Alt') + if (e.shiftKey) modifiers.push('Shift') + + const key = e.key + const preview = [...modifiers, ...(!MODIFIER_KEYS.has(key) ? [normalizeKey(key)] : [])].join('+') + + if (MODIFIER_KEYS.has(key)) { + return { value: null, preview: modifiers.join('+') } + } + + return { + value: [...modifiers, normalizeKey(key)].join('+'), + preview, + } +} + +function normalizeKey(key: string): string { + if (key === ' ') return 'Space' + if (key.length === 1) return key.toUpperCase() + if (key === 'ArrowUp') return 'Up' + if (key === 'ArrowDown') return 'Down' + if (key === 'ArrowLeft') return 'Left' + if (key === 'ArrowRight') return 'Right' + if (key === 'Escape') return 'Escape' + return key +} function RowToggle({ checked, @@ -51,10 +85,20 @@ export function SettingsPopover() { const expandedUI = useThemeStore((s) => s.expandedUI) const setExpandedUI = useThemeStore((s) => s.setExpandedUI) const isExpanded = useSessionStore((s) => s.isExpanded) + const shortcutSettings = useSessionStore((s) => s.shortcutSettings) + const shortcutSettingsSaving = useSessionStore((s) => s.shortcutSettingsSaving) + const shortcutSettingsError = useSessionStore((s) => s.shortcutSettingsError) + const loadShortcutSettings = useSessionStore((s) => s.loadShortcutSettings) + const saveShortcutSettings = useSessionStore((s) => s.saveShortcutSettings) const popoverLayer = usePopoverLayer() const colors = useColors() const [open, setOpen] = useState(false) + const [primaryShortcut, setPrimaryShortcut] = useState(DEFAULT_SHORTCUT_SETTINGS.primaryShortcut || '') + const [secondaryShortcut, setSecondaryShortcut] = useState(DEFAULT_SHORTCUT_SETTINGS.secondaryShortcut || '') + const [saveMessage, setSaveMessage] = useState(null) + const [recordingTarget, setRecordingTarget] = useState<'primary' | 'secondary' | null>(null) + const [recordingPreview, setRecordingPreview] = useState('') const triggerRef = useRef(null) const popoverRef = useRef(null) const [pos, setPos] = useState<{ right: number; top?: number; bottom?: number; maxHeight?: number }>({ right: 0 }) @@ -120,11 +164,147 @@ export function SettingsPopover() { } }, [open, expandedUI, isExpanded, updatePos]) + useEffect(() => { + if (!open || shortcutSettings) return + void loadShortcutSettings() + }, [open, shortcutSettings, loadShortcutSettings]) + + useEffect(() => { + if (!shortcutSettings) return + setPrimaryShortcut(shortcutSettings.primaryShortcut || '') + setSecondaryShortcut(shortcutSettings.secondaryShortcut || '') + }, [shortcutSettings]) + const handleToggle = () => { if (!open) updatePos() setOpen((o) => !o) } + const handleSaveShortcuts = async () => { + setSaveMessage(null) + const ok = await saveShortcutSettings({ + primaryShortcut: primaryShortcut.trim() || null, + secondaryShortcut: secondaryShortcut.trim() || null, + }) + if (ok) setSaveMessage('Shortcut updated') + } + + const stopRecording = () => { + setRecordingTarget(null) + setRecordingPreview('') + } + + const handleRecorderKeyDown = (target: 'primary' | 'secondary') => (e: React.KeyboardEvent) => { + e.preventDefault() + e.stopPropagation() + setSaveMessage(null) + + if (e.key === 'Escape') { + stopRecording() + return + } + + if (e.key === 'Backspace' || e.key === 'Delete') { + if (target === 'primary') setPrimaryShortcut('') + else setSecondaryShortcut('') + stopRecording() + return + } + + const { value, preview } = eventToShortcut(e) + setRecordingPreview(preview) + if (!value) return + + if (target === 'primary') setPrimaryShortcut(value) + else setSecondaryShortcut(value) + stopRecording() + } + + const renderShortcutRecorder = ( + label: string, + value: string, + target: 'primary' | 'secondary', + placeholder: string, + ) => { + const isRecording = recordingTarget === target + + return ( +
+ +
+ { + if (target === 'primary') setPrimaryShortcut(e.target.value) + else setSecondaryShortcut(e.target.value) + setSaveMessage(null) + }} + placeholder={isRecording ? 'Press shortcut...' : placeholder} + className="flex-1 px-2 py-1.5 rounded-md text-[11px] outline-none min-w-0" + style={{ + background: colors.surfaceSecondary, + color: value ? colors.textPrimary : colors.textMuted, + border: `1px solid ${isRecording ? colors.accent : colors.containerBorder}`, + }} + /> + + +
+ {isRecording && recordingPreview && ( +
+ {recordingPreview} +
+ )} +
+ ) + } + return ( <> + + + + +
+ {/* Theme */}
From b23cc2ace3f5f720e4e5e6839ea6ad2f8694b06f Mon Sep 17 00:00:00 2001 From: yslee Date: Thu, 19 Mar 2026 19:27:38 +0900 Subject: [PATCH 3/3] Preserve cleared shortcut bindings across restarts Shortcut settings used null to represent an intentionally cleared binding, but loadShortcutSettings() applied defaults with nullish coalescing. That collapsed persisted null values back to the default shortcuts on the next launch, so cleared bindings could not round-trip. Default only when the field is actually undefined. Persisted null values are now preserved, so removing a binding survives restart as intended. Refs #22 --- src/main/shortcut-settings.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/shortcut-settings.ts b/src/main/shortcut-settings.ts index b3893365..39b00b09 100644 --- a/src/main/shortcut-settings.ts +++ b/src/main/shortcut-settings.ts @@ -133,8 +133,12 @@ export function loadShortcutSettings(): ShortcutSettings { const raw = JSON.parse(readFileSync(filePath, 'utf-8')) as Partial return normalizeShortcutSettings({ - primaryShortcut: raw.primaryShortcut ?? DEFAULT_SHORTCUT_SETTINGS.primaryShortcut, - secondaryShortcut: raw.secondaryShortcut ?? DEFAULT_SHORTCUT_SETTINGS.secondaryShortcut, + primaryShortcut: raw.primaryShortcut === undefined + ? DEFAULT_SHORTCUT_SETTINGS.primaryShortcut + : raw.primaryShortcut, + secondaryShortcut: raw.secondaryShortcut === undefined + ? DEFAULT_SHORTCUT_SETTINGS.secondaryShortcut + : raw.secondaryShortcut, }) } catch { return DEFAULT_SHORTCUT_SETTINGS