time scrolling
The hovertext for Friday’s xkcd Borrow Your Laptop asks for scrolling mapped to undo and redo. How hard can it be? There’s more than one way to do this, but the other ways are boring. What if we’re using a program that doesn’t allow rebinding keys or buttons?
First we need to grab the scroll events. To avoid too much mischief, we’ll only bind to scroll events with the control key down. XGrabButton sure does take a lot of arguments, but only the first few are interesting.
unsigned int modifiers = ControlMask;
Bool ownerevents = False;
unsigned int eventmask = ButtonPressMask;
int pointermode = GrabModeAsync;
int keyboardmode = GrabModeAsync;
Window confine = 0;
Cursor cursor = 0;
XGrabButton(dpy, 4, modifiers, root, ownerevents, eventmask,
pointermode, keyboardmode, confine, cursor);
XGrabButton(dpy, 5, modifiers, root, ownerevents, eventmask,
pointermode, keyboardmode, confine, cursor);
while (1) {
XNextEvent(dpy, &ev);
switch (ev.type) {
case ButtonPress:
switch (ev.xbutton.button) {
case 4:
printf("undo\n");
break;
case 5:
printf("redo\n");
break;
}
}
}
This includes enough of an event loop to see that it’s working. Either undo or redo will be printed for each click of the wheel. Not exciting yet, but enough to see the code is working.
Now we need a function to send input back to the X server. Instead of printing undo on the screen, we want to make it happen. The usual way is to use XSendEvent, but many applications reject synthetic events as insecure. Fortunately, there’s a workaround using the XTest extension. In fact, it’s even less code to use XTest than XSendEvent. I have no idea why this is considered secure, though.
void
undo(Display *dpy, int reallyredo)
{
KeySym keysym;
KeyCode keycode, ctrlcode;
keysym = reallyredo ? 'r' : 'u';
keycode = XKeysymToKeycode(dpy, keysym);
ctrlcode = XKeysymToKeycode(dpy, XK_Control_L);
XTestFakeKeyEvent(dpy, ctrlcode, False, 0);
XTestFakeKeyEvent(dpy, keycode, True, 0);
XTestFakeKeyEvent(dpy, keycode, False, 0);
XTestFakeKeyEvent(dpy, ctrlcode, True, 0);
}
Because we have the control key held down, we need to fake lift it, or it’ll look like we’re sending control-u and control-r.
Testing this confirms it works, as a stream of u’s and r’s appear in my xterm, but they’re not being printed as output. They’re actually echoed input. This can be confirmed by moving the cursor over a different xterm and scrolling. Now the letters appear in that window.
Now comes the fun part. Different applications have different hotkeys for undo and redo, so we need to send different keystrokes to each. It’s only a very tiny function to read the window name (and crudely assume that’s the application).
int
whosthere(Display *dpy, Window w)
{
XTextProperty prop;
if (!XGetTextProperty(dpy, w, &prop, XA_WM_NAME))
return 0;
if (strstr(prop.value, "Mozilla Firefox"))
return 1;
if (strstr(prop.value, "VIM"))
return 2;
return 3;
}
Adapt the undo function accordingly.
switch (whosthere(dpy, w)) {
case 1:
keysym = 'Z';
keycode = XKeysymToKeycode(dpy, keysym);
if (reallyredo)
XTestFakeKeyEvent(dpy, shiftcode, True, 0);
XTestFakeKeyEvent(dpy, keycode, True, 0);
XTestFakeKeyEvent(dpy, keycode, False, 0);
if (reallyredo)
XTestFakeKeyEvent(dpy, shiftcode, False, 0);
break;
case 2:
if (reallyredo) {
keysym = 'r';
keycode = XKeysymToKeycode(dpy, keysym);
XTestFakeKeyEvent(dpy, keycode, True, 0);
XTestFakeKeyEvent(dpy, keycode, False, 0);
} else {
keysym = 'u';
keycode = XKeysymToKeycode(dpy, keysym);
XTestFakeKeyEvent(dpy, ctrlcode, False, 0);
XTestFakeKeyEvent(dpy, keycode, True, 0);
XTestFakeKeyEvent(dpy, keycode, False, 0);
XTestFakeKeyEvent(dpy, ctrlcode, True, 0);
}
break;
case 3:
keysym = reallyredo ? XK_Down : XK_Up;
keycode = XKeysymToKeycode(dpy, keysym);
XTestFakeKeyEvent(dpy, ctrlcode, False, 0);
XTestFakeKeyEvent(dpy, keycode, True, 0);
XTestFakeKeyEvent(dpy, keycode, False, 0);
XTestFakeKeyEvent(dpy, ctrlcode, True, 0);
break;
If we’re running Firefox, undo is control-z and redo is shift-control-z. We already have control down, so just a matter of pressing shift or not, and then z. For vim, undo is plain u, no control. But redo is control-r. A little of this, a little of that. And for anything else, like the shell, we just send up and down keys and hope that works.
cc -Wall -I/usr/X11R6/include -L/usr/X11R6/lib -lX11 -lXtst timescroll.c
The xbindkeys and xvkbd programs may also be interesting.