keychord: bind a sequence of keys, and dispatch events while waiting

Original keychord patch by Hai Nguyen <hhai2105@gmail.com>, updated for
dwm 6.4+ grabkeys() by Aaron Zueger <contact@azureorange.xyz>. This
revision rebases onto dwm 6.8 and fixes an event-loss bug in the chord
wait loop.

The wait loop used to discard every event that was not a KeyPress:

    while (running && !XNextEvent(dpy, &event) && !ran)
            if(event.type == KeyPress)
                    break;

Two consequences, both reproduced on Xephyr:

1. Unrecoverable input freeze. grabbuttons() grabs unfocused clients
   GrabModeSync/GrabModeSync, so clicking one freezes pointer AND
   keyboard until the grabbing client calls XAllowEvents. dwm does that
   in buttonpress() -- but if the ButtonPress is discarded here,
   buttonpress() never runs. The grab stays frozen forever and the loop
   can never exit, because a frozen keyboard cannot deliver the KeyPress
   it is waiting for. Only killing dwm ends it; XSendEvent is discarded
   by this same loop and XTEST queues behind the frozen device.
   Repro: press a chord prefix, then click any unfocused client.
   Observed: XGrabKeyboard -> GrabFrozen, XQueryPointer -> Button1 held.

2. Orphaned windows. A MapRequest arriving mid-chord is dropped, so a
   window opening during a chord is never managed. DestroyNotify,
   Expose and ConfigureRequest are lost the same way.

Dispatch through handler[] as run() does, breaking on KeyPress before
dispatch so keypress() cannot recurse. The `&& !ran` term is dropped:
ran is always 0 on entry and nothing in the body can set it.
This commit is contained in:
Lukasz Kasprzak authored and Łukasz Pankowski committed 2026-10-19 09:10:49 +02:00
1 parent 636dcaf763
commit 9610f461c2
3 files changed
+219 -168

No files matched your search

+65 -14
View File
@@ -102,9 +102,14 @@ struct Client {
typedef struct {
unsigned int mod;
KeySym keysym;
} Key;
typedef struct {
unsigned int n;
const Key keys[5];
void (*func)(const Arg *);
const Arg arg;
} Key;
} Keychord;
typedef struct {
const char *symbol;
@@ -294,6 +299,7 @@ static Monitor *mons, *selmon, *prevmon;
static Window root, wmcheckwin;
static const char **dmenucmd;
static int colormodechanged;
unsigned int currentkey = 0;
/* configuration, allows nested code to access above variables */
#include "config.h"
@@ -1088,7 +1094,8 @@ grabkeys(void)
{
updatenumlockmask();
{
unsigned int i, j, k;
/* unsigned int i, j, k; */
unsigned int i, c, k;
unsigned int modifiers[] = { 0, LockMask, numlockmask, numlockmask|LockMask };
int start, end, skip;
KeySym *syms;
@@ -1098,15 +1105,18 @@ grabkeys(void)
syms = XGetKeyboardMapping(dpy, start, end - start + 1, &skip);
if (!syms)
return;
for (k = start; k <= end; k++)
for (i = 0; i < LENGTH(keys); i++)
for (i = 0; i < LENGTH(keychords); i++)
/* skip modifier codes, we do that ourselves */
if (keys[i].keysym == syms[(k - start) * skip])
for (j = 0; j < LENGTH(modifiers); j++)
if (keychords[i]->keys[currentkey].keysym == syms[(k - start) * skip])
for (c = 0; c < LENGTH(modifiers); c++)
XGrabKey(dpy, k,
keys[i].mod | modifiers[j],
keychords[i]->keys[currentkey].mod | modifiers[c],
root, True,
GrabModeAsync, GrabModeAsync);
if(currentkey > 0)
XGrabKey(dpy, XKeysymToKeycode(dpy, XK_Escape), AnyModifier, root, True, GrabModeAsync, GrabModeAsync);
XFree(syms);
}
}
@@ -1163,17 +1173,58 @@ isuniquegeom(XineramaScreenInfo *unique, size_t n, XineramaScreenInfo *info)
void
keypress(XEvent *e)
{
unsigned int i;
/* unsigned int i; */
XEvent event = *e;
unsigned int ran = 0;
KeySym keysym;
XKeyEvent *ev;
ev = &e->xkey;
keysym = XKeycodeToKeysym(dpy, (KeyCode)ev->keycode, 0);
for (i = 0; i < LENGTH(keys); i++)
if (keysym == keys[i].keysym
&& CLEANMASK(keys[i].mod) == CLEANMASK(ev->state)
&& keys[i].func)
keys[i].func(&(keys[i].arg));
Keychord *arr1[sizeof(keychords) / sizeof(Keychord*)];
Keychord *arr2[sizeof(keychords) / sizeof(Keychord*)];
memcpy(arr1, keychords, sizeof(keychords));
Keychord **rpointer = arr1;
Keychord **wpointer = arr2;
size_t r = sizeof(keychords)/ sizeof(Keychord*);
while(1){
ev = &event.xkey;
keysym = XKeycodeToKeysym(dpy, (KeyCode)ev->keycode, 0);
size_t w = 0;
for (int i = 0; i < r; i++){
if(keysym == (*(rpointer + i))->keys[currentkey].keysym
&& CLEANMASK((*(rpointer + i))->keys[currentkey].mod) == CLEANMASK(ev->state)
&& (*(rpointer + i))->func){
if((*(rpointer + i))->n == currentkey +1){
(*(rpointer + i))->func(&((*(rpointer + i))->arg));
ran = 1;
}else{
*(wpointer + w) = *(rpointer + i);
w++;
}
}
}
currentkey++;
if(w == 0 || ran == 1)
break;
grabkeys();
/* Events arriving mid-chord must be dispatched, not dropped:
* discarding a ButtonPress that activated the GrabModeSync grab
* from grabbuttons() freezes pointer and keyboard permanently,
* since only buttonpress()'s XAllowEvents can release it. */
while (running && !XNextEvent(dpy, &event)) {
if (event.type == KeyPress)
break;
if (handler[event.type])
handler[event.type](&event); /* call handler */
}
r = w;
Keychord **holder = rpointer;
rpointer = wpointer;
wpointer = holder;
}
currentkey = 0;
grabkeys();
}
void