[PATCH 1/2] wmaker: dynamic vim-like window marking feature

24 views
Skip to first unread message

david.m...@gmail.com

unread,
Apr 1, 2026, 8:26:56 PM (5 days ago) Apr 1
to Window Maker Development
This patch is adding vim-like window marking, like in i3.
A window can be dynamically assigned a mark label.
Then a marked window can be pulled or jumped to.
Or the current focused window can be swapped with a marked window.

The mark label appears in the Window List in between the window
title and the workspace name.

Those new options in WindowMaker conf file are used to control
the actions: MarkSetKey, MarkUnsetKey, MarkBringKey, MarkJumpKey
and MarkSwapKey.

Those actions are set to None by default.
---
 WPrefs.app/KeyboardShortcuts.c |   7 ++
 src/WindowMaker.h              |   1 +
 src/defaults.c                 |  11 +++
 src/event.c                    | 141 +++++++++++++++++++++++++++++++++
 src/keybind.h                  |   5 ++
 src/misc.c                     |  78 ++++++++----------
 src/misc.h                     |   1 +
 src/screen.h                   |  10 +++
 src/session.c                  |  20 +++++
 src/startup.c                  |   2 +
 src/switchmenu.c               |  46 ++++++-----
 src/window.c                   |  60 +++++++++++++-
 src/window.h                   |   6 ++
 13 files changed, 326 insertions(+), 62 deletions(-)

diff --git a/WPrefs.app/KeyboardShortcuts.c b/WPrefs.app/KeyboardShortcuts.c
index 7d18a90a..f6750c1b 100644
--- a/WPrefs.app/KeyboardShortcuts.c
+++ b/WPrefs.app/KeyboardShortcuts.c
@@ -106,6 +106,13 @@ static struct keyOption {
  { "GroupNextKey",   N_("Focus next group window") },
  { "GroupPrevKey",   N_("Focus previous group window") },
 
+ /* Vim-like Window Marking */
+ { "MarkSetKey",   N_("Mark window: set mark") },
+ { "MarkUnsetKey", N_("Mark window: unset mark") },
+ { "MarkBringKey", N_("Mark window: bring marked window here") },
+ { "MarkJumpKey",  N_("Mark window: jump to marked window") },
+ { "MarkSwapKey",  N_("Mark window: swap with marked window") },
+
  /* Workspace Related */
  { "WorkspaceMapKey",  N_("Open workspace pager") },
  { "NextWorkspaceKey", N_("Switch to next workspace") },
diff --git a/src/WindowMaker.h b/src/WindowMaker.h
index f979573f..da727b37 100644
--- a/src/WindowMaker.h
+++ b/src/WindowMaker.h
@@ -609,6 +609,7 @@ extern struct wmaker_global_variables {
 
  Atom icon_size;
  Atom icon_tile;
+ Atom mark_key;
  } wmaker;
 
  } atom;
diff --git a/src/defaults.c b/src/defaults.c
index 101e4dc1..0a9da900 100644
--- a/src/defaults.c
+++ b/src/defaults.c
@@ -823,6 +823,17 @@ WDefaultEntry optionList[] = {
      NULL, getKeybind, setKeyGrab, NULL, NULL},
  {"PartialCaptureKey", "None", (void *)WKBD_PRINTP,
      NULL, getKeybind, setKeyGrab, NULL, NULL},
+ /* Vim-like Window Marking */
+ {"MarkSetKey", "None", (void *)WKBD_MARK_SET,
+ NULL, getKeybind, setKeyGrab, NULL, NULL},
+ {"MarkUnsetKey", "None", (void *)WKBD_MARK_UNSET,
+ NULL, getKeybind, setKeyGrab, NULL, NULL},
+ {"MarkBringKey", "None", (void *)WKBD_MARK_BRING,
+ NULL, getKeybind, setKeyGrab, NULL, NULL},
+ {"MarkJumpKey", "None", (void *)WKBD_MARK_JUMP,
+ NULL, getKeybind, setKeyGrab, NULL, NULL},
+ {"MarkSwapKey", "None", (void *)WKBD_MARK_SWAP,
+ NULL, getKeybind, setKeyGrab, NULL, NULL},
 
 #ifdef KEEP_XKB_LOCK_STATUS
  {"ToggleKbdModeKey", "None", (void *)WKBD_TOGGLE,
diff --git a/src/event.c b/src/event.c
index e5422ea4..61975315 100644
--- a/src/event.c
+++ b/src/event.c
@@ -1471,6 +1471,12 @@ static void wCancelChainTimer(void)
 #define ISMAPPED(w) ((w) && !(w)->flags.miniaturized && ((w)->flags.mapped || (w)->flags.shaded))
 #define ISFOCUSED(w) ((w) && (w)->flags.focused)
 
+static void startMarkCapture(WScreen *scr, Display *dpy, WMarkCaptureMode mode)
+{
+ scr->flags.mark_capture_mode = mode;
+ XGrabKeyboard(dpy, scr->root_win, False, GrabModeAsync, GrabModeAsync, CurrentTime);
+}
+
 static void dispatchWKBDCommand(int command, WScreen *scr, WWindow *wwin, XEvent *event)
 {
  short widx;
@@ -1975,6 +1981,29 @@ static void dispatchWKBDCommand(int command, WScreen *scr, WWindow *wwin, XEvent
  }
  break;
 #endif /* KEEP_XKB_LOCK_STATUS */
+
+ case WKBD_MARK_SET:
+ if (ISMAPPED(wwin) && ISFOCUSED(wwin))
+ startMarkCapture(scr, dpy, MARK_CAPTURE_SET);
+ break;
+
+ case WKBD_MARK_UNSET:
+ if (ISMAPPED(wwin) && ISFOCUSED(wwin) && wwin->mark_key_label != NULL)
+ wWindowUnsetMark(wwin);
+ break;
+
+ case WKBD_MARK_BRING:
+ startMarkCapture(scr, dpy, MARK_CAPTURE_BRING);
+ break;
+
+ case WKBD_MARK_JUMP:
+ startMarkCapture(scr, dpy, MARK_CAPTURE_JUMP);
+ break;
+
+ case WKBD_MARK_SWAP:
+ if (ISMAPPED(wwin) && ISFOCUSED(wwin))
+ startMarkCapture(scr, dpy, MARK_CAPTURE_SWAP);
+ break;
  }
 }
 
@@ -1990,6 +2019,118 @@ static void handleKeyPress(XEvent * event)
  /* ignore CapsLock */
  modifiers = event->xkey.state & w_global.shortcut.modifiers_mask;
 
+ /* ----------------------------------------------------------- *
+ * Window mark capture mode                                     *
+ *                                                              *
+ * We grabbed the keyboard so all keypresses come here until    *
+ * we release the grab.                                         *
+ * ------------------------------------------------------------ */
+ if (scr->flags.mark_capture_mode != MARK_CAPTURE_IDLE) {
+ int capture_mode = scr->flags.mark_capture_mode;
+ KeySym cap_ksym;
+ char label[MAX_SHORTCUT_LENGTH];
+
+ /* Skip modifier-only keypresses */
+ cap_ksym = XLookupKeysym(&event->xkey, 0);
+ if (cap_ksym == NoSymbol || IsModifierKey(cap_ksym))
+ return;
+
+ /* Real key received: exit capture mode and release grab */
+ scr->flags.mark_capture_mode = MARK_CAPTURE_IDLE;
+ XUngrabKeyboard(dpy, CurrentTime);
+
+ if (!GetCanonicalShortcutLabel(modifiers, cap_ksym, label, sizeof(label)))
+ wstrlcpy(label, "?", sizeof(label));
+
+ if (capture_mode == MARK_CAPTURE_SET) {
+ WWindow *target = scr->focused_window;
+ Bool conflict = False;
+ int i;
+
+ /* Conflict check against static wmaker bindings */
+ for (i = 0; i < WKBD_LAST; i++) {
+ if (wKeyBindings[i].keycode == event->xkey.keycode && wKeyBindings[i].modifier == modifiers) {
+ wwarning("window mark: '%s' is already a wmaker binding, mark not assigned", label);
+ conflict = True;
+ break;
+ }
+ }
+
+ /* Conflict check against existing marks on other windows */
+ if (!conflict) {
+ WWindow *tw = scr->focused_window;
+ while (tw) {
+ if (tw != target && tw->mark_key_label != NULL && strcmp(tw->mark_key_label, label) == 0) {
+ wwarning("window mark: label '%s' is already used by another window, mark not assigned", label);
+ conflict = True;
+ break;
+ }
+ tw = tw->prev;
+ }
+ }
+
+ if (!conflict && target != NULL)
+ wWindowSetMark(target, label);
+
+ } else {
+ /* Find marked window by label */
+ WWindow *tw = scr->focused_window;
+
+ while (tw) {
+ if (tw->mark_key_label != NULL && strcmp(tw->mark_key_label, label) == 0)
+ break;
+ tw = tw->prev;
+ }
+ if (tw == NULL) {
+ wwarning("window mark: no window labelled '%s'", label);
+ } else if (capture_mode == MARK_CAPTURE_BRING) {
+ if (tw->frame->workspace != scr->current_workspace)
+ wWindowChangeWorkspace(tw, scr->current_workspace);
+ wMakeWindowVisible(tw);
+ } else if (capture_mode == MARK_CAPTURE_JUMP) {
+ wMakeWindowVisible(tw);
+ } else {
+ /* MARK_CAPTURE_SWAP: swap position, size and workspace between focused and tw */
+ WWindow *focused = scr->focused_window;
+ int fx, fy, fw, fh, tx, ty, tw_w, tw_h;
+ int f_ws, t_ws;
+
+ if (focused == NULL || focused == tw)
+ return;
+
+ /* Snapshot both geometries */
+ fx   = focused->frame_x;
+ fy   = focused->frame_y;
+ fw   = focused->client.width;
+ fh   = focused->client.height;
+ f_ws = focused->frame->workspace;
+
+ tx   = tw->frame_x;
+ ty   = tw->frame_y;
+ tw_w = tw->client.width;
+ tw_h = tw->client.height;
+ t_ws = tw->frame->workspace;
+
+ /* Swap workspaces first so configure lands in the right one */
+ if (f_ws != t_ws) {
+ wWindowChangeWorkspace(focused, t_ws);
+ wWindowChangeWorkspace(tw, f_ws);
+ }
+
+ /* Swap positions and sizes */
+ wWindowConfigure(focused, tx, ty, tw_w, tw_h);
+ wWindowConfigure(tw, fx, fy, fw, fh);
+
+ /* Follow origin window: switch view to the workspace it landed in,
+ * then restore focus to it */
+ if (f_ws != t_ws)
+ wWorkspaceChange(scr, t_ws);
+ wSetFocusTo(scr, focused);
+ }
+ }
+ return;
+ }
+
  /* ------------------------------------------------------------------ *
   * Trie-based key-chain matching                                      *
   *                                                                    *
diff --git a/src/keybind.h b/src/keybind.h
index 292cbb71..ae9a5022 100644
--- a/src/keybind.h
+++ b/src/keybind.h
@@ -80,6 +80,11 @@ enum {
  WKBD_GROUPNEXT,
  WKBD_GROUPPREV,
  WKBD_CENTRAL,
+ WKBD_MARK_SET,
+ WKBD_MARK_UNSET,
+ WKBD_MARK_BRING,
+ WKBD_MARK_JUMP,
+ WKBD_MARK_SWAP,
 
  /* window, menu */
  WKBD_CLOSE,
diff --git a/src/misc.c b/src/misc.c
index 38d6238c..788c1b5d 100644
--- a/src/misc.c
+++ b/src/misc.c
@@ -847,6 +847,34 @@ static char *keysymToString(KeySym keysym, unsigned int state)
 }
 #endif
 
+Bool GetCanonicalShortcutLabel(unsigned int modifiers, KeySym ksym, char *buf, size_t bufsz)
+{
+ static const struct { unsigned int mask; const char *name; } mt[] = {
+ { ShiftMask,   "Shift+"   },
+ { ControlMask, "Control+" },
+ { Mod1Mask,    "Mod1+"    },
+ { Mod2Mask,    "Mod2+"    },
+ { Mod3Mask,    "Mod3+"    },
+ { Mod4Mask,    "Mod4+"    },
+ { Mod5Mask,    "Mod5+"    },
+ { 0, NULL }
+ };
+ const char *kname = XKeysymToString(ksym);
+ size_t i;
+
+ if (!kname)
+ return False;
+
+ buf[0] = '\0';
+ for (i = 0; mt[i].mask; i++) {
+ if (modifiers & mt[i].mask)
+ wstrlcat(buf, mt[i].name, bufsz);
+ }
+ wstrlcat(buf, kname, bufsz);
+
+ return True;
+}
+
 char *GetShortcutString(const char *shortcut)
 {
  char *buffer = NULL;
@@ -882,50 +910,12 @@ char *GetShortcutString(const char *shortcut)
 char *GetShortcutKey(WShortKey key)
 {
  char buf[MAX_SHORTCUT_LENGTH];
- char *wr, *result, *seg;
+ char *result, *seg;
  int step;
 
- void append_string(const char *text)
- {
- const char *s = text;
-
- while (*s) {
- if (wr >= buf + sizeof(buf) - 1)
- break;
- *wr++ = *s++;
- }
- }
-
- void append_modifier(int modifier_index, const char *fallback_name)
- {
- if (wPreferences.modifier_labels[modifier_index])
- append_string(wPreferences.modifier_labels[modifier_index]);
- else
- append_string(fallback_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))
+ if (!GetCanonicalShortcutLabel(key.modifier,
+        W_KeycodeToKeysym(dpy, key.keycode, 0),
+        buf, sizeof(buf)))
  return NULL;
 
  /* Convert the leader token to its display string */
@@ -938,7 +928,9 @@ char *GetShortcutKey(WShortKey key)
  if (key.chain_keycodes[step] == 0)
  break;
 
- if (!build_token(key.chain_modifiers[step], key.chain_keycodes[step]))
+ if (!GetCanonicalShortcutLabel(key.chain_modifiers[step],
+        W_KeycodeToKeysym(dpy, key.chain_keycodes[step], 0),
+        buf, sizeof(buf)))
  break;
 
  seg = GetShortcutString(buf);
diff --git a/src/misc.h b/src/misc.h
index 5fc15a8f..288a8bb4 100644
--- a/src/misc.h
+++ b/src/misc.h
@@ -50,6 +50,7 @@ char *ExpandOptions(WScreen * scr, const char *cmdline);
 void ExecuteInputCommand(WScreen *scr, const char *cmdline);
 void ExecuteExitCommand(WScreen *scr, long quickmode);
 char *GetShortcutString(const char *text);
+Bool GetCanonicalShortcutLabel(unsigned int modifiers, KeySym ksym, char *buf, size_t bufsz);
 char *GetShortcutKey(WShortKey key);
 char *EscapeWM_CLASS(const char *name, const char *class);
 char *StrConcatDot(const char *a, const char *b);
diff --git a/src/screen.h b/src/screen.h
index c3100b0b..9ecb1253 100644
--- a/src/screen.h
+++ b/src/screen.h
@@ -59,6 +59,14 @@ typedef struct WDrawerChain {
     struct WDrawerChain *next;
 } WDrawerChain;
 
+typedef enum {
+    MARK_CAPTURE_IDLE  = 0,
+    MARK_CAPTURE_SET   = 1,
+    MARK_CAPTURE_BRING = 2,
+    MARK_CAPTURE_JUMP  = 3,
+    MARK_CAPTURE_SWAP  = 4
+} WMarkCaptureMode;
+
 /*
  * each WScreen is saved into a context associated with its root window
  */
@@ -332,6 +340,8 @@ typedef struct _WScreen {
         unsigned int jump_back_pending:1;
         unsigned int ignore_focus_events:1;
         unsigned int in_hot_corner:3;
+        unsigned int mark_capture_mode:3;  /* one of WMarkCaptureMode */
+
     } flags;
 } WScreen;
 
diff --git a/src/session.c b/src/session.c
index 89df5b54..9d66b218 100644
--- a/src/session.c
+++ b/src/session.c
@@ -97,6 +97,7 @@ static WMPropList *sMaximized;
 static WMPropList *sHidden;
 static WMPropList *sGeometry;
 static WMPropList *sShortcutMask;
+static WMPropList *sMarkKey;
 
 static WMPropList *sDock;
 static WMPropList *sYes, *sNo;
@@ -118,6 +119,7 @@ static void make_keys(void)
  sGeometry = WMCreatePLString("Geometry");
  sDock = WMCreatePLString("Dock");
  sShortcutMask = WMCreatePLString("ShortcutMask");
+ sMarkKey = WMCreatePLString("MarkKey");
 
  sYes = WMCreatePLString("Yes");
  sNo = WMCreatePLString("No");
@@ -165,6 +167,7 @@ static WMPropList *makeWindowState(WWindow * wwin, WApplication * wapp)
  WMPropList *win_state, *cmd, *name, *workspace;
  WMPropList *shaded, *miniaturized, *maximized, *hidden, *geometry;
  WMPropList *dock, *shortcut;
+ WMPropList *mark_key_pl = NULL;
 
  if (wwin->orig_main_window != None && wwin->orig_main_window != wwin->client_win)
  win = wwin->orig_main_window;
@@ -215,6 +218,9 @@ static WMPropList *makeWindowState(WWindow * wwin, WApplication * wapp)
  snprintf(buffer, sizeof(buffer), "%u", mask);
  shortcut = WMCreatePLString(buffer);
 
+ if (wwin->mark_key_label)
+ mark_key_pl = WMCreatePLString(wwin->mark_key_label);
+
  win_state = WMCreatePLDictionary(sName, name,
   sCommand, cmd,
   sWorkspace, workspace,
@@ -224,6 +230,11 @@ static WMPropList *makeWindowState(WWindow * wwin, WApplication * wapp)
   sHidden, hidden,
   sShortcutMask, shortcut, sGeometry, geometry, NULL);
 
+ if (mark_key_pl) {
+ WMPutInPLDictionary(win_state, sMarkKey, mark_key_pl);
+ WMReleasePropList(mark_key_pl);
+ }
+
  WMReleasePropList(name);
  WMReleasePropList(cmd);
  WMReleasePropList(workspace);
@@ -418,6 +429,13 @@ static WSavedState *getWindowState(WScreen * scr, WMPropList * win_state)
  state->window_shortcuts = mask;
  }
 
+ value = WMGetFromPLDictionary(win_state, sMarkKey);
+ if (value != NULL && WMIsPLString(value)) {
+ char *s = WMGetFromPLString(value);
+ if (s && *s)
+ state->mark_key = wstrdup(s);
+ }
+
  value = WMGetFromPLDictionary(win_state, sGeometry);
  if (value && WMIsPLString(value)) {
  if (!(sscanf(WMGetFromPLString(value), "%ix%i+%i+%i",
@@ -531,6 +549,8 @@ void wSessionRestoreState(WScreen *scr)
  } else if ((pid = execCommand(scr, command)) > 0) {
  wWindowAddSavedState(instance, class, command, pid, state);
  } else {
+ if (state->mark_key)
+ wfree(state->mark_key);
  wfree(state);
  }
 
diff --git a/src/startup.c b/src/startup.c
index ed992901..b0e676b2 100644
--- a/src/startup.c
+++ b/src/startup.c
@@ -387,6 +387,7 @@ static char *atomNames[] = {
  "_WINDOWMAKER_COMMAND",
  "_WINDOWMAKER_ICON_SIZE",
  "_WINDOWMAKER_ICON_TILE",
+ "_WINDOWMAKER_MARK_KEY",
 
  GNUSTEP_WM_ATTR_NAME,
  GNUSTEP_WM_MINIATURIZE_WINDOW,
@@ -474,6 +475,7 @@ void StartUp(Bool defaultScreenOnly)
  w_global.atom.desktop.gtk_object_path = atom[20];
 
  w_global.atom.wm.ignore_focus_events = atom[21];
+ w_global.atom.wmaker.mark_key  = atom[22];
 
 #ifdef USE_DOCK_XDND
  wXDNDInitializeAtoms();
diff --git a/src/switchmenu.c b/src/switchmenu.c
index a8c3937f..0f1746f6 100644
--- a/src/switchmenu.c
+++ b/src/switchmenu.c
@@ -44,6 +44,8 @@
  ((w)->wm_gnustep_attr->window_level == WMMainMenuWindowLevel || \
   (w)->wm_gnustep_attr->window_level == WMSubmenuWindowLevel))
 
+#define MAX_RTEXT_LENGTH (MAX_WORKSPACENAME_WIDTH + MAX_SHORTCUT_LENGTH + 16)
+
 static int initialized = 0;
 static void observer(void *self, WMNotification * notif);
 static void wsobserver(void *self, WMNotification * notif);
@@ -214,6 +216,26 @@ static int menuIndexForWindow(WMenu * menu, WWindow * wwin, int old_pos)
  return idx;
 }
 
+static void fillRtext(char *buf, size_t bufsz, WWindow *wwin, WScreen *scr)
+{
+ char *mlbl = wwin->mark_key_label ? GetShortcutString(wwin->mark_key_label) : NULL;
+
+ if (IS_OMNIPRESENT(wwin)) {
+ if (mlbl)
+ snprintf(buf, bufsz, "[%s] [*]", mlbl);
+ else
+ snprintf(buf, bufsz, "[*]");
+ } else {
+ if (mlbl)
+ snprintf(buf, bufsz, "[%s] [%s]", mlbl,
+  scr->workspaces[wwin->frame->workspace]->name);
+ else
+ snprintf(buf, bufsz, "[%s]",
+  scr->workspaces[wwin->frame->workspace]->name);
+ }
+ wfree(mlbl);
+}
+
 /*
  * Update switch menu
  */
@@ -263,12 +285,8 @@ void UpdateSwitchMenu(WScreen * scr, WWindow * wwin, int action)
  entry->icon = switchMenuIconForWindow(scr, wwin);
 
  entry->flags.indicator = 1;
- entry->rtext = wmalloc(MAX_WORKSPACENAME_WIDTH + 8);
- if (IS_OMNIPRESENT(wwin))
- snprintf(entry->rtext, MAX_WORKSPACENAME_WIDTH, "[*]");
- else
- snprintf(entry->rtext, MAX_WORKSPACENAME_WIDTH, "[%s]",
-  scr->workspaces[wwin->frame->workspace]->name);
+ entry->rtext = wmalloc(MAX_RTEXT_LENGTH);
+ fillRtext(entry->rtext, MAX_RTEXT_LENGTH, wwin, scr);
 
  if (wwin->flags.hidden) {
  entry->flags.indicator_type = MI_HIDDEN;
@@ -311,6 +329,8 @@ void UpdateSwitchMenu(WScreen * scr, WWindow * wwin, int action)
  t = ShrinkString(scr->menu_entry_font, title, MAX_WINDOWLIST_WIDTH);
  entry->text = t;
 
+ fillRtext(entry->rtext, MAX_RTEXT_LENGTH, wwin, scr);
+
  wMenuRealize(switchmenu);
  checkVisibility = 1;
  break;
@@ -322,13 +342,7 @@ void UpdateSwitchMenu(WScreen * scr, WWindow * wwin, int action)
  WPixmap *ipix;
  int it, ion;
 
- if (IS_OMNIPRESENT(wwin)) {
- snprintf(entry->rtext, MAX_WORKSPACENAME_WIDTH, "[*]");
- } else {
- snprintf(entry->rtext, MAX_WORKSPACENAME_WIDTH,
-  "[%s]",
-  scr->workspaces[wwin->frame->workspace]->name);
- }
+ fillRtext(entry->rtext, MAX_RTEXT_LENGTH, wwin, scr);
 
  rt = entry->rtext;
  entry->rtext = NULL;
@@ -404,11 +418,7 @@ static void UpdateSwitchMenuWorkspace(WScreen *scr, int workspace)
  wwin = (WWindow *) menu->entries[i]->clientdata;
 
  if (wwin->frame->workspace == workspace && !IS_OMNIPRESENT(wwin)) {
- if (IS_OMNIPRESENT(wwin))
- snprintf(menu->entries[i]->rtext, MAX_WORKSPACENAME_WIDTH, "[*]");
- else
- snprintf(menu->entries[i]->rtext, MAX_WORKSPACENAME_WIDTH, "[%s]",
-  scr->workspaces[wwin->frame->workspace]->name);
+ fillRtext(menu->entries[i]->rtext, MAX_RTEXT_LENGTH, wwin, scr);
  menu->flags.realized = 0;
  }
  }
diff --git a/src/window.c b/src/window.c
index 9cb11882..470cfcab 100644
--- a/src/window.c
+++ b/src/window.c
@@ -75,6 +75,7 @@
 #include "startup.h"
 #include "winmenu.h"
 #include "osdep.h"
+#include "switchmenu.h"
 
 #ifdef USE_MWM_HINTS
 # include "motif.h"
@@ -200,6 +201,10 @@ void wWindowDestroy(WWindow *wwin)
  }
  }
 
+ /* clean up any mark assigned to this window */
+ if (wwin->mark_key_label != NULL)
+ wWindowUnsetMark(wwin);
+
  if (wwin->fake_group && wwin->fake_group->retainCount > 0) {
  wwin->fake_group->retainCount--;
  if (wwin->fake_group->retainCount == 0 && wwin->fake_group->leader != None) {
@@ -1006,8 +1011,17 @@ WWindow *wManageWindow(WScreen *scr, Window window)
  }
  }
 
- if (wstate != NULL)
+ /* restore mark: prefer session state, fall back to warm-restart hint */
+ if (win_state != NULL && win_state->state->mark_key != NULL)
+ wWindowSetMark(wwin, win_state->state->mark_key);
+ else if (wstate != NULL && wstate->mark_key != NULL)
+ wWindowSetMark(wwin, wstate->mark_key);
+
+ if (wstate != NULL) {
+ if (wstate->mark_key != NULL)
+ wfree(wstate->mark_key);
  wfree(wstate);
+ }
  }
 
  /* don't let transients start miniaturized if their owners are not */
@@ -2527,6 +2541,14 @@ void wWindowSaveState(WWindow *wwin)
 
  XChangeProperty(dpy, wwin->client_win, w_global.atom.wmaker.state,
  w_global.atom.wmaker.state, 32, PropModeReplace, (unsigned char *)data, 10);
+
+ if (wwin->mark_key_label != NULL)
+ XChangeProperty(dpy, wwin->client_win, w_global.atom.wmaker.mark_key,
+ XA_STRING, 8, PropModeReplace,
+ (unsigned char *)wwin->mark_key_label,
+ strlen(wwin->mark_key_label));
+ else
+ XDeleteProperty(dpy, wwin->client_win, w_global.atom.wmaker.mark_key);
 }
 
 static int getSavedState(Window window, WSavedState ** state)
@@ -2536,6 +2558,7 @@ static int getSavedState(Window window, WSavedState ** state)
  unsigned long nitems_ret;
  unsigned long bytes_after_ret;
  long *data;
+ unsigned char *mk_data = NULL;
 
  if (XGetWindowProperty(dpy, window, w_global.atom.wmaker.state, 0, 10,
         True, w_global.atom.wmaker.state,
@@ -2563,6 +2586,15 @@ static int getSavedState(Window window, WSavedState ** state)
 
  XFree(data);
 
+ (*state)->mark_key = NULL;
+ if (XGetWindowProperty(dpy, window, w_global.atom.wmaker.mark_key, 0, 256,
+ True, XA_STRING, &type_ret, &fmt_ret, &nitems_ret, &bytes_after_ret,
+ &mk_data) == Success && mk_data && nitems_ret > 0 && type_ret == XA_STRING)
+ (*state)->mark_key = wstrdup((char *)mk_data);
+
+ if (mk_data)
+ XFree(mk_data);
+
  return 1;
 }
 
@@ -2889,6 +2921,9 @@ static void release_wwindowstate(WWindowState *wstate)
  if (wstate->command)
  wfree(wstate->command);
 
+ if (wstate->state && wstate->state->mark_key)
+ wfree(wstate->state->mark_key);
+
  wfree(wstate->state);
  wfree(wstate);
 }
@@ -2902,6 +2937,29 @@ void wWindowSetOmnipresent(WWindow *wwin, Bool flag)
  WMPostNotificationName(WMNChangedState, wwin, "omnipresent");
 }
 
+void wWindowSetMark(WWindow *wwin, const char *label)
+{
+ /* Remove any previous mark first */
+ if (wwin->mark_key_label != NULL)
+ wWindowUnsetMark(wwin);
+
+ wwin->mark_key_label = wstrdup(label);
+
+ UpdateSwitchMenu(wwin->screen_ptr, wwin, ACTION_CHANGE);
+}
+
+void wWindowUnsetMark(WWindow *wwin)
+{
+ if (wwin->mark_key_label == NULL)
+ return;
+
+ wfree(wwin->mark_key_label);
+ wwin->mark_key_label = NULL;
+
+ UpdateSwitchMenu(wwin->screen_ptr, wwin, ACTION_CHANGE);
+}
+
+
 static void resizebarMouseDown(WCoreWindow *sender, void *data, XEvent *event)
 {
  WWindow *wwin = data;
diff --git a/src/window.h b/src/window.h
index 8b14564e..1229eed3 100644
--- a/src/window.h
+++ b/src/window.h
@@ -299,6 +299,7 @@ typedef struct WWindow {
  int icon_w, icon_h;
  RImage *net_icon_image; /* Window Image */
  Atom type;
+ char *mark_key_label; /* Vim-like Window Marking */
 } WWindow;
 
 #define HAS_TITLEBAR(w) (!(WFLAGP((w), no_titlebar) || (w)->flags.fullscreen))
@@ -323,6 +324,7 @@ typedef struct WSavedState {
     unsigned int w;
     unsigned int h;
     unsigned window_shortcuts; /* mask like 1<<shortcut_number */
+ char *mark_key; /* serialised mark key label */
 } WSavedState;
 
 typedef struct WWindowState {
@@ -407,6 +409,10 @@ Bool wWindowObscuresWindow(WWindow *wwin, WWindow *obscured);
 
 void wWindowSetOmnipresent(WWindow *wwin, Bool flag);
 
+/* Vim-like window marking management */
+void wWindowSetMark(WWindow *wwin, const char *label);
+void wWindowUnsetMark(WWindow *wwin);
+
 #ifdef XKB_BUTTON_HINT
 void wWindowGetLanguageLabel(int group_index, char *label);
 #endif
--
2.43.0
0001-wmaker-dynamic-vim-like-window-marking-feature.patch

Kostas Michalopoulos

unread,
Apr 2, 2026, 5:03:06 AM (4 days ago) Apr 2
to wmake...@googlegroups.com
On 4/2/26 3:26 AM, david.m...@gmail.com wrote:
> This patch is adding vim-like window marking, like in i3.
> A window can be dynamically assigned a mark label.
> Then a marked window can be pulled or jumped to.
> Or the current focused window can be swapped with a marked window.
>
> The mark label appears in the Window List in between the window
> title and the workspace name.
>
> Those new options in WindowMaker conf file are used to control
> the actions: MarkSetKey, MarkUnsetKey, MarkBringKey, MarkJumpKey
> and MarkSwapKey.

Having used neither vim nor i3, what is the exact functionality and its
purpose? Are these marks like (textual) tags i can assign to windows? If
so, can i assigned them for all windows of the same application (like
setting them in the Attributes panel)? Also, are these persistent and
can be set/unset/queried from the command-line? Are there any examples
of workflows these can enable?

Bad Sector

david.m...@gmail.com

unread,
Apr 2, 2026, 12:22:49 PM (4 days ago) Apr 2
to Window Maker Development
Hi,

A mark label is a key tag (or can be a modifier+key) that can be assigned to a unique window.

Here are some workflows:
Usually, the mark action is set to 'SUPER+M'.
So if you have, for example, an xterm, you can mark it with 'SUPER+M x'.
And every time you need it, you don't have to search for it, just jump to it.
If you have configured mark jump to 'SUPER+J', you can just do 'SUPER+J x'
or summon it with the mark bring action.
Another workflow example is if you want to keep a specific layout, you can just mark swap windows.

Those are persistent and saved in the WMState conf file under "MarkKey".
AFAIK, wmaker does not provide an IPC that allows external programs to communicate with a running instance.

regards

Carlos R. Mafra

unread,
Apr 2, 2026, 1:07:36 PM (4 days ago) Apr 2
to wmake...@googlegroups.com
On Thu, 2 Apr 2026 at 9:22:49 -0700, david.m...@gmail.com wrote:
> A mark label is a key tag (or can be a modifier+key) that can be assigned
> to a unique window.
>
> Here are some workflows:
> Usually, the mark action is set to 'SUPER+M'.
> So if you have, for example, an xterm, you can mark it with 'SUPER+M x'.
> And every time you need it, you don't have to search for it, just jump to
> it.
> If you have configured mark jump to 'SUPER+J', you can just do 'SUPER+J x'
> or summon it with the mark bring action.
> Another workflow example is if you want to keep a specific layout, you can
> just mark swap windows.
>
> Those are persistent and saved in the WMState conf file under "MarkKey".
> AFAIK, wmaker does not provide an IPC that allows external programs to
> communicate with a running instance.

Hi David,

Could you consider updating the NEWS file at the toplevel directory?

I think it is important to let people know about new features and
teach them how to use them at the same time. So if you could do it,
that would be great.

Best, Carlos

david.m...@gmail.com

unread,
Apr 2, 2026, 2:44:41 PM (4 days ago) Apr 2
to Window Maker Development
I was thinking we should update the docs and NEWS file just before a release,
in case if something is changed in between.
But I can do a check on the NEWS file now already if you want.
 

Carlos R. Mafra

unread,
Apr 2, 2026, 3:26:59 PM (4 days ago) Apr 2
to wmake...@googlegroups.com
Yes, it makes sense to update it before a release. But on the other
hand it is important to teach users how to use a new feature.

Kostas is a bit confused about how to use this new feature, and so am I.

Ideally a full explanation would be part of the commit message, but
I missed to ask that. So the NEWS file seems to be a good fallback in
this case.

If you could take a look, that'd be great.

david.m...@gmail.com

unread,
Apr 2, 2026, 4:04:10 PM (4 days ago) Apr 2
to Window Maker Development
Sure, will do. 

david.m...@gmail.com

unread,
Apr 2, 2026, 5:25:09 PM (4 days ago) Apr 2
to Window Maker Development
hum, there is second patch linked to that one but seems I cannot upload it.
[PATCH 2/2] wmaker: add directional window focus
It's being autodeleted...
Reply all
Reply to author
Forward
0 new messages