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

@@ -8,6 +8,7 @@ export const CORE_DEFAULT_CONFIG: Pick<
| 'annotationWebsocket'
| 'logging'
| 'texthooker'
| 'controller'
| 'shortcuts'
| 'secondarySub'
| 'subsync'
@@ -31,6 +32,47 @@ export const CORE_DEFAULT_CONFIG: Pick<
launchAtStartup: true,
openBrowser: true,
},
controller: {
enabled: true,
preferredGamepadId: '',
preferredGamepadLabel: '',
smoothScroll: true,
scrollPixelsPerSecond: 900,
horizontalJumpPixels: 160,
stickDeadzone: 0.2,
triggerInputMode: 'auto',
triggerDeadzone: 0.5,
repeatDelayMs: 320,
repeatIntervalMs: 120,
buttonIndices: {
select: 6,
buttonSouth: 0,
buttonEast: 1,
buttonWest: 2,
buttonNorth: 3,
leftShoulder: 4,
rightShoulder: 5,
leftStickPress: 9,
rightStickPress: 10,
leftTrigger: 6,
rightTrigger: 7,
},
bindings: {
toggleLookup: 'buttonSouth',
closeLookup: 'buttonEast',
toggleKeyboardOnlyMode: 'buttonNorth',
mineCard: 'buttonWest',
quitMpv: 'select',
previousAudio: 'none',
nextAudio: 'rightShoulder',
playCurrentAudio: 'leftShoulder',
toggleMpvPause: 'leftStickPress',
leftStickHorizontal: 'leftStickX',
leftStickVertical: 'leftStickY',
rightStickHorizontal: 'rightStickX',
rightStickVertical: 'rightStickY',
},
},
shortcuts: {
toggleVisibleOverlayGlobal: 'Alt+Shift+O',
copySubtitle: 'CommandOrControl+C',

View File

@@ -19,6 +19,8 @@ test('config option registry includes critical paths and has unique entries', ()
for (const requiredPath of [
'logging.level',
'annotationWebsocket.enabled',
'controller.enabled',
'controller.scrollPixelsPerSecond',
'startupWarmups.lowPowerMode',
'subtitleStyle.enableJlpt',
'subtitleStyle.autoPauseVideoOnYomitanPopup',
@@ -38,6 +40,7 @@ test('config template sections include expected domains and unique keys', () =>
const requiredKeys: (typeof keys)[number][] = [
'websocket',
'annotationWebsocket',
'controller',
'startupWarmups',
'subtitleStyle',
'ankiConnect',

View File

@@ -4,6 +4,21 @@ import { ConfigOptionRegistryEntry } from './shared';
export function buildCoreConfigOptionRegistry(
defaultConfig: ResolvedConfig,
): ConfigOptionRegistryEntry[] {
const controllerButtonEnumValues = [
'none',
'select',
'buttonSouth',
'buttonEast',
'buttonNorth',
'buttonWest',
'leftShoulder',
'rightShoulder',
'leftStickPress',
'rightStickPress',
'leftTrigger',
'rightTrigger',
];
return [
{
path: 'logging.level',
@@ -12,6 +27,230 @@ export function buildCoreConfigOptionRegistry(
defaultValue: defaultConfig.logging.level,
description: 'Minimum log level for runtime logging.',
},
{
path: 'controller.enabled',
kind: 'boolean',
defaultValue: defaultConfig.controller.enabled,
description: 'Enable overlay controller support through the Chrome Gamepad API.',
},
{
path: 'controller.preferredGamepadId',
kind: 'string',
defaultValue: defaultConfig.controller.preferredGamepadId,
description: 'Preferred controller id saved from the controller selection modal.',
},
{
path: 'controller.preferredGamepadLabel',
kind: 'string',
defaultValue: defaultConfig.controller.preferredGamepadLabel,
description: 'Preferred controller display label saved for diagnostics.',
},
{
path: 'controller.smoothScroll',
kind: 'boolean',
defaultValue: defaultConfig.controller.smoothScroll,
description: 'Use smooth scrolling for controller-driven popup scroll input.',
},
{
path: 'controller.scrollPixelsPerSecond',
kind: 'number',
defaultValue: defaultConfig.controller.scrollPixelsPerSecond,
description: 'Base popup scroll speed for controller stick input.',
},
{
path: 'controller.horizontalJumpPixels',
kind: 'number',
defaultValue: defaultConfig.controller.horizontalJumpPixels,
description: 'Popup page-jump distance for controller jump input.',
},
{
path: 'controller.stickDeadzone',
kind: 'number',
defaultValue: defaultConfig.controller.stickDeadzone,
description: 'Deadzone applied to controller stick axes.',
},
{
path: 'controller.triggerInputMode',
kind: 'enum',
enumValues: ['auto', 'digital', 'analog'],
defaultValue: defaultConfig.controller.triggerInputMode,
description: 'How controller triggers are interpreted: auto, pressed-only, or thresholded analog.',
},
{
path: 'controller.triggerDeadzone',
kind: 'number',
defaultValue: defaultConfig.controller.triggerDeadzone,
description: 'Minimum analog trigger value required when trigger input uses auto or analog mode.',
},
{
path: 'controller.repeatDelayMs',
kind: 'number',
defaultValue: defaultConfig.controller.repeatDelayMs,
description: 'Delay before repeating held controller actions.',
},
{
path: 'controller.repeatIntervalMs',
kind: 'number',
defaultValue: defaultConfig.controller.repeatIntervalMs,
description: 'Repeat interval for held controller actions.',
},
{
path: 'controller.buttonIndices.select',
kind: 'number',
defaultValue: defaultConfig.controller.buttonIndices.select,
description: 'Raw button index used for the controller select/minus/back button.',
},
{
path: 'controller.buttonIndices.buttonSouth',
kind: 'number',
defaultValue: defaultConfig.controller.buttonIndices.buttonSouth,
description: 'Raw button index used for controller south/A button input.',
},
{
path: 'controller.buttonIndices.buttonEast',
kind: 'number',
defaultValue: defaultConfig.controller.buttonIndices.buttonEast,
description: 'Raw button index used for controller east/B button input.',
},
{
path: 'controller.buttonIndices.buttonNorth',
kind: 'number',
defaultValue: defaultConfig.controller.buttonIndices.buttonNorth,
description: 'Raw button index used for controller north/Y button input.',
},
{
path: 'controller.buttonIndices.buttonWest',
kind: 'number',
defaultValue: defaultConfig.controller.buttonIndices.buttonWest,
description: 'Raw button index used for controller west/X button input.',
},
{
path: 'controller.buttonIndices.leftShoulder',
kind: 'number',
defaultValue: defaultConfig.controller.buttonIndices.leftShoulder,
description: 'Raw button index used for controller left shoulder input.',
},
{
path: 'controller.buttonIndices.rightShoulder',
kind: 'number',
defaultValue: defaultConfig.controller.buttonIndices.rightShoulder,
description: 'Raw button index used for controller right shoulder input.',
},
{
path: 'controller.buttonIndices.leftStickPress',
kind: 'number',
defaultValue: defaultConfig.controller.buttonIndices.leftStickPress,
description: 'Raw button index used for controller L3 input.',
},
{
path: 'controller.buttonIndices.rightStickPress',
kind: 'number',
defaultValue: defaultConfig.controller.buttonIndices.rightStickPress,
description: 'Raw button index used for controller R3 input.',
},
{
path: 'controller.buttonIndices.leftTrigger',
kind: 'number',
defaultValue: defaultConfig.controller.buttonIndices.leftTrigger,
description: 'Raw button index used for controller L2 input.',
},
{
path: 'controller.buttonIndices.rightTrigger',
kind: 'number',
defaultValue: defaultConfig.controller.buttonIndices.rightTrigger,
description: 'Raw button index used for controller R2 input.',
},
{
path: 'controller.bindings.toggleLookup',
kind: 'enum',
enumValues: controllerButtonEnumValues,
defaultValue: defaultConfig.controller.bindings.toggleLookup,
description: 'Controller binding for toggling lookup.',
},
{
path: 'controller.bindings.closeLookup',
kind: 'enum',
enumValues: controllerButtonEnumValues,
defaultValue: defaultConfig.controller.bindings.closeLookup,
description: 'Controller binding for closing lookup.',
},
{
path: 'controller.bindings.toggleKeyboardOnlyMode',
kind: 'enum',
enumValues: controllerButtonEnumValues,
defaultValue: defaultConfig.controller.bindings.toggleKeyboardOnlyMode,
description: 'Controller binding for toggling keyboard-only mode.',
},
{
path: 'controller.bindings.mineCard',
kind: 'enum',
enumValues: controllerButtonEnumValues,
defaultValue: defaultConfig.controller.bindings.mineCard,
description: 'Controller binding for mining the active card.',
},
{
path: 'controller.bindings.quitMpv',
kind: 'enum',
enumValues: controllerButtonEnumValues,
defaultValue: defaultConfig.controller.bindings.quitMpv,
description: 'Controller binding for quitting mpv.',
},
{
path: 'controller.bindings.previousAudio',
kind: 'enum',
enumValues: controllerButtonEnumValues,
defaultValue: defaultConfig.controller.bindings.previousAudio,
description: 'Controller binding for previous Yomitan audio.',
},
{
path: 'controller.bindings.nextAudio',
kind: 'enum',
enumValues: controllerButtonEnumValues,
defaultValue: defaultConfig.controller.bindings.nextAudio,
description: 'Controller binding for next Yomitan audio.',
},
{
path: 'controller.bindings.playCurrentAudio',
kind: 'enum',
enumValues: controllerButtonEnumValues,
defaultValue: defaultConfig.controller.bindings.playCurrentAudio,
description: 'Controller binding for playing the current Yomitan audio.',
},
{
path: 'controller.bindings.toggleMpvPause',
kind: 'enum',
enumValues: controllerButtonEnumValues,
defaultValue: defaultConfig.controller.bindings.toggleMpvPause,
description: 'Controller binding for toggling mpv play/pause.',
},
{
path: 'controller.bindings.leftStickHorizontal',
kind: 'enum',
enumValues: ['leftStickX', 'leftStickY', 'rightStickX', 'rightStickY'],
defaultValue: defaultConfig.controller.bindings.leftStickHorizontal,
description: 'Axis binding used for left/right token selection.',
},
{
path: 'controller.bindings.leftStickVertical',
kind: 'enum',
enumValues: ['leftStickX', 'leftStickY', 'rightStickX', 'rightStickY'],
defaultValue: defaultConfig.controller.bindings.leftStickVertical,
description: 'Axis binding used for primary popup scrolling.',
},
{
path: 'controller.bindings.rightStickHorizontal',
kind: 'enum',
enumValues: ['leftStickX', 'leftStickY', 'rightStickX', 'rightStickY'],
defaultValue: defaultConfig.controller.bindings.rightStickHorizontal,
description: 'Axis binding reserved for alternate right-stick mappings.',
},
{
path: 'controller.bindings.rightStickVertical',
kind: 'enum',
enumValues: ['leftStickX', 'leftStickY', 'rightStickX', 'rightStickY'],
defaultValue: defaultConfig.controller.bindings.rightStickVertical,
description: 'Axis binding used for popup page jumps.',
},
{
path: 'texthooker.launchAtStartup',
kind: 'boolean',

View File

@@ -34,6 +34,16 @@ const CORE_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
description: ['Controls logging verbosity.', 'Set to debug for full runtime diagnostics.'],
key: 'logging',
},
{
title: 'Controller Support',
description: [
'Gamepad support for the visible overlay while keyboard-only mode is active.',
'Use the selection modal to save a preferred controller by id for future launches.',
'Trigger input mode can be auto, digital-only, or analog-thresholded depending on the controller.',
'Override controller.buttonIndices when your pad reports non-standard raw button numbers.',
],
key: 'controller',
},
{
title: 'Startup Warmups',
description: [