# 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/main.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/
                          #   dictionary/history/logs/stats/update)
  config/                 # Launcher config parsers + CLI parser builder
  main.ts                 # Launcher entrypoint and command dispatch
plugin/
  subminer/               # Modular mpv plugin (main · init · bootstrap · lifecycle · process
                          #   state · messages · hover · ui · options · environment · log
                          #   binary · session_bindings · version)
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
    boot/                  # Pre-ready boot helpers
    app-lifecycle.ts       # App lifecycle + app-ready runtime runner factories
    character-dictionary-runtime.ts # Character-dictionary orchestration/public runtime API
    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
    character-dictionary-runtime/ # Character-dictionary fetch/build/cache modules + focused tests
    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/gamepad interaction modules
    modals/                # Modal flows (Jimaku, Kiku, subsync, runtime options, session help,
                           #   changelog, character dictionary, playlist browser, subtitle
                           #   sidebar, YouTube track picker, controller config/debug/select)
    positioning/           # Subtitle position controller (drag-to-reposition)
  settings/                # Settings window UI (model, controls, markup)
  types/                   # Domain type modules (anki, config, integrations, ...)
  window-trackers/         # Backend-specific tracker implementations (Hyprland, Sway, X11, macOS, Windows)
  jimaku/                  # Jimaku API integration helpers
  subsync/                 # Subtitle sync (alass/ffsubsync) helpers
  anki-integration/        # AnkiConnect proxy server + note-update enrichment workflow
```

### 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 (repo `src/` root, not under `core/services/`):** `src/anki-integration.ts`, `src/anki-integration/anki-connect-proxy.ts` (local proxy for push-based auto-enrichment), `src/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
    gamepad-controller.ts  # Gamepad/controller input handling
    controller-binding-capture.ts # Controller binding capture flow
  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
    character-dictionary.ts # Character dictionary modal flow
    playlist-browser.ts    # Playlist browser modal flow
    subtitle-sidebar.ts    # Subtitle sidebar modal flow
    youtube-track-picker.ts # YouTube subtitle track picker
    controller-*.ts        # Controller config/debug/select modals
  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/main.lua` is the mpv entrypoint: it sets up the module path and loads `init.lua`, a thin shim that boots the modular Lua files: `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), `session_bindings.lua` (configurable session keybindings), `version.lua` (version metadata). AniSkip intro detection lives in the SubMiner app (`src/main/runtime/aniskip-runtime.ts`), which drives mpv chapters and the skip key over the IPC socket.

## 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<T>` so required dependencies cannot be omitted at compile time
- composer outputs are declared with `ComposerOutputs<T>` 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.

## Playback Startup Flow

Before the app boots, something has to launch mpv, inject the plugin, and bring the overlay up. SubMiner-managed launches own this step - the `subminer` launcher, the app's own playback, and the packaged Windows shortcut all follow the same path. The launcher reads `config.jsonc`, spawns mpv with the IPC socket and the bundled plugin, and passes runtime settings as `--script-opts`. The plugin never reads a config file: the shipped `subminer.conf` is intentionally empty so command-line opts always win.

Once mpv is up, exactly one of two triggers brings up the overlay. On a first launch the plugin's `file-loaded` hook self-starts the app once the socket is ready (because the launcher injected `auto_start=yes`). When the app is already running - or for explicit `--start-overlay` and YouTube flows - the launcher instead attaches over the control socket and suppresses the plugin's auto-start, so the two never fire together. Both converge on the same app bring-up, which then runs the Program Lifecycle below.

```mermaid
flowchart TB
  classDef entry fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:2px,font-weight:bold
  classDef extrt fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px
  classDef decision fill:#f5a97f,stroke:#494d64,color:#24273a,stroke-width:1.5px
  classDef proc fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px
  classDef app fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px
  classDef overlay fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px

  Entry["Managed launch<br/>subminer CLI · app · Windows shortcut"]:::entry
  Entry --> Cfg["Launcher reads config.jsonc<br/>→ plugin runtime config"]:::extrt
  Cfg --> Spawn["Spawn mpv<br/>--input-ipc-server=/tmp/subminer-socket<br/>--script=plugin/subminer/main.lua<br/>--script-opts=subminer-… (auto_start, backend, …)"]:::proc
  Spawn --> Boot["Plugin boot · read_options('subminer')<br/>empty subminer.conf; CLI opts win"]:::extrt
  Boot --> Sock["mpv IPC socket ready"]:::proc
  Sock --> Who{"Overlay trigger"}:::decision

  Who -->|"app already running, or<br/>--start-overlay / YouTube"| Attach["Launcher startOverlay()<br/>attach via control socket<br/>plugin auto-start suppressed"]:::proc
  Who -->|"first launch, auto_start=yes"| Self["Plugin file-loaded hook<br/>polls socket → process.start_overlay()"]:::extrt

  Attach --> AppUp
  Self --> AppUp

  AppUp["Spawn / attach SubMiner app<br/>--start --managed-playback --socket … --backend …"]:::app
  AppUp --> Ctrl["App control server up<br/>/tmp/subminer-control-* dedupes a 2nd launch"]:::app
  Ctrl --> Life["app.whenReady → Program Lifecycle (below)"]:::app
  Life --> Conn["MpvIpcClient connects to /tmp/subminer-socket"]:::overlay
  Conn --> Show["Transparent overlay over mpv<br/>Yomitan lookup · mine"]:::overlay
```

The runtime sockets in this flow are detailed in [IPC + Runtime Contracts](./ipc-contracts#runtime-sockets).

## 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 mpv subtitle/playback properties via `observe_property`), 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 the `SubtitleProcessingController` (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 subtitle cue parser (`subtitle-cue-parser.ts`), 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.
