[PATCH] wmaker: extend default keybinding for multikeys support and add sticky-chain mode

3 views
Skip to first unread message

david.m...@gmail.com

unread,
Mar 14, 2026, 10:09:34 AM (4 days ago) Mar 14
to Window Maker Development
This patch extends the existing keybindings to support multiple keys and
add an optional "sticky chain" mode that lets a prefix remain active until users press
a cancel key so users can enter the continuation key without re-pressing the prefix.

The idea is to bring Emacs shortcuts keybinding to wmaker.

Normal (existing and enhanced) mode:

Prefix behaves like a one-shot release before the next key if any.
For example: Mod1+h -> hide the active application, that is still working as usual.
But if you want for example to have all your window management keys under the same leader key
you can now do something like that:
"Mod4+w h" which is pressing the Super key with w, releasing them and pressing h.
You can assign that key sequence to an action.

Sticky chain mode:

Pressing a configured prefix enters a short-lived sticky state.
Sticky state expires on timeout or when explicitly canceled (with KeychainCancelKey).
For example, you can define:
"Mod4+a x" -> run xterm
"Mod4+a b f" -> run firefox
"Mod4+a b c" -> run google chrome

In sticky mode, "Mod4+a x x b f", then KeychainCancelKey or KeychainTimeoutDelay, will launch 2 xterm and firefox.

New options for WindowMaker conf file:

KeychainTimeoutDelay: timeout in milliseconds (can be set to 0)
Default: 500
Example: KeychainTimeoutDelay = 500;

KeychainCancelKey: explicit keybinding used to cancel an active sticky chain.
If set to None the feature has no dedicated cancel key and the chain only ends by timeout
or naturally if the keybind pressed is not defined.
Default: None
Example: KeychainCancelKey = Escape;
---
 src/Makefile.am   |   2 +
 src/WindowMaker.h |  14 +++
 src/defaults.c    | 216 ++++++++++++++++++++++++++++++++++++++--------
 src/defaults.h    |   2 +-
 src/event.c       | 213 ++++++++++++++++++++++++++++++++++++---------
 src/keybind.h     |  15 +++-
 src/keytree.c     | 107 +++++++++++++++++++++++
 src/keytree.h     |  97 +++++++++++++++++++++
 src/misc.c        |  87 +++++++++++++------
 src/rootmenu.c    | 198 +++++++++++++++++++++++++++++++-----------
 src/rootmenu.h    |   3 +-
 src/startup.c     |   8 ++
 src/usermenu.c    |   3 -
 src/window.c      |   5 ++
 14 files changed, 810 insertions(+), 160 deletions(-)
 create mode 100644 src/keytree.c
 create mode 100644 src/keytree.h

diff --git a/src/Makefile.am b/src/Makefile.am
index 37e4d360..8782191b 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -45,6 +45,8 @@ wmaker_SOURCES = \
  icon.c \
  icon.h \
  keybind.h \
+ keytree.c \
+ keytree.h \
  main.c \
  main.h \
  menu.c \
diff --git a/src/WindowMaker.h b/src/WindowMaker.h
index f06f8015..7255ab33 100644
--- a/src/WindowMaker.h
+++ b/src/WindowMaker.h
@@ -26,6 +26,7 @@
 #include <assert.h>
 #include <limits.h>
 #include <WINGs/WINGs.h>
+#include "keytree.h"
 
 
 /* class codes */
@@ -470,6 +471,8 @@ extern struct WPreferences {
  int clip_auto_expand_delay;         /* Delay after which the clip will expand when entered */
  int clip_auto_collapse_delay;       /* Delay after which the clip will collapse when leaved */
 
+ int keychain_timeout_delay;         /* Delay after which a keychain is reset, 0 means disabled */
+
  RImage *swtileImage;
  RImage *swbackImage[9];
 
@@ -649,6 +652,17 @@ extern struct wmaker_global_variables {
   * impact the shortcuts (typically: CapsLock, NumLock, ScrollLock)
   */
  unsigned int modifiers_mask;
+
+ /*
+  * Key-chain trie cursor.
+  *
+  * curpos == NULL  : idle, no active chain.
+  * curpos != NULL  : inside a chain; curpos points to the last matched
+  *                   internal node in wKeyTreeRoot.  The next expected
+  *                   key is one of curpos->first_child's siblings.
+  */
+ WKeyNode    *curpos;
+ WMHandlerID  chain_timeout_handler; /* non-NULL while chain timer is armed */
  } shortcut;
 } w_global;
 
diff --git a/src/defaults.c b/src/defaults.c
index 8472eac3..f8fae418 100644
--- a/src/defaults.c
+++ b/src/defaults.c
@@ -4,7 +4,7 @@
  *
  *  Copyright (c) 1997-2003 Alfredo K. Kojima
  *  Copyright (c) 1998-2003 Dan Pascu
- *  Copyright (c) 2014-2023 Window Maker Team
+ *  Copyright (c) 2014-2026 Window Maker Team
 
  *
  *  This program is free software; you can redistribute it and/or modify
@@ -64,8 +64,7 @@
 #include "properties.h"
 #include "misc.h"
 #include "winmenu.h"
-
-#define MAX_SHORTCUT_LENGTH 32
+#include "rootmenu.h"
 
 typedef struct _WDefaultEntry  WDefaultEntry;
 typedef int (WDECallbackConvert) (WScreen *scr, WDefaultEntry *entry, WMPropList *plvalue, void *addr, void **tdata);
@@ -539,6 +538,8 @@ WDefaultEntry optionList[] = {
  &wPreferences.window_list_app_icons, getBool, NULL, NULL, NULL},
  {"MouseWheelFocus", "NO", NULL,
  &wPreferences.mouse_wheel_focus, getBool, NULL, NULL, NULL},
+ {"KeychainTimeoutDelay", "500", NULL,
+     &wPreferences.keychain_timeout_delay, getInt, NULL, NULL, NULL},
 
  /* style options */
 
@@ -648,6 +649,8 @@ WDefaultEntry optionList[] = {
      NULL, getKeybind, setKeyGrab, NULL, NULL},
  {"WindowMenuKey", "Control+Escape", (void *)WKBD_WINDOWMENU,
      NULL, getKeybind, setKeyGrab, NULL, NULL},
+ {"KeychainCancelKey", "None", (void *)WKBD_KEYCHAIN_CANCEL,
+     NULL, getKeybind, setKeyGrab, NULL, NULL},
  {"DockRaiseLowerKey", "None", (void*)WKBD_DOCKRAISELOWER,
      NULL, getKeybind, setKeyGrab, NULL, NULL},
  {"ClipRaiseLowerKey", "None", (void *)WKBD_CLIPRAISELOWER,
@@ -1174,6 +1177,13 @@ void wDefaultsCheckDomains(void* arg)
  wwarning(_("could not load domain %s from user defaults database"), "WMRootMenu");
  }
  w_global.domain.root_menu->timestamp = stbuf.st_mtime;
+
+ /* Rebuild the root menu (without mapping) so that shortcuts take effect immediately. */
+ for (i = 0; i < w_global.screen_count; i++) {
+ WScreen *s = wScreenWithNumber(i);
+ if (s)
+ wRootMenuReparse(s);
+ }
  }
 #ifndef HAVE_INOTIFY
  if (!arg)
@@ -1243,7 +1253,6 @@ void wReadDefaults(WScreen * scr, WMPropList * new_dict)
 
  if (entry->update)
  needs_refresh |= (*entry->update) (scr, entry, tdata, entry->extra_data);
-
  }
  }
  }
@@ -1314,6 +1323,7 @@ void wReadKeybindings(WScreen *scr, WMPropList *dict)
 {
  WDefaultEntry *entry;
  unsigned int i;
+ Bool keybindings_changed = False;
  void *tdata;
 
  for (i = 0; i < wlengthof(optionList); i++) {
@@ -1326,11 +1336,37 @@ void wReadKeybindings(WScreen *scr, WMPropList *dict)
  plvalue = entry->plvalue;
  if (plvalue) {
  int ok = (*entry->convert)(scr, entry, plvalue, entry->addr, &tdata);
- if (ok && entry->update)
+ if (ok && entry->update) {
+ /* Check whether the (re-)computed binding differs from
+  * the one already in wKeyBindings[] */
+ long widx = (long)entry->extra_data;
+ WShortKey *nw  = (WShortKey *)tdata;
+ WShortKey *cur = &wKeyBindings[widx];
+ Bool binding_changed =
+     (cur->modifier    != nw->modifier    ||
+      cur->keycode     != nw->keycode     ||
+      cur->chain_length != nw->chain_length);
+
+ if (!binding_changed && nw->chain_length > 1 &&
+ cur->chain_modifiers && cur->chain_keycodes) {
+ int n = nw->chain_length - 1;
+
+ binding_changed =
+     (memcmp(cur->chain_modifiers, nw->chain_modifiers,
+             n * sizeof(unsigned int)) != 0 ||
+      memcmp(cur->chain_keycodes,  nw->chain_keycodes,
+             n * sizeof(KeyCode)) != 0);
+ }
  (*entry->update)(scr, entry, tdata, entry->extra_data);
+ if (binding_changed)
+ keybindings_changed = True;
  }
  }
+ }
  }
+
+ if (keybindings_changed)
+ wKeyTreeRebuild(scr);
 }
 
 void wDefaultUpdateIcons(WScreen *scr)
@@ -1359,6 +1395,57 @@ void wDefaultUpdateIcons(WScreen *scr)
  }
 }
 
+/* Rebuild the global key-binding trie from scratch */
+void wKeyTreeRebuild(WScreen *scr)
+{
+ int i;
+
+ /* Parameter not used */
+ (void)scr;
+
+ wKeyTreeDestroy(wKeyTreeRoot);
+ wKeyTreeRoot = NULL;
+
+ /* Insert all wKeyBindings[] entries */
+ for (i = 0; i < WKBD_LAST; i++) {
+ WShortKey *k = &wKeyBindings[i];
+ WKeyAction *act;
+ KeyCode *keys;
+ WKeyNode *leaf;
+ int len, j;
+ unsigned int *mods;
+
+ /* WKBD_KEYCHAIN_CANCEL is only meaningful while inside an active key chain */
+ if (i == WKBD_KEYCHAIN_CANCEL)
+ continue;
+
+ if (k->keycode == 0)
+ continue;
+
+ len  = (k->chain_length > 1) ? k->chain_length : 1;
+ mods = wmalloc(len * sizeof(unsigned int));
+ keys = wmalloc(len * sizeof(KeyCode));
+ mods[0] = k->modifier;
+ keys[0] = k->keycode;
+
+ for (j = 1; j < len; j++) {
+ mods[j] = k->chain_modifiers[j - 1];
+ keys[j] = k->chain_keycodes[j - 1];
+ }
+
+ leaf = wKeyTreeInsert(&wKeyTreeRoot, mods, keys, len);
+ wfree(mods);
+ wfree(keys);
+
+ act = wKeyNodeAddAction(leaf, WKN_WKBD);
+ if (act)
+ act->u.wkbd_idx = i;
+ }
+
+ /* Insert root-menu shortcuts */
+ wRootMenuInsertIntoTree();
+}
+
 /* --------------------------- Local ----------------------- */
 
 #define GET_STRING_OR_DEFAULT(x, var) if (!WMIsPLString(value)) { \
@@ -2210,13 +2297,51 @@ static int getColor(WScreen * scr, WDefaultEntry * entry, WMPropList * value, vo
  return True;
 }
 
+static Bool parseOneKey(WDefaultEntry *entry, const char *token,
+                        unsigned int *out_mod, KeyCode *out_code)
+{
+ char tmp[MAX_SHORTCUT_LENGTH];
+ char *b, *k;
+ KeySym ksym;
+
+ wstrlcpy(tmp, token, MAX_SHORTCUT_LENGTH);
+ b = tmp;
+
+ *out_mod = 0;
+ while ((k = strchr(b, '+')) != NULL) {
+ int mod;
+ *k = 0;
+ mod = wXModifierFromKey(b);
+ if (mod < 0) {
+ wwarning(_("%s: invalid key modifier \"%s\""), entry->key, b);
+ return False;
+ }
+ *out_mod |= mod;
+ b = k + 1;
+ }
+
+ ksym = XStringToKeysym(b);
+ if (ksym == NoSymbol) {
+ wwarning(_("%s: invalid kbd shortcut specification \"%s\""), entry->key, token);
+ return False;
+ }
+
+ *out_code = XKeysymToKeycode(dpy, ksym);
+ if (*out_code == 0) {
+ wwarning(_("%s: invalid key in shortcut \"%s\""), entry->key, token);
+ return False;
+ }
+
+ return True;
+}
+
 static int getKeybind(WScreen * scr, WDefaultEntry * entry, WMPropList * value, void *addr, void **ret)
 {
  static WShortKey shortcut;
- KeySym ksym;
  const char *val;
- char *k;
- char buf[MAX_SHORTCUT_LENGTH], *b;
+ char buf[MAX_SHORTCUT_LENGTH];
+ char *token, *saveptr;
+ int step;
 
  /* Parameter not used, but tell the compiler that it is ok */
  (void) scr;
@@ -2224,9 +2349,11 @@ static int getKeybind(WScreen * scr, WDefaultEntry * entry, WMPropList * value,
 
  GET_STRING_OR_DEFAULT("Key spec", val);
 
+ /* Free old chain arrays before overwriting */
+ wShortKeyFree(&shortcut);
+
  if (!val || strcasecmp(val, "NONE") == 0) {
- shortcut.keycode = 0;
- shortcut.modifier = 0;
+ shortcut.chain_length = 1;
  if (ret)
  *ret = &shortcut;
  return True;
@@ -2234,37 +2361,36 @@ static int getKeybind(WScreen * scr, WDefaultEntry * entry, WMPropList * value,
 
  wstrlcpy(buf, val, MAX_SHORTCUT_LENGTH);
 
- b = (char *)buf;
-
- /* get modifiers */
- shortcut.modifier = 0;
- while ((k = strchr(b, '+')) != NULL) {
- int mod;
+ /*
+  * Support both the traditional single-key syntax and the
+  * key-chain syntax where space-separated tokens represent
+  * keys that must be pressed in sequence
+  */
+ step = 0;
+ token = strtok_r(buf, " ", &saveptr);
+ while (token != NULL) {
+ unsigned int mod;
+ KeyCode kcode;
 
- *k = 0;
- mod = wXModifierFromKey(b);
- if (mod < 0) {
- wwarning(_("%s: invalid key modifier \"%s\""), entry->key, b);
+ if (!parseOneKey(entry, token, &mod, &kcode))
  return False;
- }
- shortcut.modifier |= mod;
-
- b = k + 1;
- }
 
- /* get key */
- ksym = XStringToKeysym(b);
-
- if (ksym == NoSymbol) {
- wwarning(_("%s:invalid kbd shortcut specification \"%s\""), entry->key, val);
- return False;
+ if (step == 0) {
+ shortcut.modifier = mod;
+ shortcut.keycode  = kcode;
+ } else {
+ shortcut.chain_modifiers = wrealloc(shortcut.chain_modifiers,
+                                     step * sizeof(unsigned int));
+ shortcut.chain_keycodes  = wrealloc(shortcut.chain_keycodes,
+                                     step * sizeof(KeyCode));
+ shortcut.chain_modifiers[step - 1] = mod;
+ shortcut.chain_keycodes[step - 1]  = kcode;
+ }
+ step++;
+ token = strtok_r(NULL, " ", &saveptr);
  }
 
- shortcut.keycode = XKeysymToKeycode(dpy, ksym);
- if (shortcut.keycode == 0) {
- wwarning(_("%s:invalid key in shortcut \"%s\""), entry->key, val);
- return False;
- }
+ shortcut.chain_length = (step > 1) ? step : 1;
 
  if (ret)
  *ret = &shortcut;
@@ -3267,7 +3393,25 @@ static int setKeyGrab(WScreen * scr, WDefaultEntry * entry, void *tdata, void *e
  /* Parameter not used, but tell the compiler that it is ok */
  (void) entry;
 
+ /* Free old chain arrays before overwriting */
+ wShortKeyFree(&wKeyBindings[widx]);
+
+ /* Shallow copy, then deep-copy the heap arrays */
  wKeyBindings[widx] = *shortcut;
+ if (shortcut->chain_length > 1) {
+ int n = shortcut->chain_length - 1;
+
+ wKeyBindings[widx].chain_modifiers = wmalloc(n * sizeof(unsigned int));
+ wKeyBindings[widx].chain_keycodes  = wmalloc(n * sizeof(KeyCode));
+
+ memcpy(wKeyBindings[widx].chain_modifiers, shortcut->chain_modifiers,
+        n * sizeof(unsigned int));
+ memcpy(wKeyBindings[widx].chain_keycodes,  shortcut->chain_keycodes,
+        n * sizeof(KeyCode));
+ } else {
+ wKeyBindings[widx].chain_modifiers = NULL;
+ wKeyBindings[widx].chain_keycodes  = NULL;
+ }
 
  wwin = scr->focused_window;
 
diff --git a/src/defaults.h b/src/defaults.h
index 5aaf448a..492459ba 100644
--- a/src/defaults.h
+++ b/src/defaults.h
@@ -57,5 +57,5 @@ void wDefaultChangeIcon(const char *instance, const char* class, const char *fil
 RImage *get_rimage_from_file(WScreen *scr, const char *file_name, int max_size);
 
 void wDefaultPurgeInfo(const char *instance, const char *class);
-
+void wKeyTreeRebuild(WScreen *scr); /* Rebuild the key-chain trie from the current key bindings */
 #endif /* WMDEFAULTS_H_ */
diff --git a/src/event.c b/src/event.c
index a541a962..0fcbf0e7 100644
--- a/src/event.c
+++ b/src/event.c
@@ -384,6 +384,12 @@ static void handle_inotify_events(void)
  /* move to next event in the buffer */
  i += sizeof(struct inotify_event) + pevent->len;
  }
+
+ for (i = 0; i < w_global.screen_count; i++) {
+ WScreen *scr = wScreenWithNumber(i);
+ if (scr)
+ wKeyTreeRebuild(scr);
+ }
 }
 #endif /* HAVE_INOTIFY */
 
@@ -1422,56 +1428,55 @@ static int CheckFullScreenWindowFocused(WScreen * scr)
  return 0;
 }
 
-static void handleKeyPress(XEvent * event)
-{
- WScreen *scr = wScreenForRootWindow(event->xkey.root);
- WWindow *wwin = scr->focused_window;
- short i, widx;
- int modifiers;
- int command = -1;
-#ifdef KEEP_XKB_LOCK_STATUS
- XkbStateRec staterec;
-#endif /*KEEP_XKB_LOCK_STATUS */
+/* ------------------------------------------------------------------ *
+ * Key-chain timeout support                                          *
+ *                                                                    *
+ * wPreferences.keychain_timeout_delay in milliseconds after a chain  *
+ * leader is pressed, the chain is automatically cancelled so the     *
+ * user is not stuck in a half-entered sequence. Set to 0 to disable. *
+ * ------------------------------------------------------------------ */
 
- /* ignore CapsLock */
- modifiers = event->xkey.state & w_global.shortcut.modifiers_mask;
+/* Cancels the chain on inactivity */
+static void chainTimeoutCallback(void *data)
+{
+ (void)data;
 
- for (i = 0; i < WKBD_LAST; i++) {
- if (wKeyBindings[i].keycode == 0)
- continue;
+ XUngrabKeyboard(dpy, CurrentTime);
+ w_global.shortcut.curpos = NULL;
+ w_global.shortcut.chain_timeout_handler = NULL;
+}
 
- if (wKeyBindings[i].keycode == event->xkey.keycode && ( /*wKeyBindings[i].modifier==0
-    || */ wKeyBindings[i].modifier ==
-       modifiers)) {
- command = i;
- break;
- }
+/* Start (or restart) the chain inactivity timer */
+static void wStartChainTimer(void)
+{
+ if (wPreferences.keychain_timeout_delay > 0) {
+ if (w_global.shortcut.chain_timeout_handler)
+ WMDeleteTimerHandler(w_global.shortcut.chain_timeout_handler);
+ w_global.shortcut.chain_timeout_handler =
+ WMAddTimerHandler(wPreferences.keychain_timeout_delay, chainTimeoutCallback, NULL);
  }
+}
 
- if (command < 0) {
-
- if (!wRootMenuPerformShortcut(event)) {
- static int dontLoop = 0;
-
- if (dontLoop > 10) {
- wwarning("problem with key event processing code");
- return;
- }
- dontLoop++;
- /* if the focused window is an internal window, try redispatching
-  * the event to the managed window, as it can be a WINGs window */
- if (wwin && wwin->flags.internal_window && wwin->client_leader != None) {
- /* client_leader contains the WINGs toplevel */
- event->xany.window = wwin->client_leader;
- WMHandleEvent(event);
- }
- dontLoop--;
- }
- return;
+/* Cancel the chain inactivity timer, if armed */
+static void wCancelChainTimer(void)
+{
+ if (w_global.shortcut.chain_timeout_handler) {
+ WMDeleteTimerHandler(w_global.shortcut.chain_timeout_handler);
+ w_global.shortcut.chain_timeout_handler = NULL;
  }
+}
+
 #define ISMAPPED(w) ((w) && !(w)->flags.miniaturized && ((w)->flags.mapped || (w)->flags.shaded))
 #define ISFOCUSED(w) ((w) && (w)->flags.focused)
 
+static void dispatchWKBDCommand(int command, WScreen *scr, WWindow *wwin, XEvent *event)
+{
+ short widx;
+ int i;
+#ifdef KEEP_XKB_LOCK_STATUS
+ XkbStateRec staterec;
+#endif /*KEEP_XKB_LOCK_STATUS */
+
  switch (command) {
 
  case WKBD_ROOTMENU:
@@ -1970,6 +1975,132 @@ static void handleKeyPress(XEvent * event)
  }
 }
 
+static void handleKeyPress(XEvent * event)
+{
+ WScreen *scr = wScreenForRootWindow(event->xkey.root);
+ WWindow *wwin = scr->focused_window;
+ WKeyNode  *siblings;
+ WKeyNode  *match;
+ WKeyAction *act;
+ int modifiers;
+
+ /* ignore CapsLock */
+ modifiers = event->xkey.state & w_global.shortcut.modifiers_mask;
+
+ /* ------------------------------------------------------------------ *
+  * Trie-based key-chain matching                                      *
+  *                                                                    *
+  * wKeyTreeRoot is a prefix trie covering ALL key bindings            *
+  * (wKeyBindings and root-menu shortcuts combined).                   *
+  * curpos tracks the last matched internal node.                      *
+  * NULL means we are at the root (idle).                              *
+  * ------------------------------------------------------------------ */
+
+ if (w_global.shortcut.curpos != NULL) {
+ /*  Inside a chain: look for the next key among children */
+ if (event->xkey.keycode == wKeyBindings[WKBD_KEYCHAIN_CANCEL].keycode &&
+ modifiers == wKeyBindings[WKBD_KEYCHAIN_CANCEL].modifier) {
+ wCancelChainTimer();
+ XUngrabKeyboard(dpy, CurrentTime);
+ w_global.shortcut.curpos = NULL;
+ return;
+ }
+
+ siblings = w_global.shortcut.curpos->first_child;
+ match = wKeyTreeFind(siblings, modifiers, event->xkey.keycode);
+
+ if (match != NULL && match->first_child != NULL) {
+ /* Internal node: advance and keep waiting */
+ w_global.shortcut.curpos = match;
+ wStartChainTimer();
+ return;
+ }
+
+ if (match == NULL) {
+ /* Unrecognized key inside chain: exit chain mode */
+ wCancelChainTimer();
+ XUngrabKeyboard(dpy, CurrentTime);
+ w_global.shortcut.curpos = NULL;
+ return;
+ }
+
+ /*
+  * Sticky-chain mode: when a KeychainCancelKey is configured,
+  * stay at the parent level after executing a leaf instead of always
+  * returning to root.
+  */
+ if (wKeyBindings[WKBD_KEYCHAIN_CANCEL].keycode != 0) {
+ WKeyNode *parent = match->parent;
+ WKeyNode *child;
+ int nchildren = 0;
+
+ for (child = parent->first_child; child != NULL; child = child->next_sibling)
+ nchildren++;
+
+ if (nchildren > 1) {
+ /* Multi-branch parent: stay in chain mode at this level */
+ w_global.shortcut.curpos = parent;
+ wStartChainTimer();
+ } else {
+ /* Single-branch parent: nothing left to wait for, exit chain */
+ wCancelChainTimer();
+ XUngrabKeyboard(dpy, CurrentTime);
+ w_global.shortcut.curpos = NULL;
+ }
+ } else {
+ /* No cancel key configured: always exit chain after a leaf */
+ wCancelChainTimer();
+ XUngrabKeyboard(dpy, CurrentTime);
+ w_global.shortcut.curpos = NULL;
+ }
+ } else {
+ /* Idle: look for a root-level match */
+ match = wKeyTreeFind(wKeyTreeRoot, modifiers, event->xkey.keycode);
+
+ if (match == NULL) {
+ /* Not a known shortcut: try to redispatch it */
+ static int dontLoop = 0;
+
+ if (dontLoop > 10) {
+ wwarning("problem with key event processing code");
+ return;
+ }
+ dontLoop++;
+ if (wwin && wwin->flags.internal_window &&
+ wwin->client_leader != None) {
+ event->xany.window = wwin->client_leader;
+ WMHandleEvent(event);
+ }
+ dontLoop--;
+
+ return;
+ }
+
+ if (match->first_child != NULL) {
+ /* Internal node: enter chain mode */
+ w_global.shortcut.curpos = match;
+ XGrabKeyboard(dpy, scr->root_win, False,
+ GrabModeAsync, GrabModeAsync, CurrentTime);
+ wStartChainTimer();
+
+ return;
+ }
+ }
+
+ /* Execute all leaf actions for this key sequence */
+ for (act = match->actions; act != NULL; act = act->next) {
+ if (act->type == WKN_MENU) {
+ WMenu *menu  = (WMenu *) act->u.menu.menu;
+ WMenuEntry *entry = (WMenuEntry *) act->u.menu.entry;
+
+ (*entry->callback)(menu, entry);
+ } else {
+ dispatchWKBDCommand(act->u.wkbd_idx, scr, wwin, event);
+ }
+ }
+ return;
+}
+
 #define CORNER_NONE 0
 #define CORNER_TOPLEFT 1
 #define CORNER_TOPRIGHT 2
diff --git a/src/keybind.h b/src/keybind.h
index a9478574..292cbb71 100644
--- a/src/keybind.h
+++ b/src/keybind.h
@@ -2,7 +2,7 @@
  *  Window Maker window manager
  *
  *  Copyright (c) 1997-2003 Alfredo K. Kojima
- *  Copyright (c) 2014-2023 Window Maker Team
+ *  Copyright (c) 2014-2026 Window Maker Team
  *
  *  This program is free software; you can redistribute it and/or modify
  *  it under the terms of the GNU General Public License as published by
@@ -22,6 +22,8 @@
 #ifndef WMKEYBIND_H
 #define WMKEYBIND_H
 
+#define MAX_SHORTCUT_LENGTH 64
+
 /* <X11/X.h> doesn't define these, even though XFree supports them */
 #ifndef Button6
 #define Button6 6
@@ -44,6 +46,7 @@ enum {
  WKBD_ROOTMENU,
  WKBD_WINDOWMENU,
  WKBD_WINDOWLIST,
+ WKBD_KEYCHAIN_CANCEL,
 
  /* window */
  WKBD_MINIATURIZE,
@@ -166,8 +169,13 @@ enum {
 };
 
 typedef struct WShortKey {
-    unsigned int modifier;
-    KeyCode keycode;
+    unsigned int modifier;   /* leader (or only) key modifier - always valid */
+    KeyCode keycode;    /* leader (or only) key code - always valid */
+
+ /* Key-chain support */
+    int chain_length;
+    unsigned int *chain_modifiers;   /* heap-allocated, NULL for single keys */
+    KeyCode *chain_keycodes;    /* heap-allocated, NULL for single keys */
 } WShortKey;
 
 /* ---[ Global Variables ]------------------------------------------------ */
@@ -177,5 +185,6 @@ extern WShortKey wKeyBindings[WKBD_LAST];
 /* ---[ Functions ]------------------------------------------------------- */
 
 void wKeyboardInitialize(void);
+void wShortKeyFree(WShortKey *key);
 
 #endif /* WMKEYBIND_H */
diff --git a/src/keytree.c b/src/keytree.c
new file mode 100644
index 00000000..c440e5a8
--- /dev/null
+++ b/src/keytree.c
@@ -0,0 +1,107 @@
+/* keytree.c - Trie (prefix tree) for key-chain bindings
+ *
+ *  Window Maker window manager
+ *
+ *  Copyright (c) 2026 Window Maker Team
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License along
+ *  with this program; if not, write to the Free Software Foundation, Inc.,
+ *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "wconfig.h"
+#include <string.h>
+#include <WINGs/WUtil.h>
+#include "keytree.h"
+
+/* Global trie root */
+WKeyNode *wKeyTreeRoot = NULL;
+
+WKeyNode *wKeyTreeFind(WKeyNode *siblings, unsigned int mod, KeyCode key)
+{
+ WKeyNode *p = siblings;
+
+ while (p != NULL) {
+ if (p->modifier == mod && p->keycode == key)
+ return p;
+ p = p->next_sibling;
+ }
+ return NULL;
+}
+
+WKeyNode *wKeyTreeInsert(WKeyNode **root, unsigned int *mods, KeyCode *keys, int nkeys)
+{
+ WKeyNode **slot = root;
+ WKeyNode *parent = NULL;
+ int i;
+
+ if (nkeys <= 0)
+ return NULL;
+
+ for (i = 0; i < nkeys; i++) {
+ WKeyNode *node = wKeyTreeFind(*slot, mods[i], keys[i]);
+
+ if (node == NULL) {
+ node = wmalloc(sizeof(WKeyNode));
+ memset(node, 0, sizeof(WKeyNode));
+ node->modifier = mods[i];
+ node->keycode = keys[i];
+ node->parent = parent;
+ node->next_sibling = *slot;
+ *slot = node;
+ }
+
+ parent = node;
+ slot = &node->first_child;
+ }
+
+ return parent;   /* leaf */
+}
+
+
+void wKeyTreeDestroy(WKeyNode *node)
+{
+ /* Iterates siblings at each level, recurses only into children */
+ while (node != NULL) {
+ WKeyNode *next = node->next_sibling;
+ WKeyAction *act, *next_act;
+
+ wKeyTreeDestroy(node->first_child);
+ for (act = node->actions; act != NULL; act = next_act) {
+ next_act = act->next;
+ wfree(act);
+ }
+ wfree(node);
+ node = next;
+ }
+}
+
+WKeyAction *wKeyNodeAddAction(WKeyNode *leaf, WKeyActionType type)
+{
+ WKeyAction *act = wmalloc(sizeof(WKeyAction));
+ WKeyAction *p;
+
+ memset(act, 0, sizeof(WKeyAction));
+ act->type = type;
+
+ /* Append to end of list to preserve insertion order */
+ if (leaf->actions == NULL) {
+ leaf->actions = act;
+ } else {
+ p = leaf->actions;
+ while (p->next)
+ p = p->next;
+ p->next = act;
+ }
+ return act;
+}
\ No newline at end of file
diff --git a/src/keytree.h b/src/keytree.h
new file mode 100644
index 00000000..fcf223ae
--- /dev/null
+++ b/src/keytree.h
@@ -0,0 +1,97 @@
+/* keytree.h - Trie (prefix tree) for key-chain bindings
+ *
+ *  Window Maker window manager
+ *
+ *  Copyright (c) 2026 Window Maker Team
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License along
+ *  with this program; if not, write to the Free Software Foundation, Inc.,
+ *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef WMKEYTREE_H
+#define WMKEYTREE_H
+
+#include <X11/Xlib.h>
+
+/*
+ * Each key in a binding sequence occupies one node in the trie.
+ * Internal nodes (first_child != NULL) represent a prefix that has been
+ * typed so far, leaf nodes carry the action payload.
+ */
+
+typedef enum {
+ WKN_WKBD,   /* action: wKeyBindings command (WKBD_* index) */
+ WKN_MENU    /* action: root-menu entry callback */
+} WKeyActionType;
+
+/*
+ * A single action attached to a trie leaf node. Multiple actions may share
+ * the same key sequence and are chained through the 'next' pointer, allowing
+ * one key press to trigger several commands simultaneously.
+ */
+typedef struct WKeyAction {
+ WKeyActionType type;
+ union {
+ int wkbd_idx;   /* WKN_WKBD: WKBD_* enum value */
+ struct {
+ void *menu;   /* WKN_MENU: cast to WMenu */
+ void *entry;  /* WKN_MENU: cast to WMenuEntry */
+ } menu;
+ } u;
+ struct WKeyAction *next;  /* next action for this key sequence, or NULL */
+} WKeyAction;
+
+typedef struct WKeyNode {
+ unsigned int  modifier;
+ KeyCode keycode;
+
+ WKeyAction *actions;   /* non-NULL only for leaf nodes (first_child == NULL) */
+
+ struct WKeyNode *parent;
+ struct WKeyNode *first_child;   /* first key of next step in chain */
+ struct WKeyNode *next_sibling;  /* alternative binding at same depth */
+} WKeyNode;
+
+/* Global trie root */
+extern WKeyNode *wKeyTreeRoot;
+
+/*
+ * Insert a key sequence into *root.
+ *   mods[0]/keys[0] - root (leader) key
+ *   mods[1..n-1]/keys[1..n-1] - follower keys
+ * Shared prefixes are merged automatically.
+ * Returns the leaf node (caller must set its type/payload).
+ * Returns NULL if nkeys <= 0.
+ */
+WKeyNode *wKeyTreeInsert(WKeyNode **root, unsigned int *mods, KeyCode *keys, int nkeys);
+
+/*
+ * Find the first sibling in the list starting at 'siblings' that matches
+ * (mod, key). Returns NULL if not found.
+ */
+WKeyNode *wKeyTreeFind(WKeyNode *siblings, unsigned int mod, KeyCode key);
+
+/*
+ * Recursively free the entire subtree rooted at 'node'.
+ */
+void wKeyTreeDestroy(WKeyNode *node);
+
+/*
+ * Allocate a new WKeyAction of the given type, append it to leaf->actions,
+ * and return it for the caller to set the payload (wkbd_idx or menu.*).
+ * Multiple calls with the same leaf accumulate actions in insertion order.
+ */
+WKeyAction *wKeyNodeAddAction(WKeyNode *leaf, WKeyActionType type);
+
+#endif /* WMKEYTREE_H */
diff --git a/src/misc.c b/src/misc.c
index 007f71c9..38d6238c 100644
--- a/src/misc.c
+++ b/src/misc.c
@@ -881,46 +881,85 @@ char *GetShortcutString(const char *shortcut)
 
 char *GetShortcutKey(WShortKey key)
 {
- const char *key_name;
- char buffer[256];
- char *wr;
+ char buf[MAX_SHORTCUT_LENGTH];
+ char *wr, *result, *seg;
+ int step;
 
  void append_string(const char *text)
  {
- const char *string = text;
+ const char *s = text;
 
- while (*string) {
- if (wr >= buffer + sizeof(buffer) - 1)
+ while (*s) {
+ if (wr >= buf + sizeof(buf) - 1)
  break;
- *wr++ = *string++;
+ *wr++ = *s++;
  }
  }
 
  void append_modifier(int modifier_index, const char *fallback_name)
  {
- if (wPreferences.modifier_labels[modifier_index]) {
+ if (wPreferences.modifier_labels[modifier_index])
  append_string(wPreferences.modifier_labels[modifier_index]);
- } else {
+ else
  append_string(fallback_name);
- }
  }
 
- key_name = XKeysymToString(W_KeycodeToKeysym(dpy, key.keycode, 0));
- if (!key_name)
+ Bool build_token(unsigned int mod, KeyCode kcode)
+ {
+ const char *kname = XKeysymToString(W_KeycodeToKeysym(dpy, kcode, 0));
+
+ if (!kname)
+ return False;
+
+ wr = buf;
+ if (mod & ControlMask) append_modifier(1, "Control+");
+ if (mod & ShiftMask)   append_modifier(0, "Shift+");
+ if (mod & Mod1Mask)    append_modifier(2, "Mod1+");
+ if (mod & Mod2Mask)    append_modifier(3, "Mod2+");
+ if (mod & Mod3Mask)    append_modifier(4, "Mod3+");
+ if (mod & Mod4Mask)    append_modifier(5, "Mod4+");
+ if (mod & Mod5Mask)    append_modifier(6, "Mod5+");
+ append_string(kname);
+ *wr = '\0';
+
+ return True;
+ }
+
+ if (!build_token(key.modifier, key.keycode))
  return NULL;
 
- wr = buffer;
- if (key.modifier & ControlMask) append_modifier(1, "Control+");
- if (key.modifier & ShiftMask)   append_modifier(0, "Shift+");
- if (key.modifier & Mod1Mask)    append_modifier(2, "Mod1+");
- if (key.modifier & Mod2Mask)    append_modifier(3, "Mod2+");
- if (key.modifier & Mod3Mask)    append_modifier(4, "Mod3+");
- if (key.modifier & Mod4Mask)    append_modifier(5, "Mod4+");
- if (key.modifier & Mod5Mask)    append_modifier(6, "Mod5+");
- append_string(key_name);
- *wr = '\0';
-
- return GetShortcutString(buffer);
+ /* Convert the leader token to its display string */
+ result = GetShortcutString(buf);
+
+ /* Append each chain follower separated by a space */
+ for (step = 0; step < key.chain_length - 1; step++) {
+ char *combined;
+
+ if (key.chain_keycodes[step] == 0)
+ break;
+
+ if (!build_token(key.chain_modifiers[step], key.chain_keycodes[step]))
+ break;
+
+ seg = GetShortcutString(buf);
+ combined = wstrconcat(result, " ");
+ wfree(result);
+ result = wstrconcat(combined, seg);
+ wfree(combined);
+ wfree(seg);
+ }
+
+ return result;
+}
+
+void wShortKeyFree(WShortKey *key)
+{
+ if (!key)
+ return;
+
+ wfree(key->chain_modifiers);
+ wfree(key->chain_keycodes);
+ memset(key, 0, sizeof(*key));
 }
 
 char *EscapeWM_CLASS(const char *name, const char *class)
diff --git a/src/rootmenu.c b/src/rootmenu.c
index 213c30e8..fb42c30f 100644
--- a/src/rootmenu.c
+++ b/src/rootmenu.c
@@ -60,8 +60,6 @@
 
 #include <WINGs/WUtil.h>
 
-#define MAX_SHORTCUT_LENGTH 32
-
 static WMenu *readMenuPipe(WScreen * scr, char **file_name);
 static WMenu *readPLMenuPipe(WScreen * scr, char **file_name);
 static WMenu *readMenuFile(WScreen *scr, const char *file_name);
@@ -75,6 +73,11 @@ typedef struct Shortcut {
  KeyCode keycode;
  WMenuEntry *entry;
  WMenu *menu;
+
+ /* Key-chain support */
+ int chain_length;
+ unsigned int *chain_modifiers;   /* heap-allocated, NULL for single keys */
+ KeyCode *chain_keycodes;    /* heap-allocated, NULL for single keys */
 } Shortcut;
 
 static Shortcut *shortcutList = NULL;
@@ -320,27 +323,44 @@ static char *getLocalizedMenuFile(const char *menu)
  return NULL;
 }
 
-Bool wRootMenuPerformShortcut(XEvent * event)
+/*
+ * Insert all root-menu shortcuts into the
+ * global key-binding trie (wKeyTreeRoot)
+ */
+void wRootMenuInsertIntoTree(void)
 {
- WScreen *scr = wScreenForRootWindow(event->xkey.root);
  Shortcut *ptr;
- int modifiers;
- int done = 0;
-
- /* ignore CapsLock */
- modifiers = event->xkey.state & w_global.shortcut.modifiers_mask;
 
  for (ptr = shortcutList; ptr != NULL; ptr = ptr->next) {
- if (ptr->keycode == 0 || ptr->menu->menu->screen_ptr != scr)
+ unsigned int *mods;
+ KeyCode *keys;
+ int len, j;
+ WKeyNode *leaf;
+
+ if (ptr->keycode == 0)
  continue;
 
- if (ptr->keycode == event->xkey.keycode && ptr->modifier == modifiers) {
- (*ptr->entry->callback) (ptr->menu, ptr->entry);
- done = True;
+ len  = (ptr->chain_length > 1) ? ptr->chain_length : 1;
+ mods = wmalloc(len * sizeof(unsigned int));
+ keys = wmalloc(len * sizeof(KeyCode));
+ mods[0] = ptr->modifier;
+ keys[0] = ptr->keycode;
+
+ for (j = 1; j < len; j++) {
+ mods[j] = ptr->chain_modifiers[j - 1];
+ keys[j] = ptr->chain_keycodes[j - 1];
  }
- }
 
- return done;
+ leaf = wKeyTreeInsert(&wKeyTreeRoot, mods, keys, len);
+ wfree(mods);
+ wfree(keys);
+
+ if (leaf) {
+ WKeyAction *act = wKeyNodeAddAction(leaf, WKN_MENU);
+ act->u.menu.menu  = ptr->menu;
+ act->u.menu.entry = ptr->entry;
+ }
+ }
 }
 
 void wRootMenuBindShortcuts(Window window)
@@ -377,6 +397,16 @@ static void rebindKeygrabs(WScreen * scr)
  }
 }
 
+static void freeShortcut(Shortcut *s)
+{
+ if (!s)
+ return;
+
+ wfree(s->chain_modifiers);
+ wfree(s->chain_keycodes);
+ wfree(s);
+}
+
 static void removeShortcutsForMenu(WMenu * menu)
 {
  Shortcut *ptr, *tmp;
@@ -386,7 +416,7 @@ static void removeShortcutsForMenu(WMenu * menu)
  while (ptr != NULL) {
  tmp = ptr->next;
  if (ptr->menu == menu) {
- wfree(ptr);
+ freeShortcut(ptr);
  } else {
  ptr->next = newList;
  newList = ptr;
@@ -400,51 +430,76 @@ static void removeShortcutsForMenu(WMenu * menu)
 static Bool addShortcut(const char *file, const char *shortcutDefinition, WMenu *menu, WMenuEntry *entry)
 {
  Shortcut *ptr;
- KeySym ksym;
- char *k;
- char buf[MAX_SHORTCUT_LENGTH], *b;
+ char buf[MAX_SHORTCUT_LENGTH];
+ char *token, *saveptr;
+ int step;
 
  ptr = wmalloc(sizeof(Shortcut));
-
  wstrlcpy(buf, shortcutDefinition, MAX_SHORTCUT_LENGTH);
- b = (char *)buf;
-
- /* get modifiers */
- ptr->modifier = 0;
- while ((k = strchr(b, '+')) != NULL) {
- int mod;
-
- *k = 0;
- mod = wXModifierFromKey(b);
- if (mod < 0) {
- wwarning(_("%s: invalid key modifier \"%s\""), file, b);
- wfree(ptr);
- return False;
+
+ /*
+  * Parse space-separated tokens.
+  * The first token is the leader key, subsequent tokens are chain steps
+  */
+ step = 0;
+ token = strtok_r(buf, " ", &saveptr);
+ while (token != NULL) {
+ KeySym ksym;
+ KeyCode kcode;
+ unsigned int mod = 0;
+ char tmp[MAX_SHORTCUT_LENGTH];
+ char *b, *k;
+
+ wstrlcpy(tmp, token, MAX_SHORTCUT_LENGTH);
+ b = tmp;
+
+ while ((k = strchr(b, '+')) != NULL) {
+ int m;
+ *k = 0;
+ m = wXModifierFromKey(b);
+ if (m < 0) {
+ wwarning(_("%s: invalid key modifier \"%s\""), file, b);
+ freeShortcut(ptr);
+ return False;
+ }
+ mod |= m;
+ b = k + 1;
  }
- ptr->modifier |= mod;
 
- b = k + 1;
- }
+ ksym = XStringToKeysym(b);
+ if (ksym == NoSymbol) {
+ wwarning(_("%s: invalid kbd shortcut specification \"%s\" for entry %s"),
+  file, shortcutDefinition, entry->text);
+ freeShortcut(ptr);
+ return False;
+ }
 
- /* get key */
- ksym = XStringToKeysym(b);
+ kcode = XKeysymToKeycode(dpy, ksym);
+ if (kcode == 0) {
+ wwarning(_("%s: invalid key in shortcut \"%s\" for entry %s"),
+  file, shortcutDefinition, entry->text);
+ freeShortcut(ptr);
+ return False;
+ }
 
- if (ksym == NoSymbol) {
- wwarning(_("%s:invalid kbd shortcut specification \"%s\" for entry %s"),
-  file, shortcutDefinition, entry->text);
- wfree(ptr);
- return False;
- }
+ if (step == 0) {
+ ptr->modifier = mod;
+ ptr->keycode  = kcode;
+ } else {
+ ptr->chain_modifiers = wrealloc(ptr->chain_modifiers,
+                                 step * sizeof(unsigned int));
+ ptr->chain_keycodes  = wrealloc(ptr->chain_keycodes,
+                                 step * sizeof(KeyCode));
+ ptr->chain_modifiers[step - 1] = mod;
+ ptr->chain_keycodes[step - 1]  = kcode;
+ }
 
- ptr->keycode = XKeysymToKeycode(dpy, ksym);
- if (ptr->keycode == 0) {
- wwarning(_("%s:invalid key in shortcut \"%s\" for entry %s"), file,
-  shortcutDefinition, entry->text);
- wfree(ptr);
- return False;
+ step++;
+ token = strtok_r(NULL, " ", &saveptr);
  }
 
- ptr->menu = menu;
+ ptr->chain_length = (step > 1) ? step : 1;
+ ptr->menu  = menu;
  ptr->entry = entry;
 
  ptr->next = shortcutList;
@@ -1563,6 +1618,47 @@ WMenu *configureMenu(WScreen *scr, WMPropList *definition)
  return menu;
 }
 
+/*
+ *----------------------------------------------------------------------
+ * wRootMenuReparse--
+ * Rebuild the root menu (and its shortcuts / key-grabs) from the
+ * current WMRootMenu dictionary without mapping the menu.
+ *----------------------------------------------------------------------
+ */
+void wRootMenuReparse(WScreen *scr)
+{
+ WMenu *menu = NULL;
+ WMPropList *definition;
+
+ definition = w_global.domain.root_menu->dictionary;
+ if (!definition)
+ return;
+
+ scr->flags.root_menu_changed_shortcuts = 0;
+ scr->flags.added_workspace_menu = 0;
+ scr->flags.added_windows_menu = 0;
+
+ if (WMIsPLArray(definition)) {
+ if (!scr->root_menu ||
+     w_global.domain.root_menu->timestamp > scr->root_menu->timestamp) {
+ menu = configureMenu(scr, definition);
+ if (menu)
+ menu->timestamp = w_global.domain.root_menu->timestamp;
+ }
+ } else {
+ menu = configureMenu(scr, definition);
+ }
+
+ if (menu) {
+ if (scr->root_menu)
+ wMenuDestroy(scr->root_menu, True);
+ scr->root_menu = menu;
+ }
+
+ if (scr->flags.root_menu_changed_shortcuts)
+ rebindKeygrabs(scr);
+}
+
 /*
  *----------------------------------------------------------------------
  * OpenRootMenu--
diff --git a/src/rootmenu.h b/src/rootmenu.h
index 44475b09..69d0aa70 100644
--- a/src/rootmenu.h
+++ b/src/rootmenu.h
@@ -22,7 +22,8 @@
 #ifndef WMROOTMENU_H
 #define WMROOTMENU_H
 
-Bool wRootMenuPerformShortcut(XEvent * event);
+void wRootMenuInsertIntoTree(void);
+void wRootMenuReparse(WScreen *scr);
 void wRootMenuBindShortcuts(Window window);
 void OpenRootMenu(WScreen * scr, int x, int y, int keyboard);
 WMenu *configureMenu(WScreen *scr, WMPropList *definition);
diff --git a/src/startup.c b/src/startup.c
index f74df8f2..7b86746b 100644
--- a/src/startup.c
+++ b/src/startup.c
@@ -425,6 +425,10 @@ void StartUp(Bool defaultScreenOnly)
   */
  w_global.shortcut.modifiers_mask &= ~(_NumLockMask | _ScrollLockMask);
 
+ /* No active key chain at startup */
+ w_global.shortcut.curpos = NULL;
+ w_global.shortcut.chain_timeout_handler = NULL;
+
  memset(&wKeyBindings, 0, sizeof(wKeyBindings));
 
  w_global.context.client_win = XUniqueContext();
@@ -704,6 +708,10 @@ void StartUp(Bool defaultScreenOnly)
  wSessionRestoreLastWorkspace(wScreen[j]);
  }
 
+ for (j = 0; j < w_global.screen_count; j++) {
+ wKeyTreeRebuild(wScreen[j]);
+ }
+
  if (w_global.screen_count == 0) {
  wfatal(_("could not manage any screen"));
  Exit(1);
diff --git a/src/usermenu.c b/src/usermenu.c
index 7bf6ed19..c5542884 100644
--- a/src/usermenu.c
+++ b/src/usermenu.c
@@ -78,9 +78,6 @@
 
 #include "framewin.h"
 
-#define MAX_SHORTCUT_LENGTH 32
-
-
 typedef struct {
  WScreen *screen;
  WShortKey *key;
diff --git a/src/window.c b/src/window.c
index 1f688cdc..71d14a63 100644
--- a/src/window.c
+++ b/src/window.c
@@ -2639,6 +2639,11 @@ void wWindowSetKeyGrabs(WWindow * wwin)
 
  if (key->keycode == 0)
  continue;
+
+ /* WKBD_KEYCHAIN_CANCEL is only meaningful while inside an active key chain */
+ if (i == WKBD_KEYCHAIN_CANCEL)
+ continue;
+
  if (key->modifier != AnyModifier) {
  XGrabKey(dpy, key->keycode, key->modifier | LockMask,
   wwin->frame->core->window, True, GrabModeAsync, GrabModeAsync);
--
2.43.0

0001-wmaker-extend-default-keybinding-for-multikeys-suppo.patch
Reply all
Reply to author
Forward
0 new messages