[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
Reply to author
Sign in to reply to author
Forward
Sign in to forward
Delete
You do not have permission to delete messages in this group
Copy link
Report message
Show original message
Either email addresses are anonymous for this group or you need the view member email addresses permission to view the original message
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
/* 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"
/* 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;
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; +}
-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. * + * ------------------------------------------------------------------ */
#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)
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