diff --git a/launcher/main.test.ts b/launcher/main.test.ts index 0764f523..5cd5c42a 100644 --- a/launcher/main.test.ts +++ b/launcher/main.test.ts @@ -321,7 +321,6 @@ test('launcher forwards --args to mpv as parsed tokens', { timeout: 15000 }, () fs.mkdirSync(binDir, { recursive: true }); fs.mkdirSync(path.join(xdgConfigHome, 'SubMiner'), { recursive: true }); - fs.mkdirSync(path.join(xdgConfigHome, 'SubMiner'), { recursive: true }); fs.writeFileSync(videoPath, 'fake video content'); fs.writeFileSync( path.join(xdgConfigHome, 'SubMiner', 'setup-state.json'), @@ -408,7 +407,6 @@ test('launcher forwards non-info log level into mpv plugin script opts', { timeo fs.mkdirSync(binDir, { recursive: true }); fs.mkdirSync(path.join(xdgConfigHome, 'SubMiner'), { recursive: true }); - fs.mkdirSync(path.join(xdgConfigHome, 'SubMiner'), { recursive: true }); fs.writeFileSync(videoPath, 'fake video content'); fs.writeFileSync( path.join(xdgConfigHome, 'SubMiner', 'setup-state.json'), @@ -485,7 +483,6 @@ test('launcher routes youtube urls through regular playback startup', { timeout: fs.mkdirSync(binDir, { recursive: true }); fs.mkdirSync(path.join(xdgConfigHome, 'SubMiner'), { recursive: true }); - fs.mkdirSync(path.join(xdgConfigHome, 'SubMiner'), { recursive: true }); fs.writeFileSync( path.join(xdgConfigHome, 'SubMiner', 'setup-state.json'), JSON.stringify({ diff --git a/main.js b/main.js index b75a69d6..6394f3c5 100644 --- a/main.js +++ b/main.js @@ -295,10 +295,11 @@ const texthookerService = new services_1.Texthooker(() => { const config = getResolvedConfig(); const characterDictionaryEnabled = config.anilist.characterDictionary.enabled && yomitanProfilePolicy.isCharacterDictionaryEnabled(); - const knownAndNPlusOneEnabled = getRuntimeBooleanOption('subtitle.annotation.nPlusOne', config.ankiConnect.nPlusOne.enabled); + const knownWordColoringEnabled = getRuntimeBooleanOption('subtitle.annotation.knownWords.highlightEnabled', config.ankiConnect.knownWords.highlightEnabled); + const nPlusOneColoringEnabled = getRuntimeBooleanOption('subtitle.annotation.nPlusOne', config.ankiConnect.nPlusOne.enabled); return { - enableKnownWordColoring: knownAndNPlusOneEnabled, - enableNPlusOneColoring: knownAndNPlusOneEnabled, + enableKnownWordColoring: knownWordColoringEnabled, + enableNPlusOneColoring: nPlusOneColoringEnabled, enableNameMatchColoring: config.subtitleStyle.nameMatchEnabled && characterDictionaryEnabled, enableFrequencyColoring: getRuntimeBooleanOption('subtitle.annotation.frequency', config.subtitleStyle.frequencyDictionary.enabled), enableJlptColoring: getRuntimeBooleanOption('subtitle.annotation.jlpt', config.subtitleStyle.enableJlpt), @@ -1819,10 +1820,11 @@ function getRuntimeBooleanOption(id, fallback) { } function shouldInitializeMecabForAnnotations() { const config = getResolvedConfig(); + const knownWordsEnabled = getRuntimeBooleanOption('subtitle.annotation.knownWords.highlightEnabled', config.ankiConnect.knownWords.highlightEnabled); const nPlusOneEnabled = getRuntimeBooleanOption('subtitle.annotation.nPlusOne', config.ankiConnect.nPlusOne.enabled); const jlptEnabled = getRuntimeBooleanOption('subtitle.annotation.jlpt', config.subtitleStyle.enableJlpt); const frequencyEnabled = getRuntimeBooleanOption('subtitle.annotation.frequency', config.subtitleStyle.frequencyDictionary.enabled); - return nPlusOneEnabled || jlptEnabled || frequencyEnabled; + return knownWordsEnabled || nPlusOneEnabled || jlptEnabled || frequencyEnabled; } const { getResolvedJellyfinConfig, reportJellyfinRemoteProgress, reportJellyfinRemoteStopped, startJellyfinRemoteSession, stopJellyfinRemoteSession, runJellyfinCommand, openJellyfinSetupWindow, getJellyfinClientInfo, } = (0, composers_1.composeJellyfinRuntimeHandlers)({ getResolvedJellyfinConfigMainDeps: { @@ -4706,4 +4708,4 @@ function appendClipboardVideoToQueue() { return appendClipboardVideoToQueueHandler(); } registerIpcRuntimeHandlers(); -//# sourceMappingURL=main.js.map \ No newline at end of file +//# sourceMappingURL=main.js.map diff --git a/package.json b/package.json index 7429ca5a..18e6e179 100644 --- a/package.json +++ b/package.json @@ -50,8 +50,8 @@ "test:plugin:src": "lua scripts/test-plugin-lua-compat.lua && lua scripts/test-plugin-start-gate.lua && lua scripts/test-plugin-session-bindings.lua && lua scripts/test-plugin-binary-windows.lua", "test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts", "test:launcher:src": "bun test launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/mpv.test.ts launcher/picker.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/commands/update-command.test.ts launcher/smoke.e2e.test.ts && bun run test:plugin:src", - "test:core:src": "bun test src/preload-settings.test.ts src/settings/settings-anki-controls.test.ts src/cli/args.test.ts src/cli/help.test.ts src/shared/setup-state.test.ts src/core/services/cli-command.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/yomitan-extension-paths.test.ts src/core/services/yomitan-extension-loader.test.ts src/core/services/yomitan-settings.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/stats-window.test.ts src/core/services/__tests__/stats-server.test.ts src/main/runtime/stats-server-routing.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jimaku-download-path.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/main/runtime/current-subtitle-snapshot.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/services/anilist/rate-limiter.test.ts src/core/services/jlpt-token-filter.test.ts src/core/services/subtitle-position.test.ts src/core/utils/shortcut-config.test.ts src/main/runtime/startup-mode-flags.test.ts src/main/runtime/first-run-setup-plugin.test.ts src/main/runtime/first-run-setup-service.test.ts src/main/runtime/first-run-setup-window.test.ts src/main/runtime/command-line-launcher.test.ts src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts src/main/runtime/tray-main-deps.test.ts src/main/runtime/tray-runtime-handlers.test.ts src/main/runtime/cli-command-context-main-deps.test.ts src/main/runtime/app-ready-main-deps.test.ts src/main/runtime/update/appimage-updater.test.ts src/main/runtime/update/fetch-adapter.test.ts src/main/runtime/update/release-metadata-policy.test.ts src/main/runtime/update/update-dialogs.test.ts src/main/runtime/update/support-assets.test.ts src/renderer/error-recovery.test.ts src/renderer/subtitle-render.test.ts src/renderer/handlers/mouse.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/modals/jimaku.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/hyprland-tracker.test.ts src/window-trackers/x11-tracker.test.ts src/window-trackers/windows-helper.test.ts src/window-trackers/windows-tracker.test.ts launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/commands/update-command.test.ts launcher/setup-gate.test.ts stats/src/lib/api-client.test.ts stats/src/hooks/useExcludedWords.test.ts", - "test:core:dist": "bun test dist/preload-settings.test.js dist/settings/settings-anki-controls.test.js dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/yomitan-extension-paths.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jimaku-download-path.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/overlay-runtime-init.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/main/runtime/current-subtitle-snapshot.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/core/services/anilist/rate-limiter.test.js dist/core/services/jlpt-token-filter.test.js dist/core/services/subtitle-position.test.js dist/renderer/error-recovery.test.js dist/renderer/subtitle-render.test.js dist/renderer/handlers/mouse.test.js dist/renderer/handlers/keyboard.test.js dist/renderer/modals/jimaku.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/hyprland-tracker.test.js dist/window-trackers/x11-tracker.test.js dist/window-trackers/windows-helper.test.js dist/window-trackers/windows-tracker.test.js", + "test:core:src": "bun test src/preload-settings.test.ts src/settings/settings-anki-controls.test.ts src/settings/settings-model.test.ts src/cli/args.test.ts src/cli/help.test.ts src/shared/setup-state.test.ts src/core/services/cli-command.test.ts src/core/services/ipc.test.ts src/core/services/anki-jimaku-ipc.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/yomitan-extension-paths.test.ts src/core/services/yomitan-extension-loader.test.ts src/core/services/yomitan-settings.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/stats-window.test.ts src/core/services/__tests__/stats-server.test.ts src/main/runtime/stats-server-routing.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jimaku-download-path.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/main/runtime/current-subtitle-snapshot.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/services/anilist/rate-limiter.test.ts src/core/services/jlpt-token-filter.test.ts src/core/services/subtitle-position.test.ts src/core/utils/shortcut-config.test.ts src/main/runtime/startup-mode-flags.test.ts src/main/runtime/first-run-setup-plugin.test.ts src/main/runtime/first-run-setup-service.test.ts src/main/runtime/first-run-setup-window.test.ts src/main/runtime/command-line-launcher.test.ts src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts src/main/runtime/tray-main-deps.test.ts src/main/runtime/tray-runtime-handlers.test.ts src/main/runtime/cli-command-context-main-deps.test.ts src/main/runtime/app-ready-main-deps.test.ts src/main/runtime/update/appimage-updater.test.ts src/main/runtime/update/fetch-adapter.test.ts src/main/runtime/update/release-metadata-policy.test.ts src/main/runtime/update/update-dialogs.test.ts src/main/runtime/update/support-assets.test.ts src/renderer/error-recovery.test.ts src/renderer/subtitle-render.test.ts src/renderer/handlers/mouse.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/modals/jimaku.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/hyprland-tracker.test.ts src/window-trackers/x11-tracker.test.ts src/window-trackers/windows-helper.test.ts src/window-trackers/windows-tracker.test.ts launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/commands/update-command.test.ts launcher/setup-gate.test.ts stats/src/lib/api-client.test.ts stats/src/hooks/useExcludedWords.test.ts", + "test:core:dist": "bun test dist/preload-settings.test.js dist/settings/settings-anki-controls.test.js dist/settings/settings-model.test.js dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/yomitan-extension-paths.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jimaku-download-path.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/overlay-runtime-init.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/main/runtime/current-subtitle-snapshot.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/core/services/anilist/rate-limiter.test.js dist/core/services/jlpt-token-filter.test.js dist/core/services/subtitle-position.test.js dist/renderer/error-recovery.test.js dist/renderer/subtitle-render.test.js dist/renderer/handlers/mouse.test.js dist/renderer/handlers/keyboard.test.js dist/renderer/modals/jimaku.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/hyprland-tracker.test.js dist/window-trackers/x11-tracker.test.js dist/window-trackers/windows-helper.test.js dist/window-trackers/windows-tracker.test.js", "test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js", "test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist", "test:subtitle:src": "bun test src/core/services/subsync.test.ts src/subsync/utils.test.ts", diff --git a/plugin/subminer/process.lua b/plugin/subminer/process.lua index d24e4a70..382ddf13 100644 --- a/plugin/subminer/process.lua +++ b/plugin/subminer/process.lua @@ -504,7 +504,14 @@ function M.create(ctx) subminer_log("info", "process", "Restarting overlay...") show_osd("Restarting...") - run_control_command_async("stop", nil, function() + run_control_command_async("stop", nil, function(ok, result) + if not ok then + local reason = result and result.stderr or "unknown error" + subminer_log("warn", "process", "Restart stop command failed: " .. reason) + show_osd("Restart failed") + return + end + state.overlay_running = false state.texthooker_running = false disarm_auto_play_ready_gate() diff --git a/scripts/test-plugin-start-gate.lua b/scripts/test-plugin-start-gate.lua index dd7128af..487068f0 100644 --- a/scripts/test-plugin-start-gate.lua +++ b/scripts/test-plugin-start-gate.lua @@ -108,6 +108,13 @@ local function run_plugin_scenario(config) return end end + for _, value in ipairs(args) do + if value == "--stop" and config.stop_command_fails then + local stderr = config.stop_command_stderr or "stop failed" + callback(false, { status = 1, stdout = "", stderr = stderr }, stderr) + return + end + end callback(true, { status = 0, stdout = "", stderr = "" }, nil) end end @@ -593,6 +600,28 @@ do ) end +do + local recorded, err = run_plugin_scenario({ + process_list = "", + stop_command_fails = true, + stop_command_stderr = "stop refused", + option_overrides = { + binary_path = binary_path, + }, + files = { + [binary_path] = true, + }, + }) + assert_true(recorded ~= nil, "plugin failed to load for failed restart-stop scenario: " .. tostring(err)) + recorded.script_messages["subminer-restart"]() + assert_true(find_control_call(recorded.async_calls, "--stop") ~= nil, "restart should attempt stop") + assert_true(count_start_calls(recorded.async_calls) == 0, "restart should not start overlay when stop fails") + assert_true( + has_osd_message(recorded.osd, "SubMiner: Restart failed"), + "restart stop failure should show failure OSD" + ) +end + do local recorded, err = run_plugin_scenario({ process_list = "", diff --git a/src/config/config.test.ts b/src/config/config.test.ts index d500c220..cffc1921 100644 --- a/src/config/config.test.ts +++ b/src/config/config.test.ts @@ -1842,6 +1842,7 @@ test('runtime options registry is centralized', () => { const ids = RUNTIME_OPTION_REGISTRY.map((entry) => entry.id); assert.deepEqual(ids, [ 'anki.autoUpdateNewCards', + 'subtitle.annotation.knownWords.highlightEnabled', 'subtitle.annotation.nPlusOne', 'subtitle.annotation.jlpt', 'subtitle.annotation.frequency', diff --git a/src/config/definitions/runtime-options.ts b/src/config/definitions/runtime-options.ts index a8ca7b93..f5260e2e 100644 --- a/src/config/definitions/runtime-options.ts +++ b/src/config/definitions/runtime-options.ts @@ -20,9 +20,9 @@ export function buildRuntimeOptionRegistry( }), }, { - id: 'subtitle.annotation.nPlusOne', + id: 'subtitle.annotation.knownWords.highlightEnabled', path: 'ankiConnect.knownWords.highlightEnabled', - label: 'N+1 Annotation', + label: 'Known Word Annotation', scope: 'subtitle', valueType: 'boolean', allowedValues: [true, false], @@ -35,6 +35,22 @@ export function buildRuntimeOptionRegistry( }, }), }, + { + id: 'subtitle.annotation.nPlusOne', + path: 'ankiConnect.nPlusOne.enabled', + label: 'N+1 Annotation', + scope: 'subtitle', + valueType: 'boolean', + allowedValues: [true, false], + defaultValue: defaultConfig.ankiConnect.nPlusOne.enabled, + requiresRestart: false, + formatValueForOsd: (value) => (value === true ? 'On' : 'Off'), + toAnkiPatch: (value) => ({ + nPlusOne: { + enabled: value === true, + }, + }), + }, { id: 'subtitle.annotation.jlpt', path: 'subtitleStyle.enableJlpt', diff --git a/src/config/settings/registry.ts b/src/config/settings/registry.ts index 93198ebb..6f4206fd 100644 --- a/src/config/settings/registry.ts +++ b/src/config/settings/registry.ts @@ -89,7 +89,7 @@ const JSON_OBJECT_FIELDS = new Set([ const SECRET_PATHS = new Set(['ai.apiKey', 'jimaku.apiKey', 'anilist.accessToken']); -const COLOR_SUFFIXES = new Set(['Color', 'color', 'backgroundColor', 'singleColor', 'nPlusOne']); +const COLOR_SUFFIXES = new Set(['Color', 'color', 'backgroundColor', 'singleColor']); const SUBTITLE_CSS_MANAGED_CONFIG_PATHS = new Set([ ...getSubtitleCssManagedConfigPaths('primary'), ...getSubtitleCssManagedConfigPaths('secondary'), @@ -111,6 +111,9 @@ const CATEGORY_ORDER: ConfigSettingsCategory[] = [ const SECTION_ORDER = new Map( [ 'Annotation Display', + 'Known Words', + 'N+1', + 'Frequency Highlighting', 'Primary Subtitle Appearance', 'Secondary Subtitle Appearance', 'Subtitle Sidebar Appearance', diff --git a/src/main.ts b/src/main.ts index 1660d8a1..c0a710c9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -665,14 +665,18 @@ const texthookerService = new Texthooker(() => { const characterDictionaryEnabled = config.anilist.characterDictionary.enabled && yomitanProfilePolicy.isCharacterDictionaryEnabled(); - const knownAndNPlusOneEnabled = getRuntimeBooleanOption( + const knownWordColoringEnabled = getRuntimeBooleanOption( + 'subtitle.annotation.knownWords.highlightEnabled', + config.ankiConnect.knownWords.highlightEnabled, + ); + const nPlusOneColoringEnabled = getRuntimeBooleanOption( 'subtitle.annotation.nPlusOne', config.ankiConnect.nPlusOne.enabled, ); return { - enableKnownWordColoring: knownAndNPlusOneEnabled, - enableNPlusOneColoring: knownAndNPlusOneEnabled, + enableKnownWordColoring: knownWordColoringEnabled, + enableNPlusOneColoring: nPlusOneColoringEnabled, enableNameMatchColoring: config.subtitleStyle.nameMatchEnabled && characterDictionaryEnabled, enableFrequencyColoring: getRuntimeBooleanOption( 'subtitle.annotation.frequency', @@ -2578,7 +2582,11 @@ function getResolvedConfig() { } function getRuntimeBooleanOption( - id: 'subtitle.annotation.nPlusOne' | 'subtitle.annotation.jlpt' | 'subtitle.annotation.frequency', + id: + | 'subtitle.annotation.knownWords.highlightEnabled' + | 'subtitle.annotation.nPlusOne' + | 'subtitle.annotation.jlpt' + | 'subtitle.annotation.frequency', fallback: boolean, ): boolean { const value = appState.runtimeOptionsManager?.getOptionValue(id); @@ -2587,6 +2595,10 @@ function getRuntimeBooleanOption( function shouldInitializeMecabForAnnotations(): boolean { const config = getResolvedConfig(); + const knownWordsEnabled = getRuntimeBooleanOption( + 'subtitle.annotation.knownWords.highlightEnabled', + config.ankiConnect.knownWords.highlightEnabled, + ); const nPlusOneEnabled = getRuntimeBooleanOption( 'subtitle.annotation.nPlusOne', config.ankiConnect.nPlusOne.enabled, @@ -2599,7 +2611,7 @@ function shouldInitializeMecabForAnnotations(): boolean { 'subtitle.annotation.frequency', config.subtitleStyle.frequencyDictionary.enabled, ); - return nPlusOneEnabled || jlptEnabled || frequencyEnabled; + return knownWordsEnabled || nPlusOneEnabled || jlptEnabled || frequencyEnabled; } const { diff --git a/src/renderer/handlers/keyboard.test.ts b/src/renderer/handlers/keyboard.test.ts index a5243f27..bed69f66 100644 --- a/src/renderer/handlers/keyboard.test.ts +++ b/src/renderer/handlers/keyboard.test.ts @@ -521,7 +521,7 @@ test('mpv input forwarding installs local key handling when session binding IPC testGlobals.setGetSessionBindings(() => new Promise(() => {})); const setupResult = await Promise.race([ handlers.setupMpvInputForwarding().then(() => 'resolved'), - wait(25).then(() => 'pending'), + wait(75).then(() => 'pending'), ]); assert.equal(setupResult, 'resolved'); @@ -533,6 +533,35 @@ test('mpv input forwarding installs local key handling when session binding IPC } }); +test('mpv input forwarding waits for session bindings before resolving setup', async () => { + const { handlers, testGlobals } = createKeyboardHandlerHarness(); + + try { + testGlobals.setGetSessionBindings(async () => { + await wait(20); + return [ + { + sourcePath: 'keybindings[0].key', + originalKey: 'KeyH', + key: { code: 'KeyH', modifiers: [] }, + actionType: 'mpv-command', + command: ['cycle', 'pause'], + }, + ] as CompiledSessionBinding[]; + }); + + await handlers.setupMpvInputForwarding(); + + assert.deepEqual(handlers.getSessionHelpOpeningInfo(), { + bindingKey: 'KeyK', + fallbackUsed: true, + fallbackUnavailable: false, + }); + } finally { + testGlobals.restore(); + } +}); + test('mpv input forwarding retries a transient keyboard config IPC failure', async () => { const { handlers, testGlobals } = createKeyboardHandlerHarness(); let calls = 0; diff --git a/src/renderer/handlers/keyboard.ts b/src/renderer/handlers/keyboard.ts index 29f04b30..585710e1 100644 --- a/src/renderer/handlers/keyboard.ts +++ b/src/renderer/handlers/keyboard.ts @@ -35,6 +35,7 @@ export function createKeyboardHandlers( ) { // Timeout for the modal chord capture window (e.g. Y followed by H/K). const CHORD_TIMEOUT_MS = 1000; + const MPV_INPUT_FORWARDING_CONFIG_LOAD_TIMEOUT_MS = 50; const KEYBOARD_SELECTED_WORD_CLASS = 'keyboard-selected'; let pendingSelectionAnchorAfterSubtitleSeek: 'start' | 'end' | null = null; let pendingLookupRefreshAfterSubtitleSeek = false; @@ -975,26 +976,21 @@ export function createKeyboardHandlers( installMpvInputForwardingListeners(); syncKeyboardTokenSelection(); - let configLoadSettled = false; let configLoadError: unknown = null; const configLoad = loadMpvInputForwardingConfigWithRetry().then( - () => { - configLoadSettled = true; - }, + () => {}, (error) => { - configLoadSettled = true; configLoadError = error; console.error('Failed to load overlay keyboard configuration.', error); }, ); - await new Promise((resolve) => { - setTimeout(resolve, 0); - }); - if (!configLoadSettled) { - void configLoad; - return; - } + await Promise.race([ + configLoad, + new Promise((resolve) => { + setTimeout(resolve, MPV_INPUT_FORWARDING_CONFIG_LOAD_TIMEOUT_MS); + }), + ]); if (configLoadError) { return; } diff --git a/src/renderer/modals/subtitle-sidebar.test.ts b/src/renderer/modals/subtitle-sidebar.test.ts index 50b885ef..af3d4bf4 100644 --- a/src/renderer/modals/subtitle-sidebar.test.ts +++ b/src/renderer/modals/subtitle-sidebar.test.ts @@ -138,6 +138,14 @@ test('applySidebarCssDeclarations clears declarations removed by config reload', assert.equal(style.color, '#ffffff'); assert.equal(style.backgroundColor, ''); assert.deepEqual(removed, ['background-color']); + + applySidebarCssDeclarations(target, { + color: '', + 'background-color': '', + }); + + assert.equal(style.color, ''); + assert.deepEqual(removed, ['background-color', 'background-color']); }); test('subtitle sidebar modal opens from snapshot and clicking cue seeks playback', async () => { diff --git a/src/renderer/modals/subtitle-sidebar.ts b/src/renderer/modals/subtitle-sidebar.ts index 06831c09..9f766f52 100644 --- a/src/renderer/modals/subtitle-sidebar.ts +++ b/src/renderer/modals/subtitle-sidebar.ts @@ -77,7 +77,14 @@ export function applySidebarCssDeclarations( for (const [property, rawValue] of Object.entries(declarations)) { const value = rawValue.trim(); - if (value.length === 0) continue; + if (value.length === 0) { + if (property.includes('-')) { + targetStyle.removeProperty(property); + } else { + styleTarget[property] = ''; + } + continue; + } if (property.includes('-')) { targetStyle.setProperty(property, value); } else { diff --git a/src/runtime-options.test.ts b/src/runtime-options.test.ts index 4199dc79..5c5d2e47 100644 --- a/src/runtime-options.test.ts +++ b/src/runtime-options.test.ts @@ -3,6 +3,7 @@ import fs from 'node:fs'; import path from 'node:path'; import test from 'node:test'; +import { DEFAULT_CONFIG } from './config'; import { RuntimeOptionsManager } from './runtime-options'; test('SM-012 runtime options path does not use JSON serialize-clone helpers', () => { @@ -59,3 +60,25 @@ test('RuntimeOptionsManager returns detached effective Anki config copies', () = assert.deepEqual(baseConfig.tags, ['SubMiner']); assert.equal(baseConfig.behavior.autoUpdateNewCards, true); }); + +test('RuntimeOptionsManager keeps known-word and n+1 annotation toggles separate', () => { + const baseConfig = structuredClone(DEFAULT_CONFIG.ankiConnect); + const patches: unknown[] = []; + const manager = new RuntimeOptionsManager(() => structuredClone(baseConfig), { + applyAnkiPatch: (patch) => { + patches.push(patch); + }, + onOptionsChanged: () => undefined, + }); + + assert.equal( + manager.setOptionValue('subtitle.annotation.knownWords.highlightEnabled', true).ok, + true, + ); + assert.equal(manager.setOptionValue('subtitle.annotation.nPlusOne', true).ok, true); + + const effective = manager.getEffectiveAnkiConnectConfig(); + assert.equal(effective.knownWords?.highlightEnabled, true); + assert.equal(effective.nPlusOne?.enabled, true); + assert.deepEqual(patches, []); +}); diff --git a/src/settings/settings-keybinding-controls.ts b/src/settings/settings-keybinding-controls.ts index be00bea8..6310aa10 100644 --- a/src/settings/settings-keybinding-controls.ts +++ b/src/settings/settings-keybinding-controls.ts @@ -140,8 +140,8 @@ export function renderMpvKeybindingsInput( removeButton.type = 'button'; removeButton.textContent = 'Remove'; removeButton.addEventListener('click', () => { - rows.splice(i, 1); - applyMpvRows(context, field, rows); + const nextRows = rows.filter((_, index) => index !== i); + applyMpvRows(context, field, nextRows); requestRender(); }); item.append(keyButton, command, removeButton); diff --git a/src/settings/settings-model.test.ts b/src/settings/settings-model.test.ts index 3570c07c..bb46cd47 100644 --- a/src/settings/settings-model.test.ts +++ b/src/settings/settings-model.test.ts @@ -87,6 +87,27 @@ test('filterSettingsFields normalizes punctuation in query terms', () => { ); }); +test('filterSettingsFields preserves non-Latin query terms', () => { + const japaneseFields: ConfigSettingsField[] = [ + { + id: 'subtitleStyle.japaneseFontFamily', + label: '日本語フォント', + description: '字幕の表示に使う書体。', + configPath: 'subtitleStyle.japaneseFontFamily', + category: 'appearance', + section: 'Primary Subtitle Appearance', + control: 'text', + defaultValue: '', + restartBehavior: 'hot-reload', + }, + ]; + + assert.deepEqual( + filterSettingsFields(japaneseFields, { query: '日本語' }).map((field) => field.configPath), + ['subtitleStyle.japaneseFontFamily'], + ); +}); + test('settings draft tracks dirty set and emits save operations', () => { const draft = createSettingsDraft({ 'subtitleStyle.autoPauseVideoOnHover': true, diff --git a/src/settings/settings-model.ts b/src/settings/settings-model.ts index 8228c970..e78e934f 100644 --- a/src/settings/settings-model.ts +++ b/src/settings/settings-model.ts @@ -17,7 +17,7 @@ export interface SettingsDraft { } function normalizeQuery(query: string | undefined): string { - return (query ?? '').trim().toLowerCase(); + return (query ?? '').trim().toLocaleLowerCase(); } function searchableText(parts: Array): string { @@ -25,8 +25,8 @@ function searchableText(parts: Array): string { .filter(Boolean) .join(' ') .replace(/([a-z0-9])([A-Z])/g, '$1 $2') - .replace(/[^a-zA-Z0-9]+/g, ' ') - .toLowerCase(); + .replace(/[^\p{L}\p{N}]+/gu, ' ') + .toLocaleLowerCase(); } function valuesEqual(a: unknown, b: unknown): boolean { diff --git a/src/shared/ipc/validators.ts b/src/shared/ipc/validators.ts index f73473d9..f10e52ed 100644 --- a/src/shared/ipc/validators.ts +++ b/src/shared/ipc/validators.ts @@ -48,6 +48,7 @@ const SESSION_ACTION_IDS: SessionActionId[] = [ const RUNTIME_OPTION_IDS: RuntimeOptionId[] = [ 'anki.autoUpdateNewCards', + 'subtitle.annotation.knownWords.highlightEnabled', 'subtitle.annotation.nPlusOne', 'subtitle.annotation.jlpt', 'subtitle.annotation.frequency', diff --git a/src/types/runtime-options.ts b/src/types/runtime-options.ts index 18814b34..97c55162 100644 --- a/src/types/runtime-options.ts +++ b/src/types/runtime-options.ts @@ -1,5 +1,6 @@ export type RuntimeOptionId = | 'anki.autoUpdateNewCards' + | 'subtitle.annotation.knownWords.highlightEnabled' | 'subtitle.annotation.nPlusOne' | 'subtitle.annotation.jlpt' | 'subtitle.annotation.frequency'