diff --git a/backlog/tasks/task-292 - Restore-Linux-multi-subtitle-copy-digit-capture.md b/backlog/tasks/task-292 - Restore-Linux-multi-subtitle-copy-digit-capture.md new file mode 100644 index 00000000..5ef5b4cc --- /dev/null +++ b/backlog/tasks/task-292 - Restore-Linux-multi-subtitle-copy-digit-capture.md @@ -0,0 +1,52 @@ +--- +id: TASK-292 +title: Restore Linux multi-subtitle copy digit capture +status: Done +assignee: + - '@codex' +created_date: '2026-04-25 21:31' +updated_date: '2026-04-25 21:36' +labels: + - bug + - linux + - shortcuts + - clipboard +dependencies: [] +priority: high +--- + +## Description + + +On Linux, the copy-subtitle-multiple shortcut opens the numeric prompt but the follow-up digit is not captured, so the flow times out. User confirmed `wl-copy` itself is installed and working, so investigate the shortcut/digit capture path and restore multi-line subtitle copy without regressing existing session action behavior. + + +## Acceptance Criteria + +- [x] #1 Linux copy-subtitle-multiple shortcut accepts a follow-up digit and copies that number of recent subtitle lines instead of timing out. +- [x] #2 The fix avoids depending on Linux Electron global shortcut digit registration for the follow-up numeric selection when a renderer-visible session can handle it. +- [x] #3 Regression tests cover the Linux multi-copy shortcut/digit flow and existing non-Linux/global shortcut behavior remains intact. + + +## Implementation Plan + + +1. Add failing regression coverage for Linux copy-subtitle-multiple local shortcut fallback starting renderer/session numeric selection instead of main-process digit globalShortcut capture. +2. Patch the overlay shortcut fallback/runtime path so Linux visible-overlay multi-copy and mine-sentence-multiple can dispatch session-action numeric selection when renderer handling is available, while preserving main-process numeric sessions for CLI/non-renderer paths. +3. Run targeted tests for shortcut fallback, overlay runtime, and renderer keyboard numeric selection; then run typecheck or a wider focused gate if needed. +4. Update task acceptance criteria/final notes after verification. + + +## Implementation Notes + + +Implemented the approved path by keeping multi-step numeric overlay shortcuts out of the main-process local fallback. The visible overlay now receives the original keydown and uses the existing renderer/session-action numeric selection flow for follow-up digits, avoiding Linux Electron globalShortcut digit capture for multi-copy and mine-sentence-multiple. Verification: targeted shortcut/renderer tests and changelog lint pass. `bun run typecheck` is currently blocked by unrelated existing errors in CLI/AniList dictionary-candidate work and `src/main/dependencies.ts` manual-selection API shape. + + +## Final Summary + + +Restored Linux multi-line subtitle copy by preventing main-process overlay shortcut fallback from consuming multi-step numeric shortcuts (`copySubtitleMultiple` and `mineSentenceMultiple`). Those shortcuts now fall through to the visible overlay renderer, where the existing session binding flow prompts for a digit and dispatches the counted session action locally instead of relying on Electron globalShortcut digit registration. Added regression coverage for the fallback behavior and renderer follow-up digit dispatch, plus a changelog fragment. + +Verification: `bun test src/core/services/overlay-shortcut-handler.test.ts src/renderer/handlers/keyboard.test.ts`; `bun run changelog:lint`. Full `bun run typecheck` was attempted but is blocked by unrelated current worktree errors in CLI/AniList dictionary-candidate tests/types and `src/main/dependencies.ts`. + diff --git a/changes/292-linux-multi-copy.md b/changes/292-linux-multi-copy.md new file mode 100644 index 00000000..38612c51 --- /dev/null +++ b/changes/292-linux-multi-copy.md @@ -0,0 +1,4 @@ +type: fixed +area: overlay + +- Fixed Linux multi-line subtitle copy timing out after the prompt by letting the overlay handle the follow-up digit locally. diff --git a/src/core/services/overlay-shortcut-handler.test.ts b/src/core/services/overlay-shortcut-handler.test.ts index f2d80b78..2c168c06 100644 --- a/src/core/services/overlay-shortcut-handler.test.ts +++ b/src/core/services/overlay-shortcut-handler.test.ts @@ -135,12 +135,11 @@ test('createOverlayShortcutRuntimeHandlers reports async failures via OSD', asyn } }); -test('runOverlayShortcutLocalFallback dispatches matching actions with timeout', () => { +test('runOverlayShortcutLocalFallback dispatches matching single-step actions', () => { const handled: string[] = []; const matched: Array<{ accelerator: string; allowWhenRegistered: boolean }> = []; const shortcuts = makeShortcuts({ - copySubtitleMultiple: 'Ctrl+M', - multiCopyTimeoutMs: 4321, + copySubtitle: 'Ctrl+M', }); const result = runOverlayShortcutLocalFallback( @@ -169,10 +168,61 @@ test('runOverlayShortcutLocalFallback dispatches matching actions with timeout', ); assert.equal(result, true); - assert.deepEqual(handled, ['copySubtitleMultiple:4321']); + assert.deepEqual(handled, ['copySubtitle']); assert.deepEqual(matched, [{ accelerator: 'Ctrl+M', allowWhenRegistered: false }]); }); +test('runOverlayShortcutLocalFallback leaves multi-step numeric shortcuts for renderer handling', () => { + const handled: string[] = []; + const shortcuts = makeShortcuts({ + copySubtitleMultiple: 'Ctrl+M', + mineSentenceMultiple: 'Ctrl+N', + multiCopyTimeoutMs: 4321, + }); + + const copyResult = runOverlayShortcutLocalFallback( + {} as Electron.Input, + shortcuts, + (_input, accelerator) => accelerator === 'Ctrl+M', + { + openRuntimeOptions: () => handled.push('openRuntimeOptions'), + openJimaku: () => handled.push('openJimaku'), + markAudioCard: () => handled.push('markAudioCard'), + copySubtitleMultiple: (timeoutMs) => handled.push(`copySubtitleMultiple:${timeoutMs}`), + copySubtitle: () => handled.push('copySubtitle'), + toggleSecondarySub: () => handled.push('toggleSecondarySub'), + updateLastCardFromClipboard: () => handled.push('updateLastCardFromClipboard'), + triggerFieldGrouping: () => handled.push('triggerFieldGrouping'), + triggerSubsync: () => handled.push('triggerSubsync'), + mineSentence: () => handled.push('mineSentence'), + mineSentenceMultiple: (timeoutMs) => handled.push(`mineSentenceMultiple:${timeoutMs}`), + }, + ); + + const mineResult = runOverlayShortcutLocalFallback( + {} as Electron.Input, + shortcuts, + (_input, accelerator) => accelerator === 'Ctrl+N', + { + openRuntimeOptions: () => handled.push('openRuntimeOptions'), + openJimaku: () => handled.push('openJimaku'), + markAudioCard: () => handled.push('markAudioCard'), + copySubtitleMultiple: (timeoutMs) => handled.push(`copySubtitleMultiple:${timeoutMs}`), + copySubtitle: () => handled.push('copySubtitle'), + toggleSecondarySub: () => handled.push('toggleSecondarySub'), + updateLastCardFromClipboard: () => handled.push('updateLastCardFromClipboard'), + triggerFieldGrouping: () => handled.push('triggerFieldGrouping'), + triggerSubsync: () => handled.push('triggerSubsync'), + mineSentence: () => handled.push('mineSentence'), + mineSentenceMultiple: (timeoutMs) => handled.push(`mineSentenceMultiple:${timeoutMs}`), + }, + ); + + assert.equal(copyResult, false); + assert.equal(mineResult, false); + assert.deepEqual(handled, []); +}); + test('runOverlayShortcutLocalFallback passes allowWhenRegistered for secondary-sub toggle', () => { const matched: Array<{ accelerator: string; allowWhenRegistered: boolean }> = []; const shortcuts = makeShortcuts({ diff --git a/src/core/services/overlay-shortcut-handler.ts b/src/core/services/overlay-shortcut-handler.ts index 09d6e06d..b9517eb3 100644 --- a/src/core/services/overlay-shortcut-handler.ts +++ b/src/core/services/overlay-shortcut-handler.ts @@ -147,12 +147,6 @@ export function runOverlayShortcutLocalFallback( handlers.markAudioCard(); }, }, - { - accelerator: shortcuts.copySubtitleMultiple, - run: () => { - handlers.copySubtitleMultiple(shortcuts.multiCopyTimeoutMs); - }, - }, { accelerator: shortcuts.copySubtitle, run: () => { @@ -188,12 +182,6 @@ export function runOverlayShortcutLocalFallback( handlers.mineSentence(); }, }, - { - accelerator: shortcuts.mineSentenceMultiple, - run: () => { - handlers.mineSentenceMultiple(shortcuts.multiCopyTimeoutMs); - }, - }, ]; for (const action of actions) { diff --git a/src/renderer/handlers/keyboard.test.ts b/src/renderer/handlers/keyboard.test.ts index 3d6caa0e..3250ee44 100644 --- a/src/renderer/handlers/keyboard.test.ts +++ b/src/renderer/handlers/keyboard.test.ts @@ -1119,6 +1119,32 @@ test('session binding: Ctrl+Shift+O dispatches runtime options locally', async ( } }); +test('session binding: copy subtitle multiple captures follow-up digit locally', async () => { + const { handlers, testGlobals } = createKeyboardHandlerHarness(); + + try { + await handlers.setupMpvInputForwarding(); + handlers.updateSessionBindings([ + { + sourcePath: 'shortcuts.copySubtitleMultiple', + originalKey: 'Ctrl+M', + key: { code: 'KeyM', modifiers: ['ctrl'] }, + actionType: 'session-action', + actionId: 'copySubtitleMultiple', + }, + ] as never); + + testGlobals.dispatchKeydown({ key: 'm', code: 'KeyM', ctrlKey: true }); + testGlobals.dispatchKeydown({ key: '3', code: 'Digit3' }); + + assert.deepEqual(testGlobals.sessionActions, [ + { actionId: 'copySubtitleMultiple', payload: { count: 3 } }, + ]); + } finally { + testGlobals.restore(); + } +}); + test('keyboard mode: h moves left when popup is closed', async () => { const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness();