mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
Standardize core service module and export names to reduce naming ambiguity and make imports predictable across runtime, tests, scripts, and docs.
235 lines
12 KiB
Markdown
235 lines
12 KiB
Markdown
# 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.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
|
|
src/
|
|
main.ts # Entry point — delegates to src/main/ composition 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
|
|
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.
|
|
|
|
```text
|
|
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`.
|
|
|
|
```mermaid
|
|
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:
|
|
|
|
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/`:
|
|
- `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
|
|
|
|
This keeps side effects explicit and makes behavior easy to unit-test with fakes.
|
|
|
|
## Program Lifecycle
|
|
|
|
- **Startup:** `startup.ts` parses CLI args and detects the compositor backend. 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.
|
|
- **Initialization:** Once `app.whenReady()` fires, `startup-lifecycle.ts` loads config, resolves keybindings, creates the mpv client, initializes the MeCab tokenizer, starts the window tracker, and applies WebSocket policy — then creates the overlay window and establishes the IPC bridge.
|
|
- **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-quit` triggers service teardown — closes the mpv socket, unregisters shortcuts, stops WebSocket and texthooker servers, destroys the window tracker, and cleans up Anki state.
|
|
|
|
```mermaid
|
|
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 MeCab tokenizer"]:::init
|
|
Platform["Start window tracker<br/>WebSocket policy"]:::init
|
|
end
|
|
|
|
Init --> Create["Create overlay window<br/>Establish IPC bridge<br/>Load Yomitan extension"]:::phase
|
|
|
|
Create --> 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.ts` into focused modules under `src/main/`, isolating startup, lifecycle, IPC, CLI, and domain-specific runtime wiring.
|
|
- **Split MPV service:** TASK-27.4 separated `mpv.ts` into 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 new focused module in `src/main/` for composition-level logic — not as ad-hoc logic in `main.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.ts` for IPC, `cli-runtime.ts` for CLI).
|