refactor: extract ipc mpv and tokenizer runtime deps

This commit is contained in:
2026-02-10 01:22:13 -08:00
parent 444873c803
commit 7bad8ac65e
9 changed files with 308 additions and 87 deletions

View File

@@ -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/ipc-deps-runtime-service.test.js dist/core/services/anki-jimaku-ipc-deps-runtime-service.test.js dist/core/services/field-grouping-overlay-runtime-service.test.js dist/core/services/subsync-deps-runtime-service.test.js dist/core/services/numeric-shortcut-runtime-service.test.js dist/core/services/numeric-shortcut-session-service.test.js dist/core/services/overlay-visibility-facade-deps-runtime-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/anki-jimaku-ipc-deps-runtime-service.test.js dist/core/services/field-grouping-overlay-runtime-service.test.js dist/core/services/subsync-deps-runtime-service.test.js dist/core/services/numeric-shortcut-runtime-service.test.js dist/core/services/numeric-shortcut-session-service.test.js dist/core/services/overlay-visibility-facade-deps-runtime-service.test.js dist/core/services/mpv-command-ipc-deps-runtime-service.test.js dist/core/services/runtime-options-ipc-deps-runtime-service.test.js dist/core/services/tokenizer-deps-runtime-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",

View File

@@ -6,26 +6,31 @@ import {
SubsyncResult,
} from "../../types";
export interface HandleMpvCommandFromIpcOptions {
specialCommands: {
SUBSYNC_TRIGGER: string;
RUNTIME_OPTIONS_OPEN: string;
RUNTIME_OPTION_CYCLE_PREFIX: string;
REPLAY_SUBTITLE: string;
PLAY_NEXT_SUBTITLE: string;
};
triggerSubsyncFromConfig: () => void;
openRuntimeOptionsPalette: () => void;
runtimeOptionsCycle: (
id: RuntimeOptionId,
direction: 1 | -1,
) => RuntimeOptionApplyResult;
showMpvOsd: (text: string) => void;
mpvReplaySubtitle: () => void;
mpvPlayNextSubtitle: () => void;
mpvSendCommand: (command: (string | number)[]) => void;
isMpvConnected: () => boolean;
hasRuntimeOptionsManager: () => boolean;
}
export function handleMpvCommandFromIpcService(
command: (string | number)[],
options: {
specialCommands: {
SUBSYNC_TRIGGER: string;
RUNTIME_OPTIONS_OPEN: string;
RUNTIME_OPTION_CYCLE_PREFIX: string;
REPLAY_SUBTITLE: string;
PLAY_NEXT_SUBTITLE: string;
};
triggerSubsyncFromConfig: () => void;
openRuntimeOptionsPalette: () => void;
runtimeOptionsCycle: (id: RuntimeOptionId, direction: 1 | -1) => RuntimeOptionApplyResult;
showMpvOsd: (text: string) => void;
mpvReplaySubtitle: () => void;
mpvPlayNextSubtitle: () => void;
mpvSendCommand: (command: (string | number)[]) => void;
isMpvConnected: () => boolean;
hasRuntimeOptionsManager: () => boolean;
},
options: HandleMpvCommandFromIpcOptions,
): void {
const first = typeof command[0] === "string" ? command[0] : "";
if (first === options.specialCommands.SUBSYNC_TRIGGER) {

View File

@@ -0,0 +1,28 @@
import test from "node:test";
import assert from "node:assert/strict";
import { SPECIAL_COMMANDS } from "../../config";
import { createMpvCommandIpcDepsRuntimeService } from "./mpv-command-ipc-deps-runtime-service";
test("createMpvCommandIpcDepsRuntimeService wires runtime-options cycle and manager availability", () => {
const osd: string[] = [];
const deps = createMpvCommandIpcDepsRuntimeService({
specialCommands: SPECIAL_COMMANDS,
triggerSubsyncFromConfig: () => {},
openRuntimeOptionsPalette: () => {},
getRuntimeOptionsManager: () => ({
cycleOption: () => ({ ok: true, osdMessage: "cycled" }),
}),
showMpvOsd: (text) => {
osd.push(text);
},
mpvReplaySubtitle: () => {},
mpvPlayNextSubtitle: () => {},
mpvSendCommand: () => {},
isMpvConnected: () => true,
});
const result = deps.runtimeOptionsCycle("subtitles.secondaryMode" as never, 1);
assert.equal(result.ok, true);
assert.equal(deps.hasRuntimeOptionsManager(), true);
assert.ok(osd.includes("cycled"));
});

View File

@@ -0,0 +1,53 @@
import {
RuntimeOptionApplyResult,
RuntimeOptionId,
} from "../../types";
import {
HandleMpvCommandFromIpcOptions,
} from "./ipc-command-service";
import { applyRuntimeOptionResultRuntimeService } from "./runtime-options-runtime-service";
interface RuntimeOptionsManagerLike {
cycleOption: (
id: RuntimeOptionId,
direction: 1 | -1,
) => RuntimeOptionApplyResult;
}
export interface MpvCommandIpcDepsRuntimeOptions {
specialCommands: HandleMpvCommandFromIpcOptions["specialCommands"];
triggerSubsyncFromConfig: () => void;
openRuntimeOptionsPalette: () => void;
getRuntimeOptionsManager: () => RuntimeOptionsManagerLike | null;
showMpvOsd: (text: string) => void;
mpvReplaySubtitle: () => void;
mpvPlayNextSubtitle: () => void;
mpvSendCommand: (command: (string | number)[]) => void;
isMpvConnected: () => boolean;
}
export function createMpvCommandIpcDepsRuntimeService(
options: MpvCommandIpcDepsRuntimeOptions,
): HandleMpvCommandFromIpcOptions {
return {
specialCommands: options.specialCommands,
triggerSubsyncFromConfig: options.triggerSubsyncFromConfig,
openRuntimeOptionsPalette: options.openRuntimeOptionsPalette,
runtimeOptionsCycle: (id, direction) => {
const manager = options.getRuntimeOptionsManager();
if (!manager) {
return { ok: false, error: "Runtime options manager unavailable" };
}
return applyRuntimeOptionResultRuntimeService(
manager.cycleOption(id, direction),
options.showMpvOsd,
);
},
showMpvOsd: options.showMpvOsd,
mpvReplaySubtitle: options.mpvReplaySubtitle,
mpvPlayNextSubtitle: options.mpvPlayNextSubtitle,
mpvSendCommand: options.mpvSendCommand,
isMpvConnected: options.isMpvConnected,
hasRuntimeOptionsManager: () => options.getRuntimeOptionsManager() !== null,
};
}

View File

@@ -0,0 +1,28 @@
import test from "node:test";
import assert from "node:assert/strict";
import { createRuntimeOptionsIpcDepsRuntimeService } from "./runtime-options-ipc-deps-runtime-service";
test("createRuntimeOptionsIpcDepsRuntimeService delegates set/cycle with osd", () => {
const osd: string[] = [];
const deps = createRuntimeOptionsIpcDepsRuntimeService({
getRuntimeOptionsManager: () => ({
setOptionValue: () => ({ ok: true, osdMessage: "set ok" }),
cycleOption: () => ({ ok: true, osdMessage: "cycle ok" }),
}),
showMpvOsd: (text) => {
osd.push(text);
},
});
const setResult = deps.setRuntimeOption("subtitles.secondaryMode", "hidden") as {
ok: boolean;
};
const cycleResult = deps.cycleRuntimeOption("subtitles.secondaryMode", 1) as {
ok: boolean;
};
assert.equal(setResult.ok, true);
assert.equal(cycleResult.ok, true);
assert.ok(osd.includes("set ok"));
assert.ok(osd.includes("cycle ok"));
});

View File

@@ -0,0 +1,38 @@
import {
RuntimeOptionId,
RuntimeOptionValue,
} from "../../types";
import {
cycleRuntimeOptionFromIpcRuntimeService,
RuntimeOptionsManagerLike,
setRuntimeOptionFromIpcRuntimeService,
} from "./runtime-options-runtime-service";
export interface RuntimeOptionsIpcDepsRuntimeOptions {
getRuntimeOptionsManager: () => RuntimeOptionsManagerLike | null;
showMpvOsd: (text: string) => void;
}
export function createRuntimeOptionsIpcDepsRuntimeService(
options: RuntimeOptionsIpcDepsRuntimeOptions,
): {
setRuntimeOption: (id: string, value: unknown) => unknown;
cycleRuntimeOption: (id: string, direction: 1 | -1) => unknown;
} {
return {
setRuntimeOption: (id, value) =>
setRuntimeOptionFromIpcRuntimeService(
options.getRuntimeOptionsManager(),
id as RuntimeOptionId,
value as RuntimeOptionValue,
options.showMpvOsd,
),
cycleRuntimeOption: (id, direction) =>
cycleRuntimeOptionFromIpcRuntimeService(
options.getRuntimeOptionsManager(),
id as RuntimeOptionId,
direction,
options.showMpvOsd,
),
};
}

View File

@@ -0,0 +1,48 @@
import test from "node:test";
import assert from "node:assert/strict";
import { PartOfSpeech } from "../../types";
import { createTokenizerDepsRuntimeService } from "./tokenizer-deps-runtime-service";
test("createTokenizerDepsRuntimeService tokenizes with mecab and merge", async () => {
let parserWindow: any = null;
let readyPromise: Promise<void> | null = null;
let initPromise: Promise<boolean> | null = null;
const deps = createTokenizerDepsRuntimeService({
getYomitanExt: () => null,
getYomitanParserWindow: () => parserWindow,
setYomitanParserWindow: (window) => {
parserWindow = window;
},
getYomitanParserReadyPromise: () => readyPromise,
setYomitanParserReadyPromise: (promise) => {
readyPromise = promise;
},
getYomitanParserInitPromise: () => initPromise,
setYomitanParserInitPromise: (promise) => {
initPromise = promise;
},
getMecabTokenizer: () => ({
tokenize: async () => [
{
word: "猫",
partOfSpeech: PartOfSpeech.noun,
pos1: "名詞",
pos2: "一般",
pos3: "",
pos4: "",
inflectionType: "",
inflectionForm: "",
headword: "猫",
katakanaReading: "ネコ",
pronunciation: "ネコ",
},
],
}),
});
const merged = await deps.tokenizeWithMecab("猫");
assert.ok(Array.isArray(merged));
assert.equal(merged?.length, 1);
assert.equal(merged?.[0]?.surface, "猫");
});

View File

@@ -0,0 +1,45 @@
import { BrowserWindow, Extension } from "electron";
import { mergeTokens } from "../../token-merger";
import { TokenizerServiceDeps } from "./tokenizer-service";
interface RawTokenLike {}
interface MecabTokenizerLike {
tokenize: (text: string) => Promise<RawTokenLike[] | null>;
}
export interface TokenizerDepsRuntimeOptions {
getYomitanExt: () => Extension | null;
getYomitanParserWindow: () => BrowserWindow | null;
setYomitanParserWindow: (window: BrowserWindow | null) => void;
getYomitanParserReadyPromise: () => Promise<void> | null;
setYomitanParserReadyPromise: (promise: Promise<void> | null) => void;
getYomitanParserInitPromise: () => Promise<boolean> | null;
setYomitanParserInitPromise: (promise: Promise<boolean> | null) => void;
getMecabTokenizer: () => MecabTokenizerLike | null;
}
export function createTokenizerDepsRuntimeService(
options: TokenizerDepsRuntimeOptions,
): TokenizerServiceDeps {
return {
getYomitanExt: options.getYomitanExt,
getYomitanParserWindow: options.getYomitanParserWindow,
setYomitanParserWindow: options.setYomitanParserWindow,
getYomitanParserReadyPromise: options.getYomitanParserReadyPromise,
setYomitanParserReadyPromise: options.setYomitanParserReadyPromise,
getYomitanParserInitPromise: options.getYomitanParserInitPromise,
setYomitanParserInitPromise: options.setYomitanParserInitPromise,
tokenizeWithMecab: async (text) => {
const mecabTokenizer = options.getMecabTokenizer();
if (!mecabTokenizer) {
return null;
}
const rawTokens = await mecabTokenizer.tokenize(text);
if (!rawTokens || rawTokens.length === 0) {
return null;
}
return mergeTokens(rawTokens as never);
},
};
}

View File

@@ -46,7 +46,6 @@ import * as os from "os";
import * as fs from "fs";
import * as crypto from "crypto";
import { MecabTokenizer } from "./mecab-tokenizer";
import { mergeTokens } from "./token-merger";
import { BaseWindowTracker } from "./window-trackers";
import {
JimakuApiResponse,
@@ -64,9 +63,7 @@ import {
KikuFieldGroupingChoice,
KikuMergePreviewRequest,
KikuMergePreviewResponse,
RuntimeOptionId,
RuntimeOptionState,
RuntimeOptionValue,
MpvSubtitleRenderMetrics,
} from "./types";
import { SubtitleTimingTracker } from "./subtitle-timing-tracker";
@@ -136,11 +133,6 @@ import {
setMpvSubVisibilityRuntimeService,
showMpvOsdRuntimeService,
} from "./core/services/mpv-runtime-service";
import {
applyRuntimeOptionResultRuntimeService,
cycleRuntimeOptionFromIpcRuntimeService,
setRuntimeOptionFromIpcRuntimeService,
} from "./core/services/runtime-options-runtime-service";
import {
getInitialInvisibleOverlayVisibilityService,
isAutoUpdateEnabledRuntimeService,
@@ -206,6 +198,9 @@ import { createFieldGroupingOverlayRuntimeService } from "./core/services/field-
import { createSubsyncRuntimeDepsService } from "./core/services/subsync-deps-runtime-service";
import { createNumericShortcutRuntimeService } from "./core/services/numeric-shortcut-runtime-service";
import { createOverlayVisibilityFacadeDepsRuntimeService } from "./core/services/overlay-visibility-facade-deps-runtime-service";
import { createMpvCommandIpcDepsRuntimeService } from "./core/services/mpv-command-ipc-deps-runtime-service";
import { createRuntimeOptionsIpcDepsRuntimeService } from "./core/services/runtime-options-ipc-deps-runtime-service";
import { createTokenizerDepsRuntimeService } from "./core/services/tokenizer-deps-runtime-service";
import { createRuntimeOptionsManagerRuntimeService } from "./core/services/runtime-options-manager-runtime-service";
import { createAppLoggingRuntimeService } from "./core/services/app-logging-runtime-service";
import {
@@ -753,31 +748,25 @@ function updateMpvSubtitleRenderMetrics(
}
async function tokenizeSubtitle(text: string): Promise<SubtitleData> {
return tokenizeSubtitleService(text, {
getYomitanExt: () => yomitanExt,
getYomitanParserWindow: () => yomitanParserWindow,
setYomitanParserWindow: (window) => {
yomitanParserWindow = window;
},
getYomitanParserReadyPromise: () => yomitanParserReadyPromise,
setYomitanParserReadyPromise: (promise) => {
yomitanParserReadyPromise = promise;
},
getYomitanParserInitPromise: () => yomitanParserInitPromise,
setYomitanParserInitPromise: (promise) => {
yomitanParserInitPromise = promise;
},
tokenizeWithMecab: async (tokenizeText) => {
if (!mecabTokenizer) {
return null;
}
const rawTokens = await mecabTokenizer.tokenize(tokenizeText);
if (!rawTokens || rawTokens.length === 0) {
return null;
}
return mergeTokens(rawTokens);
},
});
return tokenizeSubtitleService(
text,
createTokenizerDepsRuntimeService({
getYomitanExt: () => yomitanExt,
getYomitanParserWindow: () => yomitanParserWindow,
setYomitanParserWindow: (window) => {
yomitanParserWindow = window;
},
getYomitanParserReadyPromise: () => yomitanParserReadyPromise,
setYomitanParserReadyPromise: (promise) => {
yomitanParserReadyPromise = promise;
},
getYomitanParserInitPromise: () => yomitanParserInitPromise,
setYomitanParserInitPromise: (promise) => {
yomitanParserInitPromise = promise;
},
getMecabTokenizer: () => mecabTokenizer,
}),
);
}
function updateOverlayBounds(geometry: WindowGeometry): void {
@@ -1218,27 +1207,21 @@ function handleOverlayModalClosed(modal: OverlayHostedModal): void {
}
function handleMpvCommandFromIpc(command: (string | number)[]): void {
handleMpvCommandFromIpcService(command, {
specialCommands: SPECIAL_COMMANDS,
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
runtimeOptionsCycle: (id, direction) => {
if (!runtimeOptionsManager) {
return { ok: false, error: "Runtime options manager unavailable" };
}
return applyRuntimeOptionResultRuntimeService(
runtimeOptionsManager.cycleOption(id, direction),
(text) => showMpvOsd(text),
);
},
showMpvOsd: (text) => showMpvOsd(text),
mpvReplaySubtitle: () => replayCurrentSubtitleRuntimeService(mpvClient),
mpvPlayNextSubtitle: () => playNextSubtitleRuntimeService(mpvClient),
mpvSendCommand: (rawCommand) =>
sendMpvCommandRuntimeService(mpvClient, rawCommand),
isMpvConnected: () => Boolean(mpvClient && mpvClient.connected),
hasRuntimeOptionsManager: () => runtimeOptionsManager !== null,
});
handleMpvCommandFromIpcService(
command,
createMpvCommandIpcDepsRuntimeService({
specialCommands: SPECIAL_COMMANDS,
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
getRuntimeOptionsManager: () => runtimeOptionsManager,
showMpvOsd: (text) => showMpvOsd(text),
mpvReplaySubtitle: () => replayCurrentSubtitleRuntimeService(mpvClient),
mpvPlayNextSubtitle: () => playNextSubtitleRuntimeService(mpvClient),
mpvSendCommand: (rawCommand) =>
sendMpvCommandRuntimeService(mpvClient, rawCommand),
isMpvConnected: () => Boolean(mpvClient && mpvClient.connected),
}),
);
}
async function runSubsyncManualFromIpc(
@@ -1247,6 +1230,11 @@ async function runSubsyncManualFromIpc(
return runSubsyncManualFromIpcRuntimeService(request, getSubsyncRuntimeDeps());
}
const runtimeOptionsIpcDeps = createRuntimeOptionsIpcDepsRuntimeService({
getRuntimeOptionsManager: () => runtimeOptionsManager,
showMpvOsd: (text) => showMpvOsd(text),
});
registerIpcHandlersService(
createIpcDepsRuntimeService({
getInvisibleWindow: () => invisibleWindow,
@@ -1274,20 +1262,8 @@ registerIpcHandlersService(
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),
),
setRuntimeOption: runtimeOptionsIpcDeps.setRuntimeOption,
cycleRuntimeOption: runtimeOptionsIpcDeps.cycleRuntimeOption,
}),
);