overhaul to use xkbcommon

This commit is contained in:
John Gebbie 2023-04-19 13:23:05 +01:00
parent 01fcc71667
commit f8d6461da0
10 changed files with 812 additions and 720 deletions

View file

@ -1,7 +0,0 @@
# This can stop dotool typing guff if you are using a non-us keyboard layout.
Section "InputClass"
Identifier "dotool keyboard"
MatchDriver "libinput"
MatchProduct "dotool keyboard"
Option "XkbLayout" "us"
EndSection

View file

@ -1,6 +1,2 @@
# This allows users in group input to use dotool without root permissions. # This allows users in group input to use dotool without root permissions.
KERNEL=="uinput", GROUP="input", MODE="0660", OPTIONS+="static_node=uinput" KERNEL=="uinput", GROUP="input", MODE="0660", OPTIONS+="static_node=uinput"
# This can stop dotool typing guff if you are using a non-us keyboard layout,
# but it only seems to affect X and 50-dotool.conf achieves that better.
SUBSYSTEM=="input", ACTION=="add|change", ATTRS{name}=="dotool keyboard", ENV{XKBLAYOUT}="us"

View file

@ -1,11 +1,11 @@
# dotool # dotool
dotool reads commands from stdin and simulates keyboard and mouse events. dotool reads commands from stdin and simulates keyboard and pointer events.
It works everywhere on Linux, including in X11, Wayland and TTYs. It works everywhere on Linux, including in X11, Wayland and TTYs.
## Install From Source ## Install From Source
With `go` installed, run: With `go` and `libxkbcommon-dev` installed, run:
sudo ./install.sh sudo ./install.sh
@ -35,13 +35,22 @@ and this screams for three seconds:
{ echo keydown A; sleep 3; echo key H shift+1; } | dotool { echo keydown A; sleep 3; echo key H shift+1; } | dotool
Each instance takes about half a second to register the virtual input devices, There is an initial delay registering the virtual devices, but you can
but you can keep writing commands to one instance or use the daemon and client, keep writing commands to the same instance or use the daemon and client,
`dotoold` and `dotoolc`: `dotoold` and `dotoolc`.
dotoold & dotoold &
echo 'type super' | dotoolc echo type super | dotoolc
echo 'type speedy' | dotoolc echo type speedy | dotoolc
## Keyboard Layouts
dotool will produce gobbledygook if your environment has assigned it a
different keyboard layout than it's simulating keycodes for. You can
match them up with the environment variables `DOTOOL_XKB_LAYOUT` and
`DOTOOL_XKB_VARIANT`.
echo type azerty | DOTOOL_XKB_LAYOUT=fr dotool
## Numen and Contact ## Numen and Contact

View file

@ -1,8 +0,0 @@
#!/bin/sh
# ./_install.sh [DESTDIR] [BINDIR]
mkdir -p "$1/${2:-usr/local/bin}" || exit
cp -v dotoolc dotoold "$1/${2:-usr/local/bin}" || exit
mkdir -p "$1/usr/share/X11/xorg.conf.d" || exit
cp -v 50-dotool.conf "$1/usr/share/X11/xorg.conf.d" || exit
mkdir -p "$1/etc/sway/config.d" || exit
cp -v dotool.sway "$1/etc/sway/config.d/dotool" || exit

199
dotool.go
View file

@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"errors" "errors"
"fmt" "fmt"
"git.sr.ht/~geb/dotool/xkb"
"git.sr.ht/~geb/opt" "git.sr.ht/~geb/opt"
"github.com/bendahl/uinput" "github.com/bendahl/uinput"
"math" "math"
@ -17,7 +18,7 @@ import (
var Version string var Version string
func usage() { func usage() {
fmt.Fprintln(os.Stderr, `dotool reads commands from stdin and simulates keyboard and pointer events. fmt.Println(`dotool reads commands from stdin and simulates keyboard and pointer events.
The commands are: The commands are:
key CHORD... key CHORD...
@ -35,26 +36,51 @@ The commands are:
typedelay MILLISECONDS (default: 2) typedelay MILLISECONDS (default: 2)
typehold MILLISECONDS (default: 8) typehold MILLISECONDS (default: 8)
Example: echo "key h i shift+1" | dotool
dotool is installed with a udev rule to allow users in group input to run dotool is installed with a udev rule to allow users in group input to run
it without root permissions. You can make it effective without rebooting by it without root permissions.
running: sudo udevadm trigger
The keys are those used by Linux, but can also be specified using X11 names You can add yourself to group input by running:
prefixed with x: like x:exclam, as well as their Linux keycode like k:30.
They are case insensitive, except uppercase character keys also simulate shift. sudo groupadd -f input
sudo usermod -a -G input $USER
It's foolproof to reboot to make the rule effective.
Keys can be specified by Linux names, XKB names prefixed with x:, or Linux
keycodes prefixed with k:. The Linux names are case-insensitive, except
uppercase character keys also simulate shift.
The modifiers are: super, ctrl, alt and shift. The modifiers are: super, ctrl, alt and shift.
The daemon and client, dotoold and dotoolc, can used to keep a persistent echo key shift+1 x:exclam shift+k:2 | dotool
virtual device for a quicker initial response.
There is an initial delay registering the virtual devices, but you can keep
writing commands to the same instance or use the daemon and client, dotoold
and dotoolc.
{ echo keydown A; sleep 3; echo key H shift+1; } | dotool
dotoold &
echo type super | dotoolc
echo type speedy | dotoolc
dotool will produce gobbledygook if your environment has assigned it a
different keyboard layout than it's simulating keycodes for. You can
match them up with the environment variables DOTOOL_XKB_LAYOUT and
DOTOOL_XKB_VARIANT.
echo type azerty | DOTOOL_XKB_LAYOUT=fr dotool
--list-keys --list-keys
Print the supported Linux keys and their keycodes. Print the possible Linux keys and exit.
--list-x-keys --list-x-keys
Print the supported X11 keys and their Linux keycodes. Print the possible XKB keys and exit.
--version --version
Print the version and exit. Print the version and exit.
@ -79,16 +105,38 @@ func log(err error) {
} }
type Chord struct { type Chord struct {
Super bool Super, AltGr, Ctrl, Alt, Shift bool
Ctrl bool
Alt bool
Shift bool
Key int Key int
} }
func parseChord(chord string) (Chord, error) { func parseChord(keymap *xkb.Keymap, chord string) (Chord, error) {
var c Chord var c Chord
keys := strings.Split(chord, "+") keys := strings.Split(chord, "+")
k := keys[len(keys)-1]
if strings.HasPrefix(k, "k:") {
code, err := strconv.Atoi(k[2:])
if err != nil {
return c, errors.New("invalid keycode: " + k[2:])
}
c.Key = code
} else if strings.HasPrefix(k, "x:") {
var ok bool
c, ok = XKeys[k[2:]]
if !ok {
return c, errors.New("impossible XKB key for layout: " + k[2:])
}
} else {
var ok bool
c, ok = LinuxKeys[strings.ToLower(k)]
if !ok {
return c, errors.New("impossible key for layout: " + k)
}
if len(k) == 1 && unicode.IsUpper(rune(k[0])) {
c.Shift = true
}
}
for i := 0; i < len(keys) - 1; i++ { for i := 0; i < len(keys) - 1; i++ {
switch strings.ToLower(keys[i]) { switch strings.ToLower(keys[i]) {
case "super": case "super":
@ -104,34 +152,6 @@ func parseChord(chord string) (Chord, error) {
} }
} }
k := keys[len(keys)-1]
if strings.HasPrefix(k, "k:") {
code, err := strconv.Atoi(k[2:])
if err != nil {
return c, errors.New("invalid keycode: " + k[2:])
}
c.Key = code
} else if strings.HasPrefix(k, "x:") {
if code, ok := xKeysShifted[strings.ToLower(k[2:])]; ok {
if len(k[2:]) > 1 || unicode.IsUpper(rune(k[2])) {
c.Shift = true
}
c.Key = code
} else if code, ok := xKeysNormal[strings.ToLower(k[2:])]; ok {
c.Key = code
} else {
return c, errors.New("unknown X11 key: " + k[2:])
}
} else {
code, ok := linuxKeys[strings.ToLower(k)]
if len(k) == 1 && unicode.IsUpper(rune(k[0])) {
c.Shift = true
}
if !ok {
return c, errors.New("unknown key: " + k)
}
c.Key = code
}
return c, nil return c, nil
} }
@ -139,6 +159,9 @@ func (c *Chord) KeyDown(kb uinput.Keyboard) {
if c.Super { if c.Super {
log(kb.KeyDown(uinput.KeyLeftmeta)) log(kb.KeyDown(uinput.KeyLeftmeta))
} }
if c.AltGr {
log(kb.KeyDown(84))
}
if c.Ctrl { if c.Ctrl {
log(kb.KeyDown(uinput.KeyLeftctrl)) log(kb.KeyDown(uinput.KeyLeftctrl))
} }
@ -155,6 +178,9 @@ func (c *Chord) KeyUp(kb uinput.Keyboard) {
if c.Super { if c.Super {
log(kb.KeyUp(uinput.KeyLeftmeta)) log(kb.KeyUp(uinput.KeyLeftmeta))
} }
if c.AltGr {
log(kb.KeyUp(84))
}
if c.Ctrl { if c.Ctrl {
log(kb.KeyUp(uinput.KeyLeftctrl)) log(kb.KeyUp(uinput.KeyLeftctrl))
} }
@ -168,6 +194,21 @@ func (c *Chord) KeyUp(kb uinput.Keyboard) {
} }
func listKeys(keymap *xkb.Keymap, keys map[string]Chord) {
for code := 1; code < 256; code++ {
for name, chord := range keys {
if chord.Key == code && (chord == Chord{Key: code}) {
fmt.Println(name, code)
}
}
for name, chord := range keys {
if chord.Key == code && (chord != Chord{Key: code}) {
fmt.Println(name, code)
}
}
}
}
func cutCmd(s, cmd string) (string, bool) { func cutCmd(s, cmd string) (string, bool) {
if strings.HasPrefix(s, cmd + " ") || strings.HasPrefix(s, cmd + "\t") { if strings.HasPrefix(s, cmd + " ") || strings.HasPrefix(s, cmd + "\t") {
return s[len(cmd)+1:], true return s[len(cmd)+1:], true
@ -176,6 +217,29 @@ func cutCmd(s, cmd string) (string, bool) {
} }
func main() { func main() {
var keymap *xkb.Keymap
{
names := xkb.RuleNames{
Rules: "",
Model: "",
Layout: os.Getenv("DOTOOL_XKB_LAYOUT"),
Variant: os.Getenv("DOTOOL_XKB_VARIANT"),
Options: "",
}
ctx := xkb.ContextNew(xkb.ContextNoFlags)
defer ctx.Unref()
keymap = ctx.KeymapNewFromNames(&names, xkb.KeymapCompileNoFlags)
defer keymap.Unref()
if keymap == nil {
fatal("failed to compile keymap")
}
initKeys(keymap, names != xkb.RuleNames{})
}
{ {
optset := opt.NewOptionSet() optset := opt.NewOptionSet()
@ -187,37 +251,13 @@ func main() {
optset.Alias("h", "help") optset.Alias("h", "help")
optset.BoolFunc("list-keys", func(bool) error { optset.BoolFunc("list-keys", func(bool) error {
for i := 1; i < 249; i++ { listKeys(keymap, LinuxKeys)
for name, code := range linuxKeys {
if code == i {
fmt.Printf("%s %d\n", name, code)
break
}
}
}
os.Exit(0) os.Exit(0)
panic("unreachable") panic("unreachable")
}) })
optset.BoolFunc("list-x-keys", func(bool) error { optset.BoolFunc("list-x-keys", func(bool) error {
for i := 1; i < 249; i++ { listKeys(keymap, XKeys)
for name, code := range xKeysNormal {
if code == i {
fmt.Print(name)
for name, code := range xKeysShifted {
if code == i {
if len(name) == 1 {
name = strings.ToUpper(name)
}
fmt.Printf(" %s", name)
break
}
}
fmt.Printf(" %d\n", code)
break
}
}
}
os.Exit(0) os.Exit(0)
panic("unreachable") panic("unreachable")
}) })
@ -269,7 +309,7 @@ func main() {
} }
if s, ok := cutCmd(text, "key"); ok { if s, ok := cutCmd(text, "key"); ok {
for _, field := range strings.Fields(s) { for _, field := range strings.Fields(s) {
if chord, err := parseChord(field); err == nil { if chord, err := parseChord(keymap, field); err == nil {
chord.KeyDown(keyboard) chord.KeyDown(keyboard)
time.Sleep(keyhold) time.Sleep(keyhold)
chord.KeyUp(keyboard) chord.KeyUp(keyboard)
@ -280,7 +320,7 @@ func main() {
} }
} else if s, ok := cutCmd(text, "keydown"); ok { } else if s, ok := cutCmd(text, "keydown"); ok {
for _, field := range strings.Fields(s) { for _, field := range strings.Fields(s) {
if chord, err := parseChord(field); err == nil { if chord, err := parseChord(keymap, field); err == nil {
chord.KeyDown(keyboard) chord.KeyDown(keyboard)
} else { } else {
warn(err.Error()) warn(err.Error())
@ -289,7 +329,7 @@ func main() {
} }
} else if s, ok := cutCmd(text, "keyup"); ok { } else if s, ok := cutCmd(text, "keyup"); ok {
for _, field := range strings.Fields(s) { for _, field := range strings.Fields(s) {
if chord, err := parseChord(field); err == nil { if chord, err := parseChord(keymap, field); err == nil {
chord.KeyUp(keyboard) chord.KeyUp(keyboard)
} else { } else {
warn(err.Error()) warn(err.Error())
@ -314,14 +354,19 @@ func main() {
} }
} else if s, ok := cutCmd(text, "type"); ok { } else if s, ok := cutCmd(text, "type"); ok {
for _, r := range s { for _, r := range s {
if chord, ok := runeChords[unicode.ToLower(r)]; ok { if sym := xkb.Utf32ToKeysym(uint32(r)); sym == 0 {
if unicode.IsUpper(r) { warn("invalid character: " + string(r))
chord.Shift = true } else {
} chord := getChord(keymap, sym)
if chord.Key == 0 {
warn("impossible character for layout: " + string(r))
time.Sleep(typehold)
} else {
chord.KeyDown(keyboard) chord.KeyDown(keyboard)
time.Sleep(typehold) time.Sleep(typehold)
chord.KeyUp(keyboard) chord.KeyUp(keyboard)
} }
}
time.Sleep(typedelay) time.Sleep(typedelay)
} }
} else if s, ok := cutCmd(text, "typedelay"); ok { } else if s, ok := cutCmd(text, "typedelay"); ok {

View file

@ -1,2 +0,0 @@
# This can stop dotool typing guff if you are using a non-us keyboard layout.
input "18193:2069:dotool_keyboard" xkb_layout "us"

View file

@ -1,12 +1,16 @@
#!/bin/sh #!/bin/sh
# ./install.sh [DESTDIR] [BINDIR] # ./install.sh [DESTDIR] [BINDIR]
: "${DOTOOL_VERSION=$(git describe --long --abbrev=12 --tags --dirty 2>/dev/null || echo 1.2)}" : "${DOTOOL_VERSION=$(git describe --long --abbrev=12 --tags --dirty 2>/dev/null || echo 1.2)}"
go build -ldflags "-X main.Version=$DOTOOL_VERSION" || exit go build -ldflags "-X main.Version=$DOTOOL_VERSION" || exit
mkdir -p "$1/${2:-usr/local/bin}" || exit mkdir -p "$1/${2:-usr/local/bin}" || exit
cp -v dotool "$1/${2:-usr/local/bin}" || exit cp -v dotool dotoolc dotoold "$1/${2:-usr/local/bin}" || exit
mkdir -p "$1/etc/udev/rules.d" || exit mkdir -p "$1/etc/udev/rules.d" || exit
cp -v 80-dotool.rules "$1/etc/udev/rules.d" || exit cp -v 80-dotool.rules "$1/etc/udev/rules.d" || exit
./_install.sh "$1" "$2" || exit
# Remove files from before the keyboard layout approach changed
rm -f "$1/usr/share/X11/xorg.conf.d/50-dotool.conf"
rm -f "$1/etc/sway/config.d/dotool"
# Make the new/updated udev rule effective # Make the new/updated udev rule effective
udevadm control --reload udevadm control --reload

1090
keys.go

File diff suppressed because it is too large Load diff

145
xkb/xkb.go Normal file
View file

@ -0,0 +1,145 @@
package xkb
//#cgo pkg-config: xkbcommon
//#cgo LDFLAGS: -ldl
//
//#include <stdlib.h>
//#include <xkbcommon/xkbcommon-compose.h>
//#include <xkbcommon/xkbcommon.h>
import "C"
import "unsafe"
const (
// ModNameShift is the name of Shift Modifier
ModNameShift = "Shift"
// ModNameCaps is the name of Caps Lock Modifier
ModNameCaps = "Lock"
// ModNameCtrl is the name of Control Modifier
ModNameCtrl = "Control"
// ModNameAlt is the name of Alt Modifier
ModNameAlt = "Mod1"
// ModNameNum is the name of Num Lock Modifier
ModNameNum = "Mod2"
// ModNameLogo is the name of Logo Modifier
ModNameLogo = "Mod4"
// LedNameCaps is the name of Caps Lock Led
LedNameCaps = "Caps Lock"
// LedNameNum is the name of Num Lock Led
LedNameNum = "Num Lock"
// LedNameScroll is the name of Scroll Lock Led
LedNameScroll = "Scroll Lock"
)
func KeysymGetName(keysym uint32) string {
s := "................................................................"
cs := C.CString(s)
defer C.free(unsafe.Pointer(cs))
_ = C.xkb_keysym_get_name(C.uint(keysym), cs, C.size_t(len(s)))
return C.GoString(cs)
}
func KeysymToUtf32(keysym uint32) uint32 {
return uint32(C.xkb_keysym_to_utf32(C.uint(keysym)))
}
func Utf32ToKeysym(ucs uint32) uint32 {
return uint32(C.xkb_utf32_to_keysym(C.uint(ucs)))
}
type KeymapCompileFlags int
const (
KeymapCompileNoFlags KeymapCompileFlags = C.XKB_KEYMAP_COMPILE_NO_FLAGS
)
type RuleNames struct {
Rules, Model, Layout, Variant, Options string
}
func (rn *RuleNames) toC() *C.struct_xkb_rule_names {
if rn == nil {
return nil
}
return &C.struct_xkb_rule_names{
rules: C.CString(rn.Rules),
model: C.CString(rn.Model),
layout: C.CString(rn.Layout),
variant: C.CString(rn.Variant),
options: C.CString(rn.Options),
}
}
const ContextNoFlags = 0
type Context struct {
p *C.struct_xkb_context
}
func ContextNew(flags uint32) (ctx *Context) {
ctx = new(Context)
ctx.p = C.xkb_context_new(flags)
return ctx
}
func (ctx *Context) KeymapNewFromNames(rules *RuleNames, flags KeymapCompileFlags) *Keymap {
km := C.xkb_keymap_new_from_names(ctx.p, rules.toC(), C.enum_xkb_keymap_compile_flags(flags))
if km == nil {
return nil
}
return &Keymap{km}
}
func (ctx *Context) Unref() {
C.xkb_context_unref(ctx.p)
ctx.p = nil
}
type Keymap struct {
p *C.struct_xkb_keymap
}
func (km *Keymap) KeyGetMod(key, layout, level uint32) uint32 {
var mask C.uint
C.xkb_keymap_key_get_mods_for_level(km.p, C.uint(key), C.uint(layout), C.uint(level), &mask, 1)
return uint32(mask)
}
func (km *Keymap) KeyGetSymsByLevel(key, layout, level uint32) []uint32 {
var syms *C.xkb_keysym_t
n := int(C.xkb_keymap_key_get_syms_by_level(km.p, C.uint(key), C.uint(layout), C.uint(level), &syms))
if n == 0 || syms == nil {
return nil
}
data := (*[1 << 30]C.xkb_keysym_t)(unsafe.Pointer(syms))[:n:n]
s := make([]uint32, n)
for i := 0; i < n; i++ {
s[i] = uint32(data[i])
}
return s
}
func (km *Keymap) MaxKeycode() uint32 {
return uint32(C.xkb_keymap_max_keycode(km.p))
}
func (km *Keymap) MinKeycode() uint32 {
return uint32(C.xkb_keymap_min_keycode(km.p))
}
func (km *Keymap) ModGetIndex(mod string) uint {
return uint(C.xkb_keymap_mod_get_index(km.p, C.CString(mod)))
}
func (km *Keymap) NumLevelsForKey(key, layout uint32) uint32 {
return uint32(C.xkb_keymap_num_levels_for_key(km.p, C.uint(key), C.uint(layout)))
}
func (km *Keymap) Unref() {
C.xkb_keymap_unref(km.p)
km.p = nil
}
func keymapUnref(km *Keymap) {
C.xkb_keymap_unref(km.p)
km.p = nil
}

View file

@ -1,42 +0,0 @@
#!/bin/bash
align() {
sed '/\<KeyRo\>/ d
/\<KeyKpjpcomma\>/ d
/\<KeyMacro\>/ d
/\<KeyYen\>/ d
/\<KeySetup\>/ d
/\<KeyDeletefile\>/ d
/\<KeyClosecd\>/ d
/\<KeyEjectclosecd\>/ d
/\<KeyIso\>/ d
/\<KeyMove\>/ d
/\<KeyEdit\>/ d
/\<KeyAlterase\>/ d
/\<KeyUnknown\>/ d
/\<KeyMicmute\>/ d'
}
normal="$({
paste -d ' ' <(xmodmap -pke | sed '1 d; s/.*= /"/; /.*=/ d; s/ .*/":/' | sed '/^"XF86Eject"/ { N; s/.*\n// }') \
<(go doc uinput.keyesc | sed '/Key/ !d; s/^\s*/uinput./; s/ .*/,/' | align) |
# Skip really non-matching section, we echo some of them below
sed '/^"XF86Tools"/,/^"XF86AudioPreset"/ d' |
# Remove duplicate keys
sed '/^"XF86Mail":.*Email/ d; /^"Cancel":.*Stop/ d; /^"XF86Send":.*file/ d; /^"Print":.*Sysrq/ d; /Key102Nd,$/ d'
echo '"XF86WebCam": uinput.KeyCamera,'
echo '"Print": uinput.KeyPrint,'
} | sed 's/^".*"/\L&/; s/^/\t/')"
printf %s\\n "var xKeysNormal = map[string]int{
$normal
}
"
echo 'var xKeysShifted = map[string]int{'
{
paste -d ' ' <(xmodmap -pke | sed '1 d; s/.*= /"/; /.*=/ d; s/\S* /"/; s/ .*/":/' | sed '/^"XF86Eject"/ { N; s/.*\n// }') \
<(go doc uinput.keyesc | sed '/Key/ !d; s/^\s*/uinput./; s/ .*/,/' | align) | sed '/^"NoSymbol"/ d; /^\S*_[LR]"/ d; /Key102Nd,$/ d'
} | sed 's/^".*"/\L&/; s/^/\t/' | awk 'NR == FNR {if (length($1) > 4) a[$1]; next} !($1 in a)' <(printf %s\\n "$normal") -
echo '}'