import { useMemo, useState, useEffect, useCallback } from 'react'; import { useControlSystem } from '../controls/index.js'; import { DEFAULT_KEYMAP } from '../controls/constants.js'; import { canonicalizeKeyInput, formatKeyLabel } from '../controls/keymapUtils.js'; import { setKeyboardCaptureLocked } from '../controls/inputs/keyboardCaptureLock.js'; import { useSettingsNamespace } from '../settings/index.js'; import { INPUT_SETTINGS_DEFAULTS } from '../settings/namespaces.js'; const KEY_ACTIONS = [ { id: 'driveForward', label: 'Drive Forward', group: 'Driving' }, { id: 'driveBackward', label: 'Drive Backward', group: 'Driving' }, { id: 'driveLeft', label: 'Turn Left', group: 'Driving' }, { id: 'driveRight', label: 'Turn Right', group: 'Driving' }, { id: 'boostModifier', label: 'Boost Modifier', group: 'Driving' }, { id: 'slowModifier', label: 'Slow Modifier', group: 'Driving' }, { id: 'auxMainForward', label: 'Main Brush Forward', group: 'Aux Motors' }, { id: 'auxMainReverse', label: 'Main Brush Reverse', group: 'Aux Motors' }, { id: 'auxSideForward', label: 'Side Brush Forward', group: 'Aux Motors' }, { id: 'auxSideReverse', label: 'Side Brush Reverse', group: 'Aux Motors' }, { id: 'auxVacuumFast', label: 'Vacuum Max', group: 'Aux Motors' }, { id: 'auxVacuumSlow', label: 'Vacuum Low', group: 'Aux Motors' }, { id: 'auxAllForward', label: 'All Aux Forward', group: 'Aux Motors' }, { id: 'cameraUp', label: 'Camera Up', group: 'Camera' }, { id: 'cameraDown', label: 'Camera Down', group: 'Camera' }, { id: 'nightVisionToggle', label: 'Toggle Night Vision', group: 'Camera' }, { id: 'hornHonk', label: 'Horn (Hold)', group: 'Audio' }, { id: 'driveMacro', label: 'Drive Macro', group: 'Macros' }, { id: 'dockMacro', label: 'Dock Macro', group: 'Macros' }, { id: 'chatFocus', label: 'Toggle Chat', group: 'Chat' }, { id: 'songNoteUp', label: 'Song Note Up', group: 'Audio' }, { id: 'songNoteDown', label: 'Song Note Down', group: 'Audio' }, { id: 'homeAssistantOn', label: 'Room Controls On (Cycle)', group: 'Room Controls' }, { id: 'homeAssistantOff', label: 'Room Controls Off (Cycle)', group: 'Room Controls' }, ]; function groupActions(actions) { return actions.reduce((acc, action) => { const list = acc[action.group] || (acc[action.group] = []); list.push(action); return acc; }, {}); } function clampSpeed(value, fallback) { const num = Number(value); if (!Number.isFinite(num)) return fallback; return Math.max(0, Math.min(500, num)); } const TILT_INTERVAL_MIN = 5; const TILT_INTERVAL_MAX = 500; const TILT_SPEED_MIN = 1; const TILT_SPEED_MAX = 100; function clampTiltInterval(value, fallback) { const num = Number(value); if (!Number.isFinite(num)) return fallback; return Math.max(TILT_INTERVAL_MIN, Math.min(TILT_INTERVAL_MAX, num)); } function clampTiltSpeed(value, fallback) { const num = Number(value); if (!Number.isFinite(num)) return fallback; return Math.max(TILT_SPEED_MIN, Math.min(TILT_SPEED_MAX, num)); } function mapTiltSpeedToInterval(speed) { const clampedSpeed = clampTiltSpeed(speed, speed); const ratio = (clampedSpeed - TILT_SPEED_MIN) / (TILT_SPEED_MAX - TILT_SPEED_MIN); const interval = TILT_INTERVAL_MAX - ratio * (TILT_INTERVAL_MAX - TILT_INTERVAL_MIN); return Math.round(interval); } function mapTiltIntervalToSpeed(interval) { const clampedInterval = clampTiltInterval(interval, interval); const ratio = (TILT_INTERVAL_MAX - clampedInterval) / (TILT_INTERVAL_MAX - TILT_INTERVAL_MIN); const speed = TILT_SPEED_MIN + ratio * (TILT_SPEED_MAX - TILT_SPEED_MIN); return Math.round(speed); } function useKeyCapture(onCapture) { const [active, setActive] = useState(null); const startCapture = useCallback((actionId) => { setKeyboardCaptureLocked(true); setActive(actionId); }, []); const cancel = useCallback(() => { setKeyboardCaptureLocked(false); setActive(null); }, []); const captureFromEvent = useCallback( (actionId, event) => { event.preventDefault(); event.stopPropagation(); if (event.key === 'Escape') { cancel(); return; } const canonical = canonicalizeKeyInput(event.key ?? ''); if (!canonical) return; onCapture(actionId, canonical, event); cancel(); }, [cancel, onCapture], ); useEffect(() => () => setKeyboardCaptureLocked(false), []); return { active, startCapture, cancel, captureFromEvent }; } function SpeedField({ label, description, value, onChange, min = 0, max = 500, step = 5 }) { return ( ); } export default function KeymapSettings() { const { state: { keymap }, actions: { updateKeyBinding, resetKeyBindings }, } = useControlSystem(); const { value: inputSettings, save: saveInputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS); const keyboardSettings = { ...INPUT_SETTINGS_DEFAULTS.keyboard, ...(inputSettings.keyboard ?? {}), }; const tiltSpeed = useMemo(() => { if (typeof keyboardSettings.tiltSpeed === 'number') { return clampTiltSpeed(keyboardSettings.tiltSpeed, INPUT_SETTINGS_DEFAULTS.keyboard.tiltSpeed); } if (typeof keyboardSettings.tiltIntervalMs === 'number') { return clampTiltSpeed( mapTiltIntervalToSpeed(keyboardSettings.tiltIntervalMs), INPUT_SETTINGS_DEFAULTS.keyboard.tiltSpeed, ); } return INPUT_SETTINGS_DEFAULTS.keyboard.tiltSpeed; }, [keyboardSettings.tiltIntervalMs, keyboardSettings.tiltSpeed]); const grouped = useMemo(() => groupActions(KEY_ACTIONS), []); const { active, startCapture, cancel, captureFromEvent } = useKeyCapture((actionId, value) => { updateKeyBinding(actionId, value); }); const currentKey = useCallback( (id) => keymap?.[id]?.[0] || DEFAULT_KEYMAP[id]?.[0] || '', [keymap], ); const updateKeyboardSpeed = useCallback( (key, nextValue) => { const defaults = INPUT_SETTINGS_DEFAULTS.keyboard; const clamped = clampSpeed(nextValue, defaults[key]); saveInputSettings((prev) => ({ ...(prev ?? {}), keyboard: { ...(prev?.keyboard ?? defaults), [key]: clamped, }, })); }, [saveInputSettings], ); const updateTiltInterval = useCallback( (nextValue) => { const defaults = INPUT_SETTINGS_DEFAULTS.keyboard; const clamped = clampTiltSpeed(nextValue, defaults.tiltSpeed); const mappedInterval = mapTiltSpeedToInterval(clamped); saveInputSettings((prev) => ({ ...(prev ?? {}), keyboard: { ...(prev?.keyboard ?? defaults), tiltSpeed: clamped, tiltIntervalMs: mappedInterval, }, })); }, [saveInputSettings], ); return (

Keyboard layout

Per-browser · click to change a binding

Keyboard speeds

updateKeyboardSpeed('baseSpeed', value)} /> updateKeyboardSpeed('turboSpeed', value)} /> updateKeyboardSpeed('precisionSpeed', value)} />

Camera tilt

{Object.entries(grouped).map(([group, actions]) => (

{group}

{actions.map((action) => { const value = currentKey(action.id); const isActive = active === action.id; return (

{action.label}

{formatKeyLabel(value)}

{isActive && ( )}
); })}
))}
); }