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}