tabbed

tabbed
git clone git@git.zachrice.app:repos/tabbed.git
Log | Files | Refs | README | LICENSE

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:
Mconfig.def.h | 1+
Apatches/tabbed-client-fullscreen.diff | 144+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtabbed.c | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
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); }