149 lines
3.1 KiB
Go
149 lines
3.1 KiB
Go
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<<k.keymap.ModGetIndex("Control") |
|
|
1<<k.keymap.ModGetIndex("Alt") |
|
|
1<<k.keymap.ModGetIndex("Shift") |
|
|
1<<k.keymap.ModGetIndex("Super"))
|
|
|
|
shortcutMods := stateMods & ^consumedMods & significantMods
|
|
return k.parseModMask(shortcutMods)
|
|
}
|
|
|
|
func (k *Keyboard) parseModMask(modMask xkbcommon.ModMask) (mod ModMask) {
|
|
if modMask&(1<<k.keymap.ModGetIndex("Control")) != 0 {
|
|
mod |= Control
|
|
}
|
|
if modMask&(1<<k.keymap.ModGetIndex("Alt")) != 0 {
|
|
mod |= Alt
|
|
}
|
|
if modMask&(1<<k.keymap.ModGetIndex("Shift")) != 0 {
|
|
mod |= Shift
|
|
}
|
|
if modMask&(1<<k.keymap.ModGetIndex("Super")) != 0 {
|
|
mod |= Super
|
|
}
|
|
return mod
|
|
}
|
|
|
|
func keycode(event *evdev.InputEvent) xkbcommon.Keycode {
|
|
return xkbcommon.Keycode(event.Code + 8)
|
|
}
|
|
|
|
type KeyPress struct {
|
|
Sym xkbcommon.Keysym
|
|
ShortcutMods ModMask
|
|
Char string
|
|
}
|
|
|
|
type ModMask int
|
|
|
|
const (
|
|
Control ModMask = 1 << iota
|
|
Alt
|
|
Shift
|
|
Super
|
|
)
|