mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-03-29 22:56:45 +08:00
refactor(hid): 统一 HID 键盘 CanonicalKey 语义并清理前端布局与输入链路冗余代码
This commit is contained in:
@@ -28,7 +28,6 @@ use std::time::{Duration, Instant};
|
||||
use tracing::{info, trace, warn};
|
||||
|
||||
use super::backend::{HidBackend, HidBackendStatus};
|
||||
use super::keymap;
|
||||
use super::types::{KeyEventType, KeyboardEvent, KeyboardReport, MouseEvent, MouseEventType};
|
||||
use crate::error::{AppError, Result};
|
||||
|
||||
@@ -1095,18 +1094,13 @@ impl HidBackend for Ch9329Backend {
|
||||
}
|
||||
|
||||
async fn send_keyboard(&self, event: KeyboardEvent) -> Result<()> {
|
||||
// Convert JS keycode to USB HID if needed (skip if already USB HID)
|
||||
let usb_key = if event.is_usb_hid {
|
||||
event.key
|
||||
} else {
|
||||
keymap::js_to_usb(event.key).unwrap_or(event.key)
|
||||
};
|
||||
let usb_key = event.key.to_hid_usage();
|
||||
|
||||
// Handle modifier keys separately
|
||||
if keymap::is_modifier_key(usb_key) {
|
||||
if event.key.is_modifier() {
|
||||
let mut state = self.keyboard_state.lock();
|
||||
|
||||
if let Some(bit) = keymap::modifier_bit(usb_key) {
|
||||
if let Some(bit) = event.key.modifier_bit() {
|
||||
match event.event_type {
|
||||
KeyEventType::Down => state.modifiers |= bit,
|
||||
KeyEventType::Up => state.modifiers &= !bit,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
//!
|
||||
//! Keyboard event (type 0x01):
|
||||
//! - Byte 1: Event type (0x00 = down, 0x01 = up)
|
||||
//! - Byte 2: Key code (USB HID usage code)
|
||||
//! - Byte 2: Canonical key code (stable One-KVM key id aligned with HID usage)
|
||||
//! - Byte 3: Modifiers bitmask
|
||||
//! - Bit 0: Left Ctrl
|
||||
//! - Bit 1: Left Shift
|
||||
@@ -38,7 +38,8 @@ use tracing::warn;
|
||||
|
||||
use super::types::ConsumerEvent;
|
||||
use super::{
|
||||
KeyEventType, KeyboardEvent, KeyboardModifiers, MouseButton, MouseEvent, MouseEventType,
|
||||
CanonicalKey, KeyEventType, KeyboardEvent, KeyboardModifiers, MouseButton, MouseEvent,
|
||||
MouseEventType,
|
||||
};
|
||||
|
||||
/// Message types
|
||||
@@ -101,7 +102,13 @@ fn parse_keyboard_message(data: &[u8]) -> Option<HidChannelEvent> {
|
||||
}
|
||||
};
|
||||
|
||||
let key = data[1];
|
||||
let key = match CanonicalKey::from_hid_usage(data[1]) {
|
||||
Some(key) => key,
|
||||
None => {
|
||||
warn!("Unknown canonical keyboard key code: 0x{:02X}", data[1]);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let modifiers_byte = data[2];
|
||||
|
||||
let modifiers = KeyboardModifiers {
|
||||
@@ -119,7 +126,6 @@ fn parse_keyboard_message(data: &[u8]) -> Option<HidChannelEvent> {
|
||||
event_type,
|
||||
key,
|
||||
modifiers,
|
||||
is_usb_hid: true, // WebRTC/WebSocket HID channel sends USB HID usages
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -193,7 +199,7 @@ pub fn encode_keyboard_event(event: &KeyboardEvent) -> Vec<u8> {
|
||||
|
||||
let modifiers = event.modifiers.to_hid_byte();
|
||||
|
||||
vec![MSG_KEYBOARD, event_type, event.key, modifiers]
|
||||
vec![MSG_KEYBOARD, event_type, event.key.to_hid_usage(), modifiers]
|
||||
}
|
||||
|
||||
/// Encode a mouse event to binary format (for sending to client if needed)
|
||||
@@ -242,10 +248,9 @@ mod tests {
|
||||
match event {
|
||||
HidChannelEvent::Keyboard(kb) => {
|
||||
assert!(matches!(kb.event_type, KeyEventType::Down));
|
||||
assert_eq!(kb.key, 0x04);
|
||||
assert_eq!(kb.key, CanonicalKey::KeyA);
|
||||
assert!(kb.modifiers.left_ctrl);
|
||||
assert!(!kb.modifiers.left_shift);
|
||||
assert!(kb.is_usb_hid);
|
||||
}
|
||||
_ => panic!("Expected keyboard event"),
|
||||
}
|
||||
@@ -270,7 +275,7 @@ mod tests {
|
||||
fn test_encode_keyboard() {
|
||||
let event = KeyboardEvent {
|
||||
event_type: KeyEventType::Down,
|
||||
key: 0x04,
|
||||
key: CanonicalKey::KeyA,
|
||||
modifiers: KeyboardModifiers {
|
||||
left_ctrl: true,
|
||||
left_shift: false,
|
||||
@@ -281,7 +286,6 @@ mod tests {
|
||||
right_alt: false,
|
||||
right_meta: false,
|
||||
},
|
||||
is_usb_hid: true,
|
||||
};
|
||||
|
||||
let encoded = encode_keyboard_event(&event);
|
||||
|
||||
409
src/hid/keyboard.rs
Normal file
409
src/hid/keyboard.rs
Normal file
@@ -0,0 +1,409 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
/// Shared canonical keyboard key identifiers used across frontend and backend.
|
||||
///
|
||||
/// The enum names intentionally mirror `KeyboardEvent.code` style values so the
|
||||
/// browser, virtual keyboard, and HID backend can all speak the same language.
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum CanonicalKey {
|
||||
KeyA,
|
||||
KeyB,
|
||||
KeyC,
|
||||
KeyD,
|
||||
KeyE,
|
||||
KeyF,
|
||||
KeyG,
|
||||
KeyH,
|
||||
KeyI,
|
||||
KeyJ,
|
||||
KeyK,
|
||||
KeyL,
|
||||
KeyM,
|
||||
KeyN,
|
||||
KeyO,
|
||||
KeyP,
|
||||
KeyQ,
|
||||
KeyR,
|
||||
KeyS,
|
||||
KeyT,
|
||||
KeyU,
|
||||
KeyV,
|
||||
KeyW,
|
||||
KeyX,
|
||||
KeyY,
|
||||
KeyZ,
|
||||
Digit1,
|
||||
Digit2,
|
||||
Digit3,
|
||||
Digit4,
|
||||
Digit5,
|
||||
Digit6,
|
||||
Digit7,
|
||||
Digit8,
|
||||
Digit9,
|
||||
Digit0,
|
||||
Enter,
|
||||
Escape,
|
||||
Backspace,
|
||||
Tab,
|
||||
Space,
|
||||
Minus,
|
||||
Equal,
|
||||
BracketLeft,
|
||||
BracketRight,
|
||||
Backslash,
|
||||
Semicolon,
|
||||
Quote,
|
||||
Backquote,
|
||||
Comma,
|
||||
Period,
|
||||
Slash,
|
||||
CapsLock,
|
||||
F1,
|
||||
F2,
|
||||
F3,
|
||||
F4,
|
||||
F5,
|
||||
F6,
|
||||
F7,
|
||||
F8,
|
||||
F9,
|
||||
F10,
|
||||
F11,
|
||||
F12,
|
||||
PrintScreen,
|
||||
ScrollLock,
|
||||
Pause,
|
||||
Insert,
|
||||
Home,
|
||||
PageUp,
|
||||
Delete,
|
||||
End,
|
||||
PageDown,
|
||||
ArrowRight,
|
||||
ArrowLeft,
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
NumLock,
|
||||
NumpadDivide,
|
||||
NumpadMultiply,
|
||||
NumpadSubtract,
|
||||
NumpadAdd,
|
||||
NumpadEnter,
|
||||
Numpad1,
|
||||
Numpad2,
|
||||
Numpad3,
|
||||
Numpad4,
|
||||
Numpad5,
|
||||
Numpad6,
|
||||
Numpad7,
|
||||
Numpad8,
|
||||
Numpad9,
|
||||
Numpad0,
|
||||
NumpadDecimal,
|
||||
IntlBackslash,
|
||||
ContextMenu,
|
||||
F13,
|
||||
F14,
|
||||
F15,
|
||||
F16,
|
||||
F17,
|
||||
F18,
|
||||
F19,
|
||||
F20,
|
||||
F21,
|
||||
F22,
|
||||
F23,
|
||||
F24,
|
||||
ControlLeft,
|
||||
ShiftLeft,
|
||||
AltLeft,
|
||||
MetaLeft,
|
||||
ControlRight,
|
||||
ShiftRight,
|
||||
AltRight,
|
||||
MetaRight,
|
||||
}
|
||||
|
||||
impl CanonicalKey {
|
||||
/// Convert the canonical key to a stable wire code.
|
||||
///
|
||||
/// The wire code intentionally matches the USB HID usage for keyboard page
|
||||
/// keys so existing low-level behavior stays intact while the semantic type
|
||||
/// becomes explicit.
|
||||
pub const fn to_hid_usage(self) -> u8 {
|
||||
match self {
|
||||
Self::KeyA => 0x04,
|
||||
Self::KeyB => 0x05,
|
||||
Self::KeyC => 0x06,
|
||||
Self::KeyD => 0x07,
|
||||
Self::KeyE => 0x08,
|
||||
Self::KeyF => 0x09,
|
||||
Self::KeyG => 0x0A,
|
||||
Self::KeyH => 0x0B,
|
||||
Self::KeyI => 0x0C,
|
||||
Self::KeyJ => 0x0D,
|
||||
Self::KeyK => 0x0E,
|
||||
Self::KeyL => 0x0F,
|
||||
Self::KeyM => 0x10,
|
||||
Self::KeyN => 0x11,
|
||||
Self::KeyO => 0x12,
|
||||
Self::KeyP => 0x13,
|
||||
Self::KeyQ => 0x14,
|
||||
Self::KeyR => 0x15,
|
||||
Self::KeyS => 0x16,
|
||||
Self::KeyT => 0x17,
|
||||
Self::KeyU => 0x18,
|
||||
Self::KeyV => 0x19,
|
||||
Self::KeyW => 0x1A,
|
||||
Self::KeyX => 0x1B,
|
||||
Self::KeyY => 0x1C,
|
||||
Self::KeyZ => 0x1D,
|
||||
Self::Digit1 => 0x1E,
|
||||
Self::Digit2 => 0x1F,
|
||||
Self::Digit3 => 0x20,
|
||||
Self::Digit4 => 0x21,
|
||||
Self::Digit5 => 0x22,
|
||||
Self::Digit6 => 0x23,
|
||||
Self::Digit7 => 0x24,
|
||||
Self::Digit8 => 0x25,
|
||||
Self::Digit9 => 0x26,
|
||||
Self::Digit0 => 0x27,
|
||||
Self::Enter => 0x28,
|
||||
Self::Escape => 0x29,
|
||||
Self::Backspace => 0x2A,
|
||||
Self::Tab => 0x2B,
|
||||
Self::Space => 0x2C,
|
||||
Self::Minus => 0x2D,
|
||||
Self::Equal => 0x2E,
|
||||
Self::BracketLeft => 0x2F,
|
||||
Self::BracketRight => 0x30,
|
||||
Self::Backslash => 0x31,
|
||||
Self::Semicolon => 0x33,
|
||||
Self::Quote => 0x34,
|
||||
Self::Backquote => 0x35,
|
||||
Self::Comma => 0x36,
|
||||
Self::Period => 0x37,
|
||||
Self::Slash => 0x38,
|
||||
Self::CapsLock => 0x39,
|
||||
Self::F1 => 0x3A,
|
||||
Self::F2 => 0x3B,
|
||||
Self::F3 => 0x3C,
|
||||
Self::F4 => 0x3D,
|
||||
Self::F5 => 0x3E,
|
||||
Self::F6 => 0x3F,
|
||||
Self::F7 => 0x40,
|
||||
Self::F8 => 0x41,
|
||||
Self::F9 => 0x42,
|
||||
Self::F10 => 0x43,
|
||||
Self::F11 => 0x44,
|
||||
Self::F12 => 0x45,
|
||||
Self::PrintScreen => 0x46,
|
||||
Self::ScrollLock => 0x47,
|
||||
Self::Pause => 0x48,
|
||||
Self::Insert => 0x49,
|
||||
Self::Home => 0x4A,
|
||||
Self::PageUp => 0x4B,
|
||||
Self::Delete => 0x4C,
|
||||
Self::End => 0x4D,
|
||||
Self::PageDown => 0x4E,
|
||||
Self::ArrowRight => 0x4F,
|
||||
Self::ArrowLeft => 0x50,
|
||||
Self::ArrowDown => 0x51,
|
||||
Self::ArrowUp => 0x52,
|
||||
Self::NumLock => 0x53,
|
||||
Self::NumpadDivide => 0x54,
|
||||
Self::NumpadMultiply => 0x55,
|
||||
Self::NumpadSubtract => 0x56,
|
||||
Self::NumpadAdd => 0x57,
|
||||
Self::NumpadEnter => 0x58,
|
||||
Self::Numpad1 => 0x59,
|
||||
Self::Numpad2 => 0x5A,
|
||||
Self::Numpad3 => 0x5B,
|
||||
Self::Numpad4 => 0x5C,
|
||||
Self::Numpad5 => 0x5D,
|
||||
Self::Numpad6 => 0x5E,
|
||||
Self::Numpad7 => 0x5F,
|
||||
Self::Numpad8 => 0x60,
|
||||
Self::Numpad9 => 0x61,
|
||||
Self::Numpad0 => 0x62,
|
||||
Self::NumpadDecimal => 0x63,
|
||||
Self::IntlBackslash => 0x64,
|
||||
Self::ContextMenu => 0x65,
|
||||
Self::F13 => 0x68,
|
||||
Self::F14 => 0x69,
|
||||
Self::F15 => 0x6A,
|
||||
Self::F16 => 0x6B,
|
||||
Self::F17 => 0x6C,
|
||||
Self::F18 => 0x6D,
|
||||
Self::F19 => 0x6E,
|
||||
Self::F20 => 0x6F,
|
||||
Self::F21 => 0x70,
|
||||
Self::F22 => 0x71,
|
||||
Self::F23 => 0x72,
|
||||
Self::F24 => 0x73,
|
||||
Self::ControlLeft => 0xE0,
|
||||
Self::ShiftLeft => 0xE1,
|
||||
Self::AltLeft => 0xE2,
|
||||
Self::MetaLeft => 0xE3,
|
||||
Self::ControlRight => 0xE4,
|
||||
Self::ShiftRight => 0xE5,
|
||||
Self::AltRight => 0xE6,
|
||||
Self::MetaRight => 0xE7,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a wire code / USB HID usage to its canonical key.
|
||||
pub const fn from_hid_usage(usage: u8) -> Option<Self> {
|
||||
match usage {
|
||||
0x04 => Some(Self::KeyA),
|
||||
0x05 => Some(Self::KeyB),
|
||||
0x06 => Some(Self::KeyC),
|
||||
0x07 => Some(Self::KeyD),
|
||||
0x08 => Some(Self::KeyE),
|
||||
0x09 => Some(Self::KeyF),
|
||||
0x0A => Some(Self::KeyG),
|
||||
0x0B => Some(Self::KeyH),
|
||||
0x0C => Some(Self::KeyI),
|
||||
0x0D => Some(Self::KeyJ),
|
||||
0x0E => Some(Self::KeyK),
|
||||
0x0F => Some(Self::KeyL),
|
||||
0x10 => Some(Self::KeyM),
|
||||
0x11 => Some(Self::KeyN),
|
||||
0x12 => Some(Self::KeyO),
|
||||
0x13 => Some(Self::KeyP),
|
||||
0x14 => Some(Self::KeyQ),
|
||||
0x15 => Some(Self::KeyR),
|
||||
0x16 => Some(Self::KeyS),
|
||||
0x17 => Some(Self::KeyT),
|
||||
0x18 => Some(Self::KeyU),
|
||||
0x19 => Some(Self::KeyV),
|
||||
0x1A => Some(Self::KeyW),
|
||||
0x1B => Some(Self::KeyX),
|
||||
0x1C => Some(Self::KeyY),
|
||||
0x1D => Some(Self::KeyZ),
|
||||
0x1E => Some(Self::Digit1),
|
||||
0x1F => Some(Self::Digit2),
|
||||
0x20 => Some(Self::Digit3),
|
||||
0x21 => Some(Self::Digit4),
|
||||
0x22 => Some(Self::Digit5),
|
||||
0x23 => Some(Self::Digit6),
|
||||
0x24 => Some(Self::Digit7),
|
||||
0x25 => Some(Self::Digit8),
|
||||
0x26 => Some(Self::Digit9),
|
||||
0x27 => Some(Self::Digit0),
|
||||
0x28 => Some(Self::Enter),
|
||||
0x29 => Some(Self::Escape),
|
||||
0x2A => Some(Self::Backspace),
|
||||
0x2B => Some(Self::Tab),
|
||||
0x2C => Some(Self::Space),
|
||||
0x2D => Some(Self::Minus),
|
||||
0x2E => Some(Self::Equal),
|
||||
0x2F => Some(Self::BracketLeft),
|
||||
0x30 => Some(Self::BracketRight),
|
||||
0x31 => Some(Self::Backslash),
|
||||
0x33 => Some(Self::Semicolon),
|
||||
0x34 => Some(Self::Quote),
|
||||
0x35 => Some(Self::Backquote),
|
||||
0x36 => Some(Self::Comma),
|
||||
0x37 => Some(Self::Period),
|
||||
0x38 => Some(Self::Slash),
|
||||
0x39 => Some(Self::CapsLock),
|
||||
0x3A => Some(Self::F1),
|
||||
0x3B => Some(Self::F2),
|
||||
0x3C => Some(Self::F3),
|
||||
0x3D => Some(Self::F4),
|
||||
0x3E => Some(Self::F5),
|
||||
0x3F => Some(Self::F6),
|
||||
0x40 => Some(Self::F7),
|
||||
0x41 => Some(Self::F8),
|
||||
0x42 => Some(Self::F9),
|
||||
0x43 => Some(Self::F10),
|
||||
0x44 => Some(Self::F11),
|
||||
0x45 => Some(Self::F12),
|
||||
0x46 => Some(Self::PrintScreen),
|
||||
0x47 => Some(Self::ScrollLock),
|
||||
0x48 => Some(Self::Pause),
|
||||
0x49 => Some(Self::Insert),
|
||||
0x4A => Some(Self::Home),
|
||||
0x4B => Some(Self::PageUp),
|
||||
0x4C => Some(Self::Delete),
|
||||
0x4D => Some(Self::End),
|
||||
0x4E => Some(Self::PageDown),
|
||||
0x4F => Some(Self::ArrowRight),
|
||||
0x50 => Some(Self::ArrowLeft),
|
||||
0x51 => Some(Self::ArrowDown),
|
||||
0x52 => Some(Self::ArrowUp),
|
||||
0x53 => Some(Self::NumLock),
|
||||
0x54 => Some(Self::NumpadDivide),
|
||||
0x55 => Some(Self::NumpadMultiply),
|
||||
0x56 => Some(Self::NumpadSubtract),
|
||||
0x57 => Some(Self::NumpadAdd),
|
||||
0x58 => Some(Self::NumpadEnter),
|
||||
0x59 => Some(Self::Numpad1),
|
||||
0x5A => Some(Self::Numpad2),
|
||||
0x5B => Some(Self::Numpad3),
|
||||
0x5C => Some(Self::Numpad4),
|
||||
0x5D => Some(Self::Numpad5),
|
||||
0x5E => Some(Self::Numpad6),
|
||||
0x5F => Some(Self::Numpad7),
|
||||
0x60 => Some(Self::Numpad8),
|
||||
0x61 => Some(Self::Numpad9),
|
||||
0x62 => Some(Self::Numpad0),
|
||||
0x63 => Some(Self::NumpadDecimal),
|
||||
0x64 => Some(Self::IntlBackslash),
|
||||
0x65 => Some(Self::ContextMenu),
|
||||
0x68 => Some(Self::F13),
|
||||
0x69 => Some(Self::F14),
|
||||
0x6A => Some(Self::F15),
|
||||
0x6B => Some(Self::F16),
|
||||
0x6C => Some(Self::F17),
|
||||
0x6D => Some(Self::F18),
|
||||
0x6E => Some(Self::F19),
|
||||
0x6F => Some(Self::F20),
|
||||
0x70 => Some(Self::F21),
|
||||
0x71 => Some(Self::F22),
|
||||
0x72 => Some(Self::F23),
|
||||
0x73 => Some(Self::F24),
|
||||
0xE0 => Some(Self::ControlLeft),
|
||||
0xE1 => Some(Self::ShiftLeft),
|
||||
0xE2 => Some(Self::AltLeft),
|
||||
0xE3 => Some(Self::MetaLeft),
|
||||
0xE4 => Some(Self::ControlRight),
|
||||
0xE5 => Some(Self::ShiftRight),
|
||||
0xE6 => Some(Self::AltRight),
|
||||
0xE7 => Some(Self::MetaRight),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn is_modifier(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::ControlLeft
|
||||
| Self::ShiftLeft
|
||||
| Self::AltLeft
|
||||
| Self::MetaLeft
|
||||
| Self::ControlRight
|
||||
| Self::ShiftRight
|
||||
| Self::AltRight
|
||||
| Self::MetaRight
|
||||
)
|
||||
}
|
||||
|
||||
pub const fn modifier_bit(self) -> Option<u8> {
|
||||
match self {
|
||||
Self::ControlLeft => Some(0x01),
|
||||
Self::ShiftLeft => Some(0x02),
|
||||
Self::AltLeft => Some(0x04),
|
||||
Self::MetaLeft => Some(0x08),
|
||||
Self::ControlRight => Some(0x10),
|
||||
Self::ShiftRight => Some(0x20),
|
||||
Self::AltRight => Some(0x40),
|
||||
Self::MetaRight => Some(0x80),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,430 +0,0 @@
|
||||
//! USB HID keyboard key codes mapping
|
||||
//!
|
||||
//! This module provides mapping between JavaScript key codes and USB HID usage codes.
|
||||
//! Reference: USB HID Usage Tables 1.12, Section 10 (Keyboard/Keypad Page)
|
||||
|
||||
/// USB HID key codes (Usage Page 0x07)
|
||||
#[allow(dead_code)]
|
||||
pub mod usb {
|
||||
// Letters A-Z (0x04 - 0x1D)
|
||||
pub const KEY_A: u8 = 0x04;
|
||||
pub const KEY_B: u8 = 0x05;
|
||||
pub const KEY_C: u8 = 0x06;
|
||||
pub const KEY_D: u8 = 0x07;
|
||||
pub const KEY_E: u8 = 0x08;
|
||||
pub const KEY_F: u8 = 0x09;
|
||||
pub const KEY_G: u8 = 0x0A;
|
||||
pub const KEY_H: u8 = 0x0B;
|
||||
pub const KEY_I: u8 = 0x0C;
|
||||
pub const KEY_J: u8 = 0x0D;
|
||||
pub const KEY_K: u8 = 0x0E;
|
||||
pub const KEY_L: u8 = 0x0F;
|
||||
pub const KEY_M: u8 = 0x10;
|
||||
pub const KEY_N: u8 = 0x11;
|
||||
pub const KEY_O: u8 = 0x12;
|
||||
pub const KEY_P: u8 = 0x13;
|
||||
pub const KEY_Q: u8 = 0x14;
|
||||
pub const KEY_R: u8 = 0x15;
|
||||
pub const KEY_S: u8 = 0x16;
|
||||
pub const KEY_T: u8 = 0x17;
|
||||
pub const KEY_U: u8 = 0x18;
|
||||
pub const KEY_V: u8 = 0x19;
|
||||
pub const KEY_W: u8 = 0x1A;
|
||||
pub const KEY_X: u8 = 0x1B;
|
||||
pub const KEY_Y: u8 = 0x1C;
|
||||
pub const KEY_Z: u8 = 0x1D;
|
||||
|
||||
// Numbers 1-9, 0 (0x1E - 0x27)
|
||||
pub const KEY_1: u8 = 0x1E;
|
||||
pub const KEY_2: u8 = 0x1F;
|
||||
pub const KEY_3: u8 = 0x20;
|
||||
pub const KEY_4: u8 = 0x21;
|
||||
pub const KEY_5: u8 = 0x22;
|
||||
pub const KEY_6: u8 = 0x23;
|
||||
pub const KEY_7: u8 = 0x24;
|
||||
pub const KEY_8: u8 = 0x25;
|
||||
pub const KEY_9: u8 = 0x26;
|
||||
pub const KEY_0: u8 = 0x27;
|
||||
|
||||
// Control keys
|
||||
pub const KEY_ENTER: u8 = 0x28;
|
||||
pub const KEY_ESCAPE: u8 = 0x29;
|
||||
pub const KEY_BACKSPACE: u8 = 0x2A;
|
||||
pub const KEY_TAB: u8 = 0x2B;
|
||||
pub const KEY_SPACE: u8 = 0x2C;
|
||||
pub const KEY_MINUS: u8 = 0x2D;
|
||||
pub const KEY_EQUAL: u8 = 0x2E;
|
||||
pub const KEY_LEFT_BRACKET: u8 = 0x2F;
|
||||
pub const KEY_RIGHT_BRACKET: u8 = 0x30;
|
||||
pub const KEY_BACKSLASH: u8 = 0x31;
|
||||
pub const KEY_HASH: u8 = 0x32; // Non-US # and ~
|
||||
pub const KEY_SEMICOLON: u8 = 0x33;
|
||||
pub const KEY_APOSTROPHE: u8 = 0x34;
|
||||
pub const KEY_GRAVE: u8 = 0x35;
|
||||
pub const KEY_COMMA: u8 = 0x36;
|
||||
pub const KEY_PERIOD: u8 = 0x37;
|
||||
pub const KEY_SLASH: u8 = 0x38;
|
||||
pub const KEY_CAPS_LOCK: u8 = 0x39;
|
||||
|
||||
// Function keys F1-F12
|
||||
pub const KEY_F1: u8 = 0x3A;
|
||||
pub const KEY_F2: u8 = 0x3B;
|
||||
pub const KEY_F3: u8 = 0x3C;
|
||||
pub const KEY_F4: u8 = 0x3D;
|
||||
pub const KEY_F5: u8 = 0x3E;
|
||||
pub const KEY_F6: u8 = 0x3F;
|
||||
pub const KEY_F7: u8 = 0x40;
|
||||
pub const KEY_F8: u8 = 0x41;
|
||||
pub const KEY_F9: u8 = 0x42;
|
||||
pub const KEY_F10: u8 = 0x43;
|
||||
pub const KEY_F11: u8 = 0x44;
|
||||
pub const KEY_F12: u8 = 0x45;
|
||||
|
||||
// Special keys
|
||||
pub const KEY_PRINT_SCREEN: u8 = 0x46;
|
||||
pub const KEY_SCROLL_LOCK: u8 = 0x47;
|
||||
pub const KEY_PAUSE: u8 = 0x48;
|
||||
pub const KEY_INSERT: u8 = 0x49;
|
||||
pub const KEY_HOME: u8 = 0x4A;
|
||||
pub const KEY_PAGE_UP: u8 = 0x4B;
|
||||
pub const KEY_DELETE: u8 = 0x4C;
|
||||
pub const KEY_END: u8 = 0x4D;
|
||||
pub const KEY_PAGE_DOWN: u8 = 0x4E;
|
||||
pub const KEY_RIGHT_ARROW: u8 = 0x4F;
|
||||
pub const KEY_LEFT_ARROW: u8 = 0x50;
|
||||
pub const KEY_DOWN_ARROW: u8 = 0x51;
|
||||
pub const KEY_UP_ARROW: u8 = 0x52;
|
||||
|
||||
// Numpad
|
||||
pub const KEY_NUM_LOCK: u8 = 0x53;
|
||||
pub const KEY_NUMPAD_DIVIDE: u8 = 0x54;
|
||||
pub const KEY_NUMPAD_MULTIPLY: u8 = 0x55;
|
||||
pub const KEY_NUMPAD_MINUS: u8 = 0x56;
|
||||
pub const KEY_NUMPAD_PLUS: u8 = 0x57;
|
||||
pub const KEY_NUMPAD_ENTER: u8 = 0x58;
|
||||
pub const KEY_NUMPAD_1: u8 = 0x59;
|
||||
pub const KEY_NUMPAD_2: u8 = 0x5A;
|
||||
pub const KEY_NUMPAD_3: u8 = 0x5B;
|
||||
pub const KEY_NUMPAD_4: u8 = 0x5C;
|
||||
pub const KEY_NUMPAD_5: u8 = 0x5D;
|
||||
pub const KEY_NUMPAD_6: u8 = 0x5E;
|
||||
pub const KEY_NUMPAD_7: u8 = 0x5F;
|
||||
pub const KEY_NUMPAD_8: u8 = 0x60;
|
||||
pub const KEY_NUMPAD_9: u8 = 0x61;
|
||||
pub const KEY_NUMPAD_0: u8 = 0x62;
|
||||
pub const KEY_NUMPAD_DECIMAL: u8 = 0x63;
|
||||
|
||||
// Additional keys
|
||||
pub const KEY_NON_US_BACKSLASH: u8 = 0x64;
|
||||
pub const KEY_APPLICATION: u8 = 0x65; // Context menu
|
||||
pub const KEY_POWER: u8 = 0x66;
|
||||
pub const KEY_NUMPAD_EQUAL: u8 = 0x67;
|
||||
|
||||
// F13-F24
|
||||
pub const KEY_F13: u8 = 0x68;
|
||||
pub const KEY_F14: u8 = 0x69;
|
||||
pub const KEY_F15: u8 = 0x6A;
|
||||
pub const KEY_F16: u8 = 0x6B;
|
||||
pub const KEY_F17: u8 = 0x6C;
|
||||
pub const KEY_F18: u8 = 0x6D;
|
||||
pub const KEY_F19: u8 = 0x6E;
|
||||
pub const KEY_F20: u8 = 0x6F;
|
||||
pub const KEY_F21: u8 = 0x70;
|
||||
pub const KEY_F22: u8 = 0x71;
|
||||
pub const KEY_F23: u8 = 0x72;
|
||||
pub const KEY_F24: u8 = 0x73;
|
||||
|
||||
// Modifier keys (these are handled separately in the modifier byte)
|
||||
pub const KEY_LEFT_CTRL: u8 = 0xE0;
|
||||
pub const KEY_LEFT_SHIFT: u8 = 0xE1;
|
||||
pub const KEY_LEFT_ALT: u8 = 0xE2;
|
||||
pub const KEY_LEFT_META: u8 = 0xE3;
|
||||
pub const KEY_RIGHT_CTRL: u8 = 0xE4;
|
||||
pub const KEY_RIGHT_SHIFT: u8 = 0xE5;
|
||||
pub const KEY_RIGHT_ALT: u8 = 0xE6;
|
||||
pub const KEY_RIGHT_META: u8 = 0xE7;
|
||||
}
|
||||
|
||||
/// JavaScript key codes (event.keyCode / event.code)
|
||||
#[allow(dead_code)]
|
||||
pub mod js {
|
||||
// Letters
|
||||
pub const KEY_A: u8 = 65;
|
||||
pub const KEY_B: u8 = 66;
|
||||
pub const KEY_C: u8 = 67;
|
||||
pub const KEY_D: u8 = 68;
|
||||
pub const KEY_E: u8 = 69;
|
||||
pub const KEY_F: u8 = 70;
|
||||
pub const KEY_G: u8 = 71;
|
||||
pub const KEY_H: u8 = 72;
|
||||
pub const KEY_I: u8 = 73;
|
||||
pub const KEY_J: u8 = 74;
|
||||
pub const KEY_K: u8 = 75;
|
||||
pub const KEY_L: u8 = 76;
|
||||
pub const KEY_M: u8 = 77;
|
||||
pub const KEY_N: u8 = 78;
|
||||
pub const KEY_O: u8 = 79;
|
||||
pub const KEY_P: u8 = 80;
|
||||
pub const KEY_Q: u8 = 81;
|
||||
pub const KEY_R: u8 = 82;
|
||||
pub const KEY_S: u8 = 83;
|
||||
pub const KEY_T: u8 = 84;
|
||||
pub const KEY_U: u8 = 85;
|
||||
pub const KEY_V: u8 = 86;
|
||||
pub const KEY_W: u8 = 87;
|
||||
pub const KEY_X: u8 = 88;
|
||||
pub const KEY_Y: u8 = 89;
|
||||
pub const KEY_Z: u8 = 90;
|
||||
|
||||
// Numbers (top row)
|
||||
pub const KEY_0: u8 = 48;
|
||||
pub const KEY_1: u8 = 49;
|
||||
pub const KEY_2: u8 = 50;
|
||||
pub const KEY_3: u8 = 51;
|
||||
pub const KEY_4: u8 = 52;
|
||||
pub const KEY_5: u8 = 53;
|
||||
pub const KEY_6: u8 = 54;
|
||||
pub const KEY_7: u8 = 55;
|
||||
pub const KEY_8: u8 = 56;
|
||||
pub const KEY_9: u8 = 57;
|
||||
|
||||
// Function keys
|
||||
pub const KEY_F1: u8 = 112;
|
||||
pub const KEY_F2: u8 = 113;
|
||||
pub const KEY_F3: u8 = 114;
|
||||
pub const KEY_F4: u8 = 115;
|
||||
pub const KEY_F5: u8 = 116;
|
||||
pub const KEY_F6: u8 = 117;
|
||||
pub const KEY_F7: u8 = 118;
|
||||
pub const KEY_F8: u8 = 119;
|
||||
pub const KEY_F9: u8 = 120;
|
||||
pub const KEY_F10: u8 = 121;
|
||||
pub const KEY_F11: u8 = 122;
|
||||
pub const KEY_F12: u8 = 123;
|
||||
|
||||
// Control keys
|
||||
pub const KEY_BACKSPACE: u8 = 8;
|
||||
pub const KEY_TAB: u8 = 9;
|
||||
pub const KEY_ENTER: u8 = 13;
|
||||
pub const KEY_SHIFT: u8 = 16;
|
||||
pub const KEY_CTRL: u8 = 17;
|
||||
pub const KEY_ALT: u8 = 18;
|
||||
pub const KEY_PAUSE: u8 = 19;
|
||||
pub const KEY_CAPS_LOCK: u8 = 20;
|
||||
pub const KEY_ESCAPE: u8 = 27;
|
||||
pub const KEY_SPACE: u8 = 32;
|
||||
pub const KEY_PAGE_UP: u8 = 33;
|
||||
pub const KEY_PAGE_DOWN: u8 = 34;
|
||||
pub const KEY_END: u8 = 35;
|
||||
pub const KEY_HOME: u8 = 36;
|
||||
pub const KEY_LEFT: u8 = 37;
|
||||
pub const KEY_UP: u8 = 38;
|
||||
pub const KEY_RIGHT: u8 = 39;
|
||||
pub const KEY_DOWN: u8 = 40;
|
||||
pub const KEY_INSERT: u8 = 45;
|
||||
pub const KEY_DELETE: u8 = 46;
|
||||
|
||||
// Punctuation
|
||||
pub const KEY_SEMICOLON: u8 = 186;
|
||||
pub const KEY_EQUAL: u8 = 187;
|
||||
pub const KEY_COMMA: u8 = 188;
|
||||
pub const KEY_MINUS: u8 = 189;
|
||||
pub const KEY_PERIOD: u8 = 190;
|
||||
pub const KEY_SLASH: u8 = 191;
|
||||
pub const KEY_GRAVE: u8 = 192;
|
||||
pub const KEY_LEFT_BRACKET: u8 = 219;
|
||||
pub const KEY_BACKSLASH: u8 = 220;
|
||||
pub const KEY_RIGHT_BRACKET: u8 = 221;
|
||||
pub const KEY_APOSTROPHE: u8 = 222;
|
||||
|
||||
// Numpad
|
||||
pub const KEY_NUMPAD_0: u8 = 96;
|
||||
pub const KEY_NUMPAD_1: u8 = 97;
|
||||
pub const KEY_NUMPAD_2: u8 = 98;
|
||||
pub const KEY_NUMPAD_3: u8 = 99;
|
||||
pub const KEY_NUMPAD_4: u8 = 100;
|
||||
pub const KEY_NUMPAD_5: u8 = 101;
|
||||
pub const KEY_NUMPAD_6: u8 = 102;
|
||||
pub const KEY_NUMPAD_7: u8 = 103;
|
||||
pub const KEY_NUMPAD_8: u8 = 104;
|
||||
pub const KEY_NUMPAD_9: u8 = 105;
|
||||
pub const KEY_NUMPAD_MULTIPLY: u8 = 106;
|
||||
pub const KEY_NUMPAD_ADD: u8 = 107;
|
||||
pub const KEY_NUMPAD_SUBTRACT: u8 = 109;
|
||||
pub const KEY_NUMPAD_DECIMAL: u8 = 110;
|
||||
pub const KEY_NUMPAD_DIVIDE: u8 = 111;
|
||||
|
||||
// Lock keys
|
||||
pub const KEY_NUM_LOCK: u8 = 144;
|
||||
pub const KEY_SCROLL_LOCK: u8 = 145;
|
||||
|
||||
// Windows keys
|
||||
pub const KEY_META_LEFT: u8 = 91;
|
||||
pub const KEY_META_RIGHT: u8 = 92;
|
||||
pub const KEY_CONTEXT_MENU: u8 = 93;
|
||||
}
|
||||
|
||||
/// JavaScript keyCode to USB HID keyCode mapping table
|
||||
/// Using a fixed-size array for O(1) lookup instead of HashMap
|
||||
/// Index = JavaScript keyCode, Value = USB HID keyCode (0 means unmapped)
|
||||
static JS_TO_USB_TABLE: [u8; 256] = {
|
||||
let mut table = [0u8; 256];
|
||||
|
||||
// Letters A-Z (JS 65-90 -> USB 0x04-0x1D)
|
||||
let mut i = 0u8;
|
||||
while i < 26 {
|
||||
table[(65 + i) as usize] = usb::KEY_A + i;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
// Numbers 1-9, 0 (JS 49-57, 48 -> USB 0x1E-0x27)
|
||||
table[49] = usb::KEY_1; // 1
|
||||
table[50] = usb::KEY_2; // 2
|
||||
table[51] = usb::KEY_3; // 3
|
||||
table[52] = usb::KEY_4; // 4
|
||||
table[53] = usb::KEY_5; // 5
|
||||
table[54] = usb::KEY_6; // 6
|
||||
table[55] = usb::KEY_7; // 7
|
||||
table[56] = usb::KEY_8; // 8
|
||||
table[57] = usb::KEY_9; // 9
|
||||
table[48] = usb::KEY_0; // 0
|
||||
|
||||
// Function keys F1-F12 (JS 112-123 -> USB 0x3A-0x45)
|
||||
table[112] = usb::KEY_F1;
|
||||
table[113] = usb::KEY_F2;
|
||||
table[114] = usb::KEY_F3;
|
||||
table[115] = usb::KEY_F4;
|
||||
table[116] = usb::KEY_F5;
|
||||
table[117] = usb::KEY_F6;
|
||||
table[118] = usb::KEY_F7;
|
||||
table[119] = usb::KEY_F8;
|
||||
table[120] = usb::KEY_F9;
|
||||
table[121] = usb::KEY_F10;
|
||||
table[122] = usb::KEY_F11;
|
||||
table[123] = usb::KEY_F12;
|
||||
|
||||
// Control keys
|
||||
table[13] = usb::KEY_ENTER; // Enter
|
||||
table[27] = usb::KEY_ESCAPE; // Escape
|
||||
table[8] = usb::KEY_BACKSPACE; // Backspace
|
||||
table[9] = usb::KEY_TAB; // Tab
|
||||
table[32] = usb::KEY_SPACE; // Space
|
||||
table[20] = usb::KEY_CAPS_LOCK; // Caps Lock
|
||||
|
||||
// Punctuation (JS codes vary by browser/layout)
|
||||
table[189] = usb::KEY_MINUS; // -
|
||||
table[187] = usb::KEY_EQUAL; // =
|
||||
table[219] = usb::KEY_LEFT_BRACKET; // [
|
||||
table[221] = usb::KEY_RIGHT_BRACKET; // ]
|
||||
table[220] = usb::KEY_BACKSLASH; // \
|
||||
table[186] = usb::KEY_SEMICOLON; // ;
|
||||
table[222] = usb::KEY_APOSTROPHE; // '
|
||||
table[192] = usb::KEY_GRAVE; // `
|
||||
table[188] = usb::KEY_COMMA; // ,
|
||||
table[190] = usb::KEY_PERIOD; // .
|
||||
table[191] = usb::KEY_SLASH; // /
|
||||
|
||||
// Navigation keys
|
||||
table[45] = usb::KEY_INSERT;
|
||||
table[46] = usb::KEY_DELETE;
|
||||
table[36] = usb::KEY_HOME;
|
||||
table[35] = usb::KEY_END;
|
||||
table[33] = usb::KEY_PAGE_UP;
|
||||
table[34] = usb::KEY_PAGE_DOWN;
|
||||
|
||||
// Arrow keys
|
||||
table[39] = usb::KEY_RIGHT_ARROW;
|
||||
table[37] = usb::KEY_LEFT_ARROW;
|
||||
table[40] = usb::KEY_DOWN_ARROW;
|
||||
table[38] = usb::KEY_UP_ARROW;
|
||||
|
||||
// Numpad
|
||||
table[144] = usb::KEY_NUM_LOCK;
|
||||
table[111] = usb::KEY_NUMPAD_DIVIDE;
|
||||
table[106] = usb::KEY_NUMPAD_MULTIPLY;
|
||||
table[109] = usb::KEY_NUMPAD_MINUS;
|
||||
table[107] = usb::KEY_NUMPAD_PLUS;
|
||||
table[96] = usb::KEY_NUMPAD_0;
|
||||
table[97] = usb::KEY_NUMPAD_1;
|
||||
table[98] = usb::KEY_NUMPAD_2;
|
||||
table[99] = usb::KEY_NUMPAD_3;
|
||||
table[100] = usb::KEY_NUMPAD_4;
|
||||
table[101] = usb::KEY_NUMPAD_5;
|
||||
table[102] = usb::KEY_NUMPAD_6;
|
||||
table[103] = usb::KEY_NUMPAD_7;
|
||||
table[104] = usb::KEY_NUMPAD_8;
|
||||
table[105] = usb::KEY_NUMPAD_9;
|
||||
table[110] = usb::KEY_NUMPAD_DECIMAL;
|
||||
|
||||
// Special keys
|
||||
table[19] = usb::KEY_PAUSE;
|
||||
table[145] = usb::KEY_SCROLL_LOCK;
|
||||
table[93] = usb::KEY_APPLICATION; // Context menu
|
||||
|
||||
// Modifier keys
|
||||
table[17] = usb::KEY_LEFT_CTRL;
|
||||
table[16] = usb::KEY_LEFT_SHIFT;
|
||||
table[18] = usb::KEY_LEFT_ALT;
|
||||
table[91] = usb::KEY_LEFT_META; // Left Windows/Command
|
||||
table[92] = usb::KEY_RIGHT_META; // Right Windows/Command
|
||||
|
||||
table
|
||||
};
|
||||
|
||||
/// Convert JavaScript keyCode to USB HID keyCode
|
||||
///
|
||||
/// Uses a fixed-size lookup table for O(1) performance.
|
||||
/// Returns None if the key code is not mapped.
|
||||
#[inline]
|
||||
pub fn js_to_usb(js_code: u8) -> Option<u8> {
|
||||
let usb_code = JS_TO_USB_TABLE[js_code as usize];
|
||||
if usb_code != 0 {
|
||||
Some(usb_code)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a key code is a modifier key
|
||||
pub fn is_modifier_key(usb_code: u8) -> bool {
|
||||
(0xE0..=0xE7).contains(&usb_code)
|
||||
}
|
||||
|
||||
/// Get modifier bit for a modifier key
|
||||
pub fn modifier_bit(usb_code: u8) -> Option<u8> {
|
||||
match usb_code {
|
||||
usb::KEY_LEFT_CTRL => Some(0x01),
|
||||
usb::KEY_LEFT_SHIFT => Some(0x02),
|
||||
usb::KEY_LEFT_ALT => Some(0x04),
|
||||
usb::KEY_LEFT_META => Some(0x08),
|
||||
usb::KEY_RIGHT_CTRL => Some(0x10),
|
||||
usb::KEY_RIGHT_SHIFT => Some(0x20),
|
||||
usb::KEY_RIGHT_ALT => Some(0x40),
|
||||
usb::KEY_RIGHT_META => Some(0x80),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_letter_mapping() {
|
||||
assert_eq!(js_to_usb(65), Some(usb::KEY_A)); // A
|
||||
assert_eq!(js_to_usb(90), Some(usb::KEY_Z)); // Z
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_number_mapping() {
|
||||
assert_eq!(js_to_usb(48), Some(usb::KEY_0));
|
||||
assert_eq!(js_to_usb(49), Some(usb::KEY_1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modifier_key() {
|
||||
assert!(is_modifier_key(usb::KEY_LEFT_CTRL));
|
||||
assert!(is_modifier_key(usb::KEY_RIGHT_SHIFT));
|
||||
assert!(!is_modifier_key(usb::KEY_A));
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,13 @@ pub mod backend;
|
||||
pub mod ch9329;
|
||||
pub mod consumer;
|
||||
pub mod datachannel;
|
||||
pub mod keymap;
|
||||
pub mod keyboard;
|
||||
pub mod otg;
|
||||
pub mod types;
|
||||
pub mod websocket;
|
||||
|
||||
pub use backend::{HidBackend, HidBackendStatus, HidBackendType};
|
||||
pub use keyboard::CanonicalKey;
|
||||
pub use otg::LedState;
|
||||
pub use types::{
|
||||
ConsumerEvent, KeyEventType, KeyboardEvent, KeyboardModifiers, MouseButton, MouseEvent,
|
||||
|
||||
@@ -29,7 +29,6 @@ use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
|
||||
use tracing::{debug, info, trace, warn};
|
||||
|
||||
use super::backend::{HidBackend, HidBackendStatus};
|
||||
use super::keymap;
|
||||
use super::types::{
|
||||
ConsumerEvent, KeyEventType, KeyboardEvent, KeyboardReport, MouseEvent, MouseEventType,
|
||||
};
|
||||
@@ -873,18 +872,13 @@ impl HidBackend for OtgBackend {
|
||||
}
|
||||
|
||||
async fn send_keyboard(&self, event: KeyboardEvent) -> Result<()> {
|
||||
// Convert JS keycode to USB HID if needed (skip if already USB HID)
|
||||
let usb_key = if event.is_usb_hid {
|
||||
event.key
|
||||
} else {
|
||||
keymap::js_to_usb(event.key).unwrap_or(event.key)
|
||||
};
|
||||
let usb_key = event.key.to_hid_usage();
|
||||
|
||||
// Handle modifier keys separately
|
||||
if keymap::is_modifier_key(usb_key) {
|
||||
if event.key.is_modifier() {
|
||||
let mut state = self.keyboard_state.lock();
|
||||
|
||||
if let Some(bit) = keymap::modifier_bit(usb_key) {
|
||||
if let Some(bit) = event.key.modifier_bit() {
|
||||
match event.event_type {
|
||||
KeyEventType::Down => state.modifiers |= bit,
|
||||
KeyEventType::Up => state.modifiers &= !bit,
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::keyboard::CanonicalKey;
|
||||
|
||||
/// Keyboard event type
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
@@ -105,34 +107,29 @@ pub struct KeyboardEvent {
|
||||
/// Event type (down/up)
|
||||
#[serde(rename = "type")]
|
||||
pub event_type: KeyEventType,
|
||||
/// Key code (USB HID usage code or JavaScript key code)
|
||||
pub key: u8,
|
||||
/// Canonical keyboard key identifier shared across frontend and backend
|
||||
pub key: CanonicalKey,
|
||||
/// Modifier keys state
|
||||
#[serde(default)]
|
||||
pub modifiers: KeyboardModifiers,
|
||||
/// If true, key is already USB HID code (skip js_to_usb conversion)
|
||||
#[serde(default)]
|
||||
pub is_usb_hid: bool,
|
||||
}
|
||||
|
||||
impl KeyboardEvent {
|
||||
/// Create a key down event (JS keycode, needs conversion)
|
||||
pub fn key_down(key: u8, modifiers: KeyboardModifiers) -> Self {
|
||||
/// Create a key down event
|
||||
pub fn key_down(key: CanonicalKey, modifiers: KeyboardModifiers) -> Self {
|
||||
Self {
|
||||
event_type: KeyEventType::Down,
|
||||
key,
|
||||
modifiers,
|
||||
is_usb_hid: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a key up event (JS keycode, needs conversion)
|
||||
pub fn key_up(key: u8, modifiers: KeyboardModifiers) -> Self {
|
||||
/// Create a key up event
|
||||
pub fn key_up(key: CanonicalKey, modifiers: KeyboardModifiers) -> Self {
|
||||
Self {
|
||||
event_type: KeyEventType::Up,
|
||||
key,
|
||||
modifiers,
|
||||
is_usb_hid: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ use tokio::sync::{broadcast, mpsc, Mutex};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use crate::audio::AudioController;
|
||||
use crate::hid::{HidController, KeyEventType, KeyboardEvent, KeyboardModifiers};
|
||||
use crate::hid::{CanonicalKey, HidController, KeyEventType, KeyboardEvent, KeyboardModifiers};
|
||||
use crate::video::codec_constraints::{
|
||||
encoder_codec_to_id, encoder_codec_to_video_codec, video_codec_to_encoder_codec,
|
||||
};
|
||||
@@ -1328,15 +1328,13 @@ impl Connection {
|
||||
);
|
||||
let caps_down = KeyboardEvent {
|
||||
event_type: KeyEventType::Down,
|
||||
key: 0x39, // USB HID CapsLock
|
||||
key: CanonicalKey::CapsLock,
|
||||
modifiers: KeyboardModifiers::default(),
|
||||
is_usb_hid: true,
|
||||
};
|
||||
let caps_up = KeyboardEvent {
|
||||
event_type: KeyEventType::Up,
|
||||
key: 0x39,
|
||||
key: CanonicalKey::CapsLock,
|
||||
modifiers: KeyboardModifiers::default(),
|
||||
is_usb_hid: true,
|
||||
};
|
||||
if let Err(e) = hid.send_keyboard(caps_down).await {
|
||||
warn!("Failed to send CapsLock down: {}", e);
|
||||
@@ -1351,7 +1349,7 @@ impl Connection {
|
||||
if let Some(kb_event) = convert_key_event(ke) {
|
||||
debug!(
|
||||
"Converted to HID: key=0x{:02X}, event_type={:?}, modifiers={:02X}",
|
||||
kb_event.key,
|
||||
kb_event.key.to_hid_usage(),
|
||||
kb_event.event_type,
|
||||
kb_event.modifiers.to_hid_byte()
|
||||
);
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
use super::protocol::hbb::message::key_event as ke_union;
|
||||
use super::protocol::{ControlKey, KeyEvent, MouseEvent};
|
||||
use crate::hid::{
|
||||
KeyEventType, KeyboardEvent, KeyboardModifiers, MouseButton, MouseEvent as OneKvmMouseEvent,
|
||||
MouseEventType,
|
||||
CanonicalKey, KeyEventType, KeyboardEvent, KeyboardModifiers, MouseButton,
|
||||
MouseEvent as OneKvmMouseEvent, MouseEventType,
|
||||
};
|
||||
use protobuf::Enum;
|
||||
|
||||
@@ -217,11 +217,11 @@ pub fn convert_key_event(event: &KeyEvent) -> Option<KeyboardEvent> {
|
||||
// Handle control keys
|
||||
if let Some(ke_union::Union::ControlKey(ck)) = &event.union {
|
||||
if let Some(key) = control_key_to_hid(ck.value()) {
|
||||
let key = CanonicalKey::from_hid_usage(key)?;
|
||||
return Some(KeyboardEvent {
|
||||
event_type,
|
||||
key,
|
||||
modifiers,
|
||||
is_usb_hid: true, // Already converted to USB HID code
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -230,11 +230,11 @@ pub fn convert_key_event(event: &KeyEvent) -> Option<KeyboardEvent> {
|
||||
if let Some(ke_union::Union::Chr(chr)) = &event.union {
|
||||
// chr contains USB HID scancode on Windows, X11 keycode on Linux
|
||||
if let Some(key) = keycode_to_hid(*chr) {
|
||||
let key = CanonicalKey::from_hid_usage(key)?;
|
||||
return Some(KeyboardEvent {
|
||||
event_type,
|
||||
key,
|
||||
modifiers,
|
||||
is_usb_hid: true, // Already converted to USB HID code
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -608,6 +608,6 @@ mod tests {
|
||||
|
||||
let kb_event = result.unwrap();
|
||||
assert_eq!(kb_event.event_type, KeyEventType::Down);
|
||||
assert_eq!(kb_event.key, 0x28); // Return key USB HID code
|
||||
assert_eq!(kb_event.key, CanonicalKey::Enter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// API client for One-KVM backend
|
||||
|
||||
import { request, ApiError } from './request'
|
||||
import type { CanonicalKey } from '@/types/generated'
|
||||
|
||||
const API_BASE = '/api'
|
||||
|
||||
@@ -357,7 +358,7 @@ export const hidApi = {
|
||||
}>
|
||||
}>('/hid/otg/self-check'),
|
||||
|
||||
keyboard: async (type: 'down' | 'up', key: number, modifier?: number) => {
|
||||
keyboard: async (type: 'down' | 'up', key: CanonicalKey, modifier?: number) => {
|
||||
await ensureHidConnection()
|
||||
const event: HidKeyboardEvent = {
|
||||
type: type === 'down' ? 'keydown' : 'keyup',
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { CanonicalKey } from '@/types/generated'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
pressedKeys?: string[]
|
||||
pressedKeys?: CanonicalKey[]
|
||||
capsLock?: boolean
|
||||
mousePosition?: { x: number; y: number }
|
||||
debugMode?: boolean
|
||||
@@ -18,13 +19,14 @@ const keyNameMap: Record<string, string> = {
|
||||
MetaLeft: 'Win', MetaRight: 'Win',
|
||||
ControlLeft: 'Ctrl', ControlRight: 'Ctrl',
|
||||
ShiftLeft: 'Shift', ShiftRight: 'Shift',
|
||||
AltLeft: 'Alt', AltRight: 'Alt',
|
||||
AltLeft: 'Alt', AltRight: 'AltGr',
|
||||
CapsLock: 'Caps', NumLock: 'Num', ScrollLock: 'Scroll',
|
||||
Backspace: 'Back', Delete: 'Del',
|
||||
ArrowUp: '↑', ArrowDown: '↓', ArrowLeft: '←', ArrowRight: '→',
|
||||
Escape: 'Esc', Enter: 'Enter', Tab: 'Tab', Space: 'Space',
|
||||
PageUp: 'PgUp', PageDown: 'PgDn',
|
||||
Insert: 'Ins', Home: 'Home', End: 'End',
|
||||
ContextMenu: 'Menu',
|
||||
}
|
||||
|
||||
const keysDisplay = computed(() => {
|
||||
|
||||
@@ -69,24 +69,24 @@ async function typeChar(char: string, signal: AbortSignal): Promise<boolean> {
|
||||
return true
|
||||
}
|
||||
|
||||
const { hidCode, shift } = mapping
|
||||
const { key, shift } = mapping
|
||||
const modifier = shift ? 0x02 : 0
|
||||
|
||||
try {
|
||||
// Send keydown
|
||||
await hidApi.keyboard('down', hidCode, modifier)
|
||||
await hidApi.keyboard('down', key, modifier)
|
||||
|
||||
// Small delay between down and up to ensure key is registered
|
||||
await sleep(5)
|
||||
|
||||
if (signal.aborted) {
|
||||
// Even if aborted, still send keyup to release the key
|
||||
await hidApi.keyboard('up', hidCode, modifier)
|
||||
await hidApi.keyboard('up', key, modifier)
|
||||
return false
|
||||
}
|
||||
|
||||
// Send keyup
|
||||
await hidApi.keyboard('up', hidCode, modifier)
|
||||
await hidApi.keyboard('up', key, modifier)
|
||||
|
||||
// Additional small delay after keyup to ensure it's processed
|
||||
await sleep(2)
|
||||
@@ -96,7 +96,7 @@ async function typeChar(char: string, signal: AbortSignal): Promise<boolean> {
|
||||
console.error('[Paste] Failed to type character:', char, error)
|
||||
// Try to release the key even on error
|
||||
try {
|
||||
await hidApi.keyboard('up', hidCode, modifier)
|
||||
await hidApi.keyboard('up', key, modifier)
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
|
||||
@@ -4,12 +4,13 @@ import { useI18n } from 'vue-i18n'
|
||||
import Keyboard from 'simple-keyboard'
|
||||
import 'simple-keyboard/build/css/index.css'
|
||||
import { hidApi } from '@/api'
|
||||
import { CanonicalKey } from '@/types/generated'
|
||||
import {
|
||||
keys,
|
||||
consumerKeys,
|
||||
latchingKeys,
|
||||
modifiers,
|
||||
updateModifierMaskForHidKey,
|
||||
updateModifierMaskForKey,
|
||||
type KeyName,
|
||||
type ConsumerKeyName,
|
||||
} from '@/lib/keyboardMappings'
|
||||
@@ -23,13 +24,15 @@ import {
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
attached?: boolean
|
||||
capsLock?: boolean
|
||||
pressedKeys?: CanonicalKey[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'update:attached', value: boolean): void
|
||||
(e: 'keyDown', key: string): void
|
||||
(e: 'keyUp', key: string): void
|
||||
(e: 'keyDown', key: CanonicalKey): void
|
||||
(e: 'keyUp', key: CanonicalKey): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -45,13 +48,17 @@ const arrowsKeyboard = ref<Keyboard | null>(null)
|
||||
|
||||
// Pressed keys tracking
|
||||
const pressedModifiers = ref<number>(0)
|
||||
const keysDown = ref<string[]>([])
|
||||
const keysDown = ref<CanonicalKey[]>([])
|
||||
|
||||
// Shift state for display
|
||||
const isShiftActive = computed(() => {
|
||||
return (pressedModifiers.value & 0x22) !== 0
|
||||
})
|
||||
|
||||
const areLettersUppercase = computed(() => {
|
||||
return Boolean(props.capsLock) !== isShiftActive.value
|
||||
})
|
||||
|
||||
const layoutName = computed(() => {
|
||||
return isShiftActive.value ? 'shift' : 'default'
|
||||
})
|
||||
@@ -63,7 +70,12 @@ const keyNamesForDownKeys = computed(() => {
|
||||
.filter(([_, mask]) => (activeModifierMask & mask) !== 0)
|
||||
.map(([name]) => name)
|
||||
|
||||
return [...modifierNames, ...keysDown.value, ' ']
|
||||
return Array.from(new Set([
|
||||
...modifierNames,
|
||||
...(props.pressedKeys ?? []),
|
||||
...keysDown.value,
|
||||
...(props.capsLock ? ['CapsLock'] : []),
|
||||
]))
|
||||
})
|
||||
|
||||
// Dragging state (for floating mode)
|
||||
@@ -88,7 +100,7 @@ const keyboardLayout = {
|
||||
'Tab KeyQ KeyW KeyE KeyR KeyT KeyY KeyU KeyI KeyO KeyP BracketLeft BracketRight Backslash',
|
||||
'CapsLock KeyA KeyS KeyD KeyF KeyG KeyH KeyJ KeyK KeyL Semicolon Quote Enter',
|
||||
'ShiftLeft KeyZ KeyX KeyC KeyV KeyB KeyN KeyM Comma Period Slash ShiftRight',
|
||||
'ControlLeft MetaLeft AltLeft Space AltGr MetaRight Menu ControlRight',
|
||||
'ControlLeft MetaLeft AltLeft Space AltRight MetaRight ContextMenu ControlRight',
|
||||
],
|
||||
shift: [
|
||||
'CtrlAltDelete AltMetaEscape CtrlAltBackspace',
|
||||
@@ -97,7 +109,7 @@ const keyboardLayout = {
|
||||
'Tab (KeyQ) (KeyW) (KeyE) (KeyR) (KeyT) (KeyY) (KeyU) (KeyI) (KeyO) (KeyP) (BracketLeft) (BracketRight) (Backslash)',
|
||||
'CapsLock (KeyA) (KeyS) (KeyD) (KeyF) (KeyG) (KeyH) (KeyJ) (KeyK) (KeyL) (Semicolon) (Quote) Enter',
|
||||
'ShiftLeft (KeyZ) (KeyX) (KeyC) (KeyV) (KeyB) (KeyN) (KeyM) (Comma) (Period) (Slash) ShiftRight',
|
||||
'ControlLeft MetaLeft AltLeft Space AltGr MetaRight Menu ControlRight',
|
||||
'ControlLeft MetaLeft AltLeft Space AltRight MetaRight ContextMenu ControlRight',
|
||||
],
|
||||
},
|
||||
control: {
|
||||
@@ -148,11 +160,10 @@ const keyDisplayMap = computed<Record<string, string>>(() => {
|
||||
ShiftLeft: 'Shift',
|
||||
ShiftRight: 'Shift',
|
||||
AltLeft: 'Alt',
|
||||
AltRight: 'Alt',
|
||||
AltGr: 'AltGr',
|
||||
AltRight: 'AltGr',
|
||||
MetaLeft: metaLabel,
|
||||
MetaRight: metaLabel,
|
||||
Menu: 'Menu',
|
||||
ContextMenu: 'Menu',
|
||||
|
||||
// Special keys
|
||||
Escape: 'Esc',
|
||||
@@ -187,20 +198,60 @@ const keyDisplayMap = computed<Record<string, string>>(() => {
|
||||
F9: 'F9', F10: 'F10', F11: 'F11', F12: 'F12',
|
||||
|
||||
// Letters
|
||||
KeyA: 'a', KeyB: 'b', KeyC: 'c', KeyD: 'd', KeyE: 'e',
|
||||
KeyF: 'f', KeyG: 'g', KeyH: 'h', KeyI: 'i', KeyJ: 'j',
|
||||
KeyK: 'k', KeyL: 'l', KeyM: 'm', KeyN: 'n', KeyO: 'o',
|
||||
KeyP: 'p', KeyQ: 'q', KeyR: 'r', KeyS: 's', KeyT: 't',
|
||||
KeyU: 'u', KeyV: 'v', KeyW: 'w', KeyX: 'x', KeyY: 'y',
|
||||
KeyZ: 'z',
|
||||
KeyA: areLettersUppercase.value ? 'A' : 'a',
|
||||
KeyB: areLettersUppercase.value ? 'B' : 'b',
|
||||
KeyC: areLettersUppercase.value ? 'C' : 'c',
|
||||
KeyD: areLettersUppercase.value ? 'D' : 'd',
|
||||
KeyE: areLettersUppercase.value ? 'E' : 'e',
|
||||
KeyF: areLettersUppercase.value ? 'F' : 'f',
|
||||
KeyG: areLettersUppercase.value ? 'G' : 'g',
|
||||
KeyH: areLettersUppercase.value ? 'H' : 'h',
|
||||
KeyI: areLettersUppercase.value ? 'I' : 'i',
|
||||
KeyJ: areLettersUppercase.value ? 'J' : 'j',
|
||||
KeyK: areLettersUppercase.value ? 'K' : 'k',
|
||||
KeyL: areLettersUppercase.value ? 'L' : 'l',
|
||||
KeyM: areLettersUppercase.value ? 'M' : 'm',
|
||||
KeyN: areLettersUppercase.value ? 'N' : 'n',
|
||||
KeyO: areLettersUppercase.value ? 'O' : 'o',
|
||||
KeyP: areLettersUppercase.value ? 'P' : 'p',
|
||||
KeyQ: areLettersUppercase.value ? 'Q' : 'q',
|
||||
KeyR: areLettersUppercase.value ? 'R' : 'r',
|
||||
KeyS: areLettersUppercase.value ? 'S' : 's',
|
||||
KeyT: areLettersUppercase.value ? 'T' : 't',
|
||||
KeyU: areLettersUppercase.value ? 'U' : 'u',
|
||||
KeyV: areLettersUppercase.value ? 'V' : 'v',
|
||||
KeyW: areLettersUppercase.value ? 'W' : 'w',
|
||||
KeyX: areLettersUppercase.value ? 'X' : 'x',
|
||||
KeyY: areLettersUppercase.value ? 'Y' : 'y',
|
||||
KeyZ: areLettersUppercase.value ? 'Z' : 'z',
|
||||
|
||||
// Capital letters
|
||||
'(KeyA)': 'A', '(KeyB)': 'B', '(KeyC)': 'C', '(KeyD)': 'D', '(KeyE)': 'E',
|
||||
'(KeyF)': 'F', '(KeyG)': 'G', '(KeyH)': 'H', '(KeyI)': 'I', '(KeyJ)': 'J',
|
||||
'(KeyK)': 'K', '(KeyL)': 'L', '(KeyM)': 'M', '(KeyN)': 'N', '(KeyO)': 'O',
|
||||
'(KeyP)': 'P', '(KeyQ)': 'Q', '(KeyR)': 'R', '(KeyS)': 'S', '(KeyT)': 'T',
|
||||
'(KeyU)': 'U', '(KeyV)': 'V', '(KeyW)': 'W', '(KeyX)': 'X', '(KeyY)': 'Y',
|
||||
'(KeyZ)': 'Z',
|
||||
// Letter labels in the shifted layout follow CapsLock xor Shift too
|
||||
'(KeyA)': areLettersUppercase.value ? 'A' : 'a',
|
||||
'(KeyB)': areLettersUppercase.value ? 'B' : 'b',
|
||||
'(KeyC)': areLettersUppercase.value ? 'C' : 'c',
|
||||
'(KeyD)': areLettersUppercase.value ? 'D' : 'd',
|
||||
'(KeyE)': areLettersUppercase.value ? 'E' : 'e',
|
||||
'(KeyF)': areLettersUppercase.value ? 'F' : 'f',
|
||||
'(KeyG)': areLettersUppercase.value ? 'G' : 'g',
|
||||
'(KeyH)': areLettersUppercase.value ? 'H' : 'h',
|
||||
'(KeyI)': areLettersUppercase.value ? 'I' : 'i',
|
||||
'(KeyJ)': areLettersUppercase.value ? 'J' : 'j',
|
||||
'(KeyK)': areLettersUppercase.value ? 'K' : 'k',
|
||||
'(KeyL)': areLettersUppercase.value ? 'L' : 'l',
|
||||
'(KeyM)': areLettersUppercase.value ? 'M' : 'm',
|
||||
'(KeyN)': areLettersUppercase.value ? 'N' : 'n',
|
||||
'(KeyO)': areLettersUppercase.value ? 'O' : 'o',
|
||||
'(KeyP)': areLettersUppercase.value ? 'P' : 'p',
|
||||
'(KeyQ)': areLettersUppercase.value ? 'Q' : 'q',
|
||||
'(KeyR)': areLettersUppercase.value ? 'R' : 'r',
|
||||
'(KeyS)': areLettersUppercase.value ? 'S' : 's',
|
||||
'(KeyT)': areLettersUppercase.value ? 'T' : 't',
|
||||
'(KeyU)': areLettersUppercase.value ? 'U' : 'u',
|
||||
'(KeyV)': areLettersUppercase.value ? 'V' : 'v',
|
||||
'(KeyW)': areLettersUppercase.value ? 'W' : 'w',
|
||||
'(KeyX)': areLettersUppercase.value ? 'X' : 'x',
|
||||
'(KeyY)': areLettersUppercase.value ? 'Y' : 'y',
|
||||
'(KeyZ)': areLettersUppercase.value ? 'Z' : 'z',
|
||||
|
||||
// Numbers
|
||||
Digit1: '1', Digit2: '2', Digit3: '3', Digit4: '4', Digit5: '5',
|
||||
@@ -303,47 +354,47 @@ async function onKeyDown(key: string) {
|
||||
const keyCode = keys[cleanKey as KeyName]
|
||||
|
||||
// Handle latching keys (Caps Lock, etc.)
|
||||
if ((latchingKeys as readonly string[]).includes(cleanKey)) {
|
||||
emit('keyDown', cleanKey)
|
||||
if (latchingKeys.some(latchingKey => latchingKey === keyCode)) {
|
||||
emit('keyDown', keyCode)
|
||||
const currentMask = pressedModifiers.value & 0xff
|
||||
await sendKeyPress(keyCode, true, currentMask)
|
||||
setTimeout(() => {
|
||||
sendKeyPress(keyCode, false, currentMask)
|
||||
emit('keyUp', cleanKey)
|
||||
emit('keyUp', keyCode)
|
||||
}, 100)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle modifier keys (toggle)
|
||||
if (cleanKey in modifiers) {
|
||||
const mask = modifiers[cleanKey as keyof typeof modifiers]
|
||||
const mask = modifiers[keyCode] ?? 0
|
||||
if (mask !== 0) {
|
||||
const isCurrentlyDown = (pressedModifiers.value & mask) !== 0
|
||||
|
||||
if (isCurrentlyDown) {
|
||||
const nextMask = pressedModifiers.value & ~mask
|
||||
pressedModifiers.value = nextMask
|
||||
await sendKeyPress(keyCode, false, nextMask)
|
||||
emit('keyUp', cleanKey)
|
||||
emit('keyUp', keyCode)
|
||||
} else {
|
||||
const nextMask = pressedModifiers.value | mask
|
||||
pressedModifiers.value = nextMask
|
||||
await sendKeyPress(keyCode, true, nextMask)
|
||||
emit('keyDown', cleanKey)
|
||||
emit('keyDown', keyCode)
|
||||
}
|
||||
updateKeyboardButtonTheme()
|
||||
return
|
||||
}
|
||||
|
||||
// Regular key: press and release
|
||||
keysDown.value.push(cleanKey)
|
||||
emit('keyDown', cleanKey)
|
||||
keysDown.value.push(keyCode)
|
||||
emit('keyDown', keyCode)
|
||||
const currentMask = pressedModifiers.value & 0xff
|
||||
await sendKeyPress(keyCode, true, currentMask)
|
||||
updateKeyboardButtonTheme()
|
||||
setTimeout(async () => {
|
||||
keysDown.value = keysDown.value.filter(k => k !== cleanKey)
|
||||
keysDown.value = keysDown.value.filter(k => k !== keyCode)
|
||||
await sendKeyPress(keyCode, false, currentMask)
|
||||
emit('keyUp', cleanKey)
|
||||
emit('keyUp', keyCode)
|
||||
updateKeyboardButtonTheme()
|
||||
}, 50)
|
||||
}
|
||||
@@ -352,7 +403,7 @@ async function onKeyUp() {
|
||||
// Not used for now - we handle up in onKeyDown with setTimeout
|
||||
}
|
||||
|
||||
async function sendKeyPress(keyCode: number, press: boolean, modifierMask: number) {
|
||||
async function sendKeyPress(keyCode: CanonicalKey, press: boolean, modifierMask: number) {
|
||||
try {
|
||||
await hidApi.keyboard(press ? 'down' : 'up', keyCode, modifierMask & 0xff)
|
||||
} catch (err) {
|
||||
@@ -372,7 +423,7 @@ async function executeMacro(steps: MacroStep[]) {
|
||||
for (const mod of step.modifiers) {
|
||||
if (mod in keys) {
|
||||
const modHid = keys[mod as KeyName]
|
||||
macroModifierMask = updateModifierMaskForHidKey(macroModifierMask, modHid, true)
|
||||
macroModifierMask = updateModifierMaskForKey(macroModifierMask, modHid, true)
|
||||
await sendKeyPress(modHid, true, macroModifierMask)
|
||||
}
|
||||
}
|
||||
@@ -394,7 +445,7 @@ async function executeMacro(steps: MacroStep[]) {
|
||||
for (const mod of step.modifiers) {
|
||||
if (mod in keys) {
|
||||
const modHid = keys[mod as KeyName]
|
||||
macroModifierMask = updateModifierMaskForHidKey(macroModifierMask, modHid, false)
|
||||
macroModifierMask = updateModifierMaskForKey(macroModifierMask, modHid, false)
|
||||
await sendKeyPress(modHid, false, macroModifierMask)
|
||||
}
|
||||
}
|
||||
@@ -421,8 +472,12 @@ function updateKeyboardButtonTheme() {
|
||||
}
|
||||
|
||||
// Update layout when shift state changes
|
||||
watch(layoutName, (name) => {
|
||||
mainKeyboard.value?.setOptions({ layoutName: name })
|
||||
watch([layoutName, () => props.capsLock], ([name]) => {
|
||||
mainKeyboard.value?.setOptions({
|
||||
layoutName: name,
|
||||
display: keyDisplayMap.value,
|
||||
})
|
||||
updateKeyboardButtonTheme()
|
||||
})
|
||||
|
||||
// Initialize keyboards with unique selectors
|
||||
@@ -835,12 +890,12 @@ onUnmounted(() => {
|
||||
min-width: 55px;
|
||||
}
|
||||
|
||||
.vkb .simple-keyboard .hg-button[data-skbtn="AltGr"] {
|
||||
.vkb .simple-keyboard .hg-button[data-skbtn="AltRight"] {
|
||||
flex-grow: 1.25;
|
||||
min-width: 55px;
|
||||
}
|
||||
|
||||
.vkb .simple-keyboard .hg-button[data-skbtn="Menu"] {
|
||||
.vkb .simple-keyboard .hg-button[data-skbtn="ContextMenu"] {
|
||||
flex-grow: 1.25;
|
||||
min-width: 55px;
|
||||
}
|
||||
@@ -1194,8 +1249,8 @@ html.dark .hg-theme-default .hg-button.down-key,
|
||||
.vkb .simple-keyboard .hg-button[data-skbtn="MetaLeft"],
|
||||
.vkb .simple-keyboard .hg-button[data-skbtn="MetaRight"],
|
||||
.vkb .simple-keyboard .hg-button[data-skbtn="AltLeft"],
|
||||
.vkb .simple-keyboard .hg-button[data-skbtn="AltGr"],
|
||||
.vkb .simple-keyboard .hg-button[data-skbtn="Menu"] {
|
||||
.vkb .simple-keyboard .hg-button[data-skbtn="AltRight"],
|
||||
.vkb .simple-keyboard .hg-button[data-skbtn="ContextMenu"] {
|
||||
min-width: 46px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
// HID input composable - manages keyboard and mouse input
|
||||
// Extracted from ConsoleView.vue for better separation of concerns
|
||||
|
||||
import { ref, type Ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { hidApi } from '@/api'
|
||||
import { keyboardEventToHidCode, updateModifierMaskForHidKey } from '@/lib/keyboardMappings'
|
||||
|
||||
export interface HidInputState {
|
||||
mouseMode: Ref<'absolute' | 'relative'>
|
||||
pressedKeys: Ref<string[]>
|
||||
keyboardLed: Ref<{ capsLock: boolean; numLock: boolean; scrollLock: boolean }>
|
||||
mousePosition: Ref<{ x: number; y: number }>
|
||||
isPointerLocked: Ref<boolean>
|
||||
cursorVisible: Ref<boolean>
|
||||
}
|
||||
|
||||
export interface UseHidInputOptions {
|
||||
videoContainerRef: Ref<HTMLDivElement | null>
|
||||
getVideoElement: () => HTMLElement | null
|
||||
isFullscreen: Ref<boolean>
|
||||
}
|
||||
|
||||
export function useHidInput(options: UseHidInputOptions) {
|
||||
const { t } = useI18n()
|
||||
|
||||
// State
|
||||
const mouseMode = ref<'absolute' | 'relative'>('absolute')
|
||||
const pressedKeys = ref<string[]>([])
|
||||
const keyboardLed = ref({
|
||||
capsLock: false,
|
||||
numLock: false,
|
||||
scrollLock: false,
|
||||
})
|
||||
const activeModifierMask = ref(0)
|
||||
const mousePosition = ref({ x: 0, y: 0 })
|
||||
const lastMousePosition = ref({ x: 0, y: 0 })
|
||||
const isPointerLocked = ref(false)
|
||||
const cursorVisible = ref(localStorage.getItem('hidShowCursor') !== 'false')
|
||||
const pressedMouseButton = ref<'left' | 'right' | 'middle' | null>(null)
|
||||
|
||||
// Error handling - silently handle all HID errors
|
||||
function handleHidError(_error: unknown, _operation: string) {
|
||||
// All HID errors are silently ignored
|
||||
}
|
||||
|
||||
// Check if a key should be blocked
|
||||
function shouldBlockKey(e: KeyboardEvent): boolean {
|
||||
if (options.isFullscreen.value) return true
|
||||
|
||||
const key = e.key.toUpperCase()
|
||||
if (e.ctrlKey && ['W', 'T', 'N'].includes(key)) return false
|
||||
if (key === 'F11') return false
|
||||
if (e.altKey && key === 'TAB') return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Keyboard handlers
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
const container = options.videoContainerRef.value
|
||||
if (!container) return
|
||||
|
||||
if (!options.isFullscreen.value && !container.contains(document.activeElement)) return
|
||||
|
||||
if (shouldBlockKey(e)) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
if (!options.isFullscreen.value && (e.metaKey || e.key === 'Meta')) {
|
||||
toast.info(t('console.metaKeyHint'), {
|
||||
description: t('console.metaKeyHintDesc'),
|
||||
duration: 3000,
|
||||
})
|
||||
}
|
||||
|
||||
const keyName = e.key === ' ' ? 'Space' : e.key
|
||||
if (!pressedKeys.value.includes(keyName)) {
|
||||
pressedKeys.value = [...pressedKeys.value, keyName]
|
||||
}
|
||||
|
||||
keyboardLed.value.capsLock = e.getModifierState('CapsLock')
|
||||
keyboardLed.value.numLock = e.getModifierState('NumLock')
|
||||
keyboardLed.value.scrollLock = e.getModifierState('ScrollLock')
|
||||
|
||||
const hidKey = keyboardEventToHidCode(e.code, e.key)
|
||||
if (hidKey === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const modifierMask = updateModifierMaskForHidKey(activeModifierMask.value, hidKey, true)
|
||||
activeModifierMask.value = modifierMask
|
||||
hidApi.keyboard('down', hidKey, modifierMask).catch(err => handleHidError(err, 'keyboard down'))
|
||||
}
|
||||
|
||||
function handleKeyUp(e: KeyboardEvent) {
|
||||
const container = options.videoContainerRef.value
|
||||
if (!container) return
|
||||
|
||||
if (!options.isFullscreen.value && !container.contains(document.activeElement)) return
|
||||
|
||||
if (shouldBlockKey(e)) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
const keyName = e.key === ' ' ? 'Space' : e.key
|
||||
pressedKeys.value = pressedKeys.value.filter(k => k !== keyName)
|
||||
|
||||
const hidKey = keyboardEventToHidCode(e.code, e.key)
|
||||
if (hidKey === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const modifierMask = updateModifierMaskForHidKey(activeModifierMask.value, hidKey, false)
|
||||
activeModifierMask.value = modifierMask
|
||||
hidApi.keyboard('up', hidKey, modifierMask).catch(err => handleHidError(err, 'keyboard up'))
|
||||
}
|
||||
|
||||
// Mouse handlers
|
||||
function handleMouseMove(e: MouseEvent) {
|
||||
const videoElement = options.getVideoElement()
|
||||
if (!videoElement) return
|
||||
|
||||
if (mouseMode.value === 'absolute') {
|
||||
const rect = videoElement.getBoundingClientRect()
|
||||
const x = Math.round((e.clientX - rect.left) / rect.width * 32767)
|
||||
const y = Math.round((e.clientY - rect.top) / rect.height * 32767)
|
||||
|
||||
mousePosition.value = { x, y }
|
||||
hidApi.mouse({ type: 'move_abs', x, y }).catch(err => handleHidError(err, 'mouse move'))
|
||||
} else {
|
||||
if (isPointerLocked.value) {
|
||||
const dx = e.movementX
|
||||
const dy = e.movementY
|
||||
|
||||
if (dx !== 0 || dy !== 0) {
|
||||
const clampedDx = Math.max(-127, Math.min(127, dx))
|
||||
const clampedDy = Math.max(-127, Math.min(127, dy))
|
||||
hidApi.mouse({ type: 'move', x: clampedDx, y: clampedDy }).catch(err => handleHidError(err, 'mouse move'))
|
||||
}
|
||||
|
||||
mousePosition.value = {
|
||||
x: mousePosition.value.x + dx,
|
||||
y: mousePosition.value.y + dy,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseDown(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
|
||||
const container = options.videoContainerRef.value
|
||||
if (container && document.activeElement !== container) {
|
||||
if (typeof container.focus === 'function') {
|
||||
container.focus()
|
||||
}
|
||||
}
|
||||
|
||||
if (mouseMode.value === 'relative' && !isPointerLocked.value) {
|
||||
requestPointerLock()
|
||||
return
|
||||
}
|
||||
|
||||
const button = e.button === 0 ? 'left' : e.button === 2 ? 'right' : 'middle'
|
||||
pressedMouseButton.value = button
|
||||
hidApi.mouse({ type: 'down', button }).catch(err => handleHidError(err, 'mouse down'))
|
||||
}
|
||||
|
||||
function handleMouseUp(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
handleMouseUpInternal(e.button)
|
||||
}
|
||||
|
||||
function handleWindowMouseUp(e: MouseEvent) {
|
||||
if (pressedMouseButton.value !== null) {
|
||||
handleMouseUpInternal(e.button)
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseUpInternal(rawButton: number) {
|
||||
if (mouseMode.value === 'relative' && !isPointerLocked.value) {
|
||||
pressedMouseButton.value = null
|
||||
return
|
||||
}
|
||||
|
||||
const button = rawButton === 0 ? 'left' : rawButton === 2 ? 'right' : 'middle'
|
||||
|
||||
if (pressedMouseButton.value !== button) return
|
||||
|
||||
pressedMouseButton.value = null
|
||||
hidApi.mouse({ type: 'up', button }).catch(err => handleHidError(err, 'mouse up'))
|
||||
}
|
||||
|
||||
function handleWheel(e: WheelEvent) {
|
||||
e.preventDefault()
|
||||
const scroll = e.deltaY > 0 ? -1 : 1
|
||||
hidApi.mouse({ type: 'scroll', scroll }).catch(err => handleHidError(err, 'mouse scroll'))
|
||||
}
|
||||
|
||||
function handleContextMenu(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
// Pointer lock
|
||||
function requestPointerLock() {
|
||||
const container = options.videoContainerRef.value
|
||||
if (!container) return
|
||||
|
||||
container.requestPointerLock().catch((err: Error) => {
|
||||
toast.error(t('console.pointerLockFailed'), {
|
||||
description: err.message,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function exitPointerLock() {
|
||||
if (document.pointerLockElement) {
|
||||
document.exitPointerLock()
|
||||
}
|
||||
}
|
||||
|
||||
function handlePointerLockChange() {
|
||||
const container = options.videoContainerRef.value
|
||||
isPointerLocked.value = document.pointerLockElement === container
|
||||
|
||||
if (isPointerLocked.value) {
|
||||
mousePosition.value = { x: 0, y: 0 }
|
||||
toast.info(t('console.pointerLocked'), {
|
||||
description: t('console.pointerLockedDesc'),
|
||||
duration: 3000,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function handlePointerLockError() {
|
||||
isPointerLocked.value = false
|
||||
}
|
||||
|
||||
function handleBlur() {
|
||||
pressedKeys.value = []
|
||||
activeModifierMask.value = 0
|
||||
if (pressedMouseButton.value !== null) {
|
||||
const button = pressedMouseButton.value
|
||||
pressedMouseButton.value = null
|
||||
hidApi.mouse({ type: 'up', button }).catch(err => handleHidError(err, 'mouse up (blur)'))
|
||||
}
|
||||
}
|
||||
|
||||
// Mode toggle
|
||||
function toggleMouseMode() {
|
||||
if (mouseMode.value === 'relative' && isPointerLocked.value) {
|
||||
exitPointerLock()
|
||||
}
|
||||
|
||||
mouseMode.value = mouseMode.value === 'absolute' ? 'relative' : 'absolute'
|
||||
lastMousePosition.value = { x: 0, y: 0 }
|
||||
mousePosition.value = { x: 0, y: 0 }
|
||||
|
||||
if (mouseMode.value === 'relative') {
|
||||
toast.info(t('console.relativeModeHint'), {
|
||||
description: t('console.relativeModeHintDesc'),
|
||||
duration: 5000,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Virtual keyboard handlers
|
||||
function handleVirtualKeyDown(key: string) {
|
||||
if (!pressedKeys.value.includes(key)) {
|
||||
pressedKeys.value = [...pressedKeys.value, key]
|
||||
}
|
||||
}
|
||||
|
||||
function handleVirtualKeyUp(key: string) {
|
||||
pressedKeys.value = pressedKeys.value.filter(k => k !== key)
|
||||
}
|
||||
|
||||
// Cursor visibility
|
||||
function handleCursorVisibilityChange(e: Event) {
|
||||
const customEvent = e as CustomEvent<{ visible: boolean }>
|
||||
cursorVisible.value = customEvent.detail.visible
|
||||
}
|
||||
|
||||
// Setup event listeners
|
||||
function setupEventListeners() {
|
||||
document.addEventListener('pointerlockchange', handlePointerLockChange)
|
||||
document.addEventListener('pointerlockerror', handlePointerLockError)
|
||||
window.addEventListener('blur', handleBlur)
|
||||
window.addEventListener('mouseup', handleWindowMouseUp)
|
||||
window.addEventListener('cursor-visibility-change', handleCursorVisibilityChange)
|
||||
}
|
||||
|
||||
function cleanupEventListeners() {
|
||||
document.removeEventListener('pointerlockchange', handlePointerLockChange)
|
||||
document.removeEventListener('pointerlockerror', handlePointerLockError)
|
||||
window.removeEventListener('blur', handleBlur)
|
||||
window.removeEventListener('mouseup', handleWindowMouseUp)
|
||||
window.removeEventListener('cursor-visibility-change', handleCursorVisibilityChange)
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
mouseMode,
|
||||
pressedKeys,
|
||||
keyboardLed,
|
||||
mousePosition,
|
||||
isPointerLocked,
|
||||
cursorVisible,
|
||||
|
||||
// Keyboard handlers
|
||||
handleKeyDown,
|
||||
handleKeyUp,
|
||||
|
||||
// Mouse handlers
|
||||
handleMouseMove,
|
||||
handleMouseDown,
|
||||
handleMouseUp,
|
||||
handleWheel,
|
||||
handleContextMenu,
|
||||
|
||||
// Pointer lock
|
||||
requestPointerLock,
|
||||
exitPointerLock,
|
||||
|
||||
// Mode toggle
|
||||
toggleMouseMode,
|
||||
|
||||
// Virtual keyboard
|
||||
handleVirtualKeyDown,
|
||||
handleVirtualKeyUp,
|
||||
|
||||
// Cursor visibility
|
||||
handleCursorVisibilityChange,
|
||||
|
||||
// Lifecycle
|
||||
setupEventListeners,
|
||||
cleanupEventListeners,
|
||||
}
|
||||
}
|
||||
@@ -1,129 +1,130 @@
|
||||
// Character to HID usage mapping for text paste functionality.
|
||||
// The table follows US QWERTY layout semantics.
|
||||
|
||||
import { type CanonicalKey } from '@/types/generated'
|
||||
import { keys } from '@/lib/keyboardMappings'
|
||||
|
||||
export interface CharKeyMapping {
|
||||
hidCode: number // USB HID usage code
|
||||
key: CanonicalKey
|
||||
shift: boolean // Whether Shift modifier is needed
|
||||
}
|
||||
|
||||
const charToKeyMap: Record<string, CharKeyMapping> = {
|
||||
// Lowercase letters
|
||||
a: { hidCode: keys.KeyA, shift: false },
|
||||
b: { hidCode: keys.KeyB, shift: false },
|
||||
c: { hidCode: keys.KeyC, shift: false },
|
||||
d: { hidCode: keys.KeyD, shift: false },
|
||||
e: { hidCode: keys.KeyE, shift: false },
|
||||
f: { hidCode: keys.KeyF, shift: false },
|
||||
g: { hidCode: keys.KeyG, shift: false },
|
||||
h: { hidCode: keys.KeyH, shift: false },
|
||||
i: { hidCode: keys.KeyI, shift: false },
|
||||
j: { hidCode: keys.KeyJ, shift: false },
|
||||
k: { hidCode: keys.KeyK, shift: false },
|
||||
l: { hidCode: keys.KeyL, shift: false },
|
||||
m: { hidCode: keys.KeyM, shift: false },
|
||||
n: { hidCode: keys.KeyN, shift: false },
|
||||
o: { hidCode: keys.KeyO, shift: false },
|
||||
p: { hidCode: keys.KeyP, shift: false },
|
||||
q: { hidCode: keys.KeyQ, shift: false },
|
||||
r: { hidCode: keys.KeyR, shift: false },
|
||||
s: { hidCode: keys.KeyS, shift: false },
|
||||
t: { hidCode: keys.KeyT, shift: false },
|
||||
u: { hidCode: keys.KeyU, shift: false },
|
||||
v: { hidCode: keys.KeyV, shift: false },
|
||||
w: { hidCode: keys.KeyW, shift: false },
|
||||
x: { hidCode: keys.KeyX, shift: false },
|
||||
y: { hidCode: keys.KeyY, shift: false },
|
||||
z: { hidCode: keys.KeyZ, shift: false },
|
||||
a: { key: keys.KeyA, shift: false },
|
||||
b: { key: keys.KeyB, shift: false },
|
||||
c: { key: keys.KeyC, shift: false },
|
||||
d: { key: keys.KeyD, shift: false },
|
||||
e: { key: keys.KeyE, shift: false },
|
||||
f: { key: keys.KeyF, shift: false },
|
||||
g: { key: keys.KeyG, shift: false },
|
||||
h: { key: keys.KeyH, shift: false },
|
||||
i: { key: keys.KeyI, shift: false },
|
||||
j: { key: keys.KeyJ, shift: false },
|
||||
k: { key: keys.KeyK, shift: false },
|
||||
l: { key: keys.KeyL, shift: false },
|
||||
m: { key: keys.KeyM, shift: false },
|
||||
n: { key: keys.KeyN, shift: false },
|
||||
o: { key: keys.KeyO, shift: false },
|
||||
p: { key: keys.KeyP, shift: false },
|
||||
q: { key: keys.KeyQ, shift: false },
|
||||
r: { key: keys.KeyR, shift: false },
|
||||
s: { key: keys.KeyS, shift: false },
|
||||
t: { key: keys.KeyT, shift: false },
|
||||
u: { key: keys.KeyU, shift: false },
|
||||
v: { key: keys.KeyV, shift: false },
|
||||
w: { key: keys.KeyW, shift: false },
|
||||
x: { key: keys.KeyX, shift: false },
|
||||
y: { key: keys.KeyY, shift: false },
|
||||
z: { key: keys.KeyZ, shift: false },
|
||||
|
||||
// Uppercase letters
|
||||
A: { hidCode: keys.KeyA, shift: true },
|
||||
B: { hidCode: keys.KeyB, shift: true },
|
||||
C: { hidCode: keys.KeyC, shift: true },
|
||||
D: { hidCode: keys.KeyD, shift: true },
|
||||
E: { hidCode: keys.KeyE, shift: true },
|
||||
F: { hidCode: keys.KeyF, shift: true },
|
||||
G: { hidCode: keys.KeyG, shift: true },
|
||||
H: { hidCode: keys.KeyH, shift: true },
|
||||
I: { hidCode: keys.KeyI, shift: true },
|
||||
J: { hidCode: keys.KeyJ, shift: true },
|
||||
K: { hidCode: keys.KeyK, shift: true },
|
||||
L: { hidCode: keys.KeyL, shift: true },
|
||||
M: { hidCode: keys.KeyM, shift: true },
|
||||
N: { hidCode: keys.KeyN, shift: true },
|
||||
O: { hidCode: keys.KeyO, shift: true },
|
||||
P: { hidCode: keys.KeyP, shift: true },
|
||||
Q: { hidCode: keys.KeyQ, shift: true },
|
||||
R: { hidCode: keys.KeyR, shift: true },
|
||||
S: { hidCode: keys.KeyS, shift: true },
|
||||
T: { hidCode: keys.KeyT, shift: true },
|
||||
U: { hidCode: keys.KeyU, shift: true },
|
||||
V: { hidCode: keys.KeyV, shift: true },
|
||||
W: { hidCode: keys.KeyW, shift: true },
|
||||
X: { hidCode: keys.KeyX, shift: true },
|
||||
Y: { hidCode: keys.KeyY, shift: true },
|
||||
Z: { hidCode: keys.KeyZ, shift: true },
|
||||
A: { key: keys.KeyA, shift: true },
|
||||
B: { key: keys.KeyB, shift: true },
|
||||
C: { key: keys.KeyC, shift: true },
|
||||
D: { key: keys.KeyD, shift: true },
|
||||
E: { key: keys.KeyE, shift: true },
|
||||
F: { key: keys.KeyF, shift: true },
|
||||
G: { key: keys.KeyG, shift: true },
|
||||
H: { key: keys.KeyH, shift: true },
|
||||
I: { key: keys.KeyI, shift: true },
|
||||
J: { key: keys.KeyJ, shift: true },
|
||||
K: { key: keys.KeyK, shift: true },
|
||||
L: { key: keys.KeyL, shift: true },
|
||||
M: { key: keys.KeyM, shift: true },
|
||||
N: { key: keys.KeyN, shift: true },
|
||||
O: { key: keys.KeyO, shift: true },
|
||||
P: { key: keys.KeyP, shift: true },
|
||||
Q: { key: keys.KeyQ, shift: true },
|
||||
R: { key: keys.KeyR, shift: true },
|
||||
S: { key: keys.KeyS, shift: true },
|
||||
T: { key: keys.KeyT, shift: true },
|
||||
U: { key: keys.KeyU, shift: true },
|
||||
V: { key: keys.KeyV, shift: true },
|
||||
W: { key: keys.KeyW, shift: true },
|
||||
X: { key: keys.KeyX, shift: true },
|
||||
Y: { key: keys.KeyY, shift: true },
|
||||
Z: { key: keys.KeyZ, shift: true },
|
||||
|
||||
// Number row
|
||||
'0': { hidCode: keys.Digit0, shift: false },
|
||||
'1': { hidCode: keys.Digit1, shift: false },
|
||||
'2': { hidCode: keys.Digit2, shift: false },
|
||||
'3': { hidCode: keys.Digit3, shift: false },
|
||||
'4': { hidCode: keys.Digit4, shift: false },
|
||||
'5': { hidCode: keys.Digit5, shift: false },
|
||||
'6': { hidCode: keys.Digit6, shift: false },
|
||||
'7': { hidCode: keys.Digit7, shift: false },
|
||||
'8': { hidCode: keys.Digit8, shift: false },
|
||||
'9': { hidCode: keys.Digit9, shift: false },
|
||||
'0': { key: keys.Digit0, shift: false },
|
||||
'1': { key: keys.Digit1, shift: false },
|
||||
'2': { key: keys.Digit2, shift: false },
|
||||
'3': { key: keys.Digit3, shift: false },
|
||||
'4': { key: keys.Digit4, shift: false },
|
||||
'5': { key: keys.Digit5, shift: false },
|
||||
'6': { key: keys.Digit6, shift: false },
|
||||
'7': { key: keys.Digit7, shift: false },
|
||||
'8': { key: keys.Digit8, shift: false },
|
||||
'9': { key: keys.Digit9, shift: false },
|
||||
|
||||
// Shifted number row symbols
|
||||
')': { hidCode: keys.Digit0, shift: true },
|
||||
'!': { hidCode: keys.Digit1, shift: true },
|
||||
'@': { hidCode: keys.Digit2, shift: true },
|
||||
'#': { hidCode: keys.Digit3, shift: true },
|
||||
'$': { hidCode: keys.Digit4, shift: true },
|
||||
'%': { hidCode: keys.Digit5, shift: true },
|
||||
'^': { hidCode: keys.Digit6, shift: true },
|
||||
'&': { hidCode: keys.Digit7, shift: true },
|
||||
'*': { hidCode: keys.Digit8, shift: true },
|
||||
'(': { hidCode: keys.Digit9, shift: true },
|
||||
')': { key: keys.Digit0, shift: true },
|
||||
'!': { key: keys.Digit1, shift: true },
|
||||
'@': { key: keys.Digit2, shift: true },
|
||||
'#': { key: keys.Digit3, shift: true },
|
||||
'$': { key: keys.Digit4, shift: true },
|
||||
'%': { key: keys.Digit5, shift: true },
|
||||
'^': { key: keys.Digit6, shift: true },
|
||||
'&': { key: keys.Digit7, shift: true },
|
||||
'*': { key: keys.Digit8, shift: true },
|
||||
'(': { key: keys.Digit9, shift: true },
|
||||
|
||||
// Punctuation and symbols
|
||||
'-': { hidCode: keys.Minus, shift: false },
|
||||
'=': { hidCode: keys.Equal, shift: false },
|
||||
'[': { hidCode: keys.BracketLeft, shift: false },
|
||||
']': { hidCode: keys.BracketRight, shift: false },
|
||||
'\\': { hidCode: keys.Backslash, shift: false },
|
||||
';': { hidCode: keys.Semicolon, shift: false },
|
||||
"'": { hidCode: keys.Quote, shift: false },
|
||||
'`': { hidCode: keys.Backquote, shift: false },
|
||||
',': { hidCode: keys.Comma, shift: false },
|
||||
'.': { hidCode: keys.Period, shift: false },
|
||||
'/': { hidCode: keys.Slash, shift: false },
|
||||
'-': { key: keys.Minus, shift: false },
|
||||
'=': { key: keys.Equal, shift: false },
|
||||
'[': { key: keys.BracketLeft, shift: false },
|
||||
']': { key: keys.BracketRight, shift: false },
|
||||
'\\': { key: keys.Backslash, shift: false },
|
||||
';': { key: keys.Semicolon, shift: false },
|
||||
"'": { key: keys.Quote, shift: false },
|
||||
'`': { key: keys.Backquote, shift: false },
|
||||
',': { key: keys.Comma, shift: false },
|
||||
'.': { key: keys.Period, shift: false },
|
||||
'/': { key: keys.Slash, shift: false },
|
||||
|
||||
// Shifted punctuation and symbols
|
||||
_: { hidCode: keys.Minus, shift: true },
|
||||
'+': { hidCode: keys.Equal, shift: true },
|
||||
'{': { hidCode: keys.BracketLeft, shift: true },
|
||||
'}': { hidCode: keys.BracketRight, shift: true },
|
||||
'|': { hidCode: keys.Backslash, shift: true },
|
||||
':': { hidCode: keys.Semicolon, shift: true },
|
||||
'"': { hidCode: keys.Quote, shift: true },
|
||||
'~': { hidCode: keys.Backquote, shift: true },
|
||||
'<': { hidCode: keys.Comma, shift: true },
|
||||
'>': { hidCode: keys.Period, shift: true },
|
||||
'?': { hidCode: keys.Slash, shift: true },
|
||||
_: { key: keys.Minus, shift: true },
|
||||
'+': { key: keys.Equal, shift: true },
|
||||
'{': { key: keys.BracketLeft, shift: true },
|
||||
'}': { key: keys.BracketRight, shift: true },
|
||||
'|': { key: keys.Backslash, shift: true },
|
||||
':': { key: keys.Semicolon, shift: true },
|
||||
'"': { key: keys.Quote, shift: true },
|
||||
'~': { key: keys.Backquote, shift: true },
|
||||
'<': { key: keys.Comma, shift: true },
|
||||
'>': { key: keys.Period, shift: true },
|
||||
'?': { key: keys.Slash, shift: true },
|
||||
|
||||
// Whitespace and control
|
||||
' ': { hidCode: keys.Space, shift: false },
|
||||
'\t': { hidCode: keys.Tab, shift: false },
|
||||
'\n': { hidCode: keys.Enter, shift: false },
|
||||
'\r': { hidCode: keys.Enter, shift: false },
|
||||
' ': { key: keys.Space, shift: false },
|
||||
'\t': { key: keys.Tab, shift: false },
|
||||
'\n': { key: keys.Enter, shift: false },
|
||||
'\r': { key: keys.Enter, shift: false },
|
||||
}
|
||||
|
||||
/**
|
||||
* Get HID usage code and modifier state for a character
|
||||
* Get canonical key and modifier state for a character
|
||||
* @param char - Single character to convert
|
||||
* @returns CharKeyMapping or null if character is not mappable
|
||||
*/
|
||||
|
||||
@@ -1,77 +1,15 @@
|
||||
// Keyboard layout definitions for virtual keyboard
|
||||
// Virtual keyboard layout data shared by the on-screen keyboard.
|
||||
|
||||
export interface KeyboardLayout {
|
||||
id: string
|
||||
name: string
|
||||
// Key display labels
|
||||
keyLabels: Record<string, string>
|
||||
// Shift variant labels (key in parentheses)
|
||||
shiftLabels: Record<string, string>
|
||||
// Virtual keyboard layout rows
|
||||
layout: {
|
||||
main: {
|
||||
macros: string[]
|
||||
functionRow: string[]
|
||||
default: string[][]
|
||||
shift: string[][]
|
||||
}
|
||||
control: string[][]
|
||||
arrows: string[][]
|
||||
media: string[] // Media keys row
|
||||
}
|
||||
}
|
||||
|
||||
// OS-specific keyboard layout type
|
||||
export type KeyboardOsType = 'windows' | 'mac' | 'android'
|
||||
|
||||
// Bottom row layouts for different OS
|
||||
export const osBottomRows: Record<KeyboardOsType, string[]> = {
|
||||
// Windows: Ctrl - Win - Alt - Space - Alt - Win - Menu - Ctrl
|
||||
windows: ['ControlLeft', 'MetaLeft', 'AltLeft', 'Space', 'AltRight', 'MetaRight', 'Menu', 'ControlRight'],
|
||||
// Mac: Ctrl - Option - Cmd - Space - Cmd - Option - Ctrl
|
||||
windows: ['ControlLeft', 'MetaLeft', 'AltLeft', 'Space', 'AltRight', 'MetaRight', 'ContextMenu', 'ControlRight'],
|
||||
mac: ['ControlLeft', 'AltLeft', 'MetaLeft', 'Space', 'MetaRight', 'AltRight', 'ControlRight'],
|
||||
// Android: simplified layout
|
||||
android: ['ControlLeft', 'AltLeft', 'Space', 'AltRight', 'ControlRight'],
|
||||
}
|
||||
|
||||
// OS-specific modifier display names
|
||||
export const osModifierLabels: Record<KeyboardOsType, Record<string, string>> = {
|
||||
windows: {
|
||||
ControlLeft: '^Ctrl',
|
||||
ControlRight: 'Ctrl^',
|
||||
MetaLeft: '⊞Win',
|
||||
MetaRight: 'Win⊞',
|
||||
AltLeft: 'Alt',
|
||||
AltRight: 'Alt',
|
||||
AltGr: 'AltGr',
|
||||
Menu: 'Menu',
|
||||
},
|
||||
mac: {
|
||||
ControlLeft: '^Ctrl',
|
||||
ControlRight: 'Ctrl^',
|
||||
MetaLeft: '⌘Cmd',
|
||||
MetaRight: 'Cmd⌘',
|
||||
AltLeft: '⌥Opt',
|
||||
AltRight: 'Opt⌥',
|
||||
AltGr: '⌥Opt',
|
||||
Menu: 'Menu',
|
||||
},
|
||||
android: {
|
||||
ControlLeft: 'Ctrl',
|
||||
ControlRight: 'Ctrl',
|
||||
MetaLeft: 'Meta',
|
||||
MetaRight: 'Meta',
|
||||
AltLeft: 'Alt',
|
||||
AltRight: 'Alt',
|
||||
AltGr: 'Alt',
|
||||
Menu: 'Menu',
|
||||
},
|
||||
}
|
||||
|
||||
// Media keys (Consumer Control)
|
||||
export const mediaKeys = ['PrevTrack', 'PlayPause', 'NextTrack', 'Stop', 'Mute', 'VolumeDown', 'VolumeUp']
|
||||
|
||||
// Media key display names
|
||||
export const mediaKeyLabels: Record<string, string> = {
|
||||
PlayPause: '⏯',
|
||||
Stop: '⏹',
|
||||
@@ -81,158 +19,3 @@ export const mediaKeyLabels: Record<string, string> = {
|
||||
VolumeUp: '🔊',
|
||||
VolumeDown: '🔉',
|
||||
}
|
||||
|
||||
// English US Layout
|
||||
export const enUSLayout: KeyboardLayout = {
|
||||
id: 'en-US',
|
||||
name: 'English (US)',
|
||||
keyLabels: {
|
||||
// Macros
|
||||
CtrlAltDelete: 'Ctrl+Alt+Del',
|
||||
AltMetaEscape: 'Alt+Meta+Esc',
|
||||
CtrlAltBackspace: 'Ctrl+Alt+Back',
|
||||
|
||||
// Modifiers
|
||||
ControlLeft: 'Ctrl',
|
||||
ControlRight: 'Ctrl',
|
||||
ShiftLeft: 'Shift',
|
||||
ShiftRight: 'Shift',
|
||||
AltLeft: 'Alt',
|
||||
AltRight: 'Alt',
|
||||
AltGr: 'AltGr',
|
||||
MetaLeft: 'Meta',
|
||||
MetaRight: 'Meta',
|
||||
|
||||
// Special keys
|
||||
Escape: 'Esc',
|
||||
Backspace: 'Back',
|
||||
Tab: 'Tab',
|
||||
CapsLock: 'Caps',
|
||||
Enter: 'Enter',
|
||||
Space: ' ',
|
||||
Menu: 'Menu',
|
||||
|
||||
// Navigation
|
||||
Insert: 'Ins',
|
||||
Delete: 'Del',
|
||||
Home: 'Home',
|
||||
End: 'End',
|
||||
PageUp: 'PgUp',
|
||||
PageDown: 'PgDn',
|
||||
|
||||
// Arrows
|
||||
ArrowUp: '\u2191',
|
||||
ArrowDown: '\u2193',
|
||||
ArrowLeft: '\u2190',
|
||||
ArrowRight: '\u2192',
|
||||
|
||||
// Control cluster
|
||||
PrintScreen: 'PrtSc',
|
||||
ScrollLock: 'ScrLk',
|
||||
Pause: 'Pause',
|
||||
|
||||
// Function keys
|
||||
F1: 'F1', F2: 'F2', F3: 'F3', F4: 'F4',
|
||||
F5: 'F5', F6: 'F6', F7: 'F7', F8: 'F8',
|
||||
F9: 'F9', F10: 'F10', F11: 'F11', F12: 'F12',
|
||||
|
||||
// Letters
|
||||
KeyA: 'a', KeyB: 'b', KeyC: 'c', KeyD: 'd', KeyE: 'e',
|
||||
KeyF: 'f', KeyG: 'g', KeyH: 'h', KeyI: 'i', KeyJ: 'j',
|
||||
KeyK: 'k', KeyL: 'l', KeyM: 'm', KeyN: 'n', KeyO: 'o',
|
||||
KeyP: 'p', KeyQ: 'q', KeyR: 'r', KeyS: 's', KeyT: 't',
|
||||
KeyU: 'u', KeyV: 'v', KeyW: 'w', KeyX: 'x', KeyY: 'y',
|
||||
KeyZ: 'z',
|
||||
|
||||
// Numbers
|
||||
Digit1: '1', Digit2: '2', Digit3: '3', Digit4: '4', Digit5: '5',
|
||||
Digit6: '6', Digit7: '7', Digit8: '8', Digit9: '9', Digit0: '0',
|
||||
|
||||
// Symbols
|
||||
Minus: '-',
|
||||
Equal: '=',
|
||||
BracketLeft: '[',
|
||||
BracketRight: ']',
|
||||
Backslash: '\\',
|
||||
Semicolon: ';',
|
||||
Quote: "'",
|
||||
Backquote: '`',
|
||||
Comma: ',',
|
||||
Period: '.',
|
||||
Slash: '/',
|
||||
},
|
||||
shiftLabels: {
|
||||
// Capital letters
|
||||
KeyA: 'A', KeyB: 'B', KeyC: 'C', KeyD: 'D', KeyE: 'E',
|
||||
KeyF: 'F', KeyG: 'G', KeyH: 'H', KeyI: 'I', KeyJ: 'J',
|
||||
KeyK: 'K', KeyL: 'L', KeyM: 'M', KeyN: 'N', KeyO: 'O',
|
||||
KeyP: 'P', KeyQ: 'Q', KeyR: 'R', KeyS: 'S', KeyT: 'T',
|
||||
KeyU: 'U', KeyV: 'V', KeyW: 'W', KeyX: 'X', KeyY: 'Y',
|
||||
KeyZ: 'Z',
|
||||
|
||||
// Shifted numbers
|
||||
Digit1: '!', Digit2: '@', Digit3: '#', Digit4: '$', Digit5: '%',
|
||||
Digit6: '^', Digit7: '&', Digit8: '*', Digit9: '(', Digit0: ')',
|
||||
|
||||
// Shifted symbols
|
||||
Minus: '_',
|
||||
Equal: '+',
|
||||
BracketLeft: '{',
|
||||
BracketRight: '}',
|
||||
Backslash: '|',
|
||||
Semicolon: ':',
|
||||
Quote: '"',
|
||||
Backquote: '~',
|
||||
Comma: '<',
|
||||
Period: '>',
|
||||
Slash: '?',
|
||||
},
|
||||
layout: {
|
||||
main: {
|
||||
macros: ['CtrlAltDelete', 'AltMetaEscape', 'CtrlAltBackspace'],
|
||||
functionRow: ['Escape', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12'],
|
||||
default: [
|
||||
['Backquote', 'Digit1', 'Digit2', 'Digit3', 'Digit4', 'Digit5', 'Digit6', 'Digit7', 'Digit8', 'Digit9', 'Digit0', 'Minus', 'Equal', 'Backspace'],
|
||||
['Tab', 'KeyQ', 'KeyW', 'KeyE', 'KeyR', 'KeyT', 'KeyY', 'KeyU', 'KeyI', 'KeyO', 'KeyP', 'BracketLeft', 'BracketRight', 'Backslash'],
|
||||
['CapsLock', 'KeyA', 'KeyS', 'KeyD', 'KeyF', 'KeyG', 'KeyH', 'KeyJ', 'KeyK', 'KeyL', 'Semicolon', 'Quote', 'Enter'],
|
||||
['ShiftLeft', 'KeyZ', 'KeyX', 'KeyC', 'KeyV', 'KeyB', 'KeyN', 'KeyM', 'Comma', 'Period', 'Slash', 'ShiftRight'],
|
||||
['ControlLeft', 'MetaLeft', 'AltLeft', 'Space', 'AltRight', 'MetaRight', 'Menu', 'ControlRight'],
|
||||
],
|
||||
shift: [
|
||||
['Backquote', 'Digit1', 'Digit2', 'Digit3', 'Digit4', 'Digit5', 'Digit6', 'Digit7', 'Digit8', 'Digit9', 'Digit0', 'Minus', 'Equal', 'Backspace'],
|
||||
['Tab', 'KeyQ', 'KeyW', 'KeyE', 'KeyR', 'KeyT', 'KeyY', 'KeyU', 'KeyI', 'KeyO', 'KeyP', 'BracketLeft', 'BracketRight', 'Backslash'],
|
||||
['CapsLock', 'KeyA', 'KeyS', 'KeyD', 'KeyF', 'KeyG', 'KeyH', 'KeyJ', 'KeyK', 'KeyL', 'Semicolon', 'Quote', 'Enter'],
|
||||
['ShiftLeft', 'KeyZ', 'KeyX', 'KeyC', 'KeyV', 'KeyB', 'KeyN', 'KeyM', 'Comma', 'Period', 'Slash', 'ShiftRight'],
|
||||
['ControlLeft', 'MetaLeft', 'AltLeft', 'Space', 'AltRight', 'MetaRight', 'Menu', 'ControlRight'],
|
||||
],
|
||||
},
|
||||
control: [
|
||||
['PrintScreen', 'ScrollLock', 'Pause'],
|
||||
['Insert', 'Home', 'PageUp'],
|
||||
['Delete', 'End', 'PageDown'],
|
||||
],
|
||||
arrows: [
|
||||
['ArrowUp'],
|
||||
['ArrowLeft', 'ArrowDown', 'ArrowRight'],
|
||||
],
|
||||
media: ['PrevTrack', 'PlayPause', 'NextTrack', 'Stop', 'Mute', 'VolumeDown', 'VolumeUp'],
|
||||
},
|
||||
}
|
||||
|
||||
// All available layouts
|
||||
export const keyboardLayouts: Record<string, KeyboardLayout> = {
|
||||
'en-US': enUSLayout,
|
||||
}
|
||||
|
||||
// Get layout by ID or return default
|
||||
export function getKeyboardLayout(id: string): KeyboardLayout {
|
||||
return keyboardLayouts[id] || enUSLayout
|
||||
}
|
||||
|
||||
// Get key label for display
|
||||
export function getKeyLabel(layout: KeyboardLayout, keyName: string, isShift: boolean): string {
|
||||
if (isShift && layout.shiftLabels[keyName]) {
|
||||
return layout.shiftLabels[keyName]
|
||||
}
|
||||
return layout.keyLabels[keyName] || keyName
|
||||
}
|
||||
|
||||
@@ -1,157 +1,128 @@
|
||||
// Key codes and modifiers correspond to definitions in the
|
||||
// [Linux USB HID gadget driver](https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt)
|
||||
// [Universal Serial Bus HID Usage Tables: Section 10](https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf)
|
||||
import { CanonicalKey } from '@/types/generated'
|
||||
|
||||
export const keys = {
|
||||
// Letters
|
||||
KeyA: 0x04,
|
||||
KeyB: 0x05,
|
||||
KeyC: 0x06,
|
||||
KeyD: 0x07,
|
||||
KeyE: 0x08,
|
||||
KeyF: 0x09,
|
||||
KeyG: 0x0a,
|
||||
KeyH: 0x0b,
|
||||
KeyI: 0x0c,
|
||||
KeyJ: 0x0d,
|
||||
KeyK: 0x0e,
|
||||
KeyL: 0x0f,
|
||||
KeyM: 0x10,
|
||||
KeyN: 0x11,
|
||||
KeyO: 0x12,
|
||||
KeyP: 0x13,
|
||||
KeyQ: 0x14,
|
||||
KeyR: 0x15,
|
||||
KeyS: 0x16,
|
||||
KeyT: 0x17,
|
||||
KeyU: 0x18,
|
||||
KeyV: 0x19,
|
||||
KeyW: 0x1a,
|
||||
KeyX: 0x1b,
|
||||
KeyY: 0x1c,
|
||||
KeyZ: 0x1d,
|
||||
|
||||
// Numbers
|
||||
Digit1: 0x1e,
|
||||
Digit2: 0x1f,
|
||||
Digit3: 0x20,
|
||||
Digit4: 0x21,
|
||||
Digit5: 0x22,
|
||||
Digit6: 0x23,
|
||||
Digit7: 0x24,
|
||||
Digit8: 0x25,
|
||||
Digit9: 0x26,
|
||||
Digit0: 0x27,
|
||||
|
||||
// Control keys
|
||||
Enter: 0x28,
|
||||
Escape: 0x29,
|
||||
Backspace: 0x2a,
|
||||
Tab: 0x2b,
|
||||
Space: 0x2c,
|
||||
|
||||
// Symbols
|
||||
Minus: 0x2d,
|
||||
Equal: 0x2e,
|
||||
BracketLeft: 0x2f,
|
||||
BracketRight: 0x30,
|
||||
Backslash: 0x31,
|
||||
Semicolon: 0x33,
|
||||
Quote: 0x34,
|
||||
Backquote: 0x35,
|
||||
Comma: 0x36,
|
||||
Period: 0x37,
|
||||
Slash: 0x38,
|
||||
|
||||
// Lock keys
|
||||
CapsLock: 0x39,
|
||||
|
||||
// Function keys
|
||||
F1: 0x3a,
|
||||
F2: 0x3b,
|
||||
F3: 0x3c,
|
||||
F4: 0x3d,
|
||||
F5: 0x3e,
|
||||
F6: 0x3f,
|
||||
F7: 0x40,
|
||||
F8: 0x41,
|
||||
F9: 0x42,
|
||||
F10: 0x43,
|
||||
F11: 0x44,
|
||||
F12: 0x45,
|
||||
|
||||
// Control cluster
|
||||
PrintScreen: 0x46,
|
||||
ScrollLock: 0x47,
|
||||
Pause: 0x48,
|
||||
Insert: 0x49,
|
||||
Home: 0x4a,
|
||||
PageUp: 0x4b,
|
||||
Delete: 0x4c,
|
||||
End: 0x4d,
|
||||
PageDown: 0x4e,
|
||||
|
||||
// Arrow keys
|
||||
ArrowRight: 0x4f,
|
||||
ArrowLeft: 0x50,
|
||||
ArrowDown: 0x51,
|
||||
ArrowUp: 0x52,
|
||||
|
||||
// Numpad
|
||||
NumLock: 0x53,
|
||||
NumpadDivide: 0x54,
|
||||
NumpadMultiply: 0x55,
|
||||
NumpadSubtract: 0x56,
|
||||
NumpadAdd: 0x57,
|
||||
NumpadEnter: 0x58,
|
||||
Numpad1: 0x59,
|
||||
Numpad2: 0x5a,
|
||||
Numpad3: 0x5b,
|
||||
Numpad4: 0x5c,
|
||||
Numpad5: 0x5d,
|
||||
Numpad6: 0x5e,
|
||||
Numpad7: 0x5f,
|
||||
Numpad8: 0x60,
|
||||
Numpad9: 0x61,
|
||||
Numpad0: 0x62,
|
||||
NumpadDecimal: 0x63,
|
||||
|
||||
// Non-US keys
|
||||
IntlBackslash: 0x64,
|
||||
ContextMenu: 0x65,
|
||||
Menu: 0x65,
|
||||
Application: 0x65,
|
||||
|
||||
// Extended function keys
|
||||
F13: 0x68,
|
||||
F14: 0x69,
|
||||
F15: 0x6a,
|
||||
F16: 0x6b,
|
||||
F17: 0x6c,
|
||||
F18: 0x6d,
|
||||
F19: 0x6e,
|
||||
F20: 0x6f,
|
||||
F21: 0x70,
|
||||
F22: 0x71,
|
||||
F23: 0x72,
|
||||
F24: 0x73,
|
||||
|
||||
// Modifiers (these are special - HID codes 0xE0-0xE7)
|
||||
ControlLeft: 0xe0,
|
||||
ShiftLeft: 0xe1,
|
||||
AltLeft: 0xe2,
|
||||
MetaLeft: 0xe3,
|
||||
ControlRight: 0xe4,
|
||||
ShiftRight: 0xe5,
|
||||
AltRight: 0xe6,
|
||||
AltGr: 0xe6,
|
||||
MetaRight: 0xe7,
|
||||
KeyA: CanonicalKey.KeyA,
|
||||
KeyB: CanonicalKey.KeyB,
|
||||
KeyC: CanonicalKey.KeyC,
|
||||
KeyD: CanonicalKey.KeyD,
|
||||
KeyE: CanonicalKey.KeyE,
|
||||
KeyF: CanonicalKey.KeyF,
|
||||
KeyG: CanonicalKey.KeyG,
|
||||
KeyH: CanonicalKey.KeyH,
|
||||
KeyI: CanonicalKey.KeyI,
|
||||
KeyJ: CanonicalKey.KeyJ,
|
||||
KeyK: CanonicalKey.KeyK,
|
||||
KeyL: CanonicalKey.KeyL,
|
||||
KeyM: CanonicalKey.KeyM,
|
||||
KeyN: CanonicalKey.KeyN,
|
||||
KeyO: CanonicalKey.KeyO,
|
||||
KeyP: CanonicalKey.KeyP,
|
||||
KeyQ: CanonicalKey.KeyQ,
|
||||
KeyR: CanonicalKey.KeyR,
|
||||
KeyS: CanonicalKey.KeyS,
|
||||
KeyT: CanonicalKey.KeyT,
|
||||
KeyU: CanonicalKey.KeyU,
|
||||
KeyV: CanonicalKey.KeyV,
|
||||
KeyW: CanonicalKey.KeyW,
|
||||
KeyX: CanonicalKey.KeyX,
|
||||
KeyY: CanonicalKey.KeyY,
|
||||
KeyZ: CanonicalKey.KeyZ,
|
||||
Digit1: CanonicalKey.Digit1,
|
||||
Digit2: CanonicalKey.Digit2,
|
||||
Digit3: CanonicalKey.Digit3,
|
||||
Digit4: CanonicalKey.Digit4,
|
||||
Digit5: CanonicalKey.Digit5,
|
||||
Digit6: CanonicalKey.Digit6,
|
||||
Digit7: CanonicalKey.Digit7,
|
||||
Digit8: CanonicalKey.Digit8,
|
||||
Digit9: CanonicalKey.Digit9,
|
||||
Digit0: CanonicalKey.Digit0,
|
||||
Enter: CanonicalKey.Enter,
|
||||
Escape: CanonicalKey.Escape,
|
||||
Backspace: CanonicalKey.Backspace,
|
||||
Tab: CanonicalKey.Tab,
|
||||
Space: CanonicalKey.Space,
|
||||
Minus: CanonicalKey.Minus,
|
||||
Equal: CanonicalKey.Equal,
|
||||
BracketLeft: CanonicalKey.BracketLeft,
|
||||
BracketRight: CanonicalKey.BracketRight,
|
||||
Backslash: CanonicalKey.Backslash,
|
||||
Semicolon: CanonicalKey.Semicolon,
|
||||
Quote: CanonicalKey.Quote,
|
||||
Backquote: CanonicalKey.Backquote,
|
||||
Comma: CanonicalKey.Comma,
|
||||
Period: CanonicalKey.Period,
|
||||
Slash: CanonicalKey.Slash,
|
||||
CapsLock: CanonicalKey.CapsLock,
|
||||
F1: CanonicalKey.F1,
|
||||
F2: CanonicalKey.F2,
|
||||
F3: CanonicalKey.F3,
|
||||
F4: CanonicalKey.F4,
|
||||
F5: CanonicalKey.F5,
|
||||
F6: CanonicalKey.F6,
|
||||
F7: CanonicalKey.F7,
|
||||
F8: CanonicalKey.F8,
|
||||
F9: CanonicalKey.F9,
|
||||
F10: CanonicalKey.F10,
|
||||
F11: CanonicalKey.F11,
|
||||
F12: CanonicalKey.F12,
|
||||
PrintScreen: CanonicalKey.PrintScreen,
|
||||
ScrollLock: CanonicalKey.ScrollLock,
|
||||
Pause: CanonicalKey.Pause,
|
||||
Insert: CanonicalKey.Insert,
|
||||
Home: CanonicalKey.Home,
|
||||
PageUp: CanonicalKey.PageUp,
|
||||
Delete: CanonicalKey.Delete,
|
||||
End: CanonicalKey.End,
|
||||
PageDown: CanonicalKey.PageDown,
|
||||
ArrowRight: CanonicalKey.ArrowRight,
|
||||
ArrowLeft: CanonicalKey.ArrowLeft,
|
||||
ArrowDown: CanonicalKey.ArrowDown,
|
||||
ArrowUp: CanonicalKey.ArrowUp,
|
||||
NumLock: CanonicalKey.NumLock,
|
||||
NumpadDivide: CanonicalKey.NumpadDivide,
|
||||
NumpadMultiply: CanonicalKey.NumpadMultiply,
|
||||
NumpadSubtract: CanonicalKey.NumpadSubtract,
|
||||
NumpadAdd: CanonicalKey.NumpadAdd,
|
||||
NumpadEnter: CanonicalKey.NumpadEnter,
|
||||
Numpad1: CanonicalKey.Numpad1,
|
||||
Numpad2: CanonicalKey.Numpad2,
|
||||
Numpad3: CanonicalKey.Numpad3,
|
||||
Numpad4: CanonicalKey.Numpad4,
|
||||
Numpad5: CanonicalKey.Numpad5,
|
||||
Numpad6: CanonicalKey.Numpad6,
|
||||
Numpad7: CanonicalKey.Numpad7,
|
||||
Numpad8: CanonicalKey.Numpad8,
|
||||
Numpad9: CanonicalKey.Numpad9,
|
||||
Numpad0: CanonicalKey.Numpad0,
|
||||
NumpadDecimal: CanonicalKey.NumpadDecimal,
|
||||
IntlBackslash: CanonicalKey.IntlBackslash,
|
||||
ContextMenu: CanonicalKey.ContextMenu,
|
||||
F13: CanonicalKey.F13,
|
||||
F14: CanonicalKey.F14,
|
||||
F15: CanonicalKey.F15,
|
||||
F16: CanonicalKey.F16,
|
||||
F17: CanonicalKey.F17,
|
||||
F18: CanonicalKey.F18,
|
||||
F19: CanonicalKey.F19,
|
||||
F20: CanonicalKey.F20,
|
||||
F21: CanonicalKey.F21,
|
||||
F22: CanonicalKey.F22,
|
||||
F23: CanonicalKey.F23,
|
||||
F24: CanonicalKey.F24,
|
||||
ControlLeft: CanonicalKey.ControlLeft,
|
||||
ShiftLeft: CanonicalKey.ShiftLeft,
|
||||
AltLeft: CanonicalKey.AltLeft,
|
||||
MetaLeft: CanonicalKey.MetaLeft,
|
||||
ControlRight: CanonicalKey.ControlRight,
|
||||
ShiftRight: CanonicalKey.ShiftRight,
|
||||
AltRight: CanonicalKey.AltRight,
|
||||
MetaRight: CanonicalKey.MetaRight,
|
||||
} as const
|
||||
|
||||
export type KeyName = keyof typeof keys
|
||||
|
||||
// Consumer Control Usage codes (for multimedia keys)
|
||||
// These are sent via a separate Consumer Control HID report
|
||||
export const consumerKeys = {
|
||||
PlayPause: 0x00cd,
|
||||
Stop: 0x00b7,
|
||||
@@ -164,69 +135,153 @@ export const consumerKeys = {
|
||||
|
||||
export type ConsumerKeyName = keyof typeof consumerKeys
|
||||
|
||||
// Modifier bitmasks for HID report byte 0
|
||||
export const modifiers = {
|
||||
ControlLeft: 0x01,
|
||||
ShiftLeft: 0x02,
|
||||
AltLeft: 0x04,
|
||||
MetaLeft: 0x08,
|
||||
ControlRight: 0x10,
|
||||
ShiftRight: 0x20,
|
||||
AltRight: 0x40,
|
||||
AltGr: 0x40,
|
||||
MetaRight: 0x80,
|
||||
} as const
|
||||
|
||||
export type ModifierName = keyof typeof modifiers
|
||||
|
||||
// Map HID key codes to modifier bitmasks
|
||||
export const hidKeyToModifierMask: Record<number, number> = {
|
||||
0xe0: 0x01, // ControlLeft
|
||||
0xe1: 0x02, // ShiftLeft
|
||||
0xe2: 0x04, // AltLeft
|
||||
0xe3: 0x08, // MetaLeft
|
||||
0xe4: 0x10, // ControlRight
|
||||
0xe5: 0x20, // ShiftRight
|
||||
0xe6: 0x40, // AltRight
|
||||
0xe7: 0x80, // MetaRight
|
||||
export const modifiers: Partial<Record<CanonicalKey, number>> = {
|
||||
[CanonicalKey.ControlLeft]: 0x01,
|
||||
[CanonicalKey.ShiftLeft]: 0x02,
|
||||
[CanonicalKey.AltLeft]: 0x04,
|
||||
[CanonicalKey.MetaLeft]: 0x08,
|
||||
[CanonicalKey.ControlRight]: 0x10,
|
||||
[CanonicalKey.ShiftRight]: 0x20,
|
||||
[CanonicalKey.AltRight]: 0x40,
|
||||
[CanonicalKey.MetaRight]: 0x80,
|
||||
}
|
||||
|
||||
// Update modifier mask when a HID modifier key is pressed/released.
|
||||
export function updateModifierMaskForHidKey(mask: number, hidKey: number, press: boolean): number {
|
||||
const bit = hidKeyToModifierMask[hidKey] ?? 0
|
||||
export const keyToHidUsage = {
|
||||
[CanonicalKey.KeyA]: 0x04,
|
||||
[CanonicalKey.KeyB]: 0x05,
|
||||
[CanonicalKey.KeyC]: 0x06,
|
||||
[CanonicalKey.KeyD]: 0x07,
|
||||
[CanonicalKey.KeyE]: 0x08,
|
||||
[CanonicalKey.KeyF]: 0x09,
|
||||
[CanonicalKey.KeyG]: 0x0a,
|
||||
[CanonicalKey.KeyH]: 0x0b,
|
||||
[CanonicalKey.KeyI]: 0x0c,
|
||||
[CanonicalKey.KeyJ]: 0x0d,
|
||||
[CanonicalKey.KeyK]: 0x0e,
|
||||
[CanonicalKey.KeyL]: 0x0f,
|
||||
[CanonicalKey.KeyM]: 0x10,
|
||||
[CanonicalKey.KeyN]: 0x11,
|
||||
[CanonicalKey.KeyO]: 0x12,
|
||||
[CanonicalKey.KeyP]: 0x13,
|
||||
[CanonicalKey.KeyQ]: 0x14,
|
||||
[CanonicalKey.KeyR]: 0x15,
|
||||
[CanonicalKey.KeyS]: 0x16,
|
||||
[CanonicalKey.KeyT]: 0x17,
|
||||
[CanonicalKey.KeyU]: 0x18,
|
||||
[CanonicalKey.KeyV]: 0x19,
|
||||
[CanonicalKey.KeyW]: 0x1a,
|
||||
[CanonicalKey.KeyX]: 0x1b,
|
||||
[CanonicalKey.KeyY]: 0x1c,
|
||||
[CanonicalKey.KeyZ]: 0x1d,
|
||||
[CanonicalKey.Digit1]: 0x1e,
|
||||
[CanonicalKey.Digit2]: 0x1f,
|
||||
[CanonicalKey.Digit3]: 0x20,
|
||||
[CanonicalKey.Digit4]: 0x21,
|
||||
[CanonicalKey.Digit5]: 0x22,
|
||||
[CanonicalKey.Digit6]: 0x23,
|
||||
[CanonicalKey.Digit7]: 0x24,
|
||||
[CanonicalKey.Digit8]: 0x25,
|
||||
[CanonicalKey.Digit9]: 0x26,
|
||||
[CanonicalKey.Digit0]: 0x27,
|
||||
[CanonicalKey.Enter]: 0x28,
|
||||
[CanonicalKey.Escape]: 0x29,
|
||||
[CanonicalKey.Backspace]: 0x2a,
|
||||
[CanonicalKey.Tab]: 0x2b,
|
||||
[CanonicalKey.Space]: 0x2c,
|
||||
[CanonicalKey.Minus]: 0x2d,
|
||||
[CanonicalKey.Equal]: 0x2e,
|
||||
[CanonicalKey.BracketLeft]: 0x2f,
|
||||
[CanonicalKey.BracketRight]: 0x30,
|
||||
[CanonicalKey.Backslash]: 0x31,
|
||||
[CanonicalKey.Semicolon]: 0x33,
|
||||
[CanonicalKey.Quote]: 0x34,
|
||||
[CanonicalKey.Backquote]: 0x35,
|
||||
[CanonicalKey.Comma]: 0x36,
|
||||
[CanonicalKey.Period]: 0x37,
|
||||
[CanonicalKey.Slash]: 0x38,
|
||||
[CanonicalKey.CapsLock]: 0x39,
|
||||
[CanonicalKey.F1]: 0x3a,
|
||||
[CanonicalKey.F2]: 0x3b,
|
||||
[CanonicalKey.F3]: 0x3c,
|
||||
[CanonicalKey.F4]: 0x3d,
|
||||
[CanonicalKey.F5]: 0x3e,
|
||||
[CanonicalKey.F6]: 0x3f,
|
||||
[CanonicalKey.F7]: 0x40,
|
||||
[CanonicalKey.F8]: 0x41,
|
||||
[CanonicalKey.F9]: 0x42,
|
||||
[CanonicalKey.F10]: 0x43,
|
||||
[CanonicalKey.F11]: 0x44,
|
||||
[CanonicalKey.F12]: 0x45,
|
||||
[CanonicalKey.PrintScreen]: 0x46,
|
||||
[CanonicalKey.ScrollLock]: 0x47,
|
||||
[CanonicalKey.Pause]: 0x48,
|
||||
[CanonicalKey.Insert]: 0x49,
|
||||
[CanonicalKey.Home]: 0x4a,
|
||||
[CanonicalKey.PageUp]: 0x4b,
|
||||
[CanonicalKey.Delete]: 0x4c,
|
||||
[CanonicalKey.End]: 0x4d,
|
||||
[CanonicalKey.PageDown]: 0x4e,
|
||||
[CanonicalKey.ArrowRight]: 0x4f,
|
||||
[CanonicalKey.ArrowLeft]: 0x50,
|
||||
[CanonicalKey.ArrowDown]: 0x51,
|
||||
[CanonicalKey.ArrowUp]: 0x52,
|
||||
[CanonicalKey.NumLock]: 0x53,
|
||||
[CanonicalKey.NumpadDivide]: 0x54,
|
||||
[CanonicalKey.NumpadMultiply]: 0x55,
|
||||
[CanonicalKey.NumpadSubtract]: 0x56,
|
||||
[CanonicalKey.NumpadAdd]: 0x57,
|
||||
[CanonicalKey.NumpadEnter]: 0x58,
|
||||
[CanonicalKey.Numpad1]: 0x59,
|
||||
[CanonicalKey.Numpad2]: 0x5a,
|
||||
[CanonicalKey.Numpad3]: 0x5b,
|
||||
[CanonicalKey.Numpad4]: 0x5c,
|
||||
[CanonicalKey.Numpad5]: 0x5d,
|
||||
[CanonicalKey.Numpad6]: 0x5e,
|
||||
[CanonicalKey.Numpad7]: 0x5f,
|
||||
[CanonicalKey.Numpad8]: 0x60,
|
||||
[CanonicalKey.Numpad9]: 0x61,
|
||||
[CanonicalKey.Numpad0]: 0x62,
|
||||
[CanonicalKey.NumpadDecimal]: 0x63,
|
||||
[CanonicalKey.IntlBackslash]: 0x64,
|
||||
[CanonicalKey.ContextMenu]: 0x65,
|
||||
[CanonicalKey.F13]: 0x68,
|
||||
[CanonicalKey.F14]: 0x69,
|
||||
[CanonicalKey.F15]: 0x6a,
|
||||
[CanonicalKey.F16]: 0x6b,
|
||||
[CanonicalKey.F17]: 0x6c,
|
||||
[CanonicalKey.F18]: 0x6d,
|
||||
[CanonicalKey.F19]: 0x6e,
|
||||
[CanonicalKey.F20]: 0x6f,
|
||||
[CanonicalKey.F21]: 0x70,
|
||||
[CanonicalKey.F22]: 0x71,
|
||||
[CanonicalKey.F23]: 0x72,
|
||||
[CanonicalKey.F24]: 0x73,
|
||||
[CanonicalKey.ControlLeft]: 0xe0,
|
||||
[CanonicalKey.ShiftLeft]: 0xe1,
|
||||
[CanonicalKey.AltLeft]: 0xe2,
|
||||
[CanonicalKey.MetaLeft]: 0xe3,
|
||||
[CanonicalKey.ControlRight]: 0xe4,
|
||||
[CanonicalKey.ShiftRight]: 0xe5,
|
||||
[CanonicalKey.AltRight]: 0xe6,
|
||||
[CanonicalKey.MetaRight]: 0xe7,
|
||||
} as const satisfies Record<CanonicalKey, number>
|
||||
|
||||
export function canonicalKeyToHidUsage(key: CanonicalKey): number {
|
||||
return keyToHidUsage[key]
|
||||
}
|
||||
|
||||
export function updateModifierMaskForKey(mask: number, key: CanonicalKey, press: boolean): number {
|
||||
const bit = modifiers[key] ?? 0
|
||||
if (bit === 0) return mask
|
||||
return press ? (mask | bit) : (mask & ~bit)
|
||||
}
|
||||
|
||||
// Keys that latch (toggle state) instead of being held
|
||||
export const latchingKeys = ['CapsLock', 'ScrollLock', 'NumLock'] as const
|
||||
|
||||
// Modifier key names
|
||||
export const modifierKeyNames = [
|
||||
'ControlLeft',
|
||||
'ControlRight',
|
||||
'ShiftLeft',
|
||||
'ShiftRight',
|
||||
'AltLeft',
|
||||
'AltRight',
|
||||
'AltGr',
|
||||
'MetaLeft',
|
||||
'MetaRight',
|
||||
export const latchingKeys = [
|
||||
CanonicalKey.CapsLock,
|
||||
CanonicalKey.ScrollLock,
|
||||
CanonicalKey.NumLock,
|
||||
] as const
|
||||
|
||||
// Check if a key is a modifier
|
||||
export function isModifierKey(keyName: string): keyName is ModifierName {
|
||||
return keyName in modifiers
|
||||
}
|
||||
|
||||
// Get modifier bitmask for a key name
|
||||
export function getModifierMask(keyName: string): number {
|
||||
if (keyName in modifiers) {
|
||||
return modifiers[keyName as ModifierName]
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Normalize browser-specific KeyboardEvent.code variants.
|
||||
export function normalizeKeyboardCode(code: string, key: string): string {
|
||||
if (code === 'IntlBackslash' && (key === '`' || key === '~')) return 'Backquote'
|
||||
@@ -238,18 +293,10 @@ export function normalizeKeyboardCode(code: string, key: string): string {
|
||||
return code
|
||||
}
|
||||
|
||||
// Convert KeyboardEvent.code/key to USB HID usage code.
|
||||
export function keyboardEventToHidCode(code: string, key: string): number | undefined {
|
||||
export function keyboardEventToCanonicalKey(code: string, key: string): CanonicalKey | undefined {
|
||||
const normalizedCode = normalizeKeyboardCode(code, key)
|
||||
return keys[normalizedCode as KeyName]
|
||||
}
|
||||
|
||||
// Decode modifier byte into individual states
|
||||
export function decodeModifiers(modifier: number) {
|
||||
return {
|
||||
isShiftActive: (modifier & 0x22) !== 0, // ShiftLeft | ShiftRight
|
||||
isControlActive: (modifier & 0x11) !== 0, // ControlLeft | ControlRight
|
||||
isAltActive: (modifier & 0x44) !== 0, // AltLeft | AltRight
|
||||
isMetaActive: (modifier & 0x88) !== 0, // MetaLeft | MetaRight
|
||||
if (normalizedCode in keys) {
|
||||
return keys[normalizedCode as KeyName]
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ export enum AtxDriverType {
|
||||
Gpio = "gpio",
|
||||
/** USB HID relay module */
|
||||
UsbRelay = "usbrelay",
|
||||
/** Serial/COM port relay (LCUS type) */
|
||||
/** Serial/COM port relay (taobao LCUS type) */
|
||||
Serial = "serial",
|
||||
/** Disabled / Not configured */
|
||||
None = "none",
|
||||
@@ -149,6 +149,7 @@ export interface AtxKeyConfig {
|
||||
* Pin or channel number:
|
||||
* - For GPIO: GPIO pin number
|
||||
* - For USB Relay: relay channel (0-based)
|
||||
* - For Serial Relay (LCUS): relay channel (1-based)
|
||||
*/
|
||||
pin: number;
|
||||
/** Active level (only applicable to GPIO, ignored for USB Relay) */
|
||||
@@ -444,11 +445,11 @@ export interface AtxConfigUpdate {
|
||||
/** Available ATX devices for discovery */
|
||||
export interface AtxDevices {
|
||||
/** Available GPIO chips (/dev/gpiochip*) */
|
||||
/** Available Serial ports (/dev/ttyUSB*) */
|
||||
serial_ports: string[];
|
||||
gpio_chips: string[];
|
||||
/** Available USB HID relay devices (/dev/hidraw*) */
|
||||
usb_relays: string[];
|
||||
/** Available Serial ports (/dev/ttyUSB*) */
|
||||
serial_ports: string[];
|
||||
}
|
||||
|
||||
export interface AudioConfigUpdate {
|
||||
@@ -623,19 +624,19 @@ export interface RustDeskConfigUpdate {
|
||||
device_password?: string;
|
||||
}
|
||||
|
||||
/** Stream 配置响应(包含 has_turn_password 字段) */
|
||||
/** Stream configuration response (includes has_turn_password) */
|
||||
export interface StreamConfigResponse {
|
||||
mode: StreamMode;
|
||||
encoder: EncoderType;
|
||||
bitrate_preset: BitratePreset;
|
||||
/** 是否有公共 ICE 服务器可用(编译时确定) */
|
||||
/** Whether public ICE servers are available (compile-time decision) */
|
||||
has_public_ice_servers: boolean;
|
||||
/** 当前是否正在使用公共 ICE 服务器(STUN/TURN 都为空时) */
|
||||
/** Whether public ICE servers are currently in use (when STUN/TURN are unset) */
|
||||
using_public_ice_servers: boolean;
|
||||
stun_server?: string;
|
||||
turn_server?: string;
|
||||
turn_username?: string;
|
||||
/** 指示是否已设置 TURN 密码(实际密码不返回) */
|
||||
/** Indicates whether TURN password has been configured (password is not returned) */
|
||||
has_turn_password: boolean;
|
||||
}
|
||||
|
||||
@@ -688,3 +689,130 @@ export interface WebConfigUpdate {
|
||||
bind_address?: string;
|
||||
https_enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared canonical keyboard key identifiers used across frontend and backend.
|
||||
*
|
||||
* The enum names intentionally mirror `KeyboardEvent.code` style values so the
|
||||
* browser, virtual keyboard, and HID backend can all speak the same language.
|
||||
*/
|
||||
export enum CanonicalKey {
|
||||
KeyA = "KeyA",
|
||||
KeyB = "KeyB",
|
||||
KeyC = "KeyC",
|
||||
KeyD = "KeyD",
|
||||
KeyE = "KeyE",
|
||||
KeyF = "KeyF",
|
||||
KeyG = "KeyG",
|
||||
KeyH = "KeyH",
|
||||
KeyI = "KeyI",
|
||||
KeyJ = "KeyJ",
|
||||
KeyK = "KeyK",
|
||||
KeyL = "KeyL",
|
||||
KeyM = "KeyM",
|
||||
KeyN = "KeyN",
|
||||
KeyO = "KeyO",
|
||||
KeyP = "KeyP",
|
||||
KeyQ = "KeyQ",
|
||||
KeyR = "KeyR",
|
||||
KeyS = "KeyS",
|
||||
KeyT = "KeyT",
|
||||
KeyU = "KeyU",
|
||||
KeyV = "KeyV",
|
||||
KeyW = "KeyW",
|
||||
KeyX = "KeyX",
|
||||
KeyY = "KeyY",
|
||||
KeyZ = "KeyZ",
|
||||
Digit1 = "Digit1",
|
||||
Digit2 = "Digit2",
|
||||
Digit3 = "Digit3",
|
||||
Digit4 = "Digit4",
|
||||
Digit5 = "Digit5",
|
||||
Digit6 = "Digit6",
|
||||
Digit7 = "Digit7",
|
||||
Digit8 = "Digit8",
|
||||
Digit9 = "Digit9",
|
||||
Digit0 = "Digit0",
|
||||
Enter = "Enter",
|
||||
Escape = "Escape",
|
||||
Backspace = "Backspace",
|
||||
Tab = "Tab",
|
||||
Space = "Space",
|
||||
Minus = "Minus",
|
||||
Equal = "Equal",
|
||||
BracketLeft = "BracketLeft",
|
||||
BracketRight = "BracketRight",
|
||||
Backslash = "Backslash",
|
||||
Semicolon = "Semicolon",
|
||||
Quote = "Quote",
|
||||
Backquote = "Backquote",
|
||||
Comma = "Comma",
|
||||
Period = "Period",
|
||||
Slash = "Slash",
|
||||
CapsLock = "CapsLock",
|
||||
F1 = "F1",
|
||||
F2 = "F2",
|
||||
F3 = "F3",
|
||||
F4 = "F4",
|
||||
F5 = "F5",
|
||||
F6 = "F6",
|
||||
F7 = "F7",
|
||||
F8 = "F8",
|
||||
F9 = "F9",
|
||||
F10 = "F10",
|
||||
F11 = "F11",
|
||||
F12 = "F12",
|
||||
PrintScreen = "PrintScreen",
|
||||
ScrollLock = "ScrollLock",
|
||||
Pause = "Pause",
|
||||
Insert = "Insert",
|
||||
Home = "Home",
|
||||
PageUp = "PageUp",
|
||||
Delete = "Delete",
|
||||
End = "End",
|
||||
PageDown = "PageDown",
|
||||
ArrowRight = "ArrowRight",
|
||||
ArrowLeft = "ArrowLeft",
|
||||
ArrowDown = "ArrowDown",
|
||||
ArrowUp = "ArrowUp",
|
||||
NumLock = "NumLock",
|
||||
NumpadDivide = "NumpadDivide",
|
||||
NumpadMultiply = "NumpadMultiply",
|
||||
NumpadSubtract = "NumpadSubtract",
|
||||
NumpadAdd = "NumpadAdd",
|
||||
NumpadEnter = "NumpadEnter",
|
||||
Numpad1 = "Numpad1",
|
||||
Numpad2 = "Numpad2",
|
||||
Numpad3 = "Numpad3",
|
||||
Numpad4 = "Numpad4",
|
||||
Numpad5 = "Numpad5",
|
||||
Numpad6 = "Numpad6",
|
||||
Numpad7 = "Numpad7",
|
||||
Numpad8 = "Numpad8",
|
||||
Numpad9 = "Numpad9",
|
||||
Numpad0 = "Numpad0",
|
||||
NumpadDecimal = "NumpadDecimal",
|
||||
IntlBackslash = "IntlBackslash",
|
||||
ContextMenu = "ContextMenu",
|
||||
F13 = "F13",
|
||||
F14 = "F14",
|
||||
F15 = "F15",
|
||||
F16 = "F16",
|
||||
F17 = "F17",
|
||||
F18 = "F18",
|
||||
F19 = "F19",
|
||||
F20 = "F20",
|
||||
F21 = "F21",
|
||||
F22 = "F22",
|
||||
F23 = "F23",
|
||||
F24 = "F24",
|
||||
ControlLeft = "ControlLeft",
|
||||
ShiftLeft = "ShiftLeft",
|
||||
AltLeft = "AltLeft",
|
||||
MetaLeft = "MetaLeft",
|
||||
ControlRight = "ControlRight",
|
||||
ShiftRight = "ShiftRight",
|
||||
AltRight = "AltRight",
|
||||
MetaRight = "MetaRight",
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
// HID (Human Interface Device) type definitions
|
||||
// Shared between WebRTC DataChannel and WebSocket HID channels
|
||||
|
||||
import { type CanonicalKey } from '@/types/generated'
|
||||
import { canonicalKeyToHidUsage } from '@/lib/keyboardMappings'
|
||||
|
||||
/** Keyboard event for HID input */
|
||||
export interface HidKeyboardEvent {
|
||||
type: 'keydown' | 'keyup'
|
||||
key: number
|
||||
key: CanonicalKey
|
||||
/** Raw HID modifier byte (bit0..bit7 = LCtrl..RMeta) */
|
||||
modifier?: number
|
||||
}
|
||||
@@ -51,7 +54,7 @@ export function encodeKeyboardEvent(event: HidKeyboardEvent): ArrayBuffer {
|
||||
|
||||
view.setUint8(0, MSG_KEYBOARD)
|
||||
view.setUint8(1, event.type === 'keydown' ? KB_EVENT_DOWN : KB_EVENT_UP)
|
||||
view.setUint8(2, event.key & 0xff)
|
||||
view.setUint8(2, canonicalKeyToHidUsage(event.key) & 0xff)
|
||||
|
||||
view.setUint8(3, (event.modifier ?? 0) & 0xff)
|
||||
|
||||
|
||||
@@ -12,8 +12,9 @@ import { useWebRTC } from '@/composables/useWebRTC'
|
||||
import { useVideoSession } from '@/composables/useVideoSession'
|
||||
import { getUnifiedAudio } from '@/composables/useUnifiedAudio'
|
||||
import { streamApi, hidApi, atxApi, extensionsApi, atxConfigApi, authApi } from '@/api'
|
||||
import { CanonicalKey } from '@/types/generated'
|
||||
import type { HidKeyboardEvent, HidMouseEvent } from '@/types/hid'
|
||||
import { keyboardEventToHidCode, updateModifierMaskForHidKey } from '@/lib/keyboardMappings'
|
||||
import { keyboardEventToCanonicalKey, updateModifierMaskForKey } from '@/lib/keyboardMappings'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { generateUUID } from '@/lib/utils'
|
||||
import type { VideoMode } from '@/components/VideoConfigPopover.vue'
|
||||
@@ -117,7 +118,7 @@ const myClientId = generateUUID()
|
||||
|
||||
// HID state
|
||||
const mouseMode = ref<'absolute' | 'relative'>('absolute')
|
||||
const pressedKeys = ref<string[]>([])
|
||||
const pressedKeys = ref<CanonicalKey[]>([])
|
||||
const keyboardLed = ref({
|
||||
capsLock: false,
|
||||
})
|
||||
@@ -1539,7 +1540,7 @@ function handleHidError(_error: any, _operation: string) {
|
||||
}
|
||||
|
||||
// HID channel selection: use WebRTC DataChannel when available, fallback to WebSocket
|
||||
function sendKeyboardEvent(type: 'down' | 'up', key: number, modifier?: number) {
|
||||
function sendKeyboardEvent(type: 'down' | 'up', key: CanonicalKey, modifier?: number) {
|
||||
// In WebRTC mode with DataChannel ready, use DataChannel for lower latency
|
||||
if (videoMode.value !== 'mjpeg' && webrtc.dataChannelReady.value) {
|
||||
const event: HidKeyboardEvent = {
|
||||
@@ -1618,22 +1619,21 @@ function handleKeyDown(e: KeyboardEvent) {
|
||||
})
|
||||
}
|
||||
|
||||
const keyName = e.key === ' ' ? 'Space' : e.key
|
||||
if (!pressedKeys.value.includes(keyName)) {
|
||||
pressedKeys.value = [...pressedKeys.value, keyName]
|
||||
}
|
||||
|
||||
keyboardLed.value.capsLock = e.getModifierState('CapsLock')
|
||||
|
||||
const hidKey = keyboardEventToHidCode(e.code, e.key)
|
||||
if (hidKey === undefined) {
|
||||
const canonicalKey = keyboardEventToCanonicalKey(e.code, e.key)
|
||||
if (canonicalKey === undefined) {
|
||||
console.warn(`[HID] Unmapped key down: code=${e.code}, key=${e.key}`)
|
||||
return
|
||||
}
|
||||
|
||||
const modifierMask = updateModifierMaskForHidKey(activeModifierMask.value, hidKey, true)
|
||||
if (!pressedKeys.value.includes(canonicalKey)) {
|
||||
pressedKeys.value = [...pressedKeys.value, canonicalKey]
|
||||
}
|
||||
|
||||
const modifierMask = updateModifierMaskForKey(activeModifierMask.value, canonicalKey, true)
|
||||
activeModifierMask.value = modifierMask
|
||||
sendKeyboardEvent('down', hidKey, modifierMask)
|
||||
sendKeyboardEvent('down', canonicalKey, modifierMask)
|
||||
}
|
||||
|
||||
function handleKeyUp(e: KeyboardEvent) {
|
||||
@@ -1649,18 +1649,17 @@ function handleKeyUp(e: KeyboardEvent) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
const keyName = e.key === ' ' ? 'Space' : e.key
|
||||
pressedKeys.value = pressedKeys.value.filter(k => k !== keyName)
|
||||
|
||||
const hidKey = keyboardEventToHidCode(e.code, e.key)
|
||||
if (hidKey === undefined) {
|
||||
const canonicalKey = keyboardEventToCanonicalKey(e.code, e.key)
|
||||
if (canonicalKey === undefined) {
|
||||
console.warn(`[HID] Unmapped key up: code=${e.code}, key=${e.key}`)
|
||||
return
|
||||
}
|
||||
|
||||
const modifierMask = updateModifierMaskForHidKey(activeModifierMask.value, hidKey, false)
|
||||
pressedKeys.value = pressedKeys.value.filter(k => k !== canonicalKey)
|
||||
|
||||
const modifierMask = updateModifierMaskForKey(activeModifierMask.value, canonicalKey, false)
|
||||
activeModifierMask.value = modifierMask
|
||||
sendKeyboardEvent('up', hidKey, modifierMask)
|
||||
sendKeyboardEvent('up', canonicalKey, modifierMask)
|
||||
}
|
||||
|
||||
function getActiveVideoElement(): HTMLImageElement | HTMLVideoElement | null {
|
||||
@@ -2016,18 +2015,18 @@ function handleToggleVirtualKeyboard() {
|
||||
}
|
||||
|
||||
// Virtual keyboard key event handlers
|
||||
function handleVirtualKeyDown(key: string) {
|
||||
function handleVirtualKeyDown(key: CanonicalKey) {
|
||||
// Add to pressedKeys for InfoBar display
|
||||
if (!pressedKeys.value.includes(key)) {
|
||||
pressedKeys.value = [...pressedKeys.value, key]
|
||||
}
|
||||
// Toggle CapsLock state when virtual keyboard presses CapsLock
|
||||
if (key === 'CapsLock') {
|
||||
if (key === CanonicalKey.CapsLock) {
|
||||
keyboardLed.value.capsLock = !keyboardLed.value.capsLock
|
||||
}
|
||||
}
|
||||
|
||||
function handleVirtualKeyUp(key: string) {
|
||||
function handleVirtualKeyUp(key: CanonicalKey) {
|
||||
// Remove from pressedKeys
|
||||
pressedKeys.value = pressedKeys.value.filter(k => k !== key)
|
||||
}
|
||||
@@ -2485,6 +2484,8 @@ onUnmounted(() => {
|
||||
v-if="virtualKeyboardVisible"
|
||||
v-model:visible="virtualKeyboardVisible"
|
||||
v-model:attached="virtualKeyboardAttached"
|
||||
:caps-lock="keyboardLed.capsLock"
|
||||
:pressed-keys="pressedKeys"
|
||||
@key-down="handleVirtualKeyDown"
|
||||
@key-up="handleVirtualKeyUp"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user