Add overlay gamepad support for keyboard-only mode (#17)

This commit is contained in:
2026-03-11 20:34:46 -07:00
committed by GitHub
parent 2f17859b7b
commit 4d7c80f2e4
49 changed files with 5677 additions and 42 deletions

View File

@@ -1106,6 +1106,135 @@ test('parses global shortcuts and startup settings', () => {
assert.equal(config.youtubeSubgen.fixWithAi, true);
});
test('parses controller settings with logical bindings and tuning knobs', () => {
const dir = makeTempDir();
fs.writeFileSync(
path.join(dir, 'config.jsonc'),
`{
"controller": {
"enabled": true,
"preferredGamepadId": "Xbox Wireless Controller (STANDARD GAMEPAD Vendor: 045e Product: 0b13)",
"preferredGamepadLabel": "Xbox Wireless Controller",
"smoothScroll": false,
"scrollPixelsPerSecond": 1440,
"horizontalJumpPixels": 180,
"stickDeadzone": 0.3,
"triggerInputMode": "analog",
"triggerDeadzone": 0.4,
"repeatDelayMs": 220,
"repeatIntervalMs": 70,
"buttonIndices": {
"select": 6,
"leftStickPress": 9,
"rightStickPress": 10
},
"bindings": {
"toggleLookup": "buttonWest",
"closeLookup": "buttonEast",
"toggleKeyboardOnlyMode": "buttonNorth",
"mineCard": "buttonSouth",
"quitMpv": "select",
"previousAudio": "leftShoulder",
"nextAudio": "rightShoulder",
"playCurrentAudio": "none",
"toggleMpvPause": "leftStickPress",
"leftStickHorizontal": "rightStickX",
"leftStickVertical": "rightStickY",
"rightStickHorizontal": "leftStickX",
"rightStickVertical": "leftStickY"
}
}
}`,
'utf-8',
);
const service = new ConfigService(dir);
const config = service.getConfig();
assert.equal(config.controller.enabled, true);
assert.equal(
config.controller.preferredGamepadId,
'Xbox Wireless Controller (STANDARD GAMEPAD Vendor: 045e Product: 0b13)',
);
assert.equal(config.controller.preferredGamepadLabel, 'Xbox Wireless Controller');
assert.equal(config.controller.smoothScroll, false);
assert.equal(config.controller.scrollPixelsPerSecond, 1440);
assert.equal(config.controller.horizontalJumpPixels, 180);
assert.equal(config.controller.stickDeadzone, 0.3);
assert.equal(config.controller.triggerInputMode, 'analog');
assert.equal(config.controller.triggerDeadzone, 0.4);
assert.equal(config.controller.repeatDelayMs, 220);
assert.equal(config.controller.repeatIntervalMs, 70);
assert.equal(config.controller.buttonIndices.select, 6);
assert.equal(config.controller.buttonIndices.leftStickPress, 9);
assert.equal(config.controller.bindings.toggleLookup, 'buttonWest');
assert.equal(config.controller.bindings.quitMpv, 'select');
assert.equal(config.controller.bindings.playCurrentAudio, 'none');
assert.equal(config.controller.bindings.toggleMpvPause, 'leftStickPress');
assert.equal(config.controller.bindings.leftStickHorizontal, 'rightStickX');
assert.equal(config.controller.bindings.rightStickVertical, 'leftStickY');
});
test('controller positive-number tuning rejects sub-unit values that floor to zero', () => {
const dir = makeTempDir();
fs.writeFileSync(
path.join(dir, 'config.jsonc'),
`{
"controller": {
"scrollPixelsPerSecond": 0.5,
"horizontalJumpPixels": 0.2,
"repeatDelayMs": 0.9,
"repeatIntervalMs": 0.1
}
}`,
'utf-8',
);
const service = new ConfigService(dir);
const config = service.getConfig();
const warnings = service.getWarnings();
assert.equal(config.controller.scrollPixelsPerSecond, DEFAULT_CONFIG.controller.scrollPixelsPerSecond);
assert.equal(config.controller.horizontalJumpPixels, DEFAULT_CONFIG.controller.horizontalJumpPixels);
assert.equal(config.controller.repeatDelayMs, DEFAULT_CONFIG.controller.repeatDelayMs);
assert.equal(config.controller.repeatIntervalMs, DEFAULT_CONFIG.controller.repeatIntervalMs);
assert.equal(warnings.some((warning) => warning.path === 'controller.scrollPixelsPerSecond'), true);
assert.equal(warnings.some((warning) => warning.path === 'controller.horizontalJumpPixels'), true);
assert.equal(warnings.some((warning) => warning.path === 'controller.repeatDelayMs'), true);
assert.equal(warnings.some((warning) => warning.path === 'controller.repeatIntervalMs'), true);
});
test('controller button index config rejects fractional values', () => {
const dir = makeTempDir();
fs.writeFileSync(
path.join(dir, 'config.jsonc'),
`{
"controller": {
"buttonIndices": {
"select": 6.5,
"leftStickPress": 9.1
}
}
}`,
'utf-8',
);
const service = new ConfigService(dir);
const config = service.getConfig();
const warnings = service.getWarnings();
assert.equal(config.controller.buttonIndices.select, DEFAULT_CONFIG.controller.buttonIndices.select);
assert.equal(
config.controller.buttonIndices.leftStickPress,
DEFAULT_CONFIG.controller.buttonIndices.leftStickPress,
);
assert.equal(warnings.some((warning) => warning.path === 'controller.buttonIndices.select'), true);
assert.equal(
warnings.some((warning) => warning.path === 'controller.buttonIndices.leftStickPress'),
true,
);
});
test('runtime options registry is centralized', () => {
const ids = RUNTIME_OPTION_REGISTRY.map((entry) => entry.id);
assert.deepEqual(ids, [
@@ -1638,6 +1767,7 @@ test('template generator includes known keys', () => {
const output = generateConfigTemplate(DEFAULT_CONFIG);
assert.match(output, /"ai":/);
assert.match(output, /"ankiConnect":/);
assert.match(output, /"controller":/);
assert.match(output, /"logging":/);
assert.match(output, /"websocket":/);
assert.match(output, /"discordPresence":/);
@@ -1662,6 +1792,14 @@ test('template generator includes known keys', () => {
output,
/"enabled": true,? \/\/ Annotated subtitle websocket server enabled state\. Values: true \| false/,
);
assert.match(
output,
/"scrollPixelsPerSecond": 900,? \/\/ Base popup scroll speed for controller stick input\./,
);
assert.match(
output,
/"triggerInputMode": "auto",? \/\/ How controller triggers are interpreted: auto, pressed-only, or thresholded analog\. Values: auto \| digital \| analog/,
);
assert.match(output, /"port": 6678,? \/\/ Annotated subtitle websocket server port\./);
assert.match(
output,