diff --git a/backlog/tasks/task-277 - Restore-Linux-shortcut-backed-modal-actions-after-Electron-39-upgrade.md b/backlog/tasks/task-277 - Restore-Linux-shortcut-backed-modal-actions-after-Electron-39-upgrade.md new file mode 100644 index 00000000..35f89b4b --- /dev/null +++ b/backlog/tasks/task-277 - Restore-Linux-shortcut-backed-modal-actions-after-Electron-39-upgrade.md @@ -0,0 +1,58 @@ +--- +id: TASK-277 +title: Restore Linux shortcut-backed modal actions after Electron 39 upgrade +status: Done +assignee: + - codex +created_date: '2026-04-04 06:49' +updated_date: '2026-04-04 07:08' +labels: + - bug + - linux + - electron + - shortcuts +dependencies: [] +priority: high +--- + +## Description + + +Linux builds on Electron 39 no longer open the runtime options, Jimaku, or Subsync flows from their configured shortcuts (`Ctrl/Cmd+Shift+O`, `Ctrl+Shift+J`, `Ctrl+Alt+S`). Other renderer-driven modals like session help, subtitle sidebar, and playlist browser still work, which points to the Linux `globalShortcut` / overlay shortcut path rather than modal rendering in general. Investigate the Electron 39 regression and restore these Linux shortcut-triggered actions without regressing macOS or Windows behavior. + + +## Acceptance Criteria + +- [x] #1 On Linux, the configured runtime options, Jimaku, and Subsync shortcuts trigger their existing actions again under Electron 39. +- [x] #2 The fix is covered by automated tests around the Linux shortcut/backend behavior that regressed. +- [x] #3 A changelog fragment is added for the Linux shortcut regression fix. + + +## Implementation Plan + + +1. Extend the special-command / mpv IPC command path to cover Jimaku alongside the existing runtime-options and subsync commands. +2. Add tests for IPC command dispatch and any keybinding/session-help metadata affected by the new special command. +3. Route Linux modal-opening shortcuts through the working mpv/IPC command path instead of depending on Electron globalShortcut delivery for those actions. +4. Re-run targeted tests, then the handoff verification gate, and update the changelog/task summary to reflect the final root cause and fix. + + +## Implementation Notes + + +User approved plan; proceeding with test-first implementation. + +Root cause: Linux startup unconditionally enabled Electron's GlobalShortcutsPortal path, so Electron 39 X11 sessions were routed through the portal-backed globalShortcut path even though that path should only be used for Wayland-style launches. The affected runtime-options, Jimaku, and Subsync actions all depend on that shortcut path, while renderer-driven modals did not regress. + +User retested after the portal gating fix; issue persists. Updating investigation scope from portal selection alone to Linux overlay shortcut delivery/registration under Electron 39. + +Revised fix path: Linux renderer now matches configured overlay shortcuts for runtime options, subsync, and Jimaku locally, then dispatches the existing mpv/IPC special-command route instead of depending on Electron globalShortcut delivery. + +Added Jimaku mpv special command plumbing through IPC/runtime deps and exposed an overlay-runtime helper for opening the Jimaku modal from that IPC path. + + +## Final Summary + + +Restored Linux shortcut-backed modal actions by routing runtime options, Subsync, and Jimaku through the working mpv/IPC special-command path. Added renderer-side Linux shortcut matching for configured overlay shortcuts, threaded a new Jimaku special command through IPC/runtime deps, refreshed configured shortcuts on hot reload, and covered the regression with IPC/runtime keyboard tests. Verification: bun test src/core/services/ipc-command.test.ts src/renderer/handlers/keyboard.test.ts src/main/runtime/ipc-mpv-command-main-deps.test.ts; bun run typecheck; bun run test:fast; bun run test:env; bun run build; bun run test:smoke:dist. + diff --git a/changes/2026-04-04-linux-shortcut-portal-regression.md b/changes/2026-04-04-linux-shortcut-portal-regression.md new file mode 100644 index 00000000..b6869dfb --- /dev/null +++ b/changes/2026-04-04-linux-shortcut-portal-regression.md @@ -0,0 +1 @@ +- Linux: Restored the runtime options, Jimaku, and Subsync shortcuts after the Electron 39 regression by routing those actions through the overlay's mpv/IPC shortcut path. diff --git a/src/config/definitions/shared.ts b/src/config/definitions/shared.ts index 3cf34241..adceef26 100644 --- a/src/config/definitions/shared.ts +++ b/src/config/definitions/shared.ts @@ -41,6 +41,7 @@ export interface ConfigTemplateSection { export const SPECIAL_COMMANDS = { SUBSYNC_TRIGGER: '__subsync-trigger', RUNTIME_OPTIONS_OPEN: '__runtime-options-open', + JIMAKU_OPEN: '__jimaku-open', RUNTIME_OPTION_CYCLE_PREFIX: '__runtime-option-cycle:', REPLAY_SUBTITLE: '__replay-subtitle', PLAY_NEXT_SUBTITLE: '__play-next-subtitle', diff --git a/src/core/services/ipc-command.test.ts b/src/core/services/ipc-command.test.ts index 4888fdfc..9326d9e9 100644 --- a/src/core/services/ipc-command.test.ts +++ b/src/core/services/ipc-command.test.ts @@ -10,6 +10,7 @@ function createOptions(overrides: Partial { calls.push('runtime-options'); }, + openJimaku: () => { + calls.push('jimaku'); + }, openYoutubeTrackPicker: () => { calls.push('youtube-picker'); }, @@ -114,6 +118,14 @@ test('handleMpvCommandFromIpc dispatches special youtube picker open command', ( assert.deepEqual(osd, []); }); +test('handleMpvCommandFromIpc dispatches special jimaku open command', () => { + const { options, calls, sentCommands, osd } = createOptions(); + handleMpvCommandFromIpc(['__jimaku-open'], options); + assert.deepEqual(calls, ['jimaku']); + assert.deepEqual(sentCommands, []); + assert.deepEqual(osd, []); +}); + test('handleMpvCommandFromIpc dispatches special playlist browser open command', async () => { const { options, calls, sentCommands, osd } = createOptions(); handleMpvCommandFromIpc(['__playlist-browser-open'], options); diff --git a/src/core/services/ipc-command.ts b/src/core/services/ipc-command.ts index 1a9e4144..9dbac091 100644 --- a/src/core/services/ipc-command.ts +++ b/src/core/services/ipc-command.ts @@ -9,6 +9,7 @@ export interface HandleMpvCommandFromIpcOptions { specialCommands: { SUBSYNC_TRIGGER: string; RUNTIME_OPTIONS_OPEN: string; + JIMAKU_OPEN: string; RUNTIME_OPTION_CYCLE_PREFIX: string; REPLAY_SUBTITLE: string; PLAY_NEXT_SUBTITLE: string; @@ -19,6 +20,7 @@ export interface HandleMpvCommandFromIpcOptions { }; triggerSubsyncFromConfig: () => void; openRuntimeOptionsPalette: () => void; + openJimaku: () => void; openYoutubeTrackPicker: () => void | Promise; openPlaylistBrowser: () => void | Promise; runtimeOptionsCycle: (id: RuntimeOptionId, direction: 1 | -1) => RuntimeOptionApplyResult; @@ -94,6 +96,11 @@ export function handleMpvCommandFromIpc( return; } + if (first === options.specialCommands.JIMAKU_OPEN) { + options.openJimaku(); + return; + } + if (first === options.specialCommands.YOUTUBE_PICKER_OPEN) { void options.openYoutubeTrackPicker(); return; diff --git a/src/main.ts b/src/main.ts index de310ab5..628d9332 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4191,6 +4191,7 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({ mpvCommandMainDeps: { triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(), openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(), + openJimaku: () => overlayModalRuntime.openJimaku(), openYoutubeTrackPicker: () => openYoutubeTrackPickerFromPlayback(), openPlaylistBrowser: () => openPlaylistBrowser(), cycleRuntimeOption: (id, direction) => { diff --git a/src/main/dependencies.ts b/src/main/dependencies.ts index 74a87a34..eb0f348c 100644 --- a/src/main/dependencies.ts +++ b/src/main/dependencies.ts @@ -197,6 +197,7 @@ export interface MpvCommandRuntimeServiceDepsParams { runtimeOptionsCycle: HandleMpvCommandFromIpcOptions['runtimeOptionsCycle']; triggerSubsyncFromConfig: HandleMpvCommandFromIpcOptions['triggerSubsyncFromConfig']; openRuntimeOptionsPalette: HandleMpvCommandFromIpcOptions['openRuntimeOptionsPalette']; + openJimaku: HandleMpvCommandFromIpcOptions['openJimaku']; openYoutubeTrackPicker: HandleMpvCommandFromIpcOptions['openYoutubeTrackPicker']; openPlaylistBrowser: HandleMpvCommandFromIpcOptions['openPlaylistBrowser']; showMpvOsd: HandleMpvCommandFromIpcOptions['showMpvOsd']; @@ -368,6 +369,7 @@ export function createMpvCommandRuntimeServiceDeps( specialCommands: params.specialCommands, triggerSubsyncFromConfig: params.triggerSubsyncFromConfig, openRuntimeOptionsPalette: params.openRuntimeOptionsPalette, + openJimaku: params.openJimaku, openYoutubeTrackPicker: params.openYoutubeTrackPicker, openPlaylistBrowser: params.openPlaylistBrowser, runtimeOptionsCycle: params.runtimeOptionsCycle, diff --git a/src/main/ipc-mpv-command.ts b/src/main/ipc-mpv-command.ts index 4fa34174..a36a2143 100644 --- a/src/main/ipc-mpv-command.ts +++ b/src/main/ipc-mpv-command.ts @@ -12,6 +12,7 @@ type MpvPropertyClientLike = { export interface MpvCommandFromIpcRuntimeDeps { triggerSubsyncFromConfig: () => void; openRuntimeOptionsPalette: () => void; + openJimaku: () => void; openYoutubeTrackPicker: () => void | Promise; openPlaylistBrowser: () => void | Promise; cycleRuntimeOption: (id: RuntimeOptionId, direction: 1 | -1) => RuntimeOptionApplyResult; @@ -35,6 +36,7 @@ export function handleMpvCommandFromIpcRuntime( specialCommands: SPECIAL_COMMANDS, triggerSubsyncFromConfig: deps.triggerSubsyncFromConfig, openRuntimeOptionsPalette: deps.openRuntimeOptionsPalette, + openJimaku: deps.openJimaku, openYoutubeTrackPicker: deps.openYoutubeTrackPicker, openPlaylistBrowser: deps.openPlaylistBrowser, runtimeOptionsCycle: deps.cycleRuntimeOption, diff --git a/src/main/overlay-runtime.ts b/src/main/overlay-runtime.ts index 527c7af2..0f5e0792 100644 --- a/src/main/overlay-runtime.ts +++ b/src/main/overlay-runtime.ts @@ -22,6 +22,7 @@ export interface OverlayModalRuntime { }, ) => boolean; openRuntimeOptionsPalette: () => void; + openJimaku: () => void; handleOverlayModalClosed: (modal: OverlayHostedModal) => void; notifyOverlayModalOpened: (modal: OverlayHostedModal) => void; waitForModalOpen: (modal: OverlayHostedModal, timeoutMs: number) => Promise; @@ -307,6 +308,12 @@ export function createOverlayModalRuntimeService( }); }; + const openJimaku = (): void => { + sendToActiveOverlayWindow('jimaku:open', undefined, { + restoreOnModalClose: 'jimaku', + }); + }; + const handleOverlayModalClosed = (modal: OverlayHostedModal): void => { if (!restoreVisibleOverlayOnModalClose.has(modal)) return; restoreVisibleOverlayOnModalClose.delete(modal); @@ -379,6 +386,7 @@ export function createOverlayModalRuntimeService( return { sendToActiveOverlayWindow, openRuntimeOptionsPalette, + openJimaku, handleOverlayModalClosed, notifyOverlayModalOpened, waitForModalOpen, diff --git a/src/main/runtime/composers/ipc-runtime-composer.test.ts b/src/main/runtime/composers/ipc-runtime-composer.test.ts index 9b3df025..d9f768af 100644 --- a/src/main/runtime/composers/ipc-runtime-composer.test.ts +++ b/src/main/runtime/composers/ipc-runtime-composer.test.ts @@ -10,6 +10,7 @@ test('composeIpcRuntimeHandlers returns callable IPC handlers and registration b mpvCommandMainDeps: { triggerSubsyncFromConfig: async () => {}, openRuntimeOptionsPalette: () => {}, + openJimaku: () => {}, openYoutubeTrackPicker: () => {}, openPlaylistBrowser: () => {}, cycleRuntimeOption: () => ({ ok: true }), diff --git a/src/main/runtime/ipc-bridge-actions-main-deps.test.ts b/src/main/runtime/ipc-bridge-actions-main-deps.test.ts index 4d1a3d8a..771d5bcd 100644 --- a/src/main/runtime/ipc-bridge-actions-main-deps.test.ts +++ b/src/main/runtime/ipc-bridge-actions-main-deps.test.ts @@ -13,6 +13,7 @@ test('ipc bridge action main deps builders map callbacks', async () => { buildMpvCommandDeps: () => ({ triggerSubsyncFromConfig: async () => {}, openRuntimeOptionsPalette: () => {}, + openJimaku: () => {}, openYoutubeTrackPicker: () => {}, openPlaylistBrowser: () => {}, cycleRuntimeOption: () => ({ ok: false as const, error: 'x' }), diff --git a/src/main/runtime/ipc-bridge-actions.test.ts b/src/main/runtime/ipc-bridge-actions.test.ts index 5c237f14..9b0e938a 100644 --- a/src/main/runtime/ipc-bridge-actions.test.ts +++ b/src/main/runtime/ipc-bridge-actions.test.ts @@ -10,6 +10,7 @@ test('handle mpv command handler forwards command and built deps', () => { const deps = { triggerSubsyncFromConfig: () => {}, openRuntimeOptionsPalette: () => {}, + openJimaku: () => {}, openYoutubeTrackPicker: () => {}, openPlaylistBrowser: () => {}, cycleRuntimeOption: () => ({ ok: false as const, error: 'x' }), diff --git a/src/main/runtime/ipc-mpv-command-main-deps.test.ts b/src/main/runtime/ipc-mpv-command-main-deps.test.ts index ebd59a19..6c81a74f 100644 --- a/src/main/runtime/ipc-mpv-command-main-deps.test.ts +++ b/src/main/runtime/ipc-mpv-command-main-deps.test.ts @@ -7,6 +7,7 @@ test('ipc mpv command main deps builder maps callbacks', () => { const deps = createBuildMpvCommandFromIpcRuntimeMainDepsHandler({ triggerSubsyncFromConfig: () => calls.push('subsync'), openRuntimeOptionsPalette: () => calls.push('palette'), + openJimaku: () => calls.push('jimaku'), openYoutubeTrackPicker: () => { calls.push('youtube-picker'); }, @@ -28,6 +29,7 @@ test('ipc mpv command main deps builder maps callbacks', () => { deps.triggerSubsyncFromConfig(); deps.openRuntimeOptionsPalette(); + deps.openJimaku(); void deps.openYoutubeTrackPicker(); void deps.openPlaylistBrowser(); assert.deepEqual(deps.cycleRuntimeOption('anki.nPlusOneMatchMode', 1), { ok: false, error: 'x' }); @@ -42,6 +44,7 @@ test('ipc mpv command main deps builder maps callbacks', () => { assert.deepEqual(calls, [ 'subsync', 'palette', + 'jimaku', 'youtube-picker', 'playlist-browser', 'osd:hello', diff --git a/src/main/runtime/ipc-mpv-command-main-deps.ts b/src/main/runtime/ipc-mpv-command-main-deps.ts index fd373179..c236e318 100644 --- a/src/main/runtime/ipc-mpv-command-main-deps.ts +++ b/src/main/runtime/ipc-mpv-command-main-deps.ts @@ -6,6 +6,7 @@ export function createBuildMpvCommandFromIpcRuntimeMainDepsHandler( return (): MpvCommandFromIpcRuntimeDeps => ({ triggerSubsyncFromConfig: () => deps.triggerSubsyncFromConfig(), openRuntimeOptionsPalette: () => deps.openRuntimeOptionsPalette(), + openJimaku: () => deps.openJimaku(), openYoutubeTrackPicker: () => deps.openYoutubeTrackPicker(), openPlaylistBrowser: () => deps.openPlaylistBrowser(), cycleRuntimeOption: (id, direction) => deps.cycleRuntimeOption(id, direction), diff --git a/src/renderer/handlers/keyboard.test.ts b/src/renderer/handlers/keyboard.test.ts index 51496ccb..d81b7c6f 100644 --- a/src/renderer/handlers/keyboard.test.ts +++ b/src/renderer/handlers/keyboard.test.ts @@ -53,6 +53,21 @@ function installKeyboardTestGlobals() { let playbackPausedResponse: boolean | null = false; let statsToggleKey = 'Backquote'; let markWatchedKey = 'KeyW'; + let configuredShortcuts = { + copySubtitle: '', + copySubtitleMultiple: '', + updateLastCardFromClipboard: '', + triggerFieldGrouping: '', + triggerSubsync: 'Ctrl+Alt+S', + mineSentence: '', + mineSentenceMultiple: '', + multiCopyTimeoutMs: 1000, + toggleSecondarySub: '', + markAudioCard: '', + openRuntimeOptions: 'CommandOrControl+Shift+O', + openJimaku: 'Ctrl+Shift+J', + toggleVisibleOverlayGlobal: '', + }; let markActiveVideoWatchedResult = true; let markActiveVideoWatchedCalls = 0; let statsToggleOverlayCalls = 0; @@ -138,6 +153,7 @@ function installKeyboardTestGlobals() { }, electronAPI: { getKeybindings: async () => [], + getConfiguredShortcuts: async () => configuredShortcuts, sendMpvCommand: (command: Array) => { mpvCommands.push(command); }, @@ -273,6 +289,9 @@ function installKeyboardTestGlobals() { setMarkWatchedKey: (value: string) => { markWatchedKey = value; }, + setConfiguredShortcuts: (value: typeof configuredShortcuts) => { + configuredShortcuts = value; + }, setMarkActiveVideoWatchedResult: (value: boolean) => { markActiveVideoWatchedResult = value; }, @@ -315,6 +334,7 @@ function createKeyboardHandlerHarness() { overlay: testGlobals.overlay, }, platform: { + isLinuxPlatform: false, shouldToggleMouseIgnore: false, isMacOSPlatform: false, isModalLayer: false, @@ -765,6 +785,51 @@ test('youtube picker: unhandled keys still dispatch mpv keybindings', async () = } }); +test('linux overlay shortcut: Ctrl+Alt+S dispatches subsync special command locally', async () => { + const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness(); + + try { + ctx.platform.isLinuxPlatform = true; + await handlers.setupMpvInputForwarding(); + + testGlobals.dispatchKeydown({ key: 's', code: 'KeyS', ctrlKey: true, altKey: true }); + + assert.deepEqual(testGlobals.mpvCommands, [['__subsync-trigger']]); + } finally { + testGlobals.restore(); + } +}); + +test('linux overlay shortcut: Ctrl+Shift+J dispatches jimaku special command locally', async () => { + const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness(); + + try { + ctx.platform.isLinuxPlatform = true; + await handlers.setupMpvInputForwarding(); + + testGlobals.dispatchKeydown({ key: 'J', code: 'KeyJ', ctrlKey: true, shiftKey: true }); + + assert.deepEqual(testGlobals.mpvCommands, [['__jimaku-open']]); + } finally { + testGlobals.restore(); + } +}); + +test('linux overlay shortcut: CommandOrControl+Shift+O dispatches runtime options locally', async () => { + const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness(); + + try { + ctx.platform.isLinuxPlatform = true; + await handlers.setupMpvInputForwarding(); + + testGlobals.dispatchKeydown({ key: 'O', code: 'KeyO', ctrlKey: true, shiftKey: true }); + + assert.deepEqual(testGlobals.mpvCommands, [['__runtime-options-open']]); + } finally { + testGlobals.restore(); + } +}); + test('keyboard mode: h moves left when popup is closed', async () => { const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness(); diff --git a/src/renderer/handlers/keyboard.ts b/src/renderer/handlers/keyboard.ts index 04a4dd78..33f6c9a7 100644 --- a/src/renderer/handlers/keyboard.ts +++ b/src/renderer/handlers/keyboard.ts @@ -1,4 +1,5 @@ -import type { Keybinding } from '../../types'; +import { SPECIAL_COMMANDS } from '../../config/definitions'; +import type { Keybinding, ShortcutsConfig } from '../../types'; import type { RendererContext } from '../context'; import { YOMITAN_POPUP_HIDDEN_EVENT, @@ -35,6 +36,7 @@ export function createKeyboardHandlers( // Timeout for the modal chord capture window (e.g. Y followed by H/K). const CHORD_TIMEOUT_MS = 1000; const KEYBOARD_SELECTED_WORD_CLASS = 'keyboard-selected'; + const linuxOverlayShortcutCommands = new Map(); let pendingSelectionAnchorAfterSubtitleSeek: 'start' | 'end' | null = null; let pendingLookupRefreshAfterSubtitleSeek = false; let resetSelectionToStartOnNextSubtitleSync = false; @@ -74,6 +76,117 @@ export function createKeyboardHandlers( return parts.join('+'); } + function acceleratorToKeyToken(token: string): string | null { + const normalized = token.trim(); + if (!normalized) return null; + if (/^[a-z]$/i.test(normalized)) { + return `Key${normalized.toUpperCase()}`; + } + if (/^[0-9]$/.test(normalized)) { + return `Digit${normalized}`; + } + const exactMap: Record = { + space: 'Space', + tab: 'Tab', + enter: 'Enter', + return: 'Enter', + esc: 'Escape', + escape: 'Escape', + up: 'ArrowUp', + down: 'ArrowDown', + left: 'ArrowLeft', + right: 'ArrowRight', + backspace: 'Backspace', + delete: 'Delete', + slash: 'Slash', + backslash: 'Backslash', + minus: 'Minus', + plus: 'Equal', + equal: 'Equal', + comma: 'Comma', + period: 'Period', + quote: 'Quote', + semicolon: 'Semicolon', + bracketleft: 'BracketLeft', + bracketright: 'BracketRight', + backquote: 'Backquote', + }; + const lower = normalized.toLowerCase(); + if (exactMap[lower]) return exactMap[lower]; + if (/^key[a-z]$/i.test(normalized) || /^digit[0-9]$/i.test(normalized)) { + return normalized[0]!.toUpperCase() + normalized.slice(1); + } + if (/^arrow(?:up|down|left|right)$/i.test(normalized)) { + return normalized[0]!.toUpperCase() + normalized.slice(1); + } + if (/^f\d{1,2}$/i.test(normalized)) { + return normalized.toUpperCase(); + } + return null; + } + + function acceleratorToKeyString(accelerator: string): string | null { + const normalized = accelerator + .replace(/\s+/g, '') + .replace(/cmdorctrl/gi, 'CommandOrControl'); + if (!normalized) return null; + const parts = normalized.split('+').filter(Boolean); + const keyToken = parts.pop(); + if (!keyToken) return null; + + const eventParts: string[] = []; + for (const modifier of parts) { + const lower = modifier.toLowerCase(); + if (lower === 'ctrl' || lower === 'control') { + eventParts.push('Ctrl'); + continue; + } + if (lower === 'alt' || lower === 'option') { + eventParts.push('Alt'); + continue; + } + if (lower === 'shift') { + eventParts.push('Shift'); + continue; + } + if (lower === 'meta' || lower === 'super' || lower === 'command' || lower === 'cmd') { + eventParts.push('Meta'); + continue; + } + if (lower === 'commandorcontrol') { + eventParts.push(ctx.platform.isMacOSPlatform ? 'Meta' : 'Ctrl'); + continue; + } + return null; + } + + const normalizedKey = acceleratorToKeyToken(keyToken); + if (!normalizedKey) return null; + eventParts.push(normalizedKey); + return eventParts.join('+'); + } + + function updateConfiguredShortcuts(shortcuts: Required): void { + linuxOverlayShortcutCommands.clear(); + const bindings: Array<[string | null, (string | number)[]]> = [ + [shortcuts.triggerSubsync, [SPECIAL_COMMANDS.SUBSYNC_TRIGGER]], + [shortcuts.openRuntimeOptions, [SPECIAL_COMMANDS.RUNTIME_OPTIONS_OPEN]], + [shortcuts.openJimaku, [SPECIAL_COMMANDS.JIMAKU_OPEN]], + ]; + + for (const [accelerator, command] of bindings) { + if (!accelerator) continue; + const keyString = acceleratorToKeyString(accelerator); + if (keyString) { + linuxOverlayShortcutCommands.set(keyString, command); + } + } + } + + async function refreshConfiguredShortcuts(): Promise { + updateConfiguredShortcuts(await window.electronAPI.getConfiguredShortcuts()); + } + function dispatchYomitanPopupKeydown( key: string, code: string, @@ -779,12 +892,14 @@ export function createKeyboardHandlers( } async function setupMpvInputForwarding(): Promise { - const [keybindings, statsToggleKey, markWatchedKey] = await Promise.all([ + const [keybindings, shortcuts, statsToggleKey, markWatchedKey] = await Promise.all([ window.electronAPI.getKeybindings(), + window.electronAPI.getConfiguredShortcuts(), window.electronAPI.getStatsToggleKey(), window.electronAPI.getMarkWatchedKey(), ]); updateKeybindings(keybindings); + updateConfiguredShortcuts(shortcuts); ctx.state.statsToggleKey = statsToggleKey; ctx.state.markWatchedKey = markWatchedKey; syncKeyboardTokenSelection(); @@ -982,6 +1097,14 @@ export function createKeyboardHandlers( } const keyString = keyEventToString(e); + const linuxOverlayCommand = ctx.platform.isLinuxPlatform + ? linuxOverlayShortcutCommands.get(keyString) + : undefined; + if (linuxOverlayCommand) { + e.preventDefault(); + dispatchConfiguredMpvCommand(linuxOverlayCommand); + return; + } const command = ctx.state.keybindingsMap.get(keyString); if (command) { @@ -1015,6 +1138,7 @@ export function createKeyboardHandlers( return { setupMpvInputForwarding, + refreshConfiguredShortcuts, updateKeybindings, syncKeyboardTokenSelection, handleSubtitleContentUpdated, diff --git a/src/renderer/modals/session-help.ts b/src/renderer/modals/session-help.ts index 30a7982c..65f10f81 100644 --- a/src/renderer/modals/session-help.ts +++ b/src/renderer/modals/session-help.ts @@ -130,6 +130,7 @@ function describeCommand(command: (string | number)[]): string { } if (first === SPECIAL_COMMANDS.SUBSYNC_TRIGGER) return 'Open subtitle sync controls'; if (first === SPECIAL_COMMANDS.RUNTIME_OPTIONS_OPEN) return 'Open runtime options'; + if (first === SPECIAL_COMMANDS.JIMAKU_OPEN) return 'Open jimaku'; if (first === SPECIAL_COMMANDS.PLAYLIST_BROWSER_OPEN) return 'Open playlist browser'; if (first === SPECIAL_COMMANDS.REPLAY_SUBTITLE) return 'Replay current subtitle'; if (first === SPECIAL_COMMANDS.PLAY_NEXT_SUBTITLE) return 'Play next subtitle'; @@ -165,6 +166,7 @@ function sectionForCommand(command: (string | number)[]): string { if ( first === SPECIAL_COMMANDS.RUNTIME_OPTIONS_OPEN || + first === SPECIAL_COMMANDS.JIMAKU_OPEN || first === SPECIAL_COMMANDS.PLAYLIST_BROWSER_OPEN || first.startsWith(SPECIAL_COMMANDS.RUNTIME_OPTION_CYCLE_PREFIX) ) { diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index 5954befd..1ab8fd5d 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -621,6 +621,7 @@ async function init(): Promise { window.electronAPI.onConfigHotReload((payload: ConfigHotReloadPayload) => { runGuarded('config:hot-reload', () => { keyboardHandlers.updateKeybindings(payload.keybindings); + void keyboardHandlers.refreshConfiguredShortcuts(); subtitleRenderer.applySubtitleStyle(payload.subtitleStyle); subtitleRenderer.updateSecondarySubMode(payload.secondarySubMode); ctx.state.subtitleSidebarConfig = payload.subtitleSidebar;