mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-26 00:26:05 -07:00
391 lines
23 KiB
Markdown
391 lines
23 KiB
Markdown
# 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<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.
|
|
|
|
## 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.
|