From c6fa197d0d440f6758e858605f8dcc6efa4b5a52 Mon Sep 17 00:00:00 2001 From: sudacode Date: Fri, 20 Feb 2026 01:30:46 -0800 Subject: [PATCH] refactor: normalize remaining main runtime dependency setup --- .../codex-task85-20260219T233711Z-46hc.md | 17 ++ src/main.ts | 166 +++++++++++------- .../config-hot-reload-main-deps.test.ts | 41 +++++ .../runtime/config-hot-reload-main-deps.ts | 23 +++ .../immersion-startup-main-deps.test.ts | 35 ++++ .../runtime/immersion-startup-main-deps.ts | 22 +++ ...jellyfin-playback-launch-main-deps.test.ts | 56 ++++++ .../jellyfin-playback-launch-main-deps.ts | 21 +++ ...-subtitle-render-metrics-main-deps.test.ts | 45 +++++ .../mpv-subtitle-render-metrics-main-deps.ts | 14 ++ ...numeric-shortcut-runtime-main-deps.test.ts | 32 ++++ .../numeric-shortcut-runtime-main-deps.ts | 10 ++ 12 files changed, 416 insertions(+), 66 deletions(-) create mode 100644 src/main/runtime/immersion-startup-main-deps.test.ts create mode 100644 src/main/runtime/immersion-startup-main-deps.ts create mode 100644 src/main/runtime/jellyfin-playback-launch-main-deps.test.ts create mode 100644 src/main/runtime/jellyfin-playback-launch-main-deps.ts create mode 100644 src/main/runtime/mpv-subtitle-render-metrics-main-deps.test.ts create mode 100644 src/main/runtime/mpv-subtitle-render-metrics-main-deps.ts create mode 100644 src/main/runtime/numeric-shortcut-runtime-main-deps.test.ts create mode 100644 src/main/runtime/numeric-shortcut-runtime-main-deps.ts diff --git a/docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md b/docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md index 1001d7b..caed325 100644 --- a/docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md +++ b/docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md @@ -390,3 +390,20 @@ - [2026-02-20T08:56:46Z] progress: extracted MPV IPC command dependency assembly into `src/main/runtime/ipc-mpv-command-main-deps.ts` and rewired `main.ts` IPC bridge setup to compose through `buildMpvCommandFromIpcRuntimeMainDepsHandler`. - [2026-02-20T08:56:46Z] progress: added `src/main/runtime/ipc-mpv-command-main-deps.test.ts` mapping coverage. - [2026-02-20T08:56:46Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/ipc-mpv-command-main-deps.test.js dist/main/runtime/ipc-bridge-actions-main-deps.test.js dist/main/runtime/ipc-bridge-actions.test.js` pass. +- [2026-02-20T09:00:14Z] progress: post-push slice extracted `playJellyfinItemInMpv` dependency assembly into `src/main/runtime/jellyfin-playback-launch-main-deps.ts` and rewired `main.ts` playback handler construction. +- [2026-02-20T09:00:14Z] progress: added `src/main/runtime/jellyfin-playback-launch-main-deps.test.ts` mapping coverage. +- [2026-02-20T09:00:14Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/jellyfin-playback-launch-main-deps.test.js dist/main/runtime/jellyfin-playback-launch.test.js` pass. +- [2026-02-20T09:10:53Z] progress: extracted config hot-reload message/watcher dependency assembly into builders in `config-hot-reload-main-deps.ts` (`createBuildConfigHotReloadMessageMainDepsHandler`, `createBuildWatchConfigPathMainDepsHandler`) and rewired `main.ts`. +- [2026-02-20T09:10:53Z] progress: extracted MPV subtitle render metrics dependency assembly into `mpv-subtitle-render-metrics-main-deps.ts` and rewired `main.ts` update runtime setup. +- [2026-02-20T09:10:53Z] progress: added mapping tests `config-hot-reload-main-deps.test.ts` (new cases) and `mpv-subtitle-render-metrics-main-deps.test.ts`. +- [2026-02-20T09:10:53Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/config-hot-reload-main-deps.test.js dist/main/runtime/mpv-subtitle-render-metrics-main-deps.test.js dist/main/runtime/mpv-subtitle-render-metrics.test.js dist/main/runtime/config-hot-reload-handlers.test.js` pass. +- [2026-02-20T09:14:06Z] progress: extracted numeric shortcut runtime option assembly into `src/main/runtime/numeric-shortcut-runtime-main-deps.ts` and rewired `createNumericShortcutRuntime` setup in `main.ts`. +- [2026-02-20T09:14:06Z] progress: added `src/main/runtime/numeric-shortcut-runtime-main-deps.test.ts` mapping coverage. +- [2026-02-20T09:14:06Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/numeric-shortcut-runtime-main-deps.test.js dist/core/services/numeric-shortcut-session.test.js` pass. +- [2026-02-20T09:16:42Z] progress: extracted immersion tracker startup dependency assembly into `src/main/runtime/immersion-startup-main-deps.ts` and rewired app-ready startup wiring in `main.ts`. +- [2026-02-20T09:16:42Z] progress: extracted config hot-reload watcher/message and MPV subtitle metrics dependency assembly into dedicated builder usage in `main.ts`; added `mpv-subtitle-render-metrics-main-deps.ts`. +- [2026-02-20T09:16:42Z] progress: normalized `handleInitialArgs` setup to prebuilt builder-backed handler (`buildHandleInitialArgsMainDepsHandler` + `handleInitialArgsRuntimeHandler`) to reduce repeated construction. +- [2026-02-20T09:16:42Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + targeted suites pass: `immersion-startup*`, `config-hot-reload-main-deps*`, `mpv-subtitle-render-metrics*`, `numeric-shortcut-runtime-main-deps*`, `initial-args*`. +- [2026-02-20T09:30:02Z] progress: applied 5-block safe normalization in `main.ts` by prebuilding deps handlers for global shortcuts (`getConfiguredShortcuts`, `registerGlobalShortcuts`, `refreshGlobalAndOverlayShortcuts`) and MPV logging/OSD (`appendToMpvLog`, `showMpvOsd`). +- [2026-02-20T09:30:02Z] progress: earlier in this batch also extracted `jellyfin-playback-launch-main-deps`, `immersion-startup-main-deps`, `numeric-shortcut-runtime-main-deps`, and `mpv-subtitle-render-metrics-main-deps`; extended `config-hot-reload-main-deps` with watcher/message builders. +- [2026-02-20T09:30:02Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + targeted suites pass for `global-shortcuts*`, `mpv-osd-log*`, `jellyfin-playback-launch*`, `immersion-startup*`, `config-hot-reload-main-deps*`, `mpv-subtitle-render-metrics*`, `numeric-shortcut-runtime-main-deps*`, `initial-args*`. diff --git a/src/main.ts b/src/main.ts index 6a87b16..de139ca 100644 --- a/src/main.ts +++ b/src/main.ts @@ -74,6 +74,7 @@ import { } from './main/runtime/startup-config'; import { buildConfigWarningNotificationBody } from './main/config-validation'; import { createImmersionTrackerStartupHandler } from './main/runtime/immersion-startup'; +import { createBuildImmersionTrackerStartupMainDepsHandler } from './main/runtime/immersion-startup-main-deps'; import { createImmersionMediaRuntime } from './main/runtime/immersion-media'; import { createAnilistStateRuntime } from './main/runtime/anilist-state'; import { createConfigDerivedRuntime } from './main/runtime/config-derived'; @@ -230,6 +231,7 @@ import { createBuildJlptDictionaryRuntimeMainDepsHandler, } from './main/runtime/dictionary-runtime-main-deps'; import { createPlayJellyfinItemInMpvHandler } from './main/runtime/jellyfin-playback-launch'; +import { createBuildPlayJellyfinItemInMpvMainDepsHandler } from './main/runtime/jellyfin-playback-launch-main-deps'; import { createPreloadJellyfinExternalSubtitlesHandler } from './main/runtime/jellyfin-subtitle-preload'; import { createBuildPreloadJellyfinExternalSubtitlesMainDepsHandler } from './main/runtime/jellyfin-subtitle-preload-main-deps'; import { @@ -259,6 +261,7 @@ import { createBuildBindMpvMainEventHandlersMainDepsHandler } from './main/runti import { createBuildMpvClientRuntimeServiceFactoryDepsHandler } from './main/runtime/mpv-client-runtime-service-main-deps'; import { createMpvClientRuntimeServiceFactory } from './main/runtime/mpv-client-runtime-service'; import { createUpdateMpvSubtitleRenderMetricsHandler } from './main/runtime/mpv-subtitle-render-metrics'; +import { createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler } from './main/runtime/mpv-subtitle-render-metrics-main-deps'; import { createBuildTokenizerDepsMainHandler, createCreateMecabTokenizerAndCheckMainHandler, @@ -312,6 +315,7 @@ import { createBuildCancelNumericShortcutSessionMainDepsHandler, createBuildStartNumericShortcutSessionMainDepsHandler, } from './main/runtime/numeric-shortcut-session-main-deps'; +import { createBuildNumericShortcutRuntimeMainDepsHandler } from './main/runtime/numeric-shortcut-runtime-main-deps'; import { createRefreshOverlayShortcutsHandler, createRegisterOverlayShortcutsHandler, @@ -453,8 +457,10 @@ import { resolveSubtitleStyleForRenderer, } from './main/runtime/config-hot-reload-handlers'; import { + createBuildConfigHotReloadMessageMainDepsHandler, createBuildConfigHotReloadAppliedMainDepsHandler, createBuildConfigHotReloadRuntimeMainDepsHandler, + createBuildWatchConfigPathMainDepsHandler, createWatchConfigPathHandler, } from './main/runtime/config-hot-reload-main-deps'; import { @@ -849,15 +855,19 @@ const overlayShortcutsRuntime = createOverlayShortcutsRuntimeService( })(), ); -const notifyConfigHotReloadMessage = createConfigHotReloadMessageHandler({ +const buildConfigHotReloadMessageMainDepsHandler = createBuildConfigHotReloadMessageMainDepsHandler({ showMpvOsd: (message) => showMpvOsd(message), showDesktopNotification: (title, options) => showDesktopNotification(title, options), }); -const watchConfigPathHandler = createWatchConfigPathHandler({ +const notifyConfigHotReloadMessage = createConfigHotReloadMessageHandler( + buildConfigHotReloadMessageMainDepsHandler(), +); +const buildWatchConfigPathMainDepsHandler = createBuildWatchConfigPathMainDepsHandler({ fileExists: (targetPath) => fs.existsSync(targetPath), dirname: (targetPath) => path.dirname(targetPath), watchPath: (targetPath, listener) => fs.watch(targetPath, listener), }); +const watchConfigPathHandler = createWatchConfigPathHandler(buildWatchConfigPathMainDepsHandler()); const buildConfigHotReloadAppliedMainDepsHandler = createBuildConfigHotReloadAppliedMainDepsHandler({ setKeybindings: (keybindings) => { appState.keybindings = keybindings as never; @@ -1305,7 +1315,7 @@ const preloadJellyfinExternalSubtitles = createPreloadJellyfinExternalSubtitlesH buildPreloadJellyfinExternalSubtitlesMainDepsHandler(), ); -const playJellyfinItemInMpv = createPlayJellyfinItemInMpvHandler({ +const buildPlayJellyfinItemInMpvMainDepsHandler = createBuildPlayJellyfinItemInMpvMainDepsHandler({ ensureMpvConnectedForPlayback: () => ensureMpvConnectedForJellyfinPlayback(), getMpvClient: () => appState.mpvClient, resolvePlaybackPlan: (params) => @@ -1348,6 +1358,9 @@ const playJellyfinItemInMpv = createPlayJellyfinItemInMpvHandler({ showMpvOsd(text); }, }); +const playJellyfinItemInMpv = createPlayJellyfinItemInMpvHandler( + buildPlayJellyfinItemInMpvMainDepsHandler(), +); const buildHandleJellyfinAuthCommandsMainDepsHandler = createBuildHandleJellyfinAuthCommandsMainDepsHandler({ @@ -1985,8 +1998,7 @@ const criticalConfigErrorHandler = createCriticalConfigErrorHandler( })(), ); -const appReadyRuntimeRunner = createAppReadyRuntimeRunner( - createBuildAppReadyRuntimeMainDepsHandler({ +const buildAppReadyRuntimeMainDepsHandler = createBuildAppReadyRuntimeMainDepsHandler({ loadSubtitlePosition: () => loadSubtitlePosition(), resolveKeybindings: () => { appState.keybindings = resolveKeybindings(getResolvedConfig(), DEFAULT_KEYBINDINGS); @@ -2032,21 +2044,23 @@ const appReadyRuntimeRunner = createAppReadyRuntimeRunner( const tracker = new SubtitleTimingTracker(); appState.subtitleTimingTracker = tracker; }, - createImmersionTracker: createImmersionTrackerStartupHandler({ - getResolvedConfig: () => getResolvedConfig(), - getConfiguredDbPath: () => immersionMediaRuntime.getConfiguredDbPath(), - createTrackerService: (params) => new ImmersionTrackerService(params), - setTracker: (tracker) => { - appState.immersionTracker = tracker as ImmersionTrackerService | null; - }, - getMpvClient: () => appState.mpvClient, - seedTrackerFromCurrentMedia: () => { - void immersionMediaRuntime.seedFromCurrentMedia(); - }, - logInfo: (message) => logger.info(message), - logDebug: (message) => logger.debug(message), - logWarn: (message, details) => logger.warn(message, details), - }), + createImmersionTracker: createImmersionTrackerStartupHandler( + createBuildImmersionTrackerStartupMainDepsHandler({ + getResolvedConfig: () => getResolvedConfig(), + getConfiguredDbPath: () => immersionMediaRuntime.getConfiguredDbPath(), + createTrackerService: (params) => new ImmersionTrackerService(params), + setTracker: (tracker) => { + appState.immersionTracker = tracker as ImmersionTrackerService | null; + }, + getMpvClient: () => appState.mpvClient, + seedTrackerFromCurrentMedia: () => { + void immersionMediaRuntime.seedFromCurrentMedia(); + }, + logInfo: (message) => logger.info(message), + logDebug: (message) => logger.debug(message), + logWarn: (message, details) => logger.warn(message, details), + })(), + ), loadYomitanExtension: async () => { await loadYomitanExtension(); }, @@ -2071,10 +2085,10 @@ const appReadyRuntimeRunner = createAppReadyRuntimeRunner( logger.debug(message); }, now: () => Date.now(), - })(), -); + }); +const appReadyRuntimeRunner = createAppReadyRuntimeRunner(buildAppReadyRuntimeMainDepsHandler()); -const appLifecycleRuntimeRunner = createAppLifecycleRuntimeRunner( +const buildAppLifecycleRuntimeRunnerMainDepsHandler = createBuildAppLifecycleRuntimeRunnerMainDepsHandler({ app, platform: process.platform, @@ -2089,12 +2103,12 @@ const appLifecycleRuntimeRunner = createAppLifecycleRuntimeRunner( shouldRestoreWindowsOnActivate: () => shouldRestoreWindowsOnActivateHandler(), restoreWindowsOnActivate: () => restoreWindowsOnActivateHandler(), shouldQuitOnWindowAllClosed: () => !appState.backgroundMode, - })(), + }); +const appLifecycleRuntimeRunner = createAppLifecycleRuntimeRunner( + buildAppLifecycleRuntimeRunnerMainDepsHandler(), ); -const buildStartupBootstrapRuntimeFactoryDepsHandler = - createBuildStartupBootstrapRuntimeFactoryDepsHandler( - createBuildStartupBootstrapMainDepsHandler({ +const buildStartupBootstrapMainDepsHandler = createBuildStartupBootstrapMainDepsHandler({ argv: process.argv, parseArgs: (argv: string[]) => parseArgs(argv), setLogLevel: (level: string, source: LogLevelSource) => { @@ -2126,8 +2140,9 @@ const buildStartupBootstrapRuntimeFactoryDepsHandler = quitApp: () => app.quit(), logGenerateConfigError: (message) => logger.error(message), startAppLifecycle: appLifecycleRuntimeRunner, - })(), - ); + }); +const buildStartupBootstrapRuntimeFactoryDepsHandler = + createBuildStartupBootstrapRuntimeFactoryDepsHandler(buildStartupBootstrapMainDepsHandler()); const startupState = runStartupBootstrapRuntime( createStartupBootstrapRuntimeDeps(buildStartupBootstrapRuntimeFactoryDepsHandler()), @@ -2156,19 +2171,22 @@ function handleCliCommand(args: CliArgs, source: CliCommandSource = 'initial'): handleCliCommandRuntimeServiceWithContext(args, source, cliContext); } +const buildHandleInitialArgsMainDepsHandler = createBuildHandleInitialArgsMainDepsHandler({ + getInitialArgs: () => appState.initialArgs, + isBackgroundMode: () => appState.backgroundMode, + ensureTray: () => ensureTray(), + isTexthookerOnlyMode: () => appState.texthookerOnlyMode, + hasImmersionTracker: () => Boolean(appState.immersionTracker), + getMpvClient: () => appState.mpvClient, + logInfo: (message) => logger.info(message), + handleCliCommand: (args, source) => handleCliCommand(args, source), +}); +const handleInitialArgsRuntimeHandler = createHandleInitialArgsHandler( + buildHandleInitialArgsMainDepsHandler(), +); + function handleInitialArgs(): void { - createHandleInitialArgsHandler( - createBuildHandleInitialArgsMainDepsHandler({ - getInitialArgs: () => appState.initialArgs, - isBackgroundMode: () => appState.backgroundMode, - ensureTray: () => ensureTray(), - isTexthookerOnlyMode: () => appState.texthookerOnlyMode, - hasImmersionTracker: () => Boolean(appState.immersionTracker), - getMpvClient: () => appState.mpvClient, - logInfo: (message) => logger.info(message), - handleCliCommand: (args, source) => handleCliCommand(args, source), - })(), - )(); + handleInitialArgsRuntimeHandler(); } const bindMpvClientEventHandlers = createBindMpvMainEventHandlersHandler( @@ -2241,7 +2259,8 @@ function createMpvClientRuntimeService(): MpvIpcClient { )(); } -const updateMpvSubtitleRenderMetricsRuntime = createUpdateMpvSubtitleRenderMetricsHandler({ +const buildUpdateMpvSubtitleRenderMetricsMainDepsHandler = + createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler({ getCurrentMetrics: () => appState.mpvSubtitleRenderMetrics, setCurrentMetrics: (metrics) => { appState.mpvSubtitleRenderMetrics = metrics; @@ -2251,6 +2270,9 @@ const updateMpvSubtitleRenderMetricsRuntime = createUpdateMpvSubtitleRenderMetri broadcastToOverlayWindows('mpv-subtitle-render-metrics:set', metrics); }, }); +const updateMpvSubtitleRenderMetricsRuntime = createUpdateMpvSubtitleRenderMetricsHandler( + buildUpdateMpvSubtitleRenderMetricsMainDepsHandler(), +); function updateMpvSubtitleRenderMetrics(patch: Partial): void { updateMpvSubtitleRenderMetricsRuntime(patch); @@ -2430,16 +2452,19 @@ function openYomitanSettings(): void { openYomitanSettingsHandler(); } -const getConfiguredShortcutsHandler = createGetConfiguredShortcutsHandler({ - ...createBuildGetConfiguredShortcutsMainDepsHandler({ +const buildGetConfiguredShortcutsMainDepsHandler = createBuildGetConfiguredShortcutsMainDepsHandler( + { getResolvedConfig: () => getResolvedConfig(), defaultConfig: DEFAULT_CONFIG, resolveConfiguredShortcuts, - })(), + }, +); +const getConfiguredShortcutsHandler = createGetConfiguredShortcutsHandler({ + ...buildGetConfiguredShortcutsMainDepsHandler(), }); -const registerGlobalShortcutsHandler = createRegisterGlobalShortcutsHandler({ - ...createBuildRegisterGlobalShortcutsMainDepsHandler({ +const buildRegisterGlobalShortcutsMainDepsHandler = + createBuildRegisterGlobalShortcutsMainDepsHandler({ getConfiguredShortcuts: () => getConfiguredShortcuts(), registerGlobalShortcutsCore, toggleVisibleOverlay: () => toggleVisibleOverlay(), @@ -2447,15 +2472,19 @@ const registerGlobalShortcutsHandler = createRegisterGlobalShortcutsHandler({ openYomitanSettings: () => openYomitanSettings(), isDev, getMainWindow: () => overlayManager.getMainWindow(), - })(), + }); +const registerGlobalShortcutsHandler = createRegisterGlobalShortcutsHandler({ + ...buildRegisterGlobalShortcutsMainDepsHandler(), }); -const refreshGlobalAndOverlayShortcutsHandler = createRefreshGlobalAndOverlayShortcutsHandler({ - ...createBuildRefreshGlobalAndOverlayShortcutsMainDepsHandler({ +const buildRefreshGlobalAndOverlayShortcutsMainDepsHandler = + createBuildRefreshGlobalAndOverlayShortcutsMainDepsHandler({ unregisterAllGlobalShortcuts: () => globalShortcut.unregisterAll(), registerGlobalShortcuts: () => registerGlobalShortcuts(), syncOverlayShortcuts: () => syncOverlayShortcuts(), - })(), + }); +const refreshGlobalAndOverlayShortcutsHandler = createRefreshGlobalAndOverlayShortcutsHandler({ + ...buildRefreshGlobalAndOverlayShortcutsMainDepsHandler(), }); function registerGlobalShortcuts(): void { @@ -2489,24 +2518,26 @@ function cycleSecondarySubMode(): void { ); } +const buildAppendToMpvLogMainDepsHandler = createBuildAppendToMpvLogMainDepsHandler({ + logPath: DEFAULT_MPV_LOG_PATH, + dirname: (targetPath) => path.dirname(targetPath), + mkdirSync: (targetPath, options) => fs.mkdirSync(targetPath, options), + appendFileSync: (targetPath, data, options) => fs.appendFileSync(targetPath, data, options), + now: () => new Date(), +}); const appendToMpvLogHandler = createAppendToMpvLogHandler({ - ...createBuildAppendToMpvLogMainDepsHandler({ - logPath: DEFAULT_MPV_LOG_PATH, - dirname: (targetPath) => path.dirname(targetPath), - mkdirSync: (targetPath, options) => fs.mkdirSync(targetPath, options), - appendFileSync: (targetPath, data, options) => fs.appendFileSync(targetPath, data, options), - now: () => new Date(), - })(), + ...buildAppendToMpvLogMainDepsHandler(), }); +const buildShowMpvOsdMainDepsHandler = createBuildShowMpvOsdMainDepsHandler({ + appendToMpvLog: (message) => appendToMpvLog(message), + showMpvOsdRuntime: (mpvClient, text, fallbackLog) => + showMpvOsdRuntime(mpvClient as never, text, fallbackLog), + getMpvClient: () => appState.mpvClient, + logInfo: (line) => logger.info(line), +}); const showMpvOsdHandler = createShowMpvOsdHandler({ - ...createBuildShowMpvOsdMainDepsHandler({ - appendToMpvLog: (message) => appendToMpvLog(message), - showMpvOsdRuntime: (mpvClient, text, fallbackLog) => - showMpvOsdRuntime(mpvClient as never, text, fallbackLog), - getMpvClient: () => appState.mpvClient, - logInfo: (line) => logger.info(line), - })(), + ...buildShowMpvOsdMainDepsHandler(), }); function showMpvOsd(text: string): void { @@ -2517,12 +2548,15 @@ function appendToMpvLog(message: string): void { appendToMpvLogHandler(message); } -const numericShortcutRuntime = createNumericShortcutRuntime({ +const buildNumericShortcutRuntimeMainDepsHandler = createBuildNumericShortcutRuntimeMainDepsHandler({ globalShortcut, showMpvOsd: (text) => showMpvOsd(text), setTimer: (handler, timeoutMs) => setTimeout(handler, timeoutMs), clearTimer: (timer) => clearTimeout(timer), }); +const numericShortcutRuntime = createNumericShortcutRuntime( + buildNumericShortcutRuntimeMainDepsHandler(), +); const multiCopySession = numericShortcutRuntime.createSession(); const mineSentenceSession = numericShortcutRuntime.createSession(); const buildCancelPendingMultiCopyMainDepsHandler = diff --git a/src/main/runtime/config-hot-reload-main-deps.test.ts b/src/main/runtime/config-hot-reload-main-deps.test.ts index 9166706..401e450 100644 --- a/src/main/runtime/config-hot-reload-main-deps.test.ts +++ b/src/main/runtime/config-hot-reload-main-deps.test.ts @@ -3,8 +3,10 @@ import test from 'node:test'; import type { ReloadConfigStrictResult } from '../../config'; import type { ResolvedConfig } from '../../types'; import { + createBuildConfigHotReloadMessageMainDepsHandler, createBuildConfigHotReloadAppliedMainDepsHandler, createBuildConfigHotReloadRuntimeMainDepsHandler, + createBuildWatchConfigPathMainDepsHandler, createWatchConfigPathHandler, } from './config-hot-reload-main-deps'; @@ -43,6 +45,45 @@ test('watch config path handler filters directory events to config files only', assert.deepEqual(calls, ['change', 'change', 'change']); }); +test('watch config path main deps builder maps filesystem callbacks', () => { + const calls: string[] = []; + const deps = createBuildWatchConfigPathMainDepsHandler({ + fileExists: () => true, + dirname: (targetPath) => { + calls.push(`dirname:${targetPath}`); + return '/tmp'; + }, + watchPath: (targetPath, listener) => { + calls.push(`watch:${targetPath}`); + listener('change', 'config.jsonc'); + return { close: () => calls.push('close') }; + }, + })(); + + assert.equal(deps.fileExists('/tmp/config.jsonc'), true); + assert.equal(deps.dirname('/tmp/config.jsonc'), '/tmp'); + const watcher = deps.watchPath('/tmp/config.jsonc', () => calls.push('listener')); + watcher.close(); + assert.deepEqual(calls, [ + 'dirname:/tmp/config.jsonc', + 'watch:/tmp/config.jsonc', + 'listener', + 'close', + ]); +}); + +test('config hot reload message main deps builder maps notifications', () => { + const calls: string[] = []; + const deps = createBuildConfigHotReloadMessageMainDepsHandler({ + showMpvOsd: (message) => calls.push(`osd:${message}`), + showDesktopNotification: (title) => calls.push(`notify:${title}`), + })(); + + deps.showMpvOsd('updated'); + deps.showDesktopNotification('SubMiner', { body: 'updated' }); + assert.deepEqual(calls, ['osd:updated', 'notify:SubMiner']); +}); + test('config hot reload applied main deps builder maps callbacks', () => { const calls: string[] = []; const deps = createBuildConfigHotReloadAppliedMainDepsHandler({ diff --git a/src/main/runtime/config-hot-reload-main-deps.ts b/src/main/runtime/config-hot-reload-main-deps.ts index 84b2a69..c563290 100644 --- a/src/main/runtime/config-hot-reload-main-deps.ts +++ b/src/main/runtime/config-hot-reload-main-deps.ts @@ -4,6 +4,7 @@ import type { } from '../../core/services/config-hot-reload'; import type { ReloadConfigStrictResult } from '../../config'; import type { ConfigHotReloadPayload, ConfigValidationWarning, ResolvedConfig, SecondarySubMode } from '../../types'; +import type { createConfigHotReloadMessageHandler } from './config-hot-reload-handlers'; type ConfigWatchListener = (eventType: string, filename: string | null) => void; @@ -32,6 +33,28 @@ export function createWatchConfigPathHandler(deps: { }; } +type WatchConfigPathMainDeps = Parameters[0]; +type ConfigHotReloadMessageMainDeps = Parameters[0]; + +export function createBuildWatchConfigPathMainDepsHandler(deps: WatchConfigPathMainDeps) { + return (): WatchConfigPathMainDeps => ({ + fileExists: (targetPath: string) => deps.fileExists(targetPath), + dirname: (targetPath: string) => deps.dirname(targetPath), + watchPath: (targetPath: string, listener: ConfigWatchListener) => + deps.watchPath(targetPath, listener), + }); +} + +export function createBuildConfigHotReloadMessageMainDepsHandler( + deps: ConfigHotReloadMessageMainDeps, +) { + return (): ConfigHotReloadMessageMainDeps => ({ + showMpvOsd: (message: string) => deps.showMpvOsd(message), + showDesktopNotification: (title: string, options: { body: string }) => + deps.showDesktopNotification(title, options), + }); +} + export function createBuildConfigHotReloadAppliedMainDepsHandler(deps: { setKeybindings: (keybindings: ConfigHotReloadPayload['keybindings']) => void; refreshGlobalAndOverlayShortcuts: () => void; diff --git a/src/main/runtime/immersion-startup-main-deps.test.ts b/src/main/runtime/immersion-startup-main-deps.test.ts new file mode 100644 index 0000000..45ffb05 --- /dev/null +++ b/src/main/runtime/immersion-startup-main-deps.test.ts @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { createBuildImmersionTrackerStartupMainDepsHandler } from './immersion-startup-main-deps'; + +test('immersion tracker startup main deps builder maps callbacks', () => { + const calls: string[] = []; + const deps = createBuildImmersionTrackerStartupMainDepsHandler({ + getResolvedConfig: () => ({ immersionTracking: { enabled: true } } as never), + getConfiguredDbPath: () => '/tmp/immersion.db', + createTrackerService: () => { + calls.push('create'); + return { id: 'tracker' }; + }, + setTracker: () => calls.push('set'), + getMpvClient: () => ({ connected: true, connect: () => {} }), + seedTrackerFromCurrentMedia: () => calls.push('seed'), + logInfo: (message) => calls.push(`info:${message}`), + logDebug: (message) => calls.push(`debug:${message}`), + logWarn: (message) => calls.push(`warn:${message}`), + })(); + + assert.deepEqual(deps.getResolvedConfig(), { immersionTracking: { enabled: true } }); + assert.equal(deps.getConfiguredDbPath(), '/tmp/immersion.db'); + assert.deepEqual(deps.createTrackerService({ dbPath: '/tmp/immersion.db', policy: {} as never }), { + id: 'tracker', + }); + deps.setTracker(null); + assert.equal(deps.getMpvClient()?.connected, true); + deps.seedTrackerFromCurrentMedia(); + deps.logInfo('i'); + deps.logDebug('d'); + deps.logWarn('w', null); + + assert.deepEqual(calls, ['create', 'set', 'seed', 'info:i', 'debug:d', 'warn:w']); +}); diff --git a/src/main/runtime/immersion-startup-main-deps.ts b/src/main/runtime/immersion-startup-main-deps.ts new file mode 100644 index 0000000..7df1844 --- /dev/null +++ b/src/main/runtime/immersion-startup-main-deps.ts @@ -0,0 +1,22 @@ +import type { + ImmersionTrackerStartupDeps, + createImmersionTrackerStartupHandler, +} from './immersion-startup'; + +type ImmersionTrackerStartupMainDeps = Parameters[0]; + +export function createBuildImmersionTrackerStartupMainDepsHandler( + deps: ImmersionTrackerStartupMainDeps, +) { + return (): ImmersionTrackerStartupDeps => ({ + getResolvedConfig: () => deps.getResolvedConfig(), + getConfiguredDbPath: () => deps.getConfiguredDbPath(), + createTrackerService: (params) => deps.createTrackerService(params), + setTracker: (tracker) => deps.setTracker(tracker), + getMpvClient: () => deps.getMpvClient(), + seedTrackerFromCurrentMedia: () => deps.seedTrackerFromCurrentMedia(), + logInfo: (message: string) => deps.logInfo(message), + logDebug: (message: string) => deps.logDebug(message), + logWarn: (message: string, details: unknown) => deps.logWarn(message, details), + }); +} diff --git a/src/main/runtime/jellyfin-playback-launch-main-deps.test.ts b/src/main/runtime/jellyfin-playback-launch-main-deps.test.ts new file mode 100644 index 0000000..a8ab37e --- /dev/null +++ b/src/main/runtime/jellyfin-playback-launch-main-deps.test.ts @@ -0,0 +1,56 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { createBuildPlayJellyfinItemInMpvMainDepsHandler } from './jellyfin-playback-launch-main-deps'; + +test('play jellyfin item in mpv main deps builder maps callbacks', async () => { + const calls: string[] = []; + const deps = createBuildPlayJellyfinItemInMpvMainDepsHandler({ + ensureMpvConnectedForPlayback: async () => true, + getMpvClient: () => ({ connected: true }), + resolvePlaybackPlan: async () => ({ + url: 'u', + mode: 'direct', + title: 't', + startTimeTicks: 0, + }), + applyJellyfinMpvDefaults: () => calls.push('defaults'), + sendMpvCommand: (command) => calls.push(`cmd:${command[0]}`), + armQuitOnDisconnect: () => calls.push('arm'), + schedule: (_callback, delayMs) => calls.push(`schedule:${delayMs}`), + convertTicksToSeconds: (ticks) => ticks / 10_000_000, + preloadExternalSubtitles: () => calls.push('preload'), + setActivePlayback: () => calls.push('active'), + setLastProgressAtMs: () => calls.push('progress'), + reportPlaying: () => calls.push('report'), + showMpvOsd: (text) => calls.push(`osd:${text}`), + })(); + + assert.equal(await deps.ensureMpvConnectedForPlayback(), true); + assert.equal(typeof deps.getMpvClient(), 'object'); + assert.deepEqual( + await deps.resolvePlaybackPlan({ session: {} as never, clientInfo: {} as never, jellyfinConfig: {}, itemId: 'i' }), + { url: 'u', mode: 'direct', title: 't', startTimeTicks: 0 }, + ); + deps.applyJellyfinMpvDefaults({}); + deps.sendMpvCommand(['show-text', 'x']); + deps.armQuitOnDisconnect(); + deps.schedule(() => {}, 500); + assert.equal(deps.convertTicksToSeconds(20_000_000), 2); + deps.preloadExternalSubtitles({ session: {} as never, clientInfo: {} as never, itemId: 'i' }); + deps.setActivePlayback({ itemId: 'i', mediaSourceId: undefined, playMethod: 'DirectPlay' }); + deps.setLastProgressAtMs(0); + deps.reportPlaying({ itemId: 'i', mediaSourceId: undefined, playMethod: 'DirectPlay', eventName: 'start' }); + deps.showMpvOsd('ok'); + + assert.deepEqual(calls, [ + 'defaults', + 'cmd:show-text', + 'arm', + 'schedule:500', + 'preload', + 'active', + 'progress', + 'report', + 'osd:ok', + ]); +}); diff --git a/src/main/runtime/jellyfin-playback-launch-main-deps.ts b/src/main/runtime/jellyfin-playback-launch-main-deps.ts new file mode 100644 index 0000000..83af3c1 --- /dev/null +++ b/src/main/runtime/jellyfin-playback-launch-main-deps.ts @@ -0,0 +1,21 @@ +import type { createPlayJellyfinItemInMpvHandler } from './jellyfin-playback-launch'; + +type PlayJellyfinItemInMpvMainDeps = Parameters[0]; + +export function createBuildPlayJellyfinItemInMpvMainDepsHandler(deps: PlayJellyfinItemInMpvMainDeps) { + return (): PlayJellyfinItemInMpvMainDeps => ({ + ensureMpvConnectedForPlayback: () => deps.ensureMpvConnectedForPlayback(), + getMpvClient: () => deps.getMpvClient(), + resolvePlaybackPlan: (params) => deps.resolvePlaybackPlan(params), + applyJellyfinMpvDefaults: (mpvClient) => deps.applyJellyfinMpvDefaults(mpvClient), + sendMpvCommand: (command: Array) => deps.sendMpvCommand(command), + armQuitOnDisconnect: () => deps.armQuitOnDisconnect(), + schedule: (callback: () => void, delayMs: number) => deps.schedule(callback, delayMs), + convertTicksToSeconds: (ticks: number) => deps.convertTicksToSeconds(ticks), + preloadExternalSubtitles: (params) => deps.preloadExternalSubtitles(params), + setActivePlayback: (state) => deps.setActivePlayback(state), + setLastProgressAtMs: (value: number) => deps.setLastProgressAtMs(value), + reportPlaying: (payload) => deps.reportPlaying(payload), + showMpvOsd: (text: string) => deps.showMpvOsd(text), + }); +} diff --git a/src/main/runtime/mpv-subtitle-render-metrics-main-deps.test.ts b/src/main/runtime/mpv-subtitle-render-metrics-main-deps.test.ts new file mode 100644 index 0000000..b3ea98e --- /dev/null +++ b/src/main/runtime/mpv-subtitle-render-metrics-main-deps.test.ts @@ -0,0 +1,45 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import type { MpvSubtitleRenderMetrics } from '../../types'; +import { createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler } from './mpv-subtitle-render-metrics-main-deps'; + +const BASE_METRICS: MpvSubtitleRenderMetrics = { + subPos: 100, + subFontSize: 36, + subScale: 1, + subMarginY: 0, + subMarginX: 0, + subFont: '', + subSpacing: 0, + subBold: false, + subItalic: false, + subBorderSize: 0, + subShadowOffset: 0, + subAssOverride: 'yes', + subScaleByWindow: true, + subUseMargins: true, + osdHeight: 0, + osdDimensions: null, +}; + +test('mpv subtitle render metrics main deps builder maps callbacks', () => { + const calls: string[] = []; + const deps = createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler({ + getCurrentMetrics: () => BASE_METRICS, + setCurrentMetrics: () => calls.push('set'), + applyPatch: (current, patch) => { + calls.push('apply'); + return { next: { ...current, ...patch }, changed: true }; + }, + broadcastMetrics: () => calls.push('broadcast'), + })(); + + assert.equal(deps.getCurrentMetrics().subPos, 100); + deps.setCurrentMetrics(BASE_METRICS); + const patched = deps.applyPatch(BASE_METRICS, { subPos: 90 }); + deps.broadcastMetrics(BASE_METRICS); + + assert.equal(patched.changed, true); + assert.equal(patched.next.subPos, 90); + assert.deepEqual(calls, ['set', 'apply', 'broadcast']); +}); diff --git a/src/main/runtime/mpv-subtitle-render-metrics-main-deps.ts b/src/main/runtime/mpv-subtitle-render-metrics-main-deps.ts new file mode 100644 index 0000000..a48235d --- /dev/null +++ b/src/main/runtime/mpv-subtitle-render-metrics-main-deps.ts @@ -0,0 +1,14 @@ +import type { createUpdateMpvSubtitleRenderMetricsHandler } from './mpv-subtitle-render-metrics'; + +type UpdateMpvSubtitleRenderMetricsMainDeps = Parameters[0]; + +export function createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler( + deps: UpdateMpvSubtitleRenderMetricsMainDeps, +) { + return (): UpdateMpvSubtitleRenderMetricsMainDeps => ({ + getCurrentMetrics: () => deps.getCurrentMetrics(), + setCurrentMetrics: (metrics) => deps.setCurrentMetrics(metrics), + applyPatch: (current, patch) => deps.applyPatch(current, patch), + broadcastMetrics: (metrics) => deps.broadcastMetrics(metrics), + }); +} diff --git a/src/main/runtime/numeric-shortcut-runtime-main-deps.test.ts b/src/main/runtime/numeric-shortcut-runtime-main-deps.test.ts new file mode 100644 index 0000000..f9cca84 --- /dev/null +++ b/src/main/runtime/numeric-shortcut-runtime-main-deps.test.ts @@ -0,0 +1,32 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { createBuildNumericShortcutRuntimeMainDepsHandler } from './numeric-shortcut-runtime-main-deps'; + +test('numeric shortcut runtime main deps builder maps callbacks', () => { + const calls: string[] = []; + const deps = createBuildNumericShortcutRuntimeMainDepsHandler({ + globalShortcut: { + register: () => true, + unregister: () => { + calls.push('unregister'); + }, + }, + showMpvOsd: (text) => calls.push(`osd:${text}`), + setTimer: (handler) => { + calls.push('timer'); + handler(); + return 1 as never; + }, + clearTimer: () => { + calls.push('clear'); + }, + })(); + + assert.equal(deps.globalShortcut.register('1', () => {}), true); + deps.globalShortcut.unregister('1'); + deps.showMpvOsd('x'); + deps.setTimer(() => calls.push('tick'), 1000); + deps.clearTimer(1 as never); + + assert.deepEqual(calls, ['unregister', 'osd:x', 'timer', 'tick', 'clear']); +}); diff --git a/src/main/runtime/numeric-shortcut-runtime-main-deps.ts b/src/main/runtime/numeric-shortcut-runtime-main-deps.ts new file mode 100644 index 0000000..d28f306 --- /dev/null +++ b/src/main/runtime/numeric-shortcut-runtime-main-deps.ts @@ -0,0 +1,10 @@ +import type { NumericShortcutRuntimeOptions } from '../../core/services/numeric-shortcut'; + +export function createBuildNumericShortcutRuntimeMainDepsHandler(deps: NumericShortcutRuntimeOptions) { + return (): NumericShortcutRuntimeOptions => ({ + globalShortcut: deps.globalShortcut, + showMpvOsd: (text: string) => deps.showMpvOsd(text), + setTimer: (handler: () => void, timeoutMs: number) => deps.setTimer(handler, timeoutMs), + clearTimer: (timer: ReturnType) => deps.clearTimer(timer), + }); +}