dwm-keychord-20260717-f63cde9.diff (13875B)
1 From 9c020a20a2e1091e8b47edfea5e8795d8b451bc9 Mon Sep 17 00:00:00 2001 2 From: Lukasz Kasprzak <lukas@labunix.xyz> 3 Date: Fri, 17 Jul 2026 15:45:22 +0200 4 Subject: [PATCH] keychord: bind a sequence of keys, and dispatch events while 5 waiting 6 7 Original keychord patch by Hai Nguyen <hhai2105@gmail.com>, updated for 8 dwm 6.4+ grabkeys() by Aaron Zueger <contact@azureorange.xyz>. This 9 revision rebases onto dwm 6.8 and fixes an event-loss bug in the chord 10 wait loop. 11 12 The wait loop used to discard every event that was not a KeyPress: 13 14 while (running && !XNextEvent(dpy, &event) && !ran) 15 if(event.type == KeyPress) 16 break; 17 18 Two consequences, both reproduced on Xephyr: 19 20 1. Unrecoverable input freeze. grabbuttons() grabs unfocused clients 21 GrabModeSync/GrabModeSync, so clicking one freezes pointer AND 22 keyboard until the grabbing client calls XAllowEvents. dwm does that 23 in buttonpress() -- but if the ButtonPress is discarded here, 24 buttonpress() never runs. The grab stays frozen forever and the loop 25 can never exit, because a frozen keyboard cannot deliver the KeyPress 26 it is waiting for. Only killing dwm ends it; XSendEvent is discarded 27 by this same loop and XTEST queues behind the frozen device. 28 Repro: press a chord prefix, then click any unfocused client. 29 Observed: XGrabKeyboard -> GrabFrozen, XQueryPointer -> Button1 held. 30 31 2. Orphaned windows. A MapRequest arriving mid-chord is dropped, so a 32 window opening during a chord is never managed. DestroyNotify, 33 Expose and ConfigureRequest are lost the same way. 34 35 Dispatch through handler[] as run() does, breaking on KeyPress before 36 dispatch so keypress() cannot recurse. The `&& !ran` term is dropped: 37 ran is always 0 on entry and nothing in the body can set it. 38 --- 39 config.def.h | 81 ++++++++++++++++++++++++++-------------------------- 40 dwm.c | 79 +++++++++++++++++++++++++++++++++++++++++--------- 41 2 files changed, 106 insertions(+), 54 deletions(-) 42 43 diff --git a/config.def.h b/config.def.h 44 index 81c3fc0..7c7ec36 100644 45 --- a/config.def.h 46 +++ b/config.def.h 47 @@ -47,11 +47,11 @@ static const Layout layouts[] = { 48 49 /* key definitions */ 50 #define MODKEY Mod1Mask 51 -#define TAGKEYS(KEY,TAG) \ 52 - { MODKEY, KEY, view, {.ui = 1 << TAG} }, \ 53 - { MODKEY|ControlMask, KEY, toggleview, {.ui = 1 << TAG} }, \ 54 - { MODKEY|ShiftMask, KEY, tag, {.ui = 1 << TAG} }, \ 55 - { MODKEY|ControlMask|ShiftMask, KEY, toggletag, {.ui = 1 << TAG} }, 56 +#define TAGKEYS(KEY,TAG) \ 57 + &((Keychord){1, {{MODKEY, KEY}}, view, {.ui = 1 << TAG} }), \ 58 + &((Keychord){1, {{MODKEY|ControlMask, KEY}}, toggleview, {.ui = 1 << TAG} }), \ 59 + &((Keychord){1, {{MODKEY|ShiftMask, KEY}}, tag, {.ui = 1 << TAG} }), \ 60 + &((Keychord){1, {{MODKEY|ControlMask|ShiftMask, KEY}}, toggletag, {.ui = 1 << TAG} }), 61 62 /* helper for spawning shell commands in the pre dwm-5.0 fashion */ 63 #define SHCMD(cmd) { .v = (const char*[]){ "/bin/sh", "-c", cmd, NULL } } 64 @@ -61,41 +61,42 @@ static char dmenumon[2] = "0"; /* component of dmenucmd, manipulated in spawn() 65 static const char *dmenucmd[] = { "dmenu_run", "-m", dmenumon, "-fn", dmenufont, "-nb", col_gray1, "-nf", col_gray3, "-sb", col_cyan, "-sf", col_gray4, NULL }; 66 static const char *termcmd[] = { "st", NULL }; 67 68 -static const Key keys[] = { 69 - /* modifier key function argument */ 70 - { MODKEY, XK_p, spawn, {.v = dmenucmd } }, 71 - { MODKEY|ShiftMask, XK_Return, spawn, {.v = termcmd } }, 72 - { MODKEY, XK_b, togglebar, {0} }, 73 - { MODKEY, XK_j, focusstack, {.i = +1 } }, 74 - { MODKEY, XK_k, focusstack, {.i = -1 } }, 75 - { MODKEY, XK_i, incnmaster, {.i = +1 } }, 76 - { MODKEY, XK_d, incnmaster, {.i = -1 } }, 77 - { MODKEY, XK_h, setmfact, {.f = -0.05} }, 78 - { MODKEY, XK_l, setmfact, {.f = +0.05} }, 79 - { MODKEY, XK_Return, zoom, {0} }, 80 - { MODKEY, XK_Tab, view, {0} }, 81 - { MODKEY|ShiftMask, XK_c, killclient, {0} }, 82 - { MODKEY, XK_t, setlayout, {.v = &layouts[0]} }, 83 - { MODKEY, XK_f, setlayout, {.v = &layouts[1]} }, 84 - { MODKEY, XK_m, setlayout, {.v = &layouts[2]} }, 85 - { MODKEY, XK_space, setlayout, {0} }, 86 - { MODKEY|ShiftMask, XK_space, togglefloating, {0} }, 87 - { MODKEY, XK_0, view, {.ui = ~0 } }, 88 - { MODKEY|ShiftMask, XK_0, tag, {.ui = ~0 } }, 89 - { MODKEY, XK_comma, focusmon, {.i = -1 } }, 90 - { MODKEY, XK_period, focusmon, {.i = +1 } }, 91 - { MODKEY|ShiftMask, XK_comma, tagmon, {.i = -1 } }, 92 - { MODKEY|ShiftMask, XK_period, tagmon, {.i = +1 } }, 93 - TAGKEYS( XK_1, 0) 94 - TAGKEYS( XK_2, 1) 95 - TAGKEYS( XK_3, 2) 96 - TAGKEYS( XK_4, 3) 97 - TAGKEYS( XK_5, 4) 98 - TAGKEYS( XK_6, 5) 99 - TAGKEYS( XK_7, 6) 100 - TAGKEYS( XK_8, 7) 101 - TAGKEYS( XK_9, 8) 102 - { MODKEY|ShiftMask, XK_q, quit, {0} }, 103 +static Keychord *keychords[] = { 104 + /* Keys function argument */ 105 + &((Keychord){1, {{MODKEY, XK_p}}, spawn, {.v = dmenucmd } }), 106 + &((Keychord){1, {{MODKEY|ShiftMask, XK_Return}}, spawn, {.v = termcmd } }), 107 + &((Keychord){2, {{MODKEY, XK_e}, {MODKEY, XK_e}}, spawn, {.v = termcmd } }), 108 + &((Keychord){1, {{MODKEY, XK_b}}, togglebar, {0} }), 109 + &((Keychord){1, {{MODKEY, XK_j}}, focusstack, {.i = +1 } }), 110 + &((Keychord){1, {{MODKEY, XK_k}}, focusstack, {.i = -1 } }), 111 + &((Keychord){1, {{MODKEY, XK_i}}, incnmaster, {.i = +1 } }), 112 + &((Keychord){1, {{MODKEY, XK_d}}, incnmaster, {.i = -1 } }), 113 + &((Keychord){1, {{MODKEY, XK_h}}, setmfact, {.f = -0.05} }), 114 + &((Keychord){1, {{MODKEY, XK_l}}, setmfact, {.f = +0.05} }), 115 + &((Keychord){1, {{MODKEY, XK_Return}}, zoom, {0} }), 116 + &((Keychord){1, {{MODKEY, XK_Tab}}, view, {0} }), 117 + &((Keychord){1, {{MODKEY|ShiftMask, XK_c}}, killclient, {0} }), 118 + &((Keychord){1, {{MODKEY, XK_t}}, setlayout, {.v = &layouts[0]} }), 119 + &((Keychord){1, {{MODKEY, XK_f}}, setlayout, {.v = &layouts[1]} }), 120 + &((Keychord){1, {{MODKEY, XK_m}}, setlayout, {.v = &layouts[2]} }), 121 + &((Keychord){1, {{MODKEY, XK_space}}, setlayout, {0} }), 122 + &((Keychord){1, {{MODKEY|ShiftMask, XK_space}}, togglefloating, {0} }), 123 + &((Keychord){1, {{MODKEY, XK_0}}, view, {.ui = ~0 } }), 124 + &((Keychord){1, {{MODKEY|ShiftMask, XK_0}}, tag, {.ui = ~0 } }), 125 + &((Keychord){1, {{MODKEY, XK_comma}}, focusmon, {.i = -1 } }), 126 + &((Keychord){1, {{MODKEY, XK_period}}, focusmon, {.i = +1 } }), 127 + &((Keychord){1, {{MODKEY|ShiftMask, XK_comma}}, tagmon, {.i = -1 } }), 128 + &((Keychord){1, {{MODKEY|ShiftMask, XK_period}}, tagmon, {.i = +1 } }), 129 + &((Keychord){1, {{MODKEY|ShiftMask, XK_q}}, quit, {0} }), 130 + TAGKEYS( XK_1, 0) 131 + TAGKEYS( XK_2, 1) 132 + TAGKEYS( XK_3, 2) 133 + TAGKEYS( XK_4, 3) 134 + TAGKEYS( XK_5, 4) 135 + TAGKEYS( XK_6, 5) 136 + TAGKEYS( XK_7, 6) 137 + TAGKEYS( XK_8, 7) 138 + TAGKEYS( XK_9, 8) 139 }; 140 141 /* button definitions */ 142 diff --git a/dwm.c b/dwm.c 143 index 53b393e..243b12c 100644 144 --- a/dwm.c 145 +++ b/dwm.c 146 @@ -101,9 +101,14 @@ struct Client { 147 typedef struct { 148 unsigned int mod; 149 KeySym keysym; 150 +} Key; 151 + 152 +typedef struct { 153 + unsigned int n; 154 + const Key keys[5]; 155 void (*func)(const Arg *); 156 const Arg arg; 157 -} Key; 158 +} Keychord; 159 160 typedef struct { 161 const char *symbol; 162 @@ -266,6 +271,7 @@ static Display *dpy; 163 static Drw *drw; 164 static Monitor *mons, *selmon; 165 static Window root, wmcheckwin; 166 +unsigned int currentkey = 0; 167 168 /* configuration, allows nested code to access above variables */ 169 #include "config.h" 170 @@ -954,7 +960,8 @@ grabkeys(void) 171 { 172 updatenumlockmask(); 173 { 174 - unsigned int i, j, k; 175 + /* unsigned int i, j, k; */ 176 + unsigned int i, c, k; 177 unsigned int modifiers[] = { 0, LockMask, numlockmask, numlockmask|LockMask }; 178 int start, end, skip; 179 KeySym *syms; 180 @@ -964,15 +971,18 @@ grabkeys(void) 181 syms = XGetKeyboardMapping(dpy, start, end - start + 1, &skip); 182 if (!syms) 183 return; 184 + 185 for (k = start; k <= end; k++) 186 - for (i = 0; i < LENGTH(keys); i++) 187 + for (i = 0; i < LENGTH(keychords); i++) 188 /* skip modifier codes, we do that ourselves */ 189 - if (keys[i].keysym == syms[(k - start) * skip]) 190 - for (j = 0; j < LENGTH(modifiers); j++) 191 + if (keychords[i]->keys[currentkey].keysym == syms[(k - start) * skip]) 192 + for (c = 0; c < LENGTH(modifiers); c++) 193 XGrabKey(dpy, k, 194 - keys[i].mod | modifiers[j], 195 + keychords[i]->keys[currentkey].mod | modifiers[c], 196 root, True, 197 GrabModeAsync, GrabModeAsync); 198 + if(currentkey > 0) 199 + XGrabKey(dpy, XKeysymToKeycode(dpy, XK_Escape), AnyModifier, root, True, GrabModeAsync, GrabModeAsync); 200 XFree(syms); 201 } 202 } 203 @@ -999,17 +1009,58 @@ isuniquegeom(XineramaScreenInfo *unique, size_t n, XineramaScreenInfo *info) 204 void 205 keypress(XEvent *e) 206 { 207 - unsigned int i; 208 + /* unsigned int i; */ 209 + XEvent event = *e; 210 + unsigned int ran = 0; 211 KeySym keysym; 212 XKeyEvent *ev; 213 214 - ev = &e->xkey; 215 - keysym = XKeycodeToKeysym(dpy, (KeyCode)ev->keycode, 0); 216 - for (i = 0; i < LENGTH(keys); i++) 217 - if (keysym == keys[i].keysym 218 - && CLEANMASK(keys[i].mod) == CLEANMASK(ev->state) 219 - && keys[i].func) 220 - keys[i].func(&(keys[i].arg)); 221 + Keychord *arr1[sizeof(keychords) / sizeof(Keychord*)]; 222 + Keychord *arr2[sizeof(keychords) / sizeof(Keychord*)]; 223 + memcpy(arr1, keychords, sizeof(keychords)); 224 + Keychord **rpointer = arr1; 225 + Keychord **wpointer = arr2; 226 + 227 + size_t r = sizeof(keychords)/ sizeof(Keychord*); 228 + 229 + while(1){ 230 + ev = &event.xkey; 231 + keysym = XKeycodeToKeysym(dpy, (KeyCode)ev->keycode, 0); 232 + size_t w = 0; 233 + for (int i = 0; i < r; i++){ 234 + if(keysym == (*(rpointer + i))->keys[currentkey].keysym 235 + && CLEANMASK((*(rpointer + i))->keys[currentkey].mod) == CLEANMASK(ev->state) 236 + && (*(rpointer + i))->func){ 237 + if((*(rpointer + i))->n == currentkey +1){ 238 + (*(rpointer + i))->func(&((*(rpointer + i))->arg)); 239 + ran = 1; 240 + }else{ 241 + *(wpointer + w) = *(rpointer + i); 242 + w++; 243 + } 244 + } 245 + } 246 + currentkey++; 247 + if(w == 0 || ran == 1) 248 + break; 249 + grabkeys(); 250 + /* Events arriving mid-chord must be dispatched, not dropped: 251 + * discarding a ButtonPress that activated the GrabModeSync grab 252 + * from grabbuttons() freezes pointer and keyboard permanently, 253 + * since only buttonpress()'s XAllowEvents can release it. */ 254 + while (running && !XNextEvent(dpy, &event)) { 255 + if (event.type == KeyPress) 256 + break; 257 + if (handler[event.type]) 258 + handler[event.type](&event); /* call handler */ 259 + } 260 + r = w; 261 + Keychord **holder = rpointer; 262 + rpointer = wpointer; 263 + wpointer = holder; 264 + } 265 + currentkey = 0; 266 + grabkeys(); 267 } 268 269 void 270 -- 271 2.47.3 272