commit d02758b4c026d0e2627d841dec9bfc311cbe341f
parent d9b14470bb15cf0156ecc2b05d68cb3ec5bf406c
Author: 8dcc <8dcc.git@gmail.com>
Date: Sat, 11 Apr 2026 17:55:59 +0200
[dwm][patches] Add 'workflows' patch
Diffstat:
2 files changed, 535 insertions(+), 0 deletions(-)
diff --git a/dwm.suckless.org/patches/workspaces/dwm-workspaces-20260411-23a25b2.diff b/dwm.suckless.org/patches/workspaces/dwm-workspaces-20260411-23a25b2.diff
@@ -0,0 +1,500 @@
+From 23a25b2918e083e7c9ecad934db7841004aa4629 Mon Sep 17 00:00:00 2001
+From: 8dcc <8dcc.git@gmail.com>
+Date: Sat, 11 Apr 2026 17:38:52 +0200
+Subject: [PATCH] Add support for multiple workspaces
+
+Each monitor now has N independently-named workspaces (defined via the
+new 'workspaces' array in 'config.h', like 'tags'). Every client belongs
+to exactly one workspace; switching workspaces hides all clients from
+the previous workspace and shows only those from the new one.
+
+Each workspace maintains its own tag view, layout, and layout history
+independently of all others. Switching workspaces carries the current
+tag selection over so the viewed tag does not change.
+
+Default keybinds (where 'F<n>' represents function keys):
+
+ * Mod+F<n>: Switch to workspace N.
+ * Mod+Shift+F<n>: Move focused window to workspace N.
+
+The bar shows workspace buttons leftmost, styled identically to tag
+buttons ('SchemeSel' for active, 'SchemeNorm' for others). Clicking a
+workspace button switches to it.
+---
+ config.def.h | 8 +++
+ dwm.c | 181 ++++++++++++++++++++++++++++++++++++---------------
+ 2 files changed, 135 insertions(+), 54 deletions(-)
+
+diff --git a/config.def.h b/config.def.h
+index 1c0b587..db6ce40 100644
+--- a/config.def.h
++++ b/config.def.h
+@@ -20,6 +20,7 @@ static const char *colors[][3] = {
+
+ /* tagging */
+ static const char *tags[] = { "1", "2", "3", "4", "5", "6", "7", "8", "9" };
++static const char *workspaces[] = { "A", "B", "C" };
+
+ static const Rule rules[] = {
+ /* xprop(1):
+@@ -50,6 +51,9 @@ static const Layout layouts[] = {
+ { MODKEY|ControlMask, KEY, toggleview, {.ui = 1 << TAG} }, \
+ { MODKEY|ShiftMask, KEY, tag, {.ui = 1 << TAG} }, \
+ { MODKEY|ControlMask|ShiftMask, KEY, toggletag, {.ui = 1 << TAG} },
++#define WSKEYS(KEY,WS) \
++ { MODKEY, KEY, viewws, {.i = (WS)} }, \
++ { MODKEY|ShiftMask, KEY, tagws, {.i = (WS)} },
+
+ /* helper for spawning shell commands in the pre dwm-5.0 fashion */
+ #define SHCMD(cmd) { .v = (const char*[]){ "/bin/sh", "-c", cmd, NULL } }
+@@ -93,6 +97,9 @@ static Key keys[] = {
+ TAGKEYS( XK_7, 6)
+ TAGKEYS( XK_8, 7)
+ TAGKEYS( XK_9, 8)
++ WSKEYS( XK_F1, 0)
++ WSKEYS( XK_F2, 1)
++ WSKEYS( XK_F3, 2)
+ { MODKEY|ShiftMask, XK_q, quit, {0} },
+ };
+
+@@ -100,6 +107,7 @@ static Key keys[] = {
+ /* click can be ClkTagBar, ClkLtSymbol, ClkStatusText, ClkWinTitle, ClkClientWin, or ClkRootWin */
+ static Button buttons[] = {
+ /* click event mask button function argument */
++ { ClkWsBar, 0, Button1, viewws, {0} },
+ { ClkLtSymbol, 0, Button1, setlayout, {0} },
+ { ClkLtSymbol, 0, Button3, setlayout, {.v = &layouts[2]} },
+ { ClkWinTitle, 0, Button2, zoom, {0} },
+diff --git a/dwm.c b/dwm.c
+index 4465af1..7534363 100644
+--- a/dwm.c
++++ b/dwm.c
+@@ -49,7 +49,10 @@
+ #define CLEANMASK(mask) (mask & ~(numlockmask|LockMask) & (ShiftMask|ControlMask|Mod1Mask|Mod2Mask|Mod3Mask|Mod4Mask|Mod5Mask))
+ #define INTERSECT(x,y,w,h,m) (MAX(0, MIN((x)+(w),(m)->wx+(m)->ww) - MAX((x),(m)->wx)) \
+ * MAX(0, MIN((y)+(h),(m)->wy+(m)->wh) - MAX((y),(m)->wy)))
+-#define ISVISIBLE(C) ((C->tags & C->mon->tagset[C->mon->seltags]))
++#define MWS(m) ((m)->ws[(m)->selws])
++#define MTAGSET(m) (MWS(m).tagset[MWS(m).seltags])
++#define ISVISIBLE(C) ((C)->tags & MTAGSET((C)->mon) \
++ && (C)->ws == (C)->mon->selws)
+ #define LENGTH(X) (sizeof X / sizeof X[0])
+ #define MOUSEMASK (BUTTONMASK|PointerMotionMask)
+ #define WIDTH(X) ((X)->w + 2 * (X)->bw)
+@@ -64,7 +67,7 @@ enum { NetSupported, NetWMName, NetWMState, NetWMCheck,
+ NetWMFullscreen, NetActiveWindow, NetWMWindowType,
+ NetWMWindowTypeDialog, NetClientList, NetLast }; /* EWMH atoms */
+ enum { WMProtocols, WMDelete, WMState, WMTakeFocus, WMLast }; /* default atoms */
+-enum { ClkTagBar, ClkLtSymbol, ClkStatusText, ClkWinTitle,
++enum { ClkWsBar, ClkTagBar, ClkLtSymbol, ClkStatusText, ClkWinTitle,
+ ClkClientWin, ClkRootWin, ClkLast }; /* clicks */
+
+ typedef union {
+@@ -92,6 +95,7 @@ struct Client {
+ int basew, baseh, incw, inch, maxw, maxh, minw, minh;
+ int bw, oldbw;
+ unsigned int tags;
++ int ws;
+ int isfixed, isfloating, isurgent, neverfocus, oldstate, isfullscreen;
+ Client *next;
+ Client *snext;
+@@ -111,17 +115,23 @@ typedef struct {
+ void (*arrange)(Monitor *);
+ } Layout;
+
+-struct Monitor {
++typedef struct {
++ unsigned int tagset[2];
++ unsigned int seltags;
++ unsigned int sellt;
++ const Layout *lt[2];
+ char ltsymbol[16];
++} WsState;
++
++struct Monitor {
+ float mfact;
+ int nmaster;
+ int num;
+ int by; /* bar geometry */
+ int mx, my, mw, mh; /* screen size */
+ int wx, wy, ww, wh; /* window area */
+- unsigned int seltags;
+- unsigned int sellt;
+- unsigned int tagset[2];
++ int selws;
++ WsState *ws;
+ int showbar;
+ int topbar;
+ Client *clients;
+@@ -129,7 +139,6 @@ struct Monitor {
+ Client *stack;
+ Monitor *next;
+ Window barwin;
+- const Layout *lt[2];
+ };
+
+ typedef struct {
+@@ -208,7 +217,9 @@ static void sigchld(int unused);
+ static void spawn(const Arg *arg);
+ static void tag(const Arg *arg);
+ static void tagmon(const Arg *arg);
++static void tagws(const Arg *arg);
+ static void tile(Monitor *);
++static void viewws(const Arg *arg);
+ static void togglebar(const Arg *arg);
+ static void togglefloating(const Arg *arg);
+ static void toggletag(const Arg *arg);
+@@ -308,7 +319,7 @@ applyrules(Client *c)
+ XFree(ch.res_class);
+ if (ch.res_name)
+ XFree(ch.res_name);
+- c->tags = c->tags & TAGMASK ? c->tags & TAGMASK : c->mon->tagset[c->mon->seltags];
++ c->tags = c->tags & TAGMASK ? c->tags & TAGMASK : MTAGSET(c->mon);
+ }
+
+ int
+@@ -343,7 +354,7 @@ applysizehints(Client *c, int *x, int *y, int *w, int *h, int interact)
+ *h = bh;
+ if (*w < bh)
+ *w = bh;
+- if (resizehints || c->isfloating || !c->mon->lt[c->mon->sellt]->arrange) {
++ if (resizehints || c->isfloating || !MWS(c->mon).lt[MWS(c->mon).sellt]->arrange) {
+ /* see last two sentences in ICCCM 4.1.2.3 */
+ baseismin = c->basew == c->minw && c->baseh == c->minh;
+ if (!baseismin) { /* temporarily remove base dimensions */
+@@ -394,9 +405,10 @@ arrange(Monitor *m)
+ void
+ arrangemon(Monitor *m)
+ {
+- strncpy(m->ltsymbol, m->lt[m->sellt]->symbol, sizeof m->ltsymbol);
+- if (m->lt[m->sellt]->arrange)
+- m->lt[m->sellt]->arrange(m);
++ strncpy(MWS(m).ltsymbol, MWS(m).lt[MWS(m).sellt]->symbol,
++ sizeof MWS(m).ltsymbol);
++ if (MWS(m).lt[MWS(m).sellt]->arrange)
++ MWS(m).lt[MWS(m).sellt]->arrange(m);
+ }
+
+ void
+@@ -432,17 +444,26 @@ buttonpress(XEvent *e)
+ if (ev->window == selmon->barwin) {
+ i = x = 0;
+ do
+- x += TEXTW(tags[i]);
+- while (ev->x >= x && ++i < LENGTH(tags));
+- if (i < LENGTH(tags)) {
+- click = ClkTagBar;
+- arg.ui = 1 << i;
+- } else if (ev->x < x + blw)
+- click = ClkLtSymbol;
+- else if (ev->x > selmon->ww - TEXTW(stext))
+- click = ClkStatusText;
+- else
+- click = ClkWinTitle;
++ x += TEXTW(workspaces[i]);
++ while (ev->x >= x && ++i < LENGTH(workspaces));
++ if (i < LENGTH(workspaces)) {
++ click = ClkWsBar;
++ arg.i = i;
++ } else {
++ i = 0;
++ do
++ x += TEXTW(tags[i]);
++ while (ev->x >= x && ++i < LENGTH(tags));
++ if (i < LENGTH(tags)) {
++ click = ClkTagBar;
++ arg.ui = 1 << i;
++ } else if (ev->x < x + blw)
++ click = ClkLtSymbol;
++ else if (ev->x > selmon->ww - TEXTW(stext))
++ click = ClkStatusText;
++ else
++ click = ClkWinTitle;
++ }
+ } else if ((c = wintoclient(ev->window))) {
+ focus(c);
+ restack(selmon);
+@@ -452,7 +473,8 @@ buttonpress(XEvent *e)
+ for (i = 0; i < LENGTH(buttons); i++)
+ if (click == buttons[i].click && buttons[i].func && buttons[i].button == ev->button
+ && CLEANMASK(buttons[i].mask) == CLEANMASK(ev->state))
+- buttons[i].func(click == ClkTagBar && buttons[i].arg.i == 0 ? &arg : &buttons[i].arg);
++ buttons[i].func((click == ClkTagBar || click == ClkWsBar)
++ && buttons[i].arg.i == 0 ? &arg : &buttons[i].arg);
+ }
+
+ void
+@@ -475,7 +497,7 @@ cleanup(void)
+ size_t i;
+
+ view(&a);
+- selmon->lt[selmon->sellt] = &foo;
++ MWS(selmon).lt[MWS(selmon).sellt] = &foo;
+ for (m = mons; m; m = m->next)
+ while (m->stack)
+ unmanage(m->stack, 0);
+@@ -506,6 +528,7 @@ cleanupmon(Monitor *mon)
+ }
+ XUnmapWindow(dpy, mon->barwin);
+ XDestroyWindow(dpy, mon->barwin);
++ free(mon->ws);
+ free(mon);
+ }
+
+@@ -586,7 +609,7 @@ configurerequest(XEvent *e)
+ if ((c = wintoclient(ev->window))) {
+ if (ev->value_mask & CWBorderWidth)
+ c->bw = ev->border_width;
+- else if (c->isfloating || !selmon->lt[selmon->sellt]->arrange) {
++ else if (c->isfloating || !MWS(selmon).lt[MWS(selmon).sellt]->arrange) {
+ m = c->mon;
+ if (ev->value_mask & CWX) {
+ c->oldx = c->x;
+@@ -631,16 +654,22 @@ Monitor *
+ createmon(void)
+ {
+ Monitor *m;
++ unsigned int i;
+
+ m = ecalloc(1, sizeof(Monitor));
+- m->tagset[0] = m->tagset[1] = 1;
+ m->mfact = mfact;
+ m->nmaster = nmaster;
+ m->showbar = showbar;
+ m->topbar = topbar;
+- m->lt[0] = &layouts[0];
+- m->lt[1] = &layouts[1 % LENGTH(layouts)];
+- strncpy(m->ltsymbol, layouts[0].symbol, sizeof m->ltsymbol);
++ m->selws = 0;
++ m->ws = ecalloc(LENGTH(workspaces), sizeof(WsState));
++ for (i = 0; i < LENGTH(workspaces); i++) {
++ m->ws[i].tagset[0] = m->ws[i].tagset[1] = 1;
++ m->ws[i].lt[0] = &layouts[0];
++ m->ws[i].lt[1] = &layouts[1 % LENGTH(layouts)];
++ strncpy(m->ws[i].ltsymbol, layouts[0].symbol,
++ sizeof m->ws[i].ltsymbol);
++ }
+ return m;
+ }
+
+@@ -709,14 +738,22 @@ drawbar(Monitor *m)
+ }
+
+ for (c = m->clients; c; c = c->next) {
+- occ |= c->tags;
++ if (c->ws == m->selws) /* only count occupied tags for current workspace */
++ occ |= c->tags;
+ if (c->isurgent)
+ urg |= c->tags;
+ }
+ x = 0;
++ for (i = 0; i < LENGTH(workspaces); i++) {
++ w = TEXTW(workspaces[i]);
++ drw_setscheme(drw, scheme[i == (unsigned int)m->selws
++ ? SchemeSel : SchemeNorm]);
++ drw_text(drw, x, 0, w, bh, lrpad / 2, workspaces[i], 0);
++ x += w;
++ }
+ for (i = 0; i < LENGTH(tags); i++) {
+ w = TEXTW(tags[i]);
+- drw_setscheme(drw, scheme[m->tagset[m->seltags] & 1 << i ? SchemeSel : SchemeNorm]);
++ drw_setscheme(drw, scheme[MWS(m).tagset[MWS(m).seltags] & 1 << i ? SchemeSel : SchemeNorm]);
+ drw_text(drw, x, 0, w, bh, lrpad / 2, tags[i], urg & 1 << i);
+ if (occ & 1 << i)
+ drw_rect(drw, x + boxs, boxs, boxw, boxw,
+@@ -724,9 +761,9 @@ drawbar(Monitor *m)
+ urg & 1 << i);
+ x += w;
+ }
+- w = blw = TEXTW(m->ltsymbol);
++ w = blw = TEXTW(MWS(m).ltsymbol);
+ drw_setscheme(drw, scheme[SchemeNorm]);
+- x = drw_text(drw, x, 0, w, bh, lrpad / 2, m->ltsymbol, 0);
++ x = drw_text(drw, x, 0, w, bh, lrpad / 2, MWS(m).ltsymbol, 0);
+
+ if ((w = m->ww - sw - x) > bh) {
+ if (m->sel) {
+@@ -1034,9 +1071,11 @@ manage(Window w, XWindowAttributes *wa)
+ if (XGetTransientForHint(dpy, w, &trans) && (t = wintoclient(trans))) {
+ c->mon = t->mon;
+ c->tags = t->tags;
++ c->ws = t->ws;
+ } else {
+ c->mon = selmon;
+ applyrules(c);
++ c->ws = c->mon->selws; /* set after applyrules, which may change c->mon */
+ }
+
+ if (c->x + WIDTH(c) > c->mon->mx + c->mon->mw)
+@@ -1110,7 +1149,7 @@ monocle(Monitor *m)
+ if (ISVISIBLE(c))
+ n++;
+ if (n > 0) /* override layout symbol */
+- snprintf(m->ltsymbol, sizeof m->ltsymbol, "[%d]", n);
++ snprintf(MWS(m).ltsymbol, sizeof MWS(m).ltsymbol, "[%d]", n);
+ for (c = nexttiled(m->clients); c; c = nexttiled(c->next))
+ resize(c, m->wx, m->wy, m->ww - 2 * c->bw, m->wh - 2 * c->bw, 0);
+ }
+@@ -1176,10 +1215,10 @@ movemouse(const Arg *arg)
+ ny = selmon->wy;
+ else if (abs((selmon->wy + selmon->wh) - (ny + HEIGHT(c))) < snap)
+ ny = selmon->wy + selmon->wh - HEIGHT(c);
+- if (!c->isfloating && selmon->lt[selmon->sellt]->arrange
++ if (!c->isfloating && MWS(selmon).lt[MWS(selmon).sellt]->arrange
+ && (abs(nx - c->x) > snap || abs(ny - c->y) > snap))
+ togglefloating(NULL);
+- if (!selmon->lt[selmon->sellt]->arrange || c->isfloating)
++ if (!MWS(selmon).lt[MWS(selmon).sellt]->arrange || c->isfloating)
+ resize(c, nx, ny, c->w, c->h, 1);
+ break;
+ }
+@@ -1325,11 +1364,11 @@ resizemouse(const Arg *arg)
+ if (c->mon->wx + nw >= selmon->wx && c->mon->wx + nw <= selmon->wx + selmon->ww
+ && c->mon->wy + nh >= selmon->wy && c->mon->wy + nh <= selmon->wy + selmon->wh)
+ {
+- if (!c->isfloating && selmon->lt[selmon->sellt]->arrange
++ if (!c->isfloating && MWS(selmon).lt[MWS(selmon).sellt]->arrange
+ && (abs(nw - c->w) > snap || abs(nh - c->h) > snap))
+ togglefloating(NULL);
+ }
+- if (!selmon->lt[selmon->sellt]->arrange || c->isfloating)
++ if (!MWS(selmon).lt[MWS(selmon).sellt]->arrange || c->isfloating)
+ resize(c, c->x, c->y, nw, nh, 1);
+ break;
+ }
+@@ -1354,9 +1393,9 @@ restack(Monitor *m)
+ drawbar(m);
+ if (!m->sel)
+ return;
+- if (m->sel->isfloating || !m->lt[m->sellt]->arrange)
++ if (m->sel->isfloating || !MWS(m).lt[MWS(m).sellt]->arrange)
+ XRaiseWindow(dpy, m->sel->win);
+- if (m->lt[m->sellt]->arrange) {
++ if (MWS(m).lt[MWS(m).sellt]->arrange) {
+ wc.stack_mode = Below;
+ wc.sibling = m->barwin;
+ for (c = m->stack; c; c = c->snext)
+@@ -1416,7 +1455,8 @@ sendmon(Client *c, Monitor *m)
+ detach(c);
+ detachstack(c);
+ c->mon = m;
+- c->tags = m->tagset[m->seltags]; /* assign tags of target monitor */
++ c->tags = MTAGSET(m); /* assign tags of target monitor */
++ c->ws = m->selws;
+ attach(c);
+ attachstack(c);
+ focus(NULL);
+@@ -1500,11 +1540,12 @@ setfullscreen(Client *c, int fullscreen)
+ void
+ setlayout(const Arg *arg)
+ {
+- if (!arg || !arg->v || arg->v != selmon->lt[selmon->sellt])
+- selmon->sellt ^= 1;
++ if (!arg || !arg->v || arg->v != MWS(selmon).lt[MWS(selmon).sellt])
++ MWS(selmon).sellt ^= 1;
+ if (arg && arg->v)
+- selmon->lt[selmon->sellt] = (Layout *)arg->v;
+- strncpy(selmon->ltsymbol, selmon->lt[selmon->sellt]->symbol, sizeof selmon->ltsymbol);
++ MWS(selmon).lt[MWS(selmon).sellt] = (Layout *)arg->v;
++ strncpy(MWS(selmon).ltsymbol, MWS(selmon).lt[MWS(selmon).sellt]->symbol,
++ sizeof MWS(selmon).ltsymbol);
+ if (selmon->sel)
+ arrange(selmon);
+ else
+@@ -1517,7 +1558,7 @@ setmfact(const Arg *arg)
+ {
+ float f;
+
+- if (!arg || !selmon->lt[selmon->sellt]->arrange)
++ if (!arg || !MWS(selmon).lt[MWS(selmon).sellt]->arrange)
+ return;
+ f = arg->f < 1.0 ? arg->f + selmon->mfact : arg->f - 1.0;
+ if (f < 0.1 || f > 0.9)
+@@ -1618,7 +1659,7 @@ showhide(Client *c)
+ if (ISVISIBLE(c)) {
+ /* show clients top down */
+ XMoveWindow(dpy, c->win, c->x, c->y);
+- if ((!c->mon->lt[c->mon->sellt]->arrange || c->isfloating) && !c->isfullscreen)
++ if ((!MWS(c->mon).lt[MWS(c->mon).sellt]->arrange || c->isfloating) && !c->isfullscreen)
+ resize(c, c->x, c->y, c->w, c->h, 0);
+ showhide(c->snext);
+ } else {
+@@ -1670,6 +1711,38 @@ tagmon(const Arg *arg)
+ sendmon(selmon->sel, dirtomon(arg->i));
+ }
+
++/* Move the focused client to the workspace given by arg->i */
++void
++tagws(const Arg *arg)
++{
++ if (!selmon->sel)
++ return;
++ if (arg->i < 0 || arg->i >= (int)LENGTH(workspaces))
++ return;
++ selmon->sel->ws = arg->i;
++ focus(NULL);
++ arrange(selmon);
++}
++
++/* Switch the active workspace on selmon to arg->i */
++void
++viewws(const Arg *arg)
++{
++ if (arg->i < 0 || arg->i >= (int)LENGTH(workspaces))
++ return;
++ if (arg->i == selmon->selws)
++ return;
++
++ /* carry tag selection so the viewed tag does not change */
++ selmon->ws[arg->i].tagset[0] = MWS(selmon).tagset[0];
++ selmon->ws[arg->i].tagset[1] = MWS(selmon).tagset[1];
++ selmon->ws[arg->i].seltags = MWS(selmon).seltags;
++
++ selmon->selws = arg->i;
++ focus(NULL);
++ arrange(selmon);
++}
++
+ void
+ tile(Monitor *m)
+ {
+@@ -1737,10 +1810,10 @@ toggletag(const Arg *arg)
+ void
+ toggleview(const Arg *arg)
+ {
+- unsigned int newtagset = selmon->tagset[selmon->seltags] ^ (arg->ui & TAGMASK);
++ unsigned int newtagset = MTAGSET(selmon) ^ (arg->ui & TAGMASK);
+
+ if (newtagset) {
+- selmon->tagset[selmon->seltags] = newtagset;
++ MWS(selmon).tagset[MWS(selmon).seltags] = newtagset;
+ focus(NULL);
+ arrange(selmon);
+ }
+@@ -2035,11 +2108,11 @@ updatewmhints(Client *c)
+ void
+ view(const Arg *arg)
+ {
+- if ((arg->ui & TAGMASK) == selmon->tagset[selmon->seltags])
++ if ((arg->ui & TAGMASK) == MTAGSET(selmon))
+ return;
+- selmon->seltags ^= 1; /* toggle sel tagset */
++ MWS(selmon).seltags ^= 1; /* toggle sel tagset */
+ if (arg->ui & TAGMASK)
+- selmon->tagset[selmon->seltags] = arg->ui & TAGMASK;
++ MWS(selmon).tagset[MWS(selmon).seltags] = arg->ui & TAGMASK;
+ focus(NULL);
+ arrange(selmon);
+ }
+@@ -2115,7 +2188,7 @@ zoom(const Arg *arg)
+ {
+ Client *c = selmon->sel;
+
+- if (!selmon->lt[selmon->sellt]->arrange
++ if (!MWS(selmon).lt[MWS(selmon).sellt]->arrange
+ || (selmon->sel && selmon->sel->isfloating))
+ return;
+ if (c == nexttiled(selmon->clients))
+--
+2.53.0
+
diff --git a/dwm.suckless.org/patches/workspaces/index.md b/dwm.suckless.org/patches/workspaces/index.md
@@ -0,0 +1,35 @@
+workspaces
+==========
+
+Description
+-----------
+
+This patch adds support for workspaces. Each monitor now has *N*
+independently-named workspaces (defined via the new `workspaces` array in
+`config.h`, like `tags`). Every client belongs to exactly one workspace;
+switching workspaces hides all clients from the previous workspace and shows
+only those from the new one.
+
+Each workspace maintains its own tag view, layout, and layout history
+independently of all others. Switching workspaces carries the current tag
+selection over so the viewed tag does not change.
+
+Default keybinds (where `F<n>` represents function keys):
+
+* `Mod+F<n>`: Switch to workspace *n*.
+* `Mod+Shift+F<n>`: Move focused window to workspace *n*.
+
+The bar shows workspace buttons leftmost, styled identically to tag buttons
+(`SchemeSel` for active, `SchemeNorm` for others). Clicking a workspace button
+switches to it.
+
+
+Download
+--------
+
+* [dwm-workspaces-20260411-23a25b2.diff](dwm-workspaces-20260411-23a25b2.diff)
+
+Authors
+-------
+
+* [8dcc](https://github.com/8dcc) - <8dcc.git@gmail.com>