Add QEMU Extended Key Event for layout-independent input

This commit is contained in:
Viktor Liu
2026-05-17 09:24:26 +02:00
parent 2bed8b641b
commit 4f884d9f30
11 changed files with 775 additions and 44 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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:

View File

@@ -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)
}

View File

@@ -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.

View 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)
}

View 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)
}

View 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)
}
}

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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