package keyboard import ( "context" "fmt" "log" "git.sr.ht/~guacamolie/faxmachine/xkbcommon" "github.com/holoplot/go-evdev" ) type Keyboard struct { dev *evdev.InputDevice ctx *xkbcommon.Context keymap *xkbcommon.Keymap state *xkbcommon.State } func Open(devnode string) (*Keyboard, error) { var err error k := &Keyboard{} k.dev, err = evdev.Open(devnode) if err != nil { return nil, err } err = k.dev.Grab() if err != nil { k.dev.Close() return nil, fmt.Errorf("failed to grab input: %w", err) } k.ctx, err = xkbcommon.NewContext(xkbcommon.ContextNoFlags) if err != nil { k.dev.Close() return nil, err } k.keymap, err = xkbcommon.NewKeymapFromNames(k.ctx, nil, xkbcommon.KeymapCompileFlagsNoFlags) if err != nil { k.ctx.Unref() k.dev.Close() return nil, err } k.state, err = xkbcommon.NewState(k.keymap) if err != nil { k.keymap.Unref() k.ctx.Unref() k.dev.Close() return nil, err } return k, nil } func (k *Keyboard) Close() { k.state.Unref() k.keymap.Unref() k.ctx.Unref() k.dev.Close() } func (k *Keyboard) Listen(ctx context.Context, ch chan<- KeyPress) { go func() { for { inputEvent, err := k.dev.ReadOne() if err != nil { log.Fatalf("keyboard read failed: %v\n", err) } if inputEvent.Type != evdev.EV_KEY { continue } if inputEvent.Value == 1 { keyPress := KeyPress{ Sym: k.state.GetOneSym(keycode(inputEvent)), ShortcutMods: k.getShortcutMods(keycode(inputEvent)), Char: k.state.KeyGetUtf8(keycode(inputEvent)), } log.Printf("keypress: %#v\n", keyPress) ch <- keyPress } // This should be called _after_ procesing the key, so that the key // doesn't influence itself. if inputEvent.Value == 1 { k.state.UpdateKey(keycode(inputEvent), xkbcommon.KeyDown) } else if inputEvent.Value == 0 { k.state.UpdateKey(keycode(inputEvent), xkbcommon.KeyUp) } } }() } func (k *Keyboard) getShortcutMods(key xkbcommon.Keycode) ModMask { // This implements the algorithm as described here: // https://xkbcommon.org/doc/current/group__state.html#consumed-modifiers stateMods := k.state.SerializeMods(xkbcommon.StateModsEffective) consumedMods := k.state.KeyGetConsumedMods(key) significantMods := xkbcommon.ModMask( 1<