Update docs content to match current launcher/plugin/runtime structure and fix stale home demo media assets with cache-busted URLs plus poster refresh. Add supporting backlog/subagent tracking records and docs asset regression coverage.
19 KiB
Architecture
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.lua) 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.tsfocused 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
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.lua # mpv plugin (auto-start, IPC, AniSkip + hover controls)
src/
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/ # Invisible-layer layout + offset controllers
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
token-mergers/ # Token merge strategies
translators/ # AI translation providers
Service Layer (src/core/services/)
- Overlay/window runtime:
overlay-manager.ts,overlay-window.ts,overlay-window-geometry.ts,overlay-visibility.ts,overlay-bridge.ts,overlay-runtime-init.ts,overlay-content-measurement.ts,overlay-drop.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 - 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 - 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.
src/renderer/
renderer.ts # Entrypoint/orchestration only
context.ts # Shared runtime context contract
state.ts # Centralized renderer mutable state
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
positioning/
controller.ts # Position controller orchestration
invisible-layout*.ts # Invisible layer layout computations
position-state.ts # Position state helpers
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.tsdispatches commands throughlauncher/commands/*and shared config readers inlauncher/config/*. It handles mpv startup, app passthrough, Jellyfin helper commands, and playback handoff.plugin/subminer.luaruns inside mpv and handles IPC startup checks, overlay toggles, hover-token messages, and AniSkip intro-skip UX.
Flow Diagram
The main process has three layers: main.ts delegates to composition modules that wire together domain services. The renderer runs in a separate Electron process, connected through preload.ts.
flowchart TD
classDef entry fill:#c6a0f6,stroke:#363a4f,color:#24273a,stroke-width:2px
classDef comp fill:#b7bdf8,stroke:#363a4f,color:#24273a,stroke-width:1.5px
classDef svc fill:#8aadf4,stroke:#363a4f,color:#24273a,stroke-width:1.5px
classDef bridge fill:#f5a97f,stroke:#363a4f,color:#24273a,stroke-width:1.5px
classDef rend fill:#8bd5ca,stroke:#363a4f,color:#24273a,stroke-width:1.5px
classDef ext fill:#a6da95,stroke:#363a4f,color:#24273a,stroke-width:1.5px
Main["main.ts"]:::entry
subgraph Comp["Composition — src/main/"]
Startup["Startup & Lifecycle<br/>startup · app-lifecycle<br/>startup-lifecycle · state"]:::comp
Wiring["Runtime Wiring<br/>ipc-runtime · cli-runtime<br/>overlay-runtime · subsync-runtime"]:::comp
end
subgraph Svc["Services — src/core/services/"]
direction LR
Mpv["MPV Stack<br/>transport · protocol<br/>state · properties"]:::svc
Overlay["Overlay<br/>manager · window<br/>visibility · bridge"]:::svc
Mining["Mining & Subtitles<br/>mining · field-grouping<br/>subtitle-ws · tokenizer"]:::svc
Integrations["Integrations<br/>jimaku · subsync<br/>texthooker · yomitan"]:::svc
end
Bridge(["preload.ts — Electron IPC"]):::bridge
subgraph Rend["Renderer — src/renderer/"]
Orchestration["renderer.ts<br/>orchestration · IPC wiring"]:::rend
UI["subtitle-render · positioning<br/>handlers · modals"]:::rend
end
subgraph Ext["External Systems"]
direction LR
mpv["mpv"]:::ext
Anki["AnkiConnect"]:::ext
Jimaku["Jimaku API"]:::ext
Tracker["Window Tracker"]:::ext
end
Main -->|delegates| Comp
Startup -->|initializes| Svc
Wiring -->|dispatches to| Svc
Overlay <--> Bridge
Mining <--> Bridge
Bridge <--> Orchestration
Orchestration --> UI
Mpv <-->|JSON socket| mpv
Mining -->|HTTP| Anki
Integrations -->|HTTP| Jimaku
Overlay --> Tracker
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
Composition Pattern
Most runtime code follows a dependency-injection pattern:
- Define a service interface in
src/core/services/*. - Keep core logic in pure or side-effect-bounded functions.
- Build runtime deps in
src/main/composition modules; extract an adapter/helper only when it adds meaningful behavior or reuse. - 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 flowapp-lifecycle.ts— Electron lifecycle event registrationstartup-lifecycle.ts— app-ready initialization sequencestate.ts— centralized application runtime state containeripc-runtime.ts— IPC channel registration and handler wiringcli-runtime.ts— CLI command parsing and dispatchoverlay-runtime.ts— overlay window selection and modal state managementsubsync-runtime.ts— subsync command orchestrationruntime/composers/anilist-tracking-composer.ts— AniList media tracking/probe/retry wiringruntime/composers/jellyfin-runtime-composer.ts— Jellyfin config/client/playback/command/setup composition wiringruntime/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.tsusescreateMainRuntimeRegistry()(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 insrc/main/runtime/composers/*assemble larger runtime clusters. - Many runtime handlers accept
*MainDepsobjects generated bycreateBuild*MainDepsHandlerbuilders to isolate side effects and keep units testable.
IPC Contract + Validation Boundary
- Central channel constants live in
src/shared/ipc/contracts.tsand 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 inmain.tsor composers. - Tests for migrated domains should assert both the intended field changes and non-targeted field invariants.
Program Lifecycle
- Startup:
startup.tsparses CLI args and detects the compositor backend. If--generate-configis passed, it writes the template and exits. Otherwiseapp-lifecycle.tsacquires the single-instance lock and registers Electron lifecycle hooks. - Initialization: Once
app.whenReady()fires,composeAppReadyRuntime()runs the critical path first (strict config reload, runtime options + keybindings, mpv client creation, overlay/IPC setup). Non-critical warmups are launched asynchronously (mecab,yomitan-extension, dictionary prewarm, optional Jellyfin remote session). - Runtime: Event-driven. mpv events, IPC messages, CLI commands, overlay shortcuts, hot-reload notifications, and integration callbacks route through runtime handlers/composers, update
AppState, and broadcast to overlay windows. - Overlay window model: runtime manages three overlay windows:
visible,invisible, andsecondary.splitOverlayGeometryForSecondaryBar()reserves the top 20% for the secondary subtitle bar and routes the remaining area to the active primary overlay layer. - Shutdown:
onWillQuitCleanuptears down tray + watchers + integrations, stops subtitle/texthooker servers, flushes buffered MPV OSD log writes, closes token/session windows, and stops Jellyfin/Discord runtime services.
flowchart TD
classDef start fill:#c6a0f6,stroke:#363a4f,color:#24273a,stroke-width:2px
classDef phase fill:#b7bdf8,stroke:#363a4f,color:#24273a,stroke-width:1.5px
classDef decision fill:#f5a97f,stroke:#363a4f,color:#24273a,stroke-width:1.5px
classDef init fill:#8aadf4,stroke:#363a4f,color:#24273a,stroke-width:1.5px
classDef runtime fill:#8bd5ca,stroke:#363a4f,color:#24273a,stroke-width:1.5px
classDef shutdown fill:#ed8796,stroke:#363a4f,color:#24273a,stroke-width:1.5px
CLI["CLI args & environment"]:::start
CLI --> Parse["startup.ts<br/>Parse argv · detect backend · resolve config"]:::phase
Parse --> GenCheck{"--generate-config?"}:::decision
GenCheck -->|yes| GenExit["Write config template & exit"]:::phase
GenCheck -->|no| Lifecycle["app-lifecycle.ts<br/>Acquire single-instance lock<br/>Register Electron lifecycle hooks"]:::phase
Lifecycle -->|"app.whenReady()"| Ready["startup-lifecycle.ts"]:::phase
Ready --> Init
subgraph Init["Initialization"]
direction LR
Config["Load config<br/>resolve keybindings"]:::init
Runtime["Create mpv client<br/>init runtime options"]:::init
Platform["Start window tracker<br/>WebSocket policy"]:::init
end
Init --> Create["Create overlay window<br/>Establish IPC bridge"]:::phase
Create --> Warm["Background warmups<br/>MeCab · Yomitan · dictionaries · Jellyfin"]:::phase
Warm --> Loop
subgraph Loop["Runtime — event-driven"]
direction LR
Events["mpv · IPC · CLI<br/>shortcut events"]:::runtime
Dispatch["Route to service<br/>via composition layer"]:::runtime
State["Update state<br/>broadcast to renderer"]:::runtime
Events --> Dispatch --> State
end
Loop -->|"app close"| Quit["Electron will-quit"]:::shutdown
Quit --> Teardown["Close mpv socket · unregister shortcuts<br/>Stop WebSocket & texthooker<br/>Destroy tracker · clean Anki state"]:::shutdown
style Init fill:#363a4f,stroke:#494d64,color:#cad3f5
style Loop fill:#363a4f,stroke:#494d64,color:#cad3f5
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.tsandsrc/main/runtime/domains/*reduce direct fan-in insidemain.tswhile keeping domain ownership explicit. - Extracted composition root:
main.tsdelegates to focused modules undersrc/main/andsrc/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/*andsrc/config/resolve/*, keeping config evolution localized.
Extension Rules
- Add behavior to an existing service in
src/core/services/*or create a focused runtime module undersrc/main/runtime/*; avoid ad-hoc logic inmain.ts. - Add new cross-process channels in
src/shared/ipc/contracts.tsfirst, validate payloads insrc/shared/ipc/validators.ts, then wire handlers in IPC runtime modules. - If change spans startup/overlay/mpv/integration wiring, prefer composing through
src/main/runtime/domains/*+src/main/runtime/composers/*rather than direct wiring inmain.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.