feat: replace y-j with configurable Jimaku shortcut

This commit is contained in:
2026-02-09 21:28:56 -08:00
parent f905539179
commit a4df79feb1
15 changed files with 276 additions and 12 deletions

View File

@@ -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

View File

@@ -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)
<video controls playsinline preload="metadata" poster="/assets/kiku-integration-poster.jpg" style="width: 100%; max-width: 960px;">
<source :src="'/assets/kiku-integration.webm'" type="video/webm" />
Your browser does not support the video tag.
</video>
<a :href="'/assets/kiku-integration.webm'" target="_blank" rel="noreferrer">Open demo in a new tab</a>
| 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.

View File

@@ -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
```

View File

@@ -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"
]
}
}

View File

@@ -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

View File

@@ -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"]);

View File

@@ -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: [],

View File

@@ -214,6 +214,7 @@ export class ConfigService {
"toggleSecondarySub",
"markAudioCard",
"openRuntimeOptions",
"openJimaku",
] as const;
for (const key of shortcutKeys) {

View File

@@ -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: () => {

View File

@@ -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);
}
}

View File

@@ -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,
),
};
}

View File

@@ -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");
},
});
}

View File

@@ -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);
},

View File

@@ -1953,7 +1953,6 @@ const CHORD_MAP = new Map<string, ChordAction>([
["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<void> {
window.electronAPI.notifyOverlayModalClosed("runtime-options");
});
});
window.electronAPI.onOpenJimaku(() => {
openJimakuModal();
});
window.electronAPI.onSubsyncManualOpen((payload: SubsyncManualPayload) => {
openSubsyncModal(payload);
});

View File

@@ -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;
}