492 lines
12 KiB
Go
492 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"git.sr.ht/~geb/dotool/xkb"
|
|
"git.sr.ht/~geb/opt"
|
|
"github.com/bendahl/uinput"
|
|
"math"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
)
|
|
|
|
var Version string
|
|
|
|
func usage() {
|
|
fmt.Println(`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
|
|
wheel AMOUNT (a positive AMOUNT is up, a negative is down)
|
|
hwheel AMOUNT (a positive AMOUNT is right, a negative is left)
|
|
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)
|
|
|
|
|
|
dotool is installed with a udev rule to allow users in group input to run
|
|
it without root permissions.
|
|
|
|
You can add yourself to group input by running:
|
|
|
|
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.
|
|
|
|
echo key shift+1 x:exclam shift+k:2 | dotool
|
|
|
|
|
|
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
|
|
Print the possible Linux keys and exit.
|
|
|
|
--list-x-keys
|
|
Print the possible XKB keys and exit.
|
|
|
|
--version
|
|
Print the version and exit.
|
|
`)
|
|
}
|
|
|
|
func fatal(a ...any) {
|
|
fmt.Fprintln(os.Stderr, "dotool:", fmt.Sprint(a...))
|
|
os.Exit(1)
|
|
}
|
|
|
|
func warn(a ...any) {
|
|
fmt.Fprintln(os.Stderr, "dotool: WARNING:", fmt.Sprint(a...))
|
|
}
|
|
|
|
func log(err error) {
|
|
if err != nil {
|
|
warn(err.Error())
|
|
}
|
|
}
|
|
|
|
type Chord struct {
|
|
Super, AltGr, Ctrl, Alt, Shift bool
|
|
Key int
|
|
}
|
|
|
|
func parseChord(keymap *xkb.Keymap, chord string) (Chord, error) {
|
|
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 {
|
|
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++ {
|
|
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])
|
|
}
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
func (c *Chord) KeyDown(kb uinput.Keyboard) {
|
|
if c.Super {
|
|
log(kb.KeyDown(uinput.KeyLeftmeta))
|
|
}
|
|
if c.AltGr {
|
|
log(kb.KeyDown(84))
|
|
}
|
|
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.AltGr {
|
|
log(kb.KeyUp(84))
|
|
}
|
|
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 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
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func main() {
|
|
var keymap *xkb.Keymap
|
|
{
|
|
layout := os.Getenv("DOTOOL_XKB_LAYOUT")
|
|
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)
|
|
defer ctx.Unref()
|
|
keymap = ctx.KeymapNewFromNames(&names, xkb.KeymapCompileNoFlags)
|
|
if keymap == nil {
|
|
fatal("failed to compile keymap")
|
|
}
|
|
defer keymap.Unref()
|
|
|
|
initKeys(keymap)
|
|
}
|
|
|
|
{
|
|
optset := opt.NewOptionSet()
|
|
|
|
optset.FlagFunc("h", func() error {
|
|
usage()
|
|
os.Exit(0)
|
|
panic("unreachable")
|
|
})
|
|
optset.Alias("h", "help")
|
|
|
|
optset.FlagFunc("list-keys", func() error {
|
|
listKeys(keymap, LinuxKeys)
|
|
os.Exit(0)
|
|
panic("unreachable")
|
|
})
|
|
|
|
optset.FlagFunc("list-x-keys", func() error {
|
|
listKeys(keymap, XKeys)
|
|
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 := cutWord(text, "key"); ok {
|
|
for _, field := range strings.Fields(s) {
|
|
if chord, err := parseChord(keymap, field); err == nil {
|
|
chord.KeyDown(keyboard)
|
|
time.Sleep(keyhold)
|
|
chord.KeyUp(keyboard)
|
|
} else {
|
|
warn(err.Error())
|
|
}
|
|
time.Sleep(keydelay)
|
|
}
|
|
} else if s, ok := cutWord(text, "keydown"); ok {
|
|
for _, field := range strings.Fields(s) {
|
|
if chord, err := parseChord(keymap, field); err == nil {
|
|
chord.KeyDown(keyboard)
|
|
} else {
|
|
warn(err.Error())
|
|
}
|
|
time.Sleep(keydelay)
|
|
}
|
|
} else if s, ok := cutWord(text, "keyup"); ok {
|
|
for _, field := range strings.Fields(s) {
|
|
if chord, err := parseChord(keymap, field); err == nil {
|
|
chord.KeyUp(keyboard)
|
|
} else {
|
|
warn(err.Error())
|
|
}
|
|
time.Sleep(keydelay)
|
|
}
|
|
} else if s, ok := cutWord(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 := cutWord(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 := cutWord(text, "type"); ok {
|
|
for _, r := range s {
|
|
if sym := xkb.Utf32ToKeysym(uint32(r)); sym == 0 {
|
|
warn("invalid character: " + string(r))
|
|
} else {
|
|
chord := getChord(keymap, sym)
|
|
if chord.Key == 0 {
|
|
warn("impossible character for layout: " + string(r))
|
|
time.Sleep(typehold)
|
|
} else {
|
|
chord.KeyDown(keyboard)
|
|
time.Sleep(typehold)
|
|
chord.KeyUp(keyboard)
|
|
}
|
|
}
|
|
time.Sleep(typedelay)
|
|
}
|
|
} else if s, ok := cutWord(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 := cutWord(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 := cutWord(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 := cutWord(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 := cutWord(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 := cutWord(text, "wheel"); 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
|
|
_, 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 {
|
|
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 {
|
|
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())
|
|
}
|
|
}
|