From 5c600b0cbeaa62e25fa0ed803a7a758a7ab93820 Mon Sep 17 00:00:00 2001 From: sudacode Date: Mon, 9 Feb 2026 21:28:56 -0800 Subject: [PATCH] feat: replace y-j with configurable Jimaku shortcut --- README.md | 1 + docs/configuration.md | 13 +- docs/installation.md | 12 +- docs/public/config.example.jsonc | 211 ++++++++++++++++++ docs/usage.md | 6 +- src/config/config.test.ts | 4 +- src/config/definitions.ts | 1 + src/config/service.ts | 1 + .../overlay-shortcut-fallback-runner.ts | 7 + src/core/services/overlay-shortcut-service.ts | 11 + src/core/utils/shortcut-config.ts | 4 + src/main.ts | 6 + src/preload.ts | 5 + src/renderer/renderer.ts | 4 +- src/types.ts | 2 + 15 files changed, 276 insertions(+), 12 deletions(-) create mode 100644 docs/public/config.example.jsonc diff --git a/README.md b/README.md index ae8f009..2520e3c 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ cp plugin/subminer.conf ~/.config/mpv/script-opts/ Requires mpv IPC: `--input-ipc-server=/tmp/subminer-socket` Default chord prefix: `y` (`y-y` menu, `y-s` start, `y-S` stop, `y-t` toggle visible layer). +Overlay Jimaku shortcut default: `Ctrl+Alt+J` (`shortcuts.openJimaku`). ## Documentation diff --git a/docs/configuration.md b/docs/configuration.md index 0573166..f5da729 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -4,7 +4,7 @@ Settings are stored in `~/.config/SubMiner/config.jsonc` ### Configuration File -See `config.example.jsonc` for a comprehensive example configuration file with all available options, default values, and detailed comments. Only include the options you want to customize in your config file. +See [config.example.jsonc](/config.example.jsonc) for a comprehensive example configuration file with all available options, default values, and detailed comments. Only include the options you want to customize in your config file. Generate a fresh default config from the centralized config registry: @@ -17,7 +17,7 @@ subminer.AppImage --generate-config --backup-overwrite - `--generate-config` writes a default JSONC config template. - If the target file exists, SubMiner prompts to create a timestamped backup and overwrite. - In non-interactive shells, use `--backup-overwrite` to explicitly back up and overwrite. -- `pnpm run generate:config-example` regenerates repository `config.example.jsonc` from the same centralized defaults. +- `pnpm run generate:config-example` regenerates both repository `config.example.jsonc` and docs-served `/config.example.jsonc` from the same centralized defaults. - `make generate-config` builds and runs the same default-config generator via local Electron. Invalid config values are handled with warn-and-fallback behavior: SubMiner logs the bad key/value and continues with the default for that option. @@ -164,7 +164,12 @@ When enabled, sentence cards automatically set `IsSentenceCard` to `"x"` and pop Kiku extends Lapis with **field grouping** — when a duplicate card is detected (same Word/Expression), SubMiner merges the two cards' content into one using Kiku's `data-group-id` HTML structure, organizing each mining instance into separate pages within the note. -[![Field Grouping Demo](assets/kiku-integration-poster.jpg)](https://github.com/user-attachments/assets/bf2476cb-2351-4622-8143-c90e59b19213) + + +Open demo in a new tab | Mode | Behavior | @@ -416,6 +421,7 @@ See `config.example.jsonc` for detailed configuration options. "mineSentenceMultiple": "CommandOrControl+Shift+S", "markAudioCard": "CommandOrControl+Shift+A", "openRuntimeOptions": "CommandOrControl+Shift+O", + "openJimaku": "Ctrl+Alt+J", "multiCopyTimeoutMs": 3000 } } @@ -436,6 +442,7 @@ See `config.example.jsonc` for detailed configuration options. | `toggleSecondarySub` | string \| `null` | Accelerator for cycling secondary subtitle mode (default: `"CommandOrControl+Shift+V"`) | | `markAudioCard` | string \| `null` | Accelerator for marking last card as audio card (default: `"CommandOrControl+Shift+A"`) | | `openRuntimeOptions` | string \| `null` | Opens runtime options palette for live session-only toggles (default: `"CommandOrControl+Shift+O"`) | +| `openJimaku` | string \| `null` | Opens the Jimaku search modal (default: `"Ctrl+Alt+J"`) | **See `config.example.jsonc`** for the complete list of shortcut configuration options. diff --git a/docs/installation.md b/docs/installation.md index c642334..3b84f23 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -6,14 +6,14 @@ - Hyprland (uses `hyprctl`) - X11 (uses `xdotool` and `xwininfo`) - mpv (with IPC socket support) -- mecab and mecab-ipadic (Japanese morphological analyzer) +- mecab and mecab-ipadic (optional fallback Japanese morphological analyzer) - fuse2 (for AppImage support) ### macOS - macOS 10.13 or later - mpv (with IPC socket support) -- mecab and mecab-ipadic (Japanese morphological analyzer) - optional +- mecab and mecab-ipadic (optional fallback Japanese morphological analyzer) - **Accessibility permission** required for window tracking (see [macOS Installation](#macos-installation)) **Optional:** @@ -154,8 +154,9 @@ binary_path=/Applications/SubMiner.app/Contents/MacOS/subminer The Lua plugin allows you to control the overlay directly from mpv using keybindings: -> [!IMPORTANT] -> `mpv` must be launched with `--input-ipc-server=/tmp/subminer-socket` to allow communication with the application +::: warning Important +`mpv` must be launched with `--input-ipc-server=/tmp/subminer-socket` to allow communication with the application. +::: ```bash # Copy plugin files to mpv config @@ -182,6 +183,8 @@ All keybindings use chord sequences starting with `y`: The menu provides options to start/stop/toggle the visible or invisible overlay layers and open settings. Type to filter or use arrow keys to navigate. +Jimaku modal shortcut is configured separately in SubMiner overlay shortcuts (`shortcuts.openJimaku`), default `Ctrl+Alt+J`. + #### Plugin Configuration Edit `~/.config/mpv/script-opts/subminer.conf`: @@ -243,4 +246,3 @@ Launch mpv with: ```bash mpv --input-ipc-server=\\\\.\\pipe\\subminer-socket video.mkv ``` - diff --git a/docs/public/config.example.jsonc b/docs/public/config.example.jsonc new file mode 100644 index 0000000..a1aebff --- /dev/null +++ b/docs/public/config.example.jsonc @@ -0,0 +1,211 @@ +/** + * SubMiner Example Configuration File + * + * This file is auto-generated from src/config/definitions.ts. + * Copy to ~/.config/SubMiner/config.jsonc and edit as needed. + */ +{ + + // ========================================== + // Overlay Auto-Start + // When overlay connects to mpv, automatically show overlay and hide mpv subtitles. + // ========================================== + "auto_start_overlay": false, + + // ========================================== + // Visible Overlay Subtitle Binding + // Control whether visible overlay toggles also toggle MPV subtitle visibility. + // When enabled, visible overlay hides MPV subtitles; when disabled, MPV subtitles are left unchanged. + // ========================================== + "bind_visible_overlay_to_mpv_sub_visibility": true, + + // ========================================== + // Texthooker Server + // Control whether browser opens automatically for texthooker. + // ========================================== + "texthooker": { + "openBrowser": true + }, + + // ========================================== + // WebSocket Server + // Built-in WebSocket server broadcasts subtitle text to connected clients. + // Auto mode disables built-in server if mpv_websocket is detected. + // ========================================== + "websocket": { + "enabled": "auto", + "port": 6677 + }, + + // ========================================== + // AnkiConnect Integration + // Automatic Anki updates and media generation options. + // ========================================== + "ankiConnect": { + "enabled": false, + "url": "http://127.0.0.1:8765", + "pollingRate": 3000, + "fields": { + "audio": "ExpressionAudio", + "image": "Picture", + "sentence": "Sentence", + "miscInfo": "MiscInfo", + "translation": "SelectionText" + }, + "ai": { + "enabled": false, + "alwaysUseAiTranslation": false, + "apiKey": "", + "model": "openai/gpt-4o-mini", + "baseUrl": "https://openrouter.ai/api", + "targetLanguage": "English", + "systemPrompt": "You are a translation engine. Return only the translated text with no explanations." + }, + "media": { + "generateAudio": true, + "generateImage": true, + "imageType": "static", + "imageFormat": "jpg", + "imageQuality": 92, + "animatedFps": 10, + "animatedMaxWidth": 640, + "animatedCrf": 35, + "audioPadding": 0.5, + "fallbackDuration": 3, + "maxMediaDuration": 30 + }, + "behavior": { + "overwriteAudio": true, + "overwriteImage": true, + "mediaInsertMode": "append", + "highlightWord": true, + "notificationType": "osd", + "autoUpdateNewCards": true + }, + "metadata": { + "pattern": "[SubMiner] %f (%t)" + }, + "isLapis": { + "enabled": false, + "sentenceCardModel": "Japanese sentences", + "sentenceCardSentenceField": "Sentence", + "sentenceCardAudioField": "SentenceAudio" + }, + "isKiku": { + "enabled": false, + "fieldGrouping": "disabled", + "deleteDuplicateInAuto": true + } + }, + + // ========================================== + // Keyboard Shortcuts + // Overlay keyboard shortcuts. Set a shortcut to null to disable. + // ========================================== + "shortcuts": { + "toggleVisibleOverlayGlobal": "Alt+Shift+O", + "toggleInvisibleOverlayGlobal": "Alt+Shift+I", + "copySubtitle": "CommandOrControl+C", + "copySubtitleMultiple": "CommandOrControl+Shift+C", + "updateLastCardFromClipboard": "CommandOrControl+V", + "triggerFieldGrouping": "CommandOrControl+G", + "triggerSubsync": "Ctrl+Alt+S", + "mineSentence": "CommandOrControl+S", + "mineSentenceMultiple": "CommandOrControl+Shift+S", + "multiCopyTimeoutMs": 3000, + "toggleSecondarySub": "CommandOrControl+Shift+V", + "markAudioCard": "CommandOrControl+Shift+A", + "openRuntimeOptions": "CommandOrControl+Shift+O", + "openJimaku": "Ctrl+Alt+J" + }, + + // ========================================== + // Invisible Overlay + // Startup behavior for the invisible interactive subtitle mining layer. + // ========================================== + "invisibleOverlay": { + "startupVisibility": "platform-default" + }, + + // ========================================== + // Keybindings (MPV Commands) + // Extra keybindings that are merged with built-in defaults. + // Set command to null to disable a default keybinding. + // ========================================== + "keybindings": [], + + // ========================================== + // Subtitle Appearance + // Primary and secondary subtitle styling. + // ========================================== + "subtitleStyle": { + "fontFamily": "Noto Sans CJK JP Regular, Noto Sans CJK JP, Arial Unicode MS, Arial, sans-serif", + "fontSize": 35, + "fontColor": "#cad3f5", + "fontWeight": "normal", + "fontStyle": "normal", + "backgroundColor": "rgba(54, 58, 79, 0.5)", + "secondary": { + "fontSize": 24, + "fontColor": "#ffffff", + "backgroundColor": "transparent", + "fontWeight": "normal", + "fontStyle": "normal", + "fontFamily": "Noto Sans CJK JP Regular, Noto Sans CJK JP, Arial Unicode MS, Arial, sans-serif" + } + }, + + // ========================================== + // Secondary Subtitles + // Dual subtitle track options. + // Used by subminer YouTube subtitle generation as secondary language preferences. + // ========================================== + "secondarySub": { + "secondarySubLanguages": [], + "autoLoadSecondarySub": false, + "defaultMode": "hover" + }, + + // ========================================== + // Auto Subtitle Sync + // Subsync engine and executable paths. + // ========================================== + "subsync": { + "defaultMode": "auto", + "alass_path": "", + "ffsubsync_path": "", + "ffmpeg_path": "" + }, + + // ========================================== + // Subtitle Position + // Initial vertical subtitle position from the bottom. + // ========================================== + "subtitlePosition": { + "yPercent": 10 + }, + + // ========================================== + // Jimaku + // Jimaku API configuration and defaults. + // ========================================== + "jimaku": { + "apiBaseUrl": "https://jimaku.cc", + "languagePreference": "ja", + "maxEntryResults": 10 + }, + + // ========================================== + // YouTube Subtitle Generation + // Defaults for subminer YouTube subtitle extraction/transcription mode. + // ========================================== + "youtubeSubgen": { + "mode": "automatic", + "whisperBin": "", + "whisperModel": "", + "primarySubLanguages": [ + "ja", + "jpn" + ] + } +} diff --git a/docs/usage.md b/docs/usage.md index 120d7da..8996fe3 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -7,6 +7,8 @@ There are two ways to use SubMiner: | **subminer script** | All-in-one solution. Handles video selection, launches MPV with the correct socket, starts the overlay automatically, and cleans up on exit. | | **MPV plugin** | When you launch MPV yourself or from other tools. Provides in-MPV chord keybindings (e.g. `y-y` for menu) to control visible and invisible overlay layers. Requires `--input-ipc-server=/tmp/subminer-socket`. | +Jimaku modal shortcut is an overlay shortcut, not an MPV plugin chord: default `Ctrl+Alt+J` via `shortcuts.openJimaku`. + You can use both together—install the plugin for on-demand control, but use `subminer` when you want the streamlined workflow. `subminer` is implemented as a Bun script and runs directly via shebang (no `bun run` needed), for example: `subminer video.mkv`. @@ -113,7 +115,7 @@ Notes: | `Right-click` | Toggle MPV pause (outside subtitle area) | | `Right-click + drag` | Move subtitle position (on subtitle) | -These keybindings only work when the overlay window has focus. See [Configuration](configuration.md) for customization. +These keybindings only work when the overlay window has focus. See [Configuration](/configuration) for customization. ### Overlay Chord Shortcuts @@ -125,7 +127,7 @@ These keybindings only work when the overlay window has focus. See [Configuratio 1. MPV runs with an IPC socket at `/tmp/subminer-socket` 2. The overlay connects and subscribes to subtitle changes -3. Subtitles are tokenized with MeCab and merged into natural word boundaries +3. Subtitles are tokenized with Yomitan's internal parser, with MeCab fallback when needed 4. Words are displayed as clickable spans 5. Clicking a word triggers Yomitan popup for dictionary lookup 6. Texthooker server runs at `http://127.0.0.1:5174` for external tools diff --git a/src/config/config.test.ts b/src/config/config.test.ts index c5adcfd..f7f19d1 100644 --- a/src/config/config.test.ts +++ b/src/config/config.test.ts @@ -43,7 +43,8 @@ test("parses invisible overlay config and new global shortcuts", () => { `{ "shortcuts": { "toggleVisibleOverlayGlobal": "Alt+Shift+U", - "toggleInvisibleOverlayGlobal": "Alt+Shift+I" + "toggleInvisibleOverlayGlobal": "Alt+Shift+I", + "openJimaku": "Ctrl+Alt+J" }, "invisibleOverlay": { "startupVisibility": "hidden" @@ -60,6 +61,7 @@ test("parses invisible overlay config and new global shortcuts", () => { const config = service.getConfig(); assert.equal(config.shortcuts.toggleVisibleOverlayGlobal, "Alt+Shift+U"); assert.equal(config.shortcuts.toggleInvisibleOverlayGlobal, "Alt+Shift+I"); + assert.equal(config.shortcuts.openJimaku, "Ctrl+Alt+J"); assert.equal(config.invisibleOverlay.startupVisibility, "hidden"); assert.equal(config.bind_visible_overlay_to_mpv_sub_visibility, false); assert.deepEqual(config.youtubeSubgen.primarySubLanguages, ["ja", "jpn", "jp"]); diff --git a/src/config/definitions.ts b/src/config/definitions.ts index 8e31e6c..9ddb819 100644 --- a/src/config/definitions.ts +++ b/src/config/definitions.ts @@ -152,6 +152,7 @@ export const DEFAULT_CONFIG: ResolvedConfig = { toggleSecondarySub: "CommandOrControl+Shift+V", markAudioCard: "CommandOrControl+Shift+A", openRuntimeOptions: "CommandOrControl+Shift+O", + openJimaku: "Ctrl+Alt+J", }, secondarySub: { secondarySubLanguages: [], diff --git a/src/config/service.ts b/src/config/service.ts index 5977181..db3f3ca 100644 --- a/src/config/service.ts +++ b/src/config/service.ts @@ -214,6 +214,7 @@ export class ConfigService { "toggleSecondarySub", "markAudioCard", "openRuntimeOptions", + "openJimaku", ] as const; for (const key of shortcutKeys) { diff --git a/src/core/services/overlay-shortcut-fallback-runner.ts b/src/core/services/overlay-shortcut-fallback-runner.ts index 85ef5ce..859bbf0 100644 --- a/src/core/services/overlay-shortcut-fallback-runner.ts +++ b/src/core/services/overlay-shortcut-fallback-runner.ts @@ -2,6 +2,7 @@ import { ConfiguredShortcuts } from "../utils/shortcut-config"; export interface OverlayShortcutFallbackHandlers { openRuntimeOptions: () => void; + openJimaku: () => void; markAudioCard: () => void; copySubtitleMultiple: (timeoutMs: number) => void; copySubtitle: () => void; @@ -34,6 +35,12 @@ export function runOverlayShortcutLocalFallback( handlers.openRuntimeOptions(); }, }, + { + accelerator: shortcuts.openJimaku, + run: () => { + handlers.openJimaku(); + }, + }, { accelerator: shortcuts.markAudioCard, run: () => { diff --git a/src/core/services/overlay-shortcut-service.ts b/src/core/services/overlay-shortcut-service.ts index 0ca937c..7bab1ce 100644 --- a/src/core/services/overlay-shortcut-service.ts +++ b/src/core/services/overlay-shortcut-service.ts @@ -13,6 +13,7 @@ export interface OverlayShortcutHandlers { toggleSecondarySub: () => void; markAudioCard: () => void; openRuntimeOptions: () => void; + openJimaku: () => void; } export function registerOverlayShortcutsService( @@ -118,6 +119,13 @@ export function registerOverlayShortcutsService( "openRuntimeOptions", ); } + if (shortcuts.openJimaku) { + registerOverlayShortcut( + shortcuts.openJimaku, + () => handlers.openJimaku(), + "openJimaku", + ); + } return registeredAny; } @@ -155,4 +163,7 @@ export function unregisterOverlayShortcutsService( if (shortcuts.openRuntimeOptions) { globalShortcut.unregister(shortcuts.openRuntimeOptions); } + if (shortcuts.openJimaku) { + globalShortcut.unregister(shortcuts.openJimaku); + } } diff --git a/src/core/utils/shortcut-config.ts b/src/core/utils/shortcut-config.ts index e65f1be..ff6c918 100644 --- a/src/core/utils/shortcut-config.ts +++ b/src/core/utils/shortcut-config.ts @@ -14,6 +14,7 @@ export interface ConfiguredShortcuts { toggleSecondarySub: string | null | undefined; markAudioCard: string | null | undefined; openRuntimeOptions: string | null | undefined; + openJimaku: string | null | undefined; } export function resolveConfiguredShortcuts( @@ -78,5 +79,8 @@ export function resolveConfiguredShortcuts( config.shortcuts?.openRuntimeOptions ?? defaultConfig.shortcuts?.openRuntimeOptions, ), + openJimaku: normalizeShortcut( + config.shortcuts?.openJimaku ?? defaultConfig.shortcuts?.openJimaku, + ), }; } diff --git a/src/main.ts b/src/main.ts index ac0f576..34c74d4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2265,6 +2265,9 @@ function tryHandleOverlayShortcutLocalFallback(input: Electron.Input): boolean { openRuntimeOptions: () => { openRuntimeOptionsPalette(); }, + openJimaku: () => { + sendToVisibleOverlay("jimaku:open"); + }, markAudioCard: () => { markLastCardAsAudioCard().catch((err) => { console.error("markLastCardAsAudioCard failed:", err); @@ -2644,6 +2647,9 @@ function registerOverlayShortcuts(): void { openRuntimeOptions: () => { openRuntimeOptionsPalette(); }, + openJimaku: () => { + sendToVisibleOverlay("jimaku:open"); + }, }); } diff --git a/src/preload.ts b/src/preload.ts index 71dfa17..2d9c482 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -259,6 +259,11 @@ const electronAPI: ElectronAPI = { callback(); }); }, + onOpenJimaku: (callback: () => void) => { + ipcRenderer.on("jimaku:open", () => { + callback(); + }); + }, notifyOverlayModalClosed: (modal: "runtime-options" | "subsync") => { ipcRenderer.send("overlay:modal-closed", modal); }, diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index aa02489..1284cfc 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -1953,7 +1953,6 @@ const CHORD_MAP = new Map([ ["KeyR", { type: "mpv", command: ["script-message", "subminer-restart"] }], ["KeyC", { type: "mpv", command: ["script-message", "subminer-status"] }], ["KeyY", { type: "mpv", command: ["script-message", "subminer-menu"] }], - ["KeyJ", { type: "electron", action: () => openJimakuModal() }], [ "KeyD", { type: "electron", action: () => window.electronAPI.toggleDevTools() }, @@ -2398,6 +2397,9 @@ async function init(): Promise { window.electronAPI.notifyOverlayModalClosed("runtime-options"); }); }); + window.electronAPI.onOpenJimaku(() => { + openJimakuModal(); + }); window.electronAPI.onSubsyncManualOpen((payload: SubsyncManualPayload) => { openSubsyncModal(payload); }); diff --git a/src/types.ts b/src/types.ts index 77f58f0..f5a8499 100644 --- a/src/types.ts +++ b/src/types.ts @@ -275,6 +275,7 @@ export interface ShortcutsConfig { toggleSecondarySub?: string | null; markAudioCard?: string | null; openRuntimeOptions?: string | null; + openJimaku?: string | null; } export type JimakuLanguagePreference = "ja" | "en" | "none"; @@ -606,6 +607,7 @@ export interface ElectronAPI { callback: (options: RuntimeOptionState[]) => void, ) => void; onOpenRuntimeOptions: (callback: () => void) => void; + onOpenJimaku: (callback: () => void) => void; notifyOverlayModalClosed: (modal: "runtime-options" | "subsync") => void; }