From bbb6cc4dc1fb25ac5df48b30540268a1bf0b99a3 Mon Sep 17 00:00:00 2001 From: Unrud Date: Sun, 26 Feb 2023 16:45:37 +0100 Subject: [PATCH] uinput-controller: Initial support for keymaps Keymap can be set with environment variable REMOTE_TOUCHPAD_UINPUT_KEYMAP --- inputcontrol/controller_uinput.go | 127 ++++++++++++----------------- inputcontrol/keymap.go | 130 ++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+), 76 deletions(-) create mode 100644 inputcontrol/keymap.go diff --git a/inputcontrol/controller_uinput.go b/inputcontrol/controller_uinput.go index df6b5a2..d6adedc 100644 --- a/inputcontrol/controller_uinput.go +++ b/inputcontrol/controller_uinput.go @@ -25,67 +25,14 @@ package inputcontrol import ( "fmt" "github.com/bendahl/uinput" - "unicode" + "os" + "time" ) -var ukeysMap = map[rune]int{ - 'a': uinput.KeyA, - 'b': uinput.KeyB, - 'c': uinput.KeyC, - 'd': uinput.KeyD, - 'e': uinput.KeyE, - 'f': uinput.KeyF, - 'g': uinput.KeyG, - 'h': uinput.KeyH, - 'i': uinput.KeyI, - 'j': uinput.KeyJ, - 'k': uinput.KeyK, - 'l': uinput.KeyL, - 'm': uinput.KeyM, - 'n': uinput.KeyN, - 'o': uinput.KeyO, - 'p': uinput.KeyP, - 'q': uinput.KeyQ, - 'r': uinput.KeyR, - 's': uinput.KeyS, - 't': uinput.KeyT, - 'u': uinput.KeyU, - 'v': uinput.KeyV, - 'w': uinput.KeyW, - 'x': uinput.KeyX, - 'y': uinput.KeyY, - 'z': uinput.KeyZ, - '0': uinput.Key0, - '1': uinput.Key1, - '2': uinput.Key2, - '3': uinput.Key3, - '4': uinput.Key4, - '5': uinput.Key5, - '6': uinput.Key6, - '7': uinput.Key7, - '8': uinput.Key8, - '9': uinput.Key9, - 0x1b: uinput.KeyEsc, - '-': uinput.KeyMinus, - '+': uinput.KeyKpplus, - '*': uinput.KeyKpasterisk, - '=': uinput.KeyEqual, - 0x08: uinput.KeyBackspace, - '\t': uinput.KeyTab, - '(': uinput.KeyLeftbrace, - ')': uinput.KeyRightbrace, - '\n': uinput.KeyEnter, - ';': uinput.KeySemicolon, - '\'': uinput.KeyApostrophe, - '`': uinput.KeyGrave, - '\\': uinput.KeyBackslash, - ',': uinput.KeyComma, - '.': uinput.KeyDot, - '/': uinput.KeySlash, - ' ': uinput.KeySpace, -} +const shiftKeysDelay time.Duration = 50 * time.Millisecond type uinputController struct { + keymap *Keymap keyboard uinput.Keyboard mouse uinput.Mouse } @@ -95,6 +42,14 @@ func init() { } func InitUinputController() (Controller, error) { + keymapName := "defkeymap" + if s, ok := os.LookupEnv("REMOTE_TOUCHPAD_UINPUT_KEYMAP"); ok { + keymapName = s + } + keymap, err := LoadKeymap(keymapName) + if err != nil { + return nil, err + } keyboard, err := uinput.CreateKeyboard("/dev/uinput", []byte("remote-touchpad-keyboard")) if err != nil { return nil, err @@ -104,7 +59,7 @@ func InitUinputController() (Controller, error) { keyboard.Close() return nil, err } - return &uinputController{keyboard, mouse}, nil + return &uinputController{keymap, keyboard, mouse}, nil } func (p *uinputController) Close() error { @@ -115,33 +70,53 @@ func (p *uinputController) Close() error { return p.mouse.Close() } -// No support for keyboard layouts, only works with QWERTY func (p *uinputController) KeyboardText(text string) error { - for _, runeValue := range text { - modifierShift := false - if unicode.IsUpper(runeValue) { - modifierShift = true - runeValue = unicode.ToLower(runeValue) + var activeShiftKeys KeyCombo + updateShiftKeys := func(keyCombo KeyCombo) error { + if activeShiftKeys.ShiftKeys == keyCombo.ShiftKeys { + activeShiftKeys.Key = keyCombo.Key + return nil } - uinputKey, found := ukeysMap[runeValue] + if activeShiftKeys.Key != 0 { + time.Sleep(shiftKeysDelay) + activeShiftKeys.Key = 0 + } + for i := range keyCombo.ShiftKeys { + if activeShiftKeys.ShiftKeys[i] != keyCombo.ShiftKeys[i] { + if activeShiftKeys.ShiftKeys[i] != 0 { + if err := p.keyboard.KeyUp(activeShiftKeys.ShiftKeys[i]); err != nil { + return err + } + activeShiftKeys.ShiftKeys[i] = 0 + } + if keyCombo.ShiftKeys[i] != 0 { + if err := p.keyboard.KeyDown(keyCombo.ShiftKeys[i]); err != nil { + return err + } + activeShiftKeys.ShiftKeys[i] = keyCombo.ShiftKeys[i] + } + } + } + if keyCombo.Key != 0 { + activeShiftKeys.Key = keyCombo.Key + time.Sleep(shiftKeysDelay) + } + return nil + } + defer updateShiftKeys(KeyCombo{}) + for _, runeValue := range text { + keyCombo, found := p.keymap.Get(runeValue) if !found { return fmt.Errorf("unsupported rune: %q", runeValue) } - if modifierShift { - if err := p.keyboard.KeyDown(uinput.KeyLeftshift); err != nil { - return err - } - } - if err := p.keyboard.KeyPress(uinputKey); err != nil { + if err := updateShiftKeys(keyCombo); err != nil { return err } - if modifierShift { - if err := p.keyboard.KeyUp(uinput.KeyLeftshift); err != nil { - return err - } + if err := p.keyboard.KeyPress(keyCombo.Key); err != nil { + return err } } - return nil + return updateShiftKeys(KeyCombo{Key: 1}) } func (p *uinputController) KeyboardKey(key Key) error { diff --git a/inputcontrol/keymap.go b/inputcontrol/keymap.go new file mode 100644 index 0000000..3989bb8 --- /dev/null +++ b/inputcontrol/keymap.go @@ -0,0 +1,130 @@ +//go:build uinput + +/* + * Copyright (c) 2023 Unrud + * + * This file is part of Remote-Touchpad. + * + * Remote-Touchpad is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Remote-Touchpad is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Remote-Touchpad. If not, see . + */ + +package inputcontrol + +import ( + "fmt" + "os/exec" + "strings" + "unsafe" +) + +const bkeymapFileSignature = "bkeymap" + +// uapi/linux/keyboard.h +const ( + nrKeys = 128 + maxNrKeymaps = 256 + nrShift = 9 + + ktLatin = 0 + ktShift = 7 + ktLetter = 11 +) + +type KeyCombo struct { + Key int + ShiftKeys [nrShift]int +} + +type Keymap struct { + keyComboMap map[rune]KeyCombo +} + +func (keymap Keymap) Get(character rune) (keyCombo KeyCombo, found bool) { + keyCombo, found = keymap.keyComboMap[character] + return +} + +func LoadKeymap(name string) (*Keymap, error) { + if len(name) == 0 || strings.HasPrefix(name, "-") { + return nil, fmt.Errorf("invalid keymap name: %#v", name) + } + bkeymap, err := exec.Command("loadkeys", "--bkeymap", name).Output() + if err != nil { + return nil, err + } + if !strings.HasPrefix(string(bkeymap), bkeymapFileSignature) { + return nil, fmt.Errorf("invalid bkeymap: signature not found") + } + bkeymapHeader := bkeymap[len(bkeymapFileSignature):] + var keymaps [][nrKeys]int + if len(bkeymapHeader) < maxNrKeymaps { + return nil, fmt.Errorf("invalid bkeymap: EOF") + } + currentBkeymap := bkeymapHeader[maxNrKeymaps:] + for keymapIndex := 0; keymapIndex < maxNrKeymaps; keymapIndex += 1 { + if bkeymapHeader[keymapIndex] == 0 { + continue + } + if len(currentBkeymap) < nrKeys*2 { + return nil, fmt.Errorf("invalid bkeymap: EOF") + } + for len(keymaps) <= keymapIndex { + keymaps = append(keymaps, [nrKeys]int{}) + } + for key := 0; key < nrKeys; key += 1 { + var keysym uint16 + copy(unsafe.Slice((*byte)(unsafe.Pointer(&keysym)), 2), + currentBkeymap[key*2:]) + keymaps[keymapIndex][key] = int(keysym) + } + currentBkeymap = currentBkeymap[nrKeys*2:] + } + keymap := Keymap{keyComboMap: make(map[rune]KeyCombo)} + if len(keymaps) == 0 { + return &keymap, nil + } + var shiftKeys [nrShift]int + for key, keysym := range keymaps[0] { + keyType := keysym >> 8 + keyValue := keysym & 0xff + if keyType == ktShift && keyValue < nrShift && shiftKeys[keyValue] == 0 { + shiftKeys[keyValue] = key + } + } +keymapLoop: + for keymapIndex := range keymaps { + var activeShiftKeys [nrShift]int + for i, shiftKey := range shiftKeys { + if keymapIndex&(1<> 8 + keyValue := keysym & 0xff + if (keyType != ktLatin && keyType != ktLetter) || keyValue == 0 { + continue + } + character := rune(keyValue) + if _, ok := keymap.keyComboMap[character]; ok { + continue + } + keymap.keyComboMap[character] = KeyCombo{key, activeShiftKeys} + } + } + return &keymap, nil +}