From f476c3c2057bda71ee4cfced6c5e6d773a7025a3 Mon Sep 17 00:00:00 2001 From: sudacode Date: Tue, 10 Feb 2026 01:03:10 -0800 Subject: [PATCH] refactor: extract ipc deps runtime service --- package.json | 2 +- .../services/ipc-deps-runtime-service.test.ts | 108 ++++++++++++++++++ src/core/services/ipc-deps-runtime-service.ts | 100 ++++++++++++++++ src/main.ts | 101 +++++++--------- 4 files changed, 253 insertions(+), 58 deletions(-) create mode 100644 src/core/services/ipc-deps-runtime-service.test.ts create mode 100644 src/core/services/ipc-deps-runtime-service.ts diff --git a/package.json b/package.json index 2f49904..a70e9bc 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "docs:build": "vitepress build docs", "docs:preview": "vitepress preview docs --host 0.0.0.0 --port 4173 --strictPort", "test:config": "pnpm run build && node --test dist/config/config.test.js", - "test:core": "pnpm run build && node --test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command-service.test.js dist/core/services/cli-command-deps-runtime-service.test.js dist/core/services/numeric-shortcut-session-service.test.js dist/core/services/secondary-subtitle-service.test.js dist/core/services/mpv-render-metrics-service.test.js dist/core/services/mpv-runtime-service.test.js dist/core/services/runtime-options-runtime-service.test.js dist/core/services/overlay-modal-restore-service.test.js dist/core/services/runtime-config-service.test.js dist/core/services/overlay-bridge-runtime-service.test.js dist/core/services/overlay-visibility-facade-service.test.js dist/core/services/overlay-broadcast-runtime-service.test.js dist/core/services/app-ready-runtime-service.test.js dist/core/services/app-shutdown-runtime-service.test.js dist/core/services/mpv-client-deps-runtime-service.test.js dist/core/services/app-lifecycle-deps-runtime-service.test.js dist/core/services/runtime-options-manager-runtime-service.test.js dist/core/services/config-warning-runtime-service.test.js dist/core/services/app-logging-runtime-service.test.js dist/core/services/startup-resource-runtime-service.test.js dist/core/services/config-generation-runtime-service.test.js", + "test:core": "pnpm run build && node --test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command-service.test.js dist/core/services/cli-command-deps-runtime-service.test.js dist/core/services/ipc-deps-runtime-service.test.js dist/core/services/numeric-shortcut-session-service.test.js dist/core/services/secondary-subtitle-service.test.js dist/core/services/mpv-render-metrics-service.test.js dist/core/services/mpv-runtime-service.test.js dist/core/services/runtime-options-runtime-service.test.js dist/core/services/overlay-modal-restore-service.test.js dist/core/services/runtime-config-service.test.js dist/core/services/overlay-bridge-runtime-service.test.js dist/core/services/overlay-visibility-facade-service.test.js dist/core/services/overlay-broadcast-runtime-service.test.js dist/core/services/app-ready-runtime-service.test.js dist/core/services/app-shutdown-runtime-service.test.js dist/core/services/mpv-client-deps-runtime-service.test.js dist/core/services/app-lifecycle-deps-runtime-service.test.js dist/core/services/runtime-options-manager-runtime-service.test.js dist/core/services/config-warning-runtime-service.test.js dist/core/services/app-logging-runtime-service.test.js dist/core/services/startup-resource-runtime-service.test.js dist/core/services/config-generation-runtime-service.test.js", "test:subtitle": "pnpm run build && node --test dist/subtitle/stages.test.js dist/subtitle/pipeline.test.js", "generate:config-example": "pnpm run build && node dist/generate-config-example.js", "start": "pnpm run build && electron . --start", diff --git a/src/core/services/ipc-deps-runtime-service.test.ts b/src/core/services/ipc-deps-runtime-service.test.ts new file mode 100644 index 0000000..66650db --- /dev/null +++ b/src/core/services/ipc-deps-runtime-service.test.ts @@ -0,0 +1,108 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createIpcDepsRuntimeService } from "./ipc-deps-runtime-service"; + +test("createIpcDepsRuntimeService maps window and mecab helpers", async () => { + let ignoreMouse: { ignore: boolean; forward?: boolean } | null = null; + let toggledDevTools = 0; + let mecabEnabled: boolean | null = null; + + const visibleWindow = { + isDestroyed: () => false, + setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => { + ignoreMouse = { ignore, forward: options?.forward }; + }, + webContents: { + toggleDevTools: () => { + toggledDevTools += 1; + }, + }, + }; + + const deps = createIpcDepsRuntimeService({ + getInvisibleWindow: () => visibleWindow, + getMainWindow: () => visibleWindow, + getVisibleOverlayVisibility: () => true, + getInvisibleOverlayVisibility: () => false, + onOverlayModalClosed: () => {}, + openYomitanSettings: () => {}, + quitApp: () => {}, + toggleVisibleOverlay: () => {}, + tokenizeCurrentSubtitle: async () => ({ text: "x" }), + getCurrentSubtitleAss: () => "ass", + getMpvSubtitleRenderMetrics: () => ({ subPos: 100 }), + getSubtitlePosition: () => ({ x: 1, y: 2 }), + getSubtitleStyle: () => null, + saveSubtitlePosition: () => {}, + getMecabTokenizer: () => ({ + getStatus: () => ({ available: true, enabled: true, path: "/usr/bin/mecab" }), + setEnabled: (enabled: boolean) => { + mecabEnabled = enabled; + }, + }), + handleMpvCommand: () => {}, + getKeybindings: () => ({ copySubtitle: ["C"] }), + getSecondarySubMode: () => "hidden", + getMpvClient: () => ({ currentSecondarySubText: "secondary" }), + runSubsyncManual: async () => ({ ok: true }), + getAnkiConnectStatus: () => true, + getRuntimeOptions: () => ({ values: {} }), + setRuntimeOption: () => ({ ok: true }), + cycleRuntimeOption: () => ({ ok: true }), + }); + + deps.setInvisibleIgnoreMouseEvents(true, { forward: true }); + deps.toggleDevTools(); + deps.setMecabEnabled(false); + + assert.deepEqual(ignoreMouse, { ignore: true, forward: true }); + assert.equal(toggledDevTools, 1); + assert.equal(mecabEnabled, false); + assert.deepEqual(deps.getMecabStatus(), { + available: true, + enabled: true, + path: "/usr/bin/mecab", + }); + assert.equal(deps.getCurrentSecondarySub(), "secondary"); + assert.deepEqual(await deps.tokenizeCurrentSubtitle(), { text: "x" }); +}); + +test("createIpcDepsRuntimeService handles missing optional runtime resources", () => { + const deps = createIpcDepsRuntimeService({ + getInvisibleWindow: () => null, + getMainWindow: () => null, + getVisibleOverlayVisibility: () => false, + getInvisibleOverlayVisibility: () => false, + onOverlayModalClosed: () => {}, + openYomitanSettings: () => {}, + quitApp: () => {}, + toggleVisibleOverlay: () => {}, + tokenizeCurrentSubtitle: async () => null, + getCurrentSubtitleAss: () => "", + getMpvSubtitleRenderMetrics: () => null, + getSubtitlePosition: () => null, + getSubtitleStyle: () => null, + saveSubtitlePosition: () => {}, + getMecabTokenizer: () => null, + handleMpvCommand: () => {}, + getKeybindings: () => null, + getSecondarySubMode: () => "hidden", + getMpvClient: () => null, + runSubsyncManual: async () => ({ ok: false }), + getAnkiConnectStatus: () => false, + getRuntimeOptions: () => null, + setRuntimeOption: () => ({ ok: false }), + cycleRuntimeOption: () => ({ ok: false }), + }); + + deps.setInvisibleIgnoreMouseEvents(true, { forward: true }); + deps.toggleDevTools(); + deps.setMecabEnabled(true); + + assert.deepEqual(deps.getMecabStatus(), { + available: false, + enabled: false, + path: null, + }); + assert.equal(deps.getCurrentSecondarySub(), ""); +}); diff --git a/src/core/services/ipc-deps-runtime-service.ts b/src/core/services/ipc-deps-runtime-service.ts new file mode 100644 index 0000000..24ff129 --- /dev/null +++ b/src/core/services/ipc-deps-runtime-service.ts @@ -0,0 +1,100 @@ +import { IpcServiceDeps } from "./ipc-service"; + +interface WindowLike { + isDestroyed: () => boolean; + setIgnoreMouseEvents: ( + ignore: boolean, + options?: { forward?: boolean }, + ) => void; + webContents: { + toggleDevTools: () => void; + }; +} + +interface MecabTokenizerLike { + getStatus: () => { available: boolean; enabled: boolean; path: string | null }; + setEnabled: (enabled: boolean) => void; +} + +interface MpvClientLike { + currentSecondarySubText?: string; +} + +export interface IpcDepsRuntimeOptions { + getInvisibleWindow: () => WindowLike | null; + getMainWindow: () => WindowLike | null; + getVisibleOverlayVisibility: () => boolean; + getInvisibleOverlayVisibility: () => boolean; + onOverlayModalClosed: (modal: string) => void; + openYomitanSettings: () => void; + quitApp: () => void; + toggleVisibleOverlay: () => void; + tokenizeCurrentSubtitle: () => Promise; + getCurrentSubtitleAss: () => string; + getMpvSubtitleRenderMetrics: () => unknown; + getSubtitlePosition: () => unknown; + getSubtitleStyle: () => unknown; + saveSubtitlePosition: (position: unknown) => void; + getMecabTokenizer: () => MecabTokenizerLike | null; + handleMpvCommand: (command: Array) => void; + getKeybindings: () => unknown; + getSecondarySubMode: () => unknown; + getMpvClient: () => MpvClientLike | null; + runSubsyncManual: (request: unknown) => Promise; + getAnkiConnectStatus: () => boolean; + getRuntimeOptions: () => unknown; + setRuntimeOption: (id: string, value: unknown) => unknown; + cycleRuntimeOption: (id: string, direction: 1 | -1) => unknown; +} + +export function createIpcDepsRuntimeService( + options: IpcDepsRuntimeOptions, +): IpcServiceDeps { + return { + getInvisibleWindow: () => options.getInvisibleWindow() as never, + isVisibleOverlayVisible: options.getVisibleOverlayVisibility, + setInvisibleIgnoreMouseEvents: (ignore, eventsOptions) => { + const invisibleWindow = options.getInvisibleWindow(); + if (!invisibleWindow || invisibleWindow.isDestroyed()) return; + invisibleWindow.setIgnoreMouseEvents(ignore, eventsOptions); + }, + onOverlayModalClosed: options.onOverlayModalClosed, + openYomitanSettings: options.openYomitanSettings, + quitApp: options.quitApp, + toggleDevTools: () => { + const mainWindow = options.getMainWindow(); + if (!mainWindow || mainWindow.isDestroyed()) return; + mainWindow.webContents.toggleDevTools(); + }, + getVisibleOverlayVisibility: options.getVisibleOverlayVisibility, + toggleVisibleOverlay: options.toggleVisibleOverlay, + getInvisibleOverlayVisibility: options.getInvisibleOverlayVisibility, + tokenizeCurrentSubtitle: options.tokenizeCurrentSubtitle, + getCurrentSubtitleAss: options.getCurrentSubtitleAss, + getMpvSubtitleRenderMetrics: options.getMpvSubtitleRenderMetrics, + getSubtitlePosition: options.getSubtitlePosition, + getSubtitleStyle: options.getSubtitleStyle, + saveSubtitlePosition: options.saveSubtitlePosition, + getMecabStatus: () => { + const mecabTokenizer = options.getMecabTokenizer(); + return mecabTokenizer + ? mecabTokenizer.getStatus() + : { available: false, enabled: false, path: null }; + }, + setMecabEnabled: (enabled) => { + const mecabTokenizer = options.getMecabTokenizer(); + if (!mecabTokenizer) return; + mecabTokenizer.setEnabled(enabled); + }, + handleMpvCommand: options.handleMpvCommand, + getKeybindings: options.getKeybindings, + getSecondarySubMode: options.getSecondarySubMode, + getCurrentSecondarySub: () => + options.getMpvClient()?.currentSecondarySubText || "", + runSubsyncManual: options.runSubsyncManual, + getAnkiConnectStatus: options.getAnkiConnectStatus, + getRuntimeOptions: options.getRuntimeOptions, + setRuntimeOption: options.setRuntimeOption, + cycleRuntimeOption: options.cycleRuntimeOption, + }; +} diff --git a/src/main.ts b/src/main.ts index c652094..5679599 100644 --- a/src/main.ts +++ b/src/main.ts @@ -206,6 +206,7 @@ import { runAppShutdownRuntimeService } from "./core/services/app-shutdown-runti import { createMpvIpcClientDepsRuntimeService } from "./core/services/mpv-client-deps-runtime-service"; import { createAppLifecycleDepsRuntimeService } from "./core/services/app-lifecycle-deps-runtime-service"; import { createCliCommandDepsRuntimeService } from "./core/services/cli-command-deps-runtime-service"; +import { createIpcDepsRuntimeService } from "./core/services/ipc-deps-runtime-service"; import { createRuntimeOptionsManagerRuntimeService } from "./core/services/runtime-options-manager-runtime-service"; import { createAppLoggingRuntimeService } from "./core/services/app-logging-runtime-service"; import { @@ -1243,63 +1244,49 @@ async function runSubsyncManualFromIpc( return runSubsyncManualFromIpcRuntimeService(request, getSubsyncRuntimeDeps()); } -registerIpcHandlersService({ - getInvisibleWindow: () => invisibleWindow, - isVisibleOverlayVisible: () => visibleOverlayVisible, - setInvisibleIgnoreMouseEvents: (ignore, options) => { - if (!invisibleWindow || invisibleWindow.isDestroyed()) return; - invisibleWindow.setIgnoreMouseEvents(ignore, options); - }, - onOverlayModalClosed: (modal) => - handleOverlayModalClosed(modal as OverlayHostedModal), - openYomitanSettings: () => openYomitanSettings(), - quitApp: () => app.quit(), - toggleDevTools: () => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.toggleDevTools(); - } - }, - getVisibleOverlayVisibility: () => visibleOverlayVisible, - toggleVisibleOverlay: () => toggleVisibleOverlay(), - getInvisibleOverlayVisibility: () => invisibleOverlayVisible, - tokenizeCurrentSubtitle: () => tokenizeSubtitle(currentSubText), - getCurrentSubtitleAss: () => currentSubAssText, - getMpvSubtitleRenderMetrics: () => mpvSubtitleRenderMetrics, - getSubtitlePosition: () => loadSubtitlePosition(), - getSubtitleStyle: () => getResolvedConfig().subtitleStyle ?? null, - saveSubtitlePosition: (position) => saveSubtitlePosition(position as SubtitlePosition), - getMecabStatus: () => - mecabTokenizer - ? mecabTokenizer.getStatus() - : { available: false, enabled: false, path: null }, - setMecabEnabled: (enabled) => { - if (mecabTokenizer) mecabTokenizer.setEnabled(enabled); - }, - handleMpvCommand: (command) => handleMpvCommandFromIpc(command), - getKeybindings: () => keybindings, - getSecondarySubMode: () => secondarySubMode, - getCurrentSecondarySub: () => mpvClient?.currentSecondarySubText || "", - runSubsyncManual: (request) => - runSubsyncManualFromIpc(request as SubsyncManualRunRequest), - getAnkiConnectStatus: () => ankiIntegration !== null, - getRuntimeOptions: () => getRuntimeOptionsState(), - setRuntimeOption: (id, value) => { - return setRuntimeOptionFromIpcRuntimeService( - runtimeOptionsManager, - id as RuntimeOptionId, - value as RuntimeOptionValue, - (text) => showMpvOsd(text), - ); - }, - cycleRuntimeOption: (id, direction) => { - return cycleRuntimeOptionFromIpcRuntimeService( - runtimeOptionsManager, - id as RuntimeOptionId, - direction, - (text) => showMpvOsd(text), - ); - }, -}); +registerIpcHandlersService( + createIpcDepsRuntimeService({ + getInvisibleWindow: () => invisibleWindow, + getMainWindow: () => mainWindow, + getVisibleOverlayVisibility: () => visibleOverlayVisible, + getInvisibleOverlayVisibility: () => invisibleOverlayVisible, + onOverlayModalClosed: (modal) => + handleOverlayModalClosed(modal as OverlayHostedModal), + openYomitanSettings: () => openYomitanSettings(), + quitApp: () => app.quit(), + toggleVisibleOverlay: () => toggleVisibleOverlay(), + tokenizeCurrentSubtitle: () => tokenizeSubtitle(currentSubText), + getCurrentSubtitleAss: () => currentSubAssText, + getMpvSubtitleRenderMetrics: () => mpvSubtitleRenderMetrics, + getSubtitlePosition: () => loadSubtitlePosition(), + getSubtitleStyle: () => getResolvedConfig().subtitleStyle ?? null, + saveSubtitlePosition: (position) => + saveSubtitlePosition(position as SubtitlePosition), + getMecabTokenizer: () => mecabTokenizer, + handleMpvCommand: (command) => handleMpvCommandFromIpc(command), + getKeybindings: () => keybindings, + getSecondarySubMode: () => secondarySubMode, + getMpvClient: () => mpvClient, + runSubsyncManual: (request) => + runSubsyncManualFromIpc(request as SubsyncManualRunRequest), + getAnkiConnectStatus: () => ankiIntegration !== null, + getRuntimeOptions: () => getRuntimeOptionsState(), + setRuntimeOption: (id, value) => + setRuntimeOptionFromIpcRuntimeService( + runtimeOptionsManager, + id as RuntimeOptionId, + value as RuntimeOptionValue, + (text) => showMpvOsd(text), + ), + cycleRuntimeOption: (id, direction) => + cycleRuntimeOptionFromIpcRuntimeService( + runtimeOptionsManager, + id as RuntimeOptionId, + direction, + (text) => showMpvOsd(text), + ), + }), +); /** * Create and show a desktop notification with robust icon handling.