diff --git a/backlog/milestones/m-0 - codebase-health-remediation.md b/backlog/milestones/m-0 - codebase-health-remediation.md index ca44a6b..a207f3f 100644 --- a/backlog/milestones/m-0 - codebase-health-remediation.md +++ b/backlog/milestones/m-0 - codebase-health-remediation.md @@ -1,6 +1,6 @@ --- id: m-0 -title: "Codebase Health Remediation" +title: 'Codebase Health Remediation' --- ## Description diff --git a/backlog/tasks/task-84 - Migrate-AniSkip-metadatalookup-orchestration-to-launcher-Electron.md b/backlog/tasks/task-84 - Migrate-AniSkip-metadatalookup-orchestration-to-launcher-Electron.md index 25451e3..f77db1b 100644 --- a/backlog/tasks/task-84 - Migrate-AniSkip-metadatalookup-orchestration-to-launcher-Electron.md +++ b/backlog/tasks/task-84 - Migrate-AniSkip-metadatalookup-orchestration-to-launcher-Electron.md @@ -33,11 +33,15 @@ priority: medium ## Description + Move AniSkip MAL/title-to-MAL lookup and intro payload resolution from mpv Lua to launcher Electron flow, while keeping mpv-side intro skip UX and chapter/chapter prompt behavior in plugin. Launcher should infer/analyze file metadata, fetch AniSkip payload when launching files, and pass resolved skip window via script options; plugin should trust launcher payload and fall back only when absent. + ## Acceptance Criteria + + - [x] #1 Launcher infers AniSkip metadata for file targets using existing guessit/fallback logic and performs AniSkip MAL + payload resolution during mpv startup. - [x] #2 Launcher injects script options containing resolved MAL id and intro window fields (or explicit lookup-failure status) into mpv startup. - [x] #3 Lua plugin consumes launcher-provided AniSkip intro data and skips all network lookups when payload is present. @@ -49,15 +53,18 @@ Move AniSkip MAL/title-to-MAL lookup and intro payload resolution from mpv Lua t ## Implementation Plan -1) Add launcher-side AniSkip payload resolution helpers in launcher/aniskip-metadata.ts (MAL prefix lookup + AniSkip payload fetch + result normalization). -2) Wire launcher/mpv.ts + buildSubminerScriptOpts to pass resolved AniSkip fields/mode in --script-opts for file playback. -3) Update plugin/subminer/aniskip.lua plus options/state to consume injected payload: if intro_start/end present, apply immediately and skip network lookup; otherwise retain existing async behavior. -4) Ensure fallback for standalone mpv usage remains intact for no-launcher/manual refresh. -5) Add/update tests/docs/config references for new script-opt contract and edge cases. + +1. Add launcher-side AniSkip payload resolution helpers in launcher/aniskip-metadata.ts (MAL prefix lookup + AniSkip payload fetch + result normalization). +2. Wire launcher/mpv.ts + buildSubminerScriptOpts to pass resolved AniSkip fields/mode in --script-opts for file playback. +3. Update plugin/subminer/aniskip.lua plus options/state to consume injected payload: if intro_start/end present, apply immediately and skip network lookup; otherwise retain existing async behavior. +4. Ensure fallback for standalone mpv usage remains intact for no-launcher/manual refresh. +5. Add/update tests/docs/config references for new script-opt contract and edge cases. ## Final Summary + Executed end-to-end migration so launcher resolves AniSkip title/MAL/payload before mpv start and injects it via --script-opts. Plugin now parses and consumes launcher payload (JSON/url/base64), applies OP intro from payload, tracks payload metadata in state, and keeps legacy async lookup path for non-launcher/absent payload playback. Added launcher config key aniskip_payload and updated launcher/aniskip-metadata tests for resolve/payload behavior and contract validation. + diff --git a/backlog/tasks/task-86 - Renderer-keyboard-driven-Yomitan-lookup-mode-and-popup-key-forwarding.md b/backlog/tasks/task-86 - Renderer-keyboard-driven-Yomitan-lookup-mode-and-popup-key-forwarding.md index f078c90..d87ec20 100644 --- a/backlog/tasks/task-86 - Renderer-keyboard-driven-Yomitan-lookup-mode-and-popup-key-forwarding.md +++ b/backlog/tasks/task-86 - Renderer-keyboard-driven-Yomitan-lookup-mode-and-popup-key-forwarding.md @@ -38,6 +38,7 @@ ordinal: 13000 ## Description + Add true keyboard-driven token lookup flow in overlay: - Toggle keyboard token-selection mode and navigate tokens by keyboard (`Arrow` + `HJKL`). @@ -47,7 +48,9 @@ Add true keyboard-driven token lookup flow in overlay: ## Acceptance Criteria + + - [x] #1 Keyboard mode toggle exists and shows visual selection outline for active token. - [x] #2 Navigation works via arrows and vim keys while keyboard mode is enabled. - [x] #3 Lookup window toggles from selected token with `Ctrl/Cmd+Y`; close path restores overlay keyboard focus. @@ -59,5 +62,7 @@ Add true keyboard-driven token lookup flow in overlay: ## Final Summary + Implemented keyboard-driven Yomitan workflow end-to-end in renderer + bundled Yomitan runtime bridge. Added overlay-level keyboard mode state, token selection sync, lookup toggle routing, popup command forwarding, and focus recovery after popup close. Follow-up fixes kept lookup open while moving between tokens, made popup-local `J/K` and `ArrowUp/ArrowDown` scroll work from overlay-owned focus with key repeat, skipped keyboard/token annotation flow for parser groups that have no dictionary-backed headword, and preserved paused playback when token navigation jumps across subtitle lines. Updated user docs/README to document the final shortcut behavior. + diff --git a/backlog/tasks/task-87 - Codebase-health-harden-verification-and-retire-dead-architecture-identified-in-the-March-2026-review.md b/backlog/tasks/task-87 - Codebase-health-harden-verification-and-retire-dead-architecture-identified-in-the-March-2026-review.md index fd9537e..14e5284 100644 --- a/backlog/tasks/task-87 - Codebase-health-harden-verification-and-retire-dead-architecture-identified-in-the-March-2026-review.md +++ b/backlog/tasks/task-87 - Codebase-health-harden-verification-and-retire-dead-architecture-identified-in-the-March-2026-review.md @@ -30,11 +30,15 @@ priority: high ## Description + Track the remediation work from the March 6, 2026 code review. The review found that the default test gate only exercises 53 of 241 test files, the dedicated subtitle test lane is a no-op, SQLite-backed immersion tracking tests are conditionally skipped in the standard Bun run, src/main.ts still contains a large dead-symbol backlog, several registry/pipeline modules appear unreferenced from live execution paths, and src/anki-integration.ts remains an oversized orchestration file. This parent task should coordinate a safe sequence: improve verification first, then remove dead code and continue decomposition with good test coverage in place. + ## Acceptance Criteria + + - [ ] #1 Child tasks are created for each remediation workstream with explicit dependencies and enough context for an isolated agent to execute them. - [ ] #2 The parent task records the recommended sequencing and parallelization strategy so replacement agents can resume without conversation history. - [ ] #3 Completion of the parent task leaves the repository with a materially more trustworthy test gate, less dead architecture, and clearer ownership boundaries for the main runtime and Anki integration surfaces. @@ -43,7 +47,9 @@ Track the remediation work from the March 6, 2026 code review. The review found ## Implementation Plan + Recommended sequencing: + 1. Run TASK-87.1, TASK-87.2, TASK-87.3, and TASK-87.7 first. These are the safety-net and tooling tasks and can largely proceed in parallel. 2. Start TASK-87.4 once TASK-87.1 lands so src/main.ts cleanup happens under a more trustworthy test matrix. 3. Start TASK-87.5 after TASK-87.1 and TASK-87.2 so dead subsync/pipeline cleanup happens with stronger subtitle and runtime verification. @@ -51,10 +57,12 @@ Recommended sequencing: 5. Keep PRs focused: do not combine verification work with architectural cleanup unless a narrow dependency requires it. Parallelization guidance: + - Wave 1 parallel: TASK-87.1, TASK-87.2, TASK-87.3, TASK-87.7 - Wave 2 parallel: TASK-87.4, TASK-87.5, TASK-87.6 Shared review context to restate in child tasks: + - Standard test scripts currently reference only 53 unique test files out of 241 discovered test and type-test files under src/ and launcher/. - test:subtitle is currently a placeholder echo even though subtitle sync is a user-facing feature. - SQLite-backed immersion tracker tests are conditionally skipped in the standard Bun run. diff --git a/backlog/tasks/task-87.1 - Testing-workflow-make-standard-test-commands-reflect-the-maintained-test-surface.md b/backlog/tasks/task-87.1 - Testing-workflow-make-standard-test-commands-reflect-the-maintained-test-surface.md index 16cc91e..0099f0b 100644 --- a/backlog/tasks/task-87.1 - Testing-workflow-make-standard-test-commands-reflect-the-maintained-test-surface.md +++ b/backlog/tasks/task-87.1 - Testing-workflow-make-standard-test-commands-reflect-the-maintained-test-surface.md @@ -27,11 +27,15 @@ priority: high ## Description + The current package scripts hand-enumerate a small subset of test files, which leaves the standard green signal misleading. A local audit found 241 test/type-test files under src/ and launcher/, but only 53 unique files referenced by the standard package.json test scripts. This task should redesign the runnable test matrix so maintained tests are either executed by the standard commands or intentionally excluded through a documented rule, instead of silently drifting out of coverage. + ## Acceptance Criteria + + - [ ] #1 The repository has a documented and reproducible test matrix for standard development commands, including which suites belong in the default lane versus slower or environment-specific lanes. - [ ] #2 The standard test entrypoints stop relying on a brittle hand-maintained allowlist for the currently covered unit and integration suites, or an explicit documented mechanism exists that prevents silent omission of new tests. - [ ] #3 Representative tests that were previously outside the standard lane from src/main/runtime, src/anki-integration, and entry/runtime surfaces are executed by an automated command and included in the documented matrix. @@ -41,6 +45,7 @@ The current package scripts hand-enumerate a small subset of test files, which l ## Implementation Plan + 1. Inventory the current test surface under src/ and launcher/ and compare it to package.json scripts to classify fast, full, slow, and environment-specific suites. 2. Replace or reduce the brittle hand-maintained allowlist so new maintained tests do not silently miss the standard matrix. 3. Update contributor docs with the intended fast/full/environment-specific commands. diff --git a/backlog/tasks/task-87.2 - Subtitle-sync-verification-replace-the-no-op-subtitle-lane-with-real-automated-coverage.md b/backlog/tasks/task-87.2 - Subtitle-sync-verification-replace-the-no-op-subtitle-lane-with-real-automated-coverage.md index d04dd22..4de414f 100644 --- a/backlog/tasks/task-87.2 - Subtitle-sync-verification-replace-the-no-op-subtitle-lane-with-real-automated-coverage.md +++ b/backlog/tasks/task-87.2 - Subtitle-sync-verification-replace-the-no-op-subtitle-lane-with-real-automated-coverage.md @@ -27,11 +27,15 @@ priority: high ## Description + SubMiner advertises subtitle syncing with alass and ffsubsync, but the dedicated test:subtitle command currently does not run any tests. There is already lower-level coverage in src/core/services/subsync.test.ts, but the test matrix and contributor-facing commands do not reflect that reality. This task should replace the no-op lane with real verification, align scripts with the existing subsync test surface, and make the user-facing docs honest about how subtitle sync is verified. + ## Acceptance Criteria + + - [ ] #1 The test:subtitle entrypoint runs real automated verification instead of echoing a placeholder message. - [ ] #2 The subtitle verification lane covers both alass and ffsubsync behavior, including at least one non-happy-path scenario relevant to current functionality. - [ ] #3 Contributor-facing documentation points to the real subtitle verification command and no longer implies a dedicated test lane exists when it does not. @@ -41,6 +45,7 @@ SubMiner advertises subtitle syncing with alass and ffsubsync, but the dedicated ## Implementation Plan + 1. Audit the existing subtitle-sync test surface, especially src/core/services/subsync.test.ts, and decide whether test:subtitle should reuse or regroup that coverage. 2. Replace the placeholder script with a real automated command and keep the matrix legible alongside TASK-87.1 work. 3. Update README or related docs so the advertised subtitle verification path matches reality. diff --git a/backlog/tasks/task-87.3 - Immersion-tracking-verification-make-SQLite-backed-persistence-tests-visible-and-reproducible.md b/backlog/tasks/task-87.3 - Immersion-tracking-verification-make-SQLite-backed-persistence-tests-visible-and-reproducible.md index 2b4c419..2655b8b 100644 --- a/backlog/tasks/task-87.3 - Immersion-tracking-verification-make-SQLite-backed-persistence-tests-visible-and-reproducible.md +++ b/backlog/tasks/task-87.3 - Immersion-tracking-verification-make-SQLite-backed-persistence-tests-visible-and-reproducible.md @@ -26,11 +26,15 @@ priority: medium ## Description + The immersion tracker is persistence-heavy, but its SQLite-backed tests are conditionally skipped in the standard Bun run when node:sqlite support is unavailable. That creates a blind spot around session finalization, telemetry persistence, and retention behavior. This task should establish a reliable automated verification path for the database-backed cases and make the prerequisite/runtime behavior explicit to contributors and CI. + ## Acceptance Criteria + + - [ ] #1 Database-backed immersion tracking tests run in at least one documented automated command that is practical for contributors or CI to execute. - [ ] #2 If the current runtime cannot execute the SQLite-backed tests, the repository exposes that limitation clearly instead of silently reporting a misleading green result. - [ ] #3 Contributor-facing documentation explains how to run the immersion tracker verification lane and any environment prerequisites it depends on. @@ -40,6 +44,7 @@ The immersion tracker is persistence-heavy, but its SQLite-backed tests are cond ## Implementation Plan + 1. Confirm which SQLite-backed immersion tests are currently skipped and why in the standard Bun environment. 2. Establish a reproducible command or lane for the DB-backed cases, or make the unsupported-runtime limitation explicit and actionable. 3. Document prerequisites and expected behavior for contributors and CI. diff --git a/backlog/tasks/task-87.4 - Runtime-composition-root-remove-dead-symbols-and-tighten-module-boundaries-in-src-main.ts.md b/backlog/tasks/task-87.4 - Runtime-composition-root-remove-dead-symbols-and-tighten-module-boundaries-in-src-main.ts.md index 04f310a..a5c4ef7 100644 --- a/backlog/tasks/task-87.4 - Runtime-composition-root-remove-dead-symbols-and-tighten-module-boundaries-in-src-main.ts.md +++ b/backlog/tasks/task-87.4 - Runtime-composition-root-remove-dead-symbols-and-tighten-module-boundaries-in-src-main.ts.md @@ -27,11 +27,15 @@ priority: high ## Description + A noUnusedLocals/noUnusedParameters compile pass reports a large concentration of dead imports and dead locals in src/main.ts. The file is also far beyond the repo’s preferred size guideline, which makes the runtime composition root difficult to review and easy to break. This task should remove confirmed dead symbols, continue extracting coherent slices where that improves readability, and leave the entrypoint materially easier to understand without changing behavior. + ## Acceptance Criteria + + - [ ] #1 src/main.ts no longer emits dead-symbol diagnostics under a noUnusedLocals/noUnusedParameters compile pass for the areas touched by this cleanup. - [ ] #2 Unused imports, destructured values, and stale locals identified in the current composition root are removed or relocated without behavior changes. - [ ] #3 The resulting composition root has clearer ownership boundaries for at least one runtime slice that is currently buried in the monolith. @@ -41,6 +45,7 @@ A noUnusedLocals/noUnusedParameters compile pass reports a large concentration o ## Implementation Plan + 1. Re-run the noUnusedLocals/noUnusedParameters compile pass and capture the src/main.ts diagnostics cluster before editing. 2. Remove dead imports, destructured values, and stale locals in small reviewable slices; extract a coherent helper/module only where that materially reduces coupling. 3. Keep changes behavior-preserving and avoid mixing unrelated cleanup outside src/main.ts unless required to compile. diff --git a/backlog/tasks/task-87.5 - Dead-architecture-cleanup-delete-unused-registry-and-pipeline-modules-that-are-off-the-live-path.md b/backlog/tasks/task-87.5 - Dead-architecture-cleanup-delete-unused-registry-and-pipeline-modules-that-are-off-the-live-path.md index da8f478..5639863 100644 --- a/backlog/tasks/task-87.5 - Dead-architecture-cleanup-delete-unused-registry-and-pipeline-modules-that-are-off-the-live-path.md +++ b/backlog/tasks/task-87.5 - Dead-architecture-cleanup-delete-unused-registry-and-pipeline-modules-that-are-off-the-live-path.md @@ -31,11 +31,15 @@ priority: high ## Description + The review found several modules that appear self-contained but unused from the application’s live execution paths: src/translators/index.ts, src/subsync/engines.ts, src/subtitle/pipeline.ts, src/tokenizers/index.ts, and src/token-mergers/index.ts. At the same time, the real runtime behavior is implemented elsewhere. This task should verify those modules are truly unused, remove or consolidate them, and clean up any stale exports, docs, or tests so contributors are not misled by duplicate architecture. + ## Acceptance Criteria + + - [ ] #1 Each candidate module identified in the review is either removed as dead code or justified and reconnected to a real supported execution path. - [ ] #2 Any stale exports, imports, or tests associated with the removed or consolidated modules are cleaned up so the codebase has a single obvious path for the affected behavior. - [ ] #3 The cleanup does not regress live tokenization or subtitle sync behavior and the relevant verification commands remain green. @@ -45,6 +49,7 @@ The review found several modules that appear self-contained but unused from the ## Implementation Plan + 1. Re-verify each candidate module is off the live path by tracing imports from current runtime entrypoints before deleting anything. 2. Remove or consolidate truly dead modules and clean associated exports/imports/tests so only the supported path remains obvious. 3. Pay special attention to subtitle sync and tokenization surfaces, since duplicate architecture exists near active code. diff --git a/backlog/tasks/task-87.6 - Anki-integration-maintainability-continue-decomposing-the-oversized-orchestration-layer.md b/backlog/tasks/task-87.6 - Anki-integration-maintainability-continue-decomposing-the-oversized-orchestration-layer.md index cb98367..87cb397 100644 --- a/backlog/tasks/task-87.6 - Anki-integration-maintainability-continue-decomposing-the-oversized-orchestration-layer.md +++ b/backlog/tasks/task-87.6 - Anki-integration-maintainability-continue-decomposing-the-oversized-orchestration-layer.md @@ -31,11 +31,15 @@ priority: medium ## Description + src/anki-integration.ts remains an oversized orchestration file even after earlier extractions. It still mixes config normalization, polling setup, media generation, duplicate resolution, field grouping workflows, and user feedback coordination in one class. This task should continue the decomposition so the remaining orchestration surface is smaller and easier to reason about, while preserving existing Anki, proxy, field grouping, and note update behavior. + ## Acceptance Criteria + + - [ ] #1 The responsibilities currently concentrated in src/anki-integration.ts are split into clearer modules or services with narrow ownership boundaries. - [ ] #2 The resulting orchestration surface is materially smaller and easier to review, with at least one mixed-responsibility cluster extracted behind a well-named interface. - [ ] #3 Existing Anki integration behavior remains covered by automated verification, including note update, field grouping, and proxy-related flows that the refactor touches. @@ -45,6 +49,7 @@ src/anki-integration.ts remains an oversized orchestration file even after earli ## Implementation Plan + 1. Map the remaining responsibility clusters inside src/anki-integration.ts and choose one or more extraction seams that reduce mixed concerns without changing behavior. 2. Move logic behind narrow interfaces/modules rather than creating another giant helper; keep orchestration readable. 3. Preserve coverage for field grouping, note update, proxy, and card creation flows touched by the refactor. diff --git a/backlog/tasks/task-87.7 - Developer-workflow-hygiene-make-docs-watch-reproducible-and-remove-stale-small-surface-drift.md b/backlog/tasks/task-87.7 - Developer-workflow-hygiene-make-docs-watch-reproducible-and-remove-stale-small-surface-drift.md index 189fc35..34b5849 100644 --- a/backlog/tasks/task-87.7 - Developer-workflow-hygiene-make-docs-watch-reproducible-and-remove-stale-small-surface-drift.md +++ b/backlog/tasks/task-87.7 - Developer-workflow-hygiene-make-docs-watch-reproducible-and-remove-stale-small-surface-drift.md @@ -25,11 +25,15 @@ priority: low ## Description + The review found a few low-risk but recurring hygiene issues: docs:watch depends on bunx concurrently even though concurrently is not declared in package metadata, and small stale API surface remains after recent refactors, such as unused parameters in field-grouping workflow code. This task should make the developer workflow reproducible and clean up low-risk stale symbols that do not warrant a dedicated architecture task. + ## Acceptance Criteria + + - [ ] #1 The docs:watch workflow runs through declared project tooling or is rewritten to avoid undeclared dependencies. - [ ] #2 Small stale symbols or parameters identified during the review outside the main composition-root cleanup are removed without behavior changes. - [ ] #3 Any contributor-facing command changes are reflected in repository documentation. @@ -39,6 +43,7 @@ The review found a few low-risk but recurring hygiene issues: docs:watch depends ## Implementation Plan + 1. Fix the docs:watch workflow so it relies on declared project tooling or an equivalent checked-in command path. 2. Clean up low-risk stale symbols surfaced by the review outside the main.ts architecture task, such as unused parameters left behind by refactors. 3. Keep the task scoped: avoid pulling in main composition-root cleanup or larger Anki/runtime refactors. diff --git a/docs/configuration.md b/docs/configuration.md index e4a45cf..b4f0da1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -258,8 +258,8 @@ See `config.example.jsonc` for detailed configuration options. | `backgroundColor` | string | Any CSS color, including `"transparent"` (default: `"rgb(30, 32, 48, 0.88)"`) | | `enableJlpt` | boolean | Enable JLPT level underline styling (`false` by default) | | `preserveLineBreaks` | boolean | Preserve line breaks in visible overlay subtitle rendering (`false` by default). Enable to mirror mpv line layout. | -| `autoPauseVideoOnHover` | boolean | Pause playback while mouse hovers subtitle text; resume after leaving subtitle area (`true` by default). | -| `autoPauseVideoOnYomitanPopup` | boolean | Pause playback while Yomitan popup is open; resume when popup closes (`false` by default). | +| `autoPauseVideoOnHover` | boolean | Pause playback while mouse hovers subtitle text; resume after leaving subtitle area (`true` by default). | +| `autoPauseVideoOnYomitanPopup` | boolean | Pause playback while Yomitan popup is open; resume when popup closes (`false` by default). | | `hoverTokenColor` | string | Hex color used for hovered subtitle token highlight in mpv (default: catppuccin mauve) | | `hoverTokenBackgroundColor` | string | CSS color used for hovered subtitle token background highlight (default: semi-transparent dark) | | `frequencyDictionary.enabled` | boolean | Enable frequency highlighting from dictionary lookups (`false` by default) | @@ -778,12 +778,12 @@ Sync the active subtitle track using `alass` (preferred) or `ffsubsync`: } ``` -| Option | Values | Description | -| ---------------- | -------------------- | ----------------------------------------------------------------------------------------------------------- | -| `defaultMode` | `"auto"`, `"manual"` | `auto`: try `alass` against secondary subtitle, then fallback to `ffsubsync`; `manual`: open overlay picker | -| `alass_path` | string path | Path to `alass` executable. Empty or `null` falls back to `/usr/bin/alass`. | -| `ffsubsync_path` | string path | Path to `ffsubsync` executable. Empty or `null` falls back to `/usr/bin/ffsubsync`. | -| `ffmpeg_path` | string path | Path to `ffmpeg` (used for internal subtitle extraction). Empty or `null` falls back to `/usr/bin/ffmpeg`. | +| Option | Values | Description | +| ---------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| `defaultMode` | `"auto"`, `"manual"` | `auto`: try `alass` against secondary subtitle, then fallback to `ffsubsync`; `manual`: open overlay picker | +| `alass_path` | string path | Path to `alass` executable. Empty or `null` falls back to `/usr/bin/alass`. | +| `ffsubsync_path` | string path | Path to `ffsubsync` executable. Empty or `null` falls back to `/usr/bin/ffsubsync`. | +| `ffmpeg_path` | string path | Path to `ffmpeg` (used for internal subtitle extraction). Empty or `null` falls back to `/usr/bin/ffmpeg`. | | `replace` | `true`, `false` | When `true` (default), overwrite the active subtitle file on successful sync. When `false`, write `_retimed.`. | Default trigger is `Ctrl+Alt+S` via `shortcuts.triggerSubsync`. diff --git a/docs/mpv-plugin.md b/docs/mpv-plugin.md index eb3a798..b8ea53c 100644 --- a/docs/mpv-plugin.md +++ b/docs/mpv-plugin.md @@ -120,28 +120,28 @@ aniskip_button_duration=3 ### Option Reference -| Option | Default | Values | Description | -| ------------------------------ | ----------------------------- | ------------------------------------------ | -------------------------------------------------------------------------------- | -| `binary_path` | `""` (auto-detect) | file path | Path to SubMiner binary | -| `socket_path` | `/tmp/subminer-socket` | file path | MPV IPC socket path | -| `texthooker_enabled` | `yes` | `yes` / `no` | Enable texthooker server | -| `texthooker_port` | `5174` | 1–65535 | Texthooker server port | -| `backend` | `auto` | `auto`, `hyprland`, `sway`, `x11`, `macos` | Window manager backend | -| `auto_start` | `yes` | `yes` / `no` | Auto-start overlay on file load when mpv socket matches `socket_path` | -| `auto_start_visible_overlay` | `yes` | `yes` / `no` | Show visible layer on auto-start when mpv socket matches `socket_path` | -| `auto_start_pause_until_ready` | `yes` | `yes` / `no` | Pause mpv on visible auto-start; resume when SubMiner signals tokenization-ready | -| `osd_messages` | `yes` | `yes` / `no` | Show OSD status messages | -| `log_level` | `info` | `debug`, `info`, `warn`, `error` | Log verbosity | -| `aniskip_enabled` | `yes` | `yes` / `no` | Enable AniSkip intro detection | -| `aniskip_title` | `""` | string | Override title used for lookup | -| `aniskip_season` | `""` | numeric season | Optional season hint | -| `aniskip_mal_id` | `""` | numeric MAL id | Skip title lookup; use fixed id | -| `aniskip_episode` | `""` | numeric episode | Skip episode parsing; use fixed | +| Option | Default | Values | Description | +| ------------------------------ | ----------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------ | +| `binary_path` | `""` (auto-detect) | file path | Path to SubMiner binary | +| `socket_path` | `/tmp/subminer-socket` | file path | MPV IPC socket path | +| `texthooker_enabled` | `yes` | `yes` / `no` | Enable texthooker server | +| `texthooker_port` | `5174` | 1–65535 | Texthooker server port | +| `backend` | `auto` | `auto`, `hyprland`, `sway`, `x11`, `macos` | Window manager backend | +| `auto_start` | `yes` | `yes` / `no` | Auto-start overlay on file load when mpv socket matches `socket_path` | +| `auto_start_visible_overlay` | `yes` | `yes` / `no` | Show visible layer on auto-start when mpv socket matches `socket_path` | +| `auto_start_pause_until_ready` | `yes` | `yes` / `no` | Pause mpv on visible auto-start; resume when SubMiner signals tokenization-ready | +| `osd_messages` | `yes` | `yes` / `no` | Show OSD status messages | +| `log_level` | `info` | `debug`, `info`, `warn`, `error` | Log verbosity | +| `aniskip_enabled` | `yes` | `yes` / `no` | Enable AniSkip intro detection | +| `aniskip_title` | `""` | string | Override title used for lookup | +| `aniskip_season` | `""` | numeric season | Optional season hint | +| `aniskip_mal_id` | `""` | numeric MAL id | Skip title lookup; use fixed id | +| `aniskip_episode` | `""` | numeric episode | Skip episode parsing; use fixed | | `aniskip_payload` | `""` | JSON / base64-encoded JSON | Optional pre-fetched AniSkip payload for this media. When set, plugin skips network lookup | -| `aniskip_show_button` | `yes` | `yes` / `no` | Show in-range intro skip prompt | -| `aniskip_button_text` | `You can skip by pressing %s` | string | OSD prompt format (`%s`=key) | -| `aniskip_button_key` | `y-k` | mpv key chord | Primary key for intro skip action (`y-k` always works as fallback) | -| `aniskip_button_duration` | `3` | float seconds | OSD hint duration | +| `aniskip_show_button` | `yes` | `yes` / `no` | Show in-range intro skip prompt | +| `aniskip_button_text` | `You can skip by pressing %s` | string | OSD prompt format (`%s`=key) | +| `aniskip_button_key` | `y-k` | mpv key chord | Primary key for intro skip action (`y-k` always works as fallback) | +| `aniskip_button_duration` | `3` | float seconds | OSD hint duration | ## Binary Auto-Detection diff --git a/docs/shortcuts.md b/docs/shortcuts.md index cf05ab0..6a97e18 100644 --- a/docs/shortcuts.md +++ b/docs/shortcuts.md @@ -62,25 +62,25 @@ Mouse-hover playback behavior is configured separately from shortcuts: `subtitle When a Yomitan popup is open, SubMiner also provides popup control shortcuts: -| Shortcut | Action | -| ------------- | -------------------------------------- | -| `J` | Scroll definitions down | -| `K` | Scroll definitions up | -| `ArrowDown` | Scroll definitions down | -| `ArrowUp` | Scroll definitions up | -| `M` | Mine/add selected term | -| `P` | Play selected term audio | -| `[` | Play previous available audio (selected source) | -| `]` | Play next available audio (selected source) | +| Shortcut | Action | +| ----------- | ----------------------------------------------- | +| `J` | Scroll definitions down | +| `K` | Scroll definitions up | +| `ArrowDown` | Scroll definitions down | +| `ArrowUp` | Scroll definitions up | +| `M` | Mine/add selected term | +| `P` | Play selected term audio | +| `[` | Play previous available audio (selected source) | +| `]` | Play next available audio (selected source) | ## Keyboard-Driven Lookup Mode These shortcuts are fixed (not configurable) and require overlay focus. -| Shortcut | Action | -| ------------------ | --------------------------------------------------------------------- | -| `Ctrl/Cmd+Shift+Y` | Toggle keyboard-driven token selection mode on/off | -| `Ctrl/Cmd+Y` | Toggle lookup popup for selected token (open when closed, close when open) | +| Shortcut | Action | +| ------------------------------ | -------------------------------------------------------------------------------------------- | +| `Ctrl/Cmd+Shift+Y` | Toggle keyboard-driven token selection mode on/off | +| `Ctrl/Cmd+Y` | Toggle lookup popup for selected token (open when closed, close when open) | | `ArrowLeft/Right`, `H`, or `L` | Move selected token (previous/next); if lookup is open, refresh definition for the new token | Keyboard-driven mode draws a selection outline around the active token. Use `Ctrl/Cmd+Y` to open or close lookup for that token. While the popup is open, popup-local controls still work from the overlay (`J/K`, `ArrowUp/ArrowDown`, `M`, `P`, `[`, `]`) and focus is forced back to the overlay so token navigation can continue without clicking subtitle text again. Moving left/right past the start or end of the line jumps to the previous or next subtitle line and keeps playback paused if it was already paused. diff --git a/launcher/aniskip-metadata.ts b/launcher/aniskip-metadata.ts index 0e03e4d..69aa573 100644 --- a/launcher/aniskip-metadata.ts +++ b/launcher/aniskip-metadata.ts @@ -196,7 +196,11 @@ function seasonSignalScore(requestedSeason: number | null, candidateTitle: strin } as const; const aliases = romanAliases[season] ?? []; - return aliases.some((alias) => normalized.includes(alias)) ? 40 : hasAnySequelMarker(candidateTitle) ? -20 : 5; + return aliases.some((alias) => normalized.includes(alias)) + ? 40 + : hasAnySequelMarker(candidateTitle) + ? -20 + : 5; } function toMalSearchItems(payload: unknown): MalSearchResult[] { @@ -230,7 +234,11 @@ function parseAniSkipPayload(payload: unknown): { start: number; end: number } | for (const rawResult of results) { const result = rawResult as AniSkipSkipItemPayload; - if (result.skip_type !== 'op' || typeof result.interval !== 'object' || result.interval === null) { + if ( + result.skip_type !== 'op' || + typeof result.interval !== 'object' || + result.interval === null + ) { continue; } const interval = result.interval as AniSkipIntervalPayload; @@ -287,7 +295,9 @@ async function fetchAniSkipPayload( malId: number, episode: number, ): Promise<{ start: number; end: number } | null> { - const payload = await fetchJson(`${ANISKIP_PAYLOAD_API}${malId}/${episode}?types=op&types=ed`); + const payload = await fetchJson( + `${ANISKIP_PAYLOAD_API}${malId}/${episode}?types=op&types=ed`, + ); const parsed = payload as AniSkipPayloadResponse; if (!parsed || parsed.found !== true) return null; return parseAniSkipPayload(parsed); @@ -565,7 +575,9 @@ export function buildSubminerScriptOpts( parts.push(`subminer-aniskip_intro_end=${aniSkipMetadata.introEnd}`); } if (aniSkipMetadata?.lookupStatus) { - parts.push(`subminer-aniskip_lookup_status=${sanitizeScriptOptValue(aniSkipMetadata.lookupStatus)}`); + parts.push( + `subminer-aniskip_lookup_status=${sanitizeScriptOptValue(aniSkipMetadata.lookupStatus)}`, + ); } const aniskipPayload = aniSkipMetadata ? buildLauncherAniSkipPayload(aniSkipMetadata) : null; if (aniskipPayload) { diff --git a/package.json b/package.json index 776a187..352da0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "subminer", - "version": "0.2.3", + "version": "0.3.0", "description": "All-in-one sentence mining overlay with AnkiConnect and dictionary integration", "packageManager": "bun@1.3.5", "main": "dist/main-entry.js", diff --git a/src/config/resolve/subtitle-domains.ts b/src/config/resolve/subtitle-domains.ts index 7b51e4a..baf3b72 100644 --- a/src/config/resolve/subtitle-domains.ts +++ b/src/config/resolve/subtitle-domains.ts @@ -174,7 +174,8 @@ export function applySubtitleDomainConfig(context: ResolveContext): void { } const autoPauseVideoOnYomitanPopup = asBoolean( - (src.subtitleStyle as { autoPauseVideoOnYomitanPopup?: unknown }).autoPauseVideoOnYomitanPopup, + (src.subtitleStyle as { autoPauseVideoOnYomitanPopup?: unknown }) + .autoPauseVideoOnYomitanPopup, ); if (autoPauseVideoOnYomitanPopup !== undefined) { resolved.subtitleStyle.autoPauseVideoOnYomitanPopup = autoPauseVideoOnYomitanPopup; diff --git a/src/renderer/handlers/keyboard.test.ts b/src/renderer/handlers/keyboard.test.ts index 1fb5ce5..c58120f 100644 --- a/src/renderer/handlers/keyboard.test.ts +++ b/src/renderer/handlers/keyboard.test.ts @@ -328,7 +328,9 @@ test('keyboard mode: up/down/j/k do not open or close lookup when popup is close await wait(0); - const openEvents = testGlobals.commandEvents.filter((event) => event.type === 'scanSelectedText'); + const openEvents = testGlobals.commandEvents.filter( + (event) => event.type === 'scanSelectedText', + ); assert.equal(openEvents.length, 0); const closeEvents = testGlobals.commandEvents.filter( (event) => event.type === 'setVisible' && event.visible === false, @@ -355,16 +357,28 @@ test('keyboard mode: up/down/j/k forward keydown to yomitan popup when open', as testGlobals.dispatchKeydown({ key: 'j', code: 'KeyJ' }); testGlobals.dispatchKeydown({ key: 'k', code: 'KeyK' }); - const forwarded = testGlobals.commandEvents.filter( - (event) => event.type === 'forwardKeyDown', - ); + const forwarded = testGlobals.commandEvents.filter((event) => event.type === 'forwardKeyDown'); assert.equal(forwarded.length, 4); - assert.equal(forwarded.some((event) => event.code === 'ArrowUp'), true); - assert.equal(forwarded.some((event) => event.code === 'ArrowDown'), true); - assert.equal(forwarded.some((event) => event.code === 'KeyJ'), true); - assert.equal(forwarded.some((event) => event.code === 'KeyK'), true); + assert.equal( + forwarded.some((event) => event.code === 'ArrowUp'), + true, + ); + assert.equal( + forwarded.some((event) => event.code === 'ArrowDown'), + true, + ); + assert.equal( + forwarded.some((event) => event.code === 'KeyJ'), + true, + ); + assert.equal( + forwarded.some((event) => event.code === 'KeyK'), + true, + ); - const openEvents = testGlobals.commandEvents.filter((event) => event.type === 'scanSelectedText'); + const openEvents = testGlobals.commandEvents.filter( + (event) => event.type === 'scanSelectedText', + ); assert.equal(openEvents.length, 0); const closeEvents = testGlobals.commandEvents.filter( (event) => event.type === 'setVisible' && event.visible === false, @@ -389,9 +403,7 @@ test('keyboard mode: repeated popup navigation keys are forwarded while popup is testGlobals.dispatchKeydown({ key: 'j', code: 'KeyJ', repeat: true }); testGlobals.dispatchKeydown({ key: 'ArrowDown', code: 'ArrowDown', repeat: true }); - const forwarded = testGlobals.commandEvents.filter( - (event) => event.type === 'forwardKeyDown', - ); + const forwarded = testGlobals.commandEvents.filter((event) => event.type === 'forwardKeyDown'); assert.equal(forwarded.length, 2); assert.deepEqual( forwarded.map((event) => ({ code: event.code, repeat: event.repeat })), @@ -445,7 +457,9 @@ test('keyboard mode: h moves left while popup is open and keeps lookup active', await wait(80); assert.equal(ctx.state.keyboardSelectedWordIndex, 1); - const openEvents = testGlobals.commandEvents.filter((event) => event.type === 'scanSelectedText'); + const openEvents = testGlobals.commandEvents.filter( + (event) => event.type === 'scanSelectedText', + ); assert.equal(openEvents.length > 0, true); const closeEvents = testGlobals.commandEvents.filter( (event) => event.type === 'setVisible' && event.visible === false, @@ -546,7 +560,9 @@ test('keyboard mode: popup-open edge jump refreshes lookup on the new subtitle s await wait(80); assert.equal(ctx.state.keyboardSelectedWordIndex, 0); - const openEvents = testGlobals.commandEvents.filter((event) => event.type === 'scanSelectedText'); + const openEvents = testGlobals.commandEvents.filter( + (event) => event.type === 'scanSelectedText', + ); assert.equal(openEvents.length > 0, true); } finally { ctx.state.keyboardDrivenModeEnabled = false; diff --git a/src/renderer/handlers/keyboard.ts b/src/renderer/handlers/keyboard.ts index 87f3f37..bde2db8 100644 --- a/src/renderer/handlers/keyboard.ts +++ b/src/renderer/handlers/keyboard.ts @@ -130,7 +130,9 @@ export function createKeyboardHandlers( } function getSubtitleWordNodes(): HTMLElement[] { - return Array.from(ctx.dom.subtitleRoot.querySelectorAll('.word[data-token-index]')); + return Array.from( + ctx.dom.subtitleRoot.querySelectorAll('.word[data-token-index]'), + ); } function syncKeyboardTokenSelection(): void { @@ -188,7 +190,9 @@ export function createKeyboardHandlers( setKeyboardDrivenModeEnabled(!ctx.state.keyboardDrivenModeEnabled); } - function moveKeyboardSelection(delta: -1 | 1): 'moved' | 'start-boundary' | 'end-boundary' | 'no-words' { + function moveKeyboardSelection( + delta: -1 | 1, + ): 'moved' | 'start-boundary' | 'end-boundary' | 'no-words' { const wordNodes = getSubtitleWordNodes(); if (wordNodes.length === 0) { ctx.state.keyboardSelectedWordIndex = null; diff --git a/src/renderer/handlers/mouse.test.ts b/src/renderer/handlers/mouse.test.ts index 84b0f8e..fd514a1 100644 --- a/src/renderer/handlers/mouse.test.ts +++ b/src/renderer/handlers/mouse.test.ts @@ -2,10 +2,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { createMouseHandlers } from './mouse.js'; -import { - YOMITAN_POPUP_HIDDEN_EVENT, - YOMITAN_POPUP_SHOWN_EVENT, -} from '../yomitan-popup.js'; +import { YOMITAN_POPUP_HIDDEN_EVENT, YOMITAN_POPUP_SHOWN_EVENT } from '../yomitan-popup.js'; function createClassList() { const classes = new Set();