fix(controller): save remaps per profile, gate modals on enabled (#69)

This commit is contained in:
2026-05-16 20:43:27 -07:00
committed by GitHub
parent 49f89e6452
commit 5250ca8214
31 changed files with 1639 additions and 463 deletions
@@ -93,6 +93,7 @@ function createControllerConfig(
...(buttonIndexOverrides ?? {}),
}),
},
profiles: {},
...restOverrides,
};
}
@@ -449,6 +450,60 @@ test('gamepad controller maps left stick horizontal movement to token selection
assert.deepEqual(calls, [1, 1, -1]);
});
test('gamepad controller uses active controller profile bindings before global bindings', () => {
let lookupToggles = 0;
const buttons = Array.from({ length: 12 }, () => ({
value: 0,
pressed: false,
touched: false,
}));
buttons[11] = { value: 1, pressed: true, touched: true };
const controller = createGamepadController({
getGamepads: () => [createGamepad('pad-profile', { buttons })],
getConfig: () =>
({
...createControllerConfig({
bindings: {
toggleLookup: { kind: 'button', buttonIndex: 0 },
},
}),
profiles: {
'pad-profile': {
label: 'Profile Pad',
buttonIndices: DEFAULT_BUTTON_INDICES,
bindings: {
...createControllerConfig().bindings,
toggleLookup: { kind: 'button', buttonIndex: 11 },
},
},
},
}) as ResolvedControllerConfig,
getKeyboardModeEnabled: () => true,
getLookupWindowOpen: () => false,
getInteractionBlocked: () => false,
toggleKeyboardMode: () => {},
toggleLookup: () => {
lookupToggles += 1;
},
closeLookup: () => {},
moveSelection: () => {},
mineCard: () => {},
quitMpv: () => {},
previousAudio: () => {},
nextAudio: () => {},
playCurrentAudio: () => {},
toggleMpvPause: () => {},
scrollPopup: () => {},
jumpPopup: () => {},
onState: () => {},
});
controller.poll(0);
assert.equal(lookupToggles, 1);
});
test('gamepad controller maps L1 play-current, R1 next-audio, and popup navigation', () => {
const calls: string[] = [];
const scrollCalls: number[] = [];
+49 -34
View File
@@ -5,6 +5,7 @@ import type {
ResolvedControllerConfig,
ResolvedControllerDiscreteBinding,
} from '../../types';
import { resolveControllerConfigForGamepad } from '../controller-profile-config.js';
type ControllerButtonState = {
value: number;
@@ -410,87 +411,101 @@ export function createGamepadController(options: GamepadControllerOptions) {
resetHeldAction(jumpHold);
}
let interactionAllowed =
config.enabled && options.getKeyboardModeEnabled() && !options.getInteractionBlocked();
if (config.enabled) {
const activeConfig = resolveControllerConfigForGamepad(config, activeGamepad.id);
if (activeConfig.enabled) {
handleActionEdge(
'toggleKeyboardOnlyMode',
config.bindings.toggleKeyboardOnlyMode,
activeConfig.bindings.toggleKeyboardOnlyMode,
activeGamepad,
config,
activeConfig,
options.toggleKeyboardMode,
);
}
interactionAllowed =
config.enabled && options.getKeyboardModeEnabled() && !options.getInteractionBlocked();
const interactionAllowed =
activeConfig.enabled && options.getKeyboardModeEnabled() && !options.getInteractionBlocked();
if (!interactionAllowed) {
syncBlockedInteractionState(activeGamepad, config, now);
syncBlockedInteractionState(activeGamepad, activeConfig, now);
return;
}
handleActionEdge(
'toggleLookup',
config.bindings.toggleLookup,
activeConfig.bindings.toggleLookup,
activeGamepad,
config,
activeConfig,
options.toggleLookup,
);
handleActionEdge(
'closeLookup',
config.bindings.closeLookup,
activeConfig.bindings.closeLookup,
activeGamepad,
config,
activeConfig,
options.closeLookup,
);
handleActionEdge('mineCard', config.bindings.mineCard, activeGamepad, config, options.mineCard);
handleActionEdge('quitMpv', config.bindings.quitMpv, activeGamepad, config, options.quitMpv);
handleActionEdge(
'mineCard',
activeConfig.bindings.mineCard,
activeGamepad,
activeConfig,
options.mineCard,
);
handleActionEdge(
'quitMpv',
activeConfig.bindings.quitMpv,
activeGamepad,
activeConfig,
options.quitMpv,
);
const activationThreshold = Math.max(config.stickDeadzone, 0.55);
const activationThreshold = Math.max(activeConfig.stickDeadzone, 0.55);
if (options.getLookupWindowOpen()) {
handleActionEdge(
'previousAudio',
config.bindings.previousAudio,
activeConfig.bindings.previousAudio,
activeGamepad,
config,
activeConfig,
options.previousAudio,
);
handleActionEdge(
'nextAudio',
config.bindings.nextAudio,
activeConfig.bindings.nextAudio,
activeGamepad,
config,
activeConfig,
options.nextAudio,
);
handleActionEdge(
'playCurrentAudio',
config.bindings.playCurrentAudio,
activeConfig.bindings.playCurrentAudio,
activeGamepad,
config,
activeConfig,
options.playCurrentAudio,
);
const primaryScroll = resolveAxisBindingValue(
activeGamepad,
config.bindings.leftStickVertical,
config.triggerDeadzone,
config.stickDeadzone,
activeConfig.bindings.leftStickVertical,
activeConfig.triggerDeadzone,
activeConfig.stickDeadzone,
);
if (elapsedMs > 0 && Math.abs(primaryScroll) >= config.stickDeadzone) {
options.scrollPopup((primaryScroll * config.scrollPixelsPerSecond * elapsedMs) / 1000);
if (elapsedMs > 0 && Math.abs(primaryScroll) >= activeConfig.stickDeadzone) {
options.scrollPopup(
(primaryScroll * activeConfig.scrollPixelsPerSecond * elapsedMs) / 1000,
);
}
handleJumpAxis(
resolveAxisBindingValue(
activeGamepad,
config.bindings.rightStickVertical,
config.triggerDeadzone,
activeConfig.bindings.rightStickVertical,
activeConfig.triggerDeadzone,
activationThreshold,
),
now,
config,
activeConfig,
);
} else {
resetHeldAction(jumpHold);
@@ -498,21 +513,21 @@ export function createGamepadController(options: GamepadControllerOptions) {
handleActionEdge(
'toggleMpvPause',
config.bindings.toggleMpvPause,
activeConfig.bindings.toggleMpvPause,
activeGamepad,
config,
activeConfig,
options.toggleMpvPause,
);
handleSelectionAxis(
resolveAxisBindingValue(
activeGamepad,
config.bindings.leftStickHorizontal,
config.triggerDeadzone,
activeConfig.bindings.leftStickHorizontal,
activeConfig.triggerDeadzone,
activationThreshold,
),
now,
config,
activeConfig,
);
}
+3 -3
View File
@@ -987,7 +987,7 @@ test('keyboard mode: configured controller select binding opens locally without
assert.equal(openControllerSelectCount(), 1);
assert.deepEqual(testGlobals.sessionActions, []);
assert.deepEqual(testGlobals.openedModalNotifications, ['controller-select']);
assert.deepEqual(testGlobals.openedModalNotifications, []);
} finally {
testGlobals.restore();
}
@@ -1017,7 +1017,7 @@ test('keyboard mode: configured controller debug binding opens locally without d
assert.equal(openControllerDebugCount(), 1);
assert.deepEqual(testGlobals.sessionActions, []);
assert.deepEqual(testGlobals.openedModalNotifications, ['controller-debug']);
assert.deepEqual(testGlobals.openedModalNotifications, []);
} finally {
testGlobals.restore();
}
@@ -1049,7 +1049,7 @@ test('keyboard mode: configured controller debug binding is not swallowed while
assert.equal(openControllerDebugCount(), 1);
assert.deepEqual(testGlobals.sessionActions, []);
assert.deepEqual(testGlobals.openedModalNotifications, ['controller-debug']);
assert.deepEqual(testGlobals.openedModalNotifications, []);
} finally {
testGlobals.restore();
}
-2
View File
@@ -203,13 +203,11 @@ export function createKeyboardHandlers(
}
if (binding.actionType === 'session-action' && binding.actionId === 'openControllerSelect') {
window.electronAPI.notifyOverlayModalOpened('controller-select');
options.openControllerSelectModal?.();
return;
}
if (binding.actionType === 'session-action' && binding.actionId === 'openControllerDebug') {
window.electronAPI.notifyOverlayModalOpened('controller-debug');
options.openControllerDebugModal?.();
return;
}