World Playground Deceit.net

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:

  1. xev doesn't report mouse clicks in -root mode. No problem, I reached for xinput test-xi2 --root instead.
  2. But xinput returns X11 keycodes for keyboard events instead of the keysyms consumed by xdotool. Wasted at least half an hour trying to find a way to convert between those, solved by using a dump command of xmodmap.

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.