--- dwm.c.orig	2026-10-20 12:01:23.311268000 -0700
+++ dwm.c	2026-10-20 12:00:57.255660000 -0700
@@ -259,6 +259,11 @@
 	[UnmapNotify] = unmapnotify
 };
 static Atom wmatom[WMLast], netatom[NetLast];
+static Atom dwmkeys;
+static Key *rtkeys;
+static char *rtowned;
+static int nrtkeys, rtused, rthavemod;
+static unsigned int rtnewmod;
 static int running = 1;
 static Cur *cursor[CurLast];
 static Clr **scheme;
@@ -926,8 +931,500 @@
 	text[size - 1] = '\0';
 	XFree(name.value);
 	return 1;
+}
+
+/* Root property _DWM_KEYS. One directive per line:
+ *   modkey Mod4Mask
+ *   bind <mods> <keysym> <func> [arg...]
+ *   unbind <mods> <keysym>
+ *   grab <mods> <keysym>
+ * modkey rewrites compiled bindings and mouse buttons that used MODKEY.
+ * bind adds or replaces a chord. unbind removes one, after that rewrite.
+ * grab takes a chord the table does not already own and runs nothing.
+ * An empty or deleted property restores the compiled table.
+ */
+enum { RT_NONE, RT_INT, RT_UINT, RT_FLOAT, RT_LAYOUT, RT_SPAWN };
+
+static const struct {
+	const char *name;
+	void (*func)(const Arg *);
+	int kind;
+} rtfunc[] = {
+	{ "focusmon",       focusmon,       RT_INT },
+	{ "focusstack",     focusstack,     RT_INT },
+	{ "incnmaster",     incnmaster,     RT_INT },
+	{ "tagmon",         tagmon,         RT_INT },
+	{ "setmfact",       setmfact,       RT_FLOAT },
+	{ "tag",            tag,            RT_UINT },
+	{ "toggletag",      toggletag,      RT_UINT },
+	{ "toggleview",     toggleview,     RT_UINT },
+	{ "view",           view,           RT_UINT },
+	{ "setlayout",      setlayout,      RT_LAYOUT },
+	{ "spawn",          spawn,          RT_SPAWN },
+	{ "killclient",     killclient,     RT_NONE },
+	{ "quit",           quit,           RT_NONE },
+	{ "togglebar",      togglebar,      RT_NONE },
+	{ "togglefloating", togglefloating, RT_NONE },
+	{ "zoom",           zoom,           RT_NONE },
+};
+
+static unsigned int
+rtmask(unsigned int mod)
+{
+	if (!rtused || !rthavemod || !(mod & MODKEY))
+		return mod;
+	return (mod & ~((unsigned int)MODKEY)) | rtnewmod;
 }
 
+static void
+rtfreeone(int i)
+{
+	char **v;
+	int j;
+
+	if (!rtowned || !rtowned[i] || !rtkeys[i].arg.v)
+		return;
+	v = (char **)rtkeys[i].arg.v;
+	for (j = 0; v[j]; j++)
+		free(v[j]);
+	free(v);
+	rtowned[i] = 0;
+}
+
+static void
+rtput(int i, unsigned int mod, KeySym ks, void (*func)(const Arg *), Arg arg)
+{
+	Key k = { mod, ks, func, arg };
+
+	/* Key.arg is const. Initialize a temporary and copy it in. */
+	memcpy(&rtkeys[i], &k, sizeof(k));
+}
+
+static void
+rtclear(void)
+{
+	int i;
+
+	if (rtkeys && rtowned) {
+		for (i = 0; i < nrtkeys; i++)
+			rtfreeone(i);
+	}
+	free(rtkeys);
+	free(rtowned);
+	rtkeys = NULL;
+	rtowned = NULL;
+	nrtkeys = 0;
+	rtused = 0;
+	rthavemod = 0;
+}
+
+static void
+rtseed(unsigned int newmod, int havemod)
+{
+	int i;
+
+	rtclear();
+	nrtkeys = LENGTH(keys);
+	rtkeys = ecalloc(nrtkeys, sizeof(Key));
+	rtowned = ecalloc(nrtkeys, sizeof(char));
+	for (i = 0; i < nrtkeys; i++) {
+		memcpy(&rtkeys[i], &keys[i], sizeof(keys[i]));
+		if (havemod && (keys[i].mod & MODKEY))
+			rtkeys[i].mod = (keys[i].mod & ~((unsigned int)MODKEY)) | newmod;
+	}
+	rtused = 1;
+	rthavemod = havemod;
+	rtnewmod = newmod;
+}
+
+static int
+rtadd(void)
+{
+	int i;
+
+	i = nrtkeys++;
+	if (!(rtkeys = realloc(rtkeys, nrtkeys * sizeof(Key))))
+		die("realloc:");
+	if (!(rtowned = realloc(rtowned, nrtkeys * sizeof(char))))
+		die("realloc:");
+	memset(&rtkeys[i], 0, sizeof(Key));
+	rtowned[i] = 0;
+	return i;
+}
+
+static void
+rtdel(int i)
+{
+	rtfreeone(i);
+	nrtkeys--;
+	memmove(&rtkeys[i], &rtkeys[i + 1], (nrtkeys - i) * sizeof(Key));
+	memmove(&rtowned[i], &rtowned[i + 1], (nrtkeys - i) * sizeof(char));
+}
+
+static int
+rtfind(unsigned int mod, KeySym ks)
+{
+	int i;
+
+	for (i = 0; i < nrtkeys; i++)
+		if (rtkeys[i].keysym == ks &&
+		    CLEANMASK(rtkeys[i].mod) == CLEANMASK(mod))
+			return i;
+	return -1;
+}
+
+static int
+modbit(const char *s, unsigned int *out)
+{
+	static const struct {
+		const char *n;
+		unsigned int m;
+	} tab[] = {
+		{ "ShiftMask",   ShiftMask },
+		{ "LockMask",    LockMask },
+		{ "ControlMask", ControlMask },
+		{ "Mod1Mask",    Mod1Mask },
+		{ "Mod2Mask",    Mod2Mask },
+		{ "Mod3Mask",    Mod3Mask },
+		{ "Mod4Mask",    Mod4Mask },
+		{ "Mod5Mask",    Mod5Mask },
+		{ "None",        0 },
+		{ "0",           0 },
+	};
+	unsigned int i;
+
+	for (i = 0; i < LENGTH(tab); i++)
+		if (!strcmp(s, tab[i].n)) {
+			*out = tab[i].m;
+			return 1;
+		}
+	return 0;
+}
+
+static int
+parsemods(char *s, unsigned int *out)
+{
+	unsigned int m, bit;
+	char *q;
+
+	m = 0;
+	if (!strcmp(s, "0") || !strcmp(s, "None")) {
+		*out = 0;
+		return 1;
+	}
+	while (s && *s) {
+		if ((q = strchr(s, '|')))
+			*q++ = '\0';
+		if (!modbit(s, &bit))
+			return 0;
+		m |= bit;
+		s = q;
+	}
+	*out = m;
+	return 1;
+}
+
+static KeySym
+parsekey(const char *s)
+{
+	if (!strncmp(s, "XK_", 3))
+		s += 3;
+	return XStringToKeysym(s);
+}
+
+static char *
+nexttok(char **sp)
+{
+	char *s, *e;
+
+	s = *sp;
+	while (*s == ' ' || *s == '\t')
+		s++;
+	if (!*s)
+		return NULL;
+	e = s;
+	while (*e && *e != ' ' && *e != '\t')
+		e++;
+	if (*e) {
+		*e = '\0';
+		*sp = e + 1;
+	} else
+		*sp = e;
+	return s;
+}
+
+static int
+parseint(const char *s, int *out)
+{
+	char *end;
+	long v;
+
+	errno = 0;
+	v = strtol(s, &end, 0);
+	if (!*s || *end || errno)
+		return 0;
+	*out = (int)v;
+	return 1;
+}
+
+static int
+parseuint(const char *s, unsigned int *out)
+{
+	char *end;
+	unsigned long v;
+
+	if (!strcmp(s, "~0")) {
+		*out = ~0U;
+		return 1;
+	}
+	errno = 0;
+	v = strtoul(s, &end, 0);
+	if (!*s || *end || errno)
+		return 0;
+	*out = (unsigned int)v;
+	return 1;
+}
+
+static int
+parsefloat(const char *s, float *out)
+{
+	char *end;
+	double v;
+
+	errno = 0;
+	v = strtod(s, &end);
+	if (!*s || *end || errno)
+		return 0;
+	*out = (float)v;
+	return 1;
+}
+
+static char **
+parseargv(char *s)
+{
+	char *tmp[32], *tok;
+	char **v;
+	int n, i;
+
+	n = 0;
+	while (n < 31 && (tok = nexttok(&s)))
+		tmp[n++] = tok;
+	if (!n)
+		return NULL;
+	v = ecalloc(n + 1, sizeof(char *));
+	for (i = 0; i < n; i++)
+		if (!(v[i] = strdup(tmp[i])))
+			die("strdup:");
+	return v;
+}
+
+static int
+rtapplyline(char *line)
+{
+	char *p, *cmd, *ms, *ks, *fn, *a;
+	unsigned int mod;
+	KeySym sym;
+	int i, fi, iv, layout;
+	unsigned int uv;
+	float fv;
+	Arg arg;
+
+	p = line;
+	if (!(cmd = nexttok(&p)))
+		return 0;
+	if (!strcmp(cmd, "modkey") || *cmd == '#')
+		return 0;
+	if (!(ms = nexttok(&p)) || !(ks = nexttok(&p)))
+		return 0;
+	if (!parsemods(ms, &mod))
+		return 0;
+	if ((sym = parsekey(ks)) == NoSymbol)
+		return 0;
+	if (!strcmp(cmd, "unbind")) {
+		if ((i = rtfind(mod, sym)) >= 0)
+			rtdel(i);
+		return 1;
+	}
+	if (!strcmp(cmd, "grab")) {
+		if (rtfind(mod, sym) >= 0)
+			return 1;
+		i = rtadd();
+		memset(&arg, 0, sizeof(arg));
+		rtput(i, mod, sym, NULL, arg);
+		return 1;
+	}
+	if (strcmp(cmd, "bind"))
+		return 0;
+	if (!(fn = nexttok(&p)))
+		return 0;
+	for (fi = 0; fi < (int)LENGTH(rtfunc); fi++)
+		if (!strcmp(fn, rtfunc[fi].name))
+			break;
+	if (fi == (int)LENGTH(rtfunc))
+		return 0;
+	memset(&arg, 0, sizeof(arg));
+	switch (rtfunc[fi].kind) {
+	case RT_NONE:
+		break;
+	case RT_INT:
+		if (!(a = nexttok(&p)) || !parseint(a, &iv))
+			return 0;
+		arg.i = iv;
+		break;
+	case RT_UINT:
+		if (!(a = nexttok(&p)) || !parseuint(a, &uv))
+			return 0;
+		arg.ui = uv;
+		break;
+	case RT_FLOAT:
+		if (!(a = nexttok(&p)) || !parsefloat(a, &fv))
+			return 0;
+		arg.f = fv;
+		break;
+	case RT_LAYOUT:
+		if (!(a = nexttok(&p)))
+			return 0;
+		if (!strcmp(a, "toggle"))
+			arg.v = NULL;
+		else {
+			if (!parseint(a, &layout) || layout < 0 ||
+			    layout >= (int)LENGTH(layouts))
+				return 0;
+			arg.v = &layouts[layout];
+		}
+		break;
+	case RT_SPAWN:
+		if (!(arg.v = parseargv(p)))
+			return 0;
+		break;
+	}
+	if ((i = rtfind(mod, sym)) < 0)
+		i = rtadd();
+	else
+		rtfreeone(i);
+	rtput(i, mod, sym, rtfunc[fi].func, arg);
+	if (rtfunc[fi].kind == RT_SPAWN)
+		rtowned[i] = 1;
+	return 1;
+}
+
+static int
+rtmodkey(char *line, unsigned int *mod)
+{
+	char buf[128];
+	char *p, *cmd, *arg;
+	size_t n;
+
+	/* Copy: parsing writes NULs, and the line is applied again. */
+	n = strlen(line);
+	if (n >= sizeof(buf))
+		return 0;
+	memcpy(buf, line, n + 1);
+	p = buf;
+	if (!(cmd = nexttok(&p)) || strcmp(cmd, "modkey"))
+		return 0;
+	if (!(arg = nexttok(&p)) || !parsemods(arg, mod))
+		return 0;
+	return 1;
+}
+
+static char *
+rtgetprop(void)
+{
+	Atom actual;
+	int format;
+	unsigned long n, extra;
+	unsigned char *data;
+	char *s;
+
+	data = NULL;
+	if (XGetWindowProperty(dpy, root, dwmkeys, 0, 65536, False,
+	    AnyPropertyType, &actual, &format, &n, &extra, &data) != Success)
+		return NULL;
+	if (extra || !data || format != 8 || n == 0) {
+		if (data)
+			XFree(data);
+		return NULL;
+	}
+	s = ecalloc(n + 1, 1);
+	memcpy(s, data, n);
+	XFree(data);
+	return s;
+}
+
+static char **
+splitlines(char *text, int *nout)
+{
+	char **lines;
+	char *p, *nl;
+	int n;
+	size_t len;
+
+	lines = NULL;
+	n = 0;
+	p = text;
+	while (*p) {
+		nl = strchr(p, '\n');
+		if (nl)
+			*nl = '\0';
+		len = strlen(p);
+		if (len && p[len - 1] == '\r')
+			p[--len] = '\0';
+		while (*p == ' ' || *p == '\t')
+			p++;
+		if (*p && *p != '#') {
+			if (!(lines = realloc(lines, (n + 1) * sizeof(char *))))
+				die("realloc:");
+			lines[n++] = p;
+		}
+		if (!nl)
+			break;
+		p = nl + 1;
+	}
+	*nout = n;
+	return lines;
+}
+
+static void
+rtregrab(void)
+{
+	Monitor *m;
+	Client *c;
+
+	for (m = mons; m; m = m->next)
+		for (c = m->clients; c; c = c->next)
+			grabbuttons(c, selmon && c == selmon->sel);
+}
+
+static void
+updateruntimekeys(void)
+{
+	char *text, **lines;
+	int i, n, any, havemod;
+	unsigned int newmod;
+
+	text = rtgetprop();
+	if (!text) {
+		rtclear();
+		return;
+	}
+	lines = splitlines(text, &n);
+	newmod = 0;
+	havemod = 0;
+	for (i = 0; i < n; i++)
+		if (rtmodkey(lines[i], &newmod))
+			havemod = 1;
+	rtseed(newmod, havemod);
+	any = 0;
+	for (i = 0; i < n; i++)
+		if (rtapplyline(lines[i]))
+			any = 1;
+	if (!any && !havemod)
+		rtclear();
+	free(lines);
+	free(text);
+}
+
 void
 grabbuttons(Client *c, int focused)
 {
@@ -943,7 +1440,7 @@
 			if (buttons[i].click == ClkClientWin)
 				for (j = 0; j < LENGTH(modifiers); j++)
 					XGrabButton(dpy, buttons[i].button,
-						buttons[i].mask | modifiers[j],
+						rtmask(buttons[i].mask) | modifiers[j],
 						c->win, False, BUTTONMASK,
 						GrabModeAsync, GrabModeSync, None, None);
 	}
@@ -954,23 +1451,26 @@
 {
 	updatenumlockmask();
 	{
-		unsigned int i, j, k;
+		unsigned int i, j, k, nuse;
 		unsigned int modifiers[] = { 0, LockMask, numlockmask, numlockmask|LockMask };
 		int start, end, skip;
 		KeySym *syms;
+		const Key *use;
 
 		XUngrabKey(dpy, AnyKey, AnyModifier, root);
 		XDisplayKeycodes(dpy, &start, &end);
 		syms = XGetKeyboardMapping(dpy, start, end - start + 1, &skip);
 		if (!syms)
 			return;
+		use = rtused ? rtkeys : keys;
+		nuse = rtused ? (unsigned int)nrtkeys : LENGTH(keys);
 		for (k = start; k <= end; k++)
-			for (i = 0; i < LENGTH(keys); i++)
+			for (i = 0; i < nuse; i++)
 				/* skip modifier codes, we do that ourselves */
-				if (keys[i].keysym == syms[(k - start) * skip])
+				if (use[i].keysym == syms[(k - start) * skip])
 					for (j = 0; j < LENGTH(modifiers); j++)
 						XGrabKey(dpy, k,
-							 keys[i].mod | modifiers[j],
+							 use[i].mod | modifiers[j],
 							 root, True,
 							 GrabModeAsync, GrabModeAsync);
 		XFree(syms);
@@ -999,17 +1499,20 @@
 void
 keypress(XEvent *e)
 {
-	unsigned int i;
+	unsigned int i, nuse;
 	KeySym keysym;
 	XKeyEvent *ev;
+	const Key *use;
 
 	ev = &e->xkey;
 	keysym = XKeycodeToKeysym(dpy, (KeyCode)ev->keycode, 0);
-	for (i = 0; i < LENGTH(keys); i++)
-		if (keysym == keys[i].keysym
-		&& CLEANMASK(keys[i].mod) == CLEANMASK(ev->state)
-		&& keys[i].func)
-			keys[i].func(&(keys[i].arg));
+	use = rtused ? rtkeys : keys;
+	nuse = rtused ? (unsigned int)nrtkeys : LENGTH(keys);
+	for (i = 0; i < nuse; i++)
+		if (keysym == use[i].keysym
+		&& CLEANMASK(use[i].mod) == CLEANMASK(ev->state)
+		&& use[i].func)
+			use[i].func(&(use[i].arg));
 }
 
 void
@@ -1225,6 +1728,12 @@
 	Window trans;
 	XPropertyEvent *ev = &e->xproperty;
 
+	if (ev->window == root && ev->atom == dwmkeys) {
+		updateruntimekeys();
+		grabkeys();
+		rtregrab();
+		return;
+	}
 	if ((ev->window == root) && (ev->atom == XA_WM_NAME))
 		updatestatus();
 	else if (ev->state == PropertyDelete)
@@ -1579,6 +2088,7 @@
 	netatom[NetWMWindowType] = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE", False);
 	netatom[NetWMWindowTypeDialog] = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_DIALOG", False);
 	netatom[NetClientList] = XInternAtom(dpy, "_NET_CLIENT_LIST", False);
+	dwmkeys = XInternAtom(dpy, "_DWM_KEYS", False);
 	/* init cursors */
 	cursor[CurNormal] = drw_cur_create(drw, XC_left_ptr);
 	cursor[CurResize] = drw_cur_create(drw, XC_sizing);
@@ -1609,6 +2119,7 @@
 		|LeaveWindowMask|StructureNotifyMask|PropertyChangeMask;
 	XChangeWindowAttributes(dpy, root, CWEventMask|CWCursor, &wa);
 	XSelectInput(dpy, root, wa.event_mask);
+	updateruntimekeys();
 	grabkeys();
 	focus(NULL);
 }
--- dwm.1.orig	2026-10-20 12:01:23.312679000 -0700
+++ dwm.1	2026-10-20 12:01:23.292444000 -0700
@@ -155,6 +155,37 @@
 .SH CUSTOMIZATION
 dwm is customized by creating a custom config.h and (re)compiling the source
 code. This keeps it fast, secure and simple.
+.P
+The root window property
+.B _DWM_KEYS
+is read again when it changes.
+An empty value, or deleting the property, restores the compiled key table.
+Each line is one directive.
+.B modkey
+followed by a modifier mask
+.RB ( "modkey Mod4Mask" )
+rewrites every compiled binding and mouse button that used MODKEY.
+.B bind
+adds or replaces one chord
+.RB ( "bind Mod4Mask XK_j focusstack 1" ).
+.B spawn
+takes the rest of the line as its argument vector.
+.BR view ,
+.BR tag ,
+.BR toggleview ,
+and
+.B toggletag
+take a tag mask such as 1, 2, 4, or ~0.
+.B setlayout
+takes a layout index or
+.BR toggle .
+.B unbind
+removes one chord, matched after
+.B modkey
+has been applied.
+.B grab
+grabs a chord and runs nothing, so the focused client does not see the key.
+A chord the table already owns is left alone.
 .SH SEE ALSO
 .BR dmenu (1),
 .BR st (1)
