Looks like evremap will do what I want, plus a nifty bonus! The following config lets me use CapsLock + N/P/etc to navigate. And if I just tap CapsLock without pressing anything else, it will act as escape.
device_name = "Telink Wireless Receiver"
[[dual_role]]
input = "KEY_CAPSLOCK"
hold = ["KEY_F19"]
tap = ["KEY_ESC"]
[[remap]]
input = ["KEY_F19", "KEY_N"]
output = ["KEY_DOWN"]
[[remap]]
input = ["KEY_F19", "KEY_P"]
output = ["KEY_UP"]
[[remap]]
input = ["KEY_F19", "KEY_B"]
output = ["KEY_LEFT"]
[[remap]]
input = ["KEY_F19", "KEY_F"]
output = ["KEY_RIGHT"]
[[remap]]
input = ["KEY_F19", "KEY_A"]
output = ["KEY_HOME"]
[[remap]]
input = ["KEY_F19", "KEY_E"]
output = ["KEY_END"]
Note: I used F19 because it doesn’t seem to be bound to anything by default. Apparently, a bunch of the other function keys already did things, as described in /usr/share/X11/xkb/symbols/inet
$ cat /usr/share/X11/xkb/symbols/inet | grep FK13
key { [ XF86Tools ] };
key { [ XF86MailForward ] };
key { [ XF86Word ] }; // F2
key { [ XF86MailForward ] }; // F3
$ cat /usr/share/X11/xkb/symbols/inet | grep FK20
key { [ XF86AudioMicMute ] };
My city has a tool library program that sounds exactly like this (I haven’t tried it yet, not sure how well it works in practice). Would be especially nice for one-off sorts of tools you don’t expect to use often.
The downside is you’d need to line up your project with their hours, and hope no one else is using it when you need it. But if you have the flexibility to plan ahead, could be a nifty resource.