Compare commits

..

No commits in common. "master" and "1.2" have entirely different histories.
master ... 1.2

18 changed files with 849 additions and 1269 deletions

6
50-dotool.conf Normal file
View file

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

View file

@ -1,2 +1,6 @@
# 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="0620", 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

@ -2,43 +2,6 @@
Notable changes to dotool will be documented in this file. Notable changes to dotool will be documented in this file.
## [1.5](https://git.sr.ht/~geb/dotool/refs/1.5)
### Fixed
- The selecting of the fewest modifiers for simulating keys.
## [1.4](https://git.sr.ht/~geb/dotool/refs/1.4)
### Added
- A manpage, requiring scdoc.
- Heuristic support for dead keys.
- Support for altgr.
- $DOTOOL_KEYBOARD_NAME to set the virtual keyboard's name.
- More verbose --list-keys output.
### Changed
- Replaced ./install.sh with ./build.sh.
### Fixed
- Now prefers the fewest modifiers for simulating keys. (UPDATE: see 1.5)
## [1.3](https://git.sr.ht/~geb/dotool/refs/1.3)
### Added
- Support for keyboard layouts.
- hwheel for horizontal scrolling.
### Changed
- Now depends on the xkbcommon library.
- XKB key names are now case-sensitive.
- scroll -> wheel
## [1.2](https://git.sr.ht/~geb/dotool/refs/1.2) ## [1.2](https://git.sr.ht/~geb/dotool/refs/1.2)
### Added ### Added

View file

@ -1,42 +1,53 @@
# dotool # dotool
dotool reads actions from stdin and simulates keyboard/mouse input using dotool reads commands from stdin and simulates keyboard and mouse events.
Linux's uinput module. It works systemwide and supports keyboard layouts. It works everywhere on Linux, including in X11, Wayland and TTYs.
## Install From Packages It takes about half a second to register the virtual device, but it can be
kept using the daemon.
Packages of dotool are available on:
- [Alpine](https://pkgs.alpinelinux.org/packages?name=dotool)
- [Arch (AUR)](https://aur.archlinux.org/packages?SeB=n&K=dotool)
- [Nix](https://search.nixos.org/packages?channel=unstable&type=packages&query=dotool)
- [Void](https://voidlinux.org/packages/?q=dotool)
and potentially other platforms.
## Install From Source ## Install From Source
With `go`, `libxkbcommon-dev` and `scdoc` installed, run: With go (>=1.19) run:
./build.sh && sudo ./build.sh install sudo ./install.sh
And to trigger the udev rule, run:
sudo udevadm control --reload && sudo udevadm trigger
## Usage ## Usage
See the [manpage](doc/dotool.1.scd). dotool will require root permissions unless you are in group input.
See:
## Numen and Contact dotool --help
dotool was written for [Numen](https://numenvoice.org), which has a This greets the world:
[chat on Matrix](https://matrix.to/#/#numen:matrix.org) you're welcome to join.
You can also send questions or patches by composing an email to echo 'type Sup, Lads!' | dotool
[~geb/numen@lists.sr.ht](https://lists.sr.ht/~geb/numen).
## Support My Work 👀 This screams for three seconds:
{ echo keydown A; sleep 3; echo key H shift+1; } | dotool
This drags the mouse:
printf %s\\n 'buttondown left' 'mousemove 0 100' 'buttonup left' | dotool
The daemon and client, `dotoold` and `dotoolc`, can used to keep a persistent
virtual device for a quicker initial response:
dotoold &
echo 'type super' | dotoolc
echo 'type speedy' | dotoolc
## Numen, Chat and Contact
dotool was written for [Numen Voice Control](https://numenvoice.com)
and you're very welcome to join the Matrix chat at
[#numen:matrix.org](https://matrix.to/#/#numen:matrix.org).
You can also send questions, thoughts or patches by composing an email to
[~geb/public-inbox@lists.sr.ht](https://lists.sr.ht/~geb/public-inbox).
## Support Me
[Thank you!](https://liberapay.com/geb) [Thank you!](https://liberapay.com/geb)

8
_install.sh Executable file
View file

@ -0,0 +1,8 @@
#!/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

View file

@ -1,22 +0,0 @@
#!/bin/sh
# ./build.sh ['install']
: "${DOTOOL_VERSION=$(git describe --long --abbrev=12 --tags --dirty 2>/dev/null || echo 1.5)}"
: "${DOTOOL_DESTDIR=}"
: "${DOTOOL_BINDIR=usr/local/bin}"
: "${DOTOOL_UDEV_RULES_DIR=etc/udev/rules.d}"
if [ "$*" != '' ] && [ "$*" != install ]; then
echo bad usage
exit 1
fi
if ! [ "$1" ]; then
go build -ldflags "-X main.Version=$DOTOOL_VERSION" || exit
echo Built Successfully.
else
install -Dm755 dotool dotoolc dotoold -t "$DOTOOL_DESTDIR/$DOTOOL_BINDIR" || exit
install -Dm644 80-dotool.rules -t "$DOTOOL_DESTDIR/$DOTOOL_UDEV_RULES_DIR" || exit
mkdir -p "$DOTOOL_DESTDIR/usr/share/man/man1" || exit
scdoc < doc/dotool.1.scd > "$DOTOOL_DESTDIR/usr/share/man/man1/dotool.1" || exit
echo Installed Successfully.
fi

View file

@ -1,132 +0,0 @@
dotool(1)
# NAME
*dotool* - uinput tool
# SYNOPSIS
*dotool* < _actions_
# DESCRIPTION
*dotool* reads actions from stdin and simulates keyboard/mouse input using
Linux's uinput module.
# PERMISSION
*dotool* requires write permission to */dev/uinput*, which is granted to
users in group *input* by a udev rule.
You can test:
*echo type hello | dotool*
and if need be, you could add your user to group *input* with:
*groupadd -f input*++
*usermod -a -G input $USER*
and then it's foolproof to reboot to make the group and rule effective.
# KEYBOARD LAYOUTS
*dotool* may type gobbledygook if it's simulating keycodes for a different
keyboard layout than your environment is expecting.
You can specify the layout with the environment variables *DOTOOL_XKB_LAYOUT*
and *DOTOOL_XKB_VARIANT*. For example:
*echo type azerty | DOTOOL_XKB_LAYOUT=fr dotool*
You can also specify the name to give the virtual keyboard with the environment
variable *DOTOOL_KEYBOARD_NAME*, which can be useful making rules for your
environment.
Currently the *type* action has only heuristic support for dead keys.
# OPTIONS
*-h*, *--help*
Print help and exit.
*--list-keys*
Print the possible Linux keys and exit.
*--list-x-keys*
Print the possible XKB keys and exit.
*--version*
Print the version and exit.
# ACTIONS
*key* _CHORD_...++
*keydown* _CHORD_...++
*keyup* _CHORD_...
Press and/or release each _CHORD_. A _CHORD_ is a key or a key with
modifiers, such as *a*, *shift+a* or *ctrl+shift+a*.
The supported modifiers are *super*, *altgr*, *ctrl*, *alt* and *shift*.
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. This example types *!!!* with the *us* layout:
*echo key shift+1 x:exclam shift+k:2 | dotool*
*type* _TEXT_
Type _TEXT_.
*click* *left*/*middle*/*right*++
*buttondown* *left*/*middle*/*right*++
*buttonup* *left*/*middle*/*right*
Press and/or release a mouse button.
*wheel* _AMOUNT_++
*hwheel* _AMOUNT_
Scroll a vertical/horizontal mouse wheel by a positive or negative
_AMOUNT_.
*mouseto* _X_ _Y_
Jump the cursor to the position _X_ _Y_, where _X_ and _Y_ are
percentages between 0.0 and 1.0.
*mousemove* _X_ _Y_
Move the cursor relative to its current position.
*keydelay* _MILLISECONDS_++
*keyhold* _MILLISECONDS_++
*typedelay* _MILLISECONDS_++
*typehold* _MILLISECONDS_
Set the delay between/holding each key with the *key*\* actions/*type*
action.
The default *keydelay* and *typedelay* is 2ms, and the default
*keyhold* and *typehold* is 8ms.
# LONG-RUNNING INSTANCE
Each instance of *dotool* has an initial delay registering the virtual
devices, but you can keep writing actions to a long-running instance. The
daemon and client, *dotoold* and *dotoolc*, let you do this with a pipe
behind the scenes, for example:
*dotoold &*++
*echo type super | dotoolc*++
*echo type speedy | dotoolc*
# EXAMPLES
This greets the world:
*echo type hi | dotool*
This screams for roughly three seconds:
*{ echo keydown A; sleep 3; echo key H shift+1; } | dotool*
# AUTHOR
John Gebbie

355
dotool.go
View file

@ -4,7 +4,6 @@ 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"
@ -18,39 +17,59 @@ import (
var Version string var Version string
func usage() { func usage() {
fmt.Println(`dotool reads actions from stdin and simulates input using uinput. fmt.Fprintln(os.Stderr, `dotool reads commands from stdin and simulates keyboard and pointer events.
The supported actions are: The commands are:
key CHORD... key CHORD...
keydown CHORD... keydown CHORD...
keyup CHORD... keyup CHORD...
type TEXT type TEXT
click left/middle/right click left/middle/right
buttondown left/middle/right buttondown left/middle/right
buttonup left/middle/right buttonup left/middle/right
wheel AMOUNT scroll NUMBER (where NUMBER is the amount down/up if positive/negative)
hwheel AMOUNT mouseto X Y (where X and Y are percentages between 0.0 and 1.0)
mouseto X Y (where X and Y are percentages between 0.0 and 1.0) mousemove X Y (where X and Y are the number of pixels to move)
mousemove X Y (where X and Y are amounts to move) keydelay MILLISECONDS (default: 2)
keydelay MILLISECONDS keyhold MILLISECONDS (default: 8)
keyhold MILLISECONDS typedelay MILLISECONDS (default: 2)
typedelay MILLISECONDS typehold MILLISECONDS (default: 8)
typehold MILLISECONDS
--list-keys Print the possible Linux keys and exit. Example: echo "key h i shift+1" | dotool
--list-x-keys Print the possible XKB keys and exit.
--version Print the version and exit.
See 'man dotool' for the documentation.`) 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
running: sudo udevadm trigger
The keys are those used by Linux, but can also be specified using X11 names
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.
The modifiers are: super, ctrl, alt and shift.
The daemon and client, dotoold and dotoolc, can used to keep a persistent
virtual device for a quicker initial response.
--list-keys
Print the supported Linux keys and their keycodes.
--list-x-keys
Print the supported X11 keys and their Linux keycodes.
--version
Print the version and exit.
`)
} }
func fatal(a ...any) { func fatal(a ...any) {
fmt.Fprintln(os.Stderr, "dotool:", fmt.Sprint(a...)) fmt.Fprint(os.Stderr, "dotool: ")
fmt.Fprintln(os.Stderr, a...)
os.Exit(1) os.Exit(1)
} }
func warn(a ...any) { func warn(a ...any) {
fmt.Fprintln(os.Stderr, "dotool: WARNING:", fmt.Sprint(a...)) fmt.Fprint(os.Stderr, "dotool WARNING: ")
fmt.Fprintln(os.Stderr, a...)
} }
func log(err error) { func log(err error) {
@ -60,46 +79,20 @@ func log(err error) {
} }
type Chord struct { type Chord struct {
Super, AltGr, Ctrl, Alt, Shift bool Super bool
Ctrl bool
Alt bool
Shift bool
Key int Key int
level uint32 // just for initKeys
} }
func parseChord(keymap *xkb.Keymap, chord string) (Chord, error) { func parseChord(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":
c.Super = true c.Super = true
case "altgr":
c.AltGr = true
case "ctrl", "control": case "ctrl", "control":
c.Ctrl = true c.Ctrl = true
case "alt": case "alt":
@ -111,168 +104,140 @@ func parseChord(keymap *xkb.Keymap, 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
} }
func (c Chord) KeyDown(kb uinput.Keyboard) { func (c *Chord) KeyDown(kb uinput.Keyboard) {
if c.Super { if c.Super {
log(kb.KeyDown(super)) log(kb.KeyDown(uinput.KeyLeftmeta))
}
if c.AltGr {
log(kb.KeyDown(altgr))
} }
if c.Ctrl { if c.Ctrl {
log(kb.KeyDown(ctrl)) log(kb.KeyDown(uinput.KeyLeftctrl))
} }
if c.Alt { if c.Alt {
log(kb.KeyDown(alt)) log(kb.KeyDown(uinput.KeyLeftalt))
} }
if c.Shift { if c.Shift {
log(kb.KeyDown(shift)) log(kb.KeyDown(uinput.KeyLeftshift))
} }
log(kb.KeyDown(c.Key)) log(kb.KeyDown(c.Key))
} }
func (c Chord) KeyUp(kb uinput.Keyboard) { func (c *Chord) KeyUp(kb uinput.Keyboard) {
if c.Super { if c.Super {
log(kb.KeyUp(super)) log(kb.KeyUp(uinput.KeyLeftmeta))
}
if c.AltGr {
log(kb.KeyUp(altgr))
} }
if c.Ctrl { if c.Ctrl {
log(kb.KeyUp(ctrl)) log(kb.KeyUp(uinput.KeyLeftctrl))
} }
if c.Alt { if c.Alt {
log(kb.KeyUp(alt)) log(kb.KeyUp(uinput.KeyLeftalt))
} }
if c.Shift { if c.Shift {
log(kb.KeyUp(shift)) log(kb.KeyUp(uinput.KeyLeftshift))
} }
log(kb.KeyUp(c.Key)) log(kb.KeyUp(c.Key))
} }
func (c *Chord) String() string {
var sb strings.Builder
if c.Super {
sb.WriteString("super+")
}
if c.AltGr {
sb.WriteString("altgr+")
}
if c.Ctrl {
sb.WriteString("ctrl+")
}
if c.Alt {
sb.WriteString("alt+")
}
if c.Shift {
sb.WriteString("shift+")
}
sb.WriteString("k:")
sb.WriteString(strconv.Itoa(c.Key))
return sb.String()
}
func cutCmd(s, cmd string) (string, bool) {
func listKeys(keymap *xkb.Keymap, keys map[string]Chord) { if strings.HasPrefix(s, cmd + " ") || strings.HasPrefix(s, cmd + "\t") {
var margin int return s[len(cmd)+1:], true
for code := 1; code < 256; code++ {
for name := range keys {
if len(name) > margin {
margin = len(name)
}
}
}
for code := 1; code < 256; code++ {
for name, chord := range keys {
if chord.Key == code {
fmt.Printf("%-*s %s\n", margin, name, chord.String())
}
}
}
}
func cutWord(s, word string) (string, bool) {
if s == word {
return "", true
}
if strings.HasPrefix(s, word + " ") || strings.HasPrefix(s, word + "\t") {
return s[len(word)+1:], true
} }
return "", false return "", false
} }
func main() { func main() {
var keymap *xkb.Keymap
{ {
layout := os.Getenv("DOTOOL_XKB_LAYOUT") optset := opt.NewOptionSet()
if layout == "" {
layout = os.Getenv("XKB_DEFAULT_LAYOUT")
}
variant := os.Getenv("DOTOOL_XKB_VARIANT")
if variant == "" {
variant = os.Getenv("XKB_DEFAULT_VARIANT")
}
if variant != "" && layout == "" {
// Otherwise xkbcommon just ignores the variant.
fatal("you need to set $DOTOOL_XKB_LAYOUT or $XKB_DEFAULT_LAYOUT if the variant is set")
}
names := xkb.RuleNames{Layout: layout, Variant: variant}
ctx := xkb.ContextNew(xkb.ContextNoFlags) optset.FlagFunc("h", func() error {
defer ctx.Unref()
keymap = ctx.KeymapNewFromNames(&names, xkb.KeymapCompileNoFlags)
if keymap == nil {
fatal("failed to compile keymap")
}
defer keymap.Unref()
initKeys(keymap)
}
{
o := opt.NewOptSet()
o.FlagFunc("h", func() error {
usage() usage()
os.Exit(0) os.Exit(0)
panic("unreachable") panic("unreachable")
}) })
o.Alias("h", "help") optset.Alias("h", "help")
o.FlagFunc("list-keys", func() error { optset.BoolFunc("list-keys", func(bool) error {
listKeys(keymap, LinuxKeys) for i := 1; i < 249; i++ {
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")
}) })
o.FlagFunc("list-x-keys", func() error { optset.BoolFunc("list-x-keys", func(bool) error {
listKeys(keymap, XKeys) for i := 1; i < 249; i++ {
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")
}) })
o.FlagFunc("version", func() error { optset.FlagFunc("version", func() error {
fmt.Println(Version) fmt.Println(Version)
os.Exit(0) os.Exit(0)
panic("unreachable") panic("unreachable")
}) })
err := o.Parse(true, os.Args[1:]) err := optset.Parse(true, os.Args[1:])
if err != nil { if err != nil {
fatal(err.Error()) fatal(err.Error())
} }
if len(o.Args()) > 0 { if len(optset.Args()) > 0 {
fatal("there should be no arguments, commands are read from stdin") fatal("there should be no arguments, commands are read from stdin")
} }
} }
keyboardName := []byte(os.Getenv("DOTOOL_KEYBOARD_NAME")) keyboard, err := uinput.CreateKeyboard("/dev/uinput", []byte("dotool keyboard"))
if len(keyboardName) == 0 {
keyboardName = []byte("dotool keyboard")
}
keyboard, err := uinput.CreateKeyboard("/dev/uinput", keyboardName)
if err != nil { if err != nil {
fatal(err.Error()) fatal(err.Error())
} }
@ -302,9 +267,9 @@ func main() {
if text == "" { if text == "" {
continue continue
} }
if s, ok := cutWord(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(keymap, field); err == nil { if chord, err := parseChord(field); err == nil {
chord.KeyDown(keyboard) chord.KeyDown(keyboard)
time.Sleep(keyhold) time.Sleep(keyhold)
chord.KeyUp(keyboard) chord.KeyUp(keyboard)
@ -313,25 +278,25 @@ func main() {
} }
time.Sleep(keydelay) time.Sleep(keydelay)
} }
} else if s, ok := cutWord(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(keymap, field); err == nil { if chord, err := parseChord(field); err == nil {
chord.KeyDown(keyboard) chord.KeyDown(keyboard)
} else { } else {
warn(err.Error()) warn(err.Error())
} }
time.Sleep(keydelay) time.Sleep(keydelay)
} }
} else if s, ok := cutWord(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(keymap, field); err == nil { if chord, err := parseChord(field); err == nil {
chord.KeyUp(keyboard) chord.KeyUp(keyboard)
} else { } else {
warn(err.Error()) warn(err.Error())
} }
time.Sleep(keydelay) time.Sleep(keydelay)
} }
} else if s, ok := cutWord(text, "keydelay"); ok { } else if s, ok := cutCmd(text, "keydelay"); ok {
var d float64 var d float64
_, err := fmt.Sscanf(s + "\n", "%f\n", &d) _, err := fmt.Sscanf(s + "\n", "%f\n", &d)
if err == nil { if err == nil {
@ -339,7 +304,7 @@ func main() {
} else { } else {
warn("invalid delay: " + sc.Text()) warn("invalid delay: " + sc.Text())
} }
} else if s, ok := cutWord(text, "keyhold"); ok { } else if s, ok := cutCmd(text, "keyhold"); ok {
var d float64 var d float64
_, err := fmt.Sscanf(s + "\n", "%f\n", &d) _, err := fmt.Sscanf(s + "\n", "%f\n", &d)
if err == nil { if err == nil {
@ -347,29 +312,19 @@ func main() {
} else { } else {
warn("invalid hold time: " + sc.Text()) warn("invalid hold time: " + sc.Text())
} }
} else if s, ok := cutWord(text, "type"); ok { } else if s, ok := cutCmd(text, "type"); ok {
for _, r := range s { for _, r := range s {
sym := xkb.Utf32ToKeysym(uint32(r)) if chord, ok := runeChords[unicode.ToLower(r)]; ok {
if sym == 0 { if unicode.IsUpper(r) {
warn("invalid character: " + string(r)) chord.Shift = true
} else if c := getChord(keymap, sym); c.Key != 0 { }
c.KeyDown(keyboard) chord.KeyDown(keyboard)
time.Sleep(typehold) time.Sleep(typehold)
c.KeyUp(keyboard) chord.KeyUp(keyboard)
} else if c1, c2 := getDeadChords(keymap, r); c1.Key != 0 {
c1.KeyDown(keyboard)
time.Sleep(typehold)
c1.KeyUp(keyboard)
time.Sleep(typedelay)
c2.KeyDown(keyboard)
time.Sleep(typehold)
c2.KeyUp(keyboard)
} else {
warn("impossible character for layout: " + string(r))
} }
time.Sleep(typedelay) time.Sleep(typedelay)
} }
} else if s, ok := cutWord(text, "typedelay"); ok { } else if s, ok := cutCmd(text, "typedelay"); ok {
var d float64 var d float64
_, err := fmt.Sscanf(s + "\n", "%f\n", &d) _, err := fmt.Sscanf(s + "\n", "%f\n", &d)
if err == nil { if err == nil {
@ -377,7 +332,7 @@ func main() {
} else { } else {
warn("invalid delay: " + sc.Text()) warn("invalid delay: " + sc.Text())
} }
} else if s, ok := cutWord(text, "typehold"); ok { } else if s, ok := cutCmd(text, "typehold"); ok {
var d float64 var d float64
_, err := fmt.Sscanf(s + "\n", "%f\n", &d) _, err := fmt.Sscanf(s + "\n", "%f\n", &d)
if err == nil { if err == nil {
@ -385,7 +340,7 @@ func main() {
} else { } else {
warn("invalid hold time: " + sc.Text()) warn("invalid hold time: " + sc.Text())
} }
} else if s, ok := cutWord(text, "click"); ok { } else if s, ok := cutCmd(text, "click"); ok {
for _, button := range strings.Fields(s) { for _, button := range strings.Fields(s) {
switch button { switch button {
case "left", "1": case "left", "1":
@ -398,7 +353,7 @@ func main() {
warn("unknown button: " + button) warn("unknown button: " + button)
} }
} }
} else if s, ok := cutWord(text, "buttondown"); ok { } else if s, ok := cutCmd(text, "buttondown"); ok {
for _, button := range strings.Fields(s) { for _, button := range strings.Fields(s) {
switch button { switch button {
case "left", "1": case "left", "1":
@ -411,7 +366,7 @@ func main() {
warn("unknown button: " + button) warn("unknown button: " + button)
} }
} }
} else if s, ok := cutWord(text, "buttonup"); ok { } else if s, ok := cutCmd(text, "buttonup"); ok {
for _, button := range strings.Fields(s) { for _, button := range strings.Fields(s) {
switch button { switch button {
case "left", "1": case "left", "1":
@ -424,23 +379,7 @@ func main() {
warn("unknown button: " + button) warn("unknown button: " + button)
} }
} }
} else if s, ok := cutWord(text, "wheel"); ok { } else if s, ok := cutCmd(text, "scroll"); ok {
var n float64
_, err := fmt.Sscanf(s + "\n", "%f\n", &n)
if err == nil {
log(mouse.Wheel(false, int32(n)))
} else {
warn("invalid wheel amount: " + sc.Text())
}
} else if s, ok := cutWord(text, "hwheel"); ok {
var n float64
_, err := fmt.Sscanf(s + "\n", "%f\n", &n)
if err == nil {
log(mouse.Wheel(true, int32(n)))
} else {
warn("invalid hwheel amount: " + sc.Text())
}
} else if s, ok := cutWord(text, "scroll"); ok {
var n float64 var n float64
_, err := fmt.Sscanf(s + "\n", "%f\n", &n) _, err := fmt.Sscanf(s + "\n", "%f\n", &n)
if err == nil { if err == nil {
@ -448,7 +387,7 @@ func main() {
} else { } else {
warn("invalid scroll amount: " + sc.Text()) warn("invalid scroll amount: " + sc.Text())
} }
} else if s, ok := cutWord(text, "mouseto"); ok { } else if s, ok := cutCmd(text, "mouseto"); ok {
var x, y float64 var x, y float64
_, err := fmt.Sscanf(s + "\n", "%f %f\n", &x, &y) _, err := fmt.Sscanf(s + "\n", "%f %f\n", &x, &y)
if err == nil { if err == nil {
@ -465,7 +404,7 @@ func main() {
} else { } else {
warn("invalid coordinate: " + sc.Text()) warn("invalid coordinate: " + sc.Text())
} }
} else if s, ok := cutWord(text, "mousemove"); ok { } else if s, ok := cutCmd(text, "mousemove"); ok {
var x, y float64 var x, y float64
_, err := fmt.Sscanf(s + "\n", "%f %f\n", &x, &y) _, err := fmt.Sscanf(s + "\n", "%f %f\n", &x, &y)
if err == nil { if err == nil {

2
dotool.sway Normal file
View file

@ -0,0 +1,2 @@
# 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,8 +1,9 @@
#!/bin/sh #!/bin/sh
if [ $# != 0 ]; then if [ $# != 0 ]; then
echo 'dotoolc writes its stdin to the pipe being read by dotoold. dotoolc will echo 'dotoolc writes its stdin to the pipe being read by dotoold.
exit immediately if the pipe is not being read. The path of the pipe dotoolc will exit immediately if the pipe is not being read.
is $DOTOOL_PIPE else /tmp/dotool-pipe.' >&2 The path of the pipe is $DOTOOL_PIPE else /tmp/dotool_pipe.
' >&2
[ "$1" = -h ] || [ "$1" = --help ]; exit [ "$1" = -h ] || [ "$1" = --help ]; exit
fi fi
@ -10,7 +11,7 @@ fifo_being_read(){
[ -p "$1" ] && /bin/echo 1<>"$1" >"$1" [ -p "$1" ] && /bin/echo 1<>"$1" >"$1"
} }
p="${DOTOOL_PIPE:-/tmp/dotool-pipe}" p="${DOTOOL_PIPE:-/tmp/dotool_pipe}"
if [ -p "$p" ] && ! [ -w "$p" ]; then if [ -p "$p" ] && ! [ -w "$p" ]; then
printf %s\\n "dotoolc: the pipe does not grant write permission: $p" >&2 printf %s\\n "dotoolc: the pipe does not grant write permission: $p" >&2

24
dotoold
View file

@ -1,21 +1,17 @@
#!/bin/sh #!/bin/sh
for a; do if [ $# != 0 ]; then
case "$a" in echo 'dotoold runs dotool reading from a pipe for dotoolc to write to.
-h|--help) dotoold will exit immediately if the pipe is already being read.
echo 'dotoold runs dotool reading from a pipe for dotoolc to write to. dotoold The path used for the pipe is $DOTOOL_PIPE else /tmp/dotool_pipe.
will exit immediately if the pipe is already being read. The path used ' >&2
for the pipe is $DOTOOL_PIPE else /tmp/dotool-pipe.' >&2 [ "$1" = -h ] || [ "$1" = --help ]; exit
exit fi
;;
--) break;;
esac
done
fifo_being_read(){ fifo_being_read(){
[ -p "$1" ] && /bin/echo 1<>"$1" >"$1" [ -p "$1" ] && /bin/echo 1<>"$1" >"$1"
} }
p="${DOTOOL_PIPE:-/tmp/dotool-pipe}" p="${DOTOOL_PIPE:-/tmp/dotool_pipe}"
if fifo_being_read "$p" 2> /dev/null; then if fifo_being_read "$p" 2> /dev/null; then
printf %s\\n "dotoold: another instance is already reading the pipe: $p" >&2 printf %s\\n "dotoold: another instance is already reading the pipe: $p" >&2
@ -25,5 +21,5 @@ fi
rm -f -- "$p" || exit 1 rm -f -- "$p" || exit 1
trap 'rm -f -- "$p"; pkill -P $$; trap - EXIT; exit' EXIT INT TERM HUP trap 'rm -f -- "$p"; pkill -P $$; trap - EXIT; exit' EXIT INT TERM HUP
mkfifo -m 660 "$p" || exit 1 mkfifo -m 660 "$p" || exit 1
dotool "$@" <> "$p" & dotool <> "$p" &
wait $! wait

4
go.mod
View file

@ -3,6 +3,6 @@ module git.sr.ht/~geb/dotool
go 1.19 go 1.19
require ( require (
git.sr.ht/~geb/opt v0.0.0-20230911153257-e72225a1933c git.sr.ht/~geb/opt v0.0.0-20221207200434-dad26d091d71
github.com/bendahl/uinput v1.7.0 github.com/bendahl/uinput v1.6.0
) )

8
go.sum
View file

@ -1,4 +1,4 @@
git.sr.ht/~geb/opt v0.0.0-20230911153257-e72225a1933c h1:gIC1gnCgoasPHks1x6MB+bgDmIWMxKc5HIJPJrsV5Ck= git.sr.ht/~geb/opt v0.0.0-20221207200434-dad26d091d71 h1:jh3Ite7R1ZvdLt6j52e4njO2SS/z5dWOLXllw7inalc=
git.sr.ht/~geb/opt v0.0.0-20230911153257-e72225a1933c/go.mod h1:S6h1g8P7DyG7i7YIHZ5IpYbC6lzZB9DYIEl8PyXOmsg= git.sr.ht/~geb/opt v0.0.0-20221207200434-dad26d091d71/go.mod h1:S6h1g8P7DyG7i7YIHZ5IpYbC6lzZB9DYIEl8PyXOmsg=
github.com/bendahl/uinput v1.7.0 h1:nA4fm8Wu8UYNOPykIZm66nkWEyvxzfmJ8YC02PM40jg= github.com/bendahl/uinput v1.6.0 h1:fM6r3OSC17rHh758mizKjSBuqi+XinhiGd4N3pWvZiI=
github.com/bendahl/uinput v1.7.0/go.mod h1:Np7w3DINc9wB83p12fTAM3DPPhFnAKP0WTXRqCQJ6Z8= github.com/bendahl/uinput v1.6.0/go.mod h1:Np7w3DINc9wB83p12fTAM3DPPhFnAKP0WTXRqCQJ6Z8=

13
install.sh Executable file
View file

@ -0,0 +1,13 @@
#!/bin/sh
# ./install.sh [DESTDIR] [BINDIR]
: "${DOTOOL_VERSION=$(git describe --long --abbrev=12 --tags --dirty 2>/dev/null || echo 1.2)}"
go build -ldflags "-X main.Version=$DOTOOL_VERSION" || exit
mkdir -p "$1/${2:-usr/local/bin}" || exit
cp -v dotool "$1/${2:-usr/local/bin}" || exit
mkdir -p "$1/etc/udev/rules.d" || exit
cp -v 80-dotool.rules "$1/etc/udev/rules.d" || exit
./_install.sh "$1" "$2" || exit
# Make the new/updated udev rule effective
udevadm control --reload
udevadm trigger

1244
keys.go

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,3 @@
#!/bin/sh #!/bin/sh
rm -vf /usr/local/bin/dotool /usr/local/bin/dotoolc /usr/local/bin/dotoold rm -vf /usr/local/bin/dotool /usr/local/bin/dotoolc /usr/local/bin/dotoold
rm -vf /etc/udev/rules.d/80-dotool.rules rm -vf /etc/udev/rules.d/80-dotool.rules
rm -vf /usr/share/man/man1/dotool.1

View file

@ -1,144 +0,0 @@
package xkb
//#cgo pkg-config: xkbcommon
//#cgo LDFLAGS: -ldl
//
//#include <stdlib.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 := unsafe.Slice(syms, 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
}

42
xkeys.bash Executable file
View file

@ -0,0 +1,42 @@
#!/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 '}'