X11 record and replay
Very small post about a cool script I cobbled together this evening to record and replay keyboard/mouse events in X11.
But first, the reason I went down this rabbit hole: I needed to delete my ridiculously large (to the point of uselessness) Discogs wantlist that had something like 15k entries (because adding a release adds all versions to the list) but Discogs laughs at you. The best you get is a "tick column header to select all items, click delete, confirm choice and wait for refresh" workflow with 250 entries/page. I had 60 such pages.
You guessed it, time for a small and janky xdotool
script using absolute mouse coordinates (gathered via xdotool getmouselocation
) to click on my Firefox window in my stead… I'll be frank, if you don't get the urge to
automatize this, you're no hacker.
So I search around on the Interweb to no avail, until I decide to reach for the famous words: how hard can it be?
Only two major hurdles encountered in these two hours:
xev
doesn't report mouse clicks in-root
mode. No problem, I reached forxinput test-xi2 --root
instead.- But
xinput
returns X11 keycodes for keyboard events instead of the keysyms consumed byxdotool
. Wasted at least half an hour trying to find a way to convert between those, solved by using a dump command ofxmodmap
.
The resulting POSIX AWK script doing the real work (xinput2xdotool.awk) is quite cute:
#!/usr/bin/awk -f # Dependencies: xmodmap function emit() { if (prev_time) print "sleep", (time - prev_time) / 1000.0 prev_time = time if (pos[1] != prev_x || pos[2] != prev_y) { print "mousemove", pos[1], pos[2] prev_x = pos[1]; prev_y = pos[2] } print cmd, arg } function read_keymap() { while (("xmodmap -pke" | getline) == 1) { if (NF > 3) xkbmap[$2] = $4 } close("xmodmap -pke") } BEGIN {read_keymap()} $1 == "EVENT" { if (cmd) emit() if ($4 == "(ButtonPress)") cmd = "mousedown" else if ($4 == "(ButtonRelease)") cmd = "mouseup" else if ($4 == "(KeyPress)") cmd = "keydown" else if ($4 == "(KeyRelease)") cmd = "keyup" else cmd = "" next } cmd && $1 == "time:" {time = $2; next} cmd && $1 == "detail:" { arg = (cmd ~ /^key/ ? xkbmap[$2] : $2) if (cmd == "keydown" && arg == "Scroll_Lock") exit next } cmd && $1 == "event:" {split($2, pos, "/"); next}
PS: just discovered that libinput record/replay was available all along.