refactor: extract cli command deps runtime service

This commit is contained in:
2026-02-10 00:58:57 -08:00
parent b21204c7a0
commit 3686788a72
4 changed files with 289 additions and 51 deletions

View File

@@ -16,7 +16,7 @@
"docs:build": "vitepress build docs", "docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs --host 0.0.0.0 --port 4173 --strictPort", "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: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", "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", "generate:config-example": "pnpm run build && node dist/generate-config-example.js",
"start": "pnpm run build && electron . --start", "start": "pnpm run build && electron . --start",

View File

@@ -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);
});

View File

@@ -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<void>;
startPendingMineSentenceMultiple: (timeoutMs: number) => void;
updateLastCardFromClipboard: () => Promise<void>;
triggerFieldGrouping: () => Promise<void>;
triggerSubsyncFromConfig: () => Promise<void>;
markLastCardAsAudioCard: () => Promise<void>;
}
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,
};
}

View File

@@ -205,6 +205,7 @@ import { runAppReadyRuntimeService } from "./core/services/app-ready-runtime-ser
import { runAppShutdownRuntimeService } from "./core/services/app-shutdown-runtime-service"; import { runAppShutdownRuntimeService } from "./core/services/app-shutdown-runtime-service";
import { createMpvIpcClientDepsRuntimeService } from "./core/services/mpv-client-deps-runtime-service"; import { createMpvIpcClientDepsRuntimeService } from "./core/services/mpv-client-deps-runtime-service";
import { createAppLifecycleDepsRuntimeService } from "./core/services/app-lifecycle-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 { createRuntimeOptionsManagerRuntimeService } from "./core/services/runtime-options-manager-runtime-service";
import { createAppLoggingRuntimeService } from "./core/services/app-logging-runtime-service"; import { createAppLoggingRuntimeService } from "./core/services/app-logging-runtime-service";
import { import {
@@ -653,63 +654,57 @@ function handleCliCommand(
args: CliArgs, args: CliArgs,
source: CliCommandSource = "initial", source: CliCommandSource = "initial",
): void { ): void {
handleCliCommandService(args, source, { const deps = createCliCommandDepsRuntimeService({
getMpvSocketPath: () => mpvSocketPath, mpv: {
setMpvSocketPath: (socketPath) => { getSocketPath: () => mpvSocketPath,
setSocketPath: (socketPath) => {
mpvSocketPath = socketPath; mpvSocketPath = socketPath;
}, },
setMpvClientSocketPath: (socketPath) => { getClient: () => mpvClient,
if (!mpvClient) return; showOsd: (text) => showMpvOsd(text),
mpvClient.setSocketPath(socketPath);
}, },
hasMpvClient: () => Boolean(mpvClient), texthooker: {
connectMpvClient: () => { service: texthookerService,
if (!mpvClient) return; getPort: () => texthookerPort,
mpvClient.connect(); setPort: (port) => {
},
isTexthookerRunning: () => texthookerService.isRunning(),
setTexthookerPort: (port) => {
texthookerPort = port; texthookerPort = port;
}, },
getTexthookerPort: () => texthookerPort, shouldOpenBrowser: () => getResolvedConfig().texthooker?.openBrowser !== false,
shouldOpenTexthookerBrowser: () => openInBrowser: (url) => {
getResolvedConfig().texthooker?.openBrowser !== false,
ensureTexthookerRunning: (port) => {
if (!texthookerService.isRunning()) {
texthookerService.start(port);
}
},
openTexthookerInBrowser: (url) => {
shell.openExternal(url); shell.openExternal(url);
}, },
stopApp: () => app.quit(),
isOverlayRuntimeInitialized: () => overlayRuntimeInitialized,
initializeOverlayRuntime: () => initializeOverlayRuntime(),
toggleVisibleOverlay: () => toggleVisibleOverlay(),
toggleInvisibleOverlay: () => toggleInvisibleOverlay(),
openYomitanSettingsDelayed: (delayMs) => {
setTimeout(() => {
openYomitanSettings();
}, delayMs);
}, },
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible), overlay: {
setInvisibleOverlayVisible: (visible) => isInitialized: () => overlayRuntimeInitialized,
setInvisibleOverlayVisible(visible), initialize: () => initializeOverlayRuntime(),
toggleVisible: () => toggleVisibleOverlay(),
toggleInvisible: () => toggleInvisibleOverlay(),
setVisible: (visible) => setVisibleOverlayVisible(visible),
setInvisible: (visible) => setInvisibleOverlayVisible(visible),
},
mining: {
copyCurrentSubtitle: () => copyCurrentSubtitle(), copyCurrentSubtitle: () => copyCurrentSubtitle(),
startPendingMultiCopy: (timeoutMs) => startPendingMultiCopy(timeoutMs), startPendingMultiCopy: (timeoutMs) => startPendingMultiCopy(timeoutMs),
mineSentenceCard: () => mineSentenceCard(), mineSentenceCard: () => mineSentenceCard(),
startPendingMineSentenceMultiple: (timeoutMs) => startPendingMineSentenceMultiple: (timeoutMs) =>
startPendingMineSentenceMultiple(timeoutMs), startPendingMineSentenceMultiple(timeoutMs),
updateLastCardFromClipboard: () => updateLastCardFromClipboard(), updateLastCardFromClipboard: () => updateLastCardFromClipboard(),
cycleSecondarySubMode: () => cycleSecondarySubMode(),
triggerFieldGrouping: () => triggerFieldGrouping(), triggerFieldGrouping: () => triggerFieldGrouping(),
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(), triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
markLastCardAsAudioCard: () => markLastCardAsAudioCard(), markLastCardAsAudioCard: () => markLastCardAsAudioCard(),
},
ui: {
openYomitanSettings: () => openYomitanSettings(),
cycleSecondarySubMode: () => cycleSecondarySubMode(),
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(), openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT), printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
},
app: {
stop: () => app.quit(),
hasMainWindow: () => Boolean(mainWindow), hasMainWindow: () => Boolean(mainWindow),
},
getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs, getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs,
showMpvOsd: (text) => showMpvOsd(text), schedule: (fn, delayMs) => setTimeout(fn, delayMs),
log: (message) => { log: (message) => {
console.log(message); console.log(message);
}, },
@@ -720,6 +715,7 @@ function handleCliCommand(
console.error(message, err); console.error(message, err);
}, },
}); });
handleCliCommandService(args, source, deps);
} }
function handleInitialArgs(): void { function handleInitialArgs(): void {