# Architecture This page is a contributor-facing architecture summary. Canonical internal architecture guidance lives in `docs/architecture/README.md` at the repo root. SubMiner is split into three cooperating runtimes: - Electron desktop app (`src/`) for overlay/UI/runtime orchestration. - Launcher CLI (`launcher/`) for mpv/app command workflows. - mpv Lua plugin (`plugin/subminer/init.lua` + module files) for player-side controls and IPC handoff. Within the desktop app, `src/main.ts` is a composition root that wires small runtime/domain modules plus core services. ## Goals - Keep behavior stable while reducing coupling. - Prefer small, single-purpose units that can be tested in isolation. - Keep `main.ts` focused on wiring and state ownership, not implementation detail. - Follow Unix-style composability: - each service does one job - services compose through explicit inputs/outputs - orchestration is separate from implementation ## Project Structure ```text launcher/ # Standalone CLI launcher wrapper and mpv helpers commands/ # Command modules (doctor/config/mpv/jellyfin/playback/app passthrough) config/ # Launcher config parsers + CLI parser builder main.ts # Launcher entrypoint and command dispatch plugin/ subminer/ # Modular mpv plugin (init · main · bootstrap · lifecycle · process # state · messages · hover · ui · options · environment · log # binary · aniskip · aniskip_match) src/ ai/ # AI translation provider utilities (client, config) main-entry.ts # Background-mode bootstrap wrapper before loading main.js main.ts # Entry point — delegates to runtime composers/domain modules preload.ts # Electron preload bridge types.ts # Shared type definitions main/ # Main-process composition/runtime adapters app-lifecycle.ts # App lifecycle + app-ready runtime runner factories cli-runtime.ts # CLI command runtime service adapters config-validation.ts # Startup/hot-reload config error formatting and fail-fast helpers dependencies.ts # Shared dependency builders for IPC/runtime services ipc-runtime.ts # IPC runtime registration wrappers overlay-runtime.ts # Overlay modal routing + active-window selection overlay-shortcuts-runtime.ts # Overlay keyboard shortcut handling overlay-visibility-runtime.ts # Overlay visibility + tracker-driven bounds service frequency-dictionary-runtime.ts # Frequency dictionary runtime adapter jlpt-runtime.ts # JLPT dictionary runtime adapter media-runtime.ts # Media path/title/subtitle-position runtime service startup.ts # Startup bootstrap dependency builder startup-lifecycle.ts # Lifecycle runtime runner adapter state.ts # Application runtime state container + reducer transitions subsync-runtime.ts # Subsync command runtime adapter runtime/ composers/ # High-level composition clusters used by main.ts domains/ # Domain barrel exports (startup/overlay/mpv/jellyfin/...) registry.ts # Domain registry consumed by main.ts core/ services/ # Focused runtime services (Electron adapters + pure logic) anilist/ # AniList token store/update queue/update helpers immersion-tracker/ # Immersion persistence/session/metadata modules tokenizer/ # Tokenizer stage modules (selection/enrichment/annotation) utils/ # Pure helpers and coercion/config utilities cli/ # CLI parsing and help output config/ # Config defaults/definitions, loading, parse, resolution pipeline definitions/ # Domain-specific defaults + option registries resolve/ # Domain-specific config resolution pipeline stages shared/ipc/ # Cross-process IPC channel constants + payload validators renderer/ # Overlay renderer (modularized UI/runtime) handlers/ # Keyboard/mouse interaction modules modals/ # Jimaku/Kiku/subsync/runtime-options/session-help modals positioning/ # Subtitle position controller (drag-to-reposition) window-trackers/ # Backend-specific tracker implementations (Hyprland, Sway, X11, macOS) jimaku/ # Jimaku API integration helpers subsync/ # Subtitle sync (alass/ffsubsync) helpers subtitle/ # Subtitle processing utilities tokenizers/ # Tokenizer implementations anki-integration/ # AnkiConnect proxy server + note-update enrichment workflow token-mergers/ # Token merge strategies translators/ # AI translation providers ``` ### Service Layer (`src/core/services/`) - **Overlay/window runtime:** `overlay-manager.ts`, `overlay-window.ts`, `overlay-visibility.ts`, `overlay-bridge.ts`, `overlay-runtime-init.ts`, `overlay-content-measurement.ts` - **Shortcuts/input:** `shortcut.ts`, `overlay-shortcut.ts`, `overlay-shortcut-handler.ts`, `shortcut-fallback.ts`, `numeric-shortcut.ts` - **MPV runtime:** `mpv.ts`, `mpv-transport.ts`, `mpv-protocol.ts`, `mpv-properties.ts`, `mpv-render-metrics.ts` - **Mining + Anki/Jimaku runtime:** `mining.ts`, `field-grouping.ts`, `field-grouping-overlay.ts`, `anki-jimaku.ts`, `anki-jimaku-ipc.ts` - **Subtitle/token pipeline:** `subtitle-processing-controller.ts`, `subtitle-position.ts`, `subtitle-ws.ts`, `tokenizer.ts` + `tokenizer/*` stage modules (including `parser-enrichment-worker-runtime.ts` for async MeCab enrichment and `yomitan-parser-runtime.ts`) - **Integrations:** `jimaku.ts`, `subsync.ts`, `subsync-runner.ts`, `texthooker.ts`, `jellyfin.ts`, `jellyfin-remote.ts`, `discord-presence.ts`, `yomitan-extension-loader.ts`, `yomitan-settings.ts` - **Anki integration:** `anki-integration.ts`, `anki-integration/anki-connect-proxy.ts` (local proxy for push-based auto-enrichment), `anki-integration/note-update-workflow.ts` - **Config/runtime controls:** `config-hot-reload.ts`, `runtime-options-ipc.ts`, `cli-command.ts`, `startup.ts` - **Domain submodules:** `anilist/*` (token/update queue/updater), `immersion-tracker/*` (storage/session/metadata/query/reducer) ### Renderer Layer (`src/renderer/`) The renderer keeps `renderer.ts` focused on orchestration. UI behavior is delegated to per-concern modules. ```text src/renderer/ renderer.ts # Entrypoint/orchestration only context.ts # Shared runtime context contract state.ts # Centralized renderer mutable state (visible overlay only) error-recovery.ts # Global renderer error boundary + recovery actions overlay-content-measurement.ts # Reports rendered bounds to main process subtitle-render.ts # Primary/secondary subtitle rendering + style application positioning.ts # Facade export for positioning controller yomitan-popup.ts # Yomitan popup iframe detection utilities positioning/ controller.ts # Subtitle drag-position controller position-state.ts # Position state helpers (yPercent) handlers/ keyboard.ts # Keybindings, chord handling, modal key routing mouse.ts # Hover/drag behavior, selection + observer wiring modals/ jimaku.ts # Jimaku modal flow kiku.ts # Kiku field-grouping modal flow runtime-options.ts # Runtime options modal flow session-help.ts # Keyboard shortcuts/help modal flow subsync.ts # Manual subsync modal flow utils/ dom.ts # Required DOM lookups + typed handles platform.ts # Layer/platform capability detection ``` ### Launcher + Plugin Runtimes - `launcher/main.ts` dispatches commands through `launcher/commands/*` and shared config readers in `launcher/config/*`. It handles mpv startup, app passthrough, Jellyfin helper commands, and playback handoff. - `plugin/subminer/init.lua` runs inside mpv and loads modular Lua files: `main.lua` (orchestration), `bootstrap.lua` (startup), `lifecycle.lua` (connect/disconnect), `process.lua` (process management), `state.lua` (shared state), `messages.lua` (IPC), `hover.lua` (hover-token highlight rendering), `ui.lua` (OSD rendering), `options.lua` (config), `environment.lua` (detection), `log.lua` (logging), `binary.lua` (path resolution), `aniskip.lua` + `aniskip_match.lua` (intro-skip UX). ## Flow Diagram The main process orchestrates a single primary overlay window plus modal surfaces: `main.ts` delegates to composition modules that wire together domain services. Subtitle layers (primary + secondary bar) are rendered in the same overlay renderer process, connected through `preload.ts`. External runtimes (launcher CLI and mpv plugin) operate independently and communicate via IPC socket or CLI passthrough. ```mermaid flowchart TB classDef entry fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:2px,font-weight:bold classDef comp fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px classDef svc fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px classDef bridge fill:#f5a97f,stroke:#494d64,color:#24273a,stroke-width:1.5px classDef rend fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px classDef ext fill:#a6da95,stroke:#494d64,color:#24273a,stroke-width:1.5px classDef extrt fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px subgraph ExtRt["External Runtimes"] direction LR Launcher["Launcher CLI"]:::extrt Plugin["mpv Plugin"]:::extrt end Main["main.ts"]:::entry subgraph Comp["Composition"] direction LR Startup["Startup & Lifecycle"]:::comp Wiring["Runtime Wiring"]:::comp Composers["Domain Composers"]:::comp end subgraph Svc["Services"] direction LR Mpv["MPV Stack"]:::svc OverlaySvc["Overlay Manager"]:::svc Mining["Mining & Subtitles"]:::svc AnkiProxy["Anki Proxy"]:::svc Integrations["Integrations"]:::svc Tracking["Tracking"]:::svc Config["Config & Options"]:::svc end Bridge(["preload.ts"]):::bridge subgraph Rend["Renderer"] direction LR OverlayWin["Overlay Window"]:::rend UI["Subtitles & Modals"]:::rend end subgraph Ext["External Systems"] direction LR mpvExt["mpv"]:::ext AnkiExt["AnkiConnect"]:::ext JimakuExt["Jimaku"]:::ext TrackerExt["Window Tracker"]:::ext AnilistExt["AniList"]:::ext JellyfinExt["Jellyfin"]:::ext DiscordExt["Discord"]:::ext end Launcher -->|"CLI"| Main Plugin -->|"IPC"| mpvExt Main --> Comp Comp --> Svc Svc --> Bridge Bridge --> Rend mpvExt <-->|"socket"| Mpv AnkiExt <-->|"HTTP"| AnkiProxy JimakuExt <-->|"HTTP"| Integrations TrackerExt <-->|"platform"| OverlaySvc AnilistExt <-->|"HTTP"| Tracking JellyfinExt <-->|"HTTP"| Tracking DiscordExt <-->|"RPC"| Integrations style Comp fill:#363a4f,stroke:#494d64,color:#cad3f5 style Svc fill:#363a4f,stroke:#494d64,color:#cad3f5 style Rend fill:#363a4f,stroke:#494d64,color:#cad3f5 style Ext fill:#363a4f,stroke:#494d64,color:#cad3f5 style ExtRt fill:#363a4f,stroke:#494d64,color:#cad3f5 ``` ## Composition Pattern Most runtime code follows a dependency-injection pattern: 1. Define a service interface in `src/core/services/*`. 2. Keep core logic in pure or side-effect-bounded functions. 3. Build runtime deps in `src/main/` composition modules; extract an adapter/helper only when it adds meaningful behavior or reuse. 4. Call the service from lifecycle/command wiring points. The composition root (`src/main.ts`) delegates to focused modules in `src/main/` and `src/main/runtime/composers/`: - `startup.ts` — argv/env processing and bootstrap flow - `app-lifecycle.ts` — Electron lifecycle event registration - `startup-lifecycle.ts` — app-ready initialization sequence - `state.ts` — centralized application runtime state container - `ipc-runtime.ts` — IPC channel registration and handler wiring - `cli-runtime.ts` — CLI command parsing and dispatch - `overlay-runtime.ts` — overlay window selection and modal state management - `subsync-runtime.ts` — subsync command orchestration - `runtime/composers/anilist-tracking-composer.ts` — AniList media tracking/probe/retry wiring - `runtime/composers/jellyfin-runtime-composer.ts` — Jellyfin config/client/playback/command/setup composition wiring - `runtime/composers/mpv-runtime-composer.ts` — MPV event/factory/tokenizer/warmup wiring Composer modules share contract conventions via `src/main/runtime/composers/contracts.ts`: - composer input surfaces are declared with `ComposerInputs` so required dependencies cannot be omitted at compile time - composer outputs are declared with `ComposerOutputs` to keep result contracts explicit and stable - builder return payload extraction should use shared type helpers instead of inline ad-hoc inference This keeps side effects explicit and makes behavior easy to unit-test with fakes. Additional conventions in the current code: - `main.ts` uses `createMainRuntimeRegistry()` (`src/main/runtime/registry.ts`) to access domain handlers (`startup`, `overlay`, `mpv`, `ipc`, `shortcuts`, `anilist`, `jellyfin`, `mining`) without importing every runtime module directly. - Domain barrels in `src/main/runtime/domains/*` re-export runtime handlers + main-deps builders, while composers in `src/main/runtime/composers/*` assemble larger runtime clusters. - Many runtime handlers accept `*MainDeps` objects generated by `createBuild*MainDepsHandler` builders to isolate side effects and keep units testable. ### IPC Contract + Validation Boundary - Central channel constants live in `src/shared/ipc/contracts.ts` and are consumed by both main (`ipcMain`) and renderer preload (`ipcRenderer`) wiring. - Runtime payload parsers/type guards live in `src/shared/ipc/validators.ts`. - Rule: renderer-supplied payloads must be validated at IPC entry points (`src/core/services/ipc.ts`, `src/core/services/anki-jimaku-ipc.ts`) before calling domain handlers. - Malformed invoke payloads return explicit structured errors (for example `{ ok: false, error: ... }`) and malformed fire-and-forget payloads are ignored safely. ### Runtime State Ownership (Migrated Domains) For domains migrated to reducer-style transitions (for example AniList token/queue/media-guess runtime state), follow these rules: - Composition/runtime modules own mutable state cells and expose narrow `get*`/`set*` accessors. - Domain handlers do not mutate foreign state directly; they call explicit transition helpers that encode invariants. - Transition helpers may sync derived counters/snapshots, but must preserve non-owned metadata unless the transition explicitly owns that metadata. - Reducer boundary: when a domain has transition helpers in `src/main/state.ts`, new callsites should route updates through those helpers instead of ad-hoc object mutation in `main.ts` or composers. - Tests for migrated domains should assert both the intended field changes and non-targeted field invariants. ## Program Lifecycle - **Module-level init:** Before `app.ready`, the composition root registers protocols, sets platform flags, constructs all services, and wires dependency injection. `runAndApplyStartupState()` parses CLI args and detects the compositor backend. - **Startup:** If `--generate-config` is passed, it writes the template and exits. Otherwise `app-lifecycle.ts` acquires the single-instance lock and registers Electron lifecycle hooks. - **Critical-path init:** Once `app.whenReady()` fires, `composeAppReadyRuntime()` runs strict config reload, resolves keybindings, creates the `MpvIpcClient` (which immediately connects and subscribes to 26 properties), and initializes the `RuntimeOptionsManager`, `SubtitleTimingTracker`, and `ImmersionTrackerService`. - **Overlay runtime:** `initializeOverlayRuntime()` creates the primary overlay window (interactive Yomitan lookups and subtitle rendering), registers global shortcuts, and sets up bounds tracking via the active window tracker. mpv subtitle suppression is handled by a dedicated `overlay-mpv-sub-visibility` service. - **Background warmups:** Non-critical services are launched asynchronously: MeCab tokenizer check (with async worker thread), Yomitan extension load, JLPT + frequency dictionary prewarm, optional Jellyfin remote session, Discord presence service, AniList token refresh, and optional AnkiConnect proxy server. Warmup coverage is configurable through `startupWarmups` (including low-power mode that defers all but Yomitan). - **Runtime:** Event-driven. mpv property changes, IPC messages, CLI commands, overlay shortcuts, and hot-reload notifications route through runtime handlers/composers. Subtitle text flows through `SubtitlePipeline` (normalize → tokenize → merge), and results are sent to the main overlay renderer and modal surfaces. - **Shutdown:** `onWillQuitCleanup` destroys tray + config watcher, unregisters shortcuts, stops WebSocket + texthooker servers, closes the mpv socket + flushes OSD log, stops the window tracker, closes the Yomitan parser window, flushes the immersion tracker (SQLite), stops Jellyfin/Discord services, stops the AnkiConnect proxy server, and cleans Anki/AniList state. ```mermaid flowchart TB classDef start fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:2px,font-weight:bold classDef phase fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px classDef decision fill:#f5a97f,stroke:#494d64,color:#24273a,stroke-width:1.5px classDef init fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px classDef runtime fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px classDef shutdown fill:#ed8796,stroke:#494d64,color:#24273a,stroke-width:1.5px classDef warmup fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px CLI["CLI + Environment"]:::start CLI --> Init["Module Init"]:::phase Init --> Parse["Parse argv"]:::phase Parse --> GenCheck{"--generate-config?"}:::decision GenCheck -->|"yes"| GenExit["Write & exit"]:::phase GenCheck -->|"no"| Lock["Acquire lock"]:::phase Lock -->|"app.whenReady()"| Ready["App Ready"]:::phase Ready --> Config["Config + keybindings"]:::init Ready --> MpvInit["MPV socket connect"]:::init Ready --> Platform["Runtime services"]:::init Config & MpvInit & Platform --> OverlayInit["Overlay Init"]:::phase OverlayInit --> MainWin["Create window"]:::init OverlayInit --> Shortcuts["Register shortcuts"]:::init MainWin & Shortcuts --> Warmups subgraph Warmups["Background Warmups (parallel)"] direction LR W1["MeCab"]:::warmup ~~~ W2["Yomitan"]:::warmup ~~~ W3["Dictionaries"]:::warmup ~~~ W4["Jellyfin"]:::warmup ~~~ W5["Discord"]:::warmup ~~~ W6["AniList"]:::warmup ~~~ W7["Anki Proxy"]:::warmup end Warmups --> Loop subgraph Loop["Event Loop"] direction TB Events["mpv · IPC · shortcuts · config"]:::runtime Events --> Route["Composers"]:::runtime Route --> Pipeline["Subtitle Pipeline"]:::runtime Pipeline --> Broadcast["State + Renderer"]:::runtime end style Warmups fill:#363a4f,stroke:#494d64,color:#cad3f5 Loop -->|"quit"| Quit["Shutdown"]:::shutdown subgraph Cleanup[" "] direction LR T1["UI cleanup"]:::shutdown T2["Socket + server teardown"]:::shutdown T3["Flush tracking + state"]:::shutdown end Quit --> Cleanup style Cleanup fill:transparent,stroke:none style Loop fill:#363a4f,stroke:#494d64,color:#cad3f5 ``` ## Subtitle Prefetch Pipeline SubMiner can pre-tokenize upcoming subtitle lines before they appear on screen. When an external subtitle file (SRT, VTT, or ASS) is detected on the active track, the `SubtitlePrefetchService` parses all cues via the `SubtitleCueParser`, identifies a priority window of upcoming lines based on the current playback position, and tokenizes them in the background through the same pipeline used for live subtitles. Results are stored directly into the `SubtitleProcessingController` cache, so when a subtitle actually appears during playback, it hits a warm cache and renders in ~30-50ms instead of ~200-320ms. The prefetcher yields to live subtitle processing (which always takes priority over background work) and re-computes its priority window on seek. Cache invalidation events (e.g. marking a word as known) trigger re-prefetching of the current window to keep results fresh. ```mermaid flowchart TB classDef phase fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px classDef init fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px classDef runtime fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px classDef warmup fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px SubFile["External Sub File"]:::init Parse["Cue Parser"]:::phase Window["Upcoming Lines"]:::phase Tokenize["Pre-tokenize"]:::warmup Cache["Token Cache"]:::runtime Appear["Subtitle Appears"]:::init Hit["Cache Hit"]:::runtime Render["Fast Render"]:::runtime SubFile --> Parse --> Window --> Tokenize --> Cache Appear --> Hit --> Render Cache -.->|"warm"| Hit style SubFile stroke-width:2px style Render stroke-width:2px ``` ## Why This Design - **Smaller blast radius:** changing one feature usually touches one service. - **Better testability:** most behavior can be tested without Electron windows/mpv. - **Better reviewability:** PRs can be scoped to one subsystem. - **Backward compatibility:** CLI flags and IPC channels can remain stable while internals evolve. - **Runtime registry + domain barrels:** `src/main/runtime/registry.ts` and `src/main/runtime/domains/*` reduce direct fan-in inside `main.ts` while keeping domain ownership explicit. - **Extracted composition root:** `main.ts` delegates to focused modules under `src/main/` and `src/main/runtime/composers/` for lifecycle, IPC, overlay, mpv, shortcut, and integration wiring. - **Split MPV service layers:** MPV internals are separated into transport (`mpv-transport.ts`), protocol (`mpv-protocol.ts`), and properties/render metrics modules for maintainability. - **Config by domain:** defaults, option registries, and resolution are split by domain under `src/config/definitions/*` and `src/config/resolve/*`, keeping config evolution localized. ## Extension Rules - Add behavior to an existing service in `src/core/services/*` or create a focused runtime module under `src/main/runtime/*`; avoid ad-hoc logic in `main.ts`. - Add new cross-process channels in `src/shared/ipc/contracts.ts` first, validate payloads in `src/shared/ipc/validators.ts`, then wire handlers in IPC runtime modules. - See also the contributor IPC onboarding page: [IPC + Runtime Contracts](/ipc-contracts). - If change spans startup/overlay/mpv/integration wiring, prefer composing through `src/main/runtime/domains/*` + `src/main/runtime/composers/*` rather than direct wiring in `main.ts`. - Keep service APIs explicit and narrowly scoped, and preserve existing CLI flag / IPC channel behavior unless the change is intentionally breaking. - Add or update focused tests (including malformed-payload IPC tests) when runtime boundaries or contracts change.