refactor(core): normalize core service naming

Standardize core service module and export names to reduce naming ambiguity and make imports predictable across runtime, tests, scripts, and docs.
This commit is contained in:
2026-02-17 01:18:10 -08:00
parent 02034e6dc7
commit a359e91b14
80 changed files with 793 additions and 771 deletions

View File

@@ -2,8 +2,8 @@ import test from "node:test";
import assert from "node:assert/strict";
import {
AnkiJimakuIpcRuntimeOptions,
registerAnkiJimakuIpcRuntimeService,
} from "./anki-jimaku-service";
registerAnkiJimakuIpcRuntime,
} from "./anki-jimaku";
interface RuntimeHarness {
options: AnkiJimakuIpcRuntimeOptions;
@@ -92,7 +92,7 @@ function createHarness(): RuntimeHarness {
};
let registered: Record<string, (...args: unknown[]) => unknown> = {};
registerAnkiJimakuIpcRuntimeService(
registerAnkiJimakuIpcRuntime(
options,
(deps) => {
registered = deps as unknown as Record<string, (...args: unknown[]) => unknown>;
@@ -102,7 +102,7 @@ function createHarness(): RuntimeHarness {
return { options, registered, state };
}
test("registerAnkiJimakuIpcRuntimeService provides full handler surface", () => {
test("registerAnkiJimakuIpcRuntime provides full handler surface", () => {
const { registered } = createHarness();
const expected = [
"setAnkiConnectEnabled",

View File

@@ -10,7 +10,7 @@ import {
KikuFieldGroupingRequestData,
} from "../../types";
import { sortJimakuFiles } from "../../jimaku/utils";
import type { AnkiJimakuIpcDeps } from "./anki-jimaku-ipc-service";
import type { AnkiJimakuIpcDeps } from "./anki-jimaku-ipc";
import { createLogger } from "../../logger";
export type RegisterAnkiJimakuIpcRuntimeHandler = (
@@ -65,7 +65,7 @@ export interface AnkiJimakuIpcRuntimeOptions {
const logger = createLogger("main:anki-jimaku");
export function registerAnkiJimakuIpcRuntimeService(
export function registerAnkiJimakuIpcRuntime(
options: AnkiJimakuIpcRuntimeOptions,
registerHandlers: RegisterAnkiJimakuIpcRuntimeHandler,
): void {

View File

@@ -44,7 +44,7 @@ export interface AppLifecycleDepsRuntimeOptions {
restoreWindowsOnActivate: () => void;
}
export function createAppLifecycleDepsRuntimeService(
export function createAppLifecycleDepsRuntime(
options: AppLifecycleDepsRuntimeOptions,
): AppLifecycleServiceDeps {
return {
@@ -80,7 +80,7 @@ export function createAppLifecycleDepsRuntimeService(
};
}
export function startAppLifecycleService(
export function startAppLifecycle(
initialArgs: CliArgs,
deps: AppLifecycleServiceDeps,
): void {

View File

@@ -1,6 +1,6 @@
import test from "node:test";
import assert from "node:assert/strict";
import { AppReadyRuntimeDeps, runAppReadyRuntimeService } from "./startup-service";
import { AppReadyRuntimeDeps, runAppReadyRuntime } from "./startup";
function makeDeps(overrides: Partial<AppReadyRuntimeDeps> = {}) {
const calls: string[] = [];
@@ -37,11 +37,11 @@ function makeDeps(overrides: Partial<AppReadyRuntimeDeps> = {}) {
return { deps, calls };
}
test("runAppReadyRuntimeService starts websocket in auto mode when plugin missing", async () => {
test("runAppReadyRuntime starts websocket in auto mode when plugin missing", async () => {
const { deps, calls } = makeDeps({
hasMpvWebsocketPlugin: () => false,
});
await runAppReadyRuntimeService(deps);
await runAppReadyRuntime(deps);
assert.ok(calls.includes("startSubtitleWebsocket:9001"));
assert.ok(calls.includes("initializeOverlayRuntime"));
assert.ok(calls.includes("createImmersionTracker"));
@@ -80,11 +80,11 @@ test("runAppReadyRuntimeService logs and continues when createImmersionTracker t
assert.ok(calls.includes("handleInitialArgs"));
});
test("runAppReadyRuntimeService logs defer message when overlay not auto-started", async () => {
test("runAppReadyRuntime logs defer message when overlay not auto-started", async () => {
const { deps, calls } = makeDeps({
shouldAutoInitializeOverlayRuntimeFromConfig: () => false,
});
await runAppReadyRuntimeService(deps);
await runAppReadyRuntime(deps);
assert.ok(
calls.includes(
"log:Overlay runtime deferred: waiting for explicit overlay command.",
@@ -92,7 +92,7 @@ test("runAppReadyRuntimeService logs defer message when overlay not auto-started
);
});
test("runAppReadyRuntimeService applies config logging level during app-ready", async () => {
test("runAppReadyRuntime applies config logging level during app-ready", async () => {
const { deps, calls } = makeDeps({
getResolvedConfig: () => ({
websocket: { enabled: "auto" },
@@ -100,6 +100,6 @@ test("runAppReadyRuntimeService applies config logging level during app-ready",
logging: { level: "warn" },
}),
});
await runAppReadyRuntimeService(deps);
await runAppReadyRuntime(deps);
assert.ok(calls.includes("setLogLevel:warn:config"));
});

View File

@@ -1,7 +1,7 @@
import test from "node:test";
import assert from "node:assert/strict";
import { CliArgs } from "../../cli/args";
import { CliCommandServiceDeps, handleCliCommandService } from "./cli-command-service";
import { CliCommandServiceDeps, handleCliCommand } from "./cli-command";
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
return {
@@ -148,21 +148,21 @@ function createDeps(overrides: Partial<CliCommandServiceDeps> = {}) {
return { deps, calls, osd };
}
test("handleCliCommandService ignores --start for second-instance without actions", () => {
test("handleCliCommand ignores --start for second-instance without actions", () => {
const { deps, calls } = createDeps();
const args = makeArgs({ start: true });
handleCliCommandService(args, "second-instance", deps);
handleCliCommand(args, "second-instance", deps);
assert.ok(calls.includes("log:Ignoring --start because SubMiner is already running."));
assert.equal(calls.some((value) => value.includes("connectMpvClient")), false);
});
test("handleCliCommandService runs texthooker flow with browser open", () => {
test("handleCliCommand runs texthooker flow with browser open", () => {
const { deps, calls } = createDeps();
const args = makeArgs({ texthooker: true });
handleCliCommandService(args, "initial", deps);
handleCliCommand(args, "initial", deps);
assert.ok(calls.includes("ensureTexthookerRunning:5174"));
assert.ok(
@@ -170,24 +170,24 @@ test("handleCliCommandService runs texthooker flow with browser open", () => {
);
});
test("handleCliCommandService reports async mine errors to OSD", async () => {
test("handleCliCommand reports async mine errors to OSD", async () => {
const { deps, calls, osd } = createDeps({
mineSentenceCard: async () => {
throw new Error("boom");
},
});
handleCliCommandService(makeArgs({ mineSentence: true }), "initial", deps);
handleCliCommand(makeArgs({ mineSentence: true }), "initial", deps);
await new Promise((resolve) => setImmediate(resolve));
assert.ok(calls.some((value) => value.startsWith("error:mineSentenceCard failed:")));
assert.ok(osd.some((value) => value.includes("Mine sentence failed: boom")));
});
test("handleCliCommandService applies socket path and connects on start", () => {
test("handleCliCommand applies socket path and connects on start", () => {
const { deps, calls } = createDeps();
handleCliCommandService(
handleCliCommand(
makeArgs({ start: true, socketPath: "/tmp/custom.sock" }),
"initial",
deps,
@@ -198,12 +198,12 @@ test("handleCliCommandService applies socket path and connects on start", () =>
assert.ok(calls.includes("connectMpvClient"));
});
test("handleCliCommandService warns when texthooker port override used while running", () => {
test("handleCliCommand warns when texthooker port override used while running", () => {
const { deps, calls } = createDeps({
isTexthookerRunning: () => true,
});
handleCliCommandService(
handleCliCommand(
makeArgs({ texthookerPort: 9999, texthooker: true }),
"initial",
deps,
@@ -217,25 +217,25 @@ test("handleCliCommandService warns when texthooker port override used while run
assert.equal(calls.some((value) => value === "setTexthookerPort:9999"), false);
});
test("handleCliCommandService prints help and stops app when no window exists", () => {
test("handleCliCommand prints help and stops app when no window exists", () => {
const { deps, calls } = createDeps({
hasMainWindow: () => false,
});
handleCliCommandService(makeArgs({ help: true }), "initial", deps);
handleCliCommand(makeArgs({ help: true }), "initial", deps);
assert.ok(calls.includes("printHelp"));
assert.ok(calls.includes("stopApp"));
});
test("handleCliCommandService reports async trigger-subsync errors to OSD", async () => {
test("handleCliCommand reports async trigger-subsync errors to OSD", async () => {
const { deps, calls, osd } = createDeps({
triggerSubsyncFromConfig: async () => {
throw new Error("subsync boom");
},
});
handleCliCommandService(makeArgs({ triggerSubsync: true }), "initial", deps);
handleCliCommand(makeArgs({ triggerSubsync: true }), "initial", deps);
await new Promise((resolve) => setImmediate(resolve));
assert.ok(
@@ -244,16 +244,16 @@ test("handleCliCommandService reports async trigger-subsync errors to OSD", asyn
assert.ok(osd.some((value) => value.includes("Subsync failed: subsync boom")));
});
test("handleCliCommandService stops app for --stop command", () => {
test("handleCliCommand stops app for --stop command", () => {
const { deps, calls } = createDeps();
handleCliCommandService(makeArgs({ stop: true }), "initial", deps);
handleCliCommand(makeArgs({ stop: true }), "initial", deps);
assert.ok(calls.includes("log:Stopping SubMiner..."));
assert.ok(calls.includes("stopApp"));
});
test("handleCliCommandService still runs non-start actions on second-instance", () => {
test("handleCliCommand still runs non-start actions on second-instance", () => {
const { deps, calls } = createDeps();
handleCliCommandService(
handleCliCommand(
makeArgs({ start: true, toggleVisibleOverlay: true }),
"second-instance",
deps,
@@ -262,7 +262,7 @@ test("handleCliCommandService still runs non-start actions on second-instance",
assert.equal(calls.some((value) => value === "connectMpvClient"), true);
});
test("handleCliCommandService handles visibility and utility command dispatches", () => {
test("handleCliCommand handles visibility and utility command dispatches", () => {
const cases: Array<{
args: Partial<CliArgs>;
expected: string;
@@ -285,7 +285,7 @@ test("handleCliCommandService handles visibility and utility command dispatches"
for (const entry of cases) {
const { deps, calls } = createDeps();
handleCliCommandService(makeArgs(entry.args), "initial", deps);
handleCliCommand(makeArgs(entry.args), "initial", deps);
assert.ok(
calls.includes(entry.expected),
`expected call missing for args ${JSON.stringify(entry.args)}: ${entry.expected}`,
@@ -293,22 +293,22 @@ test("handleCliCommandService handles visibility and utility command dispatches"
}
});
test("handleCliCommandService runs refresh-known-words command", () => {
test("handleCliCommand runs refresh-known-words command", () => {
const { deps, calls } = createDeps();
handleCliCommandService(makeArgs({ refreshKnownWords: true }), "initial", deps);
handleCliCommand(makeArgs({ refreshKnownWords: true }), "initial", deps);
assert.ok(calls.includes("refreshKnownWords"));
});
test("handleCliCommandService reports async refresh-known-words errors to OSD", async () => {
test("handleCliCommand reports async refresh-known-words errors to OSD", async () => {
const { deps, calls, osd } = createDeps({
refreshKnownWords: async () => {
throw new Error("refresh boom");
},
});
handleCliCommandService(makeArgs({ refreshKnownWords: true }), "initial", deps);
handleCliCommand(makeArgs({ refreshKnownWords: true }), "initial", deps);
await new Promise((resolve) => setImmediate(resolve));
assert.ok(

View File

@@ -116,7 +116,7 @@ export interface CliCommandDepsRuntimeOptions {
error: (message: string, err: unknown) => void;
}
export function createCliCommandDepsRuntimeService(
export function createCliCommandDepsRuntime(
options: CliCommandDepsRuntimeOptions,
): CliCommandServiceDeps {
return {
@@ -189,7 +189,7 @@ function runAsyncWithOsd(
});
}
export function handleCliCommandService(
export function handleCliCommand(
args: CliArgs,
source: CliCommandSource = "initial",
deps: CliCommandServiceDeps,

View File

@@ -1,15 +1,15 @@
import test from "node:test";
import assert from "node:assert/strict";
import { KikuFieldGroupingChoice } from "../../types";
import { createFieldGroupingOverlayRuntimeService } from "./field-grouping-overlay-service";
import { createFieldGroupingOverlayRuntime } from "./field-grouping-overlay";
test("createFieldGroupingOverlayRuntimeService sends overlay messages and sets restore flag", () => {
test("createFieldGroupingOverlayRuntime sends overlay messages and sets restore flag", () => {
const sent: unknown[][] = [];
let visible = false;
const restore = new Set<"runtime-options" | "subsync">();
const runtime =
createFieldGroupingOverlayRuntimeService<"runtime-options" | "subsync">({
createFieldGroupingOverlayRuntime<"runtime-options" | "subsync">({
getMainWindow: () => ({
isDestroyed: () => false,
webContents: {
@@ -40,10 +40,10 @@ test("createFieldGroupingOverlayRuntimeService sends overlay messages and sets r
assert.deepEqual(sent, [["runtime-options:open"]]);
});
test("createFieldGroupingOverlayRuntimeService callback cancels when send fails", async () => {
test("createFieldGroupingOverlayRuntime callback cancels when send fails", async () => {
let resolver: ((choice: KikuFieldGroupingChoice) => void) | null = null;
const runtime =
createFieldGroupingOverlayRuntimeService<"runtime-options" | "subsync">({
createFieldGroupingOverlayRuntime<"runtime-options" | "subsync">({
getMainWindow: () => null,
getVisibleOverlayVisible: () => false,
getInvisibleOverlayVisible: () => false,

View File

@@ -3,9 +3,9 @@ import {
KikuFieldGroupingRequestData,
} from "../../types";
import {
createFieldGroupingCallbackRuntimeService,
sendToVisibleOverlayRuntimeService,
} from "./overlay-bridge-service";
createFieldGroupingCallbackRuntime,
sendToVisibleOverlayRuntime,
} from "./overlay-bridge";
interface WindowLike {
isDestroyed: () => boolean;
@@ -32,7 +32,7 @@ export interface FieldGroupingOverlayRuntimeOptions<T extends string> {
) => boolean;
}
export function createFieldGroupingOverlayRuntimeService<T extends string>(
export function createFieldGroupingOverlayRuntime<T extends string>(
options: FieldGroupingOverlayRuntimeOptions<T>,
): {
sendToVisibleOverlay: (
@@ -52,7 +52,7 @@ export function createFieldGroupingOverlayRuntimeService<T extends string>(
if (options.sendToVisibleOverlay) {
return options.sendToVisibleOverlay(channel, payload, runtimeOptions);
}
return sendToVisibleOverlayRuntimeService({
return sendToVisibleOverlayRuntime({
mainWindow: options.getMainWindow() as never,
visibleOverlayVisible: options.getVisibleOverlayVisible(),
setVisibleOverlayVisible: options.setVisibleOverlayVisible,
@@ -67,7 +67,7 @@ export function createFieldGroupingOverlayRuntimeService<T extends string>(
const createFieldGroupingCallback = (): ((
data: KikuFieldGroupingRequestData,
) => Promise<KikuFieldGroupingChoice>) => {
return createFieldGroupingCallbackRuntimeService({
return createFieldGroupingCallbackRuntime({
getVisibleOverlayVisible: options.getVisibleOverlayVisible,
getInvisibleOverlayVisible: options.getInvisibleOverlayVisible,
setVisibleOverlayVisible: options.setVisibleOverlayVisible,

View File

@@ -3,7 +3,7 @@ import {
KikuFieldGroupingRequestData,
} from "../../types";
export function createFieldGroupingCallbackService(options: {
export function createFieldGroupingCallback(options: {
getVisibleOverlayVisible: () => boolean;
getInvisibleOverlayVisible: () => boolean;
setVisibleOverlayVisible: (visible: boolean) => void;

View File

@@ -4,15 +4,15 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { createFrequencyDictionaryLookupService } from "./frequency-dictionary-service";
import { createFrequencyDictionaryLookup } from "./frequency-dictionary";
test("createFrequencyDictionaryLookupService logs parse errors and returns no-op for invalid dictionaries", async () => {
test("createFrequencyDictionaryLookup logs parse errors and returns no-op for invalid dictionaries", async () => {
const logs: string[] = [];
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "subminer-frequency-dict-"));
const bankPath = path.join(tempDir, "term_meta_bank_1.json");
fs.writeFileSync(bankPath, "{ invalid json");
const lookup = await createFrequencyDictionaryLookupService({
const lookup = await createFrequencyDictionaryLookup({
searchPaths: [tempDir],
log: (message) => {
logs.push(message);
@@ -31,10 +31,10 @@ test("createFrequencyDictionaryLookupService logs parse errors and returns no-op
);
});
test("createFrequencyDictionaryLookupService continues with no-op lookup when search path is missing", async () => {
test("createFrequencyDictionaryLookup continues with no-op lookup when search path is missing", async () => {
const logs: string[] = [];
const missingPath = path.join(os.tmpdir(), "subminer-frequency-dict-missing-dir");
const lookup = await createFrequencyDictionaryLookupService({
const lookup = await createFrequencyDictionaryLookup({
searchPaths: [missingPath],
log: (message) => {
logs.push(message);

View File

@@ -145,7 +145,7 @@ function collectDictionaryFromPath(
return terms;
}
export async function createFrequencyDictionaryLookupService(
export async function createFrequencyDictionaryLookup(
options: FrequencyDictionaryLookupOptions,
): Promise<(term: string) => number | null> {
const attemptedPaths: string[] = [];

View File

@@ -1,39 +1,39 @@
export { TexthookerService } from "./texthooker-service";
export { hasMpvWebsocketPlugin, SubtitleWebSocketService } from "./subtitle-ws-service";
export { registerGlobalShortcutsService } from "./shortcut-service";
export { createIpcDepsRuntimeService, registerIpcHandlersService } from "./ipc-service";
export { shortcutMatchesInputForLocalFallback } from "./shortcut-fallback-service";
export { Texthooker } from "./texthooker";
export { hasMpvWebsocketPlugin, SubtitleWebSocket } from "./subtitle-ws";
export { registerGlobalShortcuts } from "./shortcut";
export { createIpcDepsRuntime, registerIpcHandlers } from "./ipc";
export { shortcutMatchesInputForLocalFallback } from "./shortcut-fallback";
export {
refreshOverlayShortcutsRuntimeService,
registerOverlayShortcutsService,
syncOverlayShortcutsRuntimeService,
unregisterOverlayShortcutsRuntimeService,
} from "./overlay-shortcut-service";
refreshOverlayShortcutsRuntime,
registerOverlayShortcuts,
syncOverlayShortcutsRuntime,
unregisterOverlayShortcutsRuntime,
} from "./overlay-shortcut";
export { createOverlayShortcutRuntimeHandlers } from "./overlay-shortcut-handler";
export { createCliCommandDepsRuntimeService, handleCliCommandService } from "./cli-command-service";
export { createCliCommandDepsRuntime, handleCliCommand } from "./cli-command";
export {
copyCurrentSubtitleService,
handleMineSentenceDigitService,
handleMultiCopyDigitService,
markLastCardAsAudioCardService,
mineSentenceCardService,
triggerFieldGroupingService,
updateLastCardFromClipboardService,
} from "./mining-service";
export { createAppLifecycleDepsRuntimeService, startAppLifecycleService } from "./app-lifecycle-service";
copyCurrentSubtitle,
handleMineSentenceDigit,
handleMultiCopyDigit,
markLastCardAsAudioCard,
mineSentenceCard,
triggerFieldGrouping,
updateLastCardFromClipboard,
} from "./mining";
export { createAppLifecycleDepsRuntime, startAppLifecycle } from "./app-lifecycle";
export {
cycleSecondarySubModeService,
} from "./subtitle-position-service";
cycleSecondarySubMode,
} from "./subtitle-position";
export {
getInitialInvisibleOverlayVisibilityService,
isAutoUpdateEnabledRuntimeService,
shouldAutoInitializeOverlayRuntimeFromConfigService,
shouldBindVisibleOverlayToMpvSubVisibilityService,
} from "./startup-service";
export { openYomitanSettingsWindow } from "./yomitan-settings-service";
export { createTokenizerDepsRuntimeService, tokenizeSubtitleService } from "./tokenizer-service";
export { createFrequencyDictionaryLookupService } from "./frequency-dictionary-service";
export { createJlptVocabularyLookupService } from "./jlpt-vocab-service";
getInitialInvisibleOverlayVisibility,
isAutoUpdateEnabledRuntime,
shouldAutoInitializeOverlayRuntimeFromConfig,
shouldBindVisibleOverlayToMpvSubVisibility,
} from "./startup";
export { openYomitanSettingsWindow } from "./yomitan-settings";
export { createTokenizerDepsRuntime, tokenizeSubtitle } from "./tokenizer";
export { createFrequencyDictionaryLookup } from "./frequency-dictionary";
export { createJlptVocabularyLookup } from "./jlpt-vocab";
export {
getIgnoredPos1Entries,
JlptIgnoredPos1Entry,
@@ -44,59 +44,59 @@ export {
shouldIgnoreJlptByTerm,
shouldIgnoreJlptForMecabPos1,
} from "./jlpt-token-filter";
export { loadYomitanExtensionService } from "./yomitan-extension-loader-service";
export { loadYomitanExtension } from "./yomitan-extension-loader";
export {
getJimakuLanguagePreferenceService,
getJimakuMaxEntryResultsService,
jimakuFetchJsonService,
resolveJimakuApiKeyService,
} from "./jimaku-service";
getJimakuLanguagePreference,
getJimakuMaxEntryResults,
jimakuFetchJson,
resolveJimakuApiKey,
} from "./jimaku";
export {
loadSubtitlePositionService,
saveSubtitlePositionService,
updateCurrentMediaPathService,
} from "./subtitle-position-service";
loadSubtitlePosition,
saveSubtitlePosition,
updateCurrentMediaPath,
} from "./subtitle-position";
export {
createOverlayWindowService,
enforceOverlayLayerOrderService,
ensureOverlayWindowLevelService,
updateOverlayWindowBoundsService,
} from "./overlay-window-service";
export { initializeOverlayRuntimeService } from "./overlay-runtime-init-service";
createOverlayWindow,
enforceOverlayLayerOrder,
ensureOverlayWindowLevel,
updateOverlayWindowBounds,
} from "./overlay-window";
export { initializeOverlayRuntime } from "./overlay-runtime-init";
export {
setInvisibleOverlayVisibleService,
setVisibleOverlayVisibleService,
syncInvisibleOverlayMousePassthroughService,
updateInvisibleOverlayVisibilityService,
updateVisibleOverlayVisibilityService,
} from "./overlay-visibility-service";
setInvisibleOverlayVisible,
setVisibleOverlayVisible,
syncInvisibleOverlayMousePassthrough,
updateInvisibleOverlayVisibility,
updateVisibleOverlayVisibility,
} from "./overlay-visibility";
export {
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
MpvIpcClient,
MpvRuntimeClientLike,
MpvTrackProperty,
playNextSubtitleRuntimeService,
replayCurrentSubtitleRuntimeService,
playNextSubtitleRuntime,
replayCurrentSubtitleRuntime,
resolveCurrentAudioStreamIndex,
sendMpvCommandRuntimeService,
setMpvSubVisibilityRuntimeService,
showMpvOsdRuntimeService,
} from "./mpv-service";
sendMpvCommandRuntime,
setMpvSubVisibilityRuntime,
showMpvOsdRuntime,
} from "./mpv";
export {
applyMpvSubtitleRenderMetricsPatchService,
applyMpvSubtitleRenderMetricsPatch,
DEFAULT_MPV_SUBTITLE_RENDER_METRICS,
sanitizeMpvSubtitleRenderMetrics,
} from "./mpv-render-metrics-service";
export { createOverlayContentMeasurementStoreService } from "./overlay-content-measurement-service";
export { handleMpvCommandFromIpcService } from "./ipc-command-service";
} from "./mpv-render-metrics";
export { createOverlayContentMeasurementStore } from "./overlay-content-measurement";
export { handleMpvCommandFromIpc } from "./ipc-command";
export { createFieldGroupingOverlayRuntime } from "./field-grouping-overlay";
export { createNumericShortcutRuntime } from "./numeric-shortcut";
export { runStartupBootstrapRuntime } from "./startup";
export { runSubsyncManualFromIpcRuntime, triggerSubsyncFromConfigRuntime } from "./subsync-runner";
export { registerAnkiJimakuIpcRuntime } from "./anki-jimaku";
export { ImmersionTrackerService } from "./immersion-tracker-service";
export { createFieldGroupingOverlayRuntimeService } from "./field-grouping-overlay-service";
export { createNumericShortcutRuntimeService } from "./numeric-shortcut-service";
export { runStartupBootstrapRuntimeService } from "./startup-service";
export { runSubsyncManualFromIpcRuntimeService, triggerSubsyncFromConfigRuntimeService } from "./subsync-runner-service";
export { registerAnkiJimakuIpcRuntimeService } from "./anki-jimaku-service";
export {
broadcastRuntimeOptionsChangedRuntimeService,
createOverlayManagerService,
setOverlayDebugVisualizationEnabledRuntimeService,
} from "./overlay-manager-service";
broadcastRuntimeOptionsChangedRuntime,
createOverlayManager,
setOverlayDebugVisualizationEnabledRuntime,
} from "./overlay-manager";

View File

@@ -28,7 +28,7 @@ export interface HandleMpvCommandFromIpcOptions {
hasRuntimeOptionsManager: () => boolean;
}
export function handleMpvCommandFromIpcService(
export function handleMpvCommandFromIpc(
command: (string | number)[],
options: HandleMpvCommandFromIpcOptions,
): void {
@@ -66,7 +66,7 @@ export function handleMpvCommandFromIpcService(
}
}
export async function runSubsyncManualFromIpcService(
export async function runSubsyncManualFromIpc(
request: SubsyncManualRunRequest,
options: {
isSubsyncInProgress: () => boolean;

View File

@@ -84,7 +84,7 @@ export interface IpcDepsRuntimeOptions {
reportOverlayContentBounds: (payload: unknown) => void;
}
export function createIpcDepsRuntimeService(
export function createIpcDepsRuntime(
options: IpcDepsRuntimeOptions,
): IpcServiceDeps {
return {
@@ -143,7 +143,7 @@ export function createIpcDepsRuntimeService(
};
}
export function registerIpcHandlersService(deps: IpcServiceDeps): void {
export function registerIpcHandlers(deps: IpcServiceDeps): void {
ipcMain.on(
"set-ignore-mouse-events",
(

View File

@@ -8,34 +8,34 @@ import {
resolveJimakuApiKey as resolveJimakuApiKeyFromConfig,
} from "../../jimaku/utils";
export function getJimakuConfigService(
export function getJimakuConfig(
getResolvedConfig: () => { jimaku?: JimakuConfig },
): JimakuConfig {
const config = getResolvedConfig();
return config.jimaku ?? {};
}
export function getJimakuBaseUrlService(
export function getJimakuBaseUrl(
getResolvedConfig: () => { jimaku?: JimakuConfig },
defaultBaseUrl: string,
): string {
const config = getJimakuConfigService(getResolvedConfig);
const config = getJimakuConfig(getResolvedConfig);
return config.apiBaseUrl || defaultBaseUrl;
}
export function getJimakuLanguagePreferenceService(
export function getJimakuLanguagePreference(
getResolvedConfig: () => { jimaku?: JimakuConfig },
defaultPreference: JimakuLanguagePreference,
): JimakuLanguagePreference {
const config = getJimakuConfigService(getResolvedConfig);
const config = getJimakuConfig(getResolvedConfig);
return config.languagePreference || defaultPreference;
}
export function getJimakuMaxEntryResultsService(
export function getJimakuMaxEntryResults(
getResolvedConfig: () => { jimaku?: JimakuConfig },
defaultValue: number,
): number {
const config = getJimakuConfigService(getResolvedConfig);
const config = getJimakuConfig(getResolvedConfig);
const value = config.maxEntryResults;
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
return Math.floor(value);
@@ -43,13 +43,13 @@ export function getJimakuMaxEntryResultsService(
return defaultValue;
}
export async function resolveJimakuApiKeyService(
export async function resolveJimakuApiKey(
getResolvedConfig: () => { jimaku?: JimakuConfig },
): Promise<string | null> {
return resolveJimakuApiKeyFromConfig(getJimakuConfigService(getResolvedConfig));
return resolveJimakuApiKeyFromConfig(getJimakuConfig(getResolvedConfig));
}
export async function jimakuFetchJsonService<T>(
export async function jimakuFetchJson<T>(
endpoint: string,
query: Record<string, string | number | boolean | null | undefined> = {},
options: {
@@ -59,7 +59,7 @@ export async function jimakuFetchJsonService<T>(
defaultLanguagePreference: JimakuLanguagePreference;
},
): Promise<JimakuApiResponse<T>> {
const apiKey = await resolveJimakuApiKeyService(options.getResolvedConfig);
const apiKey = await resolveJimakuApiKey(options.getResolvedConfig);
if (!apiKey) {
return {
ok: false,
@@ -72,7 +72,7 @@ export async function jimakuFetchJsonService<T>(
}
return jimakuFetchJsonRequest<T>(endpoint, query, {
baseUrl: getJimakuBaseUrlService(
baseUrl: getJimakuBaseUrl(
options.getResolvedConfig,
options.defaultBaseUrl,
),

View File

@@ -134,7 +134,7 @@ function collectDictionaryFromPath(
return terms;
}
export async function createJlptVocabularyLookupService(
export async function createJlptVocabularyLookup(
options: JlptVocabLookupOptions,
): Promise<(term: string) => JlptLevel | null> {
const attemptedPaths: string[] = [];

View File

@@ -1,24 +1,24 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
copyCurrentSubtitleService,
handleMineSentenceDigitService,
handleMultiCopyDigitService,
mineSentenceCardService,
} from "./mining-service";
copyCurrentSubtitle,
handleMineSentenceDigit,
handleMultiCopyDigit,
mineSentenceCard,
} from "./mining";
test("copyCurrentSubtitleService reports tracker and subtitle guards", () => {
test("copyCurrentSubtitle reports tracker and subtitle guards", () => {
const osd: string[] = [];
const copied: string[] = [];
copyCurrentSubtitleService({
copyCurrentSubtitle({
subtitleTimingTracker: null,
writeClipboardText: (text) => copied.push(text),
showMpvOsd: (text) => osd.push(text),
});
assert.equal(osd.at(-1), "Subtitle tracker not available");
copyCurrentSubtitleService({
copyCurrentSubtitle({
subtitleTimingTracker: {
getRecentBlocks: () => [],
getCurrentSubtitle: () => null,
@@ -31,11 +31,11 @@ test("copyCurrentSubtitleService reports tracker and subtitle guards", () => {
assert.deepEqual(copied, []);
});
test("copyCurrentSubtitleService copies current subtitle text", () => {
test("copyCurrentSubtitle copies current subtitle text", () => {
const osd: string[] = [];
const copied: string[] = [];
copyCurrentSubtitleService({
copyCurrentSubtitle({
subtitleTimingTracker: {
getRecentBlocks: () => [],
getCurrentSubtitle: () => "hello world",
@@ -49,11 +49,11 @@ test("copyCurrentSubtitleService copies current subtitle text", () => {
assert.equal(osd.at(-1), "Copied subtitle");
});
test("mineSentenceCardService handles missing integration and disconnected mpv", async () => {
test("mineSentenceCard handles missing integration and disconnected mpv", async () => {
const osd: string[] = [];
assert.equal(
await mineSentenceCardService({
await mineSentenceCard({
ankiIntegration: null,
mpvClient: null,
showMpvOsd: (text) => osd.push(text),
@@ -62,8 +62,8 @@ test("mineSentenceCardService handles missing integration and disconnected mpv",
);
assert.equal(osd.at(-1), "AnkiConnect integration not enabled");
assert.equal(
await mineSentenceCardService({
assert.equal(
await mineSentenceCard({
ankiIntegration: {
updateLastAddedFromClipboard: async () => {},
triggerFieldGroupingForLastAddedCard: async () => {},
@@ -84,7 +84,7 @@ test("mineSentenceCardService handles missing integration and disconnected mpv",
assert.equal(osd.at(-1), "MPV not connected");
});
test("mineSentenceCardService creates sentence card from mpv subtitle state", async () => {
test("mineSentenceCard creates sentence card from mpv subtitle state", async () => {
const created: Array<{
sentence: string;
startTime: number;
@@ -92,7 +92,7 @@ test("mineSentenceCardService creates sentence card from mpv subtitle state", as
secondarySub?: string;
}> = [];
const createdCard = await mineSentenceCardService({
const createdCard = await mineSentenceCard({
ankiIntegration: {
updateLastAddedFromClipboard: async () => {},
triggerFieldGroupingForLastAddedCard: async () => {},
@@ -123,11 +123,11 @@ test("mineSentenceCardService creates sentence card from mpv subtitle state", as
]);
});
test("handleMultiCopyDigitService copies available history and reports truncation", () => {
test("handleMultiCopyDigit copies available history and reports truncation", () => {
const osd: string[] = [];
const copied: string[] = [];
handleMultiCopyDigitService(5, {
handleMultiCopyDigit(5, {
subtitleTimingTracker: {
getRecentBlocks: (count) => ["a", "b"].slice(0, count),
getCurrentSubtitle: () => null,
@@ -141,12 +141,12 @@ test("handleMultiCopyDigitService copies available history and reports truncatio
assert.equal(osd.at(-1), "Only 2 lines available, copied 2");
});
test("handleMineSentenceDigitService reports async create failures", async () => {
test("handleMineSentenceDigit reports async create failures", async () => {
const osd: string[] = [];
const logs: Array<{ message: string; err: unknown }> = [];
let cardsMined = 0;
handleMineSentenceDigitService(2, {
handleMineSentenceDigit(2, {
subtitleTimingTracker: {
getRecentBlocks: () => ["one", "two"],
getCurrentSubtitle: () => null,
@@ -184,7 +184,7 @@ test("handleMineSentenceDigitService increments successful card count", async ()
const osd: string[] = [];
let cardsMined = 0;
handleMineSentenceDigitService(2, {
handleMineSentenceDigit(2, {
subtitleTimingTracker: {
getRecentBlocks: () => ["one", "two"],
getCurrentSubtitle: () => null,

View File

@@ -24,7 +24,7 @@ interface MpvClientLike {
currentSecondarySubText?: string;
}
export function handleMultiCopyDigitService(
export function handleMultiCopyDigit(
count: number,
deps: {
subtitleTimingTracker: SubtitleTimingTrackerLike | null;
@@ -50,7 +50,7 @@ export function handleMultiCopyDigitService(
}
}
export function copyCurrentSubtitleService(deps: {
export function copyCurrentSubtitle(deps: {
subtitleTimingTracker: SubtitleTimingTrackerLike | null;
writeClipboardText: (text: string) => void;
showMpvOsd: (text: string) => void;
@@ -79,7 +79,7 @@ function requireAnkiIntegration(
return ankiIntegration;
}
export async function updateLastCardFromClipboardService(deps: {
export async function updateLastCardFromClipboard(deps: {
ankiIntegration: AnkiIntegrationLike | null;
readClipboardText: () => string;
showMpvOsd: (text: string) => void;
@@ -89,7 +89,7 @@ export async function updateLastCardFromClipboardService(deps: {
await anki.updateLastAddedFromClipboard(deps.readClipboardText());
}
export async function triggerFieldGroupingService(deps: {
export async function triggerFieldGrouping(deps: {
ankiIntegration: AnkiIntegrationLike | null;
showMpvOsd: (text: string) => void;
}): Promise<void> {
@@ -98,7 +98,7 @@ export async function triggerFieldGroupingService(deps: {
await anki.triggerFieldGroupingForLastAddedCard();
}
export async function markLastCardAsAudioCardService(deps: {
export async function markLastCardAsAudioCard(deps: {
ankiIntegration: AnkiIntegrationLike | null;
showMpvOsd: (text: string) => void;
}): Promise<void> {
@@ -107,7 +107,7 @@ export async function markLastCardAsAudioCardService(deps: {
await anki.markLastCardAsAudioCard();
}
export async function mineSentenceCardService(deps: {
export async function mineSentenceCard(deps: {
ankiIntegration: AnkiIntegrationLike | null;
mpvClient: MpvClientLike | null;
showMpvOsd: (text: string) => void;
@@ -133,7 +133,7 @@ export async function mineSentenceCardService(deps: {
);
}
export function handleMineSentenceDigitService(
export function handleMineSentenceDigit(
count: number,
deps: {
subtitleTimingTracker: SubtitleTimingTrackerLike | null;

View File

@@ -1,16 +1,16 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
playNextSubtitleRuntimeService,
replayCurrentSubtitleRuntimeService,
sendMpvCommandRuntimeService,
setMpvSubVisibilityRuntimeService,
showMpvOsdRuntimeService,
} from "./mpv-service";
playNextSubtitleRuntime,
replayCurrentSubtitleRuntime,
sendMpvCommandRuntime,
setMpvSubVisibilityRuntime,
showMpvOsdRuntime,
} from "./mpv";
test("showMpvOsdRuntimeService sends show-text when connected", () => {
test("showMpvOsdRuntime sends show-text when connected", () => {
const commands: (string | number)[][] = [];
showMpvOsdRuntimeService(
showMpvOsdRuntime(
{
connected: true,
send: ({ command }) => {
@@ -22,9 +22,9 @@ test("showMpvOsdRuntimeService sends show-text when connected", () => {
assert.deepEqual(commands, [["show-text", "hello", "3000"]]);
});
test("showMpvOsdRuntimeService logs fallback when disconnected", () => {
test("showMpvOsdRuntime logs fallback when disconnected", () => {
const logs: string[] = [];
showMpvOsdRuntimeService(
showMpvOsdRuntime(
{
connected: false,
send: () => {},
@@ -55,10 +55,10 @@ test("mpv runtime command wrappers call expected client methods", () => {
},
};
replayCurrentSubtitleRuntimeService(client);
playNextSubtitleRuntimeService(client);
sendMpvCommandRuntimeService(client, ["script-message", "x"]);
setMpvSubVisibilityRuntimeService(client, false);
replayCurrentSubtitleRuntime(client);
playNextSubtitleRuntime(client);
sendMpvCommandRuntime(client, ["script-message", "x"]);
setMpvSubVisibilityRuntime(client, false);
assert.deepEqual(calls, [
"replay",

View File

@@ -1,25 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import { MpvSubtitleRenderMetrics } from "../../types";
import {
applyMpvSubtitleRenderMetricsPatchService,
DEFAULT_MPV_SUBTITLE_RENDER_METRICS,
} from "./mpv-render-metrics-service";
const BASE: MpvSubtitleRenderMetrics = {
...DEFAULT_MPV_SUBTITLE_RENDER_METRICS,
};
test("applyMpvSubtitleRenderMetricsPatchService returns unchanged on empty patch", () => {
const { next, changed } = applyMpvSubtitleRenderMetricsPatchService(BASE, {});
assert.equal(changed, false);
assert.deepEqual(next, BASE);
});
test("applyMpvSubtitleRenderMetricsPatchService reports changed when patch modifies value", () => {
const { next, changed } = applyMpvSubtitleRenderMetricsPatchService(BASE, {
subPos: 95,
});
assert.equal(changed, true);
assert.equal(next.subPos, 95);
});

View File

@@ -0,0 +1,25 @@
import test from "node:test";
import assert from "node:assert/strict";
import { MpvSubtitleRenderMetrics } from "../../types";
import {
applyMpvSubtitleRenderMetricsPatch,
DEFAULT_MPV_SUBTITLE_RENDER_METRICS,
} from "./mpv-render-metrics";
const BASE: MpvSubtitleRenderMetrics = {
...DEFAULT_MPV_SUBTITLE_RENDER_METRICS,
};
test("applyMpvSubtitleRenderMetricsPatch returns unchanged on empty patch", () => {
const { next, changed } = applyMpvSubtitleRenderMetricsPatch(BASE, {});
assert.equal(changed, false);
assert.deepEqual(next, BASE);
});
test("applyMpvSubtitleRenderMetricsPatch reports changed when patch modifies value", () => {
const { next, changed } = applyMpvSubtitleRenderMetricsPatch(BASE, {
subPos: 95,
});
assert.equal(changed, true);
assert.equal(next.subPos, 95);
});

View File

@@ -25,10 +25,10 @@ export function sanitizeMpvSubtitleRenderMetrics(
patch: Partial<MpvSubtitleRenderMetrics> | null | undefined,
): MpvSubtitleRenderMetrics {
if (!patch) return current;
return updateMpvSubtitleRenderMetricsService(current, patch);
return updateMpvSubtitleRenderMetrics(current, patch);
}
export function updateMpvSubtitleRenderMetricsService(
export function updateMpvSubtitleRenderMetrics(
current: MpvSubtitleRenderMetrics,
patch: Partial<MpvSubtitleRenderMetrics>,
): MpvSubtitleRenderMetrics {
@@ -83,11 +83,11 @@ export function updateMpvSubtitleRenderMetricsService(
};
}
export function applyMpvSubtitleRenderMetricsPatchService(
export function applyMpvSubtitleRenderMetricsPatch(
current: MpvSubtitleRenderMetrics,
patch: Partial<MpvSubtitleRenderMetrics>,
): { next: MpvSubtitleRenderMetrics; changed: boolean } {
const next = updateMpvSubtitleRenderMetricsService(current, patch);
const next = updateMpvSubtitleRenderMetrics(current, patch);
const changed =
next.subPos !== current.subPos ||
next.subFontSize !== current.subFontSize ||

View File

@@ -1,6 +1,6 @@
import test from "node:test";
import assert from "node:assert/strict";
import { resolveCurrentAudioStreamIndex } from "./mpv-service";
import { resolveCurrentAudioStreamIndex } from "./mpv";
test("resolveCurrentAudioStreamIndex returns selected ff-index when no current track id", () => {
assert.equal(

View File

@@ -5,7 +5,7 @@ import {
MpvIpcClientDeps,
MpvIpcClientProtocolDeps,
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
} from "./mpv-service";
} from "./mpv";
import { MPV_REQUEST_ID_TRACK_LIST_AUDIO } from "./mpv-protocol";
function makeDeps(

View File

@@ -55,7 +55,7 @@ export interface MpvRuntimeClientLike {
setSubVisibility?: (visible: boolean) => void;
}
export function showMpvOsdRuntimeService(
export function showMpvOsdRuntime(
mpvClient: MpvRuntimeClientLike | null,
text: string,
fallbackLog: (text: string) => void = (line) => logger.info(line),
@@ -67,21 +67,21 @@ export function showMpvOsdRuntimeService(
fallbackLog(`OSD (MPV not connected): ${text}`);
}
export function replayCurrentSubtitleRuntimeService(
export function replayCurrentSubtitleRuntime(
mpvClient: MpvRuntimeClientLike | null,
): void {
if (!mpvClient?.replayCurrentSubtitle) return;
mpvClient.replayCurrentSubtitle();
}
export function playNextSubtitleRuntimeService(
export function playNextSubtitleRuntime(
mpvClient: MpvRuntimeClientLike | null,
): void {
if (!mpvClient?.playNextSubtitle) return;
mpvClient.playNextSubtitle();
}
export function sendMpvCommandRuntimeService(
export function sendMpvCommandRuntime(
mpvClient: MpvRuntimeClientLike | null,
command: (string | number)[],
): void {
@@ -89,7 +89,7 @@ export function sendMpvCommandRuntimeService(
mpvClient.send({ command });
}
export function setMpvSubVisibilityRuntimeService(
export function setMpvSubVisibilityRuntime(
mpvClient: MpvRuntimeClientLike | null,
visible: boolean,
): void {

View File

@@ -1,17 +1,17 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
createNumericShortcutRuntimeService,
createNumericShortcutSessionService,
} from "./numeric-shortcut-service";
createNumericShortcutRuntime,
createNumericShortcutSession,
} from "./numeric-shortcut";
test("createNumericShortcutRuntimeService creates sessions wired to globalShortcut", () => {
test("createNumericShortcutRuntime creates sessions wired to globalShortcut", () => {
const registered: string[] = [];
const unregistered: string[] = [];
const osd: string[] = [];
const handlers = new Map<string, () => void>();
const runtime = createNumericShortcutRuntimeService({
const runtime = createNumericShortcutRuntime({
globalShortcut: {
register: (accelerator, callback) => {
registered.push(accelerator);
@@ -54,7 +54,7 @@ test("numeric shortcut session handles digit selection and unregisters shortcuts
const handlers = new Map<string, () => void>();
const unregistered: string[] = [];
const osd: string[] = [];
const session = createNumericShortcutSessionService({
const session = createNumericShortcutSession({
registerShortcut: (accelerator, handler) => {
handlers.set(accelerator, handler);
return true;
@@ -96,7 +96,7 @@ test("numeric shortcut session handles digit selection and unregisters shortcuts
test("numeric shortcut session emits timeout message", () => {
const osd: string[] = [];
const session = createNumericShortcutSessionService({
const session = createNumericShortcutSession({
registerShortcut: () => true,
unregisterShortcut: () => {},
setTimer: (handler) => {
@@ -126,7 +126,7 @@ test("numeric shortcut session emits timeout message", () => {
test("numeric shortcut session handles escape cancellation", () => {
const handlers = new Map<string, () => void>();
const osd: string[] = [];
const session = createNumericShortcutSessionService({
const session = createNumericShortcutSession({
registerShortcut: (accelerator, handler) => {
handlers.set(accelerator, handler);
return true;

View File

@@ -13,11 +13,11 @@ export interface NumericShortcutRuntimeOptions {
clearTimer: (timer: ReturnType<typeof setTimeout>) => void;
}
export function createNumericShortcutRuntimeService(
export function createNumericShortcutRuntime(
options: NumericShortcutRuntimeOptions,
) {
const createSession = () =>
createNumericShortcutSessionService({
createNumericShortcutSession({
registerShortcut: (accelerator, handler) =>
options.globalShortcut.register(accelerator, handler),
unregisterShortcut: (accelerator) =>
@@ -52,7 +52,7 @@ export interface NumericShortcutSessionStartParams {
messages: NumericShortcutSessionMessages;
}
export function createNumericShortcutSessionService(
export function createNumericShortcutSession(
deps: NumericShortcutSessionDeps,
) {
let active = false;

View File

@@ -2,16 +2,16 @@ import test from "node:test";
import assert from "node:assert/strict";
import { KikuFieldGroupingChoice } from "../../types";
import {
createFieldGroupingCallbackRuntimeService,
sendToVisibleOverlayRuntimeService,
} from "./overlay-bridge-service";
createFieldGroupingCallbackRuntime,
sendToVisibleOverlayRuntime,
} from "./overlay-bridge";
test("sendToVisibleOverlayRuntimeService restores visibility flag when opening hidden overlay modal", () => {
test("sendToVisibleOverlayRuntime restores visibility flag when opening hidden overlay modal", () => {
const sent: unknown[][] = [];
const restoreSet = new Set<"runtime-options" | "subsync">();
let visibleOverlayVisible = false;
const ok = sendToVisibleOverlayRuntimeService({
const ok = sendToVisibleOverlayRuntime({
mainWindow: {
isDestroyed: () => false,
webContents: {
@@ -36,9 +36,9 @@ test("sendToVisibleOverlayRuntimeService restores visibility flag when opening h
assert.deepEqual(sent, [["runtime-options:open"]]);
});
test("createFieldGroupingCallbackRuntimeService cancels when overlay request cannot be sent", async () => {
test("createFieldGroupingCallbackRuntime cancels when overlay request cannot be sent", async () => {
let resolver: ((choice: KikuFieldGroupingChoice) => void) | null = null;
const callback = createFieldGroupingCallbackRuntimeService<
const callback = createFieldGroupingCallbackRuntime<
"runtime-options" | "subsync"
>({
getVisibleOverlayVisible: () => false,

View File

@@ -2,10 +2,10 @@ import {
KikuFieldGroupingChoice,
KikuFieldGroupingRequestData,
} from "../../types";
import { createFieldGroupingCallbackService } from "./field-grouping-service";
import { createFieldGroupingCallback } from "./field-grouping";
import { BrowserWindow } from "electron";
export function sendToVisibleOverlayRuntimeService<T extends string>(options: {
export function sendToVisibleOverlayRuntime<T extends string>(options: {
mainWindow: BrowserWindow | null;
visibleOverlayVisible: boolean;
setVisibleOverlayVisible: (visible: boolean) => void;
@@ -45,7 +45,7 @@ export function sendToVisibleOverlayRuntimeService<T extends string>(options: {
return true;
}
export function createFieldGroupingCallbackRuntimeService<T extends string>(
export function createFieldGroupingCallbackRuntime<T extends string>(
options: {
getVisibleOverlayVisible: () => boolean;
getInvisibleOverlayVisible: () => boolean;
@@ -62,7 +62,7 @@ export function createFieldGroupingCallbackRuntimeService<T extends string>(
) => boolean;
},
): (data: KikuFieldGroupingRequestData) => Promise<KikuFieldGroupingChoice> {
return createFieldGroupingCallbackService({
return createFieldGroupingCallback({
getVisibleOverlayVisible: options.getVisibleOverlayVisible,
getInvisibleOverlayVisible: options.getInvisibleOverlayVisible,
setVisibleOverlayVisible: options.setVisibleOverlayVisible,

View File

@@ -2,9 +2,9 @@ import test from "node:test";
import assert from "node:assert/strict";
import {
createOverlayContentMeasurementStoreService,
createOverlayContentMeasurementStore,
sanitizeOverlayContentMeasurement,
} from "./overlay-content-measurement-service";
} from "./overlay-content-measurement";
test("sanitizeOverlayContentMeasurement accepts valid payload with null rect", () => {
const measurement = sanitizeOverlayContentMeasurement(
@@ -40,7 +40,7 @@ test("sanitizeOverlayContentMeasurement rejects invalid ranges", () => {
});
test("overlay measurement store keeps latest payload per layer", () => {
const store = createOverlayContentMeasurementStoreService({
const store = createOverlayContentMeasurementStore({
now: () => 1000,
warn: () => {
// noop
@@ -69,7 +69,7 @@ test("overlay measurement store keeps latest payload per layer", () => {
test("overlay measurement store rate-limits invalid payload warnings", () => {
let now = 1_000;
const warnings: string[] = [];
const store = createOverlayContentMeasurementStoreService({
const store = createOverlayContentMeasurementStore({
now: () => now,
warn: (message) => {
warnings.push(message);

View File

@@ -105,7 +105,7 @@ function readFiniteInRange(
return value;
}
export function createOverlayContentMeasurementStoreService(options?: {
export function createOverlayContentMeasurementStore(options?: {
now?: () => number;
warn?: (message: string) => void;
}) {

View File

@@ -1,13 +1,13 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
broadcastRuntimeOptionsChangedRuntimeService,
createOverlayManagerService,
setOverlayDebugVisualizationEnabledRuntimeService,
} from "./overlay-manager-service";
broadcastRuntimeOptionsChangedRuntime,
createOverlayManager,
setOverlayDebugVisualizationEnabledRuntime,
} from "./overlay-manager";
test("overlay manager initializes with empty windows and hidden overlays", () => {
const manager = createOverlayManagerService();
const manager = createOverlayManager();
assert.equal(manager.getMainWindow(), null);
assert.equal(manager.getInvisibleWindow(), null);
assert.equal(manager.getVisibleOverlayVisible(), false);
@@ -16,7 +16,7 @@ test("overlay manager initializes with empty windows and hidden overlays", () =>
});
test("overlay manager stores window references and returns stable window order", () => {
const manager = createOverlayManagerService();
const manager = createOverlayManager();
const visibleWindow = { isDestroyed: () => false } as unknown as Electron.BrowserWindow;
const invisibleWindow = { isDestroyed: () => false } as unknown as Electron.BrowserWindow;
@@ -31,7 +31,7 @@ test("overlay manager stores window references and returns stable window order",
});
test("overlay manager excludes destroyed windows", () => {
const manager = createOverlayManagerService();
const manager = createOverlayManager();
manager.setMainWindow({ isDestroyed: () => true } as unknown as Electron.BrowserWindow);
manager.setInvisibleWindow({ isDestroyed: () => false } as unknown as Electron.BrowserWindow);
@@ -39,7 +39,7 @@ test("overlay manager excludes destroyed windows", () => {
});
test("overlay manager stores visibility state", () => {
const manager = createOverlayManagerService();
const manager = createOverlayManager();
manager.setVisibleOverlayVisible(true);
manager.setInvisibleOverlayVisible(true);
@@ -48,7 +48,7 @@ test("overlay manager stores visibility state", () => {
});
test("overlay manager broadcasts to non-destroyed windows", () => {
const manager = createOverlayManagerService();
const manager = createOverlayManager();
const calls: unknown[][] = [];
const aliveWindow = {
isDestroyed: () => false,
@@ -73,7 +73,7 @@ test("overlay manager broadcasts to non-destroyed windows", () => {
});
test("overlay manager applies bounds by layer", () => {
const manager = createOverlayManagerService();
const manager = createOverlayManager();
const visibleCalls: Electron.Rectangle[] = [];
const invisibleCalls: Electron.Rectangle[] = [];
const visibleWindow = {
@@ -110,14 +110,14 @@ test("overlay manager applies bounds by layer", () => {
test("runtime-option and debug broadcasts use expected channels", () => {
const broadcasts: unknown[][] = [];
broadcastRuntimeOptionsChangedRuntimeService(
broadcastRuntimeOptionsChangedRuntime(
() => [],
(channel, ...args) => {
broadcasts.push([channel, ...args]);
},
);
let state = false;
const changed = setOverlayDebugVisualizationEnabledRuntimeService(
const changed = setOverlayDebugVisualizationEnabledRuntime(
state,
true,
(enabled) => {

View File

@@ -1,10 +1,10 @@
import { BrowserWindow } from "electron";
import { RuntimeOptionState, WindowGeometry } from "../../types";
import { updateOverlayWindowBoundsService } from "./overlay-window-service";
import { updateOverlayWindowBounds } from "./overlay-window";
type OverlayLayer = "visible" | "invisible";
export interface OverlayManagerService {
export interface OverlayManager {
getMainWindow: () => BrowserWindow | null;
setMainWindow: (window: BrowserWindow | null) => void;
getInvisibleWindow: () => BrowserWindow | null;
@@ -19,7 +19,7 @@ export interface OverlayManagerService {
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void;
}
export function createOverlayManagerService(): OverlayManagerService {
export function createOverlayManager(): OverlayManager {
let mainWindow: BrowserWindow | null = null;
let invisibleWindow: BrowserWindow | null = null;
let visibleOverlayVisible = false;
@@ -37,7 +37,7 @@ export function createOverlayManagerService(): OverlayManagerService {
getOverlayWindow: (layer) =>
layer === "visible" ? mainWindow : invisibleWindow,
setOverlayWindowBounds: (layer, geometry) => {
updateOverlayWindowBoundsService(
updateOverlayWindowBounds(
geometry,
layer === "visible" ? mainWindow : invisibleWindow,
);
@@ -75,14 +75,14 @@ export function createOverlayManagerService(): OverlayManagerService {
};
}
export function broadcastRuntimeOptionsChangedRuntimeService(
export function broadcastRuntimeOptionsChangedRuntime(
getRuntimeOptionsState: () => RuntimeOptionState[],
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void,
): void {
broadcastToOverlayWindows("runtime-options:changed", getRuntimeOptionsState());
}
export function setOverlayDebugVisualizationEnabledRuntimeService(
export function setOverlayDebugVisualizationEnabledRuntime(
currentEnabled: boolean,
nextEnabled: boolean,
setState: (enabled: boolean) => void,

View File

@@ -8,7 +8,7 @@ import {
WindowGeometry,
} from "../../types";
export function initializeOverlayRuntimeService(options: {
export function initializeOverlayRuntime(options: {
backendOverride: string | null;
getInitialInvisibleOverlayVisibility: () => boolean;
createMainWindow: () => void;

View File

@@ -1,5 +1,5 @@
import { ConfiguredShortcuts } from "../utils/shortcut-config";
import { OverlayShortcutHandlers } from "./overlay-shortcut-service";
import { OverlayShortcutHandlers } from "./overlay-shortcut";
import { createLogger } from "../../logger";
const logger = createLogger("main:overlay-shortcut-handler");

View File

@@ -1,6 +1,6 @@
import { globalShortcut } from "electron";
import { ConfiguredShortcuts } from "../utils/shortcut-config";
import { isGlobalShortcutRegisteredSafe } from "./shortcut-fallback-service";
import { isGlobalShortcutRegisteredSafe } from "./shortcut-fallback";
import { createLogger } from "../../logger";
const logger = createLogger("main:overlay-shortcut-service");
@@ -26,7 +26,7 @@ export interface OverlayShortcutLifecycleDeps {
cancelPendingMineSentenceMultiple: () => void;
}
export function registerOverlayShortcutsService(
export function registerOverlayShortcuts(
shortcuts: ConfiguredShortcuts,
handlers: OverlayShortcutHandlers,
): boolean {
@@ -140,7 +140,7 @@ export function registerOverlayShortcutsService(
return registeredAny;
}
export function unregisterOverlayShortcutsService(
export function unregisterOverlayShortcuts(
shortcuts: ConfiguredShortcuts,
): void {
if (shortcuts.copySubtitle) {
@@ -178,45 +178,45 @@ export function unregisterOverlayShortcutsService(
}
}
export function registerOverlayShortcutsRuntimeService(
export function registerOverlayShortcutsRuntime(
deps: OverlayShortcutLifecycleDeps,
): boolean {
return registerOverlayShortcutsService(
return registerOverlayShortcuts(
deps.getConfiguredShortcuts(),
deps.getOverlayHandlers(),
);
}
export function unregisterOverlayShortcutsRuntimeService(
export function unregisterOverlayShortcutsRuntime(
shortcutsRegistered: boolean,
deps: OverlayShortcutLifecycleDeps,
): boolean {
if (!shortcutsRegistered) return shortcutsRegistered;
deps.cancelPendingMultiCopy();
deps.cancelPendingMineSentenceMultiple();
unregisterOverlayShortcutsService(deps.getConfiguredShortcuts());
unregisterOverlayShortcuts(deps.getConfiguredShortcuts());
return false;
}
export function syncOverlayShortcutsRuntimeService(
export function syncOverlayShortcutsRuntime(
shouldBeActive: boolean,
shortcutsRegistered: boolean,
deps: OverlayShortcutLifecycleDeps,
): boolean {
if (shouldBeActive) {
return registerOverlayShortcutsRuntimeService(deps);
return registerOverlayShortcutsRuntime(deps);
}
return unregisterOverlayShortcutsRuntimeService(shortcutsRegistered, deps);
return unregisterOverlayShortcutsRuntime(shortcutsRegistered, deps);
}
export function refreshOverlayShortcutsRuntimeService(
export function refreshOverlayShortcutsRuntime(
shouldBeActive: boolean,
shortcutsRegistered: boolean,
deps: OverlayShortcutLifecycleDeps,
): boolean {
const cleared = unregisterOverlayShortcutsRuntimeService(
const cleared = unregisterOverlayShortcutsRuntime(
shortcutsRegistered,
deps,
);
return syncOverlayShortcutsRuntimeService(shouldBeActive, cleared, deps);
return syncOverlayShortcutsRuntime(shouldBeActive, cleared, deps);
}

View File

@@ -2,7 +2,7 @@ import { BrowserWindow, screen } from "electron";
import { BaseWindowTracker } from "../../window-trackers";
import { WindowGeometry } from "../../types";
export function updateVisibleOverlayVisibilityService(args: {
export function updateVisibleOverlayVisibility(args: {
visibleOverlayVisible: boolean;
mainWindow: BrowserWindow | null;
windowTracker: BaseWindowTracker | null;
@@ -66,7 +66,7 @@ export function updateVisibleOverlayVisibilityService(args: {
args.syncOverlayShortcuts();
}
export function updateInvisibleOverlayVisibilityService(args: {
export function updateInvisibleOverlayVisibility(args: {
invisibleWindow: BrowserWindow | null;
visibleOverlayVisible: boolean;
invisibleOverlayVisible: boolean;
@@ -131,7 +131,7 @@ export function updateInvisibleOverlayVisibilityService(args: {
args.syncOverlayShortcuts();
}
export function syncInvisibleOverlayMousePassthroughService(options: {
export function syncInvisibleOverlayMousePassthrough(options: {
hasInvisibleWindow: () => boolean;
setIgnoreMouseEvents: (ignore: boolean, extra?: { forward: boolean }) => void;
visibleOverlayVisible: boolean;
@@ -145,7 +145,7 @@ export function syncInvisibleOverlayMousePassthroughService(options: {
}
}
export function setVisibleOverlayVisibleService(options: {
export function setVisibleOverlayVisible(options: {
visible: boolean;
setVisibleOverlayVisibleState: (visible: boolean) => void;
updateVisibleOverlayVisibility: () => void;
@@ -167,7 +167,7 @@ export function setVisibleOverlayVisibleService(options: {
}
}
export function setInvisibleOverlayVisibleService(options: {
export function setInvisibleOverlayVisible(options: {
visible: boolean;
setInvisibleOverlayVisibleState: (visible: boolean) => void;
updateInvisibleOverlayVisibility: () => void;

View File

@@ -7,7 +7,7 @@ const logger = createLogger("main:overlay-window");
export type OverlayWindowKind = "visible" | "invisible";
export function updateOverlayWindowBoundsService(
export function updateOverlayWindowBounds(
geometry: WindowGeometry,
window: BrowserWindow | null,
): void {
@@ -20,7 +20,7 @@ export function updateOverlayWindowBoundsService(
});
}
export function ensureOverlayWindowLevelService(window: BrowserWindow): void {
export function ensureOverlayWindowLevel(window: BrowserWindow): void {
if (process.platform === "darwin") {
window.setAlwaysOnTop(true, "screen-saver", 1);
window.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
@@ -30,7 +30,7 @@ export function ensureOverlayWindowLevelService(window: BrowserWindow): void {
window.setAlwaysOnTop(true);
}
export function enforceOverlayLayerOrderService(options: {
export function enforceOverlayLayerOrder(options: {
visibleOverlayVisible: boolean;
invisibleOverlayVisible: boolean;
mainWindow: BrowserWindow | null;
@@ -45,7 +45,7 @@ export function enforceOverlayLayerOrderService(options: {
options.mainWindow.moveTop();
}
export function createOverlayWindowService(
export function createOverlayWindow(
kind: OverlayWindowKind,
options: {
isDev: boolean;

View File

@@ -1,98 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
getInitialInvisibleOverlayVisibilityService,
isAutoUpdateEnabledRuntimeService,
shouldAutoInitializeOverlayRuntimeFromConfigService,
shouldBindVisibleOverlayToMpvSubVisibilityService,
} from "./startup-service";
const BASE_CONFIG = {
auto_start_overlay: false,
bind_visible_overlay_to_mpv_sub_visibility: true,
invisibleOverlay: {
startupVisibility: "platform-default" as const,
},
ankiConnect: {
behavior: {
autoUpdateNewCards: true,
},
},
};
test("getInitialInvisibleOverlayVisibilityService handles visibility + platform", () => {
assert.equal(
getInitialInvisibleOverlayVisibilityService(
{ ...BASE_CONFIG, invisibleOverlay: { startupVisibility: "visible" } },
"linux",
),
true,
);
assert.equal(
getInitialInvisibleOverlayVisibilityService(
{ ...BASE_CONFIG, invisibleOverlay: { startupVisibility: "hidden" } },
"darwin",
),
false,
);
assert.equal(
getInitialInvisibleOverlayVisibilityService(BASE_CONFIG, "linux"),
false,
);
assert.equal(
getInitialInvisibleOverlayVisibilityService(BASE_CONFIG, "darwin"),
true,
);
});
test("shouldAutoInitializeOverlayRuntimeFromConfigService respects auto start and visible startup", () => {
assert.equal(
shouldAutoInitializeOverlayRuntimeFromConfigService(BASE_CONFIG),
false,
);
assert.equal(
shouldAutoInitializeOverlayRuntimeFromConfigService({
...BASE_CONFIG,
auto_start_overlay: true,
}),
true,
);
assert.equal(
shouldAutoInitializeOverlayRuntimeFromConfigService({
...BASE_CONFIG,
invisibleOverlay: { startupVisibility: "visible" },
}),
true,
);
});
test("shouldBindVisibleOverlayToMpvSubVisibilityService returns config value", () => {
assert.equal(shouldBindVisibleOverlayToMpvSubVisibilityService(BASE_CONFIG), true);
assert.equal(
shouldBindVisibleOverlayToMpvSubVisibilityService({
...BASE_CONFIG,
bind_visible_overlay_to_mpv_sub_visibility: false,
}),
false,
);
});
test("isAutoUpdateEnabledRuntimeService prefers runtime option and falls back to config", () => {
assert.equal(
isAutoUpdateEnabledRuntimeService(BASE_CONFIG, {
getOptionValue: () => false,
}),
false,
);
assert.equal(
isAutoUpdateEnabledRuntimeService(
{
...BASE_CONFIG,
ankiConnect: { behavior: { autoUpdateNewCards: false } },
},
null,
),
false,
);
assert.equal(isAutoUpdateEnabledRuntimeService(BASE_CONFIG, null), true);
});

View File

@@ -0,0 +1,98 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
getInitialInvisibleOverlayVisibility,
isAutoUpdateEnabledRuntime,
shouldAutoInitializeOverlayRuntimeFromConfig,
shouldBindVisibleOverlayToMpvSubVisibility,
} from "./startup";
const BASE_CONFIG = {
auto_start_overlay: false,
bind_visible_overlay_to_mpv_sub_visibility: true,
invisibleOverlay: {
startupVisibility: "platform-default" as const,
},
ankiConnect: {
behavior: {
autoUpdateNewCards: true,
},
},
};
test("getInitialInvisibleOverlayVisibility handles visibility + platform", () => {
assert.equal(
getInitialInvisibleOverlayVisibility(
{ ...BASE_CONFIG, invisibleOverlay: { startupVisibility: "visible" } },
"linux",
),
true,
);
assert.equal(
getInitialInvisibleOverlayVisibility(
{ ...BASE_CONFIG, invisibleOverlay: { startupVisibility: "hidden" } },
"darwin",
),
false,
);
assert.equal(
getInitialInvisibleOverlayVisibility(BASE_CONFIG, "linux"),
false,
);
assert.equal(
getInitialInvisibleOverlayVisibility(BASE_CONFIG, "darwin"),
true,
);
});
test("shouldAutoInitializeOverlayRuntimeFromConfig respects auto start and visible startup", () => {
assert.equal(
shouldAutoInitializeOverlayRuntimeFromConfig(BASE_CONFIG),
false,
);
assert.equal(
shouldAutoInitializeOverlayRuntimeFromConfig({
...BASE_CONFIG,
auto_start_overlay: true,
}),
true,
);
assert.equal(
shouldAutoInitializeOverlayRuntimeFromConfig({
...BASE_CONFIG,
invisibleOverlay: { startupVisibility: "visible" },
}),
true,
);
});
test("shouldBindVisibleOverlayToMpvSubVisibility returns config value", () => {
assert.equal(shouldBindVisibleOverlayToMpvSubVisibility(BASE_CONFIG), true);
assert.equal(
shouldBindVisibleOverlayToMpvSubVisibility({
...BASE_CONFIG,
bind_visible_overlay_to_mpv_sub_visibility: false,
}),
false,
);
});
test("isAutoUpdateEnabledRuntime prefers runtime option and falls back to config", () => {
assert.equal(
isAutoUpdateEnabledRuntime(BASE_CONFIG, {
getOptionValue: () => false,
}),
false,
);
assert.equal(
isAutoUpdateEnabledRuntime(
{
...BASE_CONFIG,
ankiConnect: { behavior: { autoUpdateNewCards: false } },
},
null,
),
false,
);
assert.equal(isAutoUpdateEnabledRuntime(BASE_CONFIG, null), true);
});

View File

@@ -1,14 +1,14 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
applyRuntimeOptionResultRuntimeService,
cycleRuntimeOptionFromIpcRuntimeService,
setRuntimeOptionFromIpcRuntimeService,
} from "./runtime-options-ipc-service";
applyRuntimeOptionResultRuntime,
cycleRuntimeOptionFromIpcRuntime,
setRuntimeOptionFromIpcRuntime,
} from "./runtime-options-ipc";
test("applyRuntimeOptionResultRuntimeService emits success OSD message", () => {
test("applyRuntimeOptionResultRuntime emits success OSD message", () => {
const osd: string[] = [];
const result = applyRuntimeOptionResultRuntimeService(
const result = applyRuntimeOptionResultRuntime(
{ ok: true, osdMessage: "Updated" },
(text) => {
osd.push(text);
@@ -19,9 +19,9 @@ test("applyRuntimeOptionResultRuntimeService emits success OSD message", () => {
assert.deepEqual(osd, ["Updated"]);
});
test("setRuntimeOptionFromIpcRuntimeService returns unavailable when manager missing", () => {
test("setRuntimeOptionFromIpcRuntime returns unavailable when manager missing", () => {
const osd: string[] = [];
const result = setRuntimeOptionFromIpcRuntimeService(
const result = setRuntimeOptionFromIpcRuntime(
null,
"anki.autoUpdateNewCards",
true,
@@ -34,9 +34,9 @@ test("setRuntimeOptionFromIpcRuntimeService returns unavailable when manager mis
assert.deepEqual(osd, []);
});
test("cycleRuntimeOptionFromIpcRuntimeService reports errors once", () => {
test("cycleRuntimeOptionFromIpcRuntime reports errors once", () => {
const osd: string[] = [];
const result = cycleRuntimeOptionFromIpcRuntimeService(
const result = cycleRuntimeOptionFromIpcRuntime(
{
setOptionValue: () => ({ ok: true }),
cycleOption: () => ({ ok: false, error: "bad option" }),

View File

@@ -15,7 +15,7 @@ export interface RuntimeOptionsManagerLike {
) => RuntimeOptionApplyResult;
}
export function applyRuntimeOptionResultRuntimeService(
export function applyRuntimeOptionResultRuntime(
result: RuntimeOptionApplyResult,
showMpvOsd: (text: string) => void,
): RuntimeOptionApplyResult {
@@ -25,7 +25,7 @@ export function applyRuntimeOptionResultRuntimeService(
return result;
}
export function setRuntimeOptionFromIpcRuntimeService(
export function setRuntimeOptionFromIpcRuntime(
manager: RuntimeOptionsManagerLike | null,
id: RuntimeOptionId,
value: RuntimeOptionValue,
@@ -34,7 +34,7 @@ export function setRuntimeOptionFromIpcRuntimeService(
if (!manager) {
return { ok: false, error: "Runtime options manager unavailable" };
}
const result = applyRuntimeOptionResultRuntimeService(
const result = applyRuntimeOptionResultRuntime(
manager.setOptionValue(id, value),
showMpvOsd,
);
@@ -44,7 +44,7 @@ export function setRuntimeOptionFromIpcRuntimeService(
return result;
}
export function cycleRuntimeOptionFromIpcRuntimeService(
export function cycleRuntimeOptionFromIpcRuntime(
manager: RuntimeOptionsManagerLike | null,
id: RuntimeOptionId,
direction: 1 | -1,
@@ -53,7 +53,7 @@ export function cycleRuntimeOptionFromIpcRuntimeService(
if (!manager) {
return { ok: false, error: "Runtime options manager unavailable" };
}
const result = applyRuntimeOptionResultRuntimeService(
const result = applyRuntimeOptionResultRuntime(
manager.cycleOption(id, direction),
showMpvOsd,
);

View File

@@ -1,15 +1,15 @@
import test from "node:test";
import assert from "node:assert/strict";
import { SecondarySubMode } from "../../types";
import { cycleSecondarySubModeService } from "./subtitle-position-service";
import { cycleSecondarySubMode } from "./subtitle-position";
test("cycleSecondarySubModeService cycles and emits broadcast + OSD", () => {
test("cycleSecondarySubMode cycles and emits broadcast + OSD", () => {
let mode: SecondarySubMode = "hover";
let lastToggleAt = 0;
const broadcasts: SecondarySubMode[] = [];
const osd: string[] = [];
cycleSecondarySubModeService({
cycleSecondarySubMode({
getSecondarySubMode: () => mode,
setSecondarySubMode: (next) => {
mode = next;
@@ -33,13 +33,13 @@ test("cycleSecondarySubModeService cycles and emits broadcast + OSD", () => {
assert.equal(lastToggleAt, 1000);
});
test("cycleSecondarySubModeService obeys debounce window", () => {
test("cycleSecondarySubMode obeys debounce window", () => {
let mode: SecondarySubMode = "visible";
let lastToggleAt = 950;
let broadcasted = false;
let osdShown = false;
cycleSecondarySubModeService({
cycleSecondarySubMode({
getSecondarySubMode: () => mode,
setSecondarySubMode: (next) => {
mode = next;

View File

@@ -19,7 +19,7 @@ export interface RegisterGlobalShortcutsServiceOptions {
getMainWindow: () => BrowserWindow | null;
}
export function registerGlobalShortcutsService(
export function registerGlobalShortcuts(
options: RegisterGlobalShortcutsServiceOptions,
): void {
const visibleShortcut = options.shortcuts.toggleVisibleOverlayGlobal;

View File

@@ -1,8 +1,8 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
runStartupBootstrapRuntimeService,
} from "./startup-service";
runStartupBootstrapRuntime,
} from "./startup";
import { CliArgs } from "../../cli/args";
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
@@ -40,7 +40,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
};
}
test("runStartupBootstrapRuntimeService configures startup state and starts lifecycle", () => {
test("runStartupBootstrapRuntime configures startup state and starts lifecycle", () => {
const calls: string[] = [];
const args = makeArgs({
logLevel: "debug",
@@ -51,7 +51,7 @@ test("runStartupBootstrapRuntimeService configures startup state and starts life
texthooker: true,
});
const result = runStartupBootstrapRuntimeService({
const result = runStartupBootstrapRuntime({
argv: ["node", "main.ts", "--log-level", "debug"],
parseArgs: () => args,
setLogLevel: (level, source) => calls.push(`setLog:${level}:${source}`),
@@ -77,13 +77,13 @@ test("runStartupBootstrapRuntimeService configures startup state and starts life
]);
});
test("runStartupBootstrapRuntimeService keeps log-level precedence for repeated calls", () => {
test("runStartupBootstrapRuntime keeps log-level precedence for repeated calls", () => {
const calls: string[] = [];
const args = makeArgs({
logLevel: "warn",
});
runStartupBootstrapRuntimeService({
runStartupBootstrapRuntime({
argv: ["node", "main.ts", "--log-level", "warn"],
parseArgs: () => args,
setLogLevel: (level, source) => calls.push(`setLog:${level}:${source}`),
@@ -102,13 +102,13 @@ test("runStartupBootstrapRuntimeService keeps log-level precedence for repeated
]);
});
test("runStartupBootstrapRuntimeService keeps --debug separate from log verbosity", () => {
test("runStartupBootstrapRuntime keeps --debug separate from log verbosity", () => {
const calls: string[] = [];
const args = makeArgs({
debug: true,
});
runStartupBootstrapRuntimeService({
runStartupBootstrapRuntime({
argv: ["node", "main.ts", "--debug"],
parseArgs: () => args,
setLogLevel: (level, source) => calls.push(`setLog:${level}:${source}`),
@@ -123,11 +123,11 @@ test("runStartupBootstrapRuntimeService keeps --debug separate from log verbosit
assert.deepEqual(calls, ["forceX11", "enforceWayland", "startLifecycle"]);
});
test("runStartupBootstrapRuntimeService skips lifecycle when generate-config flow handled", () => {
test("runStartupBootstrapRuntime skips lifecycle when generate-config flow handled", () => {
const calls: string[] = [];
const args = makeArgs({ generateConfig: true, logLevel: "warn" });
const result = runStartupBootstrapRuntimeService({
const result = runStartupBootstrapRuntime({
argv: ["node", "main.ts", "--generate-config"],
parseArgs: () => args,
setLogLevel: (level, source) => calls.push(`setLog:${level}:${source}`),

View File

@@ -40,7 +40,7 @@ export interface StartupBootstrapRuntimeDeps {
startAppLifecycle: (args: CliArgs) => void;
}
export function runStartupBootstrapRuntimeService(
export function runStartupBootstrapRuntime(
deps: StartupBootstrapRuntimeDeps,
): StartupBootstrapRuntimeState {
const initialArgs = deps.parseArgs(deps.argv);
@@ -107,7 +107,7 @@ export interface AppReadyRuntimeDeps {
handleInitialArgs: () => void;
}
export function getInitialInvisibleOverlayVisibilityService(
export function getInitialInvisibleOverlayVisibility(
config: RuntimeConfigLike,
platform: NodeJS.Platform,
): boolean {
@@ -118,7 +118,7 @@ export function getInitialInvisibleOverlayVisibilityService(
return true;
}
export function shouldAutoInitializeOverlayRuntimeFromConfigService(
export function shouldAutoInitializeOverlayRuntimeFromConfig(
config: RuntimeConfigLike,
): boolean {
if (config.auto_start_overlay === true) return true;
@@ -126,13 +126,13 @@ export function shouldAutoInitializeOverlayRuntimeFromConfigService(
return false;
}
export function shouldBindVisibleOverlayToMpvSubVisibilityService(
export function shouldBindVisibleOverlayToMpvSubVisibility(
config: RuntimeConfigLike,
): boolean {
return config.bind_visible_overlay_to_mpv_sub_visibility;
}
export function isAutoUpdateEnabledRuntimeService(
export function isAutoUpdateEnabledRuntime(
config: ResolvedConfig | RuntimeConfigLike,
runtimeOptionsManager: RuntimeAutoUpdateOptionManagerLike | null,
): boolean {
@@ -141,7 +141,7 @@ export function isAutoUpdateEnabledRuntimeService(
return (config as ResolvedConfig).ankiConnect?.behavior?.autoUpdateNewCards !== false;
}
export async function runAppReadyRuntimeService(
export async function runAppReadyRuntime(
deps: AppReadyRuntimeDeps,
): Promise<void> {
deps.loadSubtitlePosition();

View File

@@ -4,12 +4,12 @@ import {
SubsyncResult,
} from "../../types";
import { SubsyncResolvedConfig } from "../../subsync/utils";
import { runSubsyncManualFromIpcService } from "./ipc-command-service";
import { runSubsyncManualFromIpc } from "./ipc-command";
import {
TriggerSubsyncFromConfigDeps,
runSubsyncManualService,
triggerSubsyncFromConfigService,
} from "./subsync-service";
runSubsyncManual,
triggerSubsyncFromConfig,
} from "./subsync";
const AUTOSUBSYNC_SPINNER_FRAMES = ["|", "/", "-", "\\"];
@@ -62,24 +62,24 @@ function buildTriggerSubsyncDeps(
};
}
export async function triggerSubsyncFromConfigRuntimeService(
export async function triggerSubsyncFromConfigRuntime(
deps: SubsyncRuntimeDeps,
): Promise<void> {
await triggerSubsyncFromConfigService(buildTriggerSubsyncDeps(deps));
await triggerSubsyncFromConfig(buildTriggerSubsyncDeps(deps));
}
export async function runSubsyncManualFromIpcRuntimeService(
export async function runSubsyncManualFromIpcRuntime(
request: SubsyncManualRunRequest,
deps: SubsyncRuntimeDeps,
): Promise<SubsyncResult> {
const triggerDeps = buildTriggerSubsyncDeps(deps);
return runSubsyncManualFromIpcService(request, {
return runSubsyncManualFromIpc(request, {
isSubsyncInProgress: triggerDeps.isSubsyncInProgress,
setSubsyncInProgress: triggerDeps.setSubsyncInProgress,
showMpvOsd: triggerDeps.showMpvOsd,
runWithSpinner: (task) =>
triggerDeps.runWithSubsyncSpinner(() => task()),
runSubsyncManual: (subsyncRequest) =>
runSubsyncManualService(subsyncRequest, triggerDeps),
runSubsyncManual(subsyncRequest, triggerDeps),
});
}

View File

@@ -5,9 +5,9 @@ import * as os from "os";
import * as path from "path";
import {
TriggerSubsyncFromConfigDeps,
runSubsyncManualService,
triggerSubsyncFromConfigService,
} from "./subsync-service";
runSubsyncManual,
triggerSubsyncFromConfig,
} from "./subsync";
function makeDeps(
overrides: Partial<TriggerSubsyncFromConfigDeps> = {},
@@ -55,9 +55,9 @@ function makeDeps(
};
}
test("triggerSubsyncFromConfigService returns early when already in progress", async () => {
test("triggerSubsyncFromConfig returns early when already in progress", async () => {
const osd: string[] = [];
await triggerSubsyncFromConfigService(
await triggerSubsyncFromConfig(
makeDeps({
isSubsyncInProgress: () => true,
showMpvOsd: (text) => {
@@ -68,12 +68,12 @@ test("triggerSubsyncFromConfigService returns early when already in progress", a
assert.deepEqual(osd, ["Subsync already running"]);
});
test("triggerSubsyncFromConfigService opens manual picker in manual mode", async () => {
test("triggerSubsyncFromConfig opens manual picker in manual mode", async () => {
const osd: string[] = [];
let payloadTrackCount = 0;
let inProgressState: boolean | null = null;
await triggerSubsyncFromConfigService(
await triggerSubsyncFromConfig(
makeDeps({
openManualPicker: (payload) => {
payloadTrackCount = payload.sourceTracks.length;
@@ -92,9 +92,9 @@ test("triggerSubsyncFromConfigService opens manual picker in manual mode", async
assert.equal(inProgressState, false);
});
test("triggerSubsyncFromConfigService reports failures to OSD", async () => {
test("triggerSubsyncFromConfig reports failures to OSD", async () => {
const osd: string[] = [];
await triggerSubsyncFromConfigService(
await triggerSubsyncFromConfig(
makeDeps({
getMpvClient: () => null,
showMpvOsd: (text) => {
@@ -106,8 +106,8 @@ test("triggerSubsyncFromConfigService reports failures to OSD", async () => {
assert.ok(osd.some((line) => line.startsWith("Subsync failed: MPV not connected")));
});
test("runSubsyncManualService requires a source track for alass", async () => {
const result = await runSubsyncManualService(
test("runSubsyncManual requires a source track for alass", async () => {
const result = await runSubsyncManual(
{ engine: "alass", sourceTrackId: null },
makeDeps(),
);
@@ -118,11 +118,11 @@ test("runSubsyncManualService requires a source track for alass", async () => {
});
});
test("triggerSubsyncFromConfigService reports path validation failures", async () => {
test("triggerSubsyncFromConfig reports path validation failures", async () => {
const osd: string[] = [];
const inProgress: boolean[] = [];
await triggerSubsyncFromConfigService(
await triggerSubsyncFromConfig(
makeDeps({
getResolvedConfig: () => ({
defaultMode: "auto",
@@ -152,7 +152,7 @@ function writeExecutableScript(filePath: string, content: string): void {
fs.chmodSync(filePath, 0o755);
}
test("runSubsyncManualService constructs ffsubsync command and returns success", async () => {
test("runSubsyncManual constructs ffsubsync command and returns success", async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "subsync-ffsubsync-"));
const ffsubsyncLogPath = path.join(tmpDir, "ffsubsync-args.log");
const ffsubsyncPath = path.join(tmpDir, "ffsubsync.sh");
@@ -210,7 +210,7 @@ test("runSubsyncManualService constructs ffsubsync command and returns success",
}),
});
const result = await runSubsyncManualService(
const result = await runSubsyncManual(
{ engine: "ffsubsync", sourceTrackId: null },
deps,
);
@@ -227,7 +227,7 @@ test("runSubsyncManualService constructs ffsubsync command and returns success",
assert.deepEqual(sentCommands[1], ["set_property", "sub-delay", 0]);
});
test("runSubsyncManualService constructs alass command and returns failure on non-zero exit", async () => {
test("runSubsyncManual constructs alass command and returns failure on non-zero exit", async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "subsync-alass-"));
const alassLogPath = path.join(tmpDir, "alass-args.log");
const alassPath = path.join(tmpDir, "alass.sh");
@@ -285,7 +285,7 @@ test("runSubsyncManualService constructs alass command and returns failure on no
}),
});
const result = await runSubsyncManualService(
const result = await runSubsyncManual(
{ engine: "alass", sourceTrackId: 2 },
deps,
);
@@ -298,7 +298,7 @@ test("runSubsyncManualService constructs alass command and returns failure on no
assert.equal(alassArgs[1], primaryPath);
});
test("runSubsyncManualService resolves string sid values from mpv stream properties", async () => {
test("runSubsyncManual resolves string sid values from mpv stream properties", async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "subsync-stream-sid-"));
const ffsubsyncPath = path.join(tmpDir, "ffsubsync.sh");
const ffsubsyncLogPath = path.join(tmpDir, "ffsubsync-args.log");
@@ -347,7 +347,7 @@ test("runSubsyncManualService resolves string sid values from mpv stream propert
}),
});
const result = await runSubsyncManualService(
const result = await runSubsyncManual(
{ engine: "ffsubsync", sourceTrackId: null },
deps,
);

View File

@@ -399,7 +399,7 @@ async function runSubsyncAutoInternal(
);
}
export async function runSubsyncManualService(
export async function runSubsyncManual(
request: SubsyncManualRunRequest,
deps: SubsyncCoreDeps,
): Promise<SubsyncResult> {
@@ -452,7 +452,7 @@ export async function runSubsyncManualService(
}
}
export async function openSubsyncManualPickerService(
export async function openSubsyncManualPicker(
deps: TriggerSubsyncFromConfigDeps,
): Promise<void> {
const client = getMpvClientForSubsync(deps);
@@ -468,7 +468,7 @@ export async function openSubsyncManualPickerService(
deps.openManualPicker(payload);
}
export async function triggerSubsyncFromConfigService(
export async function triggerSubsyncFromConfig(
deps: TriggerSubsyncFromConfigDeps,
): Promise<void> {
if (deps.isSubsyncInProgress()) {
@@ -479,7 +479,7 @@ export async function triggerSubsyncFromConfigService(
const resolved = deps.getResolvedConfig();
try {
if (resolved.defaultMode === "manual") {
await openSubsyncManualPickerService(deps);
await openSubsyncManualPicker(deps);
deps.showMpvOsd("Subsync: choose engine and source");
return;
}

View File

@@ -19,7 +19,7 @@ export interface CycleSecondarySubModeDeps {
const SECONDARY_SUB_CYCLE: SecondarySubMode[] = ["hidden", "visible", "hover"];
const SECONDARY_SUB_TOGGLE_DEBOUNCE_MS = 120;
export function cycleSecondarySubModeService(
export function cycleSecondarySubMode(
deps: CycleSecondarySubModeDeps,
): void {
const now = deps.now ? deps.now() : Date.now();
@@ -89,7 +89,7 @@ function persistSubtitlePosition(
fs.writeFileSync(positionPath, JSON.stringify(position, null, 2));
}
export function loadSubtitlePositionService(options: {
export function loadSubtitlePosition(options: {
currentMediaPath: string | null;
fallbackPosition: SubtitlePosition;
} & { subtitlePositionsDir: string }): SubtitlePosition | null {
@@ -135,7 +135,7 @@ export function loadSubtitlePositionService(options: {
}
}
export function saveSubtitlePositionService(options: {
export function saveSubtitlePosition(options: {
position: SubtitlePosition;
currentMediaPath: string | null;
subtitlePositionsDir: string;
@@ -160,7 +160,7 @@ export function saveSubtitlePositionService(options: {
}
}
export function updateCurrentMediaPathService(options: {
export function updateCurrentMediaPath(options: {
mediaPath: unknown;
currentMediaPath: string | null;
pendingSubtitlePosition: SubtitlePosition | null;

View File

@@ -16,7 +16,7 @@ export function hasMpvWebsocketPlugin(): boolean {
return fs.existsSync(mpvWebsocketPath);
}
export class SubtitleWebSocketService {
export class SubtitleWebSocket {
private server: WebSocket.Server | null = null;
public isRunning(): boolean {

View File

@@ -5,7 +5,7 @@ import { createLogger } from "../../logger";
const logger = createLogger("main:texthooker");
export class TexthookerService {
export class Texthooker {
private server: http.Server | null = null;
public isRunning(): boolean {

View File

@@ -2,11 +2,11 @@ import test from "node:test";
import assert from "node:assert/strict";
import { PartOfSpeech } from "../../types";
import {
createTokenizerDepsRuntimeService,
createTokenizerDepsRuntime,
TokenizerServiceDeps,
TokenizerDepsRuntimeOptions,
tokenizeSubtitleService,
} from "./tokenizer-service";
tokenizeSubtitle,
} from "./tokenizer";
function makeDeps(
overrides: Partial<TokenizerServiceDeps> = {},
@@ -31,7 +31,7 @@ function makeDepsFromMecabTokenizer(
tokenize: (text: string) => Promise<import("../../types").Token[] | null>,
overrides: Partial<TokenizerDepsRuntimeOptions> = {},
): TokenizerServiceDeps {
return createTokenizerDepsRuntimeService({
return createTokenizerDepsRuntime({
getYomitanExt: () => null,
getYomitanParserWindow: () => null,
setYomitanParserWindow: () => {},
@@ -49,8 +49,8 @@ function makeDepsFromMecabTokenizer(
});
}
test("tokenizeSubtitleService assigns JLPT level to parsed Yomitan tokens", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle assigns JLPT level to parsed Yomitan tokens", async () => {
const result = await tokenizeSubtitle(
"猫です",
makeDeps({
getYomitanExt: () => ({ id: "dummy-ext" } as any),
@@ -88,9 +88,9 @@ test("tokenizeSubtitleService assigns JLPT level to parsed Yomitan tokens", asyn
assert.equal(result.tokens?.[0]?.jlptLevel, "N5");
});
test("tokenizeSubtitleService caches JLPT lookups across repeated tokens", async () => {
test("tokenizeSubtitle caches JLPT lookups across repeated tokens", async () => {
let lookupCalls = 0;
const result = await tokenizeSubtitleService(
const result = await tokenizeSubtitle(
"猫猫",
makeDepsFromMecabTokenizer(async () => [
{
@@ -133,8 +133,8 @@ test("tokenizeSubtitleService caches JLPT lookups across repeated tokens", async
assert.equal(result.tokens?.[1]?.jlptLevel, "N5");
});
test("tokenizeSubtitleService leaves JLPT unset for non-matching tokens", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle leaves JLPT unset for non-matching tokens", async () => {
const result = await tokenizeSubtitle(
"猫",
makeDepsFromMecabTokenizer(async () => [
{
@@ -159,9 +159,9 @@ test("tokenizeSubtitleService leaves JLPT unset for non-matching tokens", async
assert.equal(result.tokens?.[0]?.jlptLevel, undefined);
});
test("tokenizeSubtitleService skips JLPT lookups when disabled", async () => {
test("tokenizeSubtitle skips JLPT lookups when disabled", async () => {
let lookupCalls = 0;
const result = await tokenizeSubtitleService(
const result = await tokenizeSubtitle(
"猫です",
makeDeps({
tokenizeWithMecab: async () => [
@@ -190,8 +190,8 @@ test("tokenizeSubtitleService skips JLPT lookups when disabled", async () => {
assert.equal(lookupCalls, 0);
});
test("tokenizeSubtitleService applies frequency dictionary ranks", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle applies frequency dictionary ranks", async () => {
const result = await tokenizeSubtitle(
"猫です",
makeDeps({
getFrequencyDictionaryEnabled: () => true,
@@ -228,8 +228,8 @@ test("tokenizeSubtitleService applies frequency dictionary ranks", async () => {
assert.equal(result.tokens?.[1]?.frequencyRank, 1200);
});
test("tokenizeSubtitleService uses only selected Yomitan headword for frequency lookup", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle uses only selected Yomitan headword for frequency lookup", async () => {
const result = await tokenizeSubtitle(
"猫です",
makeDeps({
getFrequencyDictionaryEnabled: () => true,
@@ -265,8 +265,8 @@ test("tokenizeSubtitleService uses only selected Yomitan headword for frequency
assert.equal(result.tokens?.[0]?.frequencyRank, 1200);
});
test("tokenizeSubtitleService keeps furigana-split Yomitan segments as one token", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle keeps furigana-split Yomitan segments as one token", async () => {
const result = await tokenizeSubtitle(
"友達と話した",
makeDeps({
getFrequencyDictionaryEnabled: () => true,
@@ -324,8 +324,8 @@ test("tokenizeSubtitleService keeps furigana-split Yomitan segments as one token
assert.equal(result.tokens?.[2]?.frequencyRank, 90);
});
test("tokenizeSubtitleService prefers exact headword frequency over surface/reading when available", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle prefers exact headword frequency over surface/reading when available", async () => {
const result = await tokenizeSubtitle(
"猫です",
makeDeps({
getFrequencyDictionaryEnabled: () => true,
@@ -358,8 +358,8 @@ test("tokenizeSubtitleService prefers exact headword frequency over surface/read
assert.equal(result.tokens?.[0]?.frequencyRank, 8);
});
test("tokenizeSubtitleService keeps no frequency when only reading matches and headword misses", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle keeps no frequency when only reading matches and headword misses", async () => {
const result = await tokenizeSubtitle(
"猫です",
makeDeps({
getFrequencyDictionaryEnabled: () => true,
@@ -392,8 +392,8 @@ test("tokenizeSubtitleService keeps no frequency when only reading matches and h
assert.equal(result.tokens?.[0]?.frequencyRank, undefined);
});
test("tokenizeSubtitleService ignores invalid frequency rank on selected headword", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle ignores invalid frequency rank on selected headword", async () => {
const result = await tokenizeSubtitle(
"猫です",
makeDeps({
getFrequencyDictionaryEnabled: () => true,
@@ -429,8 +429,8 @@ test("tokenizeSubtitleService ignores invalid frequency rank on selected headwor
assert.equal(result.tokens?.[0]?.frequencyRank, undefined);
});
test("tokenizeSubtitleService handles real-word frequency candidates and prefers most frequent term", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle handles real-word frequency candidates and prefers most frequent term", async () => {
const result = await tokenizeSubtitle(
"昨日",
makeDeps({
getFrequencyDictionaryEnabled: () => true,
@@ -466,8 +466,8 @@ test("tokenizeSubtitleService handles real-word frequency candidates and prefers
assert.equal(result.tokens?.[0]?.frequencyRank, 40);
});
test("tokenizeSubtitleService ignores candidates with no dictionary rank when higher-frequency candidate exists", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle ignores candidates with no dictionary rank when higher-frequency candidate exists", async () => {
const result = await tokenizeSubtitle(
"猫です",
makeDeps({
getFrequencyDictionaryEnabled: () => true,
@@ -504,8 +504,8 @@ test("tokenizeSubtitleService ignores candidates with no dictionary rank when hi
assert.equal(result.tokens?.[0]?.frequencyRank, 88);
});
test("tokenizeSubtitleService ignores frequency lookup failures", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle ignores frequency lookup failures", async () => {
const result = await tokenizeSubtitle(
"猫",
makeDeps({
getFrequencyDictionaryEnabled: () => true,
@@ -531,8 +531,8 @@ test("tokenizeSubtitleService ignores frequency lookup failures", async () => {
assert.equal(result.tokens?.[0]?.frequencyRank, undefined);
});
test("tokenizeSubtitleService skips frequency rank when Yomitan token is enriched as particle by mecab pos1", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle skips frequency rank when Yomitan token is enriched as particle by mecab pos1", async () => {
const result = await tokenizeSubtitle(
"は",
makeDeps({
getFrequencyDictionaryEnabled: () => true,
@@ -580,8 +580,8 @@ test("tokenizeSubtitleService skips frequency rank when Yomitan token is enriche
assert.equal(result.tokens?.[0]?.frequencyRank, undefined);
});
test("tokenizeSubtitleService ignores invalid frequency ranks", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle ignores invalid frequency ranks", async () => {
const result = await tokenizeSubtitle(
"猫",
makeDeps({
getFrequencyDictionaryEnabled: () => true,
@@ -622,9 +622,9 @@ test("tokenizeSubtitleService ignores invalid frequency ranks", async () => {
assert.equal(result.tokens?.[1]?.frequencyRank, undefined);
});
test("tokenizeSubtitleService skips frequency lookups when disabled", async () => {
test("tokenizeSubtitle skips frequency lookups when disabled", async () => {
let frequencyCalls = 0;
const result = await tokenizeSubtitleService(
const result = await tokenizeSubtitle(
"猫",
makeDeps({
getFrequencyDictionaryEnabled: () => false,
@@ -653,8 +653,8 @@ test("tokenizeSubtitleService skips frequency lookups when disabled", async () =
assert.equal(frequencyCalls, 0);
});
test("tokenizeSubtitleService skips JLPT level for excluded demonstratives", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle skips JLPT level for excluded demonstratives", async () => {
const result = await tokenizeSubtitle(
"この",
makeDeps({
getYomitanExt: () => ({ id: "dummy-ext" } as any),
@@ -687,8 +687,8 @@ test("tokenizeSubtitleService skips JLPT level for excluded demonstratives", asy
assert.equal(result.tokens?.[0]?.jlptLevel, undefined);
});
test("tokenizeSubtitleService skips JLPT level for repeated kana SFX", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle skips JLPT level for repeated kana SFX", async () => {
const result = await tokenizeSubtitle(
"ああ",
makeDeps({
getYomitanExt: () => ({ id: "dummy-ext" } as any),
@@ -721,8 +721,8 @@ test("tokenizeSubtitleService skips JLPT level for repeated kana SFX", async ()
assert.equal(result.tokens?.[0]?.jlptLevel, undefined);
});
test("tokenizeSubtitleService assigns JLPT level to mecab tokens", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle assigns JLPT level to mecab tokens", async () => {
const result = await tokenizeSubtitle(
"猫です",
makeDepsFromMecabTokenizer(async () => [
{
@@ -747,8 +747,8 @@ test("tokenizeSubtitleService assigns JLPT level to mecab tokens", async () => {
assert.equal(result.tokens?.[0]?.jlptLevel, "N4");
});
test("tokenizeSubtitleService skips JLPT level for mecab tokens marked as ineligible", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle skips JLPT level for mecab tokens marked as ineligible", async () => {
const result = await tokenizeSubtitle(
"は",
makeDepsFromMecabTokenizer(async () => [
{
@@ -774,14 +774,14 @@ test("tokenizeSubtitleService skips JLPT level for mecab tokens marked as inelig
assert.equal(result.tokens?.[0]?.jlptLevel, undefined);
});
test("tokenizeSubtitleService returns null tokens for empty normalized text", async () => {
const result = await tokenizeSubtitleService(" \\n ", makeDeps());
test("tokenizeSubtitle returns null tokens for empty normalized text", async () => {
const result = await tokenizeSubtitle(" \\n ", makeDeps());
assert.deepEqual(result, { text: " \\n ", tokens: null });
});
test("tokenizeSubtitleService normalizes newlines before mecab fallback", async () => {
test("tokenizeSubtitle normalizes newlines before mecab fallback", async () => {
let tokenizeInput = "";
const result = await tokenizeSubtitleService(
const result = await tokenizeSubtitle(
"猫\\Nです\nね",
makeDeps({
tokenizeWithMecab: async (text) => {
@@ -808,8 +808,8 @@ test("tokenizeSubtitleService normalizes newlines before mecab fallback", async
assert.equal(result.tokens?.[0]?.surface, "猫ですね");
});
test("tokenizeSubtitleService falls back to mecab tokens when available", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle falls back to mecab tokens when available", async () => {
const result = await tokenizeSubtitle(
"猫です",
makeDeps({
tokenizeWithMecab: async () => [
@@ -833,8 +833,8 @@ test("tokenizeSubtitleService falls back to mecab tokens when available", async
assert.equal(result.tokens?.[0]?.surface, "猫");
});
test("tokenizeSubtitleService returns null tokens when mecab throws", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle returns null tokens when mecab throws", async () => {
const result = await tokenizeSubtitle(
"猫です",
makeDeps({
tokenizeWithMecab: async () => {
@@ -846,7 +846,7 @@ test("tokenizeSubtitleService returns null tokens when mecab throws", async () =
assert.deepEqual(result, { text: "猫です", tokens: null });
});
test("tokenizeSubtitleService uses Yomitan parser result when available", async () => {
test("tokenizeSubtitle uses Yomitan parser result when available", async () => {
const parserWindow = {
isDestroyed: () => false,
webContents: {
@@ -874,7 +874,7 @@ test("tokenizeSubtitleService uses Yomitan parser result when available", async
},
} as unknown as Electron.BrowserWindow;
const result = await tokenizeSubtitleService(
const result = await tokenizeSubtitle(
"猫です",
makeDeps({
getYomitanExt: () => ({ id: "dummy-ext" } as any),
@@ -893,7 +893,7 @@ test("tokenizeSubtitleService uses Yomitan parser result when available", async
assert.equal(result.tokens?.[1]?.isKnown, false);
});
test("tokenizeSubtitleService logs selected Yomitan groups when debug toggle is enabled", async () => {
test("tokenizeSubtitle logs selected Yomitan groups when debug toggle is enabled", async () => {
const infoLogs: string[] = [];
const originalInfo = console.info;
console.info = (...args: unknown[]) => {
@@ -901,7 +901,7 @@ test("tokenizeSubtitleService logs selected Yomitan groups when debug toggle is
};
try {
await tokenizeSubtitleService(
await tokenizeSubtitle(
"友達と話した",
makeDeps({
getYomitanExt: () => ({ id: "dummy-ext" } as any),
@@ -949,7 +949,7 @@ test("tokenizeSubtitleService logs selected Yomitan groups when debug toggle is
);
});
test("tokenizeSubtitleService does not log Yomitan groups when debug toggle is disabled", async () => {
test("tokenizeSubtitle does not log Yomitan groups when debug toggle is disabled", async () => {
const infoLogs: string[] = [];
const originalInfo = console.info;
console.info = (...args: unknown[]) => {
@@ -957,7 +957,7 @@ test("tokenizeSubtitleService does not log Yomitan groups when debug toggle is d
};
try {
await tokenizeSubtitleService(
await tokenizeSubtitle(
"友達と話した",
makeDeps({
getYomitanExt: () => ({ id: "dummy-ext" } as any),
@@ -999,7 +999,7 @@ test("tokenizeSubtitleService does not log Yomitan groups when debug toggle is d
);
});
test("tokenizeSubtitleService preserves segmented Yomitan line as one token", async () => {
test("tokenizeSubtitle preserves segmented Yomitan line as one token", async () => {
const parserWindow = {
isDestroyed: () => false,
webContents: {
@@ -1025,7 +1025,7 @@ test("tokenizeSubtitleService preserves segmented Yomitan line as one token", as
},
} as unknown as Electron.BrowserWindow;
const result = await tokenizeSubtitleService(
const result = await tokenizeSubtitle(
"猫です",
makeDeps({
getYomitanExt: () => ({ id: "dummy-ext" } as any),
@@ -1042,8 +1042,8 @@ test("tokenizeSubtitleService preserves segmented Yomitan line as one token", as
assert.equal(result.tokens?.[0]?.isKnown, false);
});
test("tokenizeSubtitleService prefers mecab parser tokens when scanning parser returns one token", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle prefers mecab parser tokens when scanning parser returns one token", async () => {
const result = await tokenizeSubtitle(
"俺は小園にいきたい",
makeDeps({
getYomitanExt: () => ({ id: "dummy-ext" } as any),
@@ -1091,8 +1091,8 @@ test("tokenizeSubtitleService prefers mecab parser tokens when scanning parser r
assert.equal(result.tokens?.[2]?.frequencyRank, 25);
});
test("tokenizeSubtitleService keeps scanning parser tokens when they are already split", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle keeps scanning parser tokens when they are already split", async () => {
const result = await tokenizeSubtitle(
"小園に行きたい",
makeDeps({
getYomitanExt: () => ({ id: "dummy-ext" } as any),
@@ -1139,8 +1139,8 @@ test("tokenizeSubtitleService keeps scanning parser tokens when they are already
assert.equal(result.tokens?.[2]?.frequencyRank, undefined);
});
test("tokenizeSubtitleService prefers parse candidates with fewer fragment-only kana tokens when source priority is equal", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle prefers parse candidates with fewer fragment-only kana tokens when source priority is equal", async () => {
const result = await tokenizeSubtitle(
"俺は公園にいきたい",
makeDeps({
getYomitanExt: () => ({ id: "dummy-ext" } as any),
@@ -1192,8 +1192,8 @@ test("tokenizeSubtitleService prefers parse candidates with fewer fragment-only
assert.equal(result.tokens?.[4]?.frequencyRank, 1500);
});
test("tokenizeSubtitleService still assigns frequency to non-known Yomitan tokens", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle still assigns frequency to non-known Yomitan tokens", async () => {
const result = await tokenizeSubtitle(
"小園に",
makeDeps({
getYomitanExt: () => ({ id: "dummy-ext" } as any),
@@ -1229,8 +1229,8 @@ test("tokenizeSubtitleService still assigns frequency to non-known Yomitan token
assert.equal(result.tokens?.[1]?.frequencyRank, 3000);
});
test("tokenizeSubtitleService marks tokens as known using callback", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle marks tokens as known using callback", async () => {
const result = await tokenizeSubtitle(
"猫です",
makeDepsFromMecabTokenizer(async () => [
{
@@ -1255,8 +1255,8 @@ test("tokenizeSubtitleService marks tokens as known using callback", async () =>
assert.equal(result.tokens?.[0]?.isKnown, true);
});
test("tokenizeSubtitleService still assigns frequency rank to non-known tokens", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle still assigns frequency rank to non-known tokens", async () => {
const result = await tokenizeSubtitle(
"既知未知",
makeDeps({
tokenizeWithMecab: async () => [
@@ -1312,8 +1312,8 @@ test("tokenizeSubtitleService still assigns frequency rank to non-known tokens",
assert.equal(result.tokens?.[1]?.frequencyRank, 30);
});
test("tokenizeSubtitleService selects one N+1 target token", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle selects one N+1 target token", async () => {
const result = await tokenizeSubtitle(
"猫です",
makeDeps({
tokenizeWithMecab: async () => [
@@ -1349,8 +1349,8 @@ test("tokenizeSubtitleService selects one N+1 target token", async () => {
assert.equal(targets[0]?.surface, "犬");
});
test("tokenizeSubtitleService does not mark target when sentence has multiple candidates", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle does not mark target when sentence has multiple candidates", async () => {
const result = await tokenizeSubtitle(
"猫犬",
makeDeps({
tokenizeWithMecab: async () => [
@@ -1386,7 +1386,7 @@ test("tokenizeSubtitleService does not mark target when sentence has multiple ca
);
});
test("tokenizeSubtitleService applies N+1 target marking to Yomitan results", async () => {
test("tokenizeSubtitle applies N+1 target marking to Yomitan results", async () => {
const parserWindow = {
isDestroyed: () => false,
webContents: {
@@ -1415,7 +1415,7 @@ test("tokenizeSubtitleService applies N+1 target marking to Yomitan results", as
},
} as unknown as Electron.BrowserWindow;
const result = await tokenizeSubtitleService(
const result = await tokenizeSubtitle(
"猫です",
makeDeps({
getYomitanExt: () => ({ id: "dummy-ext" } as any),
@@ -1433,8 +1433,8 @@ test("tokenizeSubtitleService applies N+1 target marking to Yomitan results", as
assert.equal(result.tokens?.[1]?.isNPlusOneTarget, false);
});
test("tokenizeSubtitleService does not color 1-2 word sentences by default", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle does not color 1-2 word sentences by default", async () => {
const result = await tokenizeSubtitle(
"猫です",
makeDeps({
tokenizeWithMecab: async () => [
@@ -1470,8 +1470,8 @@ test("tokenizeSubtitleService does not color 1-2 word sentences by default", asy
);
});
test("tokenizeSubtitleService checks known words by headword, not surface", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle checks known words by headword, not surface", async () => {
const result = await tokenizeSubtitle(
"猫です",
makeDepsFromMecabTokenizer(async () => [
{
@@ -1496,8 +1496,8 @@ test("tokenizeSubtitleService checks known words by headword, not surface", asyn
assert.equal(result.tokens?.[0]?.isKnown, true);
});
test("tokenizeSubtitleService checks known words by surface when configured", async () => {
const result = await tokenizeSubtitleService(
test("tokenizeSubtitle checks known words by surface when configured", async () => {
const result = await tokenizeSubtitle(
"猫です",
makeDepsFromMecabTokenizer(async () => [
{

View File

@@ -182,7 +182,7 @@ function getCachedFrequencyRank(
return rank;
}
export function createTokenizerDepsRuntimeService(
export function createTokenizerDepsRuntime(
options: TokenizerDepsRuntimeOptions,
): TokenizerServiceDeps {
return {
@@ -983,7 +983,7 @@ async function parseWithYomitanInternalParser(
}
}
export async function tokenizeSubtitleService(
export async function tokenizeSubtitle(
text: string,
deps: TokenizerServiceDeps,
): Promise<SubtitleData> {

View File

@@ -58,7 +58,7 @@ function ensureExtensionCopy(sourceDir: string, userDataPath: string): string {
return targetDir;
}
export async function loadYomitanExtensionService(
export async function loadYomitanExtension(
deps: YomitanExtensionLoaderDeps,
): Promise<Extension | null> {
const searchPaths = [