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:
xevdoesn't report mouse clicks in-rootmode. No problem, I reached forxinput test-xi2 --rootinstead.- But
xinputreturns 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.