feat(controller): add inline remap modal with descriptor-based bindings (#21)

This commit is contained in:
2026-03-15 15:55:45 -07:00
committed by GitHub
parent 9eed37420e
commit 478869ff28
38 changed files with 3136 additions and 1431 deletions

View File

@@ -0,0 +1,54 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { applyControllerConfigUpdate } from './controller-config-update.js';
test('applyControllerConfigUpdate replaces binding descriptors instead of deep-merging them', () => {
const next = applyControllerConfigUpdate(
{
preferredGamepadId: 'pad-1',
bindings: {
toggleLookup: { kind: 'axis', axisIndex: 4, direction: 'positive' },
closeLookup: { kind: 'button', buttonIndex: 1 },
},
},
{
bindings: {
toggleLookup: { kind: 'button', buttonIndex: 11 },
},
},
);
assert.deepEqual(next.bindings?.toggleLookup, { kind: 'button', buttonIndex: 11 });
assert.deepEqual(next.bindings?.closeLookup, { kind: 'button', buttonIndex: 1 });
});
test('applyControllerConfigUpdate merges buttonIndices while replacing only updated binding leaves', () => {
const next = applyControllerConfigUpdate(
{
buttonIndices: {
select: 6,
buttonSouth: 0,
},
bindings: {
toggleLookup: { kind: 'button', buttonIndex: 0 },
closeLookup: { kind: 'button', buttonIndex: 1 },
},
},
{
buttonIndices: {
buttonSouth: 9,
},
bindings: {
closeLookup: { kind: 'none' },
},
},
);
assert.deepEqual(next.buttonIndices, {
select: 6,
buttonSouth: 9,
});
assert.deepEqual(next.bindings?.toggleLookup, { kind: 'button', buttonIndex: 0 });
assert.deepEqual(next.bindings?.closeLookup, { kind: 'none' });
});

View File

@@ -0,0 +1,38 @@
import type { ControllerConfigUpdate, RawConfig } from '../types';
type RawControllerConfig = NonNullable<RawConfig['controller']>;
type RawControllerBindings = NonNullable<RawControllerConfig['bindings']>;
export function applyControllerConfigUpdate(
currentController: RawConfig['controller'] | undefined,
update: ControllerConfigUpdate,
): RawControllerConfig {
const nextController: RawControllerConfig = {
...(currentController ?? {}),
...update,
};
if (currentController?.buttonIndices || update.buttonIndices) {
nextController.buttonIndices = {
...(currentController?.buttonIndices ?? {}),
...(update.buttonIndices ?? {}),
};
}
if (currentController?.bindings || update.bindings) {
const nextBindings: RawControllerBindings = {
...(currentController?.bindings ?? {}),
};
for (const [key, value] of Object.entries(update.bindings ?? {}) as Array<
[keyof RawControllerBindings, RawControllerBindings[keyof RawControllerBindings] | undefined]
>) {
if (value === undefined) continue;
(nextBindings as Record<string, unknown>)[key] = JSON.parse(JSON.stringify(value));
}
nextController.bindings = nextBindings;
}
return nextController;
}

View File

@@ -73,6 +73,7 @@ export interface MainIpcRuntimeServiceDepsParams {
getKeybindings: IpcDepsRuntimeOptions['getKeybindings'];
getConfiguredShortcuts: IpcDepsRuntimeOptions['getConfiguredShortcuts'];
getControllerConfig: IpcDepsRuntimeOptions['getControllerConfig'];
saveControllerConfig: IpcDepsRuntimeOptions['saveControllerConfig'];
saveControllerPreference: IpcDepsRuntimeOptions['saveControllerPreference'];
getSecondarySubMode: IpcDepsRuntimeOptions['getSecondarySubMode'];
getMpvClient: IpcDepsRuntimeOptions['getMpvClient'];
@@ -216,6 +217,7 @@ export function createMainIpcRuntimeServiceDeps(
getKeybindings: params.getKeybindings,
getConfiguredShortcuts: params.getConfiguredShortcuts,
getControllerConfig: params.getControllerConfig,
saveControllerConfig: params.saveControllerConfig,
saveControllerPreference: params.saveControllerPreference,
focusMainWindow: params.focusMainWindow ?? (() => {}),
getSecondarySubMode: params.getSecondarySubMode,

View File

@@ -52,6 +52,7 @@ test('composeIpcRuntimeHandlers returns callable IPC handlers and registration b
getKeybindings: () => [],
getConfiguredShortcuts: () => ({}) as never,
getControllerConfig: () => ({}) as never,
saveControllerConfig: () => {},
saveControllerPreference: () => {},
getSecondarySubMode: () => 'hover' as never,
getMpvClient: () => null,