From 3180f9c583abadcb27eff81533bd71ae496c0c8e Mon Sep 17 00:00:00 2001 From: ed Date: Fri, 22 Nov 2019 14:07:59 +0100 Subject: [PATCH] xbanish added to sde --- xbanish/Makefile | 39 ++++ xbanish/README.md | 44 +++++ xbanish/xbanish.1 | 58 ++++++ xbanish/xbanish.c | 465 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 606 insertions(+) create mode 100644 xbanish/Makefile create mode 100644 xbanish/README.md create mode 100644 xbanish/xbanish.1 create mode 100644 xbanish/xbanish.c diff --git a/xbanish/Makefile b/xbanish/Makefile new file mode 100644 index 0000000..fcd0f4f --- /dev/null +++ b/xbanish/Makefile @@ -0,0 +1,39 @@ +# vim:ts=8 + +CC ?= cc +CFLAGS ?= -O2 +CFLAGS += -Wall -Wunused -Wmissing-prototypes -Wstrict-prototypes -Wunused + +PREFIX ?= /usr/local +BINDIR ?= $(PREFIX)/bin +MANDIR ?= $(PREFIX)/man/man1 + +INSTALL_PROGRAM ?= install -s +INSTALL_DATA ?= install + +X11BASE ?= /usr/X11R6 +INCLUDES?= -I$(X11BASE)/include +LDPATH ?= -L$(X11BASE)/lib +LIBS += -lX11 -lXfixes -lXi + +PROG = xbanish +OBJS = xbanish.o + +all: $(PROG) + +$(PROG): $(OBJS) + $(CC) $(OBJS) $(LDPATH) $(LIBS) -o $@ + +$(OBJS): *.c + $(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@ + +install: all + mkdir -p $(DESTDIR)$(BINDIR) + $(INSTALL_PROGRAM) $(PROG) $(DESTDIR)$(BINDIR) + mkdir -p $(DESTDIR)$(MANDIR) + $(INSTALL_DATA) -m 644 xbanish.1 $(DESTDIR)$(MANDIR)/xbanish.1 + +clean: + rm -f $(PROG) $(OBJS) + +.PHONY: all install clean diff --git a/xbanish/README.md b/xbanish/README.md new file mode 100644 index 0000000..e7cb645 --- /dev/null +++ b/xbanish/README.md @@ -0,0 +1,44 @@ +## xbanish + +xbanish hides the mouse cursor when you start typing, and shows it again when +the mouse cursor moves or a mouse button is pressed. +This is similar to xterm's `pointerMode` setting, but xbanish works globally in +the X11 session. + +unclutter's -keystroke mode is supposed to do this, but it's +[broken](https://bugs.launchpad.net/ubuntu/+source/unclutter/+bug/54148). +I looked into fixing it, but the unclutter source code is terrible, so I wrote +xbanish. + +The name comes from +[ratpoison's](https://www.nongnu.org/ratpoison/) +"banish" command that sends the cursor to the corner of the screen. + +### Implementation + +If the XInput extension is supported, xbanish uses it to request input from all +attached keyboards and mice. +If XInput 2.2 is supported, raw mouse movement and button press inputs are +requested which helps detect cursor movement while in certain applications such +as Chromium. + +If Xinput is not available, xbanish recurses through the list of windows +starting at the root, and calls `XSelectInput()` on each window to receive +notification of mouse motion, button presses, and key presses. + +In response to any available keyboard input events, the cursor is hidden. +On mouse movement or button events, the cursor is shown. + +xbanish initially hid the cursor by calling `XGrabPointer()` with a blank +cursor image, similar to unclutter's -grab mode, but this had problematic +interactions with certain X applications. +For example, xlock could not grab the pointer and sometimes didn't lock, +xwininfo wouldn't work at all, Firefox would quickly hide the Awesome Bar +dropdown as soon as a key was pressed, and xterm required two middle-clicks to +paste the clipboard contents. + +To avoid these problems and simplify the implementation, xbanish now uses the +modern +[`Xfixes` extension](http://cgit.freedesktop.org/xorg/proto/fixesproto/plain/fixesproto.txt) +to easily hide and show the cursor with `XFixesHideCursor()` and +`XFixesShowCursor()`. diff --git a/xbanish/xbanish.1 b/xbanish/xbanish.1 new file mode 100644 index 0000000..f467214 --- /dev/null +++ b/xbanish/xbanish.1 @@ -0,0 +1,58 @@ +.Dd $Mdocdate: September 16 2019$ +.Dt XBANISH 1 +.Os +.Sh NAME +.Nm xbanish +.Nd hide the X11 mouse cursor when a key is pressed +.Sh SYNOPSIS +.Nm +.Op Fl a +.Op Fl d +.Op Fl i Ar modifier +.Op Fl m Ar nw|ne|sw|se +.Sh DESCRIPTION +.Nm +hides the X11 mouse cursor when a key is pressed. +The cursor is shown again when it is moved or a mouse button is pressed. +This is similar to the +.Xr xterm 1 +setting +.Ic pointerMode +but the effect is global in the X11 session. +.Sh OPTIONS +.Bl -tag -width Ds +.It Fl a +Always keep mouse cursor hidden while +.Nm +is running. +.It Fl d +Print debugging messages to stdout. +.It Fl i Ar modifier +Ignore pressed key if +.Ar modifier +is used. +Modifiers are: +.Ic shift , +.Ic lock +(CapsLock), +.Ic control , +.Ic mod1 +(Alt or Meta), +.Ic mod2 +(NumLock), +.Ic mod3 +(Hyper), +.Ic mod4 +(Super, Windows, or Command), and +.Ic mod5 +(ISO Level 3 Shift). +.It Fl m Ar nw|ne|sw|se +When hiding the mouse cursor, move it to this corner of the screen, +then move it back when showing the cursor. +.El +.Sh SEE ALSO +.Xr XFixes 3 +.Sh AUTHORS +.Nm +was written by +.An joshua stein Aq Mt jcs@jcs.org . diff --git a/xbanish/xbanish.c b/xbanish/xbanish.c new file mode 100644 index 0000000..866797a --- /dev/null +++ b/xbanish/xbanish.c @@ -0,0 +1,465 @@ +/* + * xbanish + * Copyright (c) 2013-2015 joshua stein + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +void hide_cursor(void); +void show_cursor(void); +int snoop_xinput(Window); +void snoop_legacy(Window); +void usage(void); +int swallow_error(Display *, XErrorEvent *); + +/* xinput event type ids to be filled in later */ +static int button_press_type = -1; +static int button_release_type = -1; +static int key_press_type = -1; +static int key_release_type = -1; +static int motion_type = -1; + +extern char *__progname; + +static Display *dpy; +static int hiding = 0, legacy = 0, always_hide = 0; +static unsigned char ignored; + +static int debug = 0; +#define DPRINTF(x) { if (debug) { printf x; } }; + +static int move = 0, move_x, move_y; +enum move_types { + MOVE_NW = 1, + MOVE_NE, + MOVE_SW, + MOVE_SE, +}; + +int +main(int argc, char *argv[]) +{ + int ch, i; + XEvent e; + XGenericEventCookie *cookie; + + struct mod_lookup { + char *name; + int mask; + } mods[] = { + {"shift", ShiftMask}, {"lock", LockMask}, + {"control", ControlMask}, {"mod1", Mod1Mask}, + {"mod2", Mod2Mask}, {"mod3", Mod3Mask}, + {"mod4", Mod4Mask}, {"mod5", Mod5Mask} + }; + + while ((ch = getopt(argc, argv, "adi:m:")) != -1) + switch (ch) { + case 'a': + always_hide = 1; + break; + case 'd': + debug = 1; + break; + case 'i': + for (i = 0; + i < sizeof(mods) / sizeof(struct mod_lookup); i++) + if (strcasecmp(optarg, mods[i].name) == 0) + ignored |= mods[i].mask; + + break; + case 'm': + if (strcmp(optarg, "nw") == 0) + move = MOVE_NW; + else if (strcmp(optarg, "ne") == 0) + move = MOVE_NE; + else if (strcmp(optarg, "sw") == 0) + move = MOVE_SW; + else if (strcmp(optarg, "se") == 0) + move = MOVE_SE; + else { + warnx("invalid '-m' argument"); + usage(); + } + break; + default: + usage(); + } + + argc -= optind; + argv += optind; + + if (!(dpy = XOpenDisplay(NULL))) + errx(1, "can't open display %s", XDisplayName(NULL)); + +#ifdef __OpenBSD__ + if (pledge("stdio", NULL) == -1) + err(1, "pledge"); +#endif + + XSetErrorHandler(swallow_error); + + if (snoop_xinput(DefaultRootWindow(dpy)) == 0) { + DPRINTF(("no XInput devices found, using legacy snooping")); + legacy = 1; + snoop_legacy(DefaultRootWindow(dpy)); + } + + if (always_hide) + hide_cursor(); + + for (;;) { + cookie = &e.xcookie; + XNextEvent(dpy, &e); + + int etype = e.type; + if (e.type == motion_type) + etype = MotionNotify; + else if (e.type == key_press_type || + e.type == key_release_type) + etype = KeyRelease; + else if (e.type == button_press_type || + e.type == button_release_type) + etype = ButtonRelease; + + switch (etype) { + case KeyRelease: + if (ignored) { + unsigned int state = 0; + + /* masks are only set on key release, if + * ignore is set we must throw out non-release + * events here */ + if (e.type == key_press_type) { + break; + } + + /* extract modifier state */ + if (e.type == key_release_type) { + /* xinput device event */ + XDeviceKeyEvent *key = + (XDeviceKeyEvent *) &e; + state = key->state; + } else if (e.type == KeyRelease) { + /* legacy event */ + state = e.xkey.state; + } + + if (state & ignored) { + DPRINTF(("ignoring key %d\n", state)); + break; + } + } + + hide_cursor(); + break; + + case ButtonRelease: + case MotionNotify: + if (!always_hide) + show_cursor(); + break; + + case CreateNotify: + if (legacy) { + DPRINTF(("new window, snooping on it\n")); + + /* not sure why snooping directly on the window + * doesn't work, so snoop on all windows from + * its parent (probably root) */ + snoop_legacy(e.xcreatewindow.parent); + } + break; + + case GenericEvent: + /* xi2 raw event */ + XGetEventData(dpy, cookie); + XIDeviceEvent *xie = (XIDeviceEvent *)cookie->data; + + switch (xie->evtype) { + case XI_RawMotion: + case XI_RawButtonPress: + if (!always_hide) + show_cursor(); + break; + + case XI_RawButtonRelease: + break; + + default: + DPRINTF(("unknown XI event type %d\n", + xie->evtype)); + } + + XFreeEventData(dpy, cookie); + break; + + default: + DPRINTF(("unknown event type %d\n", e.type)); + } + } +} + +void +hide_cursor(void) +{ + Window win; + int x, y, h, w, junk; + unsigned int ujunk; + + DPRINTF(("keystroke, %shiding cursor\n", (hiding ? "already " : ""))); + + if (!hiding) { + if (move) { + if (XQueryPointer(dpy, DefaultRootWindow(dpy), + &win, &win, &x, &y, &junk, &junk, &ujunk)) { + move_x = x; + move_y = y; + + h = XHeightOfScreen(DefaultScreenOfDisplay(dpy)); + w = XWidthOfScreen(DefaultScreenOfDisplay(dpy)); + + switch (move) { + case MOVE_NW: + x = 0; + y = 0; + break; + case MOVE_NE: + x = w; + y = 0; + break; + case MOVE_SW: + x = 0; + y = h; + break; + case MOVE_SE: + x = w; + y = h; + break; + } + + XWarpPointer(dpy, None, DefaultRootWindow(dpy), + 0, 0, 0, 0, x, y); + } else { + move_x = -1; + move_y = -1; + warn("failed finding cursor coordinates"); + } + } + + XFixesHideCursor(dpy, DefaultRootWindow(dpy)); + hiding = 1; + } +} + +void +show_cursor(void) +{ + DPRINTF(("mouse moved, %sunhiding cursor\n", + (hiding ? "" : "already "))); + + if (!hiding) + return; + + if (move && move_x != -1 && move_y != -1) + XWarpPointer(dpy, None, DefaultRootWindow(dpy), 0, 0, 0, 0, + move_x, move_y); + + XFixesShowCursor(dpy, DefaultRootWindow(dpy)); + hiding = 0; +} + +int +snoop_xinput(Window win) +{ + int opcode, event, error, numdevs, i, j; + int major, minor, rc, rawmotion = 0; + int ev = 0; + unsigned char mask[(XI_LASTEVENT + 7)/8]; + XDeviceInfo *devinfo; + XInputClassInfo *ici; + XDevice *device; + XIEventMask evmasks[1]; + + if (!XQueryExtension(dpy, "XInputExtension", &opcode, &event, &error)) { + DPRINTF(("XInput extension not available")); + return 0; + } + + /* + * If we support xinput 2, use that for raw motion and button events to + * get pointer data when the cursor is over a Chromium window. We + * could also use this to get raw key input and avoid the other XInput + * stuff, but we may need to be able to examine the key value later to + * filter out ignored keys. + */ + major = minor = 2; + rc = XIQueryVersion(dpy, &major, &minor); + if (rc != BadRequest) { + memset(mask, 0, sizeof(mask)); + + XISetMask(mask, XI_RawMotion); + XISetMask(mask, XI_RawButtonPress); + evmasks[0].deviceid = XIAllMasterDevices; + evmasks[0].mask_len = sizeof(mask); + evmasks[0].mask = mask; + + XISelectEvents(dpy, win, evmasks, 1); + XFlush(dpy); + + rawmotion = 1; + + DPRINTF(("using xinput2 raw motion events\n")); + } + + devinfo = XListInputDevices(dpy, &numdevs); + XEventClass event_list[numdevs * 2]; + for (i = 0; i < numdevs; i++) { + if (devinfo[i].use != IsXExtensionKeyboard && + devinfo[i].use != IsXExtensionPointer) + continue; + + if (!(device = XOpenDevice(dpy, devinfo[i].id))) + break; + + for (ici = device->classes, j = 0; j < devinfo[i].num_classes; + ici++, j++) { + switch (ici->input_class) { + case KeyClass: + DPRINTF(("attaching to keyboard device %s " + "(use %d)\n", devinfo[i].name, + devinfo[i].use)); + + DeviceKeyPress(device, key_press_type, + event_list[ev]); ev++; + DeviceKeyRelease(device, key_release_type, + event_list[ev]); ev++; + break; + + case ButtonClass: + if (rawmotion) + continue; + + DPRINTF(("attaching to buttoned device %s " + "(use %d)\n", devinfo[i].name, + devinfo[i].use)); + + DeviceButtonPress(device, button_press_type, + event_list[ev]); ev++; + DeviceButtonRelease(device, + button_release_type, event_list[ev]); ev++; + break; + + case ValuatorClass: + if (rawmotion) + continue; + + DPRINTF(("attaching to pointing device %s " + "(use %d)\n", devinfo[i].name, + devinfo[i].use)); + + DeviceMotionNotify(device, motion_type, + event_list[ev]); ev++; + break; + } + } + + if (XSelectExtensionEvent(dpy, win, event_list, ev)) { + warn("error selecting extension events"); + return 0; + } + } + + return ev; +} + +void +snoop_legacy(Window win) +{ + Window parent, root, *kids = NULL; + XSetWindowAttributes sattrs; + unsigned int nkids = 0, i; + + /* + * Firefox stops responding to keys when KeyPressMask is used, so + * settle for KeyReleaseMask + */ + int type = PointerMotionMask | KeyReleaseMask | Button1MotionMask | + Button2MotionMask | Button3MotionMask | Button4MotionMask | + Button5MotionMask | ButtonMotionMask; + + if (XQueryTree(dpy, win, &root, &parent, &kids, &nkids) == FALSE) { + warn("can't query window tree\n"); + goto done; + } + + XSelectInput(dpy, root, type); + + /* listen for newly mapped windows */ + sattrs.event_mask = SubstructureNotifyMask; + XChangeWindowAttributes(dpy, root, CWEventMask, &sattrs); + + for (i = 0; i < nkids; i++) { + XSelectInput(dpy, kids[i], type); + snoop_legacy(kids[i]); + } + +done: + if (kids != NULL) + XFree(kids); /* hide yo kids */ +} + +void +usage(void) +{ + fprintf(stderr, "usage: %s [-a] [-d] [-i mod] [-m nw|ne|sw|se]\n", + __progname); + exit(1); +} + +int +swallow_error(Display *d, XErrorEvent *e) +{ + if (e->error_code == BadWindow) + /* no biggie */ + return 0; + else if (e->error_code & FirstExtensionError) + /* error requesting input on a particular xinput device */ + return 0; + else + errx(1, "got X error %d", e->error_code); +}