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 actions from stdin and simulates input using uinput. The supported actions are: 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 (where X and Y are percentages between 0.0 and 1.0) mousemove X Y (where X and Y are amounts to move) keydelay MILLISECONDS keyhold MILLISECONDS typedelay MILLISECONDS typehold MILLISECONDS --keyboard-name=NAME Specify the name to give the 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) { 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 "altgr": c.AltGr = 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(super)) } if c.AltGr { log(kb.KeyDown(altgr)) } if c.Ctrl { log(kb.KeyDown(ctrl)) } if c.Alt { log(kb.KeyDown(alt)) } if c.Shift { log(kb.KeyDown(shift)) } log(kb.KeyDown(c.Key)) } func (c Chord) KeyUp(kb uinput.Keyboard) { if c.Super { log(kb.KeyUp(super)) } if c.AltGr { log(kb.KeyUp(altgr)) } if c.Ctrl { log(kb.KeyUp(ctrl)) } if c.Alt { log(kb.KeyUp(alt)) } if c.Shift { log(kb.KeyUp(shift)) } 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 listKeys(keymap *xkb.Keymap, keys map[string]Chord) { var margin int 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 } 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) } keyboardName := []byte("dotool keyboard") { o := opt.NewOptSet() o.FlagFunc("h", func() error { usage() os.Exit(0) panic("unreachable") }) o.Alias("h", "help") o.Func("keyboard-name", func(s string) error { keyboardName = []byte(s) return nil }) o.FlagFunc("list-keys", func() error { listKeys(keymap, LinuxKeys) os.Exit(0) panic("unreachable") }) o.FlagFunc("list-x-keys", func() error { listKeys(keymap, XKeys) os.Exit(0) panic("unreachable") }) o.FlagFunc("version", func() error { fmt.Println(Version) os.Exit(0) panic("unreachable") }) err := o.Parse(true, os.Args[1:]) if err != nil { fatal(err.Error()) } if len(o.Args()) > 0 { fatal("there should be no arguments, commands are read from stdin") } } keyboard, err := uinput.CreateKeyboard("/dev/uinput", keyboardName) 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 { sym := xkb.Utf32ToKeysym(uint32(r)) if sym == 0 { warn("invalid character: " + string(r)) } 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) } else { warn("impossible character for layout: " + string(r)) } 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()) } }