feat(overlay): forward mpv mouse and wheel bindings through overlay

- Accept WHEEL_UP/DOWN/LEFT/RIGHT (with modifiers) in keybindings, the plugin, and the settings key editor
- Forward mpv's MBTN_* and WHEEL_* bindings from the overlay so double-click fullscreen, wheel volume, and similar inputs work on Hyprland; SubMiner bindings and right-click pause still win
- Notify the overlay via mpv-input-bindings:changed when mpv's key set changes, fixing dead imported keys when the overlay loads before mpv connects
- Sync README requirements/quick start with the installation guide and fix the Arch MeCab install command (AUR mecab-git)
This commit is contained in:
sudacode committed 2026-10-01 01:16:44 -07:00
commit a6df21bdc2
29 files changed
+643 -157

No files matched your search

@@ -71,6 +71,45 @@ test('persistSessionBindings keeps saved bindings when mpv reload notification f
}
});
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 = {
+13 -1
View File
@@ -25,6 +25,10 @@ export interface SessionBindingsRuntimeDeps {
setSessionBindingsInitialized: (initialized: boolean) => void;
logWarn: (message: string, details?: unknown) => void;
onBindingsChanged?: (bindings: CompiledSessionBinding[]) => void;
// Fires when mpv's own key bindings change, including the first discovery after
// connecting. The overlay's imported mpv keys depend on them even when the
// compiled session bindings stay identical.
onMpvInputBindingsChanged?: () => void;
onWarning?: (warning: SessionBindingWarning) => void;
}
@@ -41,6 +45,7 @@ export function createSessionBindingsRuntime(deps: SessionBindingsRuntimeDeps):
let nativeSnapshot: {
client: ReturnType<SessionBindingsRuntimeDeps['getMpvClient']>;
keys: string[];
signature: string;
} | null = null;
let pending: {
client: ReturnType<SessionBindingsRuntimeDeps['getMpvClient']>;
@@ -135,8 +140,15 @@ export function createSessionBindingsRuntime(deps: SessionBindingsRuntimeDeps):
try {
const raw = await client.requestProperty('input-bindings');
if (client !== deps.getMpvClient() || !client.connected) return;
nativeSnapshot = { client, keys: parseMpvInputBindingKeys(raw, { includeIgnored: false }) };
const signature = JSON.stringify(parseMpvInputBindingKeys(raw));
const changed = nativeSnapshot?.client !== client || nativeSnapshot.signature !== signature;
nativeSnapshot = {
client,
keys: parseMpvInputBindingKeys(raw, { includeIgnored: false }),
signature,
};
publishBindings();
if (changed) deps.onMpvInputBindingsChanged?.();
} catch {
// Keep the last successful snapshot if discovery is temporarily unavailable.
}