commit d04f7dbb443ceb41780de6e2b41ccc8991e47d08
parent 59e2e320b795855a3cb6ac0df55493fff1acfe91
Author: Zach Rice <bynxmusic@gmail.com>
Date: Mon, 18 May 2026 21:09:26 -0400
Add client fullscreen support and togglebar
The changes are:
- Client-initiated fullscreen (via ClientMessage interception on root + property fallback)
- setfullscreen() / togglebar() functions
- Event filtering fixes (createnotify, destroynotify, unmapnotify) to prevent root events from mismanaging windows
- EWMH-correct event masks on XSendEvent calls
- Mod+b togglebar keybind
Diffstat:
3 files changed, 255 insertions(+), 11 deletions(-)
diff --git a/config.def.h b/config.def.h
@@ -63,4 +63,5 @@ static const Key keys[] = {
{ MODKEY|ShiftMask, XK_u, toggle, { .v = (void*) &urgentswitch } },
{ 0, XK_F11, fullscreen, { 0 } },
+ { MODKEY, XK_b, togglebar, { 0 } },
};
diff --git a/patches/tabbed-client-fullscreen.diff b/patches/tabbed-client-fullscreen.diff
@@ -0,0 +1,144 @@
+diff --git a/config.def.h b/config.def.h
+index 51bb13d..30ad1a2 100644
+--- a/config.def.h
++++ b/config.def.h
+@@ -63,4 +63,5 @@ static const Key keys[] = {
+ { MODKEY|ShiftMask, XK_u, toggle, { .v = (void*) &urgentswitch } },
+
+ { 0, XK_F11, fullscreen, { 0 } },
++ { MODKEY, XK_b, togglebar, { 0 } },
+ };
+diff --git a/tabbed.c b/tabbed.c
+index 2fb5aa7..cf97cc1 100644
+--- a/tabbed.c
++++ b/tabbed.c
+@@ -125,10 +125,12 @@ static void rotate(const Arg *arg);
+ static void run(void);
+ static void sendxembed(int c, long msg, long detail, long d1, long d2);
+ static void setcmd(int argc, char *argv[], int);
++static void setfullscreen(Bool fs);
+ static void setup(void);
+ static void spawn(const Arg *arg);
+ static int textnw(const char *text, unsigned int len);
+ static void toggle(const Arg *arg);
++static void togglebar(const Arg *arg);
+ static void unmanage(int c);
+ static void unmapnotify(const XEvent *e);
+ static void updatenumlockmask(void);
+@@ -156,7 +158,7 @@ static int bh, obh, wx, wy, ww, wh;
+ static unsigned int numlockmask;
+ static Bool running = True, nextfocus, doinitspawn = True,
+ fillagain = False, closelastclient = False,
+- killclientsfirst = False;
++ killclientsfirst = False, isfullscreen = False;
+ static Display *dpy;
+ static DC dc;
+ static Atom wmatom[WMLast];
+@@ -257,12 +259,14 @@ configurenotify(const XEvent *e)
+ dc.drawable = XCreatePixmap(dpy, root, ww, wh,
+ DefaultDepth(dpy, screen));
+
+- if (!obh && (wh <= bh)) {
+- obh = bh;
+- bh = 0;
+- } else if (!bh && (wh > obh)) {
+- bh = obh;
+- obh = 0;
++ if (!isfullscreen) {
++ if (!obh && (wh <= bh)) {
++ obh = bh;
++ bh = 0;
++ } else if (!bh && (wh > obh)) {
++ bh = obh;
++ obh = 0;
++ }
+ }
+
+ if (sel > -1)
+@@ -883,6 +887,40 @@ propertynotify(const XEvent *e)
+ } else if (ev->state != PropertyDelete && ev->atom == XA_WM_NAME &&
+ (c = getclient(ev->window)) > -1) {
+ updatetitle(c);
++ } else if (ev->atom == wmatom[WMState] &&
++ (c = getclient(ev->window)) > -1) {
++ Atom type;
++ int fmt;
++ unsigned long nitems, after;
++ Atom *states = NULL;
++ Bool wantfs = False;
++ unsigned long i;
++
++ if (XGetWindowProperty(dpy, clients[c]->win, wmatom[WMState],
++ 0L, 32L, False, XA_ATOM, &type, &fmt, &nitems, &after,
++ (unsigned char **)&states) == Success && states) {
++ for (i = 0; i < nitems; i++) {
++ if (states[i] == wmatom[WMFullscreen]) {
++ wantfs = True;
++ break;
++ }
++ }
++ XFree(states);
++ }
++ if (wantfs != isfullscreen) {
++ isfullscreen = wantfs;
++ if (isfullscreen) {
++ obh = bh;
++ bh = 0;
++ } else {
++ bh = obh;
++ obh = 0;
++ }
++ if (sel > -1)
++ resize(sel, ww, wh - bh);
++ drawbar();
++ setfullscreen(isfullscreen);
++ }
+ }
+ }
+
+@@ -981,6 +1019,21 @@ setcmd(int argc, char *argv[], int replace)
+ cmd[cmd_append_pos] = cmd[cmd_append_pos + 1] = NULL;
+ }
+
++void
++setfullscreen(Bool fs)
++{
++ XEvent e;
++
++ e.type = ClientMessage;
++ e.xclient.window = win;
++ e.xclient.message_type = wmatom[WMState];
++ e.xclient.format = 32;
++ e.xclient.data.l[0] = fs ? 1 : 0;
++ e.xclient.data.l[1] = wmatom[WMFullscreen];
++ e.xclient.data.l[2] = 0;
++ XSendEvent(dpy, root, False, SubstructureNotifyMask, &e);
++}
++
+ void
+ setup(void)
+ {
+@@ -1141,6 +1194,23 @@ toggle(const Arg *arg)
+ *(Bool*) arg->v = !*(Bool*) arg->v;
+ }
+
++void
++togglebar(const Arg *arg)
++{
++ if (bh) {
++ obh = bh;
++ bh = 0;
++ } else {
++ bh = obh;
++ obh = 0;
++ }
++
++ if (sel > -1)
++ resize(sel, ww, wh - bh);
++
++ drawbar();
++}
++
+ void
+ unmanage(int c)
+ {
diff --git a/tabbed.c b/tabbed.c
@@ -126,10 +126,12 @@ static void rotate(const Arg *arg);
static void run(void);
static void sendxembed(int c, long msg, long detail, long d1, long d2);
static void setcmd(int argc, char *argv[], int);
+static void setfullscreen(Bool fs);
static void setup(void);
static void spawn(const Arg *arg);
static int textnw(const char *text, unsigned int len);
static void toggle(const Arg *arg);
+static void togglebar(const Arg *arg);
static void unmanage(int c);
static void unmapnotify(const XEvent *e);
static void updatenumlockmask(void);
@@ -158,7 +160,7 @@ static int bh, obh, wx, wy, ww, wh;
static unsigned int numlockmask;
static Bool running = True, nextfocus, doinitspawn = True,
fillagain = False, closelastclient = False,
- killclientsfirst = False;
+ killclientsfirst = False, isfullscreen = False;
static Display *dpy;
static DC dc;
static Atom wmatom[WMLast];
@@ -271,6 +273,7 @@ void
clientmessage(const XEvent *e)
{
const XClientMessageEvent *ev = &e->xclient;
+ int c;
if (ev->message_type == wmatom[WMProtocols] &&
ev->data.l[0] == wmatom[WMDelete]) {
@@ -279,6 +282,31 @@ clientmessage(const XEvent *e)
return;
}
running = False;
+ } else if (ev->message_type == wmatom[WMState] &&
+ ((Atom)ev->data.l[1] == wmatom[WMFullscreen] ||
+ (Atom)ev->data.l[2] == wmatom[WMFullscreen]) &&
+ (c = getclient(ev->window)) > -1) {
+ Bool wantfs;
+ if (ev->data.l[0] == 0)
+ wantfs = False;
+ else if (ev->data.l[0] == 1)
+ wantfs = True;
+ else
+ wantfs = !isfullscreen;
+ if (wantfs != isfullscreen) {
+ isfullscreen = wantfs;
+ if (isfullscreen) {
+ obh = bh;
+ bh = 0;
+ } else {
+ bh = obh;
+ obh = 0;
+ }
+ if (sel > -1)
+ resize(sel, ww, wh - bh);
+ drawbar();
+ setfullscreen(isfullscreen);
+ }
}
}
@@ -294,12 +322,14 @@ configurenotify(const XEvent *e)
dc.drawable = XCreatePixmap(dpy, root, ww, wh,
DefaultDepth(dpy, screen));
- if (!obh && (wh <= bh)) {
- obh = bh;
- bh = 0;
- } else if (!bh && (wh > obh)) {
- bh = obh;
- obh = 0;
+ if (!isfullscreen) {
+ if (!obh && (wh <= bh)) {
+ obh = bh;
+ bh = 0;
+ } else if (!bh && (wh > obh)) {
+ bh = obh;
+ obh = 0;
+ }
}
if (sel > -1)
@@ -332,7 +362,7 @@ createnotify(const XEvent *e)
{
const XCreateWindowEvent *ev = &e->xcreatewindow;
- if (ev->window != win && getclient(ev->window) < 0)
+ if (ev->window != win && ev->parent == win && getclient(ev->window) < 0)
manage(ev->window);
}
@@ -342,7 +372,7 @@ destroynotify(const XEvent *e)
const XDestroyWindowEvent *ev = &e->xdestroywindow;
int c;
- if ((c = getclient(ev->window)) > -1)
+ if (ev->event == win && (c = getclient(ev->window)) > -1)
unmanage(c);
}
@@ -583,7 +613,8 @@ fullscreen(const Arg *arg)
e.xclient.data.l[0] = 2;
e.xclient.data.l[1] = wmatom[WMFullscreen];
e.xclient.data.l[2] = 0;
- XSendEvent(dpy, root, False, SubstructureNotifyMask, &e);
+ XSendEvent(dpy, root, False,
+ SubstructureNotifyMask | SubstructureRedirectMask, &e);
}
char *
@@ -920,6 +951,40 @@ propertynotify(const XEvent *e)
} else if (ev->state != PropertyDelete && ev->atom == XA_WM_NAME &&
(c = getclient(ev->window)) > -1) {
updatetitle(c);
+ } else if (ev->atom == wmatom[WMState] &&
+ (c = getclient(ev->window)) > -1) {
+ Atom type;
+ int fmt;
+ unsigned long nitems, after;
+ Atom *states = NULL;
+ Bool wantfs = False;
+ unsigned long i;
+
+ if (XGetWindowProperty(dpy, clients[c]->win, wmatom[WMState],
+ 0L, 32L, False, XA_ATOM, &type, &fmt, &nitems, &after,
+ (unsigned char **)&states) == Success && states) {
+ for (i = 0; i < nitems; i++) {
+ if (states[i] == wmatom[WMFullscreen]) {
+ wantfs = True;
+ break;
+ }
+ }
+ XFree(states);
+ }
+ if (wantfs != isfullscreen) {
+ isfullscreen = wantfs;
+ if (isfullscreen) {
+ obh = bh;
+ bh = 0;
+ } else {
+ bh = obh;
+ obh = 0;
+ }
+ if (sel > -1)
+ resize(sel, ww, wh - bh);
+ drawbar();
+ setfullscreen(isfullscreen);
+ }
}
}
@@ -1019,6 +1084,22 @@ setcmd(int argc, char *argv[], int replace)
}
void
+setfullscreen(Bool fs)
+{
+ XEvent e;
+
+ e.type = ClientMessage;
+ e.xclient.window = win;
+ e.xclient.message_type = wmatom[WMState];
+ e.xclient.format = 32;
+ e.xclient.data.l[0] = fs ? 1 : 0;
+ e.xclient.data.l[1] = wmatom[WMFullscreen];
+ e.xclient.data.l[2] = 0;
+ XSendEvent(dpy, root, False,
+ SubstructureNotifyMask | SubstructureRedirectMask, &e);
+}
+
+void
setup(void)
{
int bitm, tx, ty, tw, th, dh, dw, isfixed;
@@ -1103,6 +1184,7 @@ setup(void)
ButtonPressMask | ExposureMask | KeyPressMask |
PropertyChangeMask | StructureNotifyMask |
SubstructureRedirectMask | ButtonMotionMask);
+ XSelectInput(dpy, root, SubstructureNotifyMask);
xerrorxlib = XSetErrorHandler(xerror);
class_hint.res_name = wmname;
@@ -1179,6 +1261,23 @@ toggle(const Arg *arg)
}
void
+togglebar(const Arg *arg)
+{
+ if (bh) {
+ obh = bh;
+ bh = 0;
+ } else {
+ bh = obh;
+ obh = 0;
+ }
+
+ if (sel > -1)
+ resize(sel, ww, wh - bh);
+
+ drawbar();
+}
+
+void
unmanage(int c)
{
if (c < 0 || c >= nclients) {
@@ -1243,7 +1342,7 @@ unmapnotify(const XEvent *e)
const XUnmapEvent *ev = &e->xunmap;
int c;
- if ((c = getclient(ev->window)) > -1)
+ if (ev->event == win && (c = getclient(ev->window)) > -1)
unmanage(c);
}