mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-11-10 04:46:07 -08:00
* fix(overlay): reload imported mpv keys after mpv connects The overlay fetched mpv's input-bindings before SubMiner connected to mpv, cached an empty list, and never refetched because the connect-time refresh only broadcast session bindings when their compiled signature changed. Keys from input.conf and mpv defaults (9/0 volume, m mute) then did nothing while the overlay had focus, most often in mpv.backend x11 mode. Main now emits mpv-input-bindings:changed whenever the discovered mpv key set or client changes, and the renderer refreshes its imported keys on it. * fix(overlay): subscribe to mpv key changes before initial discovery The renderer registered the mpv-input-bindings:changed listener only after awaiting setupMpvInputForwarding and several other startup calls. If mpv connected in that window, the event was dropped and the imported mpv keys stayed empty.
178 lines
7.0 KiB
TypeScript
178 lines
7.0 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
import type { CompiledSessionBinding, ResolvedConfig } from '../../types';
|
|
import { createSessionBindingsRuntime } from './session-bindings-runtime';
|
|
|
|
test('persistSessionBindings logs and does not publish bindings when artifact write fails', () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-session-bindings-runtime-'));
|
|
const configDir = path.join(root, 'config-file');
|
|
fs.writeFileSync(configDir, 'not a directory');
|
|
const calls: string[] = [];
|
|
const runtime = createSessionBindingsRuntime({
|
|
configDir,
|
|
getKeybindings: () => [],
|
|
getConfiguredShortcuts: () => ({ multiCopyTimeoutMs: 1500 }) as never,
|
|
getResolvedConfig: () =>
|
|
({
|
|
stats: { toggleKey: 's', markWatchedKey: 'w' },
|
|
}) as ResolvedConfig,
|
|
getMpvClient: () => null,
|
|
setSessionBindings: () => calls.push('setSessionBindings'),
|
|
setSessionBindingsInitialized: () => calls.push('setSessionBindingsInitialized'),
|
|
logWarn: (message) => calls.push(`warn:${message}`),
|
|
});
|
|
|
|
try {
|
|
assert.throws(
|
|
() => runtime.persistSessionBindings([] as CompiledSessionBinding[]),
|
|
/ENOTDIR|EEXIST/,
|
|
);
|
|
assert.deepEqual(calls, ['warn:[session-bindings] Failed to write session bindings artifact']);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('persistSessionBindings keeps saved bindings when mpv reload notification fails', () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-session-bindings-runtime-'));
|
|
const calls: string[] = [];
|
|
const runtime = createSessionBindingsRuntime({
|
|
configDir: root,
|
|
getKeybindings: () => [],
|
|
getConfiguredShortcuts: () => ({ multiCopyTimeoutMs: 1500 }) as never,
|
|
getResolvedConfig: () =>
|
|
({
|
|
stats: { toggleKey: 's', markWatchedKey: 'w' },
|
|
}) as ResolvedConfig,
|
|
getMpvClient: () =>
|
|
({
|
|
connected: true,
|
|
send: () => {
|
|
throw new Error('mpv unavailable');
|
|
},
|
|
}) as never,
|
|
setSessionBindings: () => calls.push('setSessionBindings'),
|
|
setSessionBindingsInitialized: () => calls.push('setSessionBindingsInitialized'),
|
|
logWarn: (message) => calls.push(`warn:${message}`),
|
|
});
|
|
|
|
try {
|
|
assert.doesNotThrow(() => runtime.persistSessionBindings([] as CompiledSessionBinding[]));
|
|
assert.deepEqual(calls, [
|
|
'setSessionBindings',
|
|
'setSessionBindingsInitialized',
|
|
'warn:[session-bindings] Failed to notify mpv to reload session bindings',
|
|
]);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('mpv input binding discovery notifies the overlay when the native key set changes', async () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-session-native-keys-'));
|
|
let nativeKeys: unknown = [{ key: '9', cmd: 'add volume -5', priority: 25 }];
|
|
let notifications = 0;
|
|
const client = {
|
|
connected: false,
|
|
send: () => {},
|
|
requestProperty: async () => nativeKeys,
|
|
};
|
|
const runtime = createSessionBindingsRuntime({
|
|
configDir: root,
|
|
getKeybindings: () => [],
|
|
getConfiguredShortcuts: () => ({ multiCopyTimeoutMs: 1500 }) as never,
|
|
getResolvedConfig: () => ({ stats: { toggleKey: 's', markWatchedKey: 'w' } }) as ResolvedConfig,
|
|
getMpvClient: () => client,
|
|
setSessionBindings: () => {},
|
|
setSessionBindingsInitialized: () => {},
|
|
logWarn: () => {},
|
|
onMpvInputBindingsChanged: () => {
|
|
notifications += 1;
|
|
},
|
|
});
|
|
try {
|
|
runtime.persistSessionBindings([]);
|
|
await runtime.refreshMpvSessionBindings();
|
|
assert.equal(notifications, 0, 'nothing to announce before mpv connects');
|
|
client.connected = true;
|
|
await runtime.refreshMpvSessionBindings();
|
|
assert.equal(notifications, 1, 'first discovery after connecting must reach the overlay');
|
|
await runtime.refreshMpvSessionBindings();
|
|
assert.equal(notifications, 1, 'unchanged discovery must not create a refresh loop');
|
|
nativeKeys = [{ key: '0', cmd: 'ignore', priority: 25 }];
|
|
await runtime.refreshMpvSessionBindings();
|
|
assert.equal(notifications, 2, 'ignored keys still change what the overlay may forward');
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('native prefix conflicts publish the same effective bindings to the overlay and plugin and recover', async () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-session-conflict-'));
|
|
const sequence: CompiledSessionBinding = {
|
|
sourcePath: 'shortcuts.openSubtitleSelection',
|
|
originalKey: 'g-s',
|
|
key: { code: 'KeyG-KeyS', modifiers: [] },
|
|
actionType: 'session-action',
|
|
actionId: 'openSubtitleSelection',
|
|
};
|
|
let nativeKeys: unknown = [];
|
|
let failDiscovery = false;
|
|
let published: CompiledSessionBinding[] = [];
|
|
const events: CompiledSessionBinding[][] = [];
|
|
const warnings: string[] = [];
|
|
const client = {
|
|
connected: true,
|
|
send: () => {},
|
|
requestProperty: async () => {
|
|
if (failDiscovery) throw new Error('temporarily unavailable');
|
|
return nativeKeys;
|
|
},
|
|
};
|
|
const runtime = createSessionBindingsRuntime({
|
|
configDir: root,
|
|
getKeybindings: () => [],
|
|
getConfiguredShortcuts: () => ({ multiCopyTimeoutMs: 1500 }) as never,
|
|
getResolvedConfig: () => ({ stats: { toggleKey: 's', markWatchedKey: 'w' } }) as ResolvedConfig,
|
|
getMpvClient: () => client,
|
|
setSessionBindings: (bindings) => {
|
|
published = bindings;
|
|
},
|
|
setSessionBindingsInitialized: () => {},
|
|
logWarn: () => {},
|
|
onBindingsChanged: (bindings) => events.push(bindings),
|
|
onWarning: (warning) => warnings.push(warning.message),
|
|
});
|
|
const readArtifact = () =>
|
|
JSON.parse(fs.readFileSync(path.join(root, 'session-bindings.json'), 'utf8'));
|
|
try {
|
|
runtime.persistSessionBindings([sequence]);
|
|
nativeKeys = [{ key: 'g', cmd: 'show-text single', priority: 1 }];
|
|
await runtime.refreshMpvSessionBindings();
|
|
assert.deepEqual(published, []);
|
|
assert.deepEqual(events.at(-1), readArtifact().bindings);
|
|
assert.equal(warnings.length, 1);
|
|
assert.match(warnings[0]!, /mpv input binding "g"/);
|
|
await runtime.refreshMpvSessionBindings();
|
|
assert.equal(events.length, 2, 'unchanged discovery must not create a reload loop');
|
|
assert.equal(warnings.length, 1);
|
|
failDiscovery = true;
|
|
await runtime.refreshMpvSessionBindings();
|
|
assert.deepEqual(published, [], 'failed discovery retains the known conflict');
|
|
failDiscovery = false;
|
|
nativeKeys = [{ key: 'Shift+g', cmd: 'show-text shifted', priority: 1 }];
|
|
await runtime.refreshMpvSessionBindings();
|
|
assert.deepEqual(published, [sequence]);
|
|
assert.equal(readArtifact().bindings[0].key.code, 'KeyG-KeyS');
|
|
assert.deepEqual(readArtifact().warnings, []);
|
|
client.connected = false;
|
|
await runtime.refreshMpvSessionBindings();
|
|
assert.deepEqual(published, [sequence]);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|