feat(stats): wire stats server, overlay, and CLI into main process

- Stats server auto-start on immersion tracker init
- Stats overlay toggle via keybinding and IPC
- Stats CLI command (subminer stats) with cleanup mode
- mpv plugin menu integration for stats toggle
- CLI args for --stats, --stats-cleanup, --stats-response-path
This commit is contained in:
2026-03-14 22:14:32 -07:00
parent a7c294a90c
commit 6d8650994f
37 changed files with 374 additions and 23 deletions

View File

@@ -51,6 +51,8 @@ function installKeyboardTestGlobals() {
const commandEvents: CommandEventDetail[] = [];
const mpvCommands: Array<Array<string | number>> = [];
let playbackPausedResponse: boolean | null = false;
let statsToggleKey = 'Backquote';
let statsToggleOverlayCalls = 0;
let selectionClearCount = 0;
let selectionAddCount = 0;
@@ -137,7 +139,11 @@ function installKeyboardTestGlobals() {
mpvCommands.push(command);
},
getPlaybackPaused: async () => playbackPausedResponse,
getStatsToggleKey: async () => statsToggleKey,
toggleDevTools: () => {},
toggleStatsOverlay: () => {
statsToggleOverlayCalls += 1;
},
focusMainWindow: () => {
focusMainWindowCalls += 1;
return Promise.resolve();
@@ -253,6 +259,10 @@ function installKeyboardTestGlobals() {
setPopupVisible: (value: boolean) => {
popupVisible = value;
},
setStatsToggleKey: (value: string) => {
statsToggleKey = value;
},
statsToggleOverlayCalls: () => statsToggleOverlayCalls,
getPlaybackPaused: async () => playbackPausedResponse,
setPlaybackPausedResponse: (value: boolean | null) => {
playbackPausedResponse = value;
@@ -548,6 +558,22 @@ test('keyboard mode: controller select modal handles arrow keys before yomitan p
}
});
test('keyboard mode: configured stats toggle works even while popup is open', async () => {
const { handlers, testGlobals } = createKeyboardHandlerHarness();
try {
testGlobals.setPopupVisible(true);
testGlobals.setStatsToggleKey('KeyG');
await handlers.setupMpvInputForwarding();
testGlobals.dispatchKeydown({ key: 'g', code: 'KeyG' });
assert.equal(testGlobals.statsToggleOverlayCalls(), 1);
} finally {
testGlobals.restore();
}
});
test('keyboard mode: h moves left when popup is closed', async () => {
const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness();

View File

@@ -181,6 +181,17 @@ export function createKeyboardHandlers(
return !e.ctrlKey && !e.metaKey && e.altKey && !e.repeat && e.code === 'KeyC';
}
function isStatsOverlayToggle(e: KeyboardEvent): boolean {
return (
e.code === ctx.state.statsToggleKey &&
!e.ctrlKey &&
!e.altKey &&
!e.metaKey &&
!e.shiftKey &&
!e.repeat
);
}
function getSubtitleWordNodes(): HTMLElement[] {
return Array.from(
ctx.dom.subtitleRoot.querySelectorAll<HTMLElement>('.word[data-token-index]'),
@@ -693,7 +704,12 @@ export function createKeyboardHandlers(
}
async function setupMpvInputForwarding(): Promise<void> {
updateKeybindings(await window.electronAPI.getKeybindings());
const [keybindings, statsToggleKey] = await Promise.all([
window.electronAPI.getKeybindings(),
window.electronAPI.getStatsToggleKey(),
]);
updateKeybindings(keybindings);
ctx.state.statsToggleKey = statsToggleKey;
syncKeyboardTokenSelection();
const subtitleMutationObserver = new MutationObserver(() => {
@@ -789,6 +805,12 @@ export function createKeyboardHandlers(
return;
}
if (isStatsOverlayToggle(e)) {
e.preventDefault();
window.electronAPI.toggleStatsOverlay();
return;
}
if (
(ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document)) &&
!isControllerModalShortcut(e)

View File

@@ -91,6 +91,7 @@ export type RendererState = {
frequencyDictionaryBand5Color: string;
keybindingsMap: Map<string, (string | number)[]>;
statsToggleKey: string;
chordPending: boolean;
chordTimeout: ReturnType<typeof setTimeout> | null;
keyboardDrivenModeEnabled: boolean;
@@ -170,6 +171,7 @@ export function createRendererState(): RendererState {
frequencyDictionaryBand5Color: '#8aadf4',
keybindingsMap: new Map(),
statsToggleKey: 'Backquote',
chordPending: false,
chordTimeout: null,
keyboardDrivenModeEnabled: false,