Compare commits

..

42 Commits

Author SHA1 Message Date
sudacode e8f10fe8a9 fix: macOS visible-overlay blur no longer invokes Windows-only blur call
- Split win32/darwin branches in handleOverlayWindowBlurred so darwin visible blur returns early without calling onWindowsVisibleOverlayBlur
- Add regression test asserting Windows callback stays inactive on macOS visible overlay blur
- Close TASK-347
2026-05-12 02:50:05 -07:00
sudacode ca796bfe6a fix: macOS overlay z-order and Yomitan compound token known highlighting
- Release always-on-top when tracked mpv loses foreground on macOS
- Skip visible overlay blur restacking on macOS to avoid covering unrelated windows
- Prefer Yomitan internal parse tokens over fragmented scanner output for known-word decisions
- Add regression tests for both behaviors
2026-05-12 02:34:28 -07:00
sudacode 6bf905140c fix: address PR-57 CodeRabbit findings and CI failures
- use filtered word counts in media detail session token aggregation
- cancel fullscreen refresh burst on exit via updateLinuxMpvFullscreenOverlayRefreshBurst
- guard Hyprland JSON.parse in try/catch; exclude windowtitle from geometry events
- narrow focus suppression from :focus to :focus-visible
- apply JLPT lock selectors to word-name-match tokens (N1–N5)
2026-05-12 00:28:48 -07:00
sudacode 6e666d7ca5 fix: resolve media detail from sessions when lifetime summary is absent
- Change `getMediaDetail` JOIN to LEFT JOIN on `imm_lifetime_media` and fall back to aggregated session metrics when no lifetime row exists
- Add filter `AND (lm.video_id IS NOT NULL OR s.session_id IS NOT NULL)` to keep results valid
- Add regression test covering the session-visible / media-detail-missing mismatch
2026-05-11 23:52:56 -07:00
sudacode 27be0e6fd7 fix: address coderabbit subtitle follow-ups 2026-05-11 23:48:32 -07:00
sudacode eff33e2027 fix: keep macOS overlay interactive while mpv remains active
- Overlay no longer hides or becomes click-through during tracker refreshes when mpv is the focused window
- Preserve already-visible overlay when tracker is temporarily not ready but mpv target signal is active
- Add regression tests for active-mpv tracker refresh and transient tracker-not-ready paths
2026-05-11 02:35:05 -07:00
sudacode 47499eccff fix: map openCharacterDictionary session action to --open-character-dict
- Add missing Lua CLI dispatch entry for openCharacterDictionary
- Add regression test for Alt+Meta+A binding and CLI flag forwarding
2026-05-11 01:07:11 -07:00
sudacode 0b72fa108f fix: retain frequency rank for honorific prefix-noun tokens
- Add `shouldAllowHonorificPrefixNounFrequency` to exempt お/ご/御 + noun merged tokens from frequency exclusion
- Add regression test for `ご機嫌` asserting rank 5484 is preserved after MeCab enrichment and annotation
- Close TASK-341
2026-05-10 22:19:42 -07:00
sudacode 2b60c20711 fix: align Hyprland overlay windows to mpv and stop pinning them
- Force-apply exact Hyprland move/resize/setprop dispatches when bounds are provided
- Stop pinning overlay windows; toggle pin off when Hyprland reports pinned=true
- Compensate stats overlay outer placement for Electron/Wayland content insets
- Make stats overlay window and page opaque so mpv cannot show through transparent insets
- Constrain stats app to h-screen with internal scroll so content covers mpv from y=0
- Lock overlay/stats window titles against page-title-updated events
- Add regression coverage for placement dispatches, inset compensation, and CSS overlay mode
2026-05-10 22:19:42 -07:00
sudacode 8f43f8825d fix: restore subtitle playback keybindings 2026-05-10 22:19:42 -07:00
sudacode 5396b08972 fix: align Hyprland fullscreen overlays 2026-05-10 22:19:42 -07:00
sudacode 934a7281b0 fix: hide overlay focus ring 2026-05-10 22:19:42 -07:00
sudacode 4497d0a39f fix: retry transient AniList safeStorage failures 2026-05-10 22:19:42 -07:00
sudacode 2d1e51e7e1 fix: suppress known highlights for subtitle particles 2026-05-10 22:19:42 -07:00
sudacode c97888f811 fix: stop AniList setup reopening on Linux when keyring token exists
- Gate setup success on token persistence: `saveToken` now returns `boolean`; on failure, keeps the setup window open instead of reporting success
- Config reload passes `allowSetupPrompt: false` so playback reloads don't re-open the setup window
- Add regression test for persistence-failure path
2026-05-10 22:19:42 -07:00
sudacode 77f5a48f5d fix: address PR #57 CodeRabbit feedback
- Acquire AniList post-watch in-flight lock before async gating to prevent duplicate writes
- Isolate manual watched mark result from AniList post-watch callback failures
- Report known-word cache clears as mutations during immediate append when state existed
- Add regression tests for each fix
2026-05-10 22:19:42 -07:00
sudacode f2fb9fa1b9 fix: preserve known highlighting for filtered tokens 2026-05-10 22:19:42 -07:00
sudacode 3284c40ab5 fix: preserve ordinal frequency annotations 2026-05-10 22:19:42 -07:00
sudacode 4bd8fc3db4 fix: sync AniList after seeked completion 2026-05-10 22:19:42 -07:00
sudacode 837f21b346 fix: address coderabbit feedback 2026-05-10 22:19:42 -07:00
sudacode 12e1e783c9 Fix JLPT underline color drift and AniList skipped-threshold sync
- Replace JLPT `text-decoration` underlines with `border-bottom` so Chromium selection/hover cannot repaint them to another annotation's color
- Lock JLPT underline color for combined annotation selectors (known, n+1, frequency) and character hover/selection states
- Trigger AniList post-watch check on every mpv time-position update to catch skipped completion thresholds
- Fall back to filename-parser season/episode when guessit omits them
2026-05-10 22:19:42 -07:00
sudacode 42576d99b1 fix: keep subtitle prefetch alive after cache hits 2026-05-10 22:19:42 -07:00
sudacode a2fd3cd194 fix: restore stats daemon deferral 2026-05-10 22:19:42 -07:00
sudacode 805b68dd92 Preserve overlay across macOS flaps and mpv playlist changes
- keep visible overlays alive during transient macOS tracker loss
- reuse the running mpv overlay path on playlist navigation
- update regression coverage and changelog fragments
2026-05-10 22:19:42 -07:00
sudacode dacae39544 fix: CI changelog, annotation options threading, and Jellyfin quit
- Add `type: fixed` / `area:` frontmatter to `changes/319` to pass `changelog:lint`
- Thread `TokenizerAnnotationOptions` through `stripSubtitleAnnotationMetadata` so `sourceText` is honored
- Include `jellyfinPlay` in `shouldQuitOnDisconnectWhenOverlayRuntimeInitialized` predicate
- Make mouse test `elementFromPoint` stubs coordinate-sensitive
- Make Lua test `.tmp` mkdir portable on Windows
2026-05-10 22:19:42 -07:00
sudacode 41b2c7eccf Replace grammar-ending permutations with shared matcher; preserve word a
- Extract `grammar-ending.ts` with `isStandaloneGrammarEndingText` / `isSubtitleGrammarEndingText` pattern matchers
- Replace `STANDALONE_GRAMMAR_ENDINGS` set in parser-selection-stage with shared matcher
- Replace generated phrase sets in subtitle-annotation-filter with shared matcher
- Remove stale duplicate subtitle-exclusion constants and helpers from annotation-stage
- Manual clipboard card updates now write only to the sentence audio field, leaving word/expression audio untouched
2026-05-10 22:19:42 -07:00
sudacode 9c8784672c fix: preserve jlpt underline color after lookup 2026-05-10 22:19:42 -07:00
sudacode cb1650d366 fix: suppress sigh interjection annotations 2026-05-10 22:19:42 -07:00
sudacode f17255c8e2 fix: refresh current subtitle after known-word mining 2026-05-10 22:19:42 -07:00
sudacode 1c1f498f9e Fix managed playback exit and tokenizer grammar splits
- Ignore background stats daemons during regular app startup
- Split standalone grammar endings before applying annotations
- Clear helper-span annotations for auxiliary-only tokens
2026-05-10 22:19:42 -07:00
sudacode 939a0e650e Fix kana-only N+1 tokenizer regression test
- Use a pure-kana fixture for the subtitle token N+1 case
- Update task notes for the latest CodeRabbit follow-up
2026-05-10 22:19:42 -07:00
sudacode 166cdb06ec Suppress subtitle annotations for grammar fragments
- Hide annotation metadata for auxiliary inflection and ja-nai endings
- Preserve lexical `くれる` forms and add regression coverage
2026-05-10 22:19:42 -07:00
sudacode a69e5ecfdf fix: suppress N+1 for kana-only candidates and fix minSentenceWords coun
- Treat kana-only tokens with surrounding subtitle punctuation (…, ―, etc.) as kana-only so they are not promoted to N+1 targets
- Exclude unknown tokens filtered from N+1 targeting from the minSentenceWords count so filtered kana-only unknowns cannot satisfy sentence length threshold
- Add regression tests for kana-only candidate suppression and filtered-unknown padding cases
2026-05-10 22:19:42 -07:00
sudacode d991499dda Cancel pending Linux MPV fullscreen overlay refresh bursts
- return a cancel handle from the Linux refresh burst scheduler
- clear pending refresh bursts when overlays hide or windows close
- tighten the burst test polling to wait for the async refresh
2026-05-10 22:19:42 -07:00
sudacode 6053a1b6ac fix: accept modified digits for multi-line sentence mining 2026-05-10 22:19:42 -07:00
sudacode 5cc5df4b18 fix: address CodeRabbit review comments 2026-05-10 22:19:42 -07:00
sudacode d92a2072eb fix: address fullscreen and n-plus-one review notes 2026-05-10 22:19:42 -07:00
sudacode 2bb7be3552 fix: refresh overlay on Hyprland fullscreen 2026-05-10 22:19:42 -07:00
sudacode 0855a7dfcc fix: exclude kana-only n+1 targets 2026-05-10 22:19:42 -07:00
sudacode 077c852a08 fix: restore jlpt subtitle underlines 2026-05-10 22:19:42 -07:00
sudacode 09e10c18d2 fix(tokenizer): preserve annotation and enrichment behavior 2026-05-10 22:19:42 -07:00
sudacode 8b26559203 feat(tokenizer): use Yomitan word classes for subtitle POS filtering
- Carry matched headword wordClasses from termsFind into YomitanScanToken
- Map recognized Yomitan wordClasses to SubMiner coarse POS before annotation
- MeCab enrichment now fills only missing POS fields, preserving existing coarse pos1
- Exclude standalone grammar particles, して helper fragments, and single-kana surfaces from annotations
- Respect source-text punctuation gaps when counting N+1 sentence words
- Preserve known-word highlight on excluded kanji-containing tokens
- Add backlog tasks 304 (N+1 boundary bug) and 305 (wordClasses POS, done)
2026-05-10 22:19:42 -07:00
12 changed files with 40 additions and 665 deletions
@@ -1,55 +0,0 @@
---
id: TASK-358
title: Show and verify default keybindings in example config
status: Done
assignee:
- '@Codex'
created_date: '2026-05-13 03:33'
updated_date: '2026-05-13 03:45'
labels:
- config
- keybindings
- overlay
- mpv
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Keep the shipped example configuration, overlay runtime, and mpv plugin aligned with the built-in default keybindings. The example `keybindings` array should show the same defaults that are active by default, and focused tests should catch drift between documented defaults and actual overlay/mpv wiring.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 `config.example.jsonc` and the docs-site copy show all default keybindings in the `keybindings` array.
- [x] #2 Default keybindings are registered without conflicts in the overlay session-binding path.
- [x] #3 Default keybindings are registered and dispatched correctly inside the mpv plugin.
- [x] #4 Focused regression tests cover default keybinding/config-example parity and mpv/plugin dispatch.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Inspect default keybinding definitions, config-example generation, overlay shortcut/session-binding tests, and mpv plugin binding tests.
2. Add failing tests for config-example keybinding parity and any missing default overlay/mpv wiring.
3. Update generated/example config and source wiring only where tests show drift.
4. Run focused Bun/Lua tests, regenerate examples if needed, update task AC/final notes.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented the config-template path by injecting `DEFAULT_KEYBINDINGS` into generated examples when the resolved config has an empty `keybindings` array, preserving runtime merge semantics. Added coverage for template parity, default binding compile/action mapping, overlay keyboard dispatch, and mpv plugin registration/dispatch. Regenerated both `config.example.jsonc` artifacts and added changelog fragment `changes/358-default-keybindings-config-example.md`.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Updated generated example configuration so `config.example.jsonc` and `docs-site/public/config.example.jsonc` now show every built-in default keybinding in the `keybindings` array instead of `[]`. The template copy now describes the array as default plus custom keybindings, while runtime default merge behavior remains unchanged.
Added regression coverage that the generated template parses back to `DEFAULT_KEYBINDINGS`, that every default binding compiles to the expected mpv command or session action, that the overlay keyboard handler dispatches all compiled defaults, and that the mpv plugin registers and invokes default mpv/session-action bindings. Also updated docs tables to include the default fullscreen binding and clarified that keybindings can target mpv commands or SubMiner session actions.
Verification passed: `bun run format:check:src`, `bun run changelog:lint`, `bun run docs:test`, `bun run docs:build`, `bun run verify:config-example`, focused config/session/renderer/plugin tests, `bun run typecheck`, `bun run test:env`, `bun run test:fast`, `bun run build`, and `bun run test:smoke:dist`.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -1,4 +0,0 @@
type: changed
area: config
- Config: Expanded the generated example config so `keybindings` lists every built-in default and added regression coverage that those defaults compile, dispatch in the overlay, and register through the mpv plugin.
+2 -121
View File
@@ -183,130 +183,11 @@
// ========================================== // ==========================================
// Keybindings (MPV Commands) // Keybindings (MPV Commands)
// Default and custom keybindings that are merged with built-in defaults. // Extra keybindings that are merged with built-in defaults.
// Set command to null to disable a default keybinding. // Set command to null to disable a default keybinding.
// Hot-reload: keybinding changes apply live and update the session help modal on reopen. // Hot-reload: keybinding changes apply live and update the session help modal on reopen.
// ========================================== // ==========================================
"keybindings": [ "keybindings": [], // Extra keybindings that are merged with built-in defaults.
{
"key": "Space", // Key setting.
"command": [
"cycle",
"pause"
] // Command setting.
},
{
"key": "KeyF", // Key setting.
"command": [
"cycle",
"fullscreen"
] // Command setting.
},
{
"key": "KeyJ", // Key setting.
"command": [
"cycle",
"sid"
] // Command setting.
},
{
"key": "Shift+KeyJ", // Key setting.
"command": [
"cycle",
"secondary-sid"
] // Command setting.
},
{
"key": "ArrowRight", // Key setting.
"command": [
"seek",
5
] // Command setting.
},
{
"key": "ArrowLeft", // Key setting.
"command": [
"seek",
-5
] // Command setting.
},
{
"key": "ArrowUp", // Key setting.
"command": [
"seek",
60
] // Command setting.
},
{
"key": "ArrowDown", // Key setting.
"command": [
"seek",
-60
] // Command setting.
},
{
"key": "Shift+KeyH", // Key setting.
"command": [
"sub-seek",
-1
] // Command setting.
},
{
"key": "Shift+KeyL", // Key setting.
"command": [
"sub-seek",
1
] // Command setting.
},
{
"key": "Shift+BracketRight", // Key setting.
"command": [
"__sub-delay-next-line"
] // Command setting.
},
{
"key": "Shift+BracketLeft", // Key setting.
"command": [
"__sub-delay-prev-line"
] // Command setting.
},
{
"key": "Ctrl+Alt+KeyC", // Key setting.
"command": [
"__youtube-picker-open"
] // Command setting.
},
{
"key": "Ctrl+Alt+KeyP", // Key setting.
"command": [
"__playlist-browser-open"
] // Command setting.
},
{
"key": "Ctrl+Shift+KeyH", // Key setting.
"command": [
"__replay-subtitle"
] // Command setting.
},
{
"key": "Ctrl+Shift+KeyL", // Key setting.
"command": [
"__play-next-subtitle"
] // Command setting.
},
{
"key": "KeyQ", // Key setting.
"command": [
"quit"
] // Command setting.
},
{
"key": "Ctrl+KeyW", // Key setting.
"command": [
"quit"
] // Command setting.
}
], // Default and custom keybindings that are merged with built-in defaults.
// ========================================== // ==========================================
// Secondary Subtitles // Secondary Subtitles
+1 -2
View File
@@ -461,7 +461,7 @@ See `config.example.jsonc` for detailed configuration options.
### Keybindings ### Keybindings
Add a `keybindings` array to configure keyboard shortcuts that send mpv commands or SubMiner session actions: Add a `keybindings` array to configure keyboard shortcuts that send commands to mpv:
See `config.example.jsonc` for detailed configuration options and more examples. See `config.example.jsonc` for detailed configuration options and more examples.
@@ -470,7 +470,6 @@ See `config.example.jsonc` for detailed configuration options and more examples.
| Key | Command | Description | | Key | Command | Description |
| -------------------- | ----------------------------- | --------------------------------------- | | -------------------- | ----------------------------- | --------------------------------------- |
| `Space` | `["cycle", "pause"]` | Toggle pause | | `Space` | `["cycle", "pause"]` | Toggle pause |
| `KeyF` | `["cycle", "fullscreen"]` | Toggle fullscreen |
| `KeyJ` | `["cycle", "sid"]` | Cycle primary subtitle track | | `KeyJ` | `["cycle", "sid"]` | Cycle primary subtitle track |
| `Shift+KeyJ` | `["cycle", "secondary-sid"]` | Cycle secondary subtitle track | | `Shift+KeyJ` | `["cycle", "secondary-sid"]` | Cycle secondary subtitle track |
| `Ctrl+Alt+KeyP` | `["__playlist-browser-open"]` | Open playlist browser | | `Ctrl+Alt+KeyP` | `["__playlist-browser-open"]` | Open playlist browser |
+2 -121
View File
@@ -183,130 +183,11 @@
// ========================================== // ==========================================
// Keybindings (MPV Commands) // Keybindings (MPV Commands)
// Default and custom keybindings that are merged with built-in defaults. // Extra keybindings that are merged with built-in defaults.
// Set command to null to disable a default keybinding. // Set command to null to disable a default keybinding.
// Hot-reload: keybinding changes apply live and update the session help modal on reopen. // Hot-reload: keybinding changes apply live and update the session help modal on reopen.
// ========================================== // ==========================================
"keybindings": [ "keybindings": [], // Extra keybindings that are merged with built-in defaults.
{
"key": "Space", // Key setting.
"command": [
"cycle",
"pause"
] // Command setting.
},
{
"key": "KeyF", // Key setting.
"command": [
"cycle",
"fullscreen"
] // Command setting.
},
{
"key": "KeyJ", // Key setting.
"command": [
"cycle",
"sid"
] // Command setting.
},
{
"key": "Shift+KeyJ", // Key setting.
"command": [
"cycle",
"secondary-sid"
] // Command setting.
},
{
"key": "ArrowRight", // Key setting.
"command": [
"seek",
5
] // Command setting.
},
{
"key": "ArrowLeft", // Key setting.
"command": [
"seek",
-5
] // Command setting.
},
{
"key": "ArrowUp", // Key setting.
"command": [
"seek",
60
] // Command setting.
},
{
"key": "ArrowDown", // Key setting.
"command": [
"seek",
-60
] // Command setting.
},
{
"key": "Shift+KeyH", // Key setting.
"command": [
"sub-seek",
-1
] // Command setting.
},
{
"key": "Shift+KeyL", // Key setting.
"command": [
"sub-seek",
1
] // Command setting.
},
{
"key": "Shift+BracketRight", // Key setting.
"command": [
"__sub-delay-next-line"
] // Command setting.
},
{
"key": "Shift+BracketLeft", // Key setting.
"command": [
"__sub-delay-prev-line"
] // Command setting.
},
{
"key": "Ctrl+Alt+KeyC", // Key setting.
"command": [
"__youtube-picker-open"
] // Command setting.
},
{
"key": "Ctrl+Alt+KeyP", // Key setting.
"command": [
"__playlist-browser-open"
] // Command setting.
},
{
"key": "Ctrl+Shift+KeyH", // Key setting.
"command": [
"__replay-subtitle"
] // Command setting.
},
{
"key": "Ctrl+Shift+KeyL", // Key setting.
"command": [
"__play-next-subtitle"
] // Command setting.
},
{
"key": "KeyQ", // Key setting.
"command": [
"quit"
] // Command setting.
},
{
"key": "Ctrl+KeyW", // Key setting.
"command": [
"quit"
] // Command setting.
}
], // Default and custom keybindings that are merged with built-in defaults.
// ========================================== // ==========================================
// Secondary Subtitles // Secondary Subtitles
-1
View File
@@ -38,7 +38,6 @@ These control playback and subtitle display. They require overlay window focus.
| Shortcut | Action | | Shortcut | Action |
| -------------------- | --------------------------------------------------- | | -------------------- | --------------------------------------------------- |
| `Space` | Toggle mpv pause | | `Space` | Toggle mpv pause |
| `F` | Toggle fullscreen |
| `V` | Toggle primary subtitle bar visibility | | `V` | Toggle primary subtitle bar visibility |
| `J` | Cycle primary subtitle track | | `J` | Cycle primary subtitle track |
| `Shift+J` | Cycle secondary subtitle track | | `Shift+J` | Cycle secondary subtitle track |
+32 -194
View File
@@ -21,7 +21,6 @@ local recorded = {
bindings = {}, bindings = {},
removed = {}, removed = {},
async_calls = {}, async_calls = {},
mpv_commands = {},
osd = {}, osd = {},
} }
@@ -39,10 +38,6 @@ function mp.remove_key_binding(name)
recorded.removed[#recorded.removed + 1] = name recorded.removed[#recorded.removed + 1] = name
end end
function mp.commandv(...)
recorded.mpv_commands[#recorded.mpv_commands + 1] = { ... }
end
function mp.add_timeout(seconds, callback) function mp.add_timeout(seconds, callback)
return { return {
seconds = seconds, seconds = seconds,
@@ -76,126 +71,6 @@ local ctx = {
actionType = "session-action", actionType = "session-action",
actionId = "mineSentenceMultiple", actionId = "mineSentenceMultiple",
}, },
{
key = {
code = "Space",
modifiers = {},
},
actionType = "mpv-command",
command = { "cycle", "pause" },
},
{
key = {
code = "KeyF",
modifiers = {},
},
actionType = "mpv-command",
command = { "cycle", "fullscreen" },
},
{
key = {
code = "KeyJ",
modifiers = {},
},
actionType = "mpv-command",
command = { "cycle", "sid" },
},
{
key = {
code = "KeyJ",
modifiers = { "shift" },
},
actionType = "mpv-command",
command = { "cycle", "secondary-sid" },
},
{
key = {
code = "ArrowRight",
modifiers = {},
},
actionType = "mpv-command",
command = { "seek", 5 },
},
{
key = {
code = "ArrowLeft",
modifiers = {},
},
actionType = "mpv-command",
command = { "seek", -5 },
},
{
key = {
code = "ArrowUp",
modifiers = {},
},
actionType = "mpv-command",
command = { "seek", 60 },
},
{
key = {
code = "ArrowDown",
modifiers = {},
},
actionType = "mpv-command",
command = { "seek", -60 },
},
{
key = {
code = "KeyH",
modifiers = { "shift" },
},
actionType = "mpv-command",
command = { "sub-seek", -1 },
},
{
key = {
code = "KeyL",
modifiers = { "shift" },
},
actionType = "mpv-command",
command = { "sub-seek", 1 },
},
{
key = {
code = "BracketRight",
modifiers = { "shift" },
},
actionType = "session-action",
actionId = "shiftSubDelayNextLine",
},
{
key = {
code = "BracketLeft",
modifiers = { "shift" },
},
actionType = "session-action",
actionId = "shiftSubDelayPrevLine",
},
{
key = {
code = "KeyC",
modifiers = { "ctrl", "alt" },
},
actionType = "session-action",
actionId = "openYoutubePicker",
},
{
key = {
code = "KeyP",
modifiers = { "ctrl", "alt" },
},
actionType = "session-action",
actionId = "openPlaylistBrowser",
},
{
key = {
code = "KeyH",
modifiers = { "ctrl", "shift" },
},
actionType = "session-action",
actionId = "replayCurrentSubtitle",
},
{ {
key = { key = {
code = "KeyL", code = "KeyL",
@@ -204,22 +79,6 @@ local ctx = {
actionType = "session-action", actionType = "session-action",
actionId = "playNextSubtitle", actionId = "playNextSubtitle",
}, },
{
key = {
code = "KeyQ",
modifiers = {},
},
actionType = "mpv-command",
command = { "quit" },
},
{
key = {
code = "KeyW",
modifiers = { "ctrl" },
},
actionType = "mpv-command",
command = { "quit" },
},
{ {
key = { key = {
code = "KeyA", code = "KeyA",
@@ -228,6 +87,14 @@ local ctx = {
actionType = "session-action", actionType = "session-action",
actionId = "openCharacterDictionary", actionId = "openCharacterDictionary",
}, },
{
key = {
code = "KeyL",
modifiers = { "shift" },
},
actionType = "mpv-command",
command = { "sub-seek", 1 },
},
}, },
}, nil }, nil
end, end,
@@ -262,66 +129,31 @@ local ctx = {
local bindings = session_bindings.create(ctx) local bindings = session_bindings.create(ctx)
assert_true(bindings.register_bindings(), "session bindings should register") assert_true(bindings.register_bindings(), "session bindings should register")
local function find_binding(keys) local starter = nil
for _, binding in ipairs(recorded.bindings) do for _, binding in ipairs(recorded.bindings) do
if binding.keys == keys then if binding.keys == "Ctrl+S" then
return binding starter = binding
end break
end end
return nil
end end
local starter = find_binding("Ctrl+S")
assert_true(starter ~= nil, "multi-mine starter binding should be registered") assert_true(starter ~= nil, "multi-mine starter binding should be registered")
local expected_mpv_bindings = { local play_next = nil
{ keys = "SPACE", command = { "cycle", "pause" } }, for _, binding in ipairs(recorded.bindings) do
{ keys = "f", command = { "cycle", "fullscreen" } }, if binding.keys == "Ctrl+L" then
{ keys = "j", command = { "cycle", "sid" } }, play_next = binding
{ keys = "J", command = { "cycle", "secondary-sid" } }, break
{ keys = "RIGHT", command = { "seek", 5 } },
{ keys = "LEFT", command = { "seek", -5 } },
{ keys = "UP", command = { "seek", 60 } },
{ keys = "DOWN", command = { "seek", -60 } },
{ keys = "H", command = { "sub-seek", -1 } },
{ keys = "L", command = { "sub-seek", 1 } },
{ keys = "q", command = { "quit" } },
{ keys = "Ctrl+w", command = { "quit" } },
}
for _, expected in ipairs(expected_mpv_bindings) do
local binding = find_binding(expected.keys)
assert_true(binding ~= nil, "default mpv binding should register " .. expected.keys)
binding.fn()
local command = recorded.mpv_commands[#recorded.mpv_commands]
assert_true(command ~= nil, "default mpv binding should invoke mpv command " .. expected.keys)
for index, value in ipairs(expected.command) do
assert_true(command[index] == value, "default mpv command mismatch for " .. expected.keys)
end end
end end
local expected_cli_bindings = {
{ keys = "Shift+]", flag = "--shift-sub-delay-next-line" },
{ keys = "Shift+[", flag = "--shift-sub-delay-prev-line" },
{ keys = "Ctrl+Alt+c", flag = "--open-youtube-picker" },
{ keys = "Ctrl+Alt+p", flag = "--open-playlist-browser" },
{ keys = "Ctrl+H", flag = "--replay-current-subtitle" },
{ keys = "Ctrl+L", flag = "--play-next-subtitle" },
}
for _, expected in ipairs(expected_cli_bindings) do
local binding = find_binding(expected.keys)
assert_true(binding ~= nil, "default session action should register " .. expected.keys)
binding.fn()
local cli_call = recorded.async_calls[#recorded.async_calls]
assert_true(cli_call ~= nil, "default session action should invoke CLI " .. expected.keys)
assert_true(cli_call[2] == expected.flag, "default session action should pass " .. expected.flag)
end
local play_next = find_binding("Ctrl+L")
assert_true(play_next ~= nil, "play-next subtitle binding should use mpv shifted-letter form") assert_true(play_next ~= nil, "play-next subtitle binding should use mpv shifted-letter form")
local subtitle_jump = find_binding("L") local subtitle_jump = nil
for _, binding in ipairs(recorded.bindings) do
if binding.keys == "L" then
subtitle_jump = binding
break
end
end
assert_true(subtitle_jump ~= nil, "shifted subtitle jump binding should use mpv uppercase letter form") assert_true(subtitle_jump ~= nil, "shifted subtitle jump binding should use mpv uppercase letter form")
play_next.fn() play_next.fn()
@@ -329,7 +161,13 @@ local play_next_call = recorded.async_calls[#recorded.async_calls]
assert_true(play_next_call ~= nil, "play-next binding should invoke CLI action") assert_true(play_next_call ~= nil, "play-next binding should invoke CLI action")
assert_true(play_next_call[2] == "--play-next-subtitle", "play-next binding should pass CLI flag") assert_true(play_next_call[2] == "--play-next-subtitle", "play-next binding should pass CLI flag")
local character_dictionary = find_binding("Alt+Meta+a") local character_dictionary = nil
for _, binding in ipairs(recorded.bindings) do
if binding.keys == "Alt+Meta+a" then
character_dictionary = binding
break
end
end
assert_true(character_dictionary ~= nil, "character dictionary binding should be registered") assert_true(character_dictionary ~= nil, "character dictionary binding should be registered")
character_dictionary.fn() character_dictionary.fn()
+1 -16
View File
@@ -4,13 +4,7 @@ import * as fs from 'fs';
import * as os from 'os'; import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
import { ConfigService, ConfigStartupParseError } from './service'; import { ConfigService, ConfigStartupParseError } from './service';
import { import { DEFAULT_CONFIG, RUNTIME_OPTION_REGISTRY, deepMergeRawConfig } from './definitions';
DEFAULT_CONFIG,
DEFAULT_KEYBINDINGS,
RUNTIME_OPTION_REGISTRY,
deepMergeRawConfig,
} from './definitions';
import { parseConfigContent } from './parse';
import { generateConfigTemplate } from './template'; import { generateConfigTemplate } from './template';
function makeTempDir(): string { function makeTempDir(): string {
@@ -2223,12 +2217,3 @@ test('template generator includes known keys', () => {
/"launchAtStartup": true,? \/\/ Launch texthooker server automatically when SubMiner starts\. Values: true \| false/, /"launchAtStartup": true,? \/\/ Launch texthooker server automatically when SubMiner starts\. Values: true \| false/,
); );
}); });
test('template generator shows built-in default keybindings in the keybindings array', () => {
const output = generateConfigTemplate(DEFAULT_CONFIG);
const parsed = parseConfigContent('config.example.jsonc', output) as {
keybindings?: unknown;
};
assert.deepEqual(parsed.keybindings, DEFAULT_KEYBINDINGS);
});
+1 -1
View File
@@ -62,7 +62,7 @@ const CORE_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
{ {
title: 'Keybindings (MPV Commands)', title: 'Keybindings (MPV Commands)',
description: [ description: [
'Default and custom keybindings that are merged with built-in defaults.', 'Extra keybindings that are merged with built-in defaults.',
'Set command to null to disable a default keybinding.', 'Set command to null to disable a default keybinding.',
], ],
notes: [ notes: [
+1 -14
View File
@@ -3,7 +3,6 @@ import {
CONFIG_OPTION_REGISTRY, CONFIG_OPTION_REGISTRY,
CONFIG_TEMPLATE_SECTIONS, CONFIG_TEMPLATE_SECTIONS,
DEFAULT_CONFIG, DEFAULT_CONFIG,
DEFAULT_KEYBINDINGS,
deepCloneConfig, deepCloneConfig,
} from './definitions'; } from './definitions';
@@ -104,21 +103,9 @@ function renderSection(
return lines.join('\n'); return lines.join('\n');
} }
function createTemplateConfig(config: ResolvedConfig): ResolvedConfig {
const templateConfig = deepCloneConfig(config);
if (templateConfig.keybindings.length === 0) {
templateConfig.keybindings = DEFAULT_KEYBINDINGS.map((binding) => ({
key: binding.key,
command: binding.command === null ? null : [...binding.command],
}));
}
return templateConfig;
}
export function generateConfigTemplate( export function generateConfigTemplate(
config: ResolvedConfig = deepCloneConfig(DEFAULT_CONFIG), config: ResolvedConfig = deepCloneConfig(DEFAULT_CONFIG),
): string { ): string {
const templateConfig = createTemplateConfig(config);
const lines: string[] = []; const lines: string[] = [];
lines.push('/**'); lines.push('/**');
lines.push(' * SubMiner Example Configuration File'); lines.push(' * SubMiner Example Configuration File');
@@ -136,7 +123,7 @@ export function generateConfigTemplate(
lines.push( lines.push(
renderSection( renderSection(
section.key, section.key,
templateConfig[section.key], config[section.key],
index === CONFIG_TEMPLATE_SECTIONS.length - 1, index === CONFIG_TEMPLATE_SECTIONS.length - 1,
comments, comments,
), ),
@@ -209,41 +209,6 @@ test('compileSessionBindings keeps default replay and next subtitle session acti
assert.equal(next?.actionId, 'playNextSubtitle'); assert.equal(next?.actionId, 'playNextSubtitle');
}); });
test('compileSessionBindings wires every default keybinding to an overlay or mpv action', () => {
const expectedSpecialActions: Record<string, string> = {
[SPECIAL_COMMANDS.SHIFT_SUB_DELAY_TO_PREVIOUS_SUBTITLE_START]: 'shiftSubDelayPrevLine',
[SPECIAL_COMMANDS.SHIFT_SUB_DELAY_TO_NEXT_SUBTITLE_START]: 'shiftSubDelayNextLine',
[SPECIAL_COMMANDS.YOUTUBE_PICKER_OPEN]: 'openYoutubePicker',
[SPECIAL_COMMANDS.PLAYLIST_BROWSER_OPEN]: 'openPlaylistBrowser',
[SPECIAL_COMMANDS.REPLAY_SUBTITLE]: 'replayCurrentSubtitle',
[SPECIAL_COMMANDS.PLAY_NEXT_SUBTITLE]: 'playNextSubtitle',
};
const result = compileSessionBindings({
shortcuts: createShortcuts(),
keybindings: DEFAULT_KEYBINDINGS,
platform: 'linux',
});
assert.deepEqual(result.warnings, []);
const byOriginalKey = new Map(result.bindings.map((binding) => [binding.originalKey, binding]));
assert.equal(byOriginalKey.size, DEFAULT_KEYBINDINGS.length);
for (const defaultBinding of DEFAULT_KEYBINDINGS) {
const compiled = byOriginalKey.get(defaultBinding.key);
assert.ok(compiled, `${defaultBinding.key} should compile`);
const specialAction = expectedSpecialActions[String(defaultBinding.command?.[0])];
if (specialAction) {
assert.equal(compiled.actionType, 'session-action');
assert.equal(compiled.actionId, specialAction);
continue;
}
assert.equal(compiled.actionType, 'mpv-command');
assert.deepEqual(compiled.command, defaultBinding.command);
}
});
test('compileSessionBindings omits disabled bindings', () => { test('compileSessionBindings omits disabled bindings', () => {
const result = compileSessionBindings({ const result = compileSessionBindings({
shortcuts: createShortcuts({ shortcuts: createShortcuts({
-101
View File
@@ -4,9 +4,6 @@ import test from 'node:test';
import { createKeyboardHandlers } from './keyboard.js'; import { createKeyboardHandlers } from './keyboard.js';
import { createRendererState } from '../state.js'; import { createRendererState } from '../state.js';
import type { CompiledSessionBinding } from '../../types'; import type { CompiledSessionBinding } from '../../types';
import { DEFAULT_KEYBINDINGS, SPECIAL_COMMANDS } from '../../config/definitions';
import { compileSessionBindings } from '../../core/services/session-bindings';
import type { ConfiguredShortcuts } from '../../core/utils/shortcut-config';
import { YOMITAN_POPUP_COMMAND_EVENT, YOMITAN_POPUP_HIDDEN_EVENT } from '../yomitan-popup.js'; import { YOMITAN_POPUP_COMMAND_EVENT, YOMITAN_POPUP_HIDDEN_EVENT } from '../yomitan-popup.js';
type CommandEventDetail = { type CommandEventDetail = {
@@ -43,58 +40,6 @@ function wait(ms: number): Promise<void> {
}); });
} }
function eventFromKeyString(keyString: string): {
key: string;
code: string;
ctrlKey?: boolean;
metaKey?: boolean;
altKey?: boolean;
shiftKey?: boolean;
} {
const parts = keyString.split('+');
const code = parts.pop() ?? '';
return {
key: code === 'Space' ? ' ' : code,
code,
ctrlKey: parts.includes('Ctrl'),
metaKey: parts.includes('Meta'),
altKey: parts.includes('Alt'),
shiftKey: parts.includes('Shift'),
};
}
function countedJsonValues(values: unknown[]): Array<[string, number]> {
const counts = new Map<string, number>();
for (const value of values) {
const key = JSON.stringify(value);
counts.set(key, (counts.get(key) ?? 0) + 1);
}
return [...counts.entries()].sort(([left], [right]) => left.localeCompare(right));
}
function createEmptyShortcuts(): ConfiguredShortcuts {
return {
toggleVisibleOverlayGlobal: null,
copySubtitle: null,
copySubtitleMultiple: null,
updateLastCardFromClipboard: null,
triggerFieldGrouping: null,
triggerSubsync: null,
mineSentence: null,
mineSentenceMultiple: null,
multiCopyTimeoutMs: 3000,
toggleSecondarySub: null,
markAudioCard: null,
openCharacterDictionary: null,
openRuntimeOptions: null,
openJimaku: null,
openSessionHelp: null,
openControllerSelect: null,
openControllerDebug: null,
toggleSubtitleSidebar: null,
};
}
function installKeyboardTestGlobals() { function installKeyboardTestGlobals() {
const previousWindow = (globalThis as { window?: unknown }).window; const previousWindow = (globalThis as { window?: unknown }).window;
const previousDocument = (globalThis as { document?: unknown }).document; const previousDocument = (globalThis as { document?: unknown }).document;
@@ -764,52 +709,6 @@ test('popup-visible mpv keybindings still fire for bound keys', async () => {
} }
}); });
test('default keybindings dispatch through overlay keyboard handling', async () => {
const { handlers, testGlobals } = createKeyboardHandlerHarness();
const specialActionIds: Record<string, string> = {
[SPECIAL_COMMANDS.SHIFT_SUB_DELAY_TO_PREVIOUS_SUBTITLE_START]: 'shiftSubDelayPrevLine',
[SPECIAL_COMMANDS.SHIFT_SUB_DELAY_TO_NEXT_SUBTITLE_START]: 'shiftSubDelayNextLine',
[SPECIAL_COMMANDS.YOUTUBE_PICKER_OPEN]: 'openYoutubePicker',
[SPECIAL_COMMANDS.PLAYLIST_BROWSER_OPEN]: 'openPlaylistBrowser',
[SPECIAL_COMMANDS.REPLAY_SUBTITLE]: 'replayCurrentSubtitle',
[SPECIAL_COMMANDS.PLAY_NEXT_SUBTITLE]: 'playNextSubtitle',
};
const compiled = compileSessionBindings({
shortcuts: createEmptyShortcuts(),
keybindings: DEFAULT_KEYBINDINGS,
platform: 'linux',
});
try {
assert.deepEqual(compiled.warnings, []);
await handlers.setupMpvInputForwarding();
handlers.updateSessionBindings(compiled.bindings);
for (const binding of DEFAULT_KEYBINDINGS) {
testGlobals.dispatchKeydown(eventFromKeyString(binding.key));
}
await wait(0);
const expectedMpvCommands = DEFAULT_KEYBINDINGS.filter(
(binding) => !specialActionIds[String(binding.command?.[0])],
).map((binding) => binding.command);
const expectedSessionActions = DEFAULT_KEYBINDINGS.map(
(binding) => specialActionIds[String(binding.command?.[0])],
).filter(Boolean);
assert.deepEqual(
countedJsonValues(testGlobals.mpvCommands),
countedJsonValues(expectedMpvCommands),
);
assert.deepEqual(
testGlobals.sessionActions.map((action) => action.actionId).sort(),
expectedSessionActions.sort(),
);
} finally {
testGlobals.restore();
}
});
test('paused configured subtitle-jump keybinding re-applies pause after backward seek', async () => { test('paused configured subtitle-jump keybinding re-applies pause after backward seek', async () => {
const { handlers, testGlobals } = createKeyboardHandlerHarness(); const { handlers, testGlobals } = createKeyboardHandlerHarness();