dotool/dotool.go

463 lines
11 KiB
Go
Raw Normal View History

2022-10-20 15:21:20 +02:00
package main
import (
"bufio"
"errors"
"fmt"
2023-04-19 14:23:05 +02:00
"git.sr.ht/~geb/dotool/xkb"
2022-10-20 15:21:20 +02:00
"git.sr.ht/~geb/opt"
"github.com/bendahl/uinput"
2022-10-20 15:21:20 +02:00
"math"
"os"
"strconv"
"strings"
2022-10-23 12:27:26 +02:00
"time"
2022-10-20 15:21:20 +02:00
"unicode"
)
var Version string
func usage() {
fmt.Println(`dotool reads actions from stdin and simulates keyboard/mouse input using uinput.
The supported actions are:
2023-05-11 12:16:22 +02:00
key CHORD...
keydown CHORD...
keyup CHORD...
type TEXT
click left/middle/right
buttondown left/middle/right
buttonup left/middle/right
wheel AMOUNT
hwheel AMOUNT
mouseto X Y
mousemove X Y
keydelay MILLISECONDS
keyhold MILLISECONDS
typedelay MILLISECONDS
typehold MILLISECONDS
--keyboard-name=NAME Specify the name to give the virtual keyboard device.
--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.
See 'man dotool' for the documentation.`)
}
func fatal(a ...any) {
2023-04-23 11:59:54 +02:00
fmt.Fprintln(os.Stderr, "dotool:", fmt.Sprint(a...))
2022-10-20 15:21:20 +02:00
os.Exit(1)
}
func warn(a ...any) {
2023-04-23 11:59:54 +02:00
fmt.Fprintln(os.Stderr, "dotool: WARNING:", fmt.Sprint(a...))
2022-10-20 15:21:20 +02:00
}
func log(err error) {
if err != nil {
warn(err.Error())
}
}
type Chord struct {
2023-04-19 14:23:05 +02:00
Super, AltGr, Ctrl, Alt, Shift bool
2022-10-20 15:21:20 +02:00
Key int
}
2023-04-19 14:23:05 +02:00
func parseChord(keymap *xkb.Keymap, chord string) (Chord, error) {
2022-10-20 15:21:20 +02:00
var c Chord
keys := strings.Split(chord, "+")
k := keys[len(keys)-1]
if strings.HasPrefix(k, "k:") {
code, err := strconv.Atoi(k[2:])
if err != nil {
2023-02-06 12:20:49 +01:00
return c, errors.New("invalid keycode: " + k[2:])
2022-10-20 15:21:20 +02:00
}
c.Key = code
} else if strings.HasPrefix(k, "x:") {
2023-04-19 14:23:05 +02:00
var ok bool
c, ok = XKeys[k[2:]]
if !ok {
return c, errors.New("impossible XKB key for layout: " + k[2:])
2022-10-20 15:21:20 +02:00
}
} else {
2023-04-19 14:23:05 +02:00
var ok bool
c, ok = LinuxKeys[strings.ToLower(k)]
if !ok {
return c, errors.New("impossible key for layout: " + k)
}
2022-10-20 15:21:20 +02:00
if len(k) == 1 && unicode.IsUpper(rune(k[0])) {
c.Shift = true
}
2023-04-19 14:23:05 +02:00
}
for i := 0; i < len(keys) - 1; i++ {
switch strings.ToLower(keys[i]) {
case "super":
c.Super = true
2023-09-12 13:58:11 +02:00
case "altgr":
c.AltGr = true
2023-04-19 14:23:05 +02:00
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])
2022-10-20 15:21:20 +02:00
}
}
2023-04-19 14:23:05 +02:00
2022-10-20 15:21:20 +02:00
return c, nil
}
2023-09-12 13:53:25 +02:00
func (c Chord) KeyDown(kb uinput.Keyboard) {
2022-10-20 15:21:20 +02:00
if c.Super {
2023-09-12 13:49:35 +02:00
log(kb.KeyDown(super))
2022-10-20 15:21:20 +02:00
}
2023-04-19 14:23:05 +02:00
if c.AltGr {
2023-09-12 13:49:35 +02:00
log(kb.KeyDown(altgr))
2023-04-19 14:23:05 +02:00
}
2022-10-20 15:21:20 +02:00
if c.Ctrl {
2023-09-12 13:49:35 +02:00
log(kb.KeyDown(ctrl))
2022-10-20 15:21:20 +02:00
}
if c.Alt {
2023-09-12 13:49:35 +02:00
log(kb.KeyDown(alt))
2022-10-20 15:21:20 +02:00
}
if c.Shift {
2023-09-12 13:49:35 +02:00
log(kb.KeyDown(shift))
2022-10-20 15:21:20 +02:00
}
log(kb.KeyDown(c.Key))
}
2023-09-12 13:53:25 +02:00
func (c Chord) KeyUp(kb uinput.Keyboard) {
2022-10-20 15:21:20 +02:00
if c.Super {
2023-09-12 13:49:35 +02:00
log(kb.KeyUp(super))
2022-10-20 15:21:20 +02:00
}
2023-04-19 14:23:05 +02:00
if c.AltGr {
2023-09-12 13:49:35 +02:00
log(kb.KeyUp(altgr))
2023-04-19 14:23:05 +02:00
}
2022-10-20 15:21:20 +02:00
if c.Ctrl {
2023-09-12 13:49:35 +02:00
log(kb.KeyUp(ctrl))
2022-10-20 15:21:20 +02:00
}
if c.Alt {
2023-09-12 13:49:35 +02:00
log(kb.KeyUp(alt))
2022-10-20 15:21:20 +02:00
}
if c.Shift {
2023-09-12 13:49:35 +02:00
log(kb.KeyUp(shift))
2022-10-20 15:21:20 +02:00
}
log(kb.KeyUp(c.Key))
}
2023-04-19 14:23:05 +02:00
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 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
2022-10-20 15:21:20 +02:00
}
return "", false
}
func main() {
2023-04-19 14:23:05 +02:00
var keymap *xkb.Keymap
{
layout := os.Getenv("DOTOOL_XKB_LAYOUT")
if layout == "" {
layout = os.Getenv("XKB_DEFAULT_LAYOUT")
2023-04-19 14:23:05 +02:00
}
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}
2023-04-19 14:23:05 +02:00
ctx := xkb.ContextNew(xkb.ContextNoFlags)
defer ctx.Unref()
keymap = ctx.KeymapNewFromNames(&names, xkb.KeymapCompileNoFlags)
if keymap == nil {
fatal("failed to compile keymap")
}
defer keymap.Unref()
2023-04-19 14:23:05 +02:00
initKeys(keymap)
2023-04-19 14:23:05 +02:00
}
2023-08-15 13:53:04 +02:00
keyboardName := []byte("dotool keyboard")
2022-10-20 15:21:20 +02:00
{
optset := opt.NewOptionSet()
2022-12-28 11:43:19 +01:00
optset.FlagFunc("h", func() error {
2022-10-20 15:21:20 +02:00
usage()
os.Exit(0)
panic("unreachable")
2022-12-28 11:43:19 +01:00
})
optset.Alias("h", "help")
2023-08-15 13:53:04 +02:00
optset.Func("keyboard-name", func(s string) error {
keyboardName = []byte(s)
return nil
})
2023-04-28 17:41:56 +02:00
optset.FlagFunc("list-keys", func() error {
2023-04-19 14:23:05 +02:00
listKeys(keymap, LinuxKeys)
2022-10-20 15:21:20 +02:00
os.Exit(0)
panic("unreachable")
2022-12-28 11:43:19 +01:00
})
2023-04-28 17:41:56 +02:00
optset.FlagFunc("list-x-keys", func() error {
2023-04-19 14:23:05 +02:00
listKeys(keymap, XKeys)
2022-10-20 15:21:20 +02:00
os.Exit(0)
panic("unreachable")
2022-12-28 11:43:19 +01:00
})
optset.FlagFunc("version", func() error {
fmt.Println(Version)
os.Exit(0)
panic("unreachable")
})
2022-10-20 15:21:20 +02:00
err := optset.Parse(true, os.Args[1:])
if err != nil {
fatal(err.Error())
}
2022-12-28 11:55:19 +01:00
if len(optset.Args()) > 0 {
fatal("there should be no arguments, commands are read from stdin")
}
2022-10-20 15:21:20 +02:00
}
2023-08-15 13:53:04 +02:00
keyboard, err := uinput.CreateKeyboard("/dev/uinput", keyboardName)
2022-10-20 15:21:20 +02:00
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()
2023-01-04 23:36:01 +01:00
keydelay := time.Duration(2)*time.Millisecond
keyhold := time.Duration(8)*time.Millisecond
typedelay := time.Duration(2)*time.Millisecond
typehold := time.Duration(8)*time.Millisecond
2022-10-23 12:27:26 +02:00
2022-10-20 15:21:20 +02:00
sc := bufio.NewScanner(os.Stdin)
for sc.Scan() {
text := strings.TrimLeftFunc(sc.Text(), unicode.IsSpace)
if text == "" {
continue
}
if s, ok := cutWord(text, "key"); ok {
2022-10-20 15:21:20 +02:00
for _, field := range strings.Fields(s) {
2023-04-19 14:23:05 +02:00
if chord, err := parseChord(keymap, field); err == nil {
2023-01-04 23:14:21 +01:00
chord.KeyDown(keyboard)
time.Sleep(keyhold)
chord.KeyUp(keyboard)
2022-10-20 15:21:20 +02:00
} else {
warn(err.Error())
}
2023-01-04 23:00:49 +01:00
time.Sleep(keydelay)
2022-10-20 15:21:20 +02:00
}
} else if s, ok := cutWord(text, "keydown"); ok {
2022-10-20 15:21:20 +02:00
for _, field := range strings.Fields(s) {
2023-04-19 14:23:05 +02:00
if chord, err := parseChord(keymap, field); err == nil {
2022-10-20 15:21:20 +02:00
chord.KeyDown(keyboard)
} else {
warn(err.Error())
}
2023-01-04 23:00:49 +01:00
time.Sleep(keydelay)
2022-10-20 15:21:20 +02:00
}
} else if s, ok := cutWord(text, "keyup"); ok {
2022-10-20 15:21:20 +02:00
for _, field := range strings.Fields(s) {
2023-04-19 14:23:05 +02:00
if chord, err := parseChord(keymap, field); err == nil {
2022-10-20 15:21:20 +02:00
chord.KeyUp(keyboard)
} else {
warn(err.Error())
}
2023-01-04 23:00:49 +01:00
time.Sleep(keydelay)
2022-10-20 15:21:20 +02:00
}
} else if s, ok := cutWord(text, "keydelay"); ok {
2022-10-23 12:27:26 +02:00
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 := cutWord(text, "keyhold"); ok {
2023-01-04 23:14:21 +01:00
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 := cutWord(text, "type"); ok {
2022-10-20 15:21:20 +02:00
for _, r := range s {
2023-08-15 17:57:53 +02:00
sym := xkb.Utf32ToKeysym(uint32(r))
if sym == 0 {
2023-04-19 14:23:05 +02:00
warn("invalid character: " + string(r))
2023-08-15 17:57:53 +02:00
} else if c := getChord(keymap, sym); c.Key != 0 {
c.KeyDown(keyboard)
time.Sleep(typehold)
c.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)
2023-04-19 14:23:05 +02:00
} else {
2023-08-15 17:57:53 +02:00
warn("impossible character for layout: " + string(r))
2022-10-20 15:21:20 +02:00
}
2023-01-04 23:00:49 +01:00
time.Sleep(typedelay)
2022-10-20 15:21:20 +02:00
}
} else if s, ok := cutWord(text, "typedelay"); ok {
2022-10-23 12:27:26 +02:00
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 := cutWord(text, "typehold"); ok {
2023-01-04 23:14:21 +01:00
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 := cutWord(text, "click"); ok {
2022-10-20 15:21:20 +02:00
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 := cutWord(text, "buttondown"); ok {
2022-10-20 15:21:20 +02:00
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 := cutWord(text, "buttonup"); ok {
2022-10-20 15:21:20 +02:00
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 := cutWord(text, "wheel"); ok {
2023-04-23 11:32:59 +02:00
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 {
2023-04-23 11:32:59 +02:00
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 {
2022-10-20 15:21:20 +02:00
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 := cutWord(text, "mouseto"); ok {
2022-10-20 15:21:20 +02:00
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 := cutWord(text, "mousemove"); ok {
2022-10-20 15:21:20 +02:00
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())
}
}