dotool/dotool.go
2023-02-06 11:20:49 +00:00

425 lines
9.9 KiB
Go

package main
import (
"bufio"
"errors"
"fmt"
"git.sr.ht/~geb/opt"
"github.com/bendahl/uinput"
"math"
"os"
"strconv"
"strings"
"time"
"unicode"
)
var Version string
func usage() {
fmt.Fprintln(os.Stderr, `dotool reads commands from stdin and simulates keyboard and pointer events.
The commands are:
key CHORD...
keydown CHORD...
keyup CHORD...
type TEXT
click left/middle/right
buttondown left/middle/right
buttonup left/middle/right
scroll NUMBER (where NUMBER is the amount down/up if positive/negative)
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)
keydelay MILLISECONDS (default: 2)
keyhold MILLISECONDS (default: 8)
typedelay MILLISECONDS (default: 2)
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
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) {
fmt.Fprint(os.Stderr, "dotool: ")
fmt.Fprintln(os.Stderr, a...)
os.Exit(1)
}
func warn(a ...any) {
fmt.Fprint(os.Stderr, "dotool WARNING: ")
fmt.Fprintln(os.Stderr, a...)
}
func log(err error) {
if err != nil {
warn(err.Error())
}
}
type Chord struct {
Super bool
Ctrl bool
Alt bool
Shift bool
Key int
}
func parseChord(chord string) (Chord, error) {
var c Chord
keys := strings.Split(chord, "+")
for i := 0; i < len(keys) - 1; i++ {
switch strings.ToLower(keys[i]) {
case "super":
c.Super = true
case "ctrl", "control":
c.Ctrl = true
case "alt":
c.Alt = true
case "shift":
c.Shift = true
default:
return c, errors.New("unknown modifier: " + keys[i])
}
}
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
}
func (c *Chord) KeyDown(kb uinput.Keyboard) {
if c.Super {
log(kb.KeyDown(uinput.KeyLeftmeta))
}
if c.Ctrl {
log(kb.KeyDown(uinput.KeyLeftctrl))
}
if c.Alt {
log(kb.KeyDown(uinput.KeyLeftalt))
}
if c.Shift {
log(kb.KeyDown(uinput.KeyLeftshift))
}
log(kb.KeyDown(c.Key))
}
func (c *Chord) KeyUp(kb uinput.Keyboard) {
if c.Super {
log(kb.KeyUp(uinput.KeyLeftmeta))
}
if c.Ctrl {
log(kb.KeyUp(uinput.KeyLeftctrl))
}
if c.Alt {
log(kb.KeyUp(uinput.KeyLeftalt))
}
if c.Shift {
log(kb.KeyUp(uinput.KeyLeftshift))
}
log(kb.KeyUp(c.Key))
}
func cutCmd(s, cmd string) (string, bool) {
if strings.HasPrefix(s, cmd + " ") || strings.HasPrefix(s, cmd + "\t") {
return s[len(cmd)+1:], true
}
return "", false
}
func main() {
{
optset := opt.NewOptionSet()
optset.FlagFunc("h", func() error {
usage()
os.Exit(0)
panic("unreachable")
})
optset.Alias("h", "help")
optset.BoolFunc("list-keys", func(bool) error {
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)
panic("unreachable")
})
optset.BoolFunc("list-x-keys", func(bool) error {
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)
panic("unreachable")
})
optset.FlagFunc("version", func() error {
fmt.Println(Version)
os.Exit(0)
panic("unreachable")
})
err := optset.Parse(true, os.Args[1:])
if err != nil {
fatal(err.Error())
}
if len(optset.Args()) > 0 {
fatal("there should be no arguments, commands are read from stdin")
}
}
keyboard, err := uinput.CreateKeyboard("/dev/uinput", []byte("dotool keyboard"))
if err != nil {
fatal(err.Error())
}
defer keyboard.Close()
const precision = 10000
touchpad, err := uinput.CreateTouchPad("/dev/uinput", []byte("dotool absolute pointer"), 0, precision, 0, precision)
if err != nil {
fatal(err.Error())
}
defer touchpad.Close()
mouse, err := uinput.CreateMouse("/dev/uinput", []byte("dotool relative pointer"))
if err != nil {
fatal(err.Error())
}
defer mouse.Close()
keydelay := time.Duration(2)*time.Millisecond
keyhold := time.Duration(8)*time.Millisecond
typedelay := time.Duration(2)*time.Millisecond
typehold := time.Duration(8)*time.Millisecond
sc := bufio.NewScanner(os.Stdin)
for sc.Scan() {
text := strings.TrimLeftFunc(sc.Text(), unicode.IsSpace)
if text == "" {
continue
}
if s, ok := cutCmd(text, "key"); ok {
for _, field := range strings.Fields(s) {
if chord, err := parseChord(field); err == nil {
chord.KeyDown(keyboard)
time.Sleep(keyhold)
chord.KeyUp(keyboard)
} else {
warn(err.Error())
}
time.Sleep(keydelay)
}
} else if s, ok := cutCmd(text, "keydown"); ok {
for _, field := range strings.Fields(s) {
if chord, err := parseChord(field); err == nil {
chord.KeyDown(keyboard)
} else {
warn(err.Error())
}
time.Sleep(keydelay)
}
} else if s, ok := cutCmd(text, "keyup"); ok {
for _, field := range strings.Fields(s) {
if chord, err := parseChord(field); err == nil {
chord.KeyUp(keyboard)
} else {
warn(err.Error())
}
time.Sleep(keydelay)
}
} else if s, ok := cutCmd(text, "keydelay"); ok {
var d float64
_, err := fmt.Sscanf(s + "\n", "%f\n", &d)
if err == nil {
keydelay = time.Duration(d)*time.Millisecond
} else {
warn("invalid delay: " + sc.Text())
}
} else if s, ok := cutCmd(text, "keyhold"); ok {
var d float64
_, err := fmt.Sscanf(s + "\n", "%f\n", &d)
if err == nil {
keyhold = time.Duration(d)*time.Millisecond
} else {
warn("invalid hold time: " + sc.Text())
}
} else if s, ok := cutCmd(text, "type"); ok {
for _, r := range s {
if chord, ok := runeChords[unicode.ToLower(r)]; ok {
if unicode.IsUpper(r) {
chord.Shift = true
}
chord.KeyDown(keyboard)
time.Sleep(typehold)
chord.KeyUp(keyboard)
}
time.Sleep(typedelay)
}
} else if s, ok := cutCmd(text, "typedelay"); ok {
var d float64
_, err := fmt.Sscanf(s + "\n", "%f\n", &d)
if err == nil {
typedelay = time.Duration(d)*time.Millisecond
} else {
warn("invalid delay: " + sc.Text())
}
} else if s, ok := cutCmd(text, "typehold"); ok {
var d float64
_, err := fmt.Sscanf(s + "\n", "%f\n", &d)
if err == nil {
typehold = time.Duration(d)*time.Millisecond
} else {
warn("invalid hold time: " + sc.Text())
}
} else if s, ok := cutCmd(text, "click"); ok {
for _, button := range strings.Fields(s) {
switch button {
case "left", "1":
log(mouse.LeftClick())
case "middle", "2":
log(mouse.MiddleClick())
case "right", "3":
log(mouse.RightClick())
default:
warn("unknown button: " + button)
}
}
} else if s, ok := cutCmd(text, "buttondown"); ok {
for _, button := range strings.Fields(s) {
switch button {
case "left", "1":
log(mouse.LeftPress())
case "middle", "2":
log(mouse.MiddlePress())
case "right", "3":
log(mouse.RightPress())
default:
warn("unknown button: " + button)
}
}
} else if s, ok := cutCmd(text, "buttonup"); ok {
for _, button := range strings.Fields(s) {
switch button {
case "left", "1":
log(mouse.LeftRelease())
case "middle", "2":
log(mouse.MiddleRelease())
case "right", "3":
log(mouse.RightRelease())
default:
warn("unknown button: " + button)
}
}
} 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 scroll amount: " + sc.Text())
}
} else if s, ok := cutCmd(text, "mouseto"); ok {
var x, y float64
_, err := fmt.Sscanf(s + "\n", "%f %f\n", &x, &y)
if err == nil {
if x < 0.0 || x > 1.0 || y < 0.0 || y > 1.0 {
warn("clamping percentages between 0.0 and 1.0: " + sc.Text())
x = math.Max(0, math.Min(x, 1.0))
y = math.Min(0, math.Min(y, 1.0))
}
x, y := int32(x * float64(precision)), int32(y * float64(precision))
err := touchpad.MoveTo(x, y)
if err != nil {
warn(err.Error())
}
} else {
warn("invalid coordinate: " + sc.Text())
}
} else if s, ok := cutCmd(text, "mousemove"); ok {
var x, y float64
_, err := fmt.Sscanf(s + "\n", "%f %f\n", &x, &y)
if err == nil {
err := mouse.Move(int32(x), int32(y))
if err != nil {
warn(err.Error())
}
} else {
warn("invalid coordinate: " + sc.Text())
}
} else {
warn("unknown command: " + sc.Text())
}
}
if sc.Err() != nil {
fatal(sc.Err().Error())
}
}