From f0c10eeaba04f4a586aaab5a0fd51acf27cddc44 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 18 Jul 2026 07:47:32 -0700 Subject: [PATCH 1/3] feat(connect): add Windows capture and input backend --- cmd/crabfleet-connect/main.go | 16 +- internal/connect/platform_other.go | 2 +- internal/connect/platform_windows.go | 7 + internal/connect/windows.go | 732 +++++++++++++++++++ internal/connect/windows_keymap.go | 154 ++++ internal/connect/windows_keymap_test.go | 58 ++ internal/connect/windows_keysym_unicode.go | 793 +++++++++++++++++++++ 7 files changed, 1759 insertions(+), 3 deletions(-) create mode 100644 internal/connect/platform_windows.go create mode 100644 internal/connect/windows.go create mode 100644 internal/connect/windows_keymap.go create mode 100644 internal/connect/windows_keymap_test.go create mode 100644 internal/connect/windows_keysym_unicode.go diff --git a/cmd/crabfleet-connect/main.go b/cmd/crabfleet-connect/main.go index 2ffa1ff..da95aea 100644 --- a/cmd/crabfleet-connect/main.go +++ b/cmd/crabfleet-connect/main.go @@ -10,6 +10,7 @@ import ( "net" "os" "os/signal" + "runtime" "strconv" "strings" "syscall" @@ -68,7 +69,7 @@ func run(ctx context.Context, arguments []string, stdout, stderr io.Writer) erro } hostname, err := os.Hostname() if err != nil || hostname == "" { - hostname = "Linux" + hostname = runtime.GOOS } server, err := rfb.NewServer(rfb.ServerConfig{Session: rfb.SessionConfig{ Backend: backend, @@ -95,7 +96,7 @@ func selectBackend(forceSynthetic bool, display string, stderr io.Writer) (conne if !forceSynthetic { backend, err := connect.NewPlatformBackend(display) if err == nil { - return backend, "Linux X11 (MIT-SHM capture + XTest input)", nil + return backend, nativeBackendDescription(), nil } fmt.Fprintf(stderr, "Native capture unavailable (%v); using synthetic test pattern.\n", err) } @@ -106,6 +107,17 @@ func selectBackend(forceSynthetic bool, display string, stderr io.Writer) (conne return backend, "synthetic test pattern", nil } +func nativeBackendDescription() string { + switch runtime.GOOS { + case "linux": + return "Linux X11 (MIT-SHM capture + XTest input)" + case "windows": + return "Windows GDI BitBlt capture + SendInput" + default: + return "native capture + input" + } +} + func generateSharePassword() (string, error) { const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789" const length = 8 diff --git a/internal/connect/platform_other.go b/internal/connect/platform_other.go index bf979b0..57023c6 100644 --- a/internal/connect/platform_other.go +++ b/internal/connect/platform_other.go @@ -1,4 +1,4 @@ -//go:build !linux +//go:build !linux && !windows package connect diff --git a/internal/connect/platform_windows.go b/internal/connect/platform_windows.go new file mode 100644 index 0000000..accb0ba --- /dev/null +++ b/internal/connect/platform_windows.go @@ -0,0 +1,7 @@ +//go:build windows + +package connect + +func NewPlatformBackend(string) (Backend, error) { + return NewWindowsBackend() +} diff --git a/internal/connect/windows.go b/internal/connect/windows.go new file mode 100644 index 0000000..7518546 --- /dev/null +++ b/internal/connect/windows.go @@ -0,0 +1,732 @@ +//go:build windows + +package connect + +import ( + "context" + "encoding/binary" + "errors" + "fmt" + "runtime" + "sync" + "syscall" + "unicode" + "unicode/utf16" + "unsafe" +) + +const ( + windowsSMCXScreen = 0 + windowsSMCYScreen = 1 + + windowsBIRGB = 0 + windowsDIBRGBColors = 0 + windowsSRCCopy = 0x00cc0020 + windowsCaptureBLT = 0x40000000 + + windowsInputMouse = 0 + windowsInputKeyboard = 1 + + windowsMouseMove = 0x0001 + windowsMouseLeftDown = 0x0002 + windowsMouseLeftUp = 0x0004 + windowsMouseRightDown = 0x0008 + windowsMouseRightUp = 0x0010 + windowsMouseMiddleDown = 0x0020 + windowsMouseMiddleUp = 0x0040 + windowsMouseWheel = 0x0800 + windowsMouseHWheel = 0x1000 + windowsMouseAbsolute = 0x8000 + + windowsKeyExtended = 0x0001 + windowsKeyUp = 0x0002 + windowsKeyUnicode = 0x0004 + windowsWheelDelta = 120 +) + +var ( + windowsUser32 = syscall.NewLazyDLL("user32.dll") + windowsGDI32 = syscall.NewLazyDLL("gdi32.dll") + windowsProcGetSystemMetrics = windowsUser32.NewProc("GetSystemMetrics") + windowsProcSendInput = windowsUser32.NewProc("SendInput") + windowsProcGetAsyncKeyState = windowsUser32.NewProc("GetAsyncKeyState") + windowsProcGetForegroundWindow = windowsUser32.NewProc("GetForegroundWindow") + windowsProcGetWindowThreadID = windowsUser32.NewProc("GetWindowThreadProcessId") + windowsProcGetKeyboardLayout = windowsUser32.NewProc("GetKeyboardLayout") + windowsProcVkKeyScanExW = windowsUser32.NewProc("VkKeyScanExW") + windowsProcCreateDCW = windowsGDI32.NewProc("CreateDCW") + windowsProcCreateCompatibleDC = windowsGDI32.NewProc("CreateCompatibleDC") + windowsProcDeleteDC = windowsGDI32.NewProc("DeleteDC") + windowsProcCreateDIBSection = windowsGDI32.NewProc("CreateDIBSection") + windowsProcSelectObject = windowsGDI32.NewProc("SelectObject") + windowsProcDeleteObject = windowsGDI32.NewProc("DeleteObject") + windowsProcBitBlt = windowsGDI32.NewProc("BitBlt") + windowsProcGdiFlush = windowsGDI32.NewProc("GdiFlush") +) + +type windowsBitmapInfoHeader struct { + Size uint32 + Width int32 + Height int32 + Planes uint16 + BitCount uint16 + Compression uint32 + SizeImage uint32 + XPelsPerMeter int32 + YPelsPerMeter int32 + ClrUsed uint32 + ClrImportant uint32 +} + +type windowsBitmapInfo struct { + Header windowsBitmapInfoHeader + Colors [1]uint32 +} + +type windowsHeldKey struct { + binding windowsKeyBinding + unicodeUnits []uint16 + syntheticShift bool +} + +// WindowsBackend captures the primary desktop with GDI and injects input with +// SendInput. DXGI Desktop Duplication is the intended faster capture follow-up. +type WindowsBackend struct { + mu sync.Mutex + + closed bool + screenDC uintptr + memoryDC uintptr + bitmap uintptr + previousBitmap uintptr + dibBits unsafe.Pointer + width int + height int + stride int + sequence uint64 + buttonMask byte + heldKeys map[uint32]windowsHeldKey + syntheticShift int +} + +var _ Backend = (*WindowsBackend)(nil) + +func NewWindowsBackend() (_ *WindowsBackend, resultErr error) { + width, _, _ := windowsProcGetSystemMetrics.Call(windowsSMCXScreen) + height, _, _ := windowsProcGetSystemMetrics.Call(windowsSMCYScreen) + if width < 1 || width > MaxDimension || height < 1 || height > MaxDimension { + return nil, fmt.Errorf("invalid Windows primary display dimensions %dx%d", width, height) + } + stride, ok := checkedMul(int(width), 4) + if !ok { + return nil, errors.New("Windows capture stride overflow") + } + bufferSize, ok := checkedMul(stride, int(height)) + if !ok || bufferSize > MaxFrameBytes { + return nil, errors.New("Windows primary display exceeds capture memory limit") + } + + backend := &WindowsBackend{ + width: int(width), + height: int(height), + stride: stride, + heldKeys: make(map[uint32]windowsHeldKey), + } + defer func() { + if resultErr != nil { + backend.cleanupLocked() + } + }() + + displayDriver, err := syscall.UTF16PtrFromString("DISPLAY") + if err != nil { + return nil, fmt.Errorf("encode Windows display driver name: %w", err) + } + backend.screenDC, _, _ = windowsProcCreateDCW.Call(uintptr(unsafe.Pointer(displayDriver)), 0, 0, 0) + if backend.screenDC == 0 { + return nil, errors.New("CreateDCW for the Windows display failed") + } + backend.memoryDC, _, _ = windowsProcCreateCompatibleDC.Call(backend.screenDC) + if backend.memoryDC == 0 { + return nil, errors.New("CreateCompatibleDC failed") + } + info := windowsBitmapInfo{Header: windowsBitmapInfoHeader{ + Size: uint32(unsafe.Sizeof(windowsBitmapInfoHeader{})), + Width: int32(backend.width), + Height: -int32(backend.height), + Planes: 1, + BitCount: 32, + Compression: windowsBIRGB, + SizeImage: uint32(bufferSize), + }} + backend.bitmap, _, _ = windowsProcCreateDIBSection.Call( + backend.screenDC, + uintptr(unsafe.Pointer(&info)), + windowsDIBRGBColors, + uintptr(unsafe.Pointer(&backend.dibBits)), + 0, + 0, + ) + if backend.bitmap == 0 || backend.dibBits == nil { + return nil, errors.New("CreateDIBSection failed") + } + backend.previousBitmap, _, _ = windowsProcSelectObject.Call(backend.memoryDC, backend.bitmap) + if backend.previousBitmap == 0 || backend.previousBitmap == ^uintptr(0) { + return nil, errors.New("SelectObject for the Windows capture bitmap failed") + } + return backend, nil +} + +func (backend *WindowsBackend) Capture(ctx context.Context) (Frame, error) { + if err := ctx.Err(); err != nil { + return Frame{}, err + } + backend.mu.Lock() + defer backend.mu.Unlock() + if backend.closed { + return Frame{}, ErrClosed + } + // GDI batches are thread-local. Keep BitBlt, GdiFlush, and the direct DIB + // read on one OS thread so the bitmap memory is synchronized first. + runtime.LockOSThread() + defer runtime.UnlockOSThread() + result, _, callErr := windowsProcBitBlt.Call( + backend.memoryDC, + 0, + 0, + uintptr(backend.width), + uintptr(backend.height), + backend.screenDC, + 0, + 0, + windowsSRCCopy|windowsCaptureBLT, + ) + if result == 0 { + return Frame{}, windowsCallError("BitBlt", callErr) + } + result, _, callErr = windowsProcGdiFlush.Call() + if result == 0 { + return Frame{}, windowsCallError("GdiFlush", callErr) + } + source := unsafe.Slice((*byte)(backend.dibBits), backend.stride*backend.height) + pixels := make([]byte, len(source)) + for offset := 0; offset < len(source); offset += 4 { + pixels[offset] = source[offset+2] + pixels[offset+1] = source[offset+1] + pixels[offset+2] = source[offset] + pixels[offset+3] = 0xff + } + backend.sequence++ + return Frame{ + Width: backend.width, + Height: backend.height, + Stride: backend.stride, + Pixels: pixels, + DirtyRects: []Rect{{Width: backend.width, Height: backend.height}}, + Sequence: backend.sequence, + }, nil +} + +func (backend *WindowsBackend) Pointer(ctx context.Context, event PointerEvent) error { + if err := ctx.Err(); err != nil { + return err + } + if event.ButtonMask&0x80 != 0 { + return errors.New("Windows input does not support RFB button 8") + } + backend.mu.Lock() + defer backend.mu.Unlock() + if backend.closed { + return ErrClosed + } + inputs := make([]windowsInput, 0, 8) + inputs = append(inputs, windowsInput{ + mouseX: backend.absoluteCoordinate(event.X, backend.width), + mouseY: backend.absoluteCoordinate(event.Y, backend.height), + flags: windowsMouseMove | windowsMouseAbsolute, + }) + for bit := 0; bit < 7; bit++ { + button := byte(1 << bit) + wasDown := backend.buttonMask&button != 0 + isDown := event.ButtonMask&button != 0 + if wasDown == isDown { + continue + } + input := windowsInput{} + switch bit { + case 0: + if isDown { + input.flags = windowsMouseLeftDown + } else { + input.flags = windowsMouseLeftUp + } + case 1: + if isDown { + input.flags = windowsMouseMiddleDown + } else { + input.flags = windowsMouseMiddleUp + } + case 2: + if isDown { + input.flags = windowsMouseRightDown + } else { + input.flags = windowsMouseRightUp + } + case 3: + if !isDown { + continue + } + input.flags = windowsMouseWheel + input.mouseData = windowsWheelDelta + case 4: + if !isDown { + continue + } + input.flags = windowsMouseWheel + input.mouseData = ^uint32(windowsWheelDelta - 1) + case 5: + if !isDown { + continue + } + input.flags = windowsMouseHWheel + input.mouseData = ^uint32(windowsWheelDelta - 1) + case 6: + if !isDown { + continue + } + input.flags = windowsMouseHWheel + input.mouseData = windowsWheelDelta + } + inputs = append(inputs, input) + } + if err := sendWindowsInputs(inputs); err != nil { + return err + } + backend.buttonMask = event.ButtonMask + return nil +} + +func (backend *WindowsBackend) Key(ctx context.Context, event KeyEvent) error { + if err := ctx.Err(); err != nil { + return err + } + backend.mu.Lock() + defer backend.mu.Unlock() + if backend.closed { + return ErrClosed + } + if held, ok := backend.heldKeys[event.Keysym]; ok { + if event.Down { + return sendWindowsInputs(held.keyInputs(false)) + } + return backend.releaseKey(event.Keysym, held) + } + if !event.Down { + return nil + } + held := windowsHeldKey{} + if binding, ok := windowsKeyBindingForKeysym(event.Keysym); ok { + held.binding = binding + } else if character, ok := windowsRuneForKeysym(event.Keysym); ok { + if backend.shortcutModifierHeld() { + binding, ok = windowsShortcutBinding(character) + if !ok { + return fmt.Errorf("Windows active keymap has no virtual key for keysym %#x", event.Keysym) + } + if binding.control && !backend.controlHeld() { + return fmt.Errorf("Windows active keymap requires Control for keysym %#x", event.Keysym) + } + if binding.alt && !backend.altHeld() { + return fmt.Errorf("Windows active keymap requires Alt for keysym %#x", event.Keysym) + } + held.binding = binding + } else { + held.unicodeUnits = utf16.Encode([]rune{character}) + } + } else { + return fmt.Errorf("Windows keymap has no input for keysym %#x", event.Keysym) + } + inputs := make([]windowsInput, 0, len(held.unicodeUnits)+1) + if len(held.unicodeUnits) == 0 && held.binding.shift && !backend.shiftHeld() { + shift := windowsKeyBinding{virtualKey: windowsVKShift} + inputs = append(inputs, windowsInput{keyboard: &shift}) + held.syntheticShift = true + } else if len(held.unicodeUnits) == 0 && held.binding.shift && + backend.syntheticShift > 0 && !backend.explicitShiftHeld() { + held.syntheticShift = true + } + inputs = append(inputs, held.keyInputs(false)...) + if err := sendWindowsInputs(inputs); err != nil { + return err + } + backend.heldKeys[event.Keysym] = held + if held.syntheticShift { + backend.syntheticShift++ + } + return nil +} + +func (backend *WindowsBackend) Close() error { + backend.mu.Lock() + defer backend.mu.Unlock() + if backend.closed && len(backend.heldKeys) == 0 && backend.buttonMask == 0 && backend.syntheticShift == 0 { + return nil + } + inputs := make([]windowsInput, 0, len(backend.heldKeys)+4) + released := make(map[windowsKeyBinding]struct{}, len(backend.heldKeys)) + for _, held := range backend.heldKeys { + if len(held.unicodeUnits) != 0 { + inputs = append(inputs, held.keyInputs(true)...) + continue + } + binding := held.binding + binding.shift = false + binding.control = false + binding.alt = false + if _, exists := released[binding]; exists { + continue + } + released[binding] = struct{}{} + inputs = append(inputs, windowsInput{keyboard: &binding, keyUp: true}) + } + if backend.syntheticShift > 0 { + shift := windowsKeyBinding{virtualKey: windowsVKShift} + if _, exists := released[shift]; !exists { + inputs = append(inputs, windowsInput{keyboard: &shift, keyUp: true}) + } + } + if backend.buttonMask&1 != 0 { + inputs = append(inputs, windowsInput{flags: windowsMouseLeftUp}) + } + if backend.buttonMask&2 != 0 { + inputs = append(inputs, windowsInput{flags: windowsMouseMiddleUp}) + } + if backend.buttonMask&4 != 0 { + inputs = append(inputs, windowsInput{flags: windowsMouseRightUp}) + } + releaseErr := sendWindowsInputs(inputs) + if !backend.closed { + backend.closed = true + backend.cleanupLocked() + } + if releaseErr != nil { + return fmt.Errorf("release Windows input: %w", releaseErr) + } + backend.heldKeys = nil + backend.syntheticShift = 0 + backend.buttonMask = 0 + return nil +} + +type windowsInput struct { + mouseX int32 + mouseY int32 + mouseData uint32 + flags uint32 + keyboard *windowsKeyBinding + unicode uint16 + isUnicode bool + keyUp bool +} + +func (backend *WindowsBackend) releaseKey(keysym uint32, held windowsHeldKey) error { + inputs := make([]windowsInput, 0, 2) + if len(held.unicodeUnits) != 0 { + inputs = append(inputs, held.keyInputs(true)...) + } else if !backend.bindingHeldByOther(keysym, held.binding) { + inputs = append(inputs, windowsInput{keyboard: &held.binding, keyUp: true}) + if windowsVirtualKeyIsShift(held.binding.virtualKey) && backend.syntheticShift > 0 { + shift := windowsKeyBinding{virtualKey: windowsVKShift} + inputs = append(inputs, windowsInput{keyboard: &shift}) + } + } + if held.syntheticShift && backend.syntheticShift == 1 { + shift := windowsKeyBinding{virtualKey: windowsVKShift} + inputs = append(inputs, windowsInput{keyboard: &shift, keyUp: true}) + inputs = append(inputs, backend.explicitShiftInputsExcept(keysym)...) + } + if err := sendWindowsInputs(inputs); err != nil { + return err + } + delete(backend.heldKeys, keysym) + if held.syntheticShift { + backend.syntheticShift-- + } + return nil +} + +func (backend *WindowsBackend) bindingHeldByOther(keysym uint32, binding windowsKeyBinding) bool { + for candidate, held := range backend.heldKeys { + if candidate != keysym && len(held.unicodeUnits) == 0 && + held.binding.virtualKey == binding.virtualKey && held.binding.extended == binding.extended { + return true + } + } + return false +} + +func (held windowsHeldKey) keyInputs(keyUp bool) []windowsInput { + if len(held.unicodeUnits) == 0 { + return []windowsInput{{keyboard: &held.binding, keyUp: keyUp}} + } + inputs := make([]windowsInput, 0, len(held.unicodeUnits)) + for _, unit := range held.unicodeUnits { + inputs = append(inputs, windowsInput{unicode: unit, isUnicode: true, keyUp: keyUp}) + } + return inputs +} + +func (backend *WindowsBackend) shiftHeld() bool { + if backend.syntheticShift > 0 || backend.explicitShiftHeld() { + return true + } + state, _, _ := windowsProcGetAsyncKeyState.Call(windowsVKShift) + return state&0x8000 != 0 +} + +func (backend *WindowsBackend) explicitShiftHeld() bool { + return backend.explicitShiftHeldExcept(0) +} + +func (backend *WindowsBackend) explicitShiftHeldExcept(except uint32) bool { + for keysym, held := range backend.heldKeys { + if keysym != except && len(held.unicodeUnits) == 0 && windowsVirtualKeyIsShift(held.binding.virtualKey) { + return true + } + } + return false +} + +func (backend *WindowsBackend) explicitShiftInputsExcept(except uint32) []windowsInput { + inputs := make([]windowsInput, 0, 2) + seen := make(map[uint16]struct{}, 2) + for keysym, held := range backend.heldKeys { + if keysym == except || len(held.unicodeUnits) != 0 || + !windowsVirtualKeyIsShift(held.binding.virtualKey) { + continue + } + if _, exists := seen[held.binding.virtualKey]; exists { + continue + } + seen[held.binding.virtualKey] = struct{}{} + binding := held.binding + inputs = append(inputs, windowsInput{keyboard: &binding}) + } + return inputs +} + +func (backend *WindowsBackend) shortcutModifierHeld() bool { + for _, held := range backend.heldKeys { + if len(held.unicodeUnits) == 0 && + (windowsVirtualKeyIsShift(held.binding.virtualKey) || + windowsVirtualKeyIsShortcutModifier(held.binding.virtualKey)) { + return true + } + } + return false +} + +func (backend *WindowsBackend) controlHeld() bool { + if backend.virtualKeyHeld(windowsVKControl, windowsVKLControl, windowsVKRControl) { + return true + } + _, level3Shift := backend.heldKeys[uint32(0xfe03)] + _, modeSwitch := backend.heldKeys[uint32(0xff7e)] + return level3Shift || modeSwitch +} + +func (backend *WindowsBackend) altHeld() bool { + return backend.virtualKeyHeld(windowsVKMenu, windowsVKLMenu, windowsVKRMenu) +} + +func (backend *WindowsBackend) virtualKeyHeld(virtualKeys ...uint16) bool { + for _, held := range backend.heldKeys { + if len(held.unicodeUnits) != 0 { + continue + } + for _, virtualKey := range virtualKeys { + if held.binding.virtualKey == virtualKey { + return true + } + } + } + return false +} + +func windowsShortcutBinding(character rune) (windowsKeyBinding, bool) { + if character < 0 || character > 0xffff { + return windowsKeyBinding{}, false + } + foreground, _, _ := windowsProcGetForegroundWindow.Call() + threadID := uintptr(0) + if foreground != 0 { + threadID, _, _ = windowsProcGetWindowThreadID.Call(foreground, 0) + } + layout, _, _ := windowsProcGetKeyboardLayout.Call(threadID) + if layout == 0 { + return windowsKeyBinding{}, false + } + result, _, _ := windowsProcVkKeyScanExW.Call(uintptr(character), layout) + value := uint16(result) + if value == 0xffff { + return windowsKeyBinding{}, false + } + modifiers := byte(value >> 8) + if modifiers & ^byte(0x07) != 0 { + return windowsKeyBinding{}, false + } + // RFB reports the resulting letter. Explicit Shift events carry chord + // identity; uppercase can instead result from the client's Caps Lock. + if unicode.IsLetter(character) { + modifiers &^= 1 + } + return windowsKeyBinding{ + virtualKey: value & 0xff, + shift: modifiers&1 != 0, + control: modifiers&2 != 0, + alt: modifiers&4 != 0, + }, true +} + +func (backend *WindowsBackend) absoluteCoordinate(coordinate uint16, dimension int) int32 { + if dimension <= 1 { + return 0 + } + maximum := uint64(dimension - 1) + value := min(uint64(coordinate), maximum) + return int32((value*65_535 + maximum/2) / maximum) +} + +func (backend *WindowsBackend) cleanupLocked() { + if backend.memoryDC != 0 && backend.previousBitmap != 0 && backend.previousBitmap != ^uintptr(0) { + windowsProcSelectObject.Call(backend.memoryDC, backend.previousBitmap) + backend.previousBitmap = 0 + } + if backend.bitmap != 0 { + windowsProcDeleteObject.Call(backend.bitmap) + backend.bitmap = 0 + backend.dibBits = nil + } + if backend.memoryDC != 0 { + windowsProcDeleteDC.Call(backend.memoryDC) + backend.memoryDC = 0 + } + if backend.screenDC != 0 { + windowsProcDeleteDC.Call(backend.screenDC) + backend.screenDC = 0 + } +} + +func sendWindowsInputs(inputs []windowsInput) error { + if len(inputs) == 0 { + return nil + } + inputSize, _ := windowsInputLayout() + buffer := encodeWindowsInputs(inputs) + inserted, _, callErr := windowsProcSendInput.Call( + uintptr(len(inputs)), + uintptr(unsafe.Pointer(&buffer[0])), + uintptr(inputSize), + ) + if inserted != uintptr(len(inputs)) { + failure := windowsCallError(fmt.Sprintf("SendInput inserted %d of %d events", inserted, len(inputs)), callErr) + if inserted == 0 { + return failure + } + compensation := windowsInputCompensation(inputs[:inserted]) + if len(compensation) == 0 { + return failure + } + compensationBuffer := encodeWindowsInputs(compensation) + compensated, _, compensationErr := windowsProcSendInput.Call( + uintptr(len(compensation)), + uintptr(unsafe.Pointer(&compensationBuffer[0])), + uintptr(inputSize), + ) + if compensated != uintptr(len(compensation)) { + return fmt.Errorf( + "%w; compensation inserted %d of %d events: %v", + failure, compensated, len(compensation), compensationErr, + ) + } + return failure + } + return nil +} + +func encodeWindowsInputs(inputs []windowsInput) []byte { + inputSize, dataOffset := windowsInputLayout() + buffer := make([]byte, len(inputs)*inputSize) + for index, input := range inputs { + target := buffer[index*inputSize : (index+1)*inputSize] + if input.keyboard != nil || input.isUnicode { + binary.LittleEndian.PutUint32(target, windowsInputKeyboard) + if input.isUnicode { + binary.LittleEndian.PutUint16(target[dataOffset+2:], input.unicode) + } else { + binary.LittleEndian.PutUint16(target[dataOffset:], input.keyboard.virtualKey) + } + flags := uint32(0) + if input.isUnicode { + flags |= windowsKeyUnicode + } else if input.keyboard.extended { + flags |= windowsKeyExtended + } + if input.keyUp { + flags |= windowsKeyUp + } + binary.LittleEndian.PutUint32(target[dataOffset+4:], flags) + continue + } + binary.LittleEndian.PutUint32(target, windowsInputMouse) + binary.LittleEndian.PutUint32(target[dataOffset:], uint32(input.mouseX)) + binary.LittleEndian.PutUint32(target[dataOffset+4:], uint32(input.mouseY)) + binary.LittleEndian.PutUint32(target[dataOffset+8:], input.mouseData) + binary.LittleEndian.PutUint32(target[dataOffset+12:], input.flags) + } + return buffer +} + +func windowsInputCompensation(inserted []windowsInput) []windowsInput { + result := make([]windowsInput, 0, len(inserted)) + for index := len(inserted) - 1; index >= 0; index-- { + input := inserted[index] + if input.keyboard != nil || input.isUnicode { + input.keyUp = !input.keyUp + result = append(result, input) + continue + } + switch input.flags { + case windowsMouseLeftDown: + input.flags = windowsMouseLeftUp + case windowsMouseLeftUp: + input.flags = windowsMouseLeftDown + case windowsMouseMiddleDown: + input.flags = windowsMouseMiddleUp + case windowsMouseMiddleUp: + input.flags = windowsMouseMiddleDown + case windowsMouseRightDown: + input.flags = windowsMouseRightUp + case windowsMouseRightUp: + input.flags = windowsMouseRightDown + default: + continue + } + result = append(result, input) + } + return result +} + +func windowsInputLayout() (size, dataOffset int) { + if unsafe.Sizeof(uintptr(0)) == 8 { + return 40, 8 + } + return 28, 4 +} + +func windowsCallError(action string, err error) error { + if err == nil || errors.Is(err, syscall.Errno(0)) { + return errors.New(action + " failed") + } + return fmt.Errorf("%s: %w", action, err) +} diff --git a/internal/connect/windows_keymap.go b/internal/connect/windows_keymap.go new file mode 100644 index 0000000..852c453 --- /dev/null +++ b/internal/connect/windows_keymap.go @@ -0,0 +1,154 @@ +package connect + +type windowsKeyBinding struct { + virtualKey uint16 + shift bool + control bool + alt bool + extended bool +} + +const ( + windowsVKBack = 0x08 + windowsVKTab = 0x09 + windowsVKClear = 0x0c + windowsVKReturn = 0x0d + windowsVKShift = 0x10 + windowsVKControl = 0x11 + windowsVKMenu = 0x12 + windowsVKPause = 0x13 + windowsVKCapital = 0x14 + windowsVKEscape = 0x1b + windowsVKSpace = 0x20 + windowsVKPrior = 0x21 + windowsVKNext = 0x22 + windowsVKEnd = 0x23 + windowsVKHome = 0x24 + windowsVKLeft = 0x25 + windowsVKUp = 0x26 + windowsVKRight = 0x27 + windowsVKDown = 0x28 + windowsVKSnapshot = 0x2c + windowsVKInsert = 0x2d + windowsVKDelete = 0x2e + windowsVKLWin = 0x5b + windowsVKRWin = 0x5c + windowsVKApps = 0x5d + windowsVKNumpad0 = 0x60 + windowsVKMultiply = 0x6a + windowsVKAdd = 0x6b + windowsVKSeparator = 0x6c + windowsVKSubtract = 0x6d + windowsVKDecimal = 0x6e + windowsVKDivide = 0x6f + windowsVKF1 = 0x70 + windowsVKNumLock = 0x90 + windowsVKScroll = 0x91 + windowsVKLShift = 0xa0 + windowsVKRShift = 0xa1 + windowsVKLControl = 0xa2 + windowsVKRControl = 0xa3 + windowsVKLMenu = 0xa4 + windowsVKRMenu = 0xa5 +) + +var windowsKeysymBindings = map[uint32]windowsKeyBinding{ + 0xff08: {virtualKey: windowsVKBack}, + 0xff09: {virtualKey: windowsVKTab}, + 0xff0b: {virtualKey: windowsVKClear}, + 0xff0d: {virtualKey: windowsVKReturn}, + 0xff13: {virtualKey: windowsVKPause}, + 0xff14: {virtualKey: windowsVKScroll}, + 0xff1b: {virtualKey: windowsVKEscape}, + 0xff50: {virtualKey: windowsVKHome, extended: true}, + 0xff51: {virtualKey: windowsVKLeft, extended: true}, + 0xff52: {virtualKey: windowsVKUp, extended: true}, + 0xff53: {virtualKey: windowsVKRight, extended: true}, + 0xff54: {virtualKey: windowsVKDown, extended: true}, + 0xff55: {virtualKey: windowsVKPrior, extended: true}, + 0xff56: {virtualKey: windowsVKNext, extended: true}, + 0xff57: {virtualKey: windowsVKEnd, extended: true}, + 0xff61: {virtualKey: windowsVKSnapshot, extended: true}, + 0xff63: {virtualKey: windowsVKInsert, extended: true}, + 0xff67: {virtualKey: windowsVKApps}, + 0xff7e: {virtualKey: windowsVKRMenu, extended: true}, + 0xff7f: {virtualKey: windowsVKNumLock, extended: true}, + 0xff89: {virtualKey: windowsVKTab}, + 0xff8d: {virtualKey: windowsVKReturn, extended: true}, + 0xff95: {virtualKey: windowsVKHome}, + 0xff96: {virtualKey: windowsVKLeft}, + 0xff97: {virtualKey: windowsVKUp}, + 0xff98: {virtualKey: windowsVKRight}, + 0xff99: {virtualKey: windowsVKDown}, + 0xff9a: {virtualKey: windowsVKPrior}, + 0xff9b: {virtualKey: windowsVKNext}, + 0xff9c: {virtualKey: windowsVKEnd}, + 0xff9d: {virtualKey: windowsVKClear}, + 0xff9e: {virtualKey: windowsVKInsert}, + 0xff9f: {virtualKey: windowsVKDelete}, + 0xffaa: {virtualKey: windowsVKMultiply}, + 0xffab: {virtualKey: windowsVKAdd}, + 0xffac: {virtualKey: windowsVKSeparator}, + 0xffad: {virtualKey: windowsVKSubtract}, + 0xffae: {virtualKey: windowsVKDecimal}, + 0xffaf: {virtualKey: windowsVKDivide, extended: true}, + 0xffe1: {virtualKey: windowsVKLShift}, + 0xffe2: {virtualKey: windowsVKRShift}, + 0xffe3: {virtualKey: windowsVKLControl}, + 0xffe4: {virtualKey: windowsVKRControl, extended: true}, + 0xffe5: {virtualKey: windowsVKCapital}, + 0xffe7: {virtualKey: windowsVKLWin}, + 0xffe8: {virtualKey: windowsVKRWin}, + 0xffe9: {virtualKey: windowsVKLMenu}, + 0xffea: {virtualKey: windowsVKRMenu, extended: true}, + 0xffeb: {virtualKey: windowsVKLWin}, + 0xffec: {virtualKey: windowsVKRWin}, + 0xfe03: {virtualKey: windowsVKRMenu, extended: true}, + 0xfe20: {virtualKey: windowsVKTab, shift: true}, + 0xffff: {virtualKey: windowsVKDelete, extended: true}, +} + +func windowsKeyBindingForKeysym(keysym uint32) (windowsKeyBinding, bool) { + if keysym >= 0xffb0 && keysym <= 0xffb9 { + return windowsKeyBinding{virtualKey: windowsVKNumpad0 + uint16(keysym-0xffb0)}, true + } + if keysym >= 0xffbe && keysym <= 0xffd5 { + return windowsKeyBinding{virtualKey: windowsVKF1 + uint16(keysym-0xffbe)}, true + } + if binding, ok := windowsKeysymBindings[keysym]; ok { + return binding, true + } + return windowsKeyBinding{}, false +} + +func windowsRuneForKeysym(keysym uint32) (rune, bool) { + if (keysym >= 0x20 && keysym <= 0x7e) || (keysym >= 0xa0 && keysym <= 0xff) { + return rune(keysym), true + } + if value, ok := windowsLegacyRuneForKeysym(keysym); ok { + return value, true + } + if keysym&0xff000000 != 0x01000000 { + return 0, false + } + value := rune(keysym & 0x00ffffff) + if value < 0 || value > 0x10ffff || value >= 0xd800 && value <= 0xdfff { + return 0, false + } + return value, true +} + +func windowsVirtualKeyIsShift(virtualKey uint16) bool { + return virtualKey == windowsVKShift || virtualKey == windowsVKLShift || virtualKey == windowsVKRShift +} + +func windowsVirtualKeyIsShortcutModifier(virtualKey uint16) bool { + switch virtualKey { + case windowsVKControl, windowsVKLControl, windowsVKRControl, + windowsVKMenu, windowsVKLMenu, windowsVKRMenu, + windowsVKLWin, windowsVKRWin: + return true + default: + return false + } +} diff --git a/internal/connect/windows_keymap_test.go b/internal/connect/windows_keymap_test.go new file mode 100644 index 0000000..04ca218 --- /dev/null +++ b/internal/connect/windows_keymap_test.go @@ -0,0 +1,58 @@ +package connect + +import "testing" + +func TestWindowsKeyBindingForKeysym(t *testing.T) { + t.Parallel() + tests := []struct { + name string + keysym uint32 + want windowsKeyBinding + }{ + {name: "left arrow", keysym: 0xff51, want: windowsKeyBinding{virtualKey: windowsVKLeft, extended: true}}, + {name: "right control", keysym: 0xffe4, want: windowsKeyBinding{virtualKey: windowsVKRControl, extended: true}}, + {name: "AltGr", keysym: 0xfe03, want: windowsKeyBinding{virtualKey: windowsVKRMenu, extended: true}}, + {name: "ISO left tab", keysym: 0xfe20, want: windowsKeyBinding{virtualKey: windowsVKTab, shift: true}}, + {name: "keypad begin", keysym: 0xff9d, want: windowsKeyBinding{virtualKey: windowsVKClear}}, + {name: "keypad seven", keysym: 0xffb7, want: windowsKeyBinding{virtualKey: windowsVKNumpad0 + 7}}, + {name: "F24", keysym: 0xffd5, want: windowsKeyBinding{virtualKey: windowsVKF1 + 23}}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + got, ok := windowsKeyBindingForKeysym(test.keysym) + if !ok || got != test.want { + t.Fatalf("binding(%#x) = %+v, %v; want %+v, true", test.keysym, got, ok, test.want) + } + }) + } +} + +func TestWindowsRuneForKeysym(t *testing.T) { + t.Parallel() + tests := []struct { + keysym uint32 + want rune + }{ + {keysym: 'A', want: 'A'}, + {keysym: 0xe9, want: 'é'}, + {keysym: 0x03c0, want: 'Ā'}, + {keysym: 0x06c1, want: 'а'}, + {keysym: 0x0101f980, want: '🦀'}, + } + for _, test := range tests { + got, ok := windowsRuneForKeysym(test.keysym) + if !ok || got != test.want { + t.Fatalf("rune(%#x) = %q, %v; want %q, true", test.keysym, got, ok, test.want) + } + } +} + +func TestWindowsRuneRejectsNonCharacterKeysyms(t *testing.T) { + t.Parallel() + for _, keysym := range []uint32{0x1f, 0xff51, 0x01110000, 0x0100d800} { + if _, ok := windowsRuneForKeysym(keysym); ok { + t.Fatalf("mapped non-character keysym %#x", keysym) + } + } +} diff --git a/internal/connect/windows_keysym_unicode.go b/internal/connect/windows_keysym_unicode.go new file mode 100644 index 0000000..c0456eb --- /dev/null +++ b/internal/connect/windows_keysym_unicode.go @@ -0,0 +1,793 @@ +package connect + +// Generated from the Unicode annotations in X.Org keysymdef.h. +type windowsLegacyKeysymRune struct { + keysym uint16 + value rune +} + +var windowsLegacyKeysymRunes = [...]windowsLegacyKeysymRune{ + {keysym: 0x01a1, value: 0x0104}, + {keysym: 0x01a2, value: 0x02d8}, + {keysym: 0x01a3, value: 0x0141}, + {keysym: 0x01a5, value: 0x013d}, + {keysym: 0x01a6, value: 0x015a}, + {keysym: 0x01a9, value: 0x0160}, + {keysym: 0x01aa, value: 0x015e}, + {keysym: 0x01ab, value: 0x0164}, + {keysym: 0x01ac, value: 0x0179}, + {keysym: 0x01ae, value: 0x017d}, + {keysym: 0x01af, value: 0x017b}, + {keysym: 0x01b1, value: 0x0105}, + {keysym: 0x01b2, value: 0x02db}, + {keysym: 0x01b3, value: 0x0142}, + {keysym: 0x01b5, value: 0x013e}, + {keysym: 0x01b6, value: 0x015b}, + {keysym: 0x01b7, value: 0x02c7}, + {keysym: 0x01b9, value: 0x0161}, + {keysym: 0x01ba, value: 0x015f}, + {keysym: 0x01bb, value: 0x0165}, + {keysym: 0x01bc, value: 0x017a}, + {keysym: 0x01bd, value: 0x02dd}, + {keysym: 0x01be, value: 0x017e}, + {keysym: 0x01bf, value: 0x017c}, + {keysym: 0x01c0, value: 0x0154}, + {keysym: 0x01c3, value: 0x0102}, + {keysym: 0x01c5, value: 0x0139}, + {keysym: 0x01c6, value: 0x0106}, + {keysym: 0x01c8, value: 0x010c}, + {keysym: 0x01ca, value: 0x0118}, + {keysym: 0x01cc, value: 0x011a}, + {keysym: 0x01cf, value: 0x010e}, + {keysym: 0x01d0, value: 0x0110}, + {keysym: 0x01d1, value: 0x0143}, + {keysym: 0x01d2, value: 0x0147}, + {keysym: 0x01d5, value: 0x0150}, + {keysym: 0x01d8, value: 0x0158}, + {keysym: 0x01d9, value: 0x016e}, + {keysym: 0x01db, value: 0x0170}, + {keysym: 0x01de, value: 0x0162}, + {keysym: 0x01e0, value: 0x0155}, + {keysym: 0x01e3, value: 0x0103}, + {keysym: 0x01e5, value: 0x013a}, + {keysym: 0x01e6, value: 0x0107}, + {keysym: 0x01e8, value: 0x010d}, + {keysym: 0x01ea, value: 0x0119}, + {keysym: 0x01ec, value: 0x011b}, + {keysym: 0x01ef, value: 0x010f}, + {keysym: 0x01f0, value: 0x0111}, + {keysym: 0x01f1, value: 0x0144}, + {keysym: 0x01f2, value: 0x0148}, + {keysym: 0x01f5, value: 0x0151}, + {keysym: 0x01f8, value: 0x0159}, + {keysym: 0x01f9, value: 0x016f}, + {keysym: 0x01fb, value: 0x0171}, + {keysym: 0x01fe, value: 0x0163}, + {keysym: 0x01ff, value: 0x02d9}, + {keysym: 0x02a1, value: 0x0126}, + {keysym: 0x02a6, value: 0x0124}, + {keysym: 0x02a9, value: 0x0130}, + {keysym: 0x02ab, value: 0x011e}, + {keysym: 0x02ac, value: 0x0134}, + {keysym: 0x02b1, value: 0x0127}, + {keysym: 0x02b6, value: 0x0125}, + {keysym: 0x02b9, value: 0x0131}, + {keysym: 0x02bb, value: 0x011f}, + {keysym: 0x02bc, value: 0x0135}, + {keysym: 0x02c5, value: 0x010a}, + {keysym: 0x02c6, value: 0x0108}, + {keysym: 0x02d5, value: 0x0120}, + {keysym: 0x02d8, value: 0x011c}, + {keysym: 0x02dd, value: 0x016c}, + {keysym: 0x02de, value: 0x015c}, + {keysym: 0x02e5, value: 0x010b}, + {keysym: 0x02e6, value: 0x0109}, + {keysym: 0x02f5, value: 0x0121}, + {keysym: 0x02f8, value: 0x011d}, + {keysym: 0x02fd, value: 0x016d}, + {keysym: 0x02fe, value: 0x015d}, + {keysym: 0x03a2, value: 0x0138}, + {keysym: 0x03a3, value: 0x0156}, + {keysym: 0x03a5, value: 0x0128}, + {keysym: 0x03a6, value: 0x013b}, + {keysym: 0x03aa, value: 0x0112}, + {keysym: 0x03ab, value: 0x0122}, + {keysym: 0x03ac, value: 0x0166}, + {keysym: 0x03b3, value: 0x0157}, + {keysym: 0x03b5, value: 0x0129}, + {keysym: 0x03b6, value: 0x013c}, + {keysym: 0x03ba, value: 0x0113}, + {keysym: 0x03bb, value: 0x0123}, + {keysym: 0x03bc, value: 0x0167}, + {keysym: 0x03bd, value: 0x014a}, + {keysym: 0x03bf, value: 0x014b}, + {keysym: 0x03c0, value: 0x0100}, + {keysym: 0x03c7, value: 0x012e}, + {keysym: 0x03cc, value: 0x0116}, + {keysym: 0x03cf, value: 0x012a}, + {keysym: 0x03d1, value: 0x0145}, + {keysym: 0x03d2, value: 0x014c}, + {keysym: 0x03d3, value: 0x0136}, + {keysym: 0x03d9, value: 0x0172}, + {keysym: 0x03dd, value: 0x0168}, + {keysym: 0x03de, value: 0x016a}, + {keysym: 0x03e0, value: 0x0101}, + {keysym: 0x03e7, value: 0x012f}, + {keysym: 0x03ec, value: 0x0117}, + {keysym: 0x03ef, value: 0x012b}, + {keysym: 0x03f1, value: 0x0146}, + {keysym: 0x03f2, value: 0x014d}, + {keysym: 0x03f3, value: 0x0137}, + {keysym: 0x03f9, value: 0x0173}, + {keysym: 0x03fd, value: 0x0169}, + {keysym: 0x03fe, value: 0x016b}, + {keysym: 0x047e, value: 0x203e}, + {keysym: 0x04a1, value: 0x3002}, + {keysym: 0x04a2, value: 0x300c}, + {keysym: 0x04a3, value: 0x300d}, + {keysym: 0x04a4, value: 0x3001}, + {keysym: 0x04a5, value: 0x30fb}, + {keysym: 0x04a6, value: 0x30f2}, + {keysym: 0x04a7, value: 0x30a1}, + {keysym: 0x04a8, value: 0x30a3}, + {keysym: 0x04a9, value: 0x30a5}, + {keysym: 0x04aa, value: 0x30a7}, + {keysym: 0x04ab, value: 0x30a9}, + {keysym: 0x04ac, value: 0x30e3}, + {keysym: 0x04ad, value: 0x30e5}, + {keysym: 0x04ae, value: 0x30e7}, + {keysym: 0x04af, value: 0x30c3}, + {keysym: 0x04b0, value: 0x30fc}, + {keysym: 0x04b1, value: 0x30a2}, + {keysym: 0x04b2, value: 0x30a4}, + {keysym: 0x04b3, value: 0x30a6}, + {keysym: 0x04b4, value: 0x30a8}, + {keysym: 0x04b5, value: 0x30aa}, + {keysym: 0x04b6, value: 0x30ab}, + {keysym: 0x04b7, value: 0x30ad}, + {keysym: 0x04b8, value: 0x30af}, + {keysym: 0x04b9, value: 0x30b1}, + {keysym: 0x04ba, value: 0x30b3}, + {keysym: 0x04bb, value: 0x30b5}, + {keysym: 0x04bc, value: 0x30b7}, + {keysym: 0x04bd, value: 0x30b9}, + {keysym: 0x04be, value: 0x30bb}, + {keysym: 0x04bf, value: 0x30bd}, + {keysym: 0x04c0, value: 0x30bf}, + {keysym: 0x04c1, value: 0x30c1}, + {keysym: 0x04c2, value: 0x30c4}, + {keysym: 0x04c3, value: 0x30c6}, + {keysym: 0x04c4, value: 0x30c8}, + {keysym: 0x04c5, value: 0x30ca}, + {keysym: 0x04c6, value: 0x30cb}, + {keysym: 0x04c7, value: 0x30cc}, + {keysym: 0x04c8, value: 0x30cd}, + {keysym: 0x04c9, value: 0x30ce}, + {keysym: 0x04ca, value: 0x30cf}, + {keysym: 0x04cb, value: 0x30d2}, + {keysym: 0x04cc, value: 0x30d5}, + {keysym: 0x04cd, value: 0x30d8}, + {keysym: 0x04ce, value: 0x30db}, + {keysym: 0x04cf, value: 0x30de}, + {keysym: 0x04d0, value: 0x30df}, + {keysym: 0x04d1, value: 0x30e0}, + {keysym: 0x04d2, value: 0x30e1}, + {keysym: 0x04d3, value: 0x30e2}, + {keysym: 0x04d4, value: 0x30e4}, + {keysym: 0x04d5, value: 0x30e6}, + {keysym: 0x04d6, value: 0x30e8}, + {keysym: 0x04d7, value: 0x30e9}, + {keysym: 0x04d8, value: 0x30ea}, + {keysym: 0x04d9, value: 0x30eb}, + {keysym: 0x04da, value: 0x30ec}, + {keysym: 0x04db, value: 0x30ed}, + {keysym: 0x04dc, value: 0x30ef}, + {keysym: 0x04dd, value: 0x30f3}, + {keysym: 0x04de, value: 0x309b}, + {keysym: 0x04df, value: 0x309c}, + {keysym: 0x05ac, value: 0x060c}, + {keysym: 0x05bb, value: 0x061b}, + {keysym: 0x05bf, value: 0x061f}, + {keysym: 0x05c1, value: 0x0621}, + {keysym: 0x05c2, value: 0x0622}, + {keysym: 0x05c3, value: 0x0623}, + {keysym: 0x05c4, value: 0x0624}, + {keysym: 0x05c5, value: 0x0625}, + {keysym: 0x05c6, value: 0x0626}, + {keysym: 0x05c7, value: 0x0627}, + {keysym: 0x05c8, value: 0x0628}, + {keysym: 0x05c9, value: 0x0629}, + {keysym: 0x05ca, value: 0x062a}, + {keysym: 0x05cb, value: 0x062b}, + {keysym: 0x05cc, value: 0x062c}, + {keysym: 0x05cd, value: 0x062d}, + {keysym: 0x05ce, value: 0x062e}, + {keysym: 0x05cf, value: 0x062f}, + {keysym: 0x05d0, value: 0x0630}, + {keysym: 0x05d1, value: 0x0631}, + {keysym: 0x05d2, value: 0x0632}, + {keysym: 0x05d3, value: 0x0633}, + {keysym: 0x05d4, value: 0x0634}, + {keysym: 0x05d5, value: 0x0635}, + {keysym: 0x05d6, value: 0x0636}, + {keysym: 0x05d7, value: 0x0637}, + {keysym: 0x05d8, value: 0x0638}, + {keysym: 0x05d9, value: 0x0639}, + {keysym: 0x05da, value: 0x063a}, + {keysym: 0x05e0, value: 0x0640}, + {keysym: 0x05e1, value: 0x0641}, + {keysym: 0x05e2, value: 0x0642}, + {keysym: 0x05e3, value: 0x0643}, + {keysym: 0x05e4, value: 0x0644}, + {keysym: 0x05e5, value: 0x0645}, + {keysym: 0x05e6, value: 0x0646}, + {keysym: 0x05e7, value: 0x0647}, + {keysym: 0x05e8, value: 0x0648}, + {keysym: 0x05e9, value: 0x0649}, + {keysym: 0x05ea, value: 0x064a}, + {keysym: 0x05eb, value: 0x064b}, + {keysym: 0x05ec, value: 0x064c}, + {keysym: 0x05ed, value: 0x064d}, + {keysym: 0x05ee, value: 0x064e}, + {keysym: 0x05ef, value: 0x064f}, + {keysym: 0x05f0, value: 0x0650}, + {keysym: 0x05f1, value: 0x0651}, + {keysym: 0x05f2, value: 0x0652}, + {keysym: 0x06a1, value: 0x0452}, + {keysym: 0x06a2, value: 0x0453}, + {keysym: 0x06a3, value: 0x0451}, + {keysym: 0x06a4, value: 0x0454}, + {keysym: 0x06a5, value: 0x0455}, + {keysym: 0x06a6, value: 0x0456}, + {keysym: 0x06a7, value: 0x0457}, + {keysym: 0x06a8, value: 0x0458}, + {keysym: 0x06a9, value: 0x0459}, + {keysym: 0x06aa, value: 0x045a}, + {keysym: 0x06ab, value: 0x045b}, + {keysym: 0x06ac, value: 0x045c}, + {keysym: 0x06ad, value: 0x0491}, + {keysym: 0x06ae, value: 0x045e}, + {keysym: 0x06af, value: 0x045f}, + {keysym: 0x06b0, value: 0x2116}, + {keysym: 0x06b1, value: 0x0402}, + {keysym: 0x06b2, value: 0x0403}, + {keysym: 0x06b3, value: 0x0401}, + {keysym: 0x06b4, value: 0x0404}, + {keysym: 0x06b5, value: 0x0405}, + {keysym: 0x06b6, value: 0x0406}, + {keysym: 0x06b7, value: 0x0407}, + {keysym: 0x06b8, value: 0x0408}, + {keysym: 0x06b9, value: 0x0409}, + {keysym: 0x06ba, value: 0x040a}, + {keysym: 0x06bb, value: 0x040b}, + {keysym: 0x06bc, value: 0x040c}, + {keysym: 0x06bd, value: 0x0490}, + {keysym: 0x06be, value: 0x040e}, + {keysym: 0x06bf, value: 0x040f}, + {keysym: 0x06c0, value: 0x044e}, + {keysym: 0x06c1, value: 0x0430}, + {keysym: 0x06c2, value: 0x0431}, + {keysym: 0x06c3, value: 0x0446}, + {keysym: 0x06c4, value: 0x0434}, + {keysym: 0x06c5, value: 0x0435}, + {keysym: 0x06c6, value: 0x0444}, + {keysym: 0x06c7, value: 0x0433}, + {keysym: 0x06c8, value: 0x0445}, + {keysym: 0x06c9, value: 0x0438}, + {keysym: 0x06ca, value: 0x0439}, + {keysym: 0x06cb, value: 0x043a}, + {keysym: 0x06cc, value: 0x043b}, + {keysym: 0x06cd, value: 0x043c}, + {keysym: 0x06ce, value: 0x043d}, + {keysym: 0x06cf, value: 0x043e}, + {keysym: 0x06d0, value: 0x043f}, + {keysym: 0x06d1, value: 0x044f}, + {keysym: 0x06d2, value: 0x0440}, + {keysym: 0x06d3, value: 0x0441}, + {keysym: 0x06d4, value: 0x0442}, + {keysym: 0x06d5, value: 0x0443}, + {keysym: 0x06d6, value: 0x0436}, + {keysym: 0x06d7, value: 0x0432}, + {keysym: 0x06d8, value: 0x044c}, + {keysym: 0x06d9, value: 0x044b}, + {keysym: 0x06da, value: 0x0437}, + {keysym: 0x06db, value: 0x0448}, + {keysym: 0x06dc, value: 0x044d}, + {keysym: 0x06dd, value: 0x0449}, + {keysym: 0x06de, value: 0x0447}, + {keysym: 0x06df, value: 0x044a}, + {keysym: 0x06e0, value: 0x042e}, + {keysym: 0x06e1, value: 0x0410}, + {keysym: 0x06e2, value: 0x0411}, + {keysym: 0x06e3, value: 0x0426}, + {keysym: 0x06e4, value: 0x0414}, + {keysym: 0x06e5, value: 0x0415}, + {keysym: 0x06e6, value: 0x0424}, + {keysym: 0x06e7, value: 0x0413}, + {keysym: 0x06e8, value: 0x0425}, + {keysym: 0x06e9, value: 0x0418}, + {keysym: 0x06ea, value: 0x0419}, + {keysym: 0x06eb, value: 0x041a}, + {keysym: 0x06ec, value: 0x041b}, + {keysym: 0x06ed, value: 0x041c}, + {keysym: 0x06ee, value: 0x041d}, + {keysym: 0x06ef, value: 0x041e}, + {keysym: 0x06f0, value: 0x041f}, + {keysym: 0x06f1, value: 0x042f}, + {keysym: 0x06f2, value: 0x0420}, + {keysym: 0x06f3, value: 0x0421}, + {keysym: 0x06f4, value: 0x0422}, + {keysym: 0x06f5, value: 0x0423}, + {keysym: 0x06f6, value: 0x0416}, + {keysym: 0x06f7, value: 0x0412}, + {keysym: 0x06f8, value: 0x042c}, + {keysym: 0x06f9, value: 0x042b}, + {keysym: 0x06fa, value: 0x0417}, + {keysym: 0x06fb, value: 0x0428}, + {keysym: 0x06fc, value: 0x042d}, + {keysym: 0x06fd, value: 0x0429}, + {keysym: 0x06fe, value: 0x0427}, + {keysym: 0x06ff, value: 0x042a}, + {keysym: 0x07a1, value: 0x0386}, + {keysym: 0x07a2, value: 0x0388}, + {keysym: 0x07a3, value: 0x0389}, + {keysym: 0x07a4, value: 0x038a}, + {keysym: 0x07a5, value: 0x03aa}, + {keysym: 0x07a7, value: 0x038c}, + {keysym: 0x07a8, value: 0x038e}, + {keysym: 0x07a9, value: 0x03ab}, + {keysym: 0x07ab, value: 0x038f}, + {keysym: 0x07ae, value: 0x0385}, + {keysym: 0x07af, value: 0x2015}, + {keysym: 0x07b1, value: 0x03ac}, + {keysym: 0x07b2, value: 0x03ad}, + {keysym: 0x07b3, value: 0x03ae}, + {keysym: 0x07b4, value: 0x03af}, + {keysym: 0x07b5, value: 0x03ca}, + {keysym: 0x07b6, value: 0x0390}, + {keysym: 0x07b7, value: 0x03cc}, + {keysym: 0x07b8, value: 0x03cd}, + {keysym: 0x07b9, value: 0x03cb}, + {keysym: 0x07ba, value: 0x03b0}, + {keysym: 0x07bb, value: 0x03ce}, + {keysym: 0x07c1, value: 0x0391}, + {keysym: 0x07c2, value: 0x0392}, + {keysym: 0x07c3, value: 0x0393}, + {keysym: 0x07c4, value: 0x0394}, + {keysym: 0x07c5, value: 0x0395}, + {keysym: 0x07c6, value: 0x0396}, + {keysym: 0x07c7, value: 0x0397}, + {keysym: 0x07c8, value: 0x0398}, + {keysym: 0x07c9, value: 0x0399}, + {keysym: 0x07ca, value: 0x039a}, + {keysym: 0x07cb, value: 0x039b}, + {keysym: 0x07cc, value: 0x039c}, + {keysym: 0x07cd, value: 0x039d}, + {keysym: 0x07ce, value: 0x039e}, + {keysym: 0x07cf, value: 0x039f}, + {keysym: 0x07d0, value: 0x03a0}, + {keysym: 0x07d1, value: 0x03a1}, + {keysym: 0x07d2, value: 0x03a3}, + {keysym: 0x07d4, value: 0x03a4}, + {keysym: 0x07d5, value: 0x03a5}, + {keysym: 0x07d6, value: 0x03a6}, + {keysym: 0x07d7, value: 0x03a7}, + {keysym: 0x07d8, value: 0x03a8}, + {keysym: 0x07d9, value: 0x03a9}, + {keysym: 0x07e1, value: 0x03b1}, + {keysym: 0x07e2, value: 0x03b2}, + {keysym: 0x07e3, value: 0x03b3}, + {keysym: 0x07e4, value: 0x03b4}, + {keysym: 0x07e5, value: 0x03b5}, + {keysym: 0x07e6, value: 0x03b6}, + {keysym: 0x07e7, value: 0x03b7}, + {keysym: 0x07e8, value: 0x03b8}, + {keysym: 0x07e9, value: 0x03b9}, + {keysym: 0x07ea, value: 0x03ba}, + {keysym: 0x07eb, value: 0x03bb}, + {keysym: 0x07ec, value: 0x03bc}, + {keysym: 0x07ed, value: 0x03bd}, + {keysym: 0x07ee, value: 0x03be}, + {keysym: 0x07ef, value: 0x03bf}, + {keysym: 0x07f0, value: 0x03c0}, + {keysym: 0x07f1, value: 0x03c1}, + {keysym: 0x07f2, value: 0x03c3}, + {keysym: 0x07f3, value: 0x03c2}, + {keysym: 0x07f4, value: 0x03c4}, + {keysym: 0x07f5, value: 0x03c5}, + {keysym: 0x07f6, value: 0x03c6}, + {keysym: 0x07f7, value: 0x03c7}, + {keysym: 0x07f8, value: 0x03c8}, + {keysym: 0x07f9, value: 0x03c9}, + {keysym: 0x08a1, value: 0x23b7}, + {keysym: 0x08a2, value: 0x250c}, + {keysym: 0x08a3, value: 0x2500}, + {keysym: 0x08a4, value: 0x2320}, + {keysym: 0x08a5, value: 0x2321}, + {keysym: 0x08a6, value: 0x2502}, + {keysym: 0x08a7, value: 0x23a1}, + {keysym: 0x08a8, value: 0x23a3}, + {keysym: 0x08a9, value: 0x23a4}, + {keysym: 0x08aa, value: 0x23a6}, + {keysym: 0x08ab, value: 0x239b}, + {keysym: 0x08ac, value: 0x239d}, + {keysym: 0x08ad, value: 0x239e}, + {keysym: 0x08ae, value: 0x23a0}, + {keysym: 0x08af, value: 0x23a8}, + {keysym: 0x08b0, value: 0x23ac}, + {keysym: 0x08bc, value: 0x2264}, + {keysym: 0x08bd, value: 0x2260}, + {keysym: 0x08be, value: 0x2265}, + {keysym: 0x08bf, value: 0x222b}, + {keysym: 0x08c0, value: 0x2234}, + {keysym: 0x08c1, value: 0x221d}, + {keysym: 0x08c2, value: 0x221e}, + {keysym: 0x08c5, value: 0x2207}, + {keysym: 0x08c8, value: 0x223c}, + {keysym: 0x08c9, value: 0x2243}, + {keysym: 0x08cd, value: 0x21d4}, + {keysym: 0x08ce, value: 0x21d2}, + {keysym: 0x08cf, value: 0x2261}, + {keysym: 0x08d6, value: 0x221a}, + {keysym: 0x08da, value: 0x2282}, + {keysym: 0x08db, value: 0x2283}, + {keysym: 0x08dc, value: 0x2229}, + {keysym: 0x08dd, value: 0x222a}, + {keysym: 0x08de, value: 0x2227}, + {keysym: 0x08df, value: 0x2228}, + {keysym: 0x08ef, value: 0x2202}, + {keysym: 0x08f6, value: 0x0192}, + {keysym: 0x08fb, value: 0x2190}, + {keysym: 0x08fc, value: 0x2191}, + {keysym: 0x08fd, value: 0x2192}, + {keysym: 0x08fe, value: 0x2193}, + {keysym: 0x09e0, value: 0x25c6}, + {keysym: 0x09e1, value: 0x2592}, + {keysym: 0x09e2, value: 0x2409}, + {keysym: 0x09e3, value: 0x240c}, + {keysym: 0x09e4, value: 0x240d}, + {keysym: 0x09e5, value: 0x240a}, + {keysym: 0x09e8, value: 0x2424}, + {keysym: 0x09e9, value: 0x240b}, + {keysym: 0x09ea, value: 0x2518}, + {keysym: 0x09eb, value: 0x2510}, + {keysym: 0x09ec, value: 0x250c}, + {keysym: 0x09ed, value: 0x2514}, + {keysym: 0x09ee, value: 0x253c}, + {keysym: 0x09ef, value: 0x23ba}, + {keysym: 0x09f0, value: 0x23bb}, + {keysym: 0x09f1, value: 0x2500}, + {keysym: 0x09f2, value: 0x23bc}, + {keysym: 0x09f3, value: 0x23bd}, + {keysym: 0x09f4, value: 0x251c}, + {keysym: 0x09f5, value: 0x2524}, + {keysym: 0x09f6, value: 0x2534}, + {keysym: 0x09f7, value: 0x252c}, + {keysym: 0x09f8, value: 0x2502}, + {keysym: 0x0aa1, value: 0x2003}, + {keysym: 0x0aa2, value: 0x2002}, + {keysym: 0x0aa3, value: 0x2004}, + {keysym: 0x0aa4, value: 0x2005}, + {keysym: 0x0aa5, value: 0x2007}, + {keysym: 0x0aa6, value: 0x2008}, + {keysym: 0x0aa7, value: 0x2009}, + {keysym: 0x0aa8, value: 0x200a}, + {keysym: 0x0aa9, value: 0x2014}, + {keysym: 0x0aaa, value: 0x2013}, + {keysym: 0x0aac, value: 0x2423}, + {keysym: 0x0aae, value: 0x2026}, + {keysym: 0x0aaf, value: 0x2025}, + {keysym: 0x0ab0, value: 0x2153}, + {keysym: 0x0ab1, value: 0x2154}, + {keysym: 0x0ab2, value: 0x2155}, + {keysym: 0x0ab3, value: 0x2156}, + {keysym: 0x0ab4, value: 0x2157}, + {keysym: 0x0ab5, value: 0x2158}, + {keysym: 0x0ab6, value: 0x2159}, + {keysym: 0x0ab7, value: 0x215a}, + {keysym: 0x0ab8, value: 0x2105}, + {keysym: 0x0abb, value: 0x2012}, + {keysym: 0x0abc, value: 0x2329}, + {keysym: 0x0abd, value: 0x002e}, + {keysym: 0x0abe, value: 0x232a}, + {keysym: 0x0ac3, value: 0x215b}, + {keysym: 0x0ac4, value: 0x215c}, + {keysym: 0x0ac5, value: 0x215d}, + {keysym: 0x0ac6, value: 0x215e}, + {keysym: 0x0ac9, value: 0x2122}, + {keysym: 0x0aca, value: 0x2613}, + {keysym: 0x0acc, value: 0x25c1}, + {keysym: 0x0acd, value: 0x25b7}, + {keysym: 0x0ace, value: 0x25cb}, + {keysym: 0x0acf, value: 0x25af}, + {keysym: 0x0ad0, value: 0x2018}, + {keysym: 0x0ad1, value: 0x2019}, + {keysym: 0x0ad2, value: 0x201c}, + {keysym: 0x0ad3, value: 0x201d}, + {keysym: 0x0ad4, value: 0x211e}, + {keysym: 0x0ad5, value: 0x2030}, + {keysym: 0x0ad6, value: 0x2032}, + {keysym: 0x0ad7, value: 0x2033}, + {keysym: 0x0ad9, value: 0x271d}, + {keysym: 0x0adb, value: 0x25ac}, + {keysym: 0x0adc, value: 0x25c0}, + {keysym: 0x0add, value: 0x25b6}, + {keysym: 0x0ade, value: 0x25cf}, + {keysym: 0x0adf, value: 0x25ae}, + {keysym: 0x0ae0, value: 0x25e6}, + {keysym: 0x0ae1, value: 0x25ab}, + {keysym: 0x0ae2, value: 0x25ad}, + {keysym: 0x0ae3, value: 0x25b3}, + {keysym: 0x0ae4, value: 0x25bd}, + {keysym: 0x0ae5, value: 0x2606}, + {keysym: 0x0ae6, value: 0x2022}, + {keysym: 0x0ae7, value: 0x25aa}, + {keysym: 0x0ae8, value: 0x25b2}, + {keysym: 0x0ae9, value: 0x25bc}, + {keysym: 0x0aea, value: 0x261c}, + {keysym: 0x0aeb, value: 0x261e}, + {keysym: 0x0aec, value: 0x2663}, + {keysym: 0x0aed, value: 0x2666}, + {keysym: 0x0aee, value: 0x2665}, + {keysym: 0x0af0, value: 0x2720}, + {keysym: 0x0af1, value: 0x2020}, + {keysym: 0x0af2, value: 0x2021}, + {keysym: 0x0af3, value: 0x2713}, + {keysym: 0x0af4, value: 0x2717}, + {keysym: 0x0af5, value: 0x266f}, + {keysym: 0x0af6, value: 0x266d}, + {keysym: 0x0af7, value: 0x2642}, + {keysym: 0x0af8, value: 0x2640}, + {keysym: 0x0af9, value: 0x260e}, + {keysym: 0x0afa, value: 0x2315}, + {keysym: 0x0afb, value: 0x2117}, + {keysym: 0x0afc, value: 0x2038}, + {keysym: 0x0afd, value: 0x201a}, + {keysym: 0x0afe, value: 0x201e}, + {keysym: 0x0ba3, value: 0x003c}, + {keysym: 0x0ba6, value: 0x003e}, + {keysym: 0x0ba8, value: 0x2228}, + {keysym: 0x0ba9, value: 0x2227}, + {keysym: 0x0bc0, value: 0x00af}, + {keysym: 0x0bc2, value: 0x22a4}, + {keysym: 0x0bc3, value: 0x2229}, + {keysym: 0x0bc4, value: 0x230a}, + {keysym: 0x0bc6, value: 0x005f}, + {keysym: 0x0bca, value: 0x2218}, + {keysym: 0x0bcc, value: 0x2395}, + {keysym: 0x0bce, value: 0x22a5}, + {keysym: 0x0bcf, value: 0x25cb}, + {keysym: 0x0bd3, value: 0x2308}, + {keysym: 0x0bd6, value: 0x222a}, + {keysym: 0x0bd8, value: 0x2283}, + {keysym: 0x0bda, value: 0x2282}, + {keysym: 0x0bdc, value: 0x22a3}, + {keysym: 0x0bfc, value: 0x22a2}, + {keysym: 0x0cdf, value: 0x2017}, + {keysym: 0x0ce0, value: 0x05d0}, + {keysym: 0x0ce1, value: 0x05d1}, + {keysym: 0x0ce2, value: 0x05d2}, + {keysym: 0x0ce3, value: 0x05d3}, + {keysym: 0x0ce4, value: 0x05d4}, + {keysym: 0x0ce5, value: 0x05d5}, + {keysym: 0x0ce6, value: 0x05d6}, + {keysym: 0x0ce7, value: 0x05d7}, + {keysym: 0x0ce8, value: 0x05d8}, + {keysym: 0x0ce9, value: 0x05d9}, + {keysym: 0x0cea, value: 0x05da}, + {keysym: 0x0ceb, value: 0x05db}, + {keysym: 0x0cec, value: 0x05dc}, + {keysym: 0x0ced, value: 0x05dd}, + {keysym: 0x0cee, value: 0x05de}, + {keysym: 0x0cef, value: 0x05df}, + {keysym: 0x0cf0, value: 0x05e0}, + {keysym: 0x0cf1, value: 0x05e1}, + {keysym: 0x0cf2, value: 0x05e2}, + {keysym: 0x0cf3, value: 0x05e3}, + {keysym: 0x0cf4, value: 0x05e4}, + {keysym: 0x0cf5, value: 0x05e5}, + {keysym: 0x0cf6, value: 0x05e6}, + {keysym: 0x0cf7, value: 0x05e7}, + {keysym: 0x0cf8, value: 0x05e8}, + {keysym: 0x0cf9, value: 0x05e9}, + {keysym: 0x0cfa, value: 0x05ea}, + {keysym: 0x0da1, value: 0x0e01}, + {keysym: 0x0da2, value: 0x0e02}, + {keysym: 0x0da3, value: 0x0e03}, + {keysym: 0x0da4, value: 0x0e04}, + {keysym: 0x0da5, value: 0x0e05}, + {keysym: 0x0da6, value: 0x0e06}, + {keysym: 0x0da7, value: 0x0e07}, + {keysym: 0x0da8, value: 0x0e08}, + {keysym: 0x0da9, value: 0x0e09}, + {keysym: 0x0daa, value: 0x0e0a}, + {keysym: 0x0dab, value: 0x0e0b}, + {keysym: 0x0dac, value: 0x0e0c}, + {keysym: 0x0dad, value: 0x0e0d}, + {keysym: 0x0dae, value: 0x0e0e}, + {keysym: 0x0daf, value: 0x0e0f}, + {keysym: 0x0db0, value: 0x0e10}, + {keysym: 0x0db1, value: 0x0e11}, + {keysym: 0x0db2, value: 0x0e12}, + {keysym: 0x0db3, value: 0x0e13}, + {keysym: 0x0db4, value: 0x0e14}, + {keysym: 0x0db5, value: 0x0e15}, + {keysym: 0x0db6, value: 0x0e16}, + {keysym: 0x0db7, value: 0x0e17}, + {keysym: 0x0db8, value: 0x0e18}, + {keysym: 0x0db9, value: 0x0e19}, + {keysym: 0x0dba, value: 0x0e1a}, + {keysym: 0x0dbb, value: 0x0e1b}, + {keysym: 0x0dbc, value: 0x0e1c}, + {keysym: 0x0dbd, value: 0x0e1d}, + {keysym: 0x0dbe, value: 0x0e1e}, + {keysym: 0x0dbf, value: 0x0e1f}, + {keysym: 0x0dc0, value: 0x0e20}, + {keysym: 0x0dc1, value: 0x0e21}, + {keysym: 0x0dc2, value: 0x0e22}, + {keysym: 0x0dc3, value: 0x0e23}, + {keysym: 0x0dc4, value: 0x0e24}, + {keysym: 0x0dc5, value: 0x0e25}, + {keysym: 0x0dc6, value: 0x0e26}, + {keysym: 0x0dc7, value: 0x0e27}, + {keysym: 0x0dc8, value: 0x0e28}, + {keysym: 0x0dc9, value: 0x0e29}, + {keysym: 0x0dca, value: 0x0e2a}, + {keysym: 0x0dcb, value: 0x0e2b}, + {keysym: 0x0dcc, value: 0x0e2c}, + {keysym: 0x0dcd, value: 0x0e2d}, + {keysym: 0x0dce, value: 0x0e2e}, + {keysym: 0x0dcf, value: 0x0e2f}, + {keysym: 0x0dd0, value: 0x0e30}, + {keysym: 0x0dd1, value: 0x0e31}, + {keysym: 0x0dd2, value: 0x0e32}, + {keysym: 0x0dd3, value: 0x0e33}, + {keysym: 0x0dd4, value: 0x0e34}, + {keysym: 0x0dd5, value: 0x0e35}, + {keysym: 0x0dd6, value: 0x0e36}, + {keysym: 0x0dd7, value: 0x0e37}, + {keysym: 0x0dd8, value: 0x0e38}, + {keysym: 0x0dd9, value: 0x0e39}, + {keysym: 0x0dda, value: 0x0e3a}, + {keysym: 0x0dde, value: 0x0e3e}, + {keysym: 0x0ddf, value: 0x0e3f}, + {keysym: 0x0de0, value: 0x0e40}, + {keysym: 0x0de1, value: 0x0e41}, + {keysym: 0x0de2, value: 0x0e42}, + {keysym: 0x0de3, value: 0x0e43}, + {keysym: 0x0de4, value: 0x0e44}, + {keysym: 0x0de5, value: 0x0e45}, + {keysym: 0x0de6, value: 0x0e46}, + {keysym: 0x0de7, value: 0x0e47}, + {keysym: 0x0de8, value: 0x0e48}, + {keysym: 0x0de9, value: 0x0e49}, + {keysym: 0x0dea, value: 0x0e4a}, + {keysym: 0x0deb, value: 0x0e4b}, + {keysym: 0x0dec, value: 0x0e4c}, + {keysym: 0x0ded, value: 0x0e4d}, + {keysym: 0x0df0, value: 0x0e50}, + {keysym: 0x0df1, value: 0x0e51}, + {keysym: 0x0df2, value: 0x0e52}, + {keysym: 0x0df3, value: 0x0e53}, + {keysym: 0x0df4, value: 0x0e54}, + {keysym: 0x0df5, value: 0x0e55}, + {keysym: 0x0df6, value: 0x0e56}, + {keysym: 0x0df7, value: 0x0e57}, + {keysym: 0x0df8, value: 0x0e58}, + {keysym: 0x0df9, value: 0x0e59}, + {keysym: 0x0ea1, value: 0x3131}, + {keysym: 0x0ea2, value: 0x3132}, + {keysym: 0x0ea3, value: 0x3133}, + {keysym: 0x0ea4, value: 0x3134}, + {keysym: 0x0ea5, value: 0x3135}, + {keysym: 0x0ea6, value: 0x3136}, + {keysym: 0x0ea7, value: 0x3137}, + {keysym: 0x0ea8, value: 0x3138}, + {keysym: 0x0ea9, value: 0x3139}, + {keysym: 0x0eaa, value: 0x313a}, + {keysym: 0x0eab, value: 0x313b}, + {keysym: 0x0eac, value: 0x313c}, + {keysym: 0x0ead, value: 0x313d}, + {keysym: 0x0eae, value: 0x313e}, + {keysym: 0x0eaf, value: 0x313f}, + {keysym: 0x0eb0, value: 0x3140}, + {keysym: 0x0eb1, value: 0x3141}, + {keysym: 0x0eb2, value: 0x3142}, + {keysym: 0x0eb3, value: 0x3143}, + {keysym: 0x0eb4, value: 0x3144}, + {keysym: 0x0eb5, value: 0x3145}, + {keysym: 0x0eb6, value: 0x3146}, + {keysym: 0x0eb7, value: 0x3147}, + {keysym: 0x0eb8, value: 0x3148}, + {keysym: 0x0eb9, value: 0x3149}, + {keysym: 0x0eba, value: 0x314a}, + {keysym: 0x0ebb, value: 0x314b}, + {keysym: 0x0ebc, value: 0x314c}, + {keysym: 0x0ebd, value: 0x314d}, + {keysym: 0x0ebe, value: 0x314e}, + {keysym: 0x0ebf, value: 0x314f}, + {keysym: 0x0ec0, value: 0x3150}, + {keysym: 0x0ec1, value: 0x3151}, + {keysym: 0x0ec2, value: 0x3152}, + {keysym: 0x0ec3, value: 0x3153}, + {keysym: 0x0ec4, value: 0x3154}, + {keysym: 0x0ec5, value: 0x3155}, + {keysym: 0x0ec6, value: 0x3156}, + {keysym: 0x0ec7, value: 0x3157}, + {keysym: 0x0ec8, value: 0x3158}, + {keysym: 0x0ec9, value: 0x3159}, + {keysym: 0x0eca, value: 0x315a}, + {keysym: 0x0ecb, value: 0x315b}, + {keysym: 0x0ecc, value: 0x315c}, + {keysym: 0x0ecd, value: 0x315d}, + {keysym: 0x0ece, value: 0x315e}, + {keysym: 0x0ecf, value: 0x315f}, + {keysym: 0x0ed0, value: 0x3160}, + {keysym: 0x0ed1, value: 0x3161}, + {keysym: 0x0ed2, value: 0x3162}, + {keysym: 0x0ed3, value: 0x3163}, + {keysym: 0x0ed4, value: 0x11a8}, + {keysym: 0x0ed5, value: 0x11a9}, + {keysym: 0x0ed6, value: 0x11aa}, + {keysym: 0x0ed7, value: 0x11ab}, + {keysym: 0x0ed8, value: 0x11ac}, + {keysym: 0x0ed9, value: 0x11ad}, + {keysym: 0x0eda, value: 0x11ae}, + {keysym: 0x0edb, value: 0x11af}, + {keysym: 0x0edc, value: 0x11b0}, + {keysym: 0x0edd, value: 0x11b1}, + {keysym: 0x0ede, value: 0x11b2}, + {keysym: 0x0edf, value: 0x11b3}, + {keysym: 0x0ee0, value: 0x11b4}, + {keysym: 0x0ee1, value: 0x11b5}, + {keysym: 0x0ee2, value: 0x11b6}, + {keysym: 0x0ee3, value: 0x11b7}, + {keysym: 0x0ee4, value: 0x11b8}, + {keysym: 0x0ee5, value: 0x11b9}, + {keysym: 0x0ee6, value: 0x11ba}, + {keysym: 0x0ee7, value: 0x11bb}, + {keysym: 0x0ee8, value: 0x11bc}, + {keysym: 0x0ee9, value: 0x11bd}, + {keysym: 0x0eea, value: 0x11be}, + {keysym: 0x0eeb, value: 0x11bf}, + {keysym: 0x0eec, value: 0x11c0}, + {keysym: 0x0eed, value: 0x11c1}, + {keysym: 0x0eee, value: 0x11c2}, + {keysym: 0x0eef, value: 0x316d}, + {keysym: 0x0ef0, value: 0x3171}, + {keysym: 0x0ef1, value: 0x3178}, + {keysym: 0x0ef2, value: 0x317f}, + {keysym: 0x0ef3, value: 0x3181}, + {keysym: 0x0ef4, value: 0x3184}, + {keysym: 0x0ef5, value: 0x3186}, + {keysym: 0x0ef6, value: 0x318d}, + {keysym: 0x0ef7, value: 0x318e}, + {keysym: 0x0ef8, value: 0x11eb}, + {keysym: 0x0ef9, value: 0x11f0}, + {keysym: 0x0efa, value: 0x11f9}, + {keysym: 0x0eff, value: 0x20a9}, + {keysym: 0x13bc, value: 0x0152}, + {keysym: 0x13bd, value: 0x0153}, + {keysym: 0x13be, value: 0x0178}, + {keysym: 0x20ac, value: 0x20ac}, +} + +func windowsLegacyRuneForKeysym(keysym uint32) (rune, bool) { + if keysym > 0xffff { + return 0, false + } + low, high := 0, len(windowsLegacyKeysymRunes) + for low < high { + middle := low + (high-low)/2 + candidate := uint32(windowsLegacyKeysymRunes[middle].keysym) + if candidate < keysym { + low = middle + 1 + } else { + high = middle + } + } + if low == len(windowsLegacyKeysymRunes) || uint32(windowsLegacyKeysymRunes[low].keysym) != keysym { + return 0, false + } + return windowsLegacyKeysymRunes[low].value, true +} From edafcaf2fea07a8d104832a1e6111494cf1f524e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 18 Jul 2026 07:53:13 -0700 Subject: [PATCH 2/3] docs(connect): document Windows backend boundary --- CHANGELOG.md | 1 + README.md | 20 ++++++++++++-------- cmd/crabfleet-connect/README.md | 11 +++++++++-- docs/macos-native-client.md | 27 ++++++++++++++++----------- 4 files changed, 38 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0b22e6..4e38d45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - Add opt-in single-folder sharing to Share This Mac with security-scoped bookmark persistence, negotiated `FSH1` browsing, bounded native and browser download/upload streams, strict realpath containment, 512 MiB file and 256 KiB chunk limits, root-local temporary uploads with atomic rename and teardown cleanup, and host-controlled remote writes. +- Add a pure-Go Windows Crabfleet Connect backend with synchronized GDI BitBlt primary-display capture, full-frame RGBA dirty updates, SendInput absolute pointer, wheel, and keyboard injection, active-layout shortcuts, Unicode and legacy X11 keysym text mapping, retry-safe teardown, and amd64/arm64 cross-build proof, while deferring DXGI, multi-monitor, per-monitor-DPI, packaging, and real-hardware validation. - Add the Crabfleet Connect foundation with a shared Go RFB 3.8 host core, per-run VNC-DES authentication, Tight/JPEG and client-side cursor/input support, a CI-safe synthetic backend, and a Linux X11 MIT-SHM/XFixes/XTest backend plus cross-compiled CLI, while documenting deferred codecs, Wayland, ARD, audio, and hardware validation. - Harden Share This Mac tailnet identity resolution with stable multi-IPv4 selection and candidate exposure, MagicDNS-free hostname fallback, actionable backend-state and malformed-status errors, and an explicit unsupported IPv6-only listener result. - Add a default-off Share This Mac Remote Desktop-permission experiment with persisted Privacy Settings selection, live ScreenCaptureKit capability polling, direct System Settings guidance, and unchanged Screen Recording defaults for macOS 26 capture-indicator research. diff --git a/README.md b/README.md index 1bf58ef..c84f9b2 100644 --- a/README.md +++ b/README.md @@ -430,22 +430,26 @@ Full documentation available at [docs.crabfleet.ai](https://docs.crabfleet.ai): - [API](https://docs.crabfleet.ai/api) – REST and WebSocket APIs - [Spec](https://docs.crabfleet.ai/spec) – Complete product specification -## Crabfleet Connect for Linux +## Crabfleet Connect for Linux and Windows `crabfleet-connect` is the first cross-platform host foundation for sharing a -Linux machine to the native macOS viewer or browser client. It provides a Go -RFB 3.8 server, a fresh per-run VNC password, Tight/JPEG frames, client-side -cursor updates, remote input, a synthetic test backend, and a Linux X11 -MIT-SHM/XFixes/XTest backend. The X11 backend cross-compiles in CI but has not -been validated on physical Linux hardware yet. +Linux or Windows machine to the native macOS viewer or browser client. It +provides a Go RFB 3.8 server, a fresh per-run VNC password, Tight/JPEG frames, +client-side cursor updates, remote input, a synthetic test backend, a Linux X11 +MIT-SHM/XFixes/XTest backend, and a pure-Go Windows backend using synchronized +GDI `BitBlt` primary-display capture plus `SendInput`. The Windows key path +combines virtual keys for named keys and active-layout shortcuts with Unicode +text injection and canonical legacy X11 keysym conversion. Both native paths +cross-compile, but neither has been validated on physical target hardware yet. The CLI listens on loopback by default because VNC-DES does not encrypt RFB traffic. Remote use requires an explicit `--bind` on an already protected private path. This increment does not provide ARD host authentication, H.264 or HEVC -encoding, Wayland/PipeWire capture, multi-group XKB input, audio, clipboard -synchronization, or service packaging. See +encoding, Wayland/PipeWire capture, Windows DXGI Desktop Duplication, +Windows multi-monitor or per-monitor-DPI support, multi-group XKB input, audio, +clipboard synchronization, or service packaging. See [`cmd/crabfleet-connect/README.md`](cmd/crabfleet-connect/README.md) for the exact boundary and run command. diff --git a/cmd/crabfleet-connect/README.md b/cmd/crabfleet-connect/README.md index febf8fb..757cf44 100644 --- a/cmd/crabfleet-connect/README.md +++ b/cmd/crabfleet-connect/README.md @@ -1,6 +1,6 @@ # Crabfleet Connect foundation -`crabfleet-connect` is the first Linux host foundation for sharing a machine to Crabfleet's macOS viewer or browser client. +`crabfleet-connect` is the cross-platform host foundation for sharing a Linux or Windows machine to Crabfleet's macOS viewer or browser client. Real in this increment: @@ -8,11 +8,12 @@ Real in this increment: - Tight/JPEG full-frame updates, client-side cursor pseudo-encodings, pointer/key input, strict protocol bounds, bounded sessions, and idempotent teardown. - A pure-Go synthetic capture/input backend used by tests and as an explicit fallback. - A Linux X11 backend implemented with MIT-SHM `XShmGetImage` capture, XFixes cursor images, and XTest input with key-level modifier mapping. It compiles for Linux; it has not been validated on physical Linux hardware in this track. Multi-group XKB layouts fail closed to the synthetic fallback rather than risking incorrect input. +- A pure-Go Windows backend implemented with synchronized GDI `BitBlt` primary-display capture and `SendInput` for absolute pointer, wheel, virtual-key shortcut, and layout-independent Unicode text input. Its named-key table and canonical legacy X11 keysym conversion are tested on every platform, and the host cross-builds as amd64 and arm64 PE executables. It has not been run on physical Windows hardware in this track. Deferred: - ARD host authentication. Type 30 remains in the Track H-compatible security offer but fails closed; VNC-DES type 2 is the working MVP. -- H.264/HEVC hardware encoding, Wayland/PipeWire capture, multi-group XKB input, audio, clipboard synchronization, packaging/service installation, and real-hardware validation. +- H.264/HEVC hardware encoding, Wayland/PipeWire capture, Windows DXGI Desktop Duplication, Windows multi-monitor and per-monitor-DPI support, multi-group XKB input, audio, clipboard synchronization, packaging/service installation, and real-hardware validation. Run: @@ -20,4 +21,10 @@ Run: go run ./cmd/crabfleet-connect --display :0 --port 5900 ``` +On Windows, omit `--display`; the native backend captures the primary display: + +```powershell +go run ./cmd/crabfleet-connect --port 5900 +``` + The process prints its per-run share password and listener address. If native capture cannot initialize, it says why and uses the synthetic test pattern. Use `--synthetic` to force that backend. The listener defaults to `127.0.0.1` because VNC-DES authenticates but does not encrypt the RFB session. Use `--bind` to select a private interface only when the network path is already protected, such as through an authenticated tunnel; `--bind 0.0.0.0` is an explicit insecure exposure. diff --git a/docs/macos-native-client.md b/docs/macos-native-client.md index 99d7d25..8aa7b00 100644 --- a/docs/macos-native-client.md +++ b/docs/macos-native-client.md @@ -63,26 +63,31 @@ full UTF-8 text with servers that negotiate it; against servers without the extension, standard cut text must encode losslessly as ISO-8859-1 and unsupported text is rejected instead of silently becoming empty data. -## Linux Connect foundation +## Linux and Windows Connect foundation The native viewer can also connect directly to the first -`crabfleet-connect` Linux host foundation. That Go host speaks RFB 3.8 with a -fresh per-run VNC-DES password, Tight/JPEG full-frame updates, client-side -cursor rectangles, and pointer/key input. Its synthetic backend provides the -portable CI and protocol-test path. The Linux-only backend implements X11 -capture with MIT-SHM `XShmGetImage`, cursor images with XFixes, and input with -XTest; it cross-compiles for amd64 and arm64 but has not been exercised on -physical Linux hardware in this increment. +`crabfleet-connect` Linux and Windows host foundation. That Go host speaks RFB +3.8 with a fresh per-run VNC-DES password, Tight/JPEG full-frame updates, +client-side cursor rectangles, and pointer/key input. Its synthetic backend +provides the portable CI and protocol-test path. The Linux backend implements +X11 capture with MIT-SHM `XShmGetImage`, cursor images with XFixes, and input +with XTest. The pure-Go Windows backend captures the primary display into RGBA +frames with synchronized GDI `BitBlt` and injects absolute pointer, wheel, and +keyboard input with `SendInput`; named keys and active-layout shortcuts use +virtual keys, while text uses Unicode injection plus canonical legacy X11 +keysym conversion. Both native paths cross-compile for amd64 and arm64. Neither +has been exercised on physical target hardware in these increments. The Connect listener defaults to loopback because VNC-DES does not encrypt RFB traffic. A remote listener requires an explicit private bind on a separately protected network path. -The Linux host still advertises the direct-listener ARD security type for wire +The Connect host still advertises the direct-listener ARD security type for wire compatibility, but ARD host authentication fails closed and viewers must select VNC password authentication. H.264/HEVC encoding, Wayland/PipeWire, audio, -multi-group XKB input, clipboard synchronization, service packaging, and -real-hardware validation remain follow-up work. +Windows DXGI Desktop Duplication, Windows multi-monitor and per-monitor-DPI +support, multi-group XKB input, clipboard synchronization, service packaging, +and real-hardware validation remain follow-up work. ## Wake-on-LAN From 8bb34d418ed2b86bb9d68a4ed311e07524621da4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 18 Jul 2026 08:27:38 -0700 Subject: [PATCH 3/3] fix(connect): preserve Unicode text with held Shift --- internal/connect/windows.go | 3 +-- internal/connect/windows_keymap_test.go | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/internal/connect/windows.go b/internal/connect/windows.go index 7518546..49f6d84 100644 --- a/internal/connect/windows.go +++ b/internal/connect/windows.go @@ -518,8 +518,7 @@ func (backend *WindowsBackend) explicitShiftInputsExcept(except uint32) []window func (backend *WindowsBackend) shortcutModifierHeld() bool { for _, held := range backend.heldKeys { if len(held.unicodeUnits) == 0 && - (windowsVirtualKeyIsShift(held.binding.virtualKey) || - windowsVirtualKeyIsShortcutModifier(held.binding.virtualKey)) { + windowsVirtualKeyIsShortcutModifier(held.binding.virtualKey) { return true } } diff --git a/internal/connect/windows_keymap_test.go b/internal/connect/windows_keymap_test.go index 04ca218..1f849ea 100644 --- a/internal/connect/windows_keymap_test.go +++ b/internal/connect/windows_keymap_test.go @@ -56,3 +56,17 @@ func TestWindowsRuneRejectsNonCharacterKeysyms(t *testing.T) { } } } + +func TestWindowsShortcutModifiersExcludeShift(t *testing.T) { + t.Parallel() + for _, virtualKey := range []uint16{windowsVKShift, windowsVKLShift, windowsVKRShift} { + if windowsVirtualKeyIsShortcutModifier(virtualKey) { + t.Fatalf("treated Shift virtual key %#x as a shortcut modifier", virtualKey) + } + } + for _, virtualKey := range []uint16{windowsVKLControl, windowsVKRMenu, windowsVKLWin} { + if !windowsVirtualKeyIsShortcutModifier(virtualKey) { + t.Fatalf("did not treat virtual key %#x as a shortcut modifier", virtualKey) + } + } +}