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

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