xbanish added to sde
This commit is contained in:
parent
477cf482ad
commit
3180f9c583
|
@ -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
|
|
@ -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()`.
|
|
@ -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 .
|
|
@ -0,0 +1,465 @@
|
|||
/*
|
||||
* xbanish
|
||||
* Copyright (c) 2013-2015 joshua stein <jcs@jcs.org>
|
||||
*
|
||||
* 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 <err.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <X11/X.h>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Intrinsic.h>
|
||||
#include <X11/extensions/Xfixes.h>
|
||||
#include <X11/extensions/XInput.h>
|
||||
#include <X11/extensions/XInput2.h>
|
||||
|
||||
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);
|
||||
}
|
Loading…
Reference in New Issue