mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-19 15:19:55 +00:00
Add QEMU Extended Key Event for layout-independent input
This commit is contained in:
@@ -352,6 +352,30 @@ func (m *MacInputInjector) InjectKey(keysym uint32, down bool) {
|
||||
if keycode == 0xFFFF {
|
||||
return
|
||||
}
|
||||
m.postMacKey(src, keycode, down)
|
||||
}
|
||||
|
||||
// InjectKeyScancode injects using the QEMU scancode, mapped via the
|
||||
// qemuToMacVK table to Apple's virtual-keycode space. Apple uses an
|
||||
// entirely different scheme from PC AT scancodes, so the table is the
|
||||
// authoritative bridge. On miss we fall back to the keysym path.
|
||||
func (m *MacInputInjector) InjectKeyScancode(scancode, keysym uint32, down bool) {
|
||||
wakeDisplay()
|
||||
src := ensureEventSource()
|
||||
if src == 0 {
|
||||
return
|
||||
}
|
||||
vk, ok := qemuToMacVK[scancode]
|
||||
if !ok {
|
||||
// Fall back to the keysym path so unmapped keys still work.
|
||||
m.InjectKey(keysym, down)
|
||||
return
|
||||
}
|
||||
m.postMacKey(src, vk, down)
|
||||
}
|
||||
|
||||
// postMacKey emits a single key down/up event via Core Graphics.
|
||||
func (m *MacInputInjector) postMacKey(src uintptr, keycode uint16, down bool) {
|
||||
event := cgEventCreateKeyboardEvent(src, keycode, down)
|
||||
if event == 0 {
|
||||
return
|
||||
|
||||
@@ -199,6 +199,27 @@ func (u *UInputInjector) InjectKey(keysym uint32, down bool) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
u.emitKeyCode(code, down)
|
||||
}
|
||||
|
||||
// InjectKeyScancode injects a press or release using the QEMU scancode.
|
||||
// uinput speaks Linux KEY_* codes natively, so we map QEMU scancode →
|
||||
// KEY_* via qemuToLinuxKey. On miss (scancode we don't have a mapping
|
||||
// for) we fall back to the keysym path, which is exactly the legacy
|
||||
// behaviour.
|
||||
func (u *UInputInjector) InjectKeyScancode(scancode, keysym uint32, down bool) {
|
||||
code := qemuScancodeToLinuxKey(scancode)
|
||||
if code == 0 {
|
||||
u.InjectKey(keysym, down)
|
||||
return
|
||||
}
|
||||
u.mu.Lock()
|
||||
defer u.mu.Unlock()
|
||||
u.emitKeyCode(uint16(code), down)
|
||||
}
|
||||
|
||||
// emitKeyCode emits one key down/up event plus a sync. Caller holds u.mu.
|
||||
func (u *UInputInjector) emitKeyCode(code uint16, down bool) {
|
||||
value := int32(0)
|
||||
if down {
|
||||
value = 1
|
||||
@@ -297,45 +318,8 @@ func (u *UInputInjector) Close() {
|
||||
})
|
||||
}
|
||||
|
||||
// Linux KEY_* codes for the small set we care about.
|
||||
const (
|
||||
keyEsc = 1
|
||||
keyMinus = 12
|
||||
keyEqual = 13
|
||||
keyBackspace = 14
|
||||
keyTab = 15
|
||||
keyEnter = 28
|
||||
keyLeftCtrl = 29
|
||||
keySemicolon = 39
|
||||
keyApostrophe = 40
|
||||
keyGrave = 41
|
||||
keyLeftShift = 42
|
||||
keyBackslash = 43
|
||||
keyComma = 51
|
||||
keyDot = 52
|
||||
keySlash = 53
|
||||
keyRightShift = 54
|
||||
keyLeftAlt = 56
|
||||
keySpace = 57
|
||||
keyCapsLock = 58
|
||||
keyF1 = 59
|
||||
keyLeftBracket = 26
|
||||
keyRightBracket = 27
|
||||
keyHome = 102
|
||||
keyUp = 103
|
||||
keyPageUp = 104
|
||||
keyLeft = 105
|
||||
keyRight = 106
|
||||
keyEnd = 107
|
||||
keyDown = 108
|
||||
keyPageDown = 109
|
||||
keyInsert = 110
|
||||
keyDelete = 111
|
||||
keyRightCtrl = 97
|
||||
keyRightAlt = 100
|
||||
keyLeftMeta = 125
|
||||
keyRightMeta = 126
|
||||
)
|
||||
// Linux KEY_* codes live in scancodes.go (shared with the QEMU scancode
|
||||
// path). Don't duplicate them here.
|
||||
|
||||
// buildUInputKeymap returns every linux KEY_ code we want the virtual
|
||||
// device to advertise during UI_SET_KEYBIT. Order doesn't matter.
|
||||
|
||||
@@ -106,9 +106,11 @@ const sasEventName = `Global\NetBirdVNC_SAS`
|
||||
|
||||
type inputCmd struct {
|
||||
isKey bool
|
||||
isScancode bool
|
||||
isClipboard bool
|
||||
isType bool
|
||||
keysym uint32
|
||||
scancode uint32
|
||||
down bool
|
||||
buttonMask uint8
|
||||
x, y int
|
||||
@@ -187,6 +189,8 @@ func (w *WindowsInputInjector) dispatch(cmd inputCmd) {
|
||||
w.doSetClipboard(cmd.clipText)
|
||||
case cmd.isType:
|
||||
w.typeUnicodeText(cmd.clipText)
|
||||
case cmd.isScancode:
|
||||
w.doInjectKeyScancode(cmd.scancode, cmd.keysym, cmd.down)
|
||||
case cmd.isKey:
|
||||
w.doInjectKey(cmd.keysym, cmd.down)
|
||||
default:
|
||||
@@ -199,6 +203,19 @@ func (w *WindowsInputInjector) InjectKey(keysym uint32, down bool) {
|
||||
w.tryEnqueue(inputCmd{isKey: true, keysym: keysym, down: down})
|
||||
}
|
||||
|
||||
// InjectKeyScancode queues a raw-scancode key event. PC AT Set 1 maps
|
||||
// directly onto what SendInput's KEYEVENTF_SCANCODE flag wants, so the
|
||||
// only translation is splitting the optional 0xE0 prefix off into the
|
||||
// KEYEVENTF_EXTENDEDKEY flag. keysym is the noVNC-provided fallback we
|
||||
// reach for if the scancode is zero.
|
||||
func (w *WindowsInputInjector) InjectKeyScancode(scancode uint32, keysym uint32, down bool) {
|
||||
if scancode == 0 {
|
||||
w.InjectKey(keysym, down)
|
||||
return
|
||||
}
|
||||
w.tryEnqueue(inputCmd{isScancode: true, scancode: scancode, keysym: keysym, down: down})
|
||||
}
|
||||
|
||||
// InjectPointer queues a pointer event for injection on the input desktop
|
||||
// thread. Pointer events coalesce: when the channel is full (slow desktop
|
||||
// switch, hung SendInput), drop the new sample so the read loop never
|
||||
@@ -207,6 +224,32 @@ func (w *WindowsInputInjector) InjectPointer(buttonMask uint8, x, y, serverW, se
|
||||
w.tryEnqueue(inputCmd{buttonMask: buttonMask, x: x, y: y, serverW: serverW, serverH: serverH})
|
||||
}
|
||||
|
||||
// doInjectKeyScancode injects a key event using the QEMU scancode directly,
|
||||
// bypassing the keysym→VK lookup. Windows accepts PC AT Set 1 scancodes
|
||||
// natively via KEYEVENTF_SCANCODE, so the only work is splitting the
|
||||
// optional 0xE0 prefix off into the EXTENDEDKEY flag and tracking
|
||||
// modifier state for the SAS Ctrl+Alt+Del shortcut.
|
||||
func (w *WindowsInputInjector) doInjectKeyScancode(scancode, keysym uint32, down bool) {
|
||||
switch keysym {
|
||||
case 0xffe3, 0xffe4:
|
||||
w.ctrlDown = down
|
||||
case 0xffe9, 0xffea:
|
||||
w.altDown = down
|
||||
}
|
||||
if (keysym == 0xff9f || keysym == 0xffff) && w.ctrlDown && w.altDown && down {
|
||||
signalSAS()
|
||||
return
|
||||
}
|
||||
flags := uint32(keyeventfScanCode)
|
||||
if !down {
|
||||
flags |= keyeventfKeyUp
|
||||
}
|
||||
if qemuScancodeIsExtended(scancode) {
|
||||
flags |= keyeventfExtendedKey
|
||||
}
|
||||
sendKeyInput(0, qemuScancodeLowByte(scancode), flags)
|
||||
}
|
||||
|
||||
func (w *WindowsInputInjector) doInjectKey(keysym uint32, down bool) {
|
||||
switch keysym {
|
||||
case 0xffe3, 0xffe4:
|
||||
|
||||
@@ -74,14 +74,38 @@ func (x *X11InputInjector) InjectKey(keysym uint32, down bool) {
|
||||
if keycode == 0 {
|
||||
return
|
||||
}
|
||||
x.fakeKeyEvent(keycode, down)
|
||||
}
|
||||
|
||||
// InjectKeyScancode injects using the QEMU scancode by translating to a
|
||||
// Linux KEY_ code and then to an X11 keycode (KEY_* + xkbKeycodeOffset).
|
||||
// On a server running a standard XKB keymap this is layout-independent:
|
||||
// the scancode names the physical key, the server's layout determines the
|
||||
// resulting character. Falls back to the keysym path when the scancode
|
||||
// has no Linux mapping.
|
||||
func (x *X11InputInjector) InjectKeyScancode(scancode, keysym uint32, down bool) {
|
||||
linuxKey := qemuScancodeToLinuxKey(scancode)
|
||||
if linuxKey == 0 {
|
||||
x.InjectKey(keysym, down)
|
||||
return
|
||||
}
|
||||
x.fakeKeyEvent(byte(linuxKey+xkbKeycodeOffset), down)
|
||||
}
|
||||
|
||||
// xkbKeycodeOffset is the per-server constant offset between Linux KEY_*
|
||||
// event codes and the X server's keycode space under XKB. The X protocol
|
||||
// reserves keycodes 0..7 for internal use, so any normal XKB keymap
|
||||
// starts at 8 (KEY_ESC=1 → X keycode 9, KEY_A=30 → X keycode 38, etc.).
|
||||
const xkbKeycodeOffset = 8
|
||||
|
||||
// fakeKeyEvent sends an XTest FakeInput for a press or release.
|
||||
func (x *X11InputInjector) fakeKeyEvent(keycode byte, down bool) {
|
||||
var eventType byte
|
||||
if down {
|
||||
eventType = xproto.KeyPress
|
||||
} else {
|
||||
eventType = xproto.KeyRelease
|
||||
}
|
||||
|
||||
xtest.FakeInput(x.conn, eventType, keycode, 0, x.root, 0, 0, 0)
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,14 @@ const (
|
||||
clientKeyEvent = 4
|
||||
clientPointerEvent = 5
|
||||
clientCutText = 6
|
||||
// clientQEMUMessage is the QEMU vendor message wrapper. The subtype
|
||||
// byte that follows selects the actual operation; we only handle the
|
||||
// Extended Key Event (subtype 0) which carries a hardware scancode in
|
||||
// addition to the X11 keysym. Layout-independent key entry.
|
||||
clientQEMUMessage = 255
|
||||
|
||||
// QEMU Extended Key Event subtype carried inside clientQEMUMessage.
|
||||
qemuSubtypeExtendedKeyEvent = 0
|
||||
|
||||
// clientNetbirdTypeText is a NetBird-specific message that asks the
|
||||
// server to synthesize the given text as keystrokes regardless of the
|
||||
@@ -53,10 +61,11 @@ const (
|
||||
// Pseudo-encodings carried over wire as rects with a negative
|
||||
// encoding value. The client advertises supported optional protocol
|
||||
// extensions by listing these in SetEncodings.
|
||||
pseudoEncDesktopSize = -223
|
||||
pseudoEncLastRect = -224
|
||||
pseudoEncDesktopName = -307
|
||||
pseudoEncExtendedDesktopSize = -308
|
||||
pseudoEncDesktopSize = -223
|
||||
pseudoEncLastRect = -224
|
||||
pseudoEncQEMUExtendedKeyEvent = -258
|
||||
pseudoEncDesktopName = -307
|
||||
pseudoEncExtendedDesktopSize = -308
|
||||
|
||||
// Tight compression-control byte top nibble. Stream-reset bits 0-3
|
||||
// (one per zlib stream) are unused while we run a single stream.
|
||||
|
||||
272
client/vnc/server/scancodes.go
Normal file
272
client/vnc/server/scancodes.go
Normal file
@@ -0,0 +1,272 @@
|
||||
package server
|
||||
|
||||
// QEMU Extended Key Event carries hardware scancodes encoded as PC AT Set 1.
|
||||
// Single-byte codes cover the standard keys; the "extended" prefix 0xE0 is
|
||||
// merged into the high byte (so 0xE048 is the extended-Up arrow). This file
|
||||
// translates those scancodes into the per-platform identifiers each input
|
||||
// backend wants:
|
||||
//
|
||||
// - Linux uinput wants Linux KEY_* codes (defined in
|
||||
// linux/input-event-codes.h). uinput is what we use for virtual Xvfb
|
||||
// sessions on Linux.
|
||||
// - X11 XTest wants XKB keycodes, which on a standard layout equal
|
||||
// Linux KEY_* + 8 (the per-server offset between the Linux event code
|
||||
// and the X server's keycode space).
|
||||
// - Windows SendInput accepts the PC AT scancode directly via
|
||||
// KEYEVENTF_SCANCODE, so no mapping table is needed there; the
|
||||
// extended-key bit is set when the QEMU scancode high byte is 0xE0.
|
||||
// - macOS CGEventCreateKeyboardEvent takes a "virtual keycode" from
|
||||
// Apple's HID set, which is unrelated to PC AT and needs its own
|
||||
// table (see qemuToMacVK in input_darwin.go).
|
||||
//
|
||||
// Linux KEY_* codes. Only the ones we reference, since the full
|
||||
// linux/input-event-codes.h list isn't useful here. Naming mirrors the
|
||||
// existing constants in input_uinput_unix.go (mixed case, no underscores).
|
||||
const (
|
||||
keyEsc = 1
|
||||
key1 = 2
|
||||
key2 = 3
|
||||
key3 = 4
|
||||
key4 = 5
|
||||
key5 = 6
|
||||
key6 = 7
|
||||
key7 = 8
|
||||
key8 = 9
|
||||
key9 = 10
|
||||
key0 = 11
|
||||
keyMinus = 12
|
||||
keyEqual = 13
|
||||
keyBackspace = 14
|
||||
keyTab = 15
|
||||
keyQ = 16
|
||||
keyW = 17
|
||||
keyE = 18
|
||||
keyR = 19
|
||||
keyT = 20
|
||||
keyY = 21
|
||||
keyU = 22
|
||||
keyI = 23
|
||||
keyO = 24
|
||||
keyP = 25
|
||||
keyLeftBracket = 26
|
||||
keyRightBracket = 27
|
||||
keyEnter = 28
|
||||
keyLeftCtrl = 29
|
||||
keyA = 30
|
||||
keyS = 31
|
||||
keyD = 32
|
||||
keyF = 33
|
||||
keyG = 34
|
||||
keyH = 35
|
||||
keyJ = 36
|
||||
keyK = 37
|
||||
keyL = 38
|
||||
keySemicolon = 39
|
||||
keyApostrophe = 40
|
||||
keyGrave = 41
|
||||
keyLeftShift = 42
|
||||
keyBackslash = 43
|
||||
keyZ = 44
|
||||
keyX = 45
|
||||
keyC = 46
|
||||
keyV = 47
|
||||
keyB = 48
|
||||
keyN = 49
|
||||
keyM = 50
|
||||
keyComma = 51
|
||||
keyDot = 52
|
||||
keySlash = 53
|
||||
keyRightShift = 54
|
||||
keyKPAsterisk = 55
|
||||
keyLeftAlt = 56
|
||||
keySpace = 57
|
||||
keyCapsLock = 58
|
||||
keyF1 = 59
|
||||
keyF2 = 60
|
||||
keyF3 = 61
|
||||
keyF4 = 62
|
||||
keyF5 = 63
|
||||
keyF6 = 64
|
||||
keyF7 = 65
|
||||
keyF8 = 66
|
||||
keyF9 = 67
|
||||
keyF10 = 68
|
||||
keyNumLock = 69
|
||||
keyScrollLock = 70
|
||||
keyKP7 = 71
|
||||
keyKP8 = 72
|
||||
keyKP9 = 73
|
||||
keyKPMinus = 74
|
||||
keyKP4 = 75
|
||||
keyKP5 = 76
|
||||
keyKP6 = 77
|
||||
keyKPPlus = 78
|
||||
keyKP1 = 79
|
||||
keyKP2 = 80
|
||||
keyKP3 = 81
|
||||
keyKP0 = 82
|
||||
keyKPDot = 83
|
||||
key102nd = 86
|
||||
keyF11 = 87
|
||||
keyF12 = 88
|
||||
keyKPEnter = 96
|
||||
keyRightCtrl = 97
|
||||
keyKPSlash = 98
|
||||
keySysRq = 99
|
||||
keyRightAlt = 100
|
||||
keyHome = 102
|
||||
keyUp = 103
|
||||
keyPageUp = 104
|
||||
keyLeft = 105
|
||||
keyRight = 106
|
||||
keyEnd = 107
|
||||
keyDown = 108
|
||||
keyPageDown = 109
|
||||
keyInsert = 110
|
||||
keyDelete = 111
|
||||
keyMute = 113
|
||||
keyVolumeDown = 114
|
||||
keyVolumeUp = 115
|
||||
keyLeftMeta = 125
|
||||
keyRightMeta = 126
|
||||
keyCompose = 127
|
||||
)
|
||||
|
||||
// qemuToLinuxKey maps the PC AT Set 1 scancode QEMU sends to a Linux KEY_*
|
||||
// code. The high byte 0xE0 marks "extended" scancodes (arrows, the right-
|
||||
// side modifier keys, keypad enter/divide, browser keys, etc.).
|
||||
//
|
||||
// Keep this table dense so a reviewer sees the whole keyboard at a glance,
|
||||
// and so adding a new key is a single line.
|
||||
var qemuToLinuxKey = map[uint32]int{
|
||||
// Single-byte (non-extended) scancodes.
|
||||
0x01: keyEsc,
|
||||
0x02: key1,
|
||||
0x03: key2,
|
||||
0x04: key3,
|
||||
0x05: key4,
|
||||
0x06: key5,
|
||||
0x07: key6,
|
||||
0x08: key7,
|
||||
0x09: key8,
|
||||
0x0A: key9,
|
||||
0x0B: key0,
|
||||
0x0C: keyMinus,
|
||||
0x0D: keyEqual,
|
||||
0x0E: keyBackspace,
|
||||
0x0F: keyTab,
|
||||
0x10: keyQ,
|
||||
0x11: keyW,
|
||||
0x12: keyE,
|
||||
0x13: keyR,
|
||||
0x14: keyT,
|
||||
0x15: keyY,
|
||||
0x16: keyU,
|
||||
0x17: keyI,
|
||||
0x18: keyO,
|
||||
0x19: keyP,
|
||||
0x1A: keyLeftBracket,
|
||||
0x1B: keyRightBracket,
|
||||
0x1C: keyEnter,
|
||||
0x1D: keyLeftCtrl,
|
||||
0x1E: keyA,
|
||||
0x1F: keyS,
|
||||
0x20: keyD,
|
||||
0x21: keyF,
|
||||
0x22: keyG,
|
||||
0x23: keyH,
|
||||
0x24: keyJ,
|
||||
0x25: keyK,
|
||||
0x26: keyL,
|
||||
0x27: keySemicolon,
|
||||
0x28: keyApostrophe,
|
||||
0x29: keyGrave,
|
||||
0x2A: keyLeftShift,
|
||||
0x2B: keyBackslash,
|
||||
0x2C: keyZ,
|
||||
0x2D: keyX,
|
||||
0x2E: keyC,
|
||||
0x2F: keyV,
|
||||
0x30: keyB,
|
||||
0x31: keyN,
|
||||
0x32: keyM,
|
||||
0x33: keyComma,
|
||||
0x34: keyDot,
|
||||
0x35: keySlash,
|
||||
0x36: keyRightShift,
|
||||
0x37: keyKPAsterisk,
|
||||
0x38: keyLeftAlt,
|
||||
0x39: keySpace,
|
||||
0x3A: keyCapsLock,
|
||||
0x3B: keyF1,
|
||||
0x3C: keyF2,
|
||||
0x3D: keyF3,
|
||||
0x3E: keyF4,
|
||||
0x3F: keyF5,
|
||||
0x40: keyF6,
|
||||
0x41: keyF7,
|
||||
0x42: keyF8,
|
||||
0x43: keyF9,
|
||||
0x44: keyF10,
|
||||
0x45: keyNumLock,
|
||||
0x46: keyScrollLock,
|
||||
0x47: keyKP7,
|
||||
0x48: keyKP8,
|
||||
0x49: keyKP9,
|
||||
0x4A: keyKPMinus,
|
||||
0x4B: keyKP4,
|
||||
0x4C: keyKP5,
|
||||
0x4D: keyKP6,
|
||||
0x4E: keyKPPlus,
|
||||
0x4F: keyKP1,
|
||||
0x50: keyKP2,
|
||||
0x51: keyKP3,
|
||||
0x52: keyKP0,
|
||||
0x53: keyKPDot,
|
||||
0x56: key102nd,
|
||||
0x57: keyF11,
|
||||
0x58: keyF12,
|
||||
|
||||
// Extended (0xE0-prefixed) scancodes.
|
||||
0xE01C: keyKPEnter,
|
||||
0xE01D: keyRightCtrl,
|
||||
0xE020: keyMute,
|
||||
0xE02E: keyVolumeDown,
|
||||
0xE030: keyVolumeUp,
|
||||
0xE035: keyKPSlash,
|
||||
0xE037: keySysRq, // PrintScreen
|
||||
0xE038: keyRightAlt,
|
||||
0xE047: keyHome,
|
||||
0xE048: keyUp,
|
||||
0xE049: keyPageUp,
|
||||
0xE04B: keyLeft,
|
||||
0xE04D: keyRight,
|
||||
0xE04F: keyEnd,
|
||||
0xE050: keyDown,
|
||||
0xE051: keyPageDown,
|
||||
0xE052: keyInsert,
|
||||
0xE053: keyDelete,
|
||||
0xE05B: keyLeftMeta,
|
||||
0xE05C: keyRightMeta,
|
||||
0xE05D: keyCompose,
|
||||
}
|
||||
|
||||
// qemuScancodeToLinuxKey is the lookup the uinput and X11 paths use.
|
||||
// Returns 0 (which Linux treats as KEY_RESERVED) when the scancode has no
|
||||
// mapping, signalling "fall back to the keysym path".
|
||||
func qemuScancodeToLinuxKey(scancode uint32) int {
|
||||
return qemuToLinuxKey[scancode]
|
||||
}
|
||||
|
||||
// qemuScancodeIsExtended reports whether a QEMU scancode is in the
|
||||
// 0xE0-prefixed extended range. Used by Windows SendInput to set the
|
||||
// KEYEVENTF_EXTENDEDKEY flag.
|
||||
func qemuScancodeIsExtended(scancode uint32) bool {
|
||||
return scancode&0xFF00 == 0xE000
|
||||
}
|
||||
|
||||
// qemuScancodeLowByte returns the byte SendInput's wScan field actually
|
||||
// stores: the low byte of the scancode regardless of any extended prefix.
|
||||
func qemuScancodeLowByte(scancode uint32) uint16 {
|
||||
return uint16(scancode & 0xFF)
|
||||
}
|
||||
238
client/vnc/server/scancodes_darwin.go
Normal file
238
client/vnc/server/scancodes_darwin.go
Normal file
@@ -0,0 +1,238 @@
|
||||
//go:build darwin && !ios
|
||||
|
||||
package server
|
||||
|
||||
// Apple keyboard virtual-key codes used with CGEventCreateKeyboardEvent.
|
||||
// These are the kVK_ANSI_* / kVK_* values from Apple's
|
||||
// HIToolbox/Events.h; reproduced here so we don't need to drag in the
|
||||
// HIToolbox framework just for the constants.
|
||||
const (
|
||||
macKeyA uint16 = 0x00
|
||||
macKeyS uint16 = 0x01
|
||||
macKeyD uint16 = 0x02
|
||||
macKeyF uint16 = 0x03
|
||||
macKeyH uint16 = 0x04
|
||||
macKeyG uint16 = 0x05
|
||||
macKeyZ uint16 = 0x06
|
||||
macKeyX uint16 = 0x07
|
||||
macKeyC uint16 = 0x08
|
||||
macKeyV uint16 = 0x09
|
||||
macKeyNonUSBackslash uint16 = 0x0A // ISO_Section / 102nd
|
||||
macKeyB uint16 = 0x0B
|
||||
macKeyQ uint16 = 0x0C
|
||||
macKeyW uint16 = 0x0D
|
||||
macKeyE uint16 = 0x0E
|
||||
macKeyR uint16 = 0x0F
|
||||
macKeyY uint16 = 0x10
|
||||
macKeyT uint16 = 0x11
|
||||
macKey1 uint16 = 0x12
|
||||
macKey2 uint16 = 0x13
|
||||
macKey3 uint16 = 0x14
|
||||
macKey4 uint16 = 0x15
|
||||
macKey6 uint16 = 0x16
|
||||
macKey5 uint16 = 0x17
|
||||
macKeyEqual uint16 = 0x18
|
||||
macKey9 uint16 = 0x19
|
||||
macKey7 uint16 = 0x1A
|
||||
macKeyMinus uint16 = 0x1B
|
||||
macKey8 uint16 = 0x1C
|
||||
macKey0 uint16 = 0x1D
|
||||
macKeyRightBracket uint16 = 0x1E
|
||||
macKeyO uint16 = 0x1F
|
||||
macKeyU uint16 = 0x20
|
||||
macKeyLeftBracket uint16 = 0x21
|
||||
macKeyI uint16 = 0x22
|
||||
macKeyP uint16 = 0x23
|
||||
macKeyReturn uint16 = 0x24
|
||||
macKeyL uint16 = 0x25
|
||||
macKeyJ uint16 = 0x26
|
||||
macKeyApostrophe uint16 = 0x27
|
||||
macKeyK uint16 = 0x28
|
||||
macKeySemicolon uint16 = 0x29
|
||||
macKeyBackslash uint16 = 0x2A
|
||||
macKeyComma uint16 = 0x2B
|
||||
macKeySlash uint16 = 0x2C
|
||||
macKeyN uint16 = 0x2D
|
||||
macKeyM uint16 = 0x2E
|
||||
macKeyPeriod uint16 = 0x2F
|
||||
macKeyTab uint16 = 0x30
|
||||
macKeySpace uint16 = 0x31
|
||||
macKeyGrave uint16 = 0x32
|
||||
macKeyDelete uint16 = 0x33 // Backspace
|
||||
macKeyEscape uint16 = 0x35
|
||||
macKeyCommand uint16 = 0x37
|
||||
macKeyShift uint16 = 0x38
|
||||
macKeyCapsLock uint16 = 0x39
|
||||
macKeyOption uint16 = 0x3A // Alt
|
||||
macKeyControl uint16 = 0x3B
|
||||
macKeyRightShift uint16 = 0x3C
|
||||
macKeyRightOption uint16 = 0x3D
|
||||
macKeyRightControl uint16 = 0x3E
|
||||
macKeyFunction uint16 = 0x3F
|
||||
macKeyF17 uint16 = 0x40
|
||||
macKeyKPDecimal uint16 = 0x41
|
||||
macKeyKPMultiply uint16 = 0x43
|
||||
macKeyKPPlus uint16 = 0x45
|
||||
macKeyKPClear uint16 = 0x47 // numlock
|
||||
macKeyVolumeUp uint16 = 0x48
|
||||
macKeyVolumeDown uint16 = 0x49
|
||||
macKeyMute uint16 = 0x4A
|
||||
macKeyKPDivide uint16 = 0x4B
|
||||
macKeyKPEnter uint16 = 0x4C
|
||||
macKeyKPMinus uint16 = 0x4E
|
||||
macKeyF18 uint16 = 0x4F
|
||||
macKeyF19 uint16 = 0x50
|
||||
macKeyKPEqual uint16 = 0x51
|
||||
macKeyKP0 uint16 = 0x52
|
||||
macKeyKP1 uint16 = 0x53
|
||||
macKeyKP2 uint16 = 0x54
|
||||
macKeyKP3 uint16 = 0x55
|
||||
macKeyKP4 uint16 = 0x56
|
||||
macKeyKP5 uint16 = 0x57
|
||||
macKeyKP6 uint16 = 0x58
|
||||
macKeyKP7 uint16 = 0x59
|
||||
macKeyF20 uint16 = 0x5A
|
||||
macKeyKP8 uint16 = 0x5B
|
||||
macKeyKP9 uint16 = 0x5C
|
||||
macKeyF5 uint16 = 0x60
|
||||
macKeyF6 uint16 = 0x61
|
||||
macKeyF7 uint16 = 0x62
|
||||
macKeyF3 uint16 = 0x63
|
||||
macKeyF8 uint16 = 0x64
|
||||
macKeyF9 uint16 = 0x65
|
||||
macKeyF11 uint16 = 0x67
|
||||
macKeyF13 uint16 = 0x69 // PrintScreen on most layouts
|
||||
macKeyF16 uint16 = 0x6A
|
||||
macKeyF14 uint16 = 0x6B
|
||||
macKeyF10 uint16 = 0x6D
|
||||
macKeyF12 uint16 = 0x6F
|
||||
macKeyF15 uint16 = 0x71
|
||||
macKeyHelp uint16 = 0x72 // Insert on PC keyboards
|
||||
macKeyHome uint16 = 0x73
|
||||
macKeyPageUp uint16 = 0x74
|
||||
macKeyForwardDelete uint16 = 0x75
|
||||
macKeyF4 uint16 = 0x76
|
||||
macKeyEnd uint16 = 0x77
|
||||
macKeyF2 uint16 = 0x78
|
||||
macKeyPageDown uint16 = 0x79
|
||||
macKeyF1 uint16 = 0x7A
|
||||
macKeyLeft uint16 = 0x7B
|
||||
macKeyRight uint16 = 0x7C
|
||||
macKeyDown uint16 = 0x7D
|
||||
macKeyUp uint16 = 0x7E
|
||||
)
|
||||
|
||||
// qemuToMacVK maps PC AT Set 1 scancodes (as QEMU emits them, with the
|
||||
// 0xE0 prefix merged into the high byte) onto Apple virtual-key codes.
|
||||
// Layout-independent: the scancode names the physical key, the user's
|
||||
// active keyboard layout on the Mac decides what the key produces.
|
||||
var qemuToMacVK = map[uint32]uint16{
|
||||
// Single-byte (non-extended).
|
||||
0x01: macKeyEscape,
|
||||
0x02: macKey1,
|
||||
0x03: macKey2,
|
||||
0x04: macKey3,
|
||||
0x05: macKey4,
|
||||
0x06: macKey5,
|
||||
0x07: macKey6,
|
||||
0x08: macKey7,
|
||||
0x09: macKey8,
|
||||
0x0A: macKey9,
|
||||
0x0B: macKey0,
|
||||
0x0C: macKeyMinus,
|
||||
0x0D: macKeyEqual,
|
||||
0x0E: macKeyDelete, // PC Backspace -> mac "Delete"
|
||||
0x0F: macKeyTab,
|
||||
0x10: macKeyQ,
|
||||
0x11: macKeyW,
|
||||
0x12: macKeyE,
|
||||
0x13: macKeyR,
|
||||
0x14: macKeyT,
|
||||
0x15: macKeyY,
|
||||
0x16: macKeyU,
|
||||
0x17: macKeyI,
|
||||
0x18: macKeyO,
|
||||
0x19: macKeyP,
|
||||
0x1A: macKeyLeftBracket,
|
||||
0x1B: macKeyRightBracket,
|
||||
0x1C: macKeyReturn,
|
||||
0x1D: macKeyControl,
|
||||
0x1E: macKeyA,
|
||||
0x1F: macKeyS,
|
||||
0x20: macKeyD,
|
||||
0x21: macKeyF,
|
||||
0x22: macKeyG,
|
||||
0x23: macKeyH,
|
||||
0x24: macKeyJ,
|
||||
0x25: macKeyK,
|
||||
0x26: macKeyL,
|
||||
0x27: macKeySemicolon,
|
||||
0x28: macKeyApostrophe,
|
||||
0x29: macKeyGrave,
|
||||
0x2A: macKeyShift,
|
||||
0x2B: macKeyBackslash,
|
||||
0x2C: macKeyZ,
|
||||
0x2D: macKeyX,
|
||||
0x2E: macKeyC,
|
||||
0x2F: macKeyV,
|
||||
0x30: macKeyB,
|
||||
0x31: macKeyN,
|
||||
0x32: macKeyM,
|
||||
0x33: macKeyComma,
|
||||
0x34: macKeyPeriod,
|
||||
0x35: macKeySlash,
|
||||
0x36: macKeyRightShift,
|
||||
0x37: macKeyKPMultiply,
|
||||
0x38: macKeyOption, // Left Alt -> Option
|
||||
0x39: macKeySpace,
|
||||
0x3A: macKeyCapsLock,
|
||||
0x3B: macKeyF1,
|
||||
0x3C: macKeyF2,
|
||||
0x3D: macKeyF3,
|
||||
0x3E: macKeyF4,
|
||||
0x3F: macKeyF5,
|
||||
0x40: macKeyF6,
|
||||
0x41: macKeyF7,
|
||||
0x42: macKeyF8,
|
||||
0x43: macKeyF9,
|
||||
0x44: macKeyF10,
|
||||
0x45: macKeyKPClear, // PC NumLock -> mac Clear
|
||||
0x47: macKeyKP7,
|
||||
0x48: macKeyKP8,
|
||||
0x49: macKeyKP9,
|
||||
0x4A: macKeyKPMinus,
|
||||
0x4B: macKeyKP4,
|
||||
0x4C: macKeyKP5,
|
||||
0x4D: macKeyKP6,
|
||||
0x4E: macKeyKPPlus,
|
||||
0x4F: macKeyKP1,
|
||||
0x50: macKeyKP2,
|
||||
0x51: macKeyKP3,
|
||||
0x52: macKeyKP0,
|
||||
0x53: macKeyKPDecimal,
|
||||
0x56: macKeyNonUSBackslash,
|
||||
0x57: macKeyF11,
|
||||
0x58: macKeyF12,
|
||||
|
||||
// Extended (0xE0 prefix).
|
||||
0xE01C: macKeyKPEnter,
|
||||
0xE01D: macKeyRightControl,
|
||||
0xE020: macKeyMute,
|
||||
0xE02E: macKeyVolumeDown,
|
||||
0xE030: macKeyVolumeUp,
|
||||
0xE035: macKeyKPDivide,
|
||||
0xE037: macKeyF13, // PrintScreen
|
||||
0xE038: macKeyRightOption,
|
||||
0xE047: macKeyHome,
|
||||
0xE048: macKeyUp,
|
||||
0xE049: macKeyPageUp,
|
||||
0xE04B: macKeyLeft,
|
||||
0xE04D: macKeyRight,
|
||||
0xE04F: macKeyEnd,
|
||||
0xE050: macKeyDown,
|
||||
0xE051: macKeyPageDown,
|
||||
0xE052: macKeyHelp, // PC Insert -> mac Help
|
||||
0xE053: macKeyForwardDelete,
|
||||
0xE05B: macKeyCommand, // Left Windows -> Command
|
||||
0xE05C: macKeyCommand, // Right Windows -> Command (no separate code)
|
||||
}
|
||||
98
client/vnc/server/scancodes_test.go
Normal file
98
client/vnc/server/scancodes_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package server
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestQemuScancodeToLinuxKey_KnownLetters(t *testing.T) {
|
||||
// Spot-check a few familiar letter keys against the Linux KEY_*
|
||||
// values they're supposed to land on.
|
||||
tests := []struct {
|
||||
name string
|
||||
scancode uint32
|
||||
want int
|
||||
}{
|
||||
{"A", 0x1E, keyA},
|
||||
{"S", 0x1F, keyS},
|
||||
{"D", 0x20, keyD},
|
||||
{"Q", 0x10, keyQ},
|
||||
{"Z", 0x2C, keyZ},
|
||||
{"1", 0x02, key1},
|
||||
{"Esc", 0x01, keyEsc},
|
||||
{"Tab", 0x0F, keyTab},
|
||||
{"Space", 0x39, keySpace},
|
||||
{"LeftShift", 0x2A, keyLeftShift},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
got := qemuScancodeToLinuxKey(tc.scancode)
|
||||
if got != tc.want {
|
||||
t.Errorf("%s: scancode 0x%X => %d, want %d", tc.name, tc.scancode, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestQemuScancodeToLinuxKey_Extended(t *testing.T) {
|
||||
// Extended (0xE0-prefixed) scancodes for arrow + navigation cluster.
|
||||
tests := []struct {
|
||||
name string
|
||||
scancode uint32
|
||||
want int
|
||||
}{
|
||||
{"Up", 0xE048, keyUp},
|
||||
{"Down", 0xE050, keyDown},
|
||||
{"Left", 0xE04B, keyLeft},
|
||||
{"Right", 0xE04D, keyRight},
|
||||
{"Home", 0xE047, keyHome},
|
||||
{"End", 0xE04F, keyEnd},
|
||||
{"PageUp", 0xE049, keyPageUp},
|
||||
{"PageDown", 0xE051, keyPageDown},
|
||||
{"Insert", 0xE052, keyInsert},
|
||||
{"Delete", 0xE053, keyDelete},
|
||||
{"RightCtrl", 0xE01D, keyRightCtrl},
|
||||
{"RightAlt", 0xE038, keyRightAlt},
|
||||
{"KPEnter", 0xE01C, keyKPEnter},
|
||||
{"KPSlash", 0xE035, keyKPSlash},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
got := qemuScancodeToLinuxKey(tc.scancode)
|
||||
if got != tc.want {
|
||||
t.Errorf("%s: scancode 0x%X => %d, want %d", tc.name, tc.scancode, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestQemuScancodeToLinuxKey_Miss(t *testing.T) {
|
||||
// 0xE0FF is in the extended range but not a real key. Must return 0
|
||||
// so the caller can fall back to the keysym path.
|
||||
if got := qemuScancodeToLinuxKey(0xE0FF); got != 0 {
|
||||
t.Errorf("unknown scancode should miss: got %d, want 0", got)
|
||||
}
|
||||
if got := qemuScancodeToLinuxKey(0xFF); got != 0 {
|
||||
t.Errorf("unknown non-extended scancode should miss: got %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQemuScancodeIsExtended(t *testing.T) {
|
||||
cases := []struct {
|
||||
scancode uint32
|
||||
want bool
|
||||
}{
|
||||
{0x1E, false},
|
||||
{0xE048, true},
|
||||
{0xE000, true},
|
||||
{0xFF, false},
|
||||
{0xE0FF, true},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := qemuScancodeIsExtended(tc.scancode); got != tc.want {
|
||||
t.Errorf("isExtended(0x%X) = %v, want %v", tc.scancode, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestQemuScancodeLowByte(t *testing.T) {
|
||||
if got := qemuScancodeLowByte(0xE048); got != 0x48 {
|
||||
t.Errorf("lowByte(0xE048) = 0x%X, want 0x48", got)
|
||||
}
|
||||
if got := qemuScancodeLowByte(0x1E); got != 0x1E {
|
||||
t.Errorf("lowByte(0x1E) = 0x%X, want 0x1E", got)
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,13 @@ var errFrameUnchanged = errors.New("frame unchanged")
|
||||
type InputInjector interface {
|
||||
// InjectKey simulates a key press or release. keysym is an X11 KeySym.
|
||||
InjectKey(keysym uint32, down bool)
|
||||
// InjectKeyScancode simulates a key press or release using the QEMU
|
||||
// scancode (PC AT set 1, high byte 0xE0 for extended keys). Layout-
|
||||
// independent: the server's local keyboard layout decides what
|
||||
// character the key produces. Implementations should fall back to
|
||||
// InjectKey(keysym, down) when they don't have a scancode mapping
|
||||
// for the given code; that's strictly no worse than the legacy path.
|
||||
InjectKeyScancode(scancode uint32, keysym uint32, down bool)
|
||||
// InjectPointer simulates mouse movement and button state.
|
||||
InjectPointer(buttonMask uint8, x, y, serverW, serverH int)
|
||||
// SetClipboard sets the system clipboard to the given text.
|
||||
|
||||
@@ -57,6 +57,7 @@ type session struct {
|
||||
clientSupportsExtendedDesktopSize bool
|
||||
clientSupportsDesktopName bool
|
||||
clientSupportsLastRect bool
|
||||
clientSupportsQEMUKey bool
|
||||
// prevFrame, curFrame and idleFrames live on the encoder goroutine and
|
||||
// must not be touched elsewhere. curFrame holds a session-owned copy of
|
||||
// the capturer's latest frame so the encoder works on a stable buffer
|
||||
@@ -241,6 +242,8 @@ func (s *session) messageLoop() error {
|
||||
err = s.handlePointerEvent()
|
||||
case clientCutText:
|
||||
err = s.handleCutText()
|
||||
case clientQEMUMessage:
|
||||
err = s.handleQEMUMessage()
|
||||
case clientNetbirdTypeText:
|
||||
err = s.handleTypeText()
|
||||
default:
|
||||
@@ -311,6 +314,9 @@ func (s *session) handleSetEncodings() error {
|
||||
case pseudoEncLastRect:
|
||||
s.clientSupportsLastRect = true
|
||||
encs = append(encs, "last-rect")
|
||||
case pseudoEncQEMUExtendedKeyEvent:
|
||||
s.clientSupportsQEMUKey = true
|
||||
encs = append(encs, "qemu-key")
|
||||
case encTight:
|
||||
s.useTight = true
|
||||
if s.tight == nil {
|
||||
@@ -749,6 +755,27 @@ func (s *session) handleKeyEvent() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleQEMUMessage parses one QEMU vendor message. Today we only handle
|
||||
// subtype 0 (Extended Key Event); the message itself is 12 bytes total so
|
||||
// reading 11 more after the type byte covers the layout regardless of
|
||||
// subtype, and unknown subtypes are dropped without aborting the session.
|
||||
func (s *session) handleQEMUMessage() error {
|
||||
var data [11]byte // subtype(1) + down(2) + keysym(4) + keycode(4)
|
||||
if _, err := io.ReadFull(s.conn, data[:]); err != nil {
|
||||
return fmt.Errorf("read QEMU message: %w", err)
|
||||
}
|
||||
subtype := data[0]
|
||||
if subtype != qemuSubtypeExtendedKeyEvent {
|
||||
s.log.Tracef("ignoring QEMU subtype %d", subtype)
|
||||
return nil
|
||||
}
|
||||
down := binary.BigEndian.Uint16(data[1:3]) != 0
|
||||
keysym := binary.BigEndian.Uint32(data[3:7])
|
||||
scancode := binary.BigEndian.Uint32(data[7:11])
|
||||
s.injector.InjectKeyScancode(scancode, keysym, down)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *session) handlePointerEvent() error {
|
||||
var data [5]byte
|
||||
if _, err := io.ReadFull(s.conn, data[:]); err != nil {
|
||||
|
||||
@@ -27,6 +27,11 @@ func (s *StubInputInjector) InjectKey(_ uint32, _ bool) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
// InjectKeyScancode is a no-op on unsupported platforms.
|
||||
func (s *StubInputInjector) InjectKeyScancode(_ uint32, _ uint32, _ bool) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
// InjectPointer is a no-op on unsupported platforms.
|
||||
func (s *StubInputInjector) InjectPointer(_ uint8, _, _, _, _ int) {
|
||||
// no-op
|
||||
|
||||
Reference in New Issue
Block a user