mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
13 KiB
13 KiB
Architecture
SubMiner uses a service-oriented Electron architecture with a composition-oriented main process and a modular renderer process.
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
src/
main.ts # Entry point — delegates to runtime composers/domain modules
preload.ts # Electron preload bridge
types.ts # Shared type definitions
main/ # Composition root modules (extracted from main.ts)
app-lifecycle.ts # Electron lifecycle event registration
cli-runtime.ts # CLI command handling and dispatch
dependencies.ts # Shared dependency builders for IPC/runtime
ipc-mpv-command.ts # MPV command composition helpers
ipc-runtime.ts # IPC channel registration and handlers
overlay-runtime.ts # Overlay window/modal selection and state
overlay-shortcuts-runtime.ts # Overlay keyboard shortcut handling
startup.ts # Startup bootstrap flow (argv/env processing)
startup-lifecycle.ts # App-ready initialization sequence
state.ts # Application runtime state container
subsync-runtime.ts # Subsync command orchestration
runtime/
composers/ # Composition assembly clusters consumed by main.ts
domains/ # Domain barrel exports for runtime services
core/
services/ # ~60 focused service modules (see below)
utils/ # Pure helpers and coercion/config utilities
cli/ # CLI parsing and help output
config/ # Config schema, defaults, validation, template generation
renderer/ # Overlay renderer (modularized UI/runtime)
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/)
- Startup —
startup-service,app-lifecycle-service,app-ready-service - Overlay —
overlay-manager-service,overlay-window-service,overlay-visibility-service,overlay-bridge-service,overlay-runtime-init-service,overlay-content-measurement-service - Shortcuts —
shortcut-service,overlay-shortcut-service,overlay-shortcut-handler,shortcut-fallback-service,numeric-shortcut-service,numeric-shortcut-session-service - MPV —
mpv-service,mpv-control-service,mpv-render-metrics-service,mpv-transport,mpv-protocol,mpv-state,mpv-properties - IPC —
ipc-service,ipc-command-service,runtime-options-ipc-service - Mining —
mining-service,field-grouping-service,field-grouping-overlay-service,anki-jimaku-service,anki-jimaku-ipc-service - Subtitles —
subtitle-ws-service,subtitle-position-service,secondary-subtitle-service,tokenizer-service - Integrations —
jimaku-service,subsync-service,subsync-runner-service,texthooker-service,yomitan-extension-loader-service,yomitan-settings-service - Config —
runtime-config-service,cli-command-service
Renderer Layer (src/renderer/)
The overlay renderer is split by concern so renderer.ts stays focused on bootstrapping, IPC wiring, and module composition.
src/renderer/
renderer.ts # Entrypoint/orchestration only
context.ts # Shared runtime context contract
state.ts # Centralized renderer mutable state
subtitle-render.ts # Primary/secondary subtitle rendering + style application
positioning.ts # Visible/invisible positioning + mpv metrics layout
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
subsync.ts # Manual subsync modal flow
utils/
dom.ts # Required DOM lookups + typed handles
platform.ts # Layer/platform capability detection
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/mpv-runtime-composer.ts— MPV event/factory/tokenizer/warmup wiring
This keeps side effects explicit and makes behavior easy to unit-test with fakes.
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,startup-lifecycle.tsruns a short critical path first (config reload, keybindings, mpv client, overlay setup, IPC bridge), then schedules non-critical warmups in the background (MeCab availability check, Yomitan extension load, dictionary prewarm, optional Jellyfin remote startup). - Runtime: Event-driven. mpv property changes, IPC messages, CLI commands, and keyboard shortcuts all route through the composition layer to domain services, which update state and broadcast to the renderer.
- Shutdown: Electron's
will-quittriggers service teardown — closes the mpv socket, unregisters shortcuts, stops WebSocket and texthooker servers, destroys the window tracker, and cleans up Anki state.
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.
- Extracted composition root: TASK-27 refactored
main.tsinto focused modules undersrc/main/, isolating startup, lifecycle, IPC, CLI, and domain-specific runtime wiring. - Split MPV service: TASK-27.4 separated
mpv.tsinto transport (mpv-transport.ts), protocol (mpv-protocol.ts), state (mpv-state.ts), and properties (mpv-properties.ts) layers for improved maintainability.
Extension Rules
- Add behavior to an existing service in
src/core/services/*or create a focused composition module insrc/main//src/main/runtime/composers/— not as ad-hoc logic inmain.ts. - Keep service APIs explicit and narrowly scoped.
- Prefer additive changes that preserve existing CLI flags and IPC channel behavior.
- Add/update unit tests for each service extraction or behavior change.
- For cross-cutting changes, extract-first then refactor internals after parity is verified.
- When adding new IPC channels or CLI commands, register them in the appropriate
src/main/module (ipc-runtime.tsfor IPC,cli-runtime.tsfor CLI).