Skip to main content

godspeed_sdk/
hid.rs

1// SPDX-License-Identifier: Apache-2.0
2//! USB HID boot-protocol decoding, shared by the USB host drivers (`xhci`, `ehci`, `dwc2`).
3//!
4//! Pure logic - no syscalls, no I/O. Each driver reads the fixed 8-byte boot
5//! report from its controller's DMA and hands it here; the side effects (pushing
6//! a character to the console, logging a mouse event) stay in the driver, passed
7//! in as closures. This is the controller-agnostic reuse §26.2 anticipated once
8//! both drivers existed: the report format is identical whether the bytes arrived
9//! over xHCI transfer-event rings or EHCI split qTDs.
10
11/// Decode a HID boot-keyboard usage code to ASCII (US layout, common keys). `caps` is the host's
12/// Caps Lock toggle state (the HID modifier byte does NOT carry it - Caps Lock is a host-tracked
13/// latch, see `decode_keyboard`). Caps Lock XORs Shift, but **only for letters** - it never affects
14/// digits or symbols (that's the difference from Shift).
15pub fn hid_to_ascii(key: u8, mods: u8, caps: bool) -> Option<u8> {
16    let shift = mods & 0x22 != 0; // left or right Shift
17    let ctrl  = mods & 0x11 != 0; // left or right Ctrl
18    match key {
19        0x04..=0x1D => {
20            // Ctrl+letter → the C0 control code (Ctrl+A=0x01 … Ctrl+Z=0x1A), exactly what a
21            // serial terminal sends. Without this a USB keyboard can't produce ^S/^Q/^C, so
22            // app shortcuts (the editor's save/quit, the shell's Ctrl-C) are unreachable on
23            // hardware - they only worked over the serial console, which synthesises these
24            // bytes itself. Ctrl takes precedence over Shift/Caps. (key 0x04='a' → 0x01.)
25            if ctrl { return Some(key - 0x03); }
26            let base = b'a' + (key - 0x04);
27            // Uppercase iff exactly one of Shift / Caps Lock is active (Caps Lock toggles letters,
28            // and Shift inverts Caps Lock - so SHIFT+letter is lowercase while Caps is on).
29            Some(if shift ^ caps { base - 32 } else { base })
30        }
31        0x1E..=0x26 => {
32            if shift {
33                Some([b'!', b'@', b'#', b'$', b'%', b'^', b'&', b'*', b'('][(key - 0x1E) as usize])
34            } else {
35                Some(b'1' + (key - 0x1E))
36            }
37        }
38        0x27 => Some(if shift { b')' } else { b'0' }),
39        0x28 => Some(b'\n'), // Enter
40        0x29 => Some(0x1B),  // Escape - bare ESC (the shell disambiguates it from a sequence)
41        0x2A => Some(0x08),  // Backspace
42        0x2B => Some(b'\t'), // Tab
43        0x2C => Some(b' '),  // Space
44        0x2D => Some(if shift { b'_' } else { b'-' }),
45        0x2E => Some(if shift { b'+' } else { b'=' }),
46        0x2F => Some(if shift { b'{' } else { b'[' }),
47        0x30 => Some(if shift { b'}' } else { b']' }),
48        0x31 => Some(if shift { b'|' } else { b'\\' }),
49        0x32 => Some(if shift { b'~' } else { b'#' }), // Non-US # and ~ (ISO-layout extra key)
50        0x33 => Some(if shift { b':' } else { b';' }),
51        0x34 => Some(if shift { b'"' } else { b'\'' }), // apostrophe / quote
52        0x35 => Some(if shift { b'~' } else { b'`' }),  // grave / tilde
53        0x36 => Some(if shift { b'<' } else { b',' }),
54        0x37 => Some(if shift { b'>' } else { b'.' }),
55        0x38 => Some(if shift { b'?' } else { b'/' }),
56        0x64 => Some(if shift { b'|' } else { b'\\' }), // Non-US \ and | (the 0x31 twin)
57        // Numeric keypad (a separate number pad sends these, 0x54-0x63). We don't track
58        // the NumLock LED state, so map them NumLock-ON unconditionally: digits + the
59        // arithmetic operators + keypad-Enter. This is what a shell wants from a numpad
60        // (typing numbers); the navigation interpretation (NumLock off → Home/arrows/etc.)
61        // is deliberately not modelled. Shift does not change keypad output here.
62        0x54 => Some(b'/'),  // Keypad /
63        0x55 => Some(b'*'),  // Keypad *
64        0x56 => Some(b'-'),  // Keypad -
65        0x57 => Some(b'+'),  // Keypad +
66        0x58 => Some(b'\n'), // Keypad Enter
67        0x59 => Some(b'1'),
68        0x5A => Some(b'2'),
69        0x5B => Some(b'3'),
70        0x5C => Some(b'4'),
71        0x5D => Some(b'5'),
72        0x5E => Some(b'6'),
73        0x5F => Some(b'7'),
74        0x60 => Some(b'8'),
75        0x61 => Some(b'9'),
76        0x62 => Some(b'0'),
77        0x63 => Some(b'.'),  // Keypad .
78        _ => None,
79    }
80}
81
82/// Codes in the printable-key ranges (letters, digits, punctuation, keypad) - but NOT the
83/// control keys (Enter/Esc/Backspace/Tab/Space at 0x28-0x2C) or modifiers/F-keys. Used to
84/// decide whether an unmapped key is worth reporting (a missing punctuation mapping) vs
85/// silent noise (a function/modifier key with no character). Keys in these ranges are all
86/// mapped by `hid_to_ascii`, so reaching `on_unmapped` here means a gap to fill.
87fn is_typable_code(k: u8) -> bool {
88    matches!(k, 0x04..=0x27 | 0x2D..=0x38 | 0x54..=0x63 | 0x64)
89}
90
91/// Emit the byte(s) a single keycode produces under `mods`, returning `true` if it
92/// produced any output (i.e. it is a printable / cursor key worth auto-repeating).
93/// Shared by the first-press edge path and the auto-repeat path so a repeated key is
94/// byte-for-byte identical to its first press. The cursor and navigation-cluster keys
95/// emit the same ANSI escape sequences a serial terminal sends, so the shell's one input
96/// parser (`handle_csi` / the pager) handles USB and serial alike - this is what makes a
97/// standard extended keyboard's Home/End/Delete/PageUp/PageDown work on real hardware
98/// (without it the physical keys produce nothing).
99fn emit_key(k: u8, mods: u8, caps: bool, emit: &mut impl FnMut(u8)) -> bool {
100    // ESC [ <body...> for a cursor/navigation key. Returns true (it produced output).
101    fn csi(body: &[u8], emit: &mut impl FnMut(u8)) -> bool {
102        emit(0x1B);
103        emit(b'[');
104        for &b in body { emit(b); }
105        true
106    }
107    // An audio shortcut is its signal, not the key under it: Ctrl+Alt+Up is not a cursor-up, and
108    // Ctrl+Alt+M is not the Ctrl+M carriage return it would otherwise be. First, so nothing below sees it.
109    if let Some(sig) = audio_chord(k, mods) {
110        emit(sig);
111        return true;
112    }
113    match k {
114        0x52 => csi(b"A", emit),  // Up
115        0x51 => csi(b"B", emit),  // Down
116        0x4F => csi(b"C", emit),  // Right
117        0x50 => csi(b"D", emit),  // Left
118        0x4A => csi(b"H", emit),  // Home
119        0x4D => csi(b"F", emit),  // End
120        0x49 => csi(b"2~", emit), // Insert
121        0x4C => csi(b"3~", emit), // Delete (forward delete)
122        0x4B => csi(b"5~", emit), // PageUp
123        0x4E => csi(b"6~", emit), // PageDown
124        // Function keys F1-F12: the standard xterm sequences (F1-F4 are SS3 `ESC O P/Q/R/S`,
125        // F5-F12 are `ESC[<n>~`). The shell acts on F1 (help); the rest are recognised and
126        // consumed by its escape parser, so the physical keys are no longer dead and never
127        // smear stray characters onto the line.
128        0x3A => { emit(0x1B); emit(b'O'); emit(b'P'); true } // F1
129        0x3B => { emit(0x1B); emit(b'O'); emit(b'Q'); true } // F2
130        0x3C => { emit(0x1B); emit(b'O'); emit(b'R'); true } // F3
131        0x3D => { emit(0x1B); emit(b'O'); emit(b'S'); true } // F4
132        0x3E => csi(b"15~", emit), // F5
133        0x3F => csi(b"17~", emit), // F6
134        0x40 => csi(b"18~", emit), // F7
135        0x41 => csi(b"19~", emit), // F8
136        0x42 => csi(b"20~", emit), // F9
137        0x43 => csi(b"21~", emit), // F10
138        0x44 => csi(b"23~", emit), // F11
139        0x45 => csi(b"24~", emit), // F12
140        _ => match hid_to_ascii(k, mods, caps) {
141            Some(ch) => { emit(ch); true }
142            None => false,
143        },
144    }
145}
146
147/// Tracks the currently-held key so a driver can synthesise typematic auto-repeat.
148/// USB HID boot keyboards report only on *change* - a held key sends one down report
149/// and then nothing until release - so the host must synthesise repeat itself.
150///
151/// `now`, `initial`, and `interval` are in **whatever monotonic unit the driver feeds
152/// in** - the drivers use `ServiceContext::read_tsc()` cycles (hardware-proven to
153/// advance on real machines, unlike the coarse kernel tick), so `initial`/`interval`
154/// are cycle counts (e.g. ~300 ms / ~50 ms worth at the CPU's frequency). The unit is
155/// the driver's choice; this struct only compares and adds. `decode_keyboard` arms it
156/// on a fresh printable press and disarms on release; the driver calls
157/// [`KeyRepeat::poll`] every loop iteration with the current `now`. One per keyboard.
158pub struct KeyRepeat {
159    key: u8,       // HID usage of the key being repeated (0 = none armed)
160    mods: u8,      // modifier byte captured at press (so Shift+key repeats the shifted form)
161    caps: bool,    // Caps Lock state captured at press (so a held letter repeats in the right case)
162    next_at: u64,  // `now` value at which the next repeat is due
163    initial: u64,  // delay (in the driver's `now` unit) before the first repeat
164    interval: u64, // delay between subsequent repeats
165}
166
167impl KeyRepeat {
168    /// `initial`/`interval` are in the same unit the driver passes as `now` (TSC cycles).
169    pub const fn new(initial: u64, interval: u64) -> Self {
170        KeyRepeat { key: 0, mods: 0, caps: false, next_at: 0, initial, interval }
171    }
172
173    /// Construct a repeat CALIBRATED to this machine's real TSC rate. `ticks_per_10ms` is
174    /// `ServiceContext::tsc_ticks_per_10ms()` (TSC cycles in 10 ms, PIT-calibrated by the kernel);
175    /// it is 0 only when the TSC was not calibrated (QEMU / the T630's periodic-timer path, which
176    /// has no calibrated quantum), where we fall back to ~2 GHz cycle counts (~300 ms / ~50 ms).
177    ///
178    /// The CALIBRATED path uses a deliberately CONSERVATIVE ~600 ms initial delay (not 300 ms). The
179    /// math for 300 ms is provably correct (30 * ticks_per_10ms), yet the Goldmont+ Wyse still
180    /// over-repeated at that threshold - its RDTSC appears to advance a little faster under load than
181    /// the boot-time PIT calibration captured, so a 300 ms cycle budget elapsed in less than 300 ms of
182    /// wall-clock and normal typing tripped the repeat. A 600 ms budget requires a clearly deliberate
183    /// hold, absorbing that drift with margin. The interval (~50 ms) is unchanged; only the trigger
184    /// threshold moved. The fallback (T630) is left at its known-good ~300 ms - the drift is specific
185    /// to the calibrated part.
186    pub fn new_calibrated(ticks_per_10ms: u64) -> Self {
187        if ticks_per_10ms == 0 {
188            // Uncalibrated (QEMU / T630 periodic): ~2 GHz. 600M cycles ~= 300 ms, 100M ~= 50 ms.
189            KeyRepeat::new(600_000_000, 100_000_000)
190        } else {
191            // ~600 ms initial = 60 * (cycles in 10 ms); ~50 ms interval = 5 * (cycles in 10 ms).
192            KeyRepeat::new(ticks_per_10ms.saturating_mul(60), ticks_per_10ms.saturating_mul(5))
193        }
194    }
195
196    fn arm(&mut self, key: u8, mods: u8, caps: bool, now: u64) {
197        self.key = key;
198        self.mods = mods;
199        self.caps = caps;
200        self.next_at = now.wrapping_add(self.initial);
201    }
202
203    fn disarm(&mut self) {
204        self.key = 0;
205    }
206
207    /// Is a key currently held (repeat armed)? A driver uses this to decide how long to
208    /// block: a short timeout while a key is down (to wake and emit repeats), a long one
209    /// otherwise (the keyboard is silent, so just idle until the next interrupt).
210    pub fn armed(&self) -> bool {
211        self.key != 0
212    }
213
214    /// Stop repeating, from OUTSIDE a decode.
215    ///
216    /// A driver needs this when it can no longer vouch for the hold. A boot keyboard reports only on
217    /// CHANGE, so if a release report is lost - the transaction translator discards it once its frame
218    /// is gone, and the device never resends - nothing will ever arrive to disarm the repeat, and the
219    /// key repeats forever. Losing contact is therefore a reason to STOP, not to keep going.
220    pub fn cancel(&mut self) {
221        self.disarm();
222    }
223
224    /// Emit a repeat of the held key if one is due at `now`. Call once per poll
225    /// iteration; a no-op until `initial` elapses, then fires at most once per `interval`.
226    pub fn poll(&mut self, now: u64, mut emit: impl FnMut(u8)) {
227        if self.key == 0 || now < self.next_at {
228            return;
229        }
230        emit_key(self.key, self.mods, self.caps, &mut emit);
231        self.next_at = now.wrapping_add(self.interval);
232    }
233}
234
235/// Is an 8-byte HID boot report real device data, or the all-`0xff` signature of a failed/stale
236/// DMA read (a device that vanished mid-transaction, or a buffer the controller never wrote)?
237/// Returns `false` only for an all-`0xff` report. This is the **universal** garbage check - safe
238/// for both keyboards and mice (a real mouse won't send all-`0xff`; a keyboard never does) -
239/// which a driver uses to count a wedged endpoint toward disconnect and re-enumerate. (The
240/// keyboard decoder additionally rejects any report whose reserved byte 1 ≠ 0, a stricter check
241/// that only makes sense for keyboards.)
242pub fn report_is_valid(report: &[u8; 8]) -> bool {
243    *report != [0xFF; 8]
244}
245
246/// HID usage of the Delete (forward-delete) key - the `Del` in Ctrl+Alt+Del.
247pub const KEY_DELETE: u8 = 0x4C;
248/// HID usage of the Caps Lock key. It is a host-tracked LATCH: the modifier byte never reports it,
249/// so the host toggles a `caps` flag on each fresh press (see `decode_keyboard`).
250pub const KEY_CAPS_LOCK: u8 = 0x39;
251
252/// Console-stream signal for the Ctrl+Alt+Del secure-attention chord (the SEC-2 follow-up).
253///
254/// SEC-2 removed `REBOOT` from the USB drivers, so a driver can no longer hard-reset the machine
255/// directly from any context. To keep the chord working, the driver does not reboot - it *signals*
256/// the chord on the console stream, and the **shell**, which legitimately holds `REBOOT`, decides.
257/// That preserves what SEC-2 actually won (no direct reboot syscall from a driver) while restoring
258/// the UX, and it grants the driver nothing new: a `CONSOLE_PUSH` holder can already type `reboot`,
259/// the residual §6.4 documents.
260///
261/// `0x80` is outside ASCII, so [`hid_to_ascii`] never emits it and it cannot collide with a typed
262/// key. Both the drivers (producer) and the shell (consumer) read it from here, so the two cannot
263/// drift apart.
264pub const CTRL_ALT_DEL_SIGNAL: u8 = 0x80;
265
266/// Console-stream signals for the audio shortcuts (`docs/audio.md`, "Keyboard shortcuts"): Ctrl+Alt+Up,
267/// Ctrl+Alt+Down and Ctrl+Alt+M. The Ctrl+Alt+Del pattern exactly: the keyboard driver only SAYS the chord
268/// happened, on the console stream, and the SHELL - which asks the audio driver - acts. The keyboard
269/// drivers gain no capability and never speak to the audio driver. Outside ASCII, so no typed key
270/// produces them, and read from here by both ends.
271pub const VOLUME_UP_SIGNAL: u8 = 0x81;
272pub const VOLUME_DOWN_SIGNAL: u8 = 0x82;
273pub const MUTE_TOGGLE_SIGNAL: u8 = 0x83;
274
275/// Console-stream signal from a USB host: it has just printed a device notice (`USB: WiFi dongle connected
276/// (xhci)`) over whatever the console showed, and the program reading the console may want to put its own
277/// line back. The shell redraws its prompt with the half-typed line under the notice; anything else ignores
278/// it, because it is not a key. The hosts pushed a real Enter (`\n`) for this until 2026-10-24, and an Enter
279/// is an ANSWER: a plug while `godspeed install` asked `[y/N]` answered it N, and at the prompt it ran the
280/// half-typed line (the VisionFive card). Outside ASCII, so no typed key produces it, and read from here by
281/// both ends.
282pub const NOTICE_SIGNAL: u8 = 0x84;
283
284/// HID usages of the keys the audio shortcuts use.
285const KEY_UP: u8 = 0x52;
286const KEY_DOWN: u8 = 0x51;
287const KEY_M: u8 = 0x10;
288
289/// The audio shortcut a key press is, with these modifiers: either Ctrl (0x01 / 0x10) AND either Alt
290/// (0x04 / 0x40), with Up, Down or M. `None` for every other key, which then means what it always did.
291pub fn audio_chord(key: u8, mods: u8) -> Option<u8> {
292    if mods & 0x11 == 0 || mods & 0x44 == 0 {
293        return None;
294    }
295    match key {
296        KEY_UP => Some(VOLUME_UP_SIGNAL),
297        KEY_DOWN => Some(VOLUME_DOWN_SIGNAL),
298        KEY_M => Some(MUTE_TOGGLE_SIGNAL),
299        _ => None,
300    }
301}
302
303/// True if a **keyboard** boot report is the Ctrl+Alt+Del chord: either Ctrl (left 0x01 / right
304/// 0x10) **and** either Alt (left 0x04 / right 0x40) held, with the Delete key down. This is the
305/// secure-attention combo - a driver checks it each poll for a keyboard device and, when true,
306/// pushes [`CTRL_ALT_DEL_SIGNAL`] for the shell to act on; the driver itself cannot reboot (SEC-2).
307/// (This said the driver "issues the reboot syscall", which SEC-2 removed.) Apply this ONLY to
308/// keyboard reports: a mouse boot report's button byte (byte 0) can alias the Ctrl/Alt modifier
309/// bits, so it must never be tested here.
310pub fn is_ctrl_alt_del(report: &[u8; 8]) -> bool {
311    if report[1] != 0 { return false; }                  // reserved byte ≠ 0 → invalid/stale report
312    let mods = report[0];
313    let ctrl = mods & 0x11 != 0;
314    let alt  = mods & 0x44 != 0;
315    ctrl && alt && report[2..8].contains(&KEY_DELETE)
316}
317
318/// Decode a keyboard boot report (modifiers in byte 0, up to six keycodes in
319/// bytes 2..8) with N-key edge detection: `emit(ascii)` is called for every key
320/// that is down now but was not in `last`, so rolling onto a new key before
321/// releasing the previous one drops nothing and a held key fires exactly once.
322/// `last` is updated to this report's keycodes for the next call.
323///
324/// `rep`/`now` drive typematic auto-repeat: the newest printable key still held is
325/// armed (at tick `now`) so the driver's [`KeyRepeat::poll`] re-emits it while held;
326/// releasing it disarms repeat. A key we don't map is reported via `on_unmapped`
327/// (loud, not silently dropped - §3.12) so its HID usage code can be logged and added.
328/// `caps` is the host's Caps Lock latch (toggled on each fresh Caps Lock press); the driver owns it
329/// per keyboard and passes it in, so the state persists across reports. It cases letters via
330/// `hid_to_ascii` (Caps Lock XORs Shift, letters only).
331pub fn decode_keyboard(
332    report: &[u8; 8],
333    last: &mut [u8; 6],
334    rep: &mut KeyRepeat,
335    caps: &mut bool,
336    now: u64,
337    mut emit: impl FnMut(u8),
338    mut on_unmapped: impl FnMut(u8),
339) {
340    // Reject an invalid report before decoding it. Byte 1 of a USB HID boot-keyboard report is
341    // reserved and is always 0; an all-`0xff` report (byte 1 == 0xff) is the signature of a
342    // failed/stale DMA read - what the buffer returns when the device has gone (e.g. mid-unplug)
343    // or the endpoint's buffer wasn't refreshed. Decoding it would spew 0xff "keystrokes" to the
344    // console AND corrupt `last` (poisoning edge-detection so later real keys diff wrong and
345    // never register). Drop it untouched: don't emit, don't update `last`, don't disarm repeat -
346    // so the next genuine report decodes cleanly.
347    if report[1] != 0 {
348        return;
349    }
350    let mods = report[0];
351    let cur = [report[2], report[3], report[4], report[5], report[6], report[7]];
352    for &k in cur.iter() {
353        if k == 0 || k == 0x01 { continue; } // 0 = empty slot, 0x01 = rollover error
354        if !last.contains(&k) {
355            // Caps Lock is a latch, not a character: a fresh press flips the host's `caps` state
356            // (which then cases letters) and emits nothing. It does NOT arm auto-repeat.
357            if k == KEY_CAPS_LOCK { *caps = !*caps; continue; }
358            if emit_key(k, mods, *caps, &mut emit) {
359                // Newest printable/cursor key held becomes the repeat key - except the
360                // one-shot control keys: Escape (0x29), whose repeat would make the shell
361                // re-disambiguate a bare ESC every tick; the function keys F1-F12
362                // (0x3A-0x45), which are actions, not characters (holding F1 should not
363                // re-open help over and over); and Enter/Return (0x28) + keypad Enter
364                // (0x58), commit keys whose repeat spams blank prompts and re-fires a
365                // confirmation's answer (the Wyse `y/N` "multiple gsh>" bug).
366                // And Ctrl+Alt+M: held, it would flip the mute on and off for as long as it is down.
367                // Ctrl+Alt+Up and Down DO repeat - holding them is how the volume is swept.
368                if k != 0x29 && k != 0x28 && k != 0x58 && !(0x3A..=0x45).contains(&k)
369                    && audio_chord(k, mods) != Some(MUTE_TOGGLE_SIGNAL) {
370                    rep.arm(k, mods, *caps, now);
371                }
372            } else if is_typable_code(k) {
373                // Modifiers/Caps/Esc (0x29, 0x39, 0xE0-E7) are not printable; only the
374                // typable ranges we'd expect to map are surfaced as "unmapped" noise.
375                on_unmapped(k);
376            }
377        }
378    }
379    // Stop repeating once the armed key is no longer held.
380    if rep.key != 0 && !cur.contains(&rep.key) {
381        rep.disarm();
382    }
383    *last = cur;
384}
385
386/// Left / right / middle button masks in a mouse boot report's byte 0.
387pub const MOUSE_LEFT: u8 = 0x01;
388pub const MOUSE_RIGHT: u8 = 0x02;
389pub const MOUSE_MIDDLE: u8 = 0x04;
390
391/// Name a mouse button mask, for logging.
392pub fn button_name(mask: u8) -> &'static str {
393    match mask {
394        MOUSE_LEFT => "LEFT",
395        MOUSE_RIGHT => "RIGHT",
396        MOUSE_MIDDLE => "MIDDLE",
397        _ => "?",
398    }
399}
400
401/// Tracks mouse boot reports across calls: edge-detects button transitions and
402/// accumulates relative motion. There is no on-screen cursor in a text console
403/// (that belongs to a future display server), so the driver surfaces events by
404/// logging; this struct decides *what* is worth surfacing and hands it back via
405/// closures.
406pub struct MouseTracker {
407    buttons: u8,
408    ax: i32,
409    ay: i32,
410}
411
412impl MouseTracker {
413    pub const fn new() -> Self {
414        MouseTracker { buttons: 0, ax: 0, ay: 0 }
415    }
416
417    /// Feed one boot report (byte 0 = buttons, byte 1 = dx, byte 2 = dy as signed
418    /// deltas). Calls `on_button(mask, down)` for each of left/right/middle that
419    /// changed, and `on_move(dx, dy)` once accumulated motion crosses a threshold
420    /// - a mouse emits far too many move reports to surface each one.
421    pub fn feed(
422        &mut self, report: &[u8; 8],
423        mut on_button: impl FnMut(u8, bool), mut on_move: impl FnMut(i32, i32),
424    ) {
425        let b = report[0] & 0x07;
426        let dx = report[1] as i8 as i32;
427        let dy = report[2] as i8 as i32;
428        let changed = b ^ self.buttons;
429        for &mask in &[MOUSE_LEFT, MOUSE_RIGHT, MOUSE_MIDDLE] {
430            if changed & mask != 0 {
431                on_button(mask, b & mask != 0);
432            }
433        }
434        self.buttons = b;
435        self.ax += dx;
436        self.ay += dy;
437        if self.ax.abs() + self.ay.abs() >= 60 {
438            on_move(self.ax, self.ay);
439            self.ax = 0;
440            self.ay = 0;
441        }
442    }
443}
444
445#[cfg(test)]
446mod tests {
447    use super::*;
448    extern crate std;
449    use std::vec::Vec;
450
451    // Decode a single-keycode report and collect the bytes it emits.
452    fn emit_for(code: u8) -> Vec<u8> {
453        let mut out = Vec::new();
454        let mut last = [0u8; 6];
455        let mut rep = KeyRepeat::new(0, 0);
456        let mut caps = false;
457        let report = [0, 0, code, 0, 0, 0, 0, 0];
458        decode_keyboard(&report, &mut last, &mut rep, &mut caps, 0, |b| out.push(b), |_| {});
459        out
460    }
461
462    #[test]
463    fn nav_cluster_emits_terminal_escape_sequences() {
464        // The navigation cluster a standard extended keyboard sends - each must map to
465        // the exact escape sequence the shell's CSI parser / pager understands.
466        assert_eq!(emit_for(0x4A), b"\x1b[H");  // Home
467        assert_eq!(emit_for(0x4D), b"\x1b[F");  // End
468        assert_eq!(emit_for(0x49), b"\x1b[2~"); // Insert
469        assert_eq!(emit_for(0x4C), b"\x1b[3~"); // Delete
470        assert_eq!(emit_for(0x4B), b"\x1b[5~"); // PageUp
471        assert_eq!(emit_for(0x4E), b"\x1b[6~"); // PageDown
472    }
473
474    #[test]
475    fn function_keys_emit_xterm_sequences() {
476        // F1-F4 are SS3 (ESC O P/Q/R/S); F5-F12 are ESC[<n>~. F1 is the one the shell acts
477        // on (help); the rest are recognised + consumed (no stray characters).
478        assert_eq!(emit_for(0x3A), b"\x1bOP");   // F1
479        assert_eq!(emit_for(0x3D), b"\x1bOS");   // F4
480        assert_eq!(emit_for(0x3E), b"\x1b[15~"); // F5
481        assert_eq!(emit_for(0x45), b"\x1b[24~"); // F12
482    }
483
484    #[test]
485    fn arrows_still_emit_their_sequences() {
486        assert_eq!(emit_for(0x52), b"\x1b[A"); // Up
487        assert_eq!(emit_for(0x51), b"\x1b[B"); // Down
488        assert_eq!(emit_for(0x4F), b"\x1b[C"); // Right
489        assert_eq!(emit_for(0x50), b"\x1b[D"); // Left
490    }
491
492    #[test]
493    fn ordinary_and_keypad_keys_unaffected() {
494        assert_eq!(emit_for(0x04), b"a");      // letter
495        assert_eq!(emit_for(0x59), b"1");      // keypad 1
496        assert_eq!(emit_for(0x2A), &[0x08]);   // backspace
497        assert_eq!(emit_for(0x28), b"\n");     // enter
498    }
499
500    #[test]
501    fn ctrl_letter_emits_control_codes() {
502        // Ctrl+letter must produce the C0 control byte a terminal sends, so USB-keyboard
503        // users can reach app shortcuts (editor ^S/^Q, shell ^C). Left Ctrl = 0x01.
504        assert_eq!(hid_to_ascii(0x16, 0x01, false), Some(0x13)); // Ctrl-S (save)
505        assert_eq!(hid_to_ascii(0x14, 0x01, false), Some(0x11)); // Ctrl-Q (quit)
506        assert_eq!(hid_to_ascii(0x06, 0x01, false), Some(0x03)); // Ctrl-C
507        assert_eq!(hid_to_ascii(0x04, 0x10, false), Some(0x01)); // Ctrl-A via RIGHT Ctrl (0x10)
508        assert_eq!(hid_to_ascii(0x1D, 0x01, false), Some(0x1A)); // Ctrl-Z
509        // Ctrl takes precedence over Shift/Caps, and a plain letter is unchanged.
510        assert_eq!(hid_to_ascii(0x16, 0x01 | 0x02, true), Some(0x13)); // Ctrl+Shift+S, Caps on → still ^S
511        assert_eq!(hid_to_ascii(0x16, 0x00, false), Some(b's'));       // no Ctrl → 's'
512    }
513
514    #[test]
515    fn caps_lock_cases_letters_only() {
516        // Caps Lock XORs Shift, but ONLY for letters. (key 0x16 = 's', 0x1E = '1'/'!')
517        assert_eq!(hid_to_ascii(0x16, 0x00, true),  Some(b'S')); // Caps on → uppercase
518        assert_eq!(hid_to_ascii(0x16, 0x22, true),  Some(b's')); // Caps + Shift → lowercase (XOR)
519        assert_eq!(hid_to_ascii(0x16, 0x22, false), Some(b'S')); // Shift only → uppercase
520        assert_eq!(hid_to_ascii(0x16, 0x00, false), Some(b's')); // neither → lowercase
521        // Digits/symbols ignore Caps Lock - only Shift changes them.
522        assert_eq!(hid_to_ascii(0x1E, 0x00, true),  Some(b'1')); // Caps on, a digit → still '1'
523        assert_eq!(hid_to_ascii(0x1E, 0x22, true),  Some(b'!')); // Shift → '!' (Caps irrelevant)
524    }
525
526    #[test]
527    fn caps_lock_key_toggles_the_latch() {
528        // The Caps Lock keycode (0x39) flips the host latch and emits nothing; letters then case.
529        // Each helper decodes one fresh press (last reset to empty so the press re-triggers).
530        fn decode_one(code: u8, caps: &mut bool) -> Vec<u8> {
531            let mut out = Vec::new();
532            let mut last = [0u8; 6];
533            let mut rep = KeyRepeat::new(0, 0);
534            decode_keyboard(&[0, 0, code, 0, 0, 0, 0, 0], &mut last, &mut rep, caps, 0,
535                            |b| out.push(b), |_| {});
536            out
537        }
538        let mut caps = false;
539        assert_eq!(decode_one(0x16, &mut caps), b"s");  // off → 's'  (0x16 = 's')
540        assert_eq!(decode_one(0x39, &mut caps), b"");   // Caps press emits nothing
541        assert!(caps);
542        assert_eq!(decode_one(0x16, &mut caps), b"S");  // on → 'S'
543        assert_eq!(decode_one(0x39, &mut caps), b"");
544        assert!(!caps);
545        assert_eq!(decode_one(0x16, &mut caps), b"s");  // off again → 's'
546    }
547
548    #[test]
549    fn ctrl_alt_del_chord_detected() {
550        // The reboot chord: a Ctrl bit + an Alt bit + the Delete keycode (0x4C) in a key slot.
551        assert!(is_ctrl_alt_del(&[0x01 | 0x04, 0, 0x4C, 0, 0, 0, 0, 0]));  // left Ctrl + left Alt + Del
552        assert!(is_ctrl_alt_del(&[0x10 | 0x44, 0, 0, 0x4C, 0, 0, 0, 0]));  // right Ctrl + Alts + Del in slot 2
553        // Missing any leg of the chord → not detected.
554        assert!(!is_ctrl_alt_del(&[0x01, 0, 0x4C, 0, 0, 0, 0, 0]));        // Ctrl+Del, no Alt
555        assert!(!is_ctrl_alt_del(&[0x04, 0, 0x4C, 0, 0, 0, 0, 0]));        // Alt+Del, no Ctrl
556        assert!(!is_ctrl_alt_del(&[0x05, 0, 0, 0, 0, 0, 0, 0]));           // Ctrl+Alt, no Del
557        // A stale/invalid report (reserved byte ≠ 0) is rejected even if it otherwise matches.
558        assert!(!is_ctrl_alt_del(&[0x05, 0xFF, 0x4C, 0, 0, 0, 0, 0]));
559    }
560
561    // Decode one report with these modifiers and one keycode, from a cold `last`.
562    fn emit_mods(mods: u8, code: u8, rep: &mut KeyRepeat) -> Vec<u8> {
563        let mut out = Vec::new();
564        let mut last = [0u8; 6];
565        let mut caps = false;
566        decode_keyboard(&[mods, 0, code, 0, 0, 0, 0, 0], &mut last, rep, &mut caps, 0, |b| out.push(b), |_| {});
567        out
568    }
569
570    #[test]
571    fn audio_shortcuts_are_signals_not_keys() {
572        let mut rep = KeyRepeat::new(0, 0);
573        // Ctrl+Alt+Up / Down / M, either side's modifiers: the signal, and nothing else.
574        assert_eq!(emit_mods(0x01 | 0x04, 0x52, &mut rep), [VOLUME_UP_SIGNAL]);
575        assert_eq!(emit_mods(0x10 | 0x40, 0x51, &mut rep), [VOLUME_DOWN_SIGNAL]);
576        assert_eq!(emit_mods(0x01 | 0x40, 0x10, &mut rep), [MUTE_TOGGLE_SIGNAL]);
577        // Without both Ctrl and Alt they are what they always were: an arrow is still an arrow.
578        assert_eq!(emit_mods(0, 0x52, &mut rep), b"\x1b[A");
579        assert_eq!(emit_mods(0x01, 0x52, &mut rep), b"\x1b[A");
580        assert_eq!(emit_mods(0x04, 0x10, &mut rep), b"m");
581        // Outside ASCII, so no typed key can produce one.
582        for s in [VOLUME_UP_SIGNAL, VOLUME_DOWN_SIGNAL, MUTE_TOGGLE_SIGNAL, CTRL_ALT_DEL_SIGNAL, NOTICE_SIGNAL] {
583            assert!(s >= 0x80);
584        }
585    }
586
587    #[test]
588    fn a_held_volume_chord_repeats_and_a_held_mute_does_not() {
589        // Holding Ctrl+Alt+Up sweeps the volume: the key arms auto-repeat, which re-emits the signal.
590        let mut rep = KeyRepeat::new(10, 5);
591        let _ = emit_mods(0x05, 0x52, &mut rep);
592        let mut again = Vec::new();
593        rep.poll(100, |b| again.push(b));
594        assert_eq!(again, [VOLUME_UP_SIGNAL]);
595        // Holding Ctrl+Alt+M must not flip the mute back and forth: it never arms.
596        let mut rep = KeyRepeat::new(10, 5);
597        let _ = emit_mods(0x05, 0x10, &mut rep);
598        let mut again = Vec::new();
599        rep.poll(100, |b| again.push(b));
600        assert!(again.is_empty());
601    }
602}