Source mirror · commit 369512f · 2026-10-26 · MIT · canonical repo: gitlab.com/jiapunk/upgrade-airlock
git clone https://upgrade-airlock.pages.dev/source/upgrade-airlock.git
upgrade-airlock / web / src / controls.ts
web/src/controls.ts80 lines · 3035 bytes
1// Presenter controls: keyboard shortcuts (and a clicker's Page Up / Page Down) -> page actions. Pure, so the self-test
2// checks the whole map. The on-screen help (?) and web/README.md list the same keys.
3//
4// ← / PageUp previous (older) proposal
5// → / PageDown next (newer) proposal
6// N / Home / End newest proposal
7// Space / Enter replay the selected proposal's verdict moment
8// F auto-follow on / off (offline: the replay loop)
9// P play / pause the replay loop
10// D details drawer
11// L language
12// ? / H help
13// 1 - 7 replay attack card 1-7 of the picker (lab.ts), also with ?clean=1 where the picker is hidden
14// O the operator's launch panel (only where operator mode is on; operator-client.ts)
15// Esc close the drawer, the help and the operator panel
16
17export type Action = 'prev' | 'next' | 'newest' | 'replay' | 'follow' | 'loop' | 'details' | 'lang' | 'help' | 'lab' | 'operator' | 'close'
18
19const KEYS: Record<string, Action> = {
20 arrowleft: 'prev',
21 pageup: 'prev',
22 arrowright: 'next',
23 pagedown: 'next',
24 n: 'newest',
25 home: 'newest',
26 end: 'newest',
27 ' ': 'replay',
28 spacebar: 'replay',
29 enter: 'replay',
30 f: 'follow',
31 p: 'loop',
32 d: 'details',
33 l: 'lang',
34 '?': 'help',
35 h: 'help',
36 '1': 'lab',
37 '2': 'lab',
38 '3': 'lab',
39 '4': 'lab',
40 '5': 'lab',
41 '6': 'lab',
42 '7': 'lab',
43 o: 'operator',
44 escape: 'close',
45}
46
47/** Keys 1-7: the picker card's index (0-6), else null */
48export function labIndexOfKey(key: string): number | null {
49 return /^[1-7]$/.test(key) ? Number(key) - 1 : null
50}
51
52/** KeyboardEvent.key -> action; null for keys the page leaves alone (and for any key with Ctrl, Alt or Meta held) */
53export function keyAction(key: string, mods: { ctrl?: boolean; alt?: boolean; meta?: boolean } = {}): Action | null {
54 if (mods.ctrl || mods.alt || mods.meta) return null
55 return KEYS[key.toLowerCase()] ?? null
56}
57
58/** The help overlay's rows: the keys shown for each action, in this order */
59export const HELP_ROWS: { keys: string[]; action: Action }[] = [
60 { keys: ['←', '→'], action: 'prev' },
61 { keys: ['N'], action: 'newest' },
62 { keys: ['Space'], action: 'replay' },
63 { keys: ['F'], action: 'follow' },
64 { keys: ['P'], action: 'loop' },
65 { keys: ['D'], action: 'details' },
66 { keys: ['L'], action: 'lang' },
67 { keys: ['?'], action: 'help' },
68 { keys: ['1–7'], action: 'lab' },
69 { keys: ['O'], action: 'operator' },
70 { keys: ['Esc'], action: 'close' },
71]
72
73/** Step through ids (sorted oldest to newest) from the current one; stays at either end */
74export function stepId(ids: string[], cur: string | null, dir: -1 | 1): string | null {
75 if (!ids.length) return null
76 const sorted = [...ids].sort((a, b) => Number(a) - Number(b))
77 const i = cur === null ? -1 : sorted.indexOf(cur)
78 if (i < 0) return sorted[sorted.length - 1]
79 return sorted[Math.max(0, Math.min(sorted.length - 1, i + dir))]
80}