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

@@ -0,0 +1,64 @@
---
id: TASK-55
title: Normalize service naming conventions across core/services
status: Done
assignee: []
created_date: '2026-02-16 04:47'
updated_date: '2026-02-17 09:12'
labels: []
dependencies: []
references:
- /home/sudacode/projects/japanese/SubMiner/src/core/services/index.ts
priority: low
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
The core/services directory has inconsistent naming patterns that create confusion:
- Some files use `*Service.ts` suffix (e.g., `mpv-service.ts`, `tokenizer-service.ts`)
- Others use `*RuntimeService.ts` or just descriptive names (e.g., `overlay-visibility-service.ts` exports functions with 'Service' in name)
- Some functions in files have 'Service' suffix, others don't
This inconsistency makes it hard to predict file/function names and creates cognitive overhead.
Standardize on:
- File names: `kebab-case.ts` without 'service' suffix (e.g., `mpv.ts`, `tokenizer.ts`)
- Function names: descriptive verbs without 'Service' suffix (e.g., `createMpvClient()`, `tokenizeSubtitle()`)
- Barrel exports: clean, predictable names
Files needing audit (47 services):
- All files in src/core/services/ need review
Note: This is a large-scale refactor that should be done carefully to avoid breaking changes.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Establish naming convention rules (document in code or docs)
- [x] #2 Audit all service files for naming inconsistencies
- [x] #3 Rename files to follow convention (kebab-case, no 'service' suffix)
- [x] #4 Rename exported functions to remove 'Service' suffix where present
- [x] #5 Update all imports across the entire codebase
- [x] #6 Update barrel exports
- [x] #7 Run full test suite
- [x] #8 Update any documentation referencing old names
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Starting implementation. Planning and executing a mechanical refactor: file names and exported symbols with Service suffix in `src/core/services`, then cascading import updates across `src/`.
Implemented naming convention refactor across `src/core/services`: removed `-service` from service file names, renamed Service-suffixed exported symbols to non-Service names, and updated barrel exports in `src/core/services/index.ts`.
Updated call sites across `src/main/**`, `src/core/services/**` tests, `scripts/**`, `package.json` test paths, and docs references (`docs/development.md`, `docs/architecture.md`, `docs/structure-roadmap.md`).
Validation completed: `pnpm run build` and `pnpm run test:fast` both pass after refactor.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Normalized `src/core/services` naming by removing `-service` from module filenames, dropping `Service` suffixes from exported service functions, and updating `src/core/services/index.ts` barrel exports to the new names. Updated all import/call sites across `src/main/**`, service tests, scripts, and docs/package test paths to match the new module and symbol names. Verified no behavior regressions with `pnpm run build` and `pnpm run test:fast` (all passing).
<!-- SECTION:FINAL_SUMMARY:END -->

View File

@@ -1,45 +0,0 @@
---
id: TASK-55
title: Normalize service naming conventions across core/services
status: To Do
assignee: []
created_date: '2026-02-16 04:47'
labels: []
dependencies: []
references:
- /home/sudacode/projects/japanese/SubMiner/src/core/services/index.ts
priority: low
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
The core/services directory has inconsistent naming patterns that create confusion:
- Some files use `*Service.ts` suffix (e.g., `mpv-service.ts`, `tokenizer-service.ts`)
- Others use `*RuntimeService.ts` or just descriptive names (e.g., `overlay-visibility-service.ts` exports functions with 'Service' in name)
- Some functions in files have 'Service' suffix, others don't
This inconsistency makes it hard to predict file/function names and creates cognitive overhead.
Standardize on:
- File names: `kebab-case.ts` without 'service' suffix (e.g., `mpv.ts`, `tokenizer.ts`)
- Function names: descriptive verbs without 'Service' suffix (e.g., `createMpvClient()`, `tokenizeSubtitle()`)
- Barrel exports: clean, predictable names
Files needing audit (47 services):
- All files in src/core/services/ need review
Note: This is a large-scale refactor that should be done carefully to avoid breaking changes.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 Establish naming convention rules (document in code or docs)
- [ ] #2 Audit all service files for naming inconsistencies
- [ ] #3 Rename files to follow convention (kebab-case, no 'service' suffix)
- [ ] #4 Rename exported functions to remove 'Service' suffix where present
- [ ] #5 Update all imports across the entire codebase
- [ ] #6 Update barrel exports
- [ ] #7 Run full test suite
- [ ] #8 Update any documentation referencing old names
<!-- AC:END -->

View File

@@ -222,7 +222,7 @@ flowchart TD
- **Better reviewability:** PRs can be scoped to one subsystem. - **Better reviewability:** PRs can be scoped to one subsystem.
- **Backward compatibility:** CLI flags and IPC channels can remain stable while internals evolve. - **Backward compatibility:** CLI flags and IPC channels can remain stable while internals evolve.
- **Extracted composition root:** TASK-27 refactored `main.ts` into focused modules under `src/main/`, isolating startup, lifecycle, IPC, CLI, and domain-specific runtime wiring. - **Extracted composition root:** TASK-27 refactored `main.ts` into focused modules under `src/main/`, isolating startup, lifecycle, IPC, CLI, and domain-specific runtime wiring.
- **Split MPV service:** TASK-27.4 separated `mpv-service.ts` into transport (`mpv-transport.ts`), protocol (`mpv-protocol.ts`), state (`mpv-state.ts`), and properties (`mpv-properties.ts`) layers for improved maintainability. - **Split MPV service:** TASK-27.4 separated `mpv.ts` into transport (`mpv-transport.ts`), protocol (`mpv-protocol.ts`), state (`mpv-state.ts`), and properties (`mpv-properties.ts`) layers for improved maintainability.
## Extension Rules ## Extension Rules

View File

@@ -96,7 +96,7 @@ Run `make help` for a full list of targets. Key ones:
## Contributor Notes ## Contributor Notes
- To add or change a config option, update `src/config/definitions.ts` first. Defaults, runtime-option metadata, and generated `config.example.jsonc` are derived from this centralized source. - To add or change a config option, update `src/config/definitions.ts` first. Defaults, runtime-option metadata, and generated `config.example.jsonc` are derived from this centralized source.
- Overlay window/visibility state is owned by `src/core/services/overlay-manager-service.ts`. - Overlay window/visibility state is owned by `src/core/services/overlay-manager.ts`.
- Main process composition is now split across `src/main/` modules (`startup.ts`, `app-lifecycle.ts`, `startup-lifecycle.ts`, `state.ts`, `ipc-runtime.ts`, `cli-runtime.ts`, `overlay-runtime.ts`, `subsync-runtime.ts`). - Main process composition is now split across `src/main/` modules (`startup.ts`, `app-lifecycle.ts`, `startup-lifecycle.ts`, `state.ts`, `ipc-runtime.ts`, `cli-runtime.ts`, `overlay-runtime.ts`, `subsync-runtime.ts`).
- MPV service has been split into transport, protocol, state, and properties layers in `src/core/services/`. - MPV service has been split into transport, protocol, state, and properties layers in `src/core/services/`.
- Prefer direct inline deps objects in `src/main/` modules for simple pass-through wiring. - Prefer direct inline deps objects in `src/main/` modules for simple pass-through wiring.

View File

@@ -8,8 +8,8 @@ Date: 2026-02-14
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `src/main.ts` | Bootstrap / composition root / orchestration | Active | Main entrypoint owns startup, lifecycle orchestration, service construction, state mutation surfaces, and IPC bindings | | `src/main.ts` | Bootstrap / composition root / orchestration | Active | Main entrypoint owns startup, lifecycle orchestration, service construction, state mutation surfaces, and IPC bindings |
| `src/anki-integration.ts` | Domain service orchestration / integrations | Active | 2.6k+ LOC, high cyclomatic coupling to mpv/subtitle timing and mining flows | | `src/anki-integration.ts` | Domain service orchestration / integrations | Active | 2.6k+ LOC, high cyclomatic coupling to mpv/subtitle timing and mining flows |
| `src/core/services/mpv-service.ts` | MPV protocol + app state bridge | Active | ~780 LOC, large protocol and behavior mix, 22-entry dep interface | | `src/core/services/mpv.ts` | MPV protocol + app state bridge | Active | ~780 LOC, large protocol and behavior mix, 22-entry dep interface |
| `src/core/services/subsync-service.ts` | Subsync orchestration (ffsubsync/alass workflows) | Active | ~494 LOC, file IO + mpv command orchestration + result shaping | | `src/core/services/subsync.ts` | Subsync orchestration (ffsubsync/alass workflows) | Active | ~494 LOC, file IO + mpv command orchestration + result shaping |
| `src/renderer/positioning.ts` | Renderer positioning layout policy | Active (downstream of TASK-27.5) | 513 LOC, layout/rules + platform-specific behavior in one module | | `src/renderer/positioning.ts` | Renderer positioning layout policy | Active (downstream of TASK-27.5) | 513 LOC, layout/rules + platform-specific behavior in one module |
| `src/config/service.ts` | Config load/validation | Active support | ~601 LOC, central schema validation + mutation APIs | | `src/config/service.ts` | Config load/validation | Active support | ~601 LOC, central schema validation + mutation APIs |
| `src/types.ts` | Shared contract surface | Active support | ~640 LOC, foundational type exports driving module boundaries | | `src/types.ts` | Shared contract surface | Active support | ~640 LOC, foundational type exports driving module boundaries |
@@ -34,7 +34,7 @@ Date: 2026-02-14
- Electron event loop (`app.whenReady`, `process` signals) - Electron event loop (`app.whenReady`, `process` signals)
- startup/bootstrap services, service adapters in `src/core/services` - startup/bootstrap services, service adapters in `src/core/services`
### `src/core/services/mpv-service.ts` (protocol + facade) ### `src/core/services/mpv.ts` (protocol + facade)
- **Core exports**: `MpvIpcClient`, `MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY`, `MpvIpcClientDeps` - **Core exports**: `MpvIpcClient`, `MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY`, `MpvIpcClientDeps`
- **Primary responsibilities / API**: - **Primary responsibilities / API**:
@@ -45,9 +45,9 @@ Date: 2026-02-14
- state restoration (`restorePreviousSecondarySubVisibility`) - state restoration (`restorePreviousSecondarySubVisibility`)
- **Current caller set**: - **Current caller set**:
- `src/main.ts` (construction + lifecycle + service invocations) - `src/main.ts` (construction + lifecycle + service invocations)
- `src/core/services/mpv-control-service.ts` (runtime control API) - `src/main/ipc-mpv-command.ts` (runtime control API)
- `src/core/services/subsync-service.ts` (`requestProperty`, request ID usage) - `src/core/services/subsync.ts` (`requestProperty`, request ID usage)
- tests under `src/core/services/mpv-service.test.ts` - tests under `src/core/services/mpv.test.ts`
- **Observed coupling risk**: - **Observed coupling risk**:
- `MpvIpcClientDeps` mixes protocol config with app-level side effects (subtitle broadcast, tokenizer, overlay updates, config reads) - `MpvIpcClientDeps` mixes protocol config with app-level side effects (subtitle broadcast, tokenizer, overlay updates, config reads)
@@ -61,15 +61,15 @@ Date: 2026-02-14
- **State dependencies (constructor)**: - **State dependencies (constructor)**:
- config, subtitle timing tracker, mpv client, OSD/notification callbacks - config, subtitle timing tracker, mpv client, OSD/notification callbacks
- **Primary callers**: - **Primary callers**:
- `src/core/services/overlay-runtime-init-service.ts` (initial integration creation) - `src/core/services/overlay-runtime-init.ts` (initial integration creation)
- `src/core/services/anki-jimaku-service.ts` (enable/disable and field-grouping RPC) - `src/core/services/anki-jimaku.ts` (enable/disable and field-grouping RPC)
- `src/core/services/mining-service.ts` (delegates mining actions) - `src/core/services/mining.ts` (delegates mining actions)
### `src/core/services/subsync-service.ts` ### `src/core/services/subsync.ts`
- **Exports**: `runSubsyncManualService`, `openSubsyncManualPickerService`, `triggerSubsyncFromConfigService` - **Exports**: `runSubsyncManualService`, `openSubsyncManualPickerService`, `triggerSubsyncFromConfigService`
- **Caller set**: - **Caller set**:
- `src/core/services/subsync-runner-service.ts` (runtime wrappers) - `src/core/services/subsync-runner.ts` (runtime wrappers)
- `src/core/services/mpv-jimaku/`? (through runtime services and IPC command handlers) - `src/core/services/mpv-jimaku/`? (through runtime services and IPC command handlers)
### `src/config/service.ts` ### `src/config/service.ts`

View File

@@ -14,7 +14,7 @@
"docs:build": "VITE_EXTRA_EXTENSIONS=jsonc vitepress build docs", "docs:build": "VITE_EXTRA_EXTENSIONS=jsonc vitepress build docs",
"docs:preview": "VITE_EXTRA_EXTENSIONS=jsonc vitepress preview docs --host 0.0.0.0 --port 4173 --strictPort", "docs:preview": "VITE_EXTRA_EXTENSIONS=jsonc vitepress preview docs --host 0.0.0.0 --port 4173 --strictPort",
"test:config:dist": "node --test dist/config/config.test.js", "test:config:dist": "node --test dist/config/config.test.js",
"test:core:dist": "node --test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command-service.test.js dist/core/services/field-grouping-overlay-service.test.js dist/core/services/numeric-shortcut-session-service.test.js dist/core/services/secondary-subtitle-service.test.js dist/core/services/mpv-render-metrics-service.test.js dist/core/services/overlay-content-measurement-service.test.js dist/core/services/mpv-control-service.test.js dist/core/services/mpv-service.test.js dist/core/services/runtime-options-ipc-service.test.js dist/core/services/runtime-config-service.test.js dist/core/services/tokenizer-service.test.js dist/core/services/subsync-service.test.js dist/core/services/overlay-bridge-service.test.js dist/core/services/overlay-manager-service.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining-service.test.js dist/core/services/anki-jimaku-service.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/app-ready-service.test.js dist/core/services/startup-bootstrap-service.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js", "test:core:dist": "node --test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/tokenizer.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js",
"test:subtitle:dist": "echo \"Subtitle tests are currently not configured\"", "test:subtitle:dist": "echo \"Subtitle tests are currently not configured\"",
"test": "pnpm run test:config && pnpm run test:core", "test": "pnpm run test:config && pnpm run test:core",
"test:config": "pnpm run build && pnpm run test:config:dist", "test:config": "pnpm run build && pnpm run test:config:dist",

View File

@@ -2,8 +2,8 @@ import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import process from "node:process"; import process from "node:process";
import { createTokenizerDepsRuntimeService, tokenizeSubtitleService } from "../src/core/services/tokenizer-service.js"; import { createTokenizerDepsRuntime, tokenizeSubtitle } from "../src/core/services/tokenizer.js";
import { createFrequencyDictionaryLookupService } from "../src/core/services/frequency-dictionary-service.js"; import { createFrequencyDictionaryLookup } from "../src/core/services/index.js";
import { MecabTokenizer } from "../src/mecab-tokenizer.js"; import { MecabTokenizer } from "../src/mecab-tokenizer.js";
import type { MergedToken, FrequencyDictionaryLookup } from "../src/types.js"; import type { MergedToken, FrequencyDictionaryLookup } from "../src/types.js";
@@ -537,11 +537,11 @@ async function createYomitanRuntimeState(
try { try {
await electronImport.app.whenReady(); await electronImport.app.whenReady();
const loadYomitanExtensionService = ( const loadYomitanExtension = (
await import( await import(
"../src/core/services/yomitan-extension-loader-service.js" "../src/core/services/yomitan-extension-loader.js"
) )
).loadYomitanExtensionService as ( ).loadYomitanExtension as (
options: { options: {
userDataPath: string; userDataPath: string;
getYomitanParserWindow: () => unknown; getYomitanParserWindow: () => unknown;
@@ -552,7 +552,7 @@ async function createYomitanRuntimeState(
}, },
) => Promise<unknown>; ) => Promise<unknown>;
const extension = await loadYomitanExtensionService({ const extension = await loadYomitanExtension({
userDataPath, userDataPath,
getYomitanParserWindow: () => state.parserWindow, getYomitanParserWindow: () => state.parserWindow,
setYomitanParserWindow: (window) => { setYomitanParserWindow: (window) => {
@@ -623,7 +623,7 @@ async function createYomitanRuntimeStateWithSearch(
} }
async function getFrequencyLookup(dictionaryPath: string): Promise<FrequencyDictionaryLookup> { async function getFrequencyLookup(dictionaryPath: string): Promise<FrequencyDictionaryLookup> {
return createFrequencyDictionaryLookupService({ return createFrequencyDictionaryLookup({
searchPaths: [dictionaryPath], searchPaths: [dictionaryPath],
log: (message) => { log: (message) => {
// Keep script output pure JSON by default // Keep script output pure JSON by default
@@ -786,7 +786,7 @@ async function main(): Promise<void> {
: null; : null;
const hasYomitan = Boolean(yomitanState?.available && yomitanState?.yomitanExt); const hasYomitan = Boolean(yomitanState?.available && yomitanState?.yomitanExt);
const deps = createTokenizerDepsRuntimeService({ const deps = createTokenizerDepsRuntime({
getYomitanExt: () => getYomitanExt: () =>
(hasYomitan ? yomitanState!.yomitanExt : null) as never, (hasYomitan ? yomitanState!.yomitanExt : null) as never,
getYomitanParserWindow: () => getYomitanParserWindow: () =>
@@ -823,7 +823,7 @@ async function main(): Promise<void> {
}), }),
}); });
const subtitleData = await tokenizeSubtitleService(args.input, deps); const subtitleData = await tokenizeSubtitle(args.input, deps);
const tokenCount = subtitleData.tokens?.length ?? 0; const tokenCount = subtitleData.tokens?.length ?? 0;
const mergedCount = subtitleData.tokens?.filter((token) => token.isMerged).length ?? 0; const mergedCount = subtitleData.tokens?.filter((token) => token.isMerged).length ?? 0;
const tokens = const tokens =

View File

@@ -2,7 +2,7 @@ import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import process from "node:process"; import process from "node:process";
import { createTokenizerDepsRuntimeService, tokenizeSubtitleService } from "../src/core/services/tokenizer-service.js"; import { createTokenizerDepsRuntime, tokenizeSubtitle } from "../src/core/services/tokenizer.js";
import { MecabTokenizer } from "../src/mecab-tokenizer.js"; import { MecabTokenizer } from "../src/mecab-tokenizer.js";
import type { MergedToken } from "../src/types.js"; import type { MergedToken } from "../src/types.js";
@@ -563,7 +563,7 @@ async function main(): Promise<void> {
yomitan.parserReadyPromise = runtime.parserReadyPromise; yomitan.parserReadyPromise = runtime.parserReadyPromise;
yomitan.parserInitPromise = runtime.parserInitPromise; yomitan.parserInitPromise = runtime.parserInitPromise;
const deps = createTokenizerDepsRuntimeService({ const deps = createTokenizerDepsRuntime({
getYomitanExt: () => yomitan.extension, getYomitanExt: () => yomitan.extension,
getYomitanParserWindow: () => yomitan.parserWindow, getYomitanParserWindow: () => yomitan.parserWindow,
setYomitanParserWindow: (window) => { setYomitanParserWindow: (window) => {
@@ -585,7 +585,7 @@ async function main(): Promise<void> {
}), }),
}); });
const subtitleData = await tokenizeSubtitleService(args.input, deps); const subtitleData = await tokenizeSubtitle(args.input, deps);
const tokenizeText = normalizeTokenizerText(args.input); const tokenizeText = normalizeTokenizerText(args.input);
let rawParseResults: unknown = null; let rawParseResults: unknown = null;
if ( if (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,15 +4,15 @@ import fs from "node:fs";
import os from "node:os"; import os from "node:os";
import path from "node:path"; 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 logs: string[] = [];
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "subminer-frequency-dict-")); const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "subminer-frequency-dict-"));
const bankPath = path.join(tempDir, "term_meta_bank_1.json"); const bankPath = path.join(tempDir, "term_meta_bank_1.json");
fs.writeFileSync(bankPath, "{ invalid json"); fs.writeFileSync(bankPath, "{ invalid json");
const lookup = await createFrequencyDictionaryLookupService({ const lookup = await createFrequencyDictionaryLookup({
searchPaths: [tempDir], searchPaths: [tempDir],
log: (message) => { log: (message) => {
logs.push(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 logs: string[] = [];
const missingPath = path.join(os.tmpdir(), "subminer-frequency-dict-missing-dir"); const missingPath = path.join(os.tmpdir(), "subminer-frequency-dict-missing-dir");
const lookup = await createFrequencyDictionaryLookupService({ const lookup = await createFrequencyDictionaryLookup({
searchPaths: [missingPath], searchPaths: [missingPath],
log: (message) => { log: (message) => {
logs.push(message); logs.push(message);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,16 @@
import test from "node:test"; import test from "node:test";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { import {
playNextSubtitleRuntimeService, playNextSubtitleRuntime,
replayCurrentSubtitleRuntimeService, replayCurrentSubtitleRuntime,
sendMpvCommandRuntimeService, sendMpvCommandRuntime,
setMpvSubVisibilityRuntimeService, setMpvSubVisibilityRuntime,
showMpvOsdRuntimeService, showMpvOsdRuntime,
} from "./mpv-service"; } from "./mpv";
test("showMpvOsdRuntimeService sends show-text when connected", () => { test("showMpvOsdRuntime sends show-text when connected", () => {
const commands: (string | number)[][] = []; const commands: (string | number)[][] = [];
showMpvOsdRuntimeService( showMpvOsdRuntime(
{ {
connected: true, connected: true,
send: ({ command }) => { send: ({ command }) => {
@@ -22,9 +22,9 @@ test("showMpvOsdRuntimeService sends show-text when connected", () => {
assert.deepEqual(commands, [["show-text", "hello", "3000"]]); assert.deepEqual(commands, [["show-text", "hello", "3000"]]);
}); });
test("showMpvOsdRuntimeService logs fallback when disconnected", () => { test("showMpvOsdRuntime logs fallback when disconnected", () => {
const logs: string[] = []; const logs: string[] = [];
showMpvOsdRuntimeService( showMpvOsdRuntime(
{ {
connected: false, connected: false,
send: () => {}, send: () => {},
@@ -55,10 +55,10 @@ test("mpv runtime command wrappers call expected client methods", () => {
}, },
}; };
replayCurrentSubtitleRuntimeService(client); replayCurrentSubtitleRuntime(client);
playNextSubtitleRuntimeService(client); playNextSubtitleRuntime(client);
sendMpvCommandRuntimeService(client, ["script-message", "x"]); sendMpvCommandRuntime(client, ["script-message", "x"]);
setMpvSubVisibilityRuntimeService(client, false); setMpvSubVisibilityRuntime(client, false);
assert.deepEqual(calls, [ assert.deepEqual(calls, [
"replay", "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, patch: Partial<MpvSubtitleRenderMetrics> | null | undefined,
): MpvSubtitleRenderMetrics { ): MpvSubtitleRenderMetrics {
if (!patch) return current; if (!patch) return current;
return updateMpvSubtitleRenderMetricsService(current, patch); return updateMpvSubtitleRenderMetrics(current, patch);
} }
export function updateMpvSubtitleRenderMetricsService( export function updateMpvSubtitleRenderMetrics(
current: MpvSubtitleRenderMetrics, current: MpvSubtitleRenderMetrics,
patch: Partial<MpvSubtitleRenderMetrics>, patch: Partial<MpvSubtitleRenderMetrics>,
): MpvSubtitleRenderMetrics { ): MpvSubtitleRenderMetrics {
@@ -83,11 +83,11 @@ export function updateMpvSubtitleRenderMetricsService(
}; };
} }
export function applyMpvSubtitleRenderMetricsPatchService( export function applyMpvSubtitleRenderMetricsPatch(
current: MpvSubtitleRenderMetrics, current: MpvSubtitleRenderMetrics,
patch: Partial<MpvSubtitleRenderMetrics>, patch: Partial<MpvSubtitleRenderMetrics>,
): { next: MpvSubtitleRenderMetrics; changed: boolean } { ): { next: MpvSubtitleRenderMetrics; changed: boolean } {
const next = updateMpvSubtitleRenderMetricsService(current, patch); const next = updateMpvSubtitleRenderMetrics(current, patch);
const changed = const changed =
next.subPos !== current.subPos || next.subPos !== current.subPos ||
next.subFontSize !== current.subFontSize || next.subFontSize !== current.subFontSize ||

View File

@@ -1,6 +1,6 @@
import test from "node:test"; import test from "node:test";
import assert from "node:assert/strict"; 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", () => { test("resolveCurrentAudioStreamIndex returns selected ff-index when no current track id", () => {
assert.equal( assert.equal(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { globalShortcut } from "electron"; import { globalShortcut } from "electron";
import { ConfiguredShortcuts } from "../utils/shortcut-config"; import { ConfiguredShortcuts } from "../utils/shortcut-config";
import { isGlobalShortcutRegisteredSafe } from "./shortcut-fallback-service"; import { isGlobalShortcutRegisteredSafe } from "./shortcut-fallback";
import { createLogger } from "../../logger"; import { createLogger } from "../../logger";
const logger = createLogger("main:overlay-shortcut-service"); const logger = createLogger("main:overlay-shortcut-service");
@@ -26,7 +26,7 @@ export interface OverlayShortcutLifecycleDeps {
cancelPendingMineSentenceMultiple: () => void; cancelPendingMineSentenceMultiple: () => void;
} }
export function registerOverlayShortcutsService( export function registerOverlayShortcuts(
shortcuts: ConfiguredShortcuts, shortcuts: ConfiguredShortcuts,
handlers: OverlayShortcutHandlers, handlers: OverlayShortcutHandlers,
): boolean { ): boolean {
@@ -140,7 +140,7 @@ export function registerOverlayShortcutsService(
return registeredAny; return registeredAny;
} }
export function unregisterOverlayShortcutsService( export function unregisterOverlayShortcuts(
shortcuts: ConfiguredShortcuts, shortcuts: ConfiguredShortcuts,
): void { ): void {
if (shortcuts.copySubtitle) { if (shortcuts.copySubtitle) {
@@ -178,45 +178,45 @@ export function unregisterOverlayShortcutsService(
} }
} }
export function registerOverlayShortcutsRuntimeService( export function registerOverlayShortcutsRuntime(
deps: OverlayShortcutLifecycleDeps, deps: OverlayShortcutLifecycleDeps,
): boolean { ): boolean {
return registerOverlayShortcutsService( return registerOverlayShortcuts(
deps.getConfiguredShortcuts(), deps.getConfiguredShortcuts(),
deps.getOverlayHandlers(), deps.getOverlayHandlers(),
); );
} }
export function unregisterOverlayShortcutsRuntimeService( export function unregisterOverlayShortcutsRuntime(
shortcutsRegistered: boolean, shortcutsRegistered: boolean,
deps: OverlayShortcutLifecycleDeps, deps: OverlayShortcutLifecycleDeps,
): boolean { ): boolean {
if (!shortcutsRegistered) return shortcutsRegistered; if (!shortcutsRegistered) return shortcutsRegistered;
deps.cancelPendingMultiCopy(); deps.cancelPendingMultiCopy();
deps.cancelPendingMineSentenceMultiple(); deps.cancelPendingMineSentenceMultiple();
unregisterOverlayShortcutsService(deps.getConfiguredShortcuts()); unregisterOverlayShortcuts(deps.getConfiguredShortcuts());
return false; return false;
} }
export function syncOverlayShortcutsRuntimeService( export function syncOverlayShortcutsRuntime(
shouldBeActive: boolean, shouldBeActive: boolean,
shortcutsRegistered: boolean, shortcutsRegistered: boolean,
deps: OverlayShortcutLifecycleDeps, deps: OverlayShortcutLifecycleDeps,
): boolean { ): boolean {
if (shouldBeActive) { if (shouldBeActive) {
return registerOverlayShortcutsRuntimeService(deps); return registerOverlayShortcutsRuntime(deps);
} }
return unregisterOverlayShortcutsRuntimeService(shortcutsRegistered, deps); return unregisterOverlayShortcutsRuntime(shortcutsRegistered, deps);
} }
export function refreshOverlayShortcutsRuntimeService( export function refreshOverlayShortcutsRuntime(
shouldBeActive: boolean, shouldBeActive: boolean,
shortcutsRegistered: boolean, shortcutsRegistered: boolean,
deps: OverlayShortcutLifecycleDeps, deps: OverlayShortcutLifecycleDeps,
): boolean { ): boolean {
const cleared = unregisterOverlayShortcutsRuntimeService( const cleared = unregisterOverlayShortcutsRuntime(
shortcutsRegistered, shortcutsRegistered,
deps, 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 { BaseWindowTracker } from "../../window-trackers";
import { WindowGeometry } from "../../types"; import { WindowGeometry } from "../../types";
export function updateVisibleOverlayVisibilityService(args: { export function updateVisibleOverlayVisibility(args: {
visibleOverlayVisible: boolean; visibleOverlayVisible: boolean;
mainWindow: BrowserWindow | null; mainWindow: BrowserWindow | null;
windowTracker: BaseWindowTracker | null; windowTracker: BaseWindowTracker | null;
@@ -66,7 +66,7 @@ export function updateVisibleOverlayVisibilityService(args: {
args.syncOverlayShortcuts(); args.syncOverlayShortcuts();
} }
export function updateInvisibleOverlayVisibilityService(args: { export function updateInvisibleOverlayVisibility(args: {
invisibleWindow: BrowserWindow | null; invisibleWindow: BrowserWindow | null;
visibleOverlayVisible: boolean; visibleOverlayVisible: boolean;
invisibleOverlayVisible: boolean; invisibleOverlayVisible: boolean;
@@ -131,7 +131,7 @@ export function updateInvisibleOverlayVisibilityService(args: {
args.syncOverlayShortcuts(); args.syncOverlayShortcuts();
} }
export function syncInvisibleOverlayMousePassthroughService(options: { export function syncInvisibleOverlayMousePassthrough(options: {
hasInvisibleWindow: () => boolean; hasInvisibleWindow: () => boolean;
setIgnoreMouseEvents: (ignore: boolean, extra?: { forward: boolean }) => void; setIgnoreMouseEvents: (ignore: boolean, extra?: { forward: boolean }) => void;
visibleOverlayVisible: boolean; visibleOverlayVisible: boolean;
@@ -145,7 +145,7 @@ export function syncInvisibleOverlayMousePassthroughService(options: {
} }
} }
export function setVisibleOverlayVisibleService(options: { export function setVisibleOverlayVisible(options: {
visible: boolean; visible: boolean;
setVisibleOverlayVisibleState: (visible: boolean) => void; setVisibleOverlayVisibleState: (visible: boolean) => void;
updateVisibleOverlayVisibility: () => void; updateVisibleOverlayVisibility: () => void;
@@ -167,7 +167,7 @@ export function setVisibleOverlayVisibleService(options: {
} }
} }
export function setInvisibleOverlayVisibleService(options: { export function setInvisibleOverlayVisible(options: {
visible: boolean; visible: boolean;
setInvisibleOverlayVisibleState: (visible: boolean) => void; setInvisibleOverlayVisibleState: (visible: boolean) => void;
updateInvisibleOverlayVisibility: () => void; updateInvisibleOverlayVisibility: () => void;

View File

@@ -7,7 +7,7 @@ const logger = createLogger("main:overlay-window");
export type OverlayWindowKind = "visible" | "invisible"; export type OverlayWindowKind = "visible" | "invisible";
export function updateOverlayWindowBoundsService( export function updateOverlayWindowBounds(
geometry: WindowGeometry, geometry: WindowGeometry,
window: BrowserWindow | null, window: BrowserWindow | null,
): void { ): void {
@@ -20,7 +20,7 @@ export function updateOverlayWindowBoundsService(
}); });
} }
export function ensureOverlayWindowLevelService(window: BrowserWindow): void { export function ensureOverlayWindowLevel(window: BrowserWindow): void {
if (process.platform === "darwin") { if (process.platform === "darwin") {
window.setAlwaysOnTop(true, "screen-saver", 1); window.setAlwaysOnTop(true, "screen-saver", 1);
window.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); window.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
@@ -30,7 +30,7 @@ export function ensureOverlayWindowLevelService(window: BrowserWindow): void {
window.setAlwaysOnTop(true); window.setAlwaysOnTop(true);
} }
export function enforceOverlayLayerOrderService(options: { export function enforceOverlayLayerOrder(options: {
visibleOverlayVisible: boolean; visibleOverlayVisible: boolean;
invisibleOverlayVisible: boolean; invisibleOverlayVisible: boolean;
mainWindow: BrowserWindow | null; mainWindow: BrowserWindow | null;
@@ -45,7 +45,7 @@ export function enforceOverlayLayerOrderService(options: {
options.mainWindow.moveTop(); options.mainWindow.moveTop();
} }
export function createOverlayWindowService( export function createOverlayWindow(
kind: OverlayWindowKind, kind: OverlayWindowKind,
options: { options: {
isDev: boolean; 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 test from "node:test";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { import {
applyRuntimeOptionResultRuntimeService, applyRuntimeOptionResultRuntime,
cycleRuntimeOptionFromIpcRuntimeService, cycleRuntimeOptionFromIpcRuntime,
setRuntimeOptionFromIpcRuntimeService, setRuntimeOptionFromIpcRuntime,
} from "./runtime-options-ipc-service"; } from "./runtime-options-ipc";
test("applyRuntimeOptionResultRuntimeService emits success OSD message", () => { test("applyRuntimeOptionResultRuntime emits success OSD message", () => {
const osd: string[] = []; const osd: string[] = [];
const result = applyRuntimeOptionResultRuntimeService( const result = applyRuntimeOptionResultRuntime(
{ ok: true, osdMessage: "Updated" }, { ok: true, osdMessage: "Updated" },
(text) => { (text) => {
osd.push(text); osd.push(text);
@@ -19,9 +19,9 @@ test("applyRuntimeOptionResultRuntimeService emits success OSD message", () => {
assert.deepEqual(osd, ["Updated"]); assert.deepEqual(osd, ["Updated"]);
}); });
test("setRuntimeOptionFromIpcRuntimeService returns unavailable when manager missing", () => { test("setRuntimeOptionFromIpcRuntime returns unavailable when manager missing", () => {
const osd: string[] = []; const osd: string[] = [];
const result = setRuntimeOptionFromIpcRuntimeService( const result = setRuntimeOptionFromIpcRuntime(
null, null,
"anki.autoUpdateNewCards", "anki.autoUpdateNewCards",
true, true,
@@ -34,9 +34,9 @@ test("setRuntimeOptionFromIpcRuntimeService returns unavailable when manager mis
assert.deepEqual(osd, []); assert.deepEqual(osd, []);
}); });
test("cycleRuntimeOptionFromIpcRuntimeService reports errors once", () => { test("cycleRuntimeOptionFromIpcRuntime reports errors once", () => {
const osd: string[] = []; const osd: string[] = [];
const result = cycleRuntimeOptionFromIpcRuntimeService( const result = cycleRuntimeOptionFromIpcRuntime(
{ {
setOptionValue: () => ({ ok: true }), setOptionValue: () => ({ ok: true }),
cycleOption: () => ({ ok: false, error: "bad option" }), cycleOption: () => ({ ok: false, error: "bad option" }),

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
import test from "node:test"; import test from "node:test";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { import {
runStartupBootstrapRuntimeService, runStartupBootstrapRuntime,
} from "./startup-service"; } from "./startup";
import { CliArgs } from "../../cli/args"; import { CliArgs } from "../../cli/args";
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs { 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 calls: string[] = [];
const args = makeArgs({ const args = makeArgs({
logLevel: "debug", logLevel: "debug",
@@ -51,7 +51,7 @@ test("runStartupBootstrapRuntimeService configures startup state and starts life
texthooker: true, texthooker: true,
}); });
const result = runStartupBootstrapRuntimeService({ const result = runStartupBootstrapRuntime({
argv: ["node", "main.ts", "--log-level", "debug"], argv: ["node", "main.ts", "--log-level", "debug"],
parseArgs: () => args, parseArgs: () => args,
setLogLevel: (level, source) => calls.push(`setLog:${level}:${source}`), 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 calls: string[] = [];
const args = makeArgs({ const args = makeArgs({
logLevel: "warn", logLevel: "warn",
}); });
runStartupBootstrapRuntimeService({ runStartupBootstrapRuntime({
argv: ["node", "main.ts", "--log-level", "warn"], argv: ["node", "main.ts", "--log-level", "warn"],
parseArgs: () => args, parseArgs: () => args,
setLogLevel: (level, source) => calls.push(`setLog:${level}:${source}`), 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 calls: string[] = [];
const args = makeArgs({ const args = makeArgs({
debug: true, debug: true,
}); });
runStartupBootstrapRuntimeService({ runStartupBootstrapRuntime({
argv: ["node", "main.ts", "--debug"], argv: ["node", "main.ts", "--debug"],
parseArgs: () => args, parseArgs: () => args,
setLogLevel: (level, source) => calls.push(`setLog:${level}:${source}`), 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"]); 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 calls: string[] = [];
const args = makeArgs({ generateConfig: true, logLevel: "warn" }); const args = makeArgs({ generateConfig: true, logLevel: "warn" });
const result = runStartupBootstrapRuntimeService({ const result = runStartupBootstrapRuntime({
argv: ["node", "main.ts", "--generate-config"], argv: ["node", "main.ts", "--generate-config"],
parseArgs: () => args, parseArgs: () => args,
setLogLevel: (level, source) => calls.push(`setLog:${level}:${source}`), setLogLevel: (level, source) => calls.push(`setLog:${level}:${source}`),

View File

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

View File

@@ -4,12 +4,12 @@ import {
SubsyncResult, SubsyncResult,
} from "../../types"; } from "../../types";
import { SubsyncResolvedConfig } from "../../subsync/utils"; import { SubsyncResolvedConfig } from "../../subsync/utils";
import { runSubsyncManualFromIpcService } from "./ipc-command-service"; import { runSubsyncManualFromIpc } from "./ipc-command";
import { import {
TriggerSubsyncFromConfigDeps, TriggerSubsyncFromConfigDeps,
runSubsyncManualService, runSubsyncManual,
triggerSubsyncFromConfigService, triggerSubsyncFromConfig,
} from "./subsync-service"; } from "./subsync";
const AUTOSUBSYNC_SPINNER_FRAMES = ["|", "/", "-", "\\"]; const AUTOSUBSYNC_SPINNER_FRAMES = ["|", "/", "-", "\\"];
@@ -62,24 +62,24 @@ function buildTriggerSubsyncDeps(
}; };
} }
export async function triggerSubsyncFromConfigRuntimeService( export async function triggerSubsyncFromConfigRuntime(
deps: SubsyncRuntimeDeps, deps: SubsyncRuntimeDeps,
): Promise<void> { ): Promise<void> {
await triggerSubsyncFromConfigService(buildTriggerSubsyncDeps(deps)); await triggerSubsyncFromConfig(buildTriggerSubsyncDeps(deps));
} }
export async function runSubsyncManualFromIpcRuntimeService( export async function runSubsyncManualFromIpcRuntime(
request: SubsyncManualRunRequest, request: SubsyncManualRunRequest,
deps: SubsyncRuntimeDeps, deps: SubsyncRuntimeDeps,
): Promise<SubsyncResult> { ): Promise<SubsyncResult> {
const triggerDeps = buildTriggerSubsyncDeps(deps); const triggerDeps = buildTriggerSubsyncDeps(deps);
return runSubsyncManualFromIpcService(request, { return runSubsyncManualFromIpc(request, {
isSubsyncInProgress: triggerDeps.isSubsyncInProgress, isSubsyncInProgress: triggerDeps.isSubsyncInProgress,
setSubsyncInProgress: triggerDeps.setSubsyncInProgress, setSubsyncInProgress: triggerDeps.setSubsyncInProgress,
showMpvOsd: triggerDeps.showMpvOsd, showMpvOsd: triggerDeps.showMpvOsd,
runWithSpinner: (task) => runWithSpinner: (task) =>
triggerDeps.runWithSubsyncSpinner(() => task()), triggerDeps.runWithSubsyncSpinner(() => task()),
runSubsyncManual: (subsyncRequest) => 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 * as path from "path";
import { import {
TriggerSubsyncFromConfigDeps, TriggerSubsyncFromConfigDeps,
runSubsyncManualService, runSubsyncManual,
triggerSubsyncFromConfigService, triggerSubsyncFromConfig,
} from "./subsync-service"; } from "./subsync";
function makeDeps( function makeDeps(
overrides: Partial<TriggerSubsyncFromConfigDeps> = {}, 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[] = []; const osd: string[] = [];
await triggerSubsyncFromConfigService( await triggerSubsyncFromConfig(
makeDeps({ makeDeps({
isSubsyncInProgress: () => true, isSubsyncInProgress: () => true,
showMpvOsd: (text) => { showMpvOsd: (text) => {
@@ -68,12 +68,12 @@ test("triggerSubsyncFromConfigService returns early when already in progress", a
assert.deepEqual(osd, ["Subsync already running"]); 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[] = []; const osd: string[] = [];
let payloadTrackCount = 0; let payloadTrackCount = 0;
let inProgressState: boolean | null = null; let inProgressState: boolean | null = null;
await triggerSubsyncFromConfigService( await triggerSubsyncFromConfig(
makeDeps({ makeDeps({
openManualPicker: (payload) => { openManualPicker: (payload) => {
payloadTrackCount = payload.sourceTracks.length; payloadTrackCount = payload.sourceTracks.length;
@@ -92,9 +92,9 @@ test("triggerSubsyncFromConfigService opens manual picker in manual mode", async
assert.equal(inProgressState, false); assert.equal(inProgressState, false);
}); });
test("triggerSubsyncFromConfigService reports failures to OSD", async () => { test("triggerSubsyncFromConfig reports failures to OSD", async () => {
const osd: string[] = []; const osd: string[] = [];
await triggerSubsyncFromConfigService( await triggerSubsyncFromConfig(
makeDeps({ makeDeps({
getMpvClient: () => null, getMpvClient: () => null,
showMpvOsd: (text) => { 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"))); assert.ok(osd.some((line) => line.startsWith("Subsync failed: MPV not connected")));
}); });
test("runSubsyncManualService requires a source track for alass", async () => { test("runSubsyncManual requires a source track for alass", async () => {
const result = await runSubsyncManualService( const result = await runSubsyncManual(
{ engine: "alass", sourceTrackId: null }, { engine: "alass", sourceTrackId: null },
makeDeps(), 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 osd: string[] = [];
const inProgress: boolean[] = []; const inProgress: boolean[] = [];
await triggerSubsyncFromConfigService( await triggerSubsyncFromConfig(
makeDeps({ makeDeps({
getResolvedConfig: () => ({ getResolvedConfig: () => ({
defaultMode: "auto", defaultMode: "auto",
@@ -152,7 +152,7 @@ function writeExecutableScript(filePath: string, content: string): void {
fs.chmodSync(filePath, 0o755); 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 tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "subsync-ffsubsync-"));
const ffsubsyncLogPath = path.join(tmpDir, "ffsubsync-args.log"); const ffsubsyncLogPath = path.join(tmpDir, "ffsubsync-args.log");
const ffsubsyncPath = path.join(tmpDir, "ffsubsync.sh"); 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 }, { engine: "ffsubsync", sourceTrackId: null },
deps, deps,
); );
@@ -227,7 +227,7 @@ test("runSubsyncManualService constructs ffsubsync command and returns success",
assert.deepEqual(sentCommands[1], ["set_property", "sub-delay", 0]); 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 tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "subsync-alass-"));
const alassLogPath = path.join(tmpDir, "alass-args.log"); const alassLogPath = path.join(tmpDir, "alass-args.log");
const alassPath = path.join(tmpDir, "alass.sh"); 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 }, { engine: "alass", sourceTrackId: 2 },
deps, deps,
); );
@@ -298,7 +298,7 @@ test("runSubsyncManualService constructs alass command and returns failure on no
assert.equal(alassArgs[1], primaryPath); 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 tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "subsync-stream-sid-"));
const ffsubsyncPath = path.join(tmpDir, "ffsubsync.sh"); const ffsubsyncPath = path.join(tmpDir, "ffsubsync.sh");
const ffsubsyncLogPath = path.join(tmpDir, "ffsubsync-args.log"); 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 }, { engine: "ffsubsync", sourceTrackId: null },
deps, deps,
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -85,53 +85,53 @@ import {
} from "./core/utils"; } from "./core/utils";
import { import {
MpvIpcClient, MpvIpcClient,
SubtitleWebSocketService, SubtitleWebSocket,
TexthookerService, Texthooker,
applyMpvSubtitleRenderMetricsPatchService, applyMpvSubtitleRenderMetricsPatch,
broadcastRuntimeOptionsChangedRuntimeService, broadcastRuntimeOptionsChangedRuntime,
copyCurrentSubtitleService, copyCurrentSubtitle as copyCurrentSubtitleCore,
createOverlayManagerService, createOverlayManager,
createFieldGroupingOverlayRuntimeService, createFieldGroupingOverlayRuntime,
createNumericShortcutRuntimeService, createNumericShortcutRuntime,
createOverlayContentMeasurementStoreService, createOverlayContentMeasurementStore,
createOverlayWindowService, createOverlayWindow as createOverlayWindowCore,
createTokenizerDepsRuntimeService, createTokenizerDepsRuntime,
cycleSecondarySubModeService, cycleSecondarySubMode as cycleSecondarySubModeCore,
enforceOverlayLayerOrderService, enforceOverlayLayerOrder as enforceOverlayLayerOrderCore,
ensureOverlayWindowLevelService, ensureOverlayWindowLevel as ensureOverlayWindowLevelCore,
getInitialInvisibleOverlayVisibilityService, getInitialInvisibleOverlayVisibility as getInitialInvisibleOverlayVisibilityCore,
getJimakuLanguagePreferenceService, getJimakuLanguagePreference as getJimakuLanguagePreferenceCore,
getJimakuMaxEntryResultsService, getJimakuMaxEntryResults as getJimakuMaxEntryResultsCore,
handleMineSentenceDigitService, handleMineSentenceDigit as handleMineSentenceDigitCore,
handleMultiCopyDigitService, handleMultiCopyDigit as handleMultiCopyDigitCore,
hasMpvWebsocketPlugin, hasMpvWebsocketPlugin,
initializeOverlayRuntimeService, initializeOverlayRuntime as initializeOverlayRuntimeCore,
isAutoUpdateEnabledRuntimeService, isAutoUpdateEnabledRuntime as isAutoUpdateEnabledRuntimeCore,
jimakuFetchJsonService, jimakuFetchJson as jimakuFetchJsonCore,
loadSubtitlePositionService, loadSubtitlePosition as loadSubtitlePositionCore,
loadYomitanExtensionService, loadYomitanExtension as loadYomitanExtensionCore,
markLastCardAsAudioCardService, markLastCardAsAudioCard as markLastCardAsAudioCardCore,
DEFAULT_MPV_SUBTITLE_RENDER_METRICS, DEFAULT_MPV_SUBTITLE_RENDER_METRICS,
mineSentenceCardService, mineSentenceCard as mineSentenceCardCore,
ImmersionTrackerService, ImmersionTrackerService,
openYomitanSettingsWindow, openYomitanSettingsWindow,
playNextSubtitleRuntimeService, playNextSubtitleRuntime,
registerGlobalShortcutsService, registerGlobalShortcuts as registerGlobalShortcutsCore,
replayCurrentSubtitleRuntimeService, replayCurrentSubtitleRuntime,
resolveJimakuApiKeyService, resolveJimakuApiKey as resolveJimakuApiKeyCore,
runStartupBootstrapRuntimeService, runStartupBootstrapRuntime,
saveSubtitlePositionService, saveSubtitlePosition as saveSubtitlePositionCore,
sendMpvCommandRuntimeService, sendMpvCommandRuntime,
setInvisibleOverlayVisibleService, setInvisibleOverlayVisible as setInvisibleOverlayVisibleCore,
setMpvSubVisibilityRuntimeService, setMpvSubVisibilityRuntime,
setOverlayDebugVisualizationEnabledRuntimeService, setOverlayDebugVisualizationEnabledRuntime,
setVisibleOverlayVisibleService, setVisibleOverlayVisible as setVisibleOverlayVisibleCore,
shouldAutoInitializeOverlayRuntimeFromConfigService, shouldAutoInitializeOverlayRuntimeFromConfig as shouldAutoInitializeOverlayRuntimeFromConfigCore,
shouldBindVisibleOverlayToMpvSubVisibilityService, shouldBindVisibleOverlayToMpvSubVisibility as shouldBindVisibleOverlayToMpvSubVisibilityCore,
showMpvOsdRuntimeService, showMpvOsdRuntime,
tokenizeSubtitleService, tokenizeSubtitle as tokenizeSubtitleCore,
triggerFieldGroupingService, triggerFieldGrouping as triggerFieldGroupingCore,
updateLastCardFromClipboardService, updateLastCardFromClipboard as updateLastCardFromClipboardCore,
} from "./core/services"; } from "./core/services";
import { import {
guessAnilistMediaInfo, guessAnilistMediaInfo,
@@ -140,7 +140,7 @@ import {
} from "./core/services/anilist/anilist-updater"; } from "./core/services/anilist/anilist-updater";
import { createAnilistTokenStore } from "./core/services/anilist/anilist-token-store"; import { createAnilistTokenStore } from "./core/services/anilist/anilist-token-store";
import { createAnilistUpdateQueue } from "./core/services/anilist/anilist-update-queue"; import { createAnilistUpdateQueue } from "./core/services/anilist/anilist-update-queue";
import { applyRuntimeOptionResultRuntimeService } from "./core/services/runtime-options-ipc-service"; import { applyRuntimeOptionResultRuntime } from "./core/services/runtime-options-ipc";
import { import {
createAppReadyRuntimeRunner, createAppReadyRuntimeRunner,
} from "./main/app-lifecycle"; } from "./main/app-lifecycle";
@@ -283,8 +283,8 @@ const anilistUpdateQueue = createAnilistUpdateQueue(
); );
const isDev = const isDev =
process.argv.includes("--dev") || process.argv.includes("--debug"); process.argv.includes("--dev") || process.argv.includes("--debug");
const texthookerService = new TexthookerService(); const texthookerService = new Texthooker();
const subtitleWsService = new SubtitleWebSocketService(); const subtitleWsService = new SubtitleWebSocket();
const logger = createLogger("main"); const logger = createLogger("main");
const appLogger = { const appLogger = {
logInfo: (message: string) => { logInfo: (message: string) => {
@@ -330,8 +330,8 @@ process.on("SIGTERM", () => {
app.quit(); app.quit();
}); });
const overlayManager = createOverlayManagerService(); const overlayManager = createOverlayManager();
const overlayContentMeasurementStore = createOverlayContentMeasurementStoreService({ const overlayContentMeasurementStore = createOverlayContentMeasurementStore({
now: () => Date.now(), now: () => Date.now(),
warn: (message: string) => logger.warn(message), warn: (message: string) => logger.warn(message),
}); });
@@ -460,7 +460,7 @@ function setFieldGroupingResolver(
appState.fieldGroupingResolver = wrappedResolver; appState.fieldGroupingResolver = wrappedResolver;
} }
const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntimeService<OverlayHostedModal>({ const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntime<OverlayHostedModal>({
getMainWindow: () => overlayManager.getMainWindow(), getMainWindow: () => overlayManager.getMainWindow(),
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(), getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(), getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
@@ -548,7 +548,7 @@ function broadcastToOverlayWindows(channel: string, ...args: unknown[]): void {
} }
function broadcastRuntimeOptionsChanged(): void { function broadcastRuntimeOptionsChanged(): void {
broadcastRuntimeOptionsChangedRuntimeService( broadcastRuntimeOptionsChangedRuntime(
() => getRuntimeOptionsState(), () => getRuntimeOptionsState(),
(channel, ...args) => broadcastToOverlayWindows(channel, ...args), (channel, ...args) => broadcastToOverlayWindows(channel, ...args),
); );
@@ -567,7 +567,7 @@ function sendToActiveOverlayWindow(
} }
function setOverlayDebugVisualizationEnabled(enabled: boolean): void { function setOverlayDebugVisualizationEnabled(enabled: boolean): void {
setOverlayDebugVisualizationEnabledRuntimeService( setOverlayDebugVisualizationEnabledRuntime(
appState.overlayDebugVisualizationEnabled, appState.overlayDebugVisualizationEnabled,
enabled, enabled,
(next) => { (next) => {
@@ -646,32 +646,32 @@ async function getCurrentMpvMediaStateForTracker(): Promise<ImmersionMediaState>
} }
function getInitialInvisibleOverlayVisibility(): boolean { function getInitialInvisibleOverlayVisibility(): boolean {
return getInitialInvisibleOverlayVisibilityService( return getInitialInvisibleOverlayVisibilityCore(
getResolvedConfig(), getResolvedConfig(),
process.platform, process.platform,
); );
} }
function shouldAutoInitializeOverlayRuntimeFromConfig(): boolean { function shouldAutoInitializeOverlayRuntimeFromConfig(): boolean {
return shouldAutoInitializeOverlayRuntimeFromConfigService(getResolvedConfig()); return shouldAutoInitializeOverlayRuntimeFromConfigCore(getResolvedConfig());
} }
function shouldBindVisibleOverlayToMpvSubVisibility(): boolean { function shouldBindVisibleOverlayToMpvSubVisibility(): boolean {
return shouldBindVisibleOverlayToMpvSubVisibilityService(getResolvedConfig()); return shouldBindVisibleOverlayToMpvSubVisibilityCore(getResolvedConfig());
} }
function isAutoUpdateEnabledRuntime(): boolean { function isAutoUpdateEnabledRuntime(): boolean {
return isAutoUpdateEnabledRuntimeService( return isAutoUpdateEnabledRuntimeCore(
getResolvedConfig(), getResolvedConfig(),
appState.runtimeOptionsManager, appState.runtimeOptionsManager,
); );
} }
function getJimakuLanguagePreference(): JimakuLanguagePreference { return getJimakuLanguagePreferenceService(() => getResolvedConfig(), DEFAULT_CONFIG.jimaku.languagePreference); } function getJimakuLanguagePreference(): JimakuLanguagePreference { return getJimakuLanguagePreferenceCore(() => getResolvedConfig(), DEFAULT_CONFIG.jimaku.languagePreference); }
function getJimakuMaxEntryResults(): number { return getJimakuMaxEntryResultsService(() => getResolvedConfig(), DEFAULT_CONFIG.jimaku.maxEntryResults); } function getJimakuMaxEntryResults(): number { return getJimakuMaxEntryResultsCore(() => getResolvedConfig(), DEFAULT_CONFIG.jimaku.maxEntryResults); }
async function resolveJimakuApiKey(): Promise<string | null> { return resolveJimakuApiKeyService(() => getResolvedConfig()); } async function resolveJimakuApiKey(): Promise<string | null> { return resolveJimakuApiKeyCore(() => getResolvedConfig()); }
function seedImmersionTrackerFromCurrentMedia(): void { function seedImmersionTrackerFromCurrentMedia(): void {
const tracker = appState.immersionTracker; const tracker = appState.immersionTracker;
@@ -754,7 +754,7 @@ async function jimakuFetchJson<T>(
endpoint: string, endpoint: string,
query: Record<string, string | number | boolean | null | undefined> = {}, query: Record<string, string | number | boolean | null | undefined> = {},
): Promise<JimakuApiResponse<T>> { ): Promise<JimakuApiResponse<T>> {
return jimakuFetchJsonService<T>(endpoint, query, { return jimakuFetchJsonCore<T>(endpoint, query, {
getResolvedConfig: () => getResolvedConfig(), getResolvedConfig: () => getResolvedConfig(),
defaultBaseUrl: DEFAULT_CONFIG.jimaku.apiBaseUrl, defaultBaseUrl: DEFAULT_CONFIG.jimaku.apiBaseUrl,
defaultMaxEntryResults: DEFAULT_CONFIG.jimaku.maxEntryResults, defaultMaxEntryResults: DEFAULT_CONFIG.jimaku.maxEntryResults,
@@ -1193,7 +1193,7 @@ async function maybeRunAnilistPostWatchUpdate(): Promise<void> {
} }
function loadSubtitlePosition(): SubtitlePosition | null { function loadSubtitlePosition(): SubtitlePosition | null {
appState.subtitlePosition = loadSubtitlePositionService({ appState.subtitlePosition = loadSubtitlePositionCore({
currentMediaPath: appState.currentMediaPath, currentMediaPath: appState.currentMediaPath,
fallbackPosition: getResolvedConfig().subtitlePosition, fallbackPosition: getResolvedConfig().subtitlePosition,
subtitlePositionsDir: SUBTITLE_POSITIONS_DIR, subtitlePositionsDir: SUBTITLE_POSITIONS_DIR,
@@ -1203,7 +1203,7 @@ function loadSubtitlePosition(): SubtitlePosition | null {
function saveSubtitlePosition(position: SubtitlePosition): void { function saveSubtitlePosition(position: SubtitlePosition): void {
appState.subtitlePosition = position; appState.subtitlePosition = position;
saveSubtitlePositionService({ saveSubtitlePositionCore({
position, position,
currentMediaPath: appState.currentMediaPath, currentMediaPath: appState.currentMediaPath,
subtitlePositionsDir: SUBTITLE_POSITIONS_DIR, subtitlePositionsDir: SUBTITLE_POSITIONS_DIR,
@@ -1216,7 +1216,7 @@ function saveSubtitlePosition(position: SubtitlePosition): void {
}); });
} }
const startupState = runStartupBootstrapRuntimeService( const startupState = runStartupBootstrapRuntime(
createStartupBootstrapRuntimeDeps({ createStartupBootstrapRuntimeDeps({
argv: process.argv, argv: process.argv,
parseArgs: (argv: string[]) => parseArgs(argv), parseArgs: (argv: string[]) => parseArgs(argv),
@@ -1558,7 +1558,7 @@ function createMpvClientRuntimeService(): MpvIpcClient {
function updateMpvSubtitleRenderMetrics( function updateMpvSubtitleRenderMetrics(
patch: Partial<MpvSubtitleRenderMetrics>, patch: Partial<MpvSubtitleRenderMetrics>,
): void { ): void {
const { next, changed } = applyMpvSubtitleRenderMetricsPatchService( const { next, changed } = applyMpvSubtitleRenderMetricsPatch(
appState.mpvSubtitleRenderMetrics, appState.mpvSubtitleRenderMetrics,
patch, patch,
); );
@@ -1573,9 +1573,9 @@ function updateMpvSubtitleRenderMetrics(
async function tokenizeSubtitle(text: string): Promise<SubtitleData> { async function tokenizeSubtitle(text: string): Promise<SubtitleData> {
await jlptDictionaryRuntime.ensureJlptDictionaryLookup(); await jlptDictionaryRuntime.ensureJlptDictionaryLookup();
await frequencyDictionaryRuntime.ensureFrequencyDictionaryLookup(); await frequencyDictionaryRuntime.ensureFrequencyDictionaryLookup();
return tokenizeSubtitleService( return tokenizeSubtitleCore(
text, text,
createTokenizerDepsRuntimeService({ createTokenizerDepsRuntime({
getYomitanExt: () => appState.yomitanExt, getYomitanExt: () => appState.yomitanExt,
getYomitanParserWindow: () => appState.yomitanParserWindow, getYomitanParserWindow: () => appState.yomitanParserWindow,
setYomitanParserWindow: (window) => { setYomitanParserWindow: (window) => {
@@ -1621,11 +1621,11 @@ function updateInvisibleOverlayBounds(geometry: WindowGeometry): void {
} }
function ensureOverlayWindowLevel(window: BrowserWindow): void { function ensureOverlayWindowLevel(window: BrowserWindow): void {
ensureOverlayWindowLevelService(window); ensureOverlayWindowLevelCore(window);
} }
function enforceOverlayLayerOrder(): void { function enforceOverlayLayerOrder(): void {
enforceOverlayLayerOrderService({ enforceOverlayLayerOrderCore({
visibleOverlayVisible: overlayManager.getVisibleOverlayVisible(), visibleOverlayVisible: overlayManager.getVisibleOverlayVisible(),
invisibleOverlayVisible: overlayManager.getInvisibleOverlayVisible(), invisibleOverlayVisible: overlayManager.getInvisibleOverlayVisible(),
mainWindow: overlayManager.getMainWindow(), mainWindow: overlayManager.getMainWindow(),
@@ -1635,7 +1635,7 @@ function enforceOverlayLayerOrder(): void {
} }
async function loadYomitanExtension(): Promise<Extension | null> { async function loadYomitanExtension(): Promise<Extension | null> {
return loadYomitanExtensionService({ return loadYomitanExtensionCore({
userDataPath: USER_DATA_PATH, userDataPath: USER_DATA_PATH,
getYomitanParserWindow: () => appState.yomitanParserWindow, getYomitanParserWindow: () => appState.yomitanParserWindow,
setYomitanParserWindow: (window) => { setYomitanParserWindow: (window) => {
@@ -1654,7 +1654,7 @@ async function loadYomitanExtension(): Promise<Extension | null> {
} }
function createOverlayWindow(kind: "visible" | "invisible"): BrowserWindow { function createOverlayWindow(kind: "visible" | "invisible"): BrowserWindow {
return createOverlayWindowService( return createOverlayWindowCore(
kind, kind,
{ {
isDev, isDev,
@@ -1695,7 +1695,7 @@ function initializeOverlayRuntime(): void {
if (appState.overlayRuntimeInitialized) { if (appState.overlayRuntimeInitialized) {
return; return;
} }
const result = initializeOverlayRuntimeService( const result = initializeOverlayRuntimeCore(
{ {
backendOverride: appState.backendOverride, backendOverride: appState.backendOverride,
getInitialInvisibleOverlayVisibility: () => getInitialInvisibleOverlayVisibility: () =>
@@ -1759,7 +1759,7 @@ function openYomitanSettings(): void {
); );
} }
function registerGlobalShortcuts(): void { function registerGlobalShortcuts(): void {
registerGlobalShortcutsService( registerGlobalShortcutsCore(
{ {
shortcuts: getConfiguredShortcuts(), shortcuts: getConfiguredShortcuts(),
onToggleVisibleOverlay: () => toggleVisibleOverlay(), onToggleVisibleOverlay: () => toggleVisibleOverlay(),
@@ -1774,7 +1774,7 @@ function registerGlobalShortcuts(): void {
function getConfiguredShortcuts() { return resolveConfiguredShortcuts(getResolvedConfig(), DEFAULT_CONFIG); } function getConfiguredShortcuts() { return resolveConfiguredShortcuts(getResolvedConfig(), DEFAULT_CONFIG); }
function cycleSecondarySubMode(): void { function cycleSecondarySubMode(): void {
cycleSecondarySubModeService( cycleSecondarySubModeCore(
{ {
getSecondarySubMode: () => appState.secondarySubMode, getSecondarySubMode: () => appState.secondarySubMode,
setSecondarySubMode: (mode: SecondarySubMode) => { setSecondarySubMode: (mode: SecondarySubMode) => {
@@ -1794,7 +1794,7 @@ function cycleSecondarySubMode(): void {
function showMpvOsd(text: string): void { function showMpvOsd(text: string): void {
appendToMpvLog(`[OSD] ${text}`); appendToMpvLog(`[OSD] ${text}`);
showMpvOsdRuntimeService( showMpvOsdRuntime(
appState.mpvClient, appState.mpvClient,
text, text,
(line) => { (line) => {
@@ -1816,7 +1816,7 @@ function appendToMpvLog(message: string): void {
} }
} }
const numericShortcutRuntime = createNumericShortcutRuntimeService({ const numericShortcutRuntime = createNumericShortcutRuntime({
globalShortcut, globalShortcut,
showMpvOsd: (text) => showMpvOsd(text), showMpvOsd: (text) => showMpvOsd(text),
setTimer: (handler, timeoutMs) => setTimeout(handler, timeoutMs), setTimer: (handler, timeoutMs) => setTimeout(handler, timeoutMs),
@@ -1863,7 +1863,7 @@ function startPendingMultiCopy(timeoutMs: number): void {
} }
function handleMultiCopyDigit(count: number): void { function handleMultiCopyDigit(count: number): void {
handleMultiCopyDigitService( handleMultiCopyDigitCore(
count, count,
{ {
subtitleTimingTracker: appState.subtitleTimingTracker, subtitleTimingTracker: appState.subtitleTimingTracker,
@@ -1874,7 +1874,7 @@ function handleMultiCopyDigit(count: number): void {
} }
function copyCurrentSubtitle(): void { function copyCurrentSubtitle(): void {
copyCurrentSubtitleService( copyCurrentSubtitleCore(
{ {
subtitleTimingTracker: appState.subtitleTimingTracker, subtitleTimingTracker: appState.subtitleTimingTracker,
writeClipboardText: (text) => clipboard.writeText(text), writeClipboardText: (text) => clipboard.writeText(text),
@@ -1884,7 +1884,7 @@ function copyCurrentSubtitle(): void {
} }
async function updateLastCardFromClipboard(): Promise<void> { async function updateLastCardFromClipboard(): Promise<void> {
await updateLastCardFromClipboardService( await updateLastCardFromClipboardCore(
{ {
ankiIntegration: appState.ankiIntegration, ankiIntegration: appState.ankiIntegration,
readClipboardText: () => clipboard.readText(), readClipboardText: () => clipboard.readText(),
@@ -1902,7 +1902,7 @@ async function refreshKnownWordCache(): Promise<void> {
} }
async function triggerFieldGrouping(): Promise<void> { async function triggerFieldGrouping(): Promise<void> {
await triggerFieldGroupingService( await triggerFieldGroupingCore(
{ {
ankiIntegration: appState.ankiIntegration, ankiIntegration: appState.ankiIntegration,
showMpvOsd: (text) => showMpvOsd(text), showMpvOsd: (text) => showMpvOsd(text),
@@ -1911,7 +1911,7 @@ async function triggerFieldGrouping(): Promise<void> {
} }
async function markLastCardAsAudioCard(): Promise<void> { async function markLastCardAsAudioCard(): Promise<void> {
await markLastCardAsAudioCardService( await markLastCardAsAudioCardCore(
{ {
ankiIntegration: appState.ankiIntegration, ankiIntegration: appState.ankiIntegration,
showMpvOsd: (text) => showMpvOsd(text), showMpvOsd: (text) => showMpvOsd(text),
@@ -1920,7 +1920,7 @@ async function markLastCardAsAudioCard(): Promise<void> {
} }
async function mineSentenceCard(): Promise<void> { async function mineSentenceCard(): Promise<void> {
const created = await mineSentenceCardService( const created = await mineSentenceCardCore(
{ {
ankiIntegration: appState.ankiIntegration, ankiIntegration: appState.ankiIntegration,
mpvClient: appState.mpvClient, mpvClient: appState.mpvClient,
@@ -1949,7 +1949,7 @@ function startPendingMineSentenceMultiple(timeoutMs: number): void {
} }
function handleMineSentenceDigit(count: number): void { function handleMineSentenceDigit(count: number): void {
handleMineSentenceDigitService( handleMineSentenceDigitCore(
count, count,
{ {
subtitleTimingTracker: appState.subtitleTimingTracker, subtitleTimingTracker: appState.subtitleTimingTracker,
@@ -1983,7 +1983,7 @@ function refreshOverlayShortcuts(): void {
} }
function setVisibleOverlayVisible(visible: boolean): void { function setVisibleOverlayVisible(visible: boolean): void {
setVisibleOverlayVisibleService({ setVisibleOverlayVisibleCore({
visible, visible,
setVisibleOverlayVisibleState: (nextVisible) => { setVisibleOverlayVisibleState: (nextVisible) => {
overlayManager.setVisibleOverlayVisible(nextVisible); overlayManager.setVisibleOverlayVisible(nextVisible);
@@ -1998,13 +1998,13 @@ function setVisibleOverlayVisible(visible: boolean): void {
shouldBindVisibleOverlayToMpvSubVisibility(), shouldBindVisibleOverlayToMpvSubVisibility(),
isMpvConnected: () => Boolean(appState.mpvClient && appState.mpvClient.connected), isMpvConnected: () => Boolean(appState.mpvClient && appState.mpvClient.connected),
setMpvSubVisibility: (mpvSubVisible) => { setMpvSubVisibility: (mpvSubVisible) => {
setMpvSubVisibilityRuntimeService(appState.mpvClient, mpvSubVisible); setMpvSubVisibilityRuntime(appState.mpvClient, mpvSubVisible);
}, },
}); });
} }
function setInvisibleOverlayVisible(visible: boolean): void { function setInvisibleOverlayVisible(visible: boolean): void {
setInvisibleOverlayVisibleService({ setInvisibleOverlayVisibleCore({
visible, visible,
setInvisibleOverlayVisibleState: (nextVisible) => { setInvisibleOverlayVisibleState: (nextVisible) => {
overlayManager.setInvisibleOverlayVisible(nextVisible); overlayManager.setInvisibleOverlayVisible(nextVisible);
@@ -2036,16 +2036,16 @@ function handleMpvCommandFromIpc(command: (string | number)[]): void {
if (!appState.runtimeOptionsManager) { if (!appState.runtimeOptionsManager) {
return { ok: false, error: "Runtime options manager unavailable" }; return { ok: false, error: "Runtime options manager unavailable" };
} }
return applyRuntimeOptionResultRuntimeService( return applyRuntimeOptionResultRuntime(
appState.runtimeOptionsManager.cycleOption(id, direction), appState.runtimeOptionsManager.cycleOption(id, direction),
(text) => showMpvOsd(text), (text) => showMpvOsd(text),
); );
}, },
showMpvOsd: (text: string) => showMpvOsd(text), showMpvOsd: (text: string) => showMpvOsd(text),
replayCurrentSubtitle: () => replayCurrentSubtitleRuntimeService(appState.mpvClient), replayCurrentSubtitle: () => replayCurrentSubtitleRuntime(appState.mpvClient),
playNextSubtitle: () => playNextSubtitleRuntimeService(appState.mpvClient), playNextSubtitle: () => playNextSubtitleRuntime(appState.mpvClient),
sendMpvCommand: (rawCommand: (string | number)[]) => sendMpvCommand: (rawCommand: (string | number)[]) =>
sendMpvCommandRuntimeService(appState.mpvClient, rawCommand), sendMpvCommandRuntime(appState.mpvClient, rawCommand),
isMpvConnected: () => Boolean(appState.mpvClient && appState.mpvClient.connected), isMpvConnected: () => Boolean(appState.mpvClient && appState.mpvClient.connected),
hasRuntimeOptionsManager: () => appState.runtimeOptionsManager !== null, hasRuntimeOptionsManager: () => appState.runtimeOptionsManager !== null,
}); });

View File

@@ -1,7 +1,7 @@
import type { CliArgs, CliCommandSource } from "../cli/args"; import type { CliArgs, CliCommandSource } from "../cli/args";
import { runAppReadyRuntimeService } from "../core/services/startup-service"; import { runAppReadyRuntime } from "../core/services/startup";
import type { AppReadyRuntimeDeps } from "../core/services/startup-service"; import type { AppReadyRuntimeDeps } from "../core/services/startup";
import type { AppLifecycleDepsRuntimeOptions } from "../core/services/app-lifecycle-service"; import type { AppLifecycleDepsRuntimeOptions } from "../core/services/app-lifecycle";
export interface AppLifecycleRuntimeDepsFactoryInput { export interface AppLifecycleRuntimeDepsFactoryInput {
app: AppLifecycleDepsRuntimeOptions["app"]; app: AppLifecycleDepsRuntimeOptions["app"];
@@ -96,6 +96,6 @@ export function createAppReadyRuntimeRunner(
params: AppReadyRuntimeDepsFactoryInput, params: AppReadyRuntimeDepsFactoryInput,
): () => Promise<void> { ): () => Promise<void> {
return async () => { return async () => {
await runAppReadyRuntimeService(createAppReadyRuntimeDeps(params)); await runAppReadyRuntime(createAppReadyRuntimeDeps(params));
}; };
} }

View File

@@ -1,4 +1,4 @@
import { handleCliCommandService, createCliCommandDepsRuntimeService } from "../core/services"; import { handleCliCommand, createCliCommandDepsRuntime } from "../core/services";
import type { CliArgs, CliCommandSource } from "../cli/args"; import type { CliArgs, CliCommandSource } from "../cli/args";
import { createCliCommandRuntimeServiceDeps, CliCommandRuntimeServiceDepsParams } from "./dependencies"; import { createCliCommandRuntimeServiceDeps, CliCommandRuntimeServiceDepsParams } from "./dependencies";
@@ -102,10 +102,10 @@ export function handleCliCommandRuntimeService(
source: CliCommandSource, source: CliCommandSource,
params: CliCommandRuntimeServiceDepsParams, params: CliCommandRuntimeServiceDepsParams,
): void { ): void {
const deps = createCliCommandDepsRuntimeService( const deps = createCliCommandDepsRuntime(
createCliCommandRuntimeServiceDeps(params), createCliCommandRuntimeServiceDeps(params),
); );
handleCliCommandService(args, source, deps); handleCliCommand(args, source, deps);
} }
export function handleCliCommandRuntimeServiceWithContext( export function handleCliCommandRuntimeServiceWithContext(

View File

@@ -4,15 +4,15 @@ import {
SubsyncManualPayload, SubsyncManualPayload,
} from "../types"; } from "../types";
import { SubsyncResolvedConfig } from "../subsync/utils"; import { SubsyncResolvedConfig } from "../subsync/utils";
import type { SubsyncRuntimeDeps } from "../core/services/subsync-runner-service"; import type { SubsyncRuntimeDeps } from "../core/services/subsync-runner";
import type { IpcDepsRuntimeOptions } from "../core/services/ipc-service"; import type { IpcDepsRuntimeOptions } from "../core/services/ipc";
import type { AnkiJimakuIpcRuntimeOptions } from "../core/services/anki-jimaku-service"; import type { AnkiJimakuIpcRuntimeOptions } from "../core/services/anki-jimaku";
import type { CliCommandDepsRuntimeOptions } from "../core/services/cli-command-service"; import type { CliCommandDepsRuntimeOptions } from "../core/services/cli-command";
import type { HandleMpvCommandFromIpcOptions } from "../core/services/ipc-command-service"; import type { HandleMpvCommandFromIpcOptions } from "../core/services/ipc-command";
import { import {
cycleRuntimeOptionFromIpcRuntimeService, cycleRuntimeOptionFromIpcRuntime,
setRuntimeOptionFromIpcRuntimeService, setRuntimeOptionFromIpcRuntime,
} from "../core/services/runtime-options-ipc-service"; } from "../core/services/runtime-options-ipc";
import { RuntimeOptionsManager } from "../runtime-options"; import { RuntimeOptionsManager } from "../runtime-options";
export interface RuntimeOptionsIpcDepsParams { export interface RuntimeOptionsIpcDepsParams {
@@ -35,14 +35,14 @@ export function createRuntimeOptionsIpcDeps(params: RuntimeOptionsIpcDepsParams)
} { } {
return { return {
setRuntimeOption: (id, value) => setRuntimeOption: (id, value) =>
setRuntimeOptionFromIpcRuntimeService( setRuntimeOptionFromIpcRuntime(
params.getRuntimeOptionsManager(), params.getRuntimeOptionsManager(),
id as RuntimeOptionId, id as RuntimeOptionId,
value as RuntimeOptionValue, value as RuntimeOptionValue,
(text) => params.showMpvOsd(text), (text) => params.showMpvOsd(text),
), ),
cycleRuntimeOption: (id, direction) => cycleRuntimeOption: (id, direction) =>
cycleRuntimeOptionFromIpcRuntimeService( cycleRuntimeOptionFromIpcRuntime(
params.getRuntimeOptionsManager(), params.getRuntimeOptionsManager(),
id as RuntimeOptionId, id as RuntimeOptionId,
direction, direction,

View File

@@ -1,6 +1,6 @@
import * as path from "path"; import * as path from "path";
import type { FrequencyDictionaryLookup } from "../types"; import type { FrequencyDictionaryLookup } from "../types";
import { createFrequencyDictionaryLookupService } from "../core/services"; import { createFrequencyDictionaryLookup } from "../core/services";
export interface FrequencyDictionarySearchPathDeps { export interface FrequencyDictionarySearchPathDeps {
getDictionaryRoots: () => string[]; getDictionaryRoots: () => string[];
@@ -47,7 +47,7 @@ export function getFrequencyDictionarySearchPaths(
export async function initializeFrequencyDictionaryLookup( export async function initializeFrequencyDictionaryLookup(
deps: FrequencyDictionaryRuntimeDeps, deps: FrequencyDictionaryRuntimeDeps,
): Promise<void> { ): Promise<void> {
const lookup = await createFrequencyDictionaryLookupService({ const lookup = await createFrequencyDictionaryLookup({
searchPaths: deps.getSearchPaths(), searchPaths: deps.getSearchPaths(),
log: deps.log, log: deps.log,
}); });

View File

@@ -1,5 +1,5 @@
import type { RuntimeOptionApplyResult, RuntimeOptionId } from "../types"; import type { RuntimeOptionApplyResult, RuntimeOptionId } from "../types";
import { handleMpvCommandFromIpcService } from "../core/services"; import { handleMpvCommandFromIpc } from "../core/services";
import { createMpvCommandRuntimeServiceDeps } from "./dependencies"; import { createMpvCommandRuntimeServiceDeps } from "./dependencies";
import { SPECIAL_COMMANDS } from "../config"; import { SPECIAL_COMMANDS } from "../config";
@@ -22,7 +22,7 @@ export function handleMpvCommandFromIpcRuntime(
command: (string | number)[], command: (string | number)[],
deps: MpvCommandFromIpcRuntimeDeps, deps: MpvCommandFromIpcRuntimeDeps,
): void { ): void {
handleMpvCommandFromIpcService( handleMpvCommandFromIpc(
command, command,
createMpvCommandRuntimeServiceDeps({ createMpvCommandRuntimeServiceDeps({
specialCommands: SPECIAL_COMMANDS, specialCommands: SPECIAL_COMMANDS,

View File

@@ -1,9 +1,9 @@
import { import {
createIpcDepsRuntimeService, createIpcDepsRuntime,
registerAnkiJimakuIpcRuntimeService, registerAnkiJimakuIpcRuntime,
registerIpcHandlersService, registerIpcHandlers,
} from "../core/services"; } from "../core/services";
import { registerAnkiJimakuIpcHandlers } from "../core/services/anki-jimaku-ipc-service"; import { registerAnkiJimakuIpcHandlers } from "../core/services/anki-jimaku-ipc";
import { import {
createAnkiJimakuIpcRuntimeServiceDeps, createAnkiJimakuIpcRuntimeServiceDeps,
AnkiJimakuIpcRuntimeServiceDepsParams, AnkiJimakuIpcRuntimeServiceDepsParams,
@@ -25,15 +25,15 @@ export interface RegisterIpcRuntimeServicesParams {
export function registerMainIpcRuntimeServices( export function registerMainIpcRuntimeServices(
params: MainIpcRuntimeServiceDepsParams, params: MainIpcRuntimeServiceDepsParams,
): void { ): void {
registerIpcHandlersService( registerIpcHandlers(
createIpcDepsRuntimeService(createMainIpcRuntimeServiceDeps(params)), createIpcDepsRuntime(createMainIpcRuntimeServiceDeps(params)),
); );
} }
export function registerAnkiJimakuIpcRuntimeServices( export function registerAnkiJimakuIpcRuntimeServices(
params: AnkiJimakuIpcRuntimeServiceDepsParams, params: AnkiJimakuIpcRuntimeServiceDepsParams,
): void { ): void {
registerAnkiJimakuIpcRuntimeService( registerAnkiJimakuIpcRuntime(
createAnkiJimakuIpcRuntimeServiceDeps(params), createAnkiJimakuIpcRuntimeServiceDeps(params),
registerAnkiJimakuIpcHandlers, registerAnkiJimakuIpcHandlers,
); );

View File

@@ -1,7 +1,7 @@
import * as path from "path"; import * as path from "path";
import type { JlptLevel } from "../types"; import type { JlptLevel } from "../types";
import { createJlptVocabularyLookupService } from "../core/services"; import { createJlptVocabularyLookup } from "../core/services";
export interface JlptDictionarySearchPathDeps { export interface JlptDictionarySearchPathDeps {
getDictionaryRoots: () => string[]; getDictionaryRoots: () => string[];
@@ -39,7 +39,7 @@ export async function initializeJlptDictionaryLookup(
deps: JlptDictionaryRuntimeDeps, deps: JlptDictionaryRuntimeDeps,
): Promise<void> { ): Promise<void> {
deps.setJlptLevelLookup( deps.setJlptLevelLookup(
await createJlptVocabularyLookupService({ await createJlptVocabularyLookup({
searchPaths: deps.getSearchPaths(), searchPaths: deps.getSearchPaths(),
log: deps.log, log: deps.log,
}), }),

View File

@@ -1,4 +1,4 @@
import { updateCurrentMediaPathService } from "../core/services"; import { updateCurrentMediaPath } from "../core/services";
import type { SubtitlePosition } from "../types"; import type { SubtitlePosition } from "../types";
@@ -31,7 +31,7 @@ export function createMediaRuntimeService(
deps.setCurrentMediaTitle(null); deps.setCurrentMediaTitle(null);
} }
updateCurrentMediaPathService({ updateCurrentMediaPath({
mediaPath, mediaPath,
currentMediaPath: deps.getCurrentMediaPath(), currentMediaPath: deps.getCurrentMediaPath(),
pendingSubtitlePosition: deps.getPendingSubtitlePosition(), pendingSubtitlePosition: deps.getPendingSubtitlePosition(),

View File

@@ -4,10 +4,10 @@ import {
shortcutMatchesInputForLocalFallback, shortcutMatchesInputForLocalFallback,
} from "../core/services"; } from "../core/services";
import { import {
refreshOverlayShortcutsRuntimeService, refreshOverlayShortcutsRuntime,
registerOverlayShortcutsService, registerOverlayShortcuts,
syncOverlayShortcutsRuntimeService, syncOverlayShortcutsRuntime,
unregisterOverlayShortcutsRuntimeService, unregisterOverlayShortcutsRuntime,
} from "../core/services"; } from "../core/services";
import { runOverlayShortcutLocalFallback } from "../core/services/overlay-shortcut-handler"; import { runOverlayShortcutLocalFallback } from "../core/services/overlay-shortcut-handler";
@@ -102,7 +102,7 @@ export function createOverlayShortcutsRuntimeService(
), ),
registerOverlayShortcuts: () => { registerOverlayShortcuts: () => {
input.setShortcutsRegistered( input.setShortcutsRegistered(
registerOverlayShortcutsService( registerOverlayShortcuts(
input.getConfiguredShortcuts(), input.getConfiguredShortcuts(),
handlers.overlayHandlers, handlers.overlayHandlers,
), ),
@@ -110,7 +110,7 @@ export function createOverlayShortcutsRuntimeService(
}, },
unregisterOverlayShortcuts: () => { unregisterOverlayShortcuts: () => {
input.setShortcutsRegistered( input.setShortcutsRegistered(
unregisterOverlayShortcutsRuntimeService( unregisterOverlayShortcutsRuntime(
input.getShortcutsRegistered(), input.getShortcutsRegistered(),
getShortcutLifecycleDeps(), getShortcutLifecycleDeps(),
), ),
@@ -118,7 +118,7 @@ export function createOverlayShortcutsRuntimeService(
}, },
syncOverlayShortcuts: () => { syncOverlayShortcuts: () => {
input.setShortcutsRegistered( input.setShortcutsRegistered(
syncOverlayShortcutsRuntimeService( syncOverlayShortcutsRuntime(
shouldOverlayShortcutsBeActive(), shouldOverlayShortcutsBeActive(),
input.getShortcutsRegistered(), input.getShortcutsRegistered(),
getShortcutLifecycleDeps(), getShortcutLifecycleDeps(),
@@ -127,7 +127,7 @@ export function createOverlayShortcutsRuntimeService(
}, },
refreshOverlayShortcuts: () => { refreshOverlayShortcuts: () => {
input.setShortcutsRegistered( input.setShortcutsRegistered(
refreshOverlayShortcutsRuntimeService( refreshOverlayShortcutsRuntime(
shouldOverlayShortcutsBeActive(), shouldOverlayShortcutsBeActive(),
input.getShortcutsRegistered(), input.getShortcutsRegistered(),
getShortcutLifecycleDeps(), getShortcutLifecycleDeps(),

View File

@@ -3,9 +3,9 @@ import type { BrowserWindow } from "electron";
import type { BaseWindowTracker } from "../window-trackers"; import type { BaseWindowTracker } from "../window-trackers";
import type { WindowGeometry } from "../types"; import type { WindowGeometry } from "../types";
import { import {
syncInvisibleOverlayMousePassthroughService, syncInvisibleOverlayMousePassthrough,
updateInvisibleOverlayVisibilityService, updateInvisibleOverlayVisibility,
updateVisibleOverlayVisibilityService, updateVisibleOverlayVisibility,
} from "../core/services"; } from "../core/services";
export interface OverlayVisibilityRuntimeDeps { export interface OverlayVisibilityRuntimeDeps {
@@ -48,7 +48,7 @@ export function createOverlayVisibilityRuntimeService(
return { return {
updateVisibleOverlayVisibility(): void { updateVisibleOverlayVisibility(): void {
updateVisibleOverlayVisibilityService({ updateVisibleOverlayVisibility({
visibleOverlayVisible: deps.getVisibleOverlayVisible(), visibleOverlayVisible: deps.getVisibleOverlayVisible(),
mainWindow: deps.getMainWindow(), mainWindow: deps.getMainWindow(),
windowTracker: deps.getWindowTracker(), windowTracker: deps.getWindowTracker(),
@@ -66,7 +66,7 @@ export function createOverlayVisibilityRuntimeService(
}, },
updateInvisibleOverlayVisibility(): void { updateInvisibleOverlayVisibility(): void {
updateInvisibleOverlayVisibilityService({ updateInvisibleOverlayVisibility({
invisibleWindow: deps.getInvisibleWindow(), invisibleWindow: deps.getInvisibleWindow(),
visibleOverlayVisible: deps.getVisibleOverlayVisible(), visibleOverlayVisible: deps.getVisibleOverlayVisible(),
invisibleOverlayVisible: deps.getInvisibleOverlayVisible(), invisibleOverlayVisible: deps.getInvisibleOverlayVisible(),
@@ -81,7 +81,7 @@ export function createOverlayVisibilityRuntimeService(
}, },
syncInvisibleOverlayMousePassthrough(): void { syncInvisibleOverlayMousePassthrough(): void {
syncInvisibleOverlayMousePassthroughService({ syncInvisibleOverlayMousePassthrough({
hasInvisibleWindow, hasInvisibleWindow,
setIgnoreMouseEvents, setIgnoreMouseEvents,
visibleOverlayVisible: deps.getVisibleOverlayVisible(), visibleOverlayVisible: deps.getVisibleOverlayVisible(),

View File

@@ -1,7 +1,7 @@
import { CliArgs, CliCommandSource } from "../cli/args"; import { CliArgs, CliCommandSource } from "../cli/args";
import { createAppLifecycleDepsRuntimeService } from "../core/services"; import { createAppLifecycleDepsRuntime } from "../core/services";
import { startAppLifecycleService } from "../core/services/app-lifecycle-service"; import { startAppLifecycle } from "../core/services/app-lifecycle";
import type { AppLifecycleDepsRuntimeOptions } from "../core/services/app-lifecycle-service"; import type { AppLifecycleDepsRuntimeOptions } from "../core/services/app-lifecycle";
import { createAppLifecycleRuntimeDeps } from "./app-lifecycle"; import { createAppLifecycleRuntimeDeps } from "./app-lifecycle";
export interface AppLifecycleRuntimeRunnerParams { export interface AppLifecycleRuntimeRunnerParams {
@@ -22,9 +22,9 @@ export function createAppLifecycleRuntimeRunner(
params: AppLifecycleRuntimeRunnerParams, params: AppLifecycleRuntimeRunnerParams,
): (args: CliArgs) => void { ): (args: CliArgs) => void {
return (args: CliArgs): void => { return (args: CliArgs): void => {
startAppLifecycleService( startAppLifecycle(
args, args,
createAppLifecycleDepsRuntimeService( createAppLifecycleDepsRuntime(
createAppLifecycleRuntimeDeps({ createAppLifecycleRuntimeDeps({
app: params.app, app: params.app,
platform: params.platform, platform: params.platform,

View File

@@ -1,6 +1,6 @@
import { CliArgs } from "../cli/args"; import { CliArgs } from "../cli/args";
import type { ResolvedConfig } from "../types"; import type { ResolvedConfig } from "../types";
import type { StartupBootstrapRuntimeDeps } from "../core/services/startup-service"; import type { StartupBootstrapRuntimeDeps } from "../core/services/startup";
import type { LogLevelSource } from "../logger"; import type { LogLevelSource } from "../logger";
export interface StartupBootstrapRuntimeFactoryDeps { export interface StartupBootstrapRuntimeFactoryDeps {

View File

@@ -1,8 +1,11 @@
import { SubsyncResolvedConfig } from "../subsync/utils"; import { SubsyncResolvedConfig } from "../subsync/utils";
import type { SubsyncManualPayload, SubsyncManualRunRequest, SubsyncResult } from "../types"; import type { SubsyncManualPayload, SubsyncManualRunRequest, SubsyncResult } from "../types";
import type { SubsyncRuntimeDeps } from "../core/services/subsync-runner-service"; import type { SubsyncRuntimeDeps } from "../core/services/subsync-runner";
import { createSubsyncRuntimeDeps } from "./dependencies"; import { createSubsyncRuntimeDeps } from "./dependencies";
import { runSubsyncManualFromIpcRuntimeService, triggerSubsyncFromConfigRuntimeService } from "../core/services"; import {
runSubsyncManualFromIpcRuntime as runSubsyncManualFromIpcRuntimeCore,
triggerSubsyncFromConfigRuntime as triggerSubsyncFromConfigRuntimeCore,
} from "../core/services";
export interface SubsyncRuntimeServiceInput { export interface SubsyncRuntimeServiceInput {
getMpvClient: SubsyncRuntimeDeps["getMpvClient"]; getMpvClient: SubsyncRuntimeDeps["getMpvClient"];
@@ -51,14 +54,14 @@ export function createSubsyncRuntimeServiceDeps(
export function triggerSubsyncFromConfigRuntime( export function triggerSubsyncFromConfigRuntime(
params: SubsyncRuntimeServiceInput, params: SubsyncRuntimeServiceInput,
): Promise<void> { ): Promise<void> {
return triggerSubsyncFromConfigRuntimeService(createSubsyncRuntimeServiceDeps(params)); return triggerSubsyncFromConfigRuntimeCore(createSubsyncRuntimeServiceDeps(params));
} }
export async function runSubsyncManualFromIpcRuntime( export async function runSubsyncManualFromIpcRuntime(
request: SubsyncManualRunRequest, request: SubsyncManualRunRequest,
params: SubsyncRuntimeServiceInput, params: SubsyncRuntimeServiceInput,
): Promise<SubsyncResult> { ): Promise<SubsyncResult> {
return runSubsyncManualFromIpcRuntimeService( return runSubsyncManualFromIpcRuntimeCore(
request, request,
createSubsyncRuntimeServiceDeps(params), createSubsyncRuntimeServiceDeps(params),
); );