stdwinjector
Copying another idea from Old New Thing and porting to unix. This time it’s Piping to notepad. Instead of starting a new notepad process, let’s feed stdin to any existing window.
This requires as many as two helper functions. First we need to find a target, by having the user click on a window.
static Window
findtarget(Display *disp, Window root)
{
Cursor cursor;
XEvent ev;
cursor = XCreateFontCursor(disp, XC_target);
XGrabPointer(disp, root, False, ButtonPressMask|ButtonReleaseMask,
GrabModeSync, GrabModeAsync, root, cursor, CurrentTime);
XAllowEvents(disp, SyncPointer, CurrentTime);
XWindowEvent(disp, root, ButtonPressMask, &ev);
XUngrabPointer(disp, CurrentTime);
XFreeCursor(disp, cursor);
return ev.xbutton.subwindow;
}
This is similar to code in xkill or xwd, although a little simpler. We create a special mouse pointer, then grab the pointer so that we receive the click and not the window below. The click event will tell us what window was clicked, however.
Next we need to jam a string into our target window. We send fake keypresses for this part.
static void
writestring(Display *disp, Window root, Window target, char *string, size_t len)
{
XKeyEvent kev;
KeySym keysym;
KeyCode keycode;
char *p, *end;
end = string + len;
memset(&kev, 0, sizeof(kev));
kev.display = disp;
kev.window = target;
kev.root = root;
kev.subwindow = None;
kev.time = CurrentTime;
kev.same_screen = True;
for (p = string; p < end; p++) {
unsigned char c = *p;
if (c < 0x20) {
if (c == '\n')
c = '\r';
keysym = 0xff00 | c;
} else if (c >= 0x20 && c <= 0x7f) {
keysym = c;
} else {
keysym = 0;
}
if (keysym == 0)
continue;
keycode = XKeysymToKeycode(disp, keysym);
if (keycode == 0)
continue;
kev.keycode = keycode;
kev.type = KeyPress;
XSendEvent(disp, target, True, KeyPressMask, (XEvent *)&kev);
kev.type = KeyRelease;
XSendEvent(disp, target, True, KeyReleaseMask, (XEvent *)&kev);
XFlush(disp);
}
}
For most of the characters we’re interested in, the keysym value is the same as the ascii value. Some special keys, like tab and return (and backspace, etc.) map to control sequences (ctrl-I, ctrl-M, ctrl-H) and have keysym values of 0xff00 + an offset. And of course, unix new lines are actually line feeds, not carriage returns, so we need to convert that or it won’t have the expected result.
$ cc -std=c99 -I/usr/X11R6/include -L/usr/X11R6/lib -lX11 stdwinjector.c
$ printf "ls std\t\n" | ./a.out
Click another xterm with a shell in the same directory, and magically...
$ ls stdwinjector.c
stdwinjector.c
The complete stdwinjector source is in here.
There are a number of drawbacks to this approach. First, many programs do not accept fake events and will ignore input. xterm ignores it by default, but can be changed by control clicking to open the option menu. Firefox also reportedly ignores events. I was able to successfully inject input into the location bar, but when I tried with a web page text area, the whole browser window hung and I had to kill it. I suppose breaking the entire event loop is one way of ignoring fake events. Instead of hacking around this for each application (or using LD_PRELOAD), it would be nice if the X server instead offered a way to disable flagging fake events globally.
Other programmatic approaches involve using the test extension, but that only works on the currently focused window. The obvious solution is probably to use xclip, but that works less well with a long running process that, for whatever reason, you’d like to continuously feed into a window. It’s also limited by the fact that clipboards are in short supply, whereas sending targeted events works for lots of pipes. Not to mention you’re never quite sure what or when some other program will dump into the clipboard.
Some good notes on X Window Event Handling with other caveats and pitfalls.