Compare commits

...

64 commits
1.0 ... master

Author SHA1 Message Date
John Gebbie
945a7daede uinput udev rule MODE 0660 -> 0620 2024-08-13 14:36:06 +01:00
John Gebbie
7c0a53ef94 version 1.5 2024-05-04 12:36:44 +01:00
John Gebbie
5bc814cb39 bump dependency 2024-05-04 12:35:34 +01:00
John Gebbie
b5812c001d prefer keys on lower levels for realsies 2023-11-05 16:51:46 +00:00
John Gebbie
ba67589517 version 1.4 2023-10-26 14:33:08 +01:00
John Gebbie
5404859248 replace --keyboard-name with $DOTOOL_KEYBOARD_NAME 2023-10-26 13:13:48 +01:00
John Gebbie
ba251d1afd readme & man: tweak 2023-10-12 23:09:57 +01:00
John Gebbie
7688cc321c detail mouseto and mousemove usage 2023-09-20 10:37:02 +01:00
John Gebbie
39689f9b34 man: tweak example 2023-09-20 10:24:43 +01:00
John Gebbie
a169e2e131 bump dependencies 2023-09-13 11:55:19 +01:00
John Gebbie
f47908ebf2 tweak usage 2023-09-13 11:42:47 +01:00
John Gebbie
240298271c readme: public-inbox -> numen 2023-09-13 11:03:02 +01:00
John Gebbie
a580b0944b more verbose --list-keys output with modifiers 2023-09-13 11:03:02 +01:00
John Gebbie
a0049f668b support altgr+ 2023-09-13 11:03:02 +01:00
John Gebbie
ec566eb9ff make Chord not a pointer receiver 2023-09-13 11:03:02 +01:00
John Gebbie
d5a6aeef74 init modifiers 2023-09-13 11:03:02 +01:00
John Gebbie
29f30ad52b prefer keys without modifiers 2023-09-13 11:03:02 +01:00
John Gebbie
f0e9e7102d readme: add packages section 2023-09-13 11:03:02 +01:00
John Gebbie
d0b10e88bc install: replace install.sh with build.sh 2023-09-13 11:03:02 +01:00
John Gebbie
e939562ee4 add a manpage and simplify --help and the readme
doesn't update the install which is about to change
2023-09-13 11:03:02 +01:00
John Gebbie
3845810f7c add cheeky support for dead keys 2023-08-16 18:46:39 +01:00
John Gebbie
d1e4b66dd1 add --keyboard-name option 2023-08-16 18:40:39 +01:00
John Gebbie
ee4a1cd95d install: add -buildvcs=false 2023-08-07 22:06:13 +01:00
John Gebbie
eb41d9d70c version 1.3 2023-06-01 16:34:54 +01:00
John Gebbie
618358e136 xkb: use unsafe.Slice 2023-06-01 16:34:40 +01:00
John Gebbie
41268e1ece minor usage formatting 2023-05-11 11:21:13 +01:00
John Gebbie
1b330dd7c1 always use xkb and complain if only variant is set 2023-05-11 11:21:13 +01:00
John Gebbie
874650ad22 /tmp/dotool_pipe -> /tmp/dotool-pipe 2023-04-28 16:44:54 +01:00
John Gebbie
1b79ad39bb use optset.FlagFunc 2023-04-28 16:42:29 +01:00
John Gebbie
7b38f27d30 remove unnecessary #include 2023-04-23 11:20:24 +01:00
John Gebbie
b12bc48ef5 tweak fatal() and warn() 2023-04-23 10:59:54 +01:00
John Gebbie
87ec7cf475 handle commands given without arguments better 2023-04-23 10:47:06 +01:00
John Gebbie
abb50f8438 scroll -> wheel and hwheel 2023-04-23 10:32:59 +01:00
John Gebbie
f8d6461da0 overhaul to use xkbcommon 2023-04-21 11:25:08 +01:00
John Gebbie
01fcc71667 readme: numenvoice.com -> numenvoice.org 2023-04-21 11:23:21 +01:00
John Gebbie
9018410748 readme: improve 2023-03-05 15:08:52 +00:00
John Gebbie
7287191747 add MatchProduct to X conf file 2023-02-25 22:37:57 +00:00
John Gebbie
bc16ae7fe0 version 1.2 2023-02-06 11:40:01 +00:00
John Gebbie
5230969381 omit k: and x: in error messages 2023-02-06 11:20:49 +00:00
John Gebbie
9682b408ab install: have ./_install.sh for packaging 2023-01-31 12:26:10 +00:00
John Gebbie
8dd7d72f35 add keyboard layout sway config file 2023-01-30 14:41:14 +00:00
John Gebbie
3b44ef5aae add keyboard layout X config file 2023-01-29 21:26:39 +00:00
John Gebbie
39e0e25299 tweak udev rule 2023-01-29 21:12:48 +00:00
John Gebbie
04227e1d0e stop some x: keys simulating shift 2023-01-26 13:34:00 +00:00
John Gebbie
c828f2d8ed improve pipe error messages 2023-01-26 13:33:57 +00:00
John Gebbie
c427ab5394 dotoold: rm -rf pipe -> rm -f -- pipe 2023-01-26 13:32:57 +00:00
John Gebbie
c778104f02 version 1.1 2023-01-21 13:30:35 +00:00
John Gebbie
7c091d6654 bump copyright years 2023-01-21 13:30:35 +00:00
John Gebbie
41be5e5e4f install: let $DOTOOL_VERSION set --version 2023-01-21 13:30:35 +00:00
John Gebbie
589d8d63a0 add keyboard layout udev rule 2023-01-21 13:30:35 +00:00
John Gebbie
18a8fd0385 readme: format 2023-01-21 13:30:35 +00:00
John Gebbie
0bd6024b2e readme: mention --help and numen 2023-01-21 13:30:35 +00:00
John Gebbie
a5a9086a7c add .gitignore 2023-01-21 13:30:35 +00:00
John Gebbie
dc12ab157c add --version, rearrange, and overhaul install 2023-01-21 13:30:35 +00:00
John Gebbie
3a313c2cf0 add default delays 2023-01-04 23:18:29 +00:00
John Gebbie
29da15e1d4 add keyhold and typehold 2023-01-04 22:32:03 +00:00
John Gebbie
68987774fd sleep after not before keypresses 2023-01-04 22:00:49 +00:00
John Gebbie
07d333b3eb mention dotoold and dotoolc in --help 2023-01-04 21:47:34 +00:00
John Gebbie
dbd8a9098a abort if any command-line arguments 2022-12-28 10:55:19 +00:00
John Gebbie
9f57e0ccc9 use updated opt package 2022-12-28 10:43:19 +00:00
John Gebbie
22dd3a62ff stop x:backspace simulating shift 2022-12-17 21:28:48 +00:00
John Gebbie
08bf51c855 readme: format and add support me section 2022-12-12 18:27:10 +00:00
John Gebbie
b28cb87289 readme: add license section 2022-11-19 10:08:08 +00:00
John Gebbie
9874f01cfb use >&2 instead of >/dev/stderr 2022-11-01 10:26:02 +00:00
16 changed files with 1377 additions and 875 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/dotool

View file

@ -1,2 +1,2 @@
# This allows users in group input to use dotool without root permissions.
KERNEL=="uinput", GROUP="input", MODE="0660", OPTIONS+="static_node=uinput"
KERNEL=="uinput", GROUP="input", MODE="0620", OPTIONS+="static_node=uinput"

70
CHANGELOG.md Normal file
View file

@ -0,0 +1,70 @@
# Changelog
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)
### Added
- Added X11 and sway config files for the virtual keyboard's layout.
- Added ./\_install.sh for packaging the basic files.
### Fixed
- Stopped some x: keys simulating shift.
## [1.1](https://git.sr.ht/~geb/dotool/refs/1.1)
### Added
- Added --version.
- Added keyhold and typehold.
### Changed
- There is now a default keydelay, keyhold, typedelay and typehold.
- Delays now come after keypresses not before.
- Now aborts if there are any command-line arguments.
### Fixed
- Added a udev rule so it shouldn't type guff if you're using a non-us
keyboard layout on at least X11.
- Fixed x:backspace that was simulating shift.

View file

@ -1,34 +1,47 @@
# dotool
dotool reads commands from stdin and simulates keyboard and mouse events.
It works everywhere on Linux, including in X11, Wayland and TTYs.
dotool reads actions from stdin and simulates keyboard/mouse input using
Linux's uinput module. It works systemwide and supports keyboard layouts.
It takes about half a second to register the virtual device, but it can be kept using the daemon.
## Install From Packages
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
With go (>=1.19) run `sudo ./install.sh`.
With `go`, `libxkbcommon-dev` and `scdoc` installed, run:
./build.sh && sudo ./build.sh install
And to trigger the udev rule, run:
sudo udevadm control --reload && sudo udevadm trigger
## Usage
dotool will require root permissions unless you are in group input.
See the [manpage](doc/dotool.1.scd).
This greets the world:
`echo 'type Sup, Lads!' | dotool`
## Numen and Contact
This screams for three seconds:
`{ echo keydown A; sleep 3; echo key H shift+1; } | dotool`
dotool was written for [Numen](https://numenvoice.org), which has a
[chat on Matrix](https://matrix.to/#/#numen:matrix.org) you're welcome to join.
This drags the mouse:
`printf %s\\n 'buttondown left' 'mousemove 0 100' 'buttonup left' | dotool`
You can also send questions or patches by composing an email to
[~geb/numen@lists.sr.ht](https://lists.sr.ht/~geb/numen).
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
```
## Support My Work 👀
## Contact
[Thank you!](https://liberapay.com/geb)
You can ask a question or send a patch by composing an email to [~geb/public-inbox@lists.sr.ht](https://lists.sr.ht/~geb/public-inbox).
## License
GPLv3 only, see [LICENSE](./LICENSE).
Copyright (c) 2022-2023 John Gebbie

22
build.sh Executable file
View file

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

132
doc/dotool.1.scd Normal file
View file

@ -0,0 +1,132 @@
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

465
dotool.go
View file

@ -4,8 +4,9 @@ import (
"bufio"
"errors"
"fmt"
"github.com/bendahl/uinput"
"git.sr.ht/~geb/dotool/xkb"
"git.sr.ht/~geb/opt"
"github.com/bendahl/uinput"
"math"
"os"
"strconv"
@ -14,15 +15,42 @@ import (
"unicode"
)
func fatal(v ...interface{}) {
fmt.Fprint(os.Stderr, "dotool: ")
fmt.Fprintln(os.Stderr, v...)
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
--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(v ...interface{}) {
fmt.Fprint(os.Stderr, "dotool WARNING: ")
fmt.Fprintln(os.Stderr, v...)
func warn(a ...any) {
fmt.Fprintln(os.Stderr, "dotool: WARNING:", fmt.Sprint(a...))
}
func log(err error) {
@ -32,20 +60,46 @@ func log(err error) {
}
type Chord struct {
Super bool
Ctrl bool
Alt bool
Shift bool
Super, AltGr, Ctrl, Alt, Shift bool
Key int
level uint32 // just for initKeys
}
func parseChord(chord string) (Chord, error) {
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":
@ -57,195 +111,168 @@ func parseChord(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)
}
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)
}
} 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) Press(kb uinput.Keyboard) {
func (c Chord) KeyDown(kb uinput.Keyboard) {
if c.Super {
log(kb.KeyDown(uinput.KeyLeftmeta))
log(kb.KeyDown(super))
}
if c.AltGr {
log(kb.KeyDown(altgr))
}
if c.Ctrl {
log(kb.KeyDown(uinput.KeyLeftctrl))
log(kb.KeyDown(ctrl))
}
if c.Alt {
log(kb.KeyDown(uinput.KeyLeftalt))
log(kb.KeyDown(alt))
}
if c.Shift {
log(kb.KeyDown(uinput.KeyLeftshift))
}
log(kb.KeyPress(c.Key))
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))
}
}
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(shift))
}
log(kb.KeyDown(c.Key))
}
func (c *Chord) KeyUp(kb uinput.Keyboard) {
func (c Chord) KeyUp(kb uinput.Keyboard) {
if c.Super {
log(kb.KeyUp(uinput.KeyLeftmeta))
log(kb.KeyUp(super))
}
if c.AltGr {
log(kb.KeyUp(altgr))
}
if c.Ctrl {
log(kb.KeyUp(uinput.KeyLeftctrl))
log(kb.KeyUp(ctrl))
}
if c.Alt {
log(kb.KeyUp(uinput.KeyLeftalt))
log(kb.KeyUp(alt))
}
if c.Shift {
log(kb.KeyUp(uinput.KeyLeftshift))
log(kb.KeyUp(shift))
}
log(kb.KeyUp(c.Key))
}
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
typedelay MILLISECONDS
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.
--list-keys
Print the supported Linux keys and their keycodes.
--list-x-keys
Print the supported X11 keys and their Linux keycodes.
`)
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) {
if strings.HasPrefix(s, cmd + " ") || strings.HasPrefix(s, cmd + "\t") {
return s[len(cmd)+1:], true
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
{
optset := opt.NewOptionSet()
help := func(bool) error {
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)
}
{
o := opt.NewOptSet()
o.FlagFunc("h", func() error {
usage()
os.Exit(0)
panic("unreachable")
}
listKeys := 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
}
}
}
})
o.Alias("h", "help")
o.FlagFunc("list-keys", func() error {
listKeys(keymap, LinuxKeys)
os.Exit(0)
panic("unreachable")
}
listXKeys := 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
}
}
}
})
o.FlagFunc("list-x-keys", func() error {
listKeys(keymap, XKeys)
os.Exit(0)
panic("unreachable")
}
optset.BoolFunc("h", help)
optset.BoolFunc("help", help)
optset.BoolFunc("list-keys", listKeys)
optset.BoolFunc("list-x-keys", listXKeys)
err := optset.Parse(true, os.Args[1:])
})
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", []byte("dotool keyboard"))
keyboardName := []byte(os.Getenv("DOTOOL_KEYBOARD_NAME"))
if len(keyboardName) == 0 {
keyboardName = []byte("dotool keyboard")
}
keyboard, err := uinput.CreateKeyboard("/dev/uinput", keyboardName)
if err != nil {
fatal(err.Error())
}
@ -264,8 +291,10 @@ func main() {
}
defer mouse.Close()
keydelay := time.Duration(0)
typedelay := time.Duration(0)
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() {
@ -273,34 +302,36 @@ func main() {
if text == "" {
continue
}
if s, ok := cutCmd(text, "key"); ok {
if s, ok := cutWord(text, "key"); ok {
for _, field := range strings.Fields(s) {
time.Sleep(keydelay)
if chord, err := parseChord(field); err == nil {
chord.Press(keyboard)
} else {
warn(err.Error())
}
}
} else if s, ok := cutCmd(text, "keydown"); ok {
for _, field := range strings.Fields(s) {
time.Sleep(keydelay)
if chord, err := parseChord(field); err == nil {
if chord, err := parseChord(keymap, field); err == nil {
chord.KeyDown(keyboard)
} else {
warn(err.Error())
}
}
} else if s, ok := cutCmd(text, "keyup"); ok {
for _, field := range strings.Fields(s) {
time.Sleep(keydelay)
if chord, err := parseChord(field); err == nil {
time.Sleep(keyhold)
chord.KeyUp(keyboard)
} else {
warn(err.Error())
}
time.Sleep(keydelay)
}
} else if s, ok := cutCmd(text, "keydelay"); ok {
} 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 {
@ -308,17 +339,37 @@ func main() {
} else {
warn("invalid delay: " + sc.Text())
}
} else if s, ok := cutCmd(text, "type"); ok {
for _, r := range s {
time.Sleep(typedelay)
if chord, ok := runeChords[unicode.ToLower(r)]; ok {
if unicode.IsUpper(r) {
chord.Shift = true
}
chord.Press(keyboard)
}
} 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 := cutCmd(text, "typedelay"); ok {
} 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 {
@ -326,7 +377,15 @@ func main() {
} else {
warn("invalid delay: " + sc.Text())
}
} else if s, ok := cutCmd(text, "click"); ok {
} 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":
@ -339,7 +398,7 @@ func main() {
warn("unknown button: " + button)
}
}
} else if s, ok := cutCmd(text, "buttondown"); ok {
} else if s, ok := cutWord(text, "buttondown"); ok {
for _, button := range strings.Fields(s) {
switch button {
case "left", "1":
@ -352,7 +411,7 @@ func main() {
warn("unknown button: " + button)
}
}
} else if s, ok := cutCmd(text, "buttonup"); ok {
} else if s, ok := cutWord(text, "buttonup"); ok {
for _, button := range strings.Fields(s) {
switch button {
case "left", "1":
@ -365,7 +424,23 @@ func main() {
warn("unknown button: " + button)
}
}
} else if s, ok := cutCmd(text, "scroll"); ok {
} 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 {
@ -373,7 +448,7 @@ func main() {
} else {
warn("invalid scroll amount: " + sc.Text())
}
} else if s, ok := cutCmd(text, "mouseto"); ok {
} 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 {
@ -390,7 +465,7 @@ func main() {
} else {
warn("invalid coordinate: " + sc.Text())
}
} else if s, ok := cutCmd(text, "mousemove"); ok {
} 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 {

13
dotoolc
View file

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

30
dotoold
View file

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

4
go.mod
View file

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

8
go.sum
View file

@ -1,4 +1,4 @@
git.sr.ht/~geb/opt v0.0.0-20220627180516-52214b5b84a1 h1:bmje0IdPzrY5nX6fAx8KuHP5G8EP4XMedMFcrssfJXc=
git.sr.ht/~geb/opt v0.0.0-20220627180516-52214b5b84a1/go.mod h1:T5QFtG9s8i/kW5pDVCke6Mt2WmElJCIfTL1HMdpP7Rk=
github.com/bendahl/uinput v1.6.0 h1:fM6r3OSC17rHh758mizKjSBuqi+XinhiGd4N3pWvZiI=
github.com/bendahl/uinput v1.6.0/go.mod h1:Np7w3DINc9wB83p12fTAM3DPPhFnAKP0WTXRqCQJ6Z8=
git.sr.ht/~geb/opt v0.0.0-20230911153257-e72225a1933c h1:gIC1gnCgoasPHks1x6MB+bgDmIWMxKc5HIJPJrsV5Ck=
git.sr.ht/~geb/opt v0.0.0-20230911153257-e72225a1933c/go.mod h1:S6h1g8P7DyG7i7YIHZ5IpYbC6lzZB9DYIEl8PyXOmsg=
github.com/bendahl/uinput v1.7.0 h1:nA4fm8Wu8UYNOPykIZm66nkWEyvxzfmJ8YC02PM40jg=
github.com/bendahl/uinput v1.7.0/go.mod h1:Np7w3DINc9wB83p12fTAM3DPPhFnAKP0WTXRqCQJ6Z8=

View file

@ -1,4 +0,0 @@
#!/bin/sh
go build && cp -v dotool dotoolc dotoold /usr/local/bin || exit
mkdir -p /etc/udev/rules.d && cp -v 80-dotool.rules /etc/udev/rules.d || exit
udevadm trigger

1263
keys.go

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,4 @@
#!/bin/sh
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 /usr/share/man/man1/dotool.1

144
xkb/xkb.go Normal file
View file

@ -0,0 +1,144 @@
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
}

View file

@ -1,42 +0,0 @@
#!/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'
}
echo 'var xKeysNormal = map[string]int{'
{
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'
echo '"XF86WebCam": uinput.KeyCamera,'
echo '"Print": uinput.KeyPrint,'
} | sed 's/^".*"/\L&/; s/^/\t/'
echo '}'
echo ''
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' |
# Remove duplicate keys
sed '/^"KP_Decimal":.*Kpcomma/ d; /\<Key102Nd\>/ d'
} | sed 's/^".*"/\L&/; s/^/\t/'
echo '}'