diff --git a/package.json b/package.json index 1c5a6a6..2f49904 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/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/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/cli-command-deps-runtime-service.test.ts b/src/core/services/cli-command-deps-runtime-service.test.ts new file mode 100644 index 0000000..81370fc --- /dev/null +++ b/src/core/services/cli-command-deps-runtime-service.test.ts @@ -0,0 +1,110 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createCliCommandDepsRuntimeService } from "./cli-command-deps-runtime-service"; + +test("createCliCommandDepsRuntimeService wires runtime helpers", () => { + let socketPath = "/tmp/mpv"; + let setClientSocketPath: string | null = null; + let connectCalls = 0; + let texthookerPort = 7000; + let texthookerRunning = false; + let texthookerStartPort: number | null = null; + let overlayVisible = false; + let overlayInvisible = false; + let openYomitanAfterDelay: number | null = null; + + const deps = createCliCommandDepsRuntimeService({ + mpv: { + getSocketPath: () => socketPath, + setSocketPath: (next) => { + socketPath = next; + }, + getClient: () => ({ + setSocketPath: (next) => { + setClientSocketPath = next; + }, + connect: () => { + connectCalls += 1; + }, + }), + showOsd: () => {}, + }, + texthooker: { + service: { + isRunning: () => texthookerRunning, + start: (port) => { + texthookerRunning = true; + texthookerStartPort = port; + }, + }, + getPort: () => texthookerPort, + setPort: (port) => { + texthookerPort = port; + }, + shouldOpenBrowser: () => true, + openInBrowser: () => {}, + }, + overlay: { + isInitialized: () => false, + initialize: () => {}, + toggleVisible: () => { + overlayVisible = !overlayVisible; + }, + toggleInvisible: () => { + overlayInvisible = !overlayInvisible; + }, + setVisible: (visible) => { + overlayVisible = visible; + }, + setInvisible: (visible) => { + overlayInvisible = visible; + }, + }, + mining: { + copyCurrentSubtitle: () => {}, + startPendingMultiCopy: () => {}, + mineSentenceCard: async () => {}, + startPendingMineSentenceMultiple: () => {}, + updateLastCardFromClipboard: async () => {}, + triggerFieldGrouping: async () => {}, + triggerSubsyncFromConfig: async () => {}, + markLastCardAsAudioCard: async () => {}, + }, + ui: { + openYomitanSettings: () => {}, + cycleSecondarySubMode: () => {}, + openRuntimeOptionsPalette: () => {}, + printHelp: () => {}, + }, + app: { + stop: () => {}, + hasMainWindow: () => true, + }, + getMultiCopyTimeoutMs: () => 2500, + schedule: (_fn, delayMs) => { + openYomitanAfterDelay = delayMs; + return null; + }, + log: () => {}, + warn: () => {}, + error: () => {}, + }); + + deps.setMpvSocketPath("/tmp/new"); + deps.setMpvClientSocketPath("/tmp/new"); + deps.connectMpvClient(); + deps.ensureTexthookerRunning(9000); + deps.openYomitanSettingsDelayed(1000); + deps.toggleVisibleOverlay(); + deps.toggleInvisibleOverlay(); + + assert.equal(deps.getMpvSocketPath(), "/tmp/new"); + assert.equal(setClientSocketPath, "/tmp/new"); + assert.equal(connectCalls, 1); + assert.equal(texthookerStartPort, 9000); + assert.equal(texthookerPort, 7000); + assert.equal(openYomitanAfterDelay, 1000); + assert.equal(overlayVisible, true); + assert.equal(overlayInvisible, true); + assert.equal(deps.getMultiCopyTimeoutMs(), 2500); +}); diff --git a/src/core/services/cli-command-deps-runtime-service.ts b/src/core/services/cli-command-deps-runtime-service.ts new file mode 100644 index 0000000..7bb06d4 --- /dev/null +++ b/src/core/services/cli-command-deps-runtime-service.ts @@ -0,0 +1,132 @@ +import { CliCommandServiceDeps } from "./cli-command-service"; + +interface MpvClientLike { + setSocketPath: (socketPath: string) => void; + connect: () => void; +} + +interface TexthookerServiceLike { + isRunning: () => boolean; + start: (port: number) => void; +} + +interface MpvCliRuntime { + getSocketPath: () => string; + setSocketPath: (socketPath: string) => void; + getClient: () => MpvClientLike | null; + showOsd: (text: string) => void; +} + +interface TexthookerCliRuntime { + service: TexthookerServiceLike; + getPort: () => number; + setPort: (port: number) => void; + shouldOpenBrowser: () => boolean; + openInBrowser: (url: string) => void; +} + +interface OverlayCliRuntime { + isInitialized: () => boolean; + initialize: () => void; + toggleVisible: () => void; + toggleInvisible: () => void; + setVisible: (visible: boolean) => void; + setInvisible: (visible: boolean) => void; +} + +interface MiningCliRuntime { + copyCurrentSubtitle: () => void; + startPendingMultiCopy: (timeoutMs: number) => void; + mineSentenceCard: () => Promise; + startPendingMineSentenceMultiple: (timeoutMs: number) => void; + updateLastCardFromClipboard: () => Promise; + triggerFieldGrouping: () => Promise; + triggerSubsyncFromConfig: () => Promise; + markLastCardAsAudioCard: () => Promise; +} + +interface UiCliRuntime { + openYomitanSettings: () => void; + cycleSecondarySubMode: () => void; + openRuntimeOptionsPalette: () => void; + printHelp: () => void; +} + +interface AppCliRuntime { + stop: () => void; + hasMainWindow: () => boolean; +} + +export interface CliCommandDepsRuntimeOptions { + mpv: MpvCliRuntime; + texthooker: TexthookerCliRuntime; + overlay: OverlayCliRuntime; + mining: MiningCliRuntime; + ui: UiCliRuntime; + app: AppCliRuntime; + getMultiCopyTimeoutMs: () => number; + schedule: (fn: () => void, delayMs: number) => unknown; + log: (message: string) => void; + warn: (message: string) => void; + error: (message: string, err: unknown) => void; +} + +export function createCliCommandDepsRuntimeService( + options: CliCommandDepsRuntimeOptions, +): CliCommandServiceDeps { + return { + getMpvSocketPath: options.mpv.getSocketPath, + setMpvSocketPath: options.mpv.setSocketPath, + setMpvClientSocketPath: (socketPath) => { + const client = options.mpv.getClient(); + if (!client) return; + client.setSocketPath(socketPath); + }, + hasMpvClient: () => Boolean(options.mpv.getClient()), + connectMpvClient: () => { + const client = options.mpv.getClient(); + if (!client) return; + client.connect(); + }, + isTexthookerRunning: () => options.texthooker.service.isRunning(), + setTexthookerPort: options.texthooker.setPort, + getTexthookerPort: options.texthooker.getPort, + shouldOpenTexthookerBrowser: options.texthooker.shouldOpenBrowser, + ensureTexthookerRunning: (port) => { + if (!options.texthooker.service.isRunning()) { + options.texthooker.service.start(port); + } + }, + openTexthookerInBrowser: options.texthooker.openInBrowser, + stopApp: options.app.stop, + isOverlayRuntimeInitialized: options.overlay.isInitialized, + initializeOverlayRuntime: options.overlay.initialize, + toggleVisibleOverlay: options.overlay.toggleVisible, + toggleInvisibleOverlay: options.overlay.toggleInvisible, + openYomitanSettingsDelayed: (delayMs) => { + options.schedule(() => { + options.ui.openYomitanSettings(); + }, delayMs); + }, + setVisibleOverlayVisible: options.overlay.setVisible, + setInvisibleOverlayVisible: options.overlay.setInvisible, + copyCurrentSubtitle: options.mining.copyCurrentSubtitle, + startPendingMultiCopy: options.mining.startPendingMultiCopy, + mineSentenceCard: options.mining.mineSentenceCard, + startPendingMineSentenceMultiple: + options.mining.startPendingMineSentenceMultiple, + updateLastCardFromClipboard: options.mining.updateLastCardFromClipboard, + cycleSecondarySubMode: options.ui.cycleSecondarySubMode, + triggerFieldGrouping: options.mining.triggerFieldGrouping, + triggerSubsyncFromConfig: options.mining.triggerSubsyncFromConfig, + markLastCardAsAudioCard: options.mining.markLastCardAsAudioCard, + openRuntimeOptionsPalette: options.ui.openRuntimeOptionsPalette, + printHelp: options.ui.printHelp, + hasMainWindow: options.app.hasMainWindow, + getMultiCopyTimeoutMs: options.getMultiCopyTimeoutMs, + showMpvOsd: options.mpv.showOsd, + log: options.log, + warn: options.warn, + error: options.error, + }; +} diff --git a/src/main.ts b/src/main.ts index fa036f6..c652094 100644 --- a/src/main.ts +++ b/src/main.ts @@ -205,6 +205,7 @@ import { runAppReadyRuntimeService } from "./core/services/app-ready-runtime-ser import { runAppShutdownRuntimeService } from "./core/services/app-shutdown-runtime-service"; 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 { createRuntimeOptionsManagerRuntimeService } from "./core/services/runtime-options-manager-runtime-service"; import { createAppLoggingRuntimeService } from "./core/services/app-logging-runtime-service"; import { @@ -653,63 +654,57 @@ function handleCliCommand( args: CliArgs, source: CliCommandSource = "initial", ): void { - handleCliCommandService(args, source, { - getMpvSocketPath: () => mpvSocketPath, - setMpvSocketPath: (socketPath) => { - mpvSocketPath = socketPath; + const deps = createCliCommandDepsRuntimeService({ + mpv: { + getSocketPath: () => mpvSocketPath, + setSocketPath: (socketPath) => { + mpvSocketPath = socketPath; + }, + getClient: () => mpvClient, + showOsd: (text) => showMpvOsd(text), }, - setMpvClientSocketPath: (socketPath) => { - if (!mpvClient) return; - mpvClient.setSocketPath(socketPath); + texthooker: { + service: texthookerService, + getPort: () => texthookerPort, + setPort: (port) => { + texthookerPort = port; + }, + shouldOpenBrowser: () => getResolvedConfig().texthooker?.openBrowser !== false, + openInBrowser: (url) => { + shell.openExternal(url); + }, }, - hasMpvClient: () => Boolean(mpvClient), - connectMpvClient: () => { - if (!mpvClient) return; - mpvClient.connect(); + overlay: { + isInitialized: () => overlayRuntimeInitialized, + initialize: () => initializeOverlayRuntime(), + toggleVisible: () => toggleVisibleOverlay(), + toggleInvisible: () => toggleInvisibleOverlay(), + setVisible: (visible) => setVisibleOverlayVisible(visible), + setInvisible: (visible) => setInvisibleOverlayVisible(visible), }, - isTexthookerRunning: () => texthookerService.isRunning(), - setTexthookerPort: (port) => { - texthookerPort = port; + mining: { + copyCurrentSubtitle: () => copyCurrentSubtitle(), + startPendingMultiCopy: (timeoutMs) => startPendingMultiCopy(timeoutMs), + mineSentenceCard: () => mineSentenceCard(), + startPendingMineSentenceMultiple: (timeoutMs) => + startPendingMineSentenceMultiple(timeoutMs), + updateLastCardFromClipboard: () => updateLastCardFromClipboard(), + triggerFieldGrouping: () => triggerFieldGrouping(), + triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(), + markLastCardAsAudioCard: () => markLastCardAsAudioCard(), }, - getTexthookerPort: () => texthookerPort, - shouldOpenTexthookerBrowser: () => - getResolvedConfig().texthooker?.openBrowser !== false, - ensureTexthookerRunning: (port) => { - if (!texthookerService.isRunning()) { - texthookerService.start(port); - } + ui: { + openYomitanSettings: () => openYomitanSettings(), + cycleSecondarySubMode: () => cycleSecondarySubMode(), + openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(), + printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT), }, - openTexthookerInBrowser: (url) => { - shell.openExternal(url); + app: { + stop: () => app.quit(), + hasMainWindow: () => Boolean(mainWindow), }, - stopApp: () => app.quit(), - isOverlayRuntimeInitialized: () => overlayRuntimeInitialized, - initializeOverlayRuntime: () => initializeOverlayRuntime(), - toggleVisibleOverlay: () => toggleVisibleOverlay(), - toggleInvisibleOverlay: () => toggleInvisibleOverlay(), - openYomitanSettingsDelayed: (delayMs) => { - setTimeout(() => { - openYomitanSettings(); - }, delayMs); - }, - setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible), - setInvisibleOverlayVisible: (visible) => - setInvisibleOverlayVisible(visible), - copyCurrentSubtitle: () => copyCurrentSubtitle(), - startPendingMultiCopy: (timeoutMs) => startPendingMultiCopy(timeoutMs), - mineSentenceCard: () => mineSentenceCard(), - startPendingMineSentenceMultiple: (timeoutMs) => - startPendingMineSentenceMultiple(timeoutMs), - updateLastCardFromClipboard: () => updateLastCardFromClipboard(), - cycleSecondarySubMode: () => cycleSecondarySubMode(), - triggerFieldGrouping: () => triggerFieldGrouping(), - triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(), - markLastCardAsAudioCard: () => markLastCardAsAudioCard(), - openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(), - printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT), - hasMainWindow: () => Boolean(mainWindow), getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs, - showMpvOsd: (text) => showMpvOsd(text), + schedule: (fn, delayMs) => setTimeout(fn, delayMs), log: (message) => { console.log(message); }, @@ -720,6 +715,7 @@ function handleCliCommand( console.error(message, err); }, }); + handleCliCommandService(args, source, deps); } function handleInitialArgs(): void {