From ae71ac98398ce213eeaf3fffc411fa5e6068951f Mon Sep 17 00:00:00 2001 From: Damien Date: Wed, 6 Mar 2024 11:02:42 +1100 Subject: [PATCH 1/2] feat(keyboard): keyboard state/layout from events --- cmd/evtest/main.go | 37 +++++++++++++++++--- kbext/azerty.go | 1 + kbext/keymap.go | 7 ++++ kbext/layouts.go | 23 +++++++++++++ kbext/querty.go | 84 +++++++++++++++++++++++++++++++++++++++++++++ kbext/resolve.go | 85 ++++++++++++++++++++++++++++++++++++++++++++++ keyevent.go | 58 +++++++++++++++++++++++++++++++ 7 files changed, 291 insertions(+), 4 deletions(-) create mode 100644 kbext/azerty.go create mode 100644 kbext/keymap.go create mode 100644 kbext/layouts.go create mode 100644 kbext/querty.go create mode 100644 kbext/resolve.go create mode 100644 keyevent.go diff --git a/cmd/evtest/main.go b/cmd/evtest/main.go index e7ce220..89a106b 100644 --- a/cmd/evtest/main.go +++ b/cmd/evtest/main.go @@ -1,10 +1,12 @@ package main import ( + "flag" "fmt" "os" "github.com/holoplot/go-evdev" + "github.com/holoplot/go-evdev/kbext" ) func listDevices() { @@ -18,18 +20,36 @@ func listDevices() { } } +func listLayouts() { + ids := kbext.LayoutKeys() + for _, d := range ids { + fmt.Println(d) + } +} + +var ( + kbLayoutStr string +) + func main() { - if len(os.Args) < 2 { + flag.StringVar(&kbLayoutStr, "kblayout", string(kbext.LayoutQuertyEnUs), "Keyboard layout") + flag.Parse() + + args := flag.Args() + if len(args) < 1 { fmt.Printf("Usage: %s \n\n", os.Args[0]) fmt.Printf("Available devices:\n") listDevices() + + fmt.Printf("\nAvailable keyboard layouts:\n") + listLayouts() return } - d, err := evdev.Open(os.Args[1]) + d, err := evdev.Open(args[0]) if err != nil { - fmt.Printf("Cannot read %s: %v\n", os.Args[1], err) + fmt.Printf("Cannot read %s: %v\n", args[0], err) return } @@ -104,7 +124,8 @@ func main() { fmt.Printf(" Property type %d (%s)\n", p, evdev.PropName(p)) } - fmt.Printf("Testing ... (interrupt to exit)\n") + fmt.Printf("Testing (kb layout=%s)... (interrupt to exit)\n", kbLayoutStr) + kbState := kbext.NewKbState(kbext.LayoutID(kbLayoutStr)) for { e, err := d.ReadOne() @@ -125,6 +146,14 @@ func main() { default: fmt.Printf("%s, -------------- %s ------------\n", ts, e.CodeName()) } + case evdev.EV_KEY: + kbevt := evdev.NewKeyEvent(e) + fmt.Printf("%s, %s\n", ts, kbevt.String()) + + pressed, err := kbState.KeyEvent(kbevt) + if err == nil { + fmt.Printf(" kb char: %s\n", pressed) + } default: fmt.Printf("%s, %s\n", ts, e.String()) } diff --git a/kbext/azerty.go b/kbext/azerty.go new file mode 100644 index 0000000..d51c43f --- /dev/null +++ b/kbext/azerty.go @@ -0,0 +1 @@ +package kbext diff --git a/kbext/keymap.go b/kbext/keymap.go new file mode 100644 index 0000000..701096e --- /dev/null +++ b/kbext/keymap.go @@ -0,0 +1,7 @@ +package kbext + +type keymap struct { + plain string + altgr string + shift string +} diff --git a/kbext/layouts.go b/kbext/layouts.go new file mode 100644 index 0000000..c0a5262 --- /dev/null +++ b/kbext/layouts.go @@ -0,0 +1,23 @@ +package kbext + +import evdev "github.com/holoplot/go-evdev" + +type LayoutID string + +var ( + LayoutQuertyEnUs LayoutID = "querty-en-US" +) + +type layout map[evdev.EvCode]keymap + +var layouts = map[LayoutID]layout{ + LayoutQuertyEnUs: quertyEnUs, +} + +func LayoutKeys() []LayoutID { + r := make([]LayoutID, 0, len(layouts)) + for k := range layouts { + r = append(r, k) + } + return r +} diff --git a/kbext/querty.go b/kbext/querty.go new file mode 100644 index 0000000..fe7063c --- /dev/null +++ b/kbext/querty.go @@ -0,0 +1,84 @@ +package kbext + +import evdev "github.com/holoplot/go-evdev" + +var ( + // LayoutQuertyEnUs LayoutID = "querty-en-US" + + quertyEnUs = map[evdev.EvCode]keymap{ + evdev.KEY_NUMERIC_STAR: {plain: "*", altgr: "*", shift: "*"}, + evdev.KEY_NUMERIC_3: {plain: "3", altgr: "3", shift: "3"}, + evdev.KEY_NUMERIC_2: {plain: "2", altgr: "2", shift: "2"}, + evdev.KEY_NUMERIC_5: {plain: "5", altgr: "5", shift: "5"}, + evdev.KEY_NUMERIC_4: {plain: "4", altgr: "4", shift: "4"}, + evdev.KEY_NUMERIC_7: {plain: "7", altgr: "7", shift: "7"}, + evdev.KEY_NUMERIC_6: {plain: "6", altgr: "6", shift: "6"}, + evdev.KEY_NUMERIC_9: {plain: "9", altgr: "9", shift: "9"}, + evdev.KEY_NUMERIC_8: {plain: "8", altgr: "8", shift: "8"}, + evdev.KEY_NUMERIC_1: {plain: "1", altgr: "1", shift: "1"}, + evdev.KEY_NUMERIC_0: {plain: "0", altgr: "0", shift: "0"}, + + evdev.KEY_KP4: {plain: "4", altgr: "4", shift: "4"}, + evdev.KEY_KP5: {plain: "5", altgr: "5", shift: "5"}, + evdev.KEY_KP6: {plain: "6", altgr: "6", shift: "6"}, + evdev.KEY_KP7: {plain: "7", altgr: "7", shift: "7"}, + evdev.KEY_KP0: {plain: "0", altgr: "0", shift: "0"}, + evdev.KEY_KP1: {plain: "1", altgr: "1", shift: "1"}, + evdev.KEY_KP2: {plain: "2", altgr: "2", shift: "2"}, + evdev.KEY_KP3: {plain: "3", altgr: "3", shift: "3"}, + evdev.KEY_KP8: {plain: "8", altgr: "8", shift: "8"}, + evdev.KEY_KP9: {plain: "9", altgr: "9", shift: "9"}, + + evdev.KEY_U: {plain: "u", altgr: "u", shift: "U"}, + evdev.KEY_W: {plain: "w", altgr: "w", shift: "W"}, + evdev.KEY_E: {plain: "e", altgr: "e", shift: "E"}, + evdev.KEY_D: {plain: "d", altgr: "d", shift: "D"}, + evdev.KEY_G: {plain: "g", altgr: "g", shift: "G"}, + evdev.KEY_F: {plain: "f", altgr: "f", shift: "F"}, + evdev.KEY_A: {plain: "a", altgr: "a", shift: "A"}, + evdev.KEY_C: {plain: "c", altgr: "c", shift: "C"}, + evdev.KEY_B: {plain: "b", altgr: "b", shift: "B"}, + evdev.KEY_M: {plain: "m", altgr: "m", shift: "M"}, + evdev.KEY_L: {plain: "l", altgr: "l", shift: "L"}, + evdev.KEY_O: {plain: "o", altgr: "o", shift: "O"}, + evdev.KEY_N: {plain: "n", altgr: "n", shift: "N"}, + evdev.KEY_I: {plain: "i", altgr: "i", shift: "I"}, + evdev.KEY_H: {plain: "h", altgr: "h", shift: "H"}, + evdev.KEY_K: {plain: "k", altgr: "k", shift: "K"}, + evdev.KEY_J: {plain: "j", altgr: "j", shift: "J"}, + evdev.KEY_Q: {plain: "q", altgr: "q", shift: "Q"}, + evdev.KEY_P: {plain: "p", altgr: "p", shift: "P"}, + evdev.KEY_S: {plain: "s", altgr: "s", shift: "S"}, + evdev.KEY_X: {plain: "x", altgr: "x", shift: "X"}, + evdev.KEY_Z: {plain: "z", altgr: "z", shift: "Z"}, + evdev.KEY_T: {plain: "t", altgr: "t", shift: "T"}, + evdev.KEY_V: {plain: "v", altgr: "v", shift: "V"}, + evdev.KEY_R: {plain: "r", altgr: "r", shift: "R"}, + evdev.KEY_Y: {plain: "y", altgr: "y", shift: "Y"}, + + evdev.KEY_1: {plain: "1", altgr: "~", shift: "!"}, + evdev.KEY_2: {plain: "2", altgr: "2", shift: "@"}, + evdev.KEY_3: {plain: "3", altgr: "^", shift: "#"}, + evdev.KEY_4: {plain: "4", altgr: "4", shift: "$"}, + evdev.KEY_5: {plain: "5", altgr: "5", shift: "%"}, + evdev.KEY_6: {plain: "6", altgr: "6", shift: "^"}, + evdev.KEY_7: {plain: "7", altgr: "7", shift: "&"}, + evdev.KEY_8: {plain: "8", altgr: "8", shift: "*"}, + evdev.KEY_9: {plain: "9", altgr: "9", shift: "("}, + evdev.KEY_0: {plain: "0", altgr: "0", shift: ")"}, + + evdev.KEY_BACKSLASH: {plain: "\\", altgr: "\\", shift: "|"}, + evdev.KEY_TAB: {plain: "\t", altgr: "\t", shift: "\t"}, + evdev.KEY_MINUS: {plain: "-", altgr: "-", shift: "-"}, + evdev.KEY_SPACE: {plain: " ", altgr: " ", shift: " "}, + evdev.KEY_GRAVE: {plain: "`", altgr: "`", shift: "`"}, + evdev.KEY_LEFTBRACE: {plain: "[", altgr: "[", shift: "["}, + evdev.KEY_RIGHTBRACE: {plain: "]", altgr: "]", shift: "]"}, + evdev.KEY_COMMA: {plain: ",", altgr: ",", shift: ","}, + evdev.KEY_EQUAL: {plain: "=", altgr: "=", shift: "="}, + evdev.KEY_SEMICOLON: {plain: ";", altgr: ";", shift: ";"}, + evdev.KEY_APOSTROPHE: {plain: "'", altgr: "'", shift: "'"}, + evdev.KEY_DOT: {plain: ".", altgr: ".", shift: "."}, + evdev.KEY_SLASH: {plain: "/", altgr: "/", shift: "/"}, + } +) diff --git a/kbext/resolve.go b/kbext/resolve.go new file mode 100644 index 0000000..0b9f745 --- /dev/null +++ b/kbext/resolve.go @@ -0,0 +1,85 @@ +package kbext + +import ( + "errors" + "log" + + evdev "github.com/holoplot/go-evdev" +) + +type KbState struct { + layout LayoutID + shift bool + altgr bool +} + +var ( + ErrNotACharKey = errors.New("last event was not a printable char key down") + ErrNotHandled = errors.New("event not handled") + ErrUnknwownLayout = errors.New("unknown layout") +) + +func NewKbState(layout LayoutID) KbState { + return KbState{ + layout: layout, + shift: false, + altgr: false, + } +} + +func (kb *KbState) KeyEvent(kbEvt *evdev.KeyEvent) (string, error) { + switch kbEvt.State { + case evdev.KeyUp: + _, err := kb.handleKey(kbEvt, false) + if err == nil { + err = ErrNotACharKey + } + return "", err + case evdev.KeyDown: + return kb.handleKey(kbEvt, true) + } + return "", ErrNotHandled +} + +func (kb *KbState) handleKey(kbEvt *evdev.KeyEvent, down bool) (string, error) { + switch kbEvt.Scancode { + case evdev.KEY_LEFTSHIFT: + fallthrough + case evdev.KEY_RIGHTSHIFT: + kb.shift = down + return "", ErrNotACharKey + + case evdev.KEY_ENTER: + fallthrough + case evdev.KEY_KPENTER: + return "\n", nil + } + + layout, ok := layouts[kb.layout] + if !ok { + return "", ErrUnknwownLayout + } + + keyinfo, ok := layout[kbEvt.Scancode] + if !ok { + return "", errors.New("KeyInfo not found") + } + + if kb.altgr { + if keyinfo.plain != keyinfo.altgr { + return keyinfo.altgr, nil + } + keyName := evdev.KEYToString[kbEvt.Scancode] + log.Printf("TODO : altgr mapping for %v", keyName) + return keyinfo.altgr, nil + } + if kb.shift { + if keyinfo.plain != keyinfo.shift { + return keyinfo.shift, nil + } + keyName := evdev.KEYToString[kbEvt.Scancode] + log.Printf("TODO : shift mapping for %v", keyName) + return keyinfo.shift, nil + } + return keyinfo.plain, nil +} diff --git a/keyevent.go b/keyevent.go new file mode 100644 index 0000000..488c5da --- /dev/null +++ b/keyevent.go @@ -0,0 +1,58 @@ +package evdev + +import ( + "fmt" +) + +type KeyEventState uint8 + +const ( + KeyUp KeyEventState = 0x0 + KeyDown KeyEventState = 0x1 + KeyHold KeyEventState = 0x2 +) + +// KeyEvents are used to describe state changes of keyboards, buttons, +// or other key-like devices. +type KeyEvent struct { + Event *InputEvent + Scancode EvCode + Keycode uint16 + State KeyEventState +} + +func (kev *KeyEvent) New(ev *InputEvent) { + kev.Event = ev + kev.Keycode = 0 // :todo + kev.Scancode = ev.Code + + switch ev.Value { + case 0: + kev.State = KeyUp + case 2: + kev.State = KeyHold + case 1: + kev.State = KeyDown + } +} + +func NewKeyEvent(ev *InputEvent) *KeyEvent { + kev := &KeyEvent{} + kev.New(ev) + return kev +} + +func (ev *KeyEvent) String() string { + state := "unknown" + + switch ev.State { + case KeyUp: + state = "up" + case KeyHold: + state = "hold" + case KeyDown: + state = "down" + } + + return fmt.Sprintf("key event %d (%d), (%s)", ev.Scancode, ev.Event.Code, state) +} From 3c66e5e161eeb073e9256ac0e47c89f3b9a5d038 Mon Sep 17 00:00:00 2001 From: Richard Kriesman Date: Mon, 19 May 2025 20:52:25 -0500 Subject: [PATCH 2/2] add function to set min/max/fuzz/flat on abs inputs when creating a device --- types.go | 13 +++++++++---- uinput.go | 13 +++++++++++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/types.go b/types.go index c227cc2..2ef5051 100644 --- a/types.go +++ b/types.go @@ -74,13 +74,18 @@ type InputMask struct { CodesPtr uint64 } -// UinputUserDevice is used when creating or cloning a device -type UinputUserDevice struct { - Name [uinputMaxNameSize]byte - ID InputID +// UserDeviceAbsParams specifies the range (min/max), fuzz, and flat values for an absolute input. +type UserDeviceAbsParams struct { EffectsMax uint32 Absmax [absSize]int32 Absmin [absSize]int32 Absfuzz [absSize]int32 Absflat [absSize]int32 } + +// UinputUserDevice is used when creating or cloning a device +type UinputUserDevice struct { + Name [uinputMaxNameSize]byte + ID InputID + UserDeviceAbsParams UserDeviceAbsParams +} diff --git a/uinput.go b/uinput.go index 0f2a691..a679472 100644 --- a/uinput.go +++ b/uinput.go @@ -17,6 +17,14 @@ const ( // If set up fails the device will be removed from the system, // once set up it can be removed by calling dev.Close func CreateDevice(name string, id InputID, capabilities map[EvType][]EvCode) (*InputDevice, error) { + return CreateDeviceWithAbsParams(name, id, capabilities, UserDeviceAbsParams{}) +} + +// CreateDeviceWithAbsParams creates a device from scratch with the specified name, identifiers, +// capabilities, and a set of parameters for absolute inputs. +// If setup fails, the device is removed from the system. +// It can be closed manually using the dev.Close() method. +func CreateDeviceWithAbsParams(name string, id InputID, capabilities map[EvType][]EvCode, absParams UserDeviceAbsParams) (*InputDevice, error) { deviceFile, err := os.OpenFile("/dev/uinput", syscall.O_WRONLY|syscall.O_NONBLOCK, 0660) if err != nil { return nil, err @@ -39,8 +47,9 @@ func CreateDevice(name string, id InputID, capabilities map[EvType][]EvCode) (*I } if _, err = createInputDevice(newDev.file, UinputUserDevice{ - Name: toUinputName([]byte(name)), - ID: id, + Name: toUinputName([]byte(name)), + ID: id, + UserDeviceAbsParams: absParams, }); err != nil { DestroyDevice(newDev) return nil, fmt.Errorf("failed to create device: %w", err)