sites

public wiki contents of suckless.org
git clone git://git.suckless.org/sites
Log | Files | Refs

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