sites

public wiki contents of suckless.org
git clone git://git.suckless.org/sites
Log | Files | Refs

dwm-dynamicswallow-20240320-061e9fe.diff (29463B)


      1 From 20175007156c243ee577e0aa518a05282950e766 Mon Sep 17 00:00:00 2001
      2 From: visil <workregor@mail.ru>
      3 Date: Wed, 20 Mar 2024 17:48:47 +0300
      4 Subject: [PATCH] Dynamic swallowing patch for 6.5
      5 
      6 ---
      7  Makefile     |   3 +
      8  config.def.h |   6 +
      9  config.mk    |   4 +-
     10  dwm.c        | 588 +++++++++++++++++++++++++++++++++++++++++++++++++--
     11  dwmswallow   | 120 +++++++++++
     12  util.c       |  29 +++
     13  util.h       |   1 +
     14  7 files changed, 728 insertions(+), 23 deletions(-)
     15  create mode 100644 dwmswallow
     16 
     17 diff --git a/Makefile b/Makefile
     18 index ffa69b4..67aa239 100644
     19 --- a/Makefile
     20 +++ b/Makefile
     21 @@ -34,12 +34,15 @@ install: all
     22  	mkdir -p ${DESTDIR}${PREFIX}/bin
     23  	cp -f dwm ${DESTDIR}${PREFIX}/bin
     24  	chmod 755 ${DESTDIR}${PREFIX}/bin/dwm
     25 +	cp -f dwmswallow ${DESTDIR}${PREFIX}/bin
     26 +	chmod 755 ${DESTDIR}${PREFIX}/bin/dwmswallow
     27  	mkdir -p ${DESTDIR}${MANPREFIX}/man1
     28  	sed "s/VERSION/${VERSION}/g" < dwm.1 > ${DESTDIR}${MANPREFIX}/man1/dwm.1
     29  	chmod 644 ${DESTDIR}${MANPREFIX}/man1/dwm.1
     30  
     31  uninstall:
     32  	rm -f ${DESTDIR}${PREFIX}/bin/dwm\
     33 +		${DESTDIR}${MANPREFIX}/bin/dwmswallow\
     34  		${DESTDIR}${MANPREFIX}/man1/dwm.1
     35  
     36  .PHONY: all clean dist install uninstall
     37 diff --git a/config.def.h b/config.def.h
     38 index 9efa774..bf94c30 100644
     39 --- a/config.def.h
     40 +++ b/config.def.h
     41 @@ -30,6 +30,10 @@ static const Rule rules[] = {
     42  	{ "Gimp",     NULL,       NULL,       0,            1,           -1 },
     43  	{ "Firefox",  NULL,       NULL,       1 << 8,       0,           -1 },
     44  };
     45 +/* window swallowing */
     46 +static const int swaldecay = 3;
     47 +static const int swalretroactive = 1;
     48 +static const char swalsymbol[] = "👅";
     49  
     50  /* layout(s) */
     51  static const float mfact     = 0.55; /* factor of master area size [0.05..0.95] */
     52 @@ -85,6 +89,7 @@ static const Key keys[] = {
     53  	{ MODKEY,                       XK_period, focusmon,       {.i = +1 } },
     54  	{ MODKEY|ShiftMask,             XK_comma,  tagmon,         {.i = -1 } },
     55  	{ MODKEY|ShiftMask,             XK_period, tagmon,         {.i = +1 } },
     56 +  { MODKEY,                       XK_u,      swalstopsel,    {0} },
     57  	TAGKEYS(                        XK_1,                      0)
     58  	TAGKEYS(                        XK_2,                      1)
     59  	TAGKEYS(                        XK_3,                      2)
     60 @@ -108,6 +113,7 @@ static const Button buttons[] = {
     61  	{ ClkClientWin,         MODKEY,         Button1,        movemouse,      {0} },
     62  	{ ClkClientWin,         MODKEY,         Button2,        togglefloating, {0} },
     63  	{ ClkClientWin,         MODKEY,         Button3,        resizemouse,    {0} },
     64 +  { ClkClientWin,         MODKEY|ShiftMask, Button1,      swalmouse,      {0} },
     65  	{ ClkTagBar,            0,              Button1,        view,           {0} },
     66  	{ ClkTagBar,            0,              Button3,        toggleview,     {0} },
     67  	{ ClkTagBar,            MODKEY,         Button1,        tag,            {0} },
     68 diff --git a/config.mk b/config.mk
     69 index 8efca9a..6a1883c 100644
     70 --- a/config.mk
     71 +++ b/config.mk
     72 @@ -27,8 +27,8 @@ LIBS = -L${X11LIB} -lX11 ${XINERAMALIBS} ${FREETYPELIBS}
     73  
     74  # flags
     75  CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700L -DVERSION=\"${VERSION}\" ${XINERAMAFLAGS}
     76 -#CFLAGS   = -g -std=c99 -pedantic -Wall -O0 ${INCS} ${CPPFLAGS}
     77 -CFLAGS   = -std=c99 -pedantic -Wall -Wno-deprecated-declarations -Os ${INCS} ${CPPFLAGS}
     78 +CFLAGS   = -g -std=c99 -pedantic -Wall -O0 ${INCS} ${CPPFLAGS}
     79 +#CFLAGS   = -std=c99 -pedantic -Wall -Wno-deprecated-declarations -Os ${INCS} ${CPPFLAGS}
     80  LDFLAGS  = ${LIBS}
     81  
     82  # Solaris
     83 diff --git a/dwm.c b/dwm.c
     84 index f1d86b2..c01cf41 100644
     85 --- a/dwm.c
     86 +++ b/dwm.c
     87 @@ -58,7 +58,7 @@
     88  #define TEXTW(X)                (drw_fontset_getwidth(drw, (X)) + lrpad)
     89  
     90  /* enums */
     91 -enum { CurNormal, CurResize, CurMove, CurLast }; /* cursor */
     92 +enum { CurNormal, CurResize, CurMove, CurSwal, CurLast }; /* cursor */
     93  enum { SchemeNorm, SchemeSel }; /* color schemes */
     94  enum { NetSupported, NetWMName, NetWMState, NetWMCheck,
     95         NetWMFullscreen, NetActiveWindow, NetWMWindowType,
     96 @@ -66,6 +66,7 @@ enum { NetSupported, NetWMName, NetWMState, NetWMCheck,
     97  enum { WMProtocols, WMDelete, WMState, WMTakeFocus, WMLast }; /* default atoms */
     98  enum { ClkTagBar, ClkLtSymbol, ClkStatusText, ClkWinTitle,
     99         ClkClientWin, ClkRootWin, ClkLast }; /* clicks */
    100 +enum { ClientRegular = 1, ClientSwallowee, ClientSwallower }; /* client types */
    101  
    102  typedef union {
    103  	int i;
    104 @@ -95,6 +96,7 @@ struct Client {
    105  	int isfixed, isfloating, isurgent, neverfocus, oldstate, isfullscreen;
    106  	Client *next;
    107  	Client *snext;
    108 +  Client *swer; /* swallower of client, NULL by default */
    109  	Monitor *mon;
    110  	Window win;
    111  };
    112 @@ -140,6 +142,27 @@ typedef struct {
    113  	int isfloating;
    114  	int monitor;
    115  } Rule;
    116 +typedef struct Swallow Swallow;
    117 +struct Swallow {
    118 +	/* Window class name, instance name (WM_CLASS) and title
    119 +	 * (WM_NAME/_NET_WM_NAME, latter preferred if it exists). An empty string
    120 +	 * implies a wildcard as per strstr(). */
    121 +	char class[256];
    122 +	char inst[256];
    123 +	char title[256];
    124 +
    125 +	/* Used to delete swallow instance after 'swaldecay' windows were mapped
    126 +	 * without the swallow having been consumed. 'decay' keeps track of the
    127 +	 * remaining "charges". */
    128 +	int decay;
    129 +
    130 +	/* The swallower, i.e. the client which will swallow the next mapped window
    131 +	 * whose filters match the above properties. */
    132 +	Client *client;
    133 +
    134 +	/* Linked list of registered swallow instances. */
    135 +	Swallow *next;
    136 +};
    137  
    138  /* function declarations */
    139  static void applyrules(Client *c);
    140 @@ -165,6 +188,7 @@ static void drawbar(Monitor *m);
    141  static void drawbars(void);
    142  static void enternotify(XEvent *e);
    143  static void expose(XEvent *e);
    144 +static int fakesignal(void);
    145  static void focus(Client *c);
    146  static void focusin(XEvent *e);
    147  static void focusmon(const Arg *arg);
    148 @@ -206,6 +230,16 @@ static void setup(void);
    149  static void seturgent(Client *c, int urg);
    150  static void showhide(Client *c);
    151  static void spawn(const Arg *arg);
    152 +static void swal(Client *swer, Client *swee, int manage);
    153 +static void swalreg(Client *c, const char* class, const char* inst, const char* title);
    154 +static void swaldecayby(int decayby);
    155 +static void swalmanage(Swallow *s, Window w, XWindowAttributes *wa);
    156 +static Swallow *swalmatch(Window w);
    157 +static void swalmouse(const Arg *arg);
    158 +static void swalrm(Swallow *s);
    159 +static void swalunreg(Client *c);
    160 +static void swalstop(Client *c, Client *root);
    161 +static void swalstopsel(const Arg *unused);
    162  static void tag(const Arg *arg);
    163  static void tagmon(const Arg *arg);
    164  static void tile(Monitor *m);
    165 @@ -228,6 +262,7 @@ static void updatewindowtype(Client *c);
    166  static void updatewmhints(Client *c);
    167  static void view(const Arg *arg);
    168  static Client *wintoclient(Window w);
    169 +static int wintoclient2(Window w, Client **pc, Client **proot);
    170  static Monitor *wintomon(Window w);
    171  static int xerror(Display *dpy, XErrorEvent *ee);
    172  static int xerrordummy(Display *dpy, XErrorEvent *ee);
    173 @@ -266,6 +301,7 @@ static Clr **scheme;
    174  static Display *dpy;
    175  static Drw *drw;
    176  static Monitor *mons, *selmon;
    177 +static Swallow *swallows;
    178  static Window root, wmcheckwin;
    179  
    180  /* configuration, allows nested code to access above variables */
    181 @@ -586,7 +622,9 @@ configurerequest(XEvent *e)
    182  	XConfigureRequestEvent *ev = &e->xconfigurerequest;
    183  	XWindowChanges wc;
    184  
    185 -	if ((c = wintoclient(ev->window))) {
    186 +	switch (wintoclient2(ev->window, &c, NULL)) {
    187 +	case ClientRegular: /* fallthrough */
    188 +	case ClientSwallowee:
    189  		if (ev->value_mask & CWBorderWidth)
    190  			c->bw = ev->border_width;
    191  		else if (c->isfloating || !selmon->lt[selmon->sellt]->arrange) {
    192 @@ -617,7 +655,13 @@ configurerequest(XEvent *e)
    193  				XMoveResizeWindow(dpy, c->win, c->x, c->y, c->w, c->h);
    194  		} else
    195  			configure(c);
    196 -	} else {
    197 +		break;
    198 +	case ClientSwallower:
    199 +		/* Reject any move/resize requests for swallowers and communicate
    200 +		 * refusal to client via a synthetic ConfigureNotify (ICCCM 4.1.5). */
    201 +		configure(c);
    202 +		break;
    203 +	default:
    204  		wc.x = ev->x;
    205  		wc.y = ev->y;
    206  		wc.width = ev->width;
    207 @@ -626,6 +670,7 @@ configurerequest(XEvent *e)
    208  		wc.sibling = ev->above;
    209  		wc.stack_mode = ev->detail;
    210  		XConfigureWindow(dpy, ev->window, ev->value_mask, &wc);
    211 +		break;
    212  	}
    213  	XSync(dpy, False);
    214  }
    215 @@ -650,11 +695,30 @@ createmon(void)
    216  void
    217  destroynotify(XEvent *e)
    218  {
    219 -	Client *c;
    220 +	Client *c, *swee, *root;
    221  	XDestroyWindowEvent *ev = &e->xdestroywindow;
    222  
    223 -	if ((c = wintoclient(ev->window)))
    224 +	switch (wintoclient2(ev->window, &c, &root)) {
    225 +	case ClientRegular:
    226  		unmanage(c, 1);
    227 +		break;
    228 +	case ClientSwallowee:
    229 +		swalstop(c, NULL);
    230 +		unmanage(c, 1);
    231 +		break;
    232 +	case ClientSwallower:
    233 +		/* If the swallower is swallowed by another client, terminate the
    234 +		 * swallow. This cuts off the swallow chain after the client. */
    235 +		swalstop(c, root);
    236 +
    237 +		/* Cut off the swallow chain before the client. */
    238 +		for (swee = root; swee->swer != c; swee = swee->swer);
    239 +		swee->swer = NULL;
    240 +
    241 +		free(c);
    242 +		updateclientlist();
    243 +		break;
    244 +	}
    245  }
    246  
    247  void
    248 @@ -734,6 +798,12 @@ drawbar(Monitor *m)
    249  	drw_setscheme(drw, scheme[SchemeNorm]);
    250  	x = drw_text(drw, x, 0, w, bh, lrpad / 2, m->ltsymbol, 0);
    251  
    252 +	/* Draw swalsymbol next to ltsymbol. */
    253 +	if (m->sel && m->sel->swer) {
    254 +		w = TEXTW(swalsymbol);
    255 +		x = drw_text(drw, x, 0, w, bh, lrpad / 2, swalsymbol, 0);
    256 +	}
    257 +
    258  	if ((w = m->ww - tw - x) > bh) {
    259  		if (m->sel) {
    260  			drw_setscheme(drw, scheme[m == selmon ? SchemeSel : SchemeNorm]);
    261 @@ -786,6 +856,81 @@ expose(XEvent *e)
    262  		drawbar(m);
    263  }
    264  
    265 +int
    266 +fakesignal(void)
    267 +{
    268 +	/* Command syntax: <PREFIX><COMMAND>[<SEP><ARG>]... */
    269 +	static const char sep[] = "###";
    270 +	static const char prefix[] = "#!";
    271 +
    272 +	size_t numsegments, numargs;
    273 +	char rootname[256];
    274 +	char *segments[16] = {0};
    275 +
    276 +	/* Get root name, split by separator and find the prefix */
    277 +	if (!gettextprop(root, XA_WM_NAME, rootname, sizeof(rootname))
    278 +		|| strncmp(rootname, prefix, sizeof(prefix) - 1)) {
    279 +		return 0;
    280 +	}
    281 +	numsegments = split(rootname + sizeof(prefix) - 1, sep, segments, sizeof(segments));
    282 +	numargs = numsegments - 1; /* number of arguments to COMMAND */
    283 +
    284 +	if (!strcmp(segments[0], "swalreg")) {
    285 +		/* Params: windowid, [class], [instance], [title] */
    286 +		Window w;
    287 +		Client *c;
    288 +
    289 +		if (numargs >= 1) {
    290 +			w = strtoul(segments[1], NULL, 0);
    291 +			switch (wintoclient2(w, &c, NULL)) {
    292 +			case ClientRegular: /* fallthrough */
    293 +			case ClientSwallowee:
    294 +				swalreg(c, segments[2], segments[3], segments[4]);
    295 +				break;
    296 +			}
    297 +		}
    298 +	}
    299 +	else if (!strcmp(segments[0], "swal")) {
    300 +		/* Params: swallower's windowid, swallowee's window-id */
    301 +		Client *swer, *swee;
    302 +		Window winswer, winswee;
    303 +		int typeswer, typeswee;
    304 +
    305 +		if (numargs >= 2) {
    306 +			winswer = strtoul(segments[1], NULL, 0);
    307 +			typeswer = wintoclient2(winswer, &swer, NULL);
    308 +			winswee = strtoul(segments[2], NULL, 0);
    309 +			typeswee = wintoclient2(winswee, &swee, NULL);
    310 +			if ((typeswer == ClientRegular || typeswer == ClientSwallowee)
    311 +				&& (typeswee == ClientRegular || typeswee == ClientSwallowee))
    312 +				swal(swer, swee, 0);
    313 +		}
    314 +	}
    315 +	else if (!strcmp(segments[0], "swalunreg")) {
    316 +		/* Params: swallower's windowid */
    317 +		Client *swer;
    318 +		Window winswer;
    319 +
    320 +		if (numargs == 1) {
    321 +			winswer = strtoul(segments[1], NULL, 0);
    322 +			if ((swer = wintoclient(winswer)))
    323 +				swalunreg(swer);
    324 +		}
    325 +	}
    326 +	else if (!strcmp(segments[0], "swalstop")) {
    327 +		/* Params: swallowee's windowid */
    328 +		Client *swee;
    329 +		Window winswee;
    330 +
    331 +		if (numargs == 1) {
    332 +			winswee = strtoul(segments[1], NULL, 0);
    333 +			if ((swee = wintoclient(winswee)))
    334 +				swalstop(swee, NULL);
    335 +		}
    336 +	}
    337 +	return 1;
    338 +}
    339 +
    340  void
    341  focus(Client *c)
    342  {
    343 @@ -1101,13 +1246,35 @@ mappingnotify(XEvent *e)
    344  void
    345  maprequest(XEvent *e)
    346  {
    347 +	Client *c, *swee, *root;
    348  	static XWindowAttributes wa;
    349  	XMapRequestEvent *ev = &e->xmaprequest;
    350 +	Swallow *s;
    351  
    352  	if (!XGetWindowAttributes(dpy, ev->window, &wa) || wa.override_redirect)
    353  		return;
    354 -	if (!wintoclient(ev->window))
    355 -		manage(ev->window, &wa);
    356 +	switch (wintoclient2(ev->window, &c, &root)) {
    357 +	case ClientRegular: /* fallthrough */
    358 +	case ClientSwallowee:
    359 +		/* Regulars and swallowees are always mapped. Nothing to do. */
    360 +		break;
    361 +	case ClientSwallower:
    362 +		/* Remapping a swallower will simply stop the swallow. */
    363 +		for (swee = root; swee->swer != c; swee = swee->swer);
    364 +		swalstop(swee, root);
    365 +		break;
    366 +	default:
    367 +		/* No client is managing the window. See if any swallows match. */
    368 +		if ((s = swalmatch(ev->window)))
    369 +			swalmanage(s, ev->window, &wa);
    370 +		else
    371 +			manage(ev->window, &wa);
    372 +		break;
    373 +	}
    374 +
    375 +	/* Reduce decay counter of all swallow instances. */
    376 +	if (swaldecay)
    377 +		swaldecayby(1);
    378  }
    379  
    380  void
    381 @@ -1223,11 +1390,13 @@ propertynotify(XEvent *e)
    382  {
    383  	Client *c;
    384  	Window trans;
    385 +	Swallow *s;
    386  	XPropertyEvent *ev = &e->xproperty;
    387  
    388 -	if ((ev->window == root) && (ev->atom == XA_WM_NAME))
    389 -		updatestatus();
    390 -	else if (ev->state == PropertyDelete)
    391 +	if ((ev->window == root) && (ev->atom == XA_WM_NAME)) {
    392 +		if (!fakesignal())
    393 +			updatestatus();
    394 +	} else if (ev->state == PropertyDelete)
    395  		return; /* ignore */
    396  	else if ((c = wintoclient(ev->window))) {
    397  		switch(ev->atom) {
    398 @@ -1249,6 +1418,9 @@ propertynotify(XEvent *e)
    399  			updatetitle(c);
    400  			if (c == c->mon->sel)
    401  				drawbar(c->mon);
    402 +			if (swalretroactive && (s = swalmatch(c->win))) {
    403 +				swal(s->client, c, 0);
    404 +			}
    405  		}
    406  		if (ev->atom == netatom[NetWMWindowType])
    407  			updatewindowtype(c);
    408 @@ -1583,6 +1755,7 @@ setup(void)
    409  	cursor[CurNormal] = drw_cur_create(drw, XC_left_ptr);
    410  	cursor[CurResize] = drw_cur_create(drw, XC_sizing);
    411  	cursor[CurMove] = drw_cur_create(drw, XC_fleur);
    412 +	cursor[CurSwal] = drw_cur_create(drw, XC_bottom_side);
    413  	/* init appearance */
    414  	scheme = ecalloc(LENGTH(colors), sizeof(Clr *));
    415  	for (i = 0; i < LENGTH(colors); i++)
    416 @@ -1666,6 +1839,326 @@ spawn(const Arg *arg)
    417  	}
    418  }
    419  
    420 +/*
    421 + * Perform immediate swallow of client 'swee' by client 'swer'. 'manage' shall
    422 + * be set if swal() is called from swalmanage(). 'swer' and 'swee' must be
    423 + * regular or swallowee, but not swallower.
    424 + */
    425 +void
    426 +swal(Client *swer, Client *swee, int manage)
    427 +{
    428 +	Client *c, **pc;
    429 +	int sweefocused = selmon->sel == swee;
    430 +
    431 +	/* No self-swallowing! */
    432 +	if (swer == swee)
    433 +		return;
    434 +
    435 +	/* Remove any swallows registered for the swer. Asking a swallower to
    436 +	 * swallow another window is ambiguous and is thus avoided altogether. In
    437 +	 * contrast, a swallowee can swallow in a well-defined manner by attaching
    438 +	 * to the head of the swallow chain. */
    439 +	if (!manage)
    440 +		swalunreg(swer);
    441 +
    442 +	/* Disable fullscreen prior to swallow. Swallows involving fullscreen
    443 +	 * windows produces quirky artefacts such as fullscreen terminals or tiled
    444 +	 * pseudo-fullscreen windows. */
    445 +	setfullscreen(swer, 0);
    446 +	setfullscreen(swee, 0);
    447 +
    448 +	/* Swap swallowee into client and focus lists. Keeps current focus unless
    449 +	 * the swer (which gets unmapped) is focused in which case the swee will
    450 +	 * receive focus. */
    451 +	detach(swee);
    452 +	for (pc = &swer->mon->clients; *pc && *pc != swer; pc = &(*pc)->next);
    453 +	*pc = swee;
    454 +	swee->next = swer->next;
    455 +	detachstack(swee);
    456 +	for (pc = &swer->mon->stack; *pc && *pc != swer; pc = &(*pc)->snext);
    457 +	*pc = swee;
    458 +	swee->snext = swer->snext;
    459 +	swee->mon = swer->mon;
    460 +	if (sweefocused) {
    461 +		detachstack(swee);
    462 +		attachstack(swee);
    463 +		selmon = swer->mon;
    464 +	}
    465 +	swee->tags = swer->tags;
    466 +	swee->isfloating = swer->isfloating;
    467 +	for (c = swee; c->swer; c = c->swer);
    468 +	c->swer = swer;
    469 +
    470 +	/* Configure geometry params obtained from patches (e.g. cfacts) here. */
    471 +	// swee->cfact = swer->cfact;
    472 +
    473 +	/* ICCCM 4.1.3.1 */
    474 +	setclientstate(swer, WithdrawnState);
    475 +	if (manage)
    476 +		setclientstate(swee, NormalState);
    477 +
    478 +	if (swee->isfloating || !swee->mon->lt[swee->mon->sellt]->arrange)
    479 +		XRaiseWindow(dpy, swee->win);
    480 +	resize(swee, swer->x, swer->y, swer->w, swer->h, 0);
    481 +
    482 +	focus(NULL);
    483 +	arrange(NULL);
    484 +	if (manage)
    485 +		XMapWindow(dpy, swee->win);
    486 +	XUnmapWindow(dpy, swer->win);
    487 +	restack(swer->mon);
    488 +}
    489 +
    490 +/*
    491 + * Register a future swallow with swallower 'c'. 'class', 'inst' and 'title'
    492 + * shall point null-terminated strings and must not be NULL. If an already
    493 + * existing swallow instance targets 'c' its filters are updated and no new
    494 + * swallow instance is created. 'c' may be ClientRegular or ClientSwallowee.
    495 + * Complement to swalrm().
    496 + */
    497 +void
    498 +swalreg(Client *c, const char *class, const char *inst, const char *title)
    499 +{
    500 +	Swallow *s;
    501 +
    502 +	if (!c)
    503 +		return;
    504 +
    505 +	/* Update existing swallow */
    506 +	for (s = swallows; s; s = s->next) {
    507 +		if (s->client == c) {
    508 +			strncpy(s->class, class, sizeof(s->class) - 1);
    509 +			strncpy(s->inst, inst, sizeof(s->inst) - 1);
    510 +			strncpy(s->title, title, sizeof(s->title) - 1);
    511 +			s->decay = swaldecay;
    512 +
    513 +			/* Only one swallow per client. May return after first hit. */
    514 +			return;
    515 +		}
    516 +	}
    517 +
    518 +	s = ecalloc(1, sizeof(Swallow));
    519 +	s->decay = swaldecay;
    520 +	s->client = c;
    521 +	strncpy(s->class, class, sizeof(s->class) - 1);
    522 +	strncpy(s->inst, inst, sizeof(s->inst) - 1);
    523 +	strncpy(s->title, title, sizeof(s->title) - 1);
    524 +
    525 +	s->next = swallows;
    526 +	swallows = s;
    527 +}
    528 +
    529 +/*
    530 + * Decrease decay counter of all registered swallows by 'decayby' and remove any
    531 + * swallow instances whose counter is less than or equal to zero.
    532 + */
    533 +void
    534 +swaldecayby(int decayby)
    535 +{
    536 +	Swallow *s, *t;
    537 +
    538 +	for (s = swallows; s; s = t) {
    539 +		s->decay -= decayby;
    540 +		t = s->next;
    541 +		if (s->decay <= 0)
    542 +			swalrm(s);
    543 +	}
    544 +}
    545 +
    546 +/*
    547 + * Window configuration and client setup for new windows which are to be
    548 + * swallowed immediately. Pendant to manage() for such windows.
    549 + */
    550 +void
    551 +swalmanage(Swallow *s, Window w, XWindowAttributes *wa)
    552 +{
    553 +	Client *swee, *swer;
    554 +	XWindowChanges wc;
    555 +
    556 +	swer = s->client;
    557 +	swalrm(s);
    558 +
    559 +	/* Perform bare minimum setup of a client for window 'w' such that swal()
    560 +	 * may be used to perform the swallow. The following lines are basically a
    561 +	 * minimal implementation of manage() with a few chunks delegated to
    562 +	 * swal(). */
    563 +	swee = ecalloc(1, sizeof(Client));
    564 +	swee->win = w;
    565 +	swee->mon = swer->mon;
    566 +	swee->oldbw = wa->border_width;
    567 +	swee->bw = borderpx;
    568 +	attach(swee);
    569 +	attachstack(swee);
    570 +	updatetitle(swee);
    571 +	updatesizehints(swee);
    572 +	XSelectInput(dpy, swee->win, EnterWindowMask|FocusChangeMask|PropertyChangeMask|StructureNotifyMask);
    573 +	wc.border_width = swee->bw;
    574 +	XConfigureWindow(dpy, swee->win, CWBorderWidth, &wc);
    575 +	grabbuttons(swee, 0);
    576 +	XChangeProperty(dpy, root, netatom[NetClientList], XA_WINDOW, 32, PropModeAppend,
    577 +		(unsigned char *) &(swee->win), 1);
    578 +
    579 +	swal(swer, swee, 1);
    580 +}
    581 +
    582 +/*
    583 + * Return swallow instance which targets window 'w' as determined by its class
    584 + * name, instance name and window title. Returns NULL if none is found. Pendant
    585 + * to wintoclient().
    586 + */
    587 +Swallow *
    588 +swalmatch(Window w)
    589 +{
    590 +	XClassHint ch = { NULL, NULL };
    591 +	Swallow *s = NULL;
    592 +	char title[sizeof(s->title)];
    593 +
    594 +	XGetClassHint(dpy, w, &ch);
    595 +	if (!gettextprop(w, netatom[NetWMName], title, sizeof(title)))
    596 +		gettextprop(w, XA_WM_NAME, title, sizeof(title));
    597 +
    598 +	for (s = swallows; s; s = s->next) {
    599 +		if ((!ch.res_class || strstr(ch.res_class, s->class))
    600 +			&& (!ch.res_name || strstr(ch.res_name, s->inst))
    601 +			&& (title[0] == '\0' || strstr(title, s->title)))
    602 +			break;
    603 +	}
    604 +
    605 +	if (ch.res_class)
    606 +		XFree(ch.res_class);
    607 +	if (ch.res_name)
    608 +		XFree(ch.res_name);
    609 +	return s;
    610 +}
    611 +
    612 +/*
    613 + * Interactive drag-and-drop swallow.
    614 + */
    615 +void
    616 +swalmouse(const Arg *arg)
    617 +{
    618 +	Client *swer, *swee;
    619 +	XEvent ev;
    620 +
    621 +	if (!(swee = selmon->sel))
    622 +		return;
    623 +
    624 +	if (XGrabPointer(dpy, root, False, ButtonPressMask|ButtonReleaseMask, GrabModeAsync,
    625 +		GrabModeAsync, None, cursor[CurSwal]->cursor, CurrentTime) != GrabSuccess)
    626 +		return;
    627 +
    628 +	do {
    629 +		XMaskEvent(dpy, MOUSEMASK|ExposureMask|SubstructureRedirectMask, &ev);
    630 +		switch(ev.type) {
    631 +		case ConfigureRequest: /* fallthrough */
    632 +		case Expose: /* fallthrough */
    633 +		case MapRequest:
    634 +			handler[ev.type](&ev);
    635 +			break;
    636 +		}
    637 +	} while (ev.type != ButtonRelease);
    638 +	XUngrabPointer(dpy, CurrentTime);
    639 +
    640 +	if ((swer = wintoclient(ev.xbutton.subwindow))
    641 +		&& swer != swee)
    642 +		swal(swer, swee, 0);
    643 +
    644 +	/* Remove accumulated pending EnterWindow events caused by the mouse
    645 +	 * movements. */
    646 +	XCheckMaskEvent(dpy, EnterWindowMask, &ev);
    647 +}
    648 +
    649 +/*
    650 + * Delete swallow instance swallows and free its resources. Complement to
    651 + * swalreg(). If NULL is passed all registered swallows are deleted.
    652 + */
    653 +void
    654 +swalrm(Swallow *s)
    655 +{
    656 +	Swallow *t, **ps;
    657 +
    658 +	if (s) {
    659 +		for (ps = &swallows; *ps && *ps != s; ps = &(*ps)->next);
    660 +		*ps = s->next;
    661 +		free(s);
    662 +	}
    663 +	else {
    664 +		for(s = swallows; s; s = t) {
    665 +			t = s->next;
    666 +			free(s);
    667 +		}
    668 +		swallows = NULL;
    669 +	}
    670 +}
    671 +
    672 +/*
    673 + * Removes swallow instance targeting 'c' if it exists. Complement to swalreg().
    674 + */
    675 +void
    676 +swalunreg(Client *c) { Swallow *s;
    677 +
    678 +	for (s = swallows; s; s = s->next) {
    679 +		if (c == s->client) {
    680 +			swalrm(s);
    681 +			/* Max. 1 registered swallow per client. No need to continue. */
    682 +			break;
    683 +		}
    684 +	}
    685 +}
    686 +
    687 +/*
    688 + * Stop an active swallow of swallowed client 'swee' and remap the swallower.
    689 + * If 'swee' is a swallower itself 'root' must point the root client of the
    690 + * swallow chain containing 'swee'.
    691 + */
    692 +void
    693 +swalstop(Client *swee, Client *root)
    694 +{
    695 +	Client *swer;
    696 +
    697 +	if (!swee || !(swer = swee->swer))
    698 +		return;
    699 +
    700 +	swee->swer = NULL;
    701 +	root = root ? root : swee;
    702 +	swer->mon = root->mon;
    703 +	swer->tags = root->tags;
    704 +	swer->next = root->next;
    705 +	root->next = swer;
    706 +	swer->snext = root->snext;
    707 +	root->snext = swer;
    708 +	swer->isfloating = swee->isfloating;
    709 +
    710 +	/* Configure geometry params obtained from patches (e.g. cfacts) here. */
    711 +	// swer->cfact = 1.0;
    712 +
    713 +	/* If swer is not in tiling mode reuse swee's geometry. */
    714 +	if (swer->isfloating || !root->mon->lt[root->mon->sellt]->arrange) {
    715 +		XRaiseWindow(dpy, swer->win);
    716 +		resize(swer, swee->x, swee->y, swee->w, swee->h, 0);
    717 +	}
    718 +
    719 +	/* Override swer's border scheme which may be using SchemeSel. */
    720 +	XSetWindowBorder(dpy, swer->win, scheme[SchemeNorm][ColBorder].pixel);
    721 +
    722 +	/* ICCCM 4.1.3.1 */
    723 +	setclientstate(swer, NormalState);
    724 +
    725 +	XMapWindow(dpy, swer->win);
    726 +	focus(NULL);
    727 +	arrange(swer->mon);
    728 +}
    729 +
    730 +/*
    731 + * Stop active swallow for currently selected client.
    732 + */
    733 +void
    734 +swalstopsel(const Arg *unused)
    735 +{
    736 +	if (selmon->sel)
    737 +		swalstop(selmon->sel, NULL);
    738 +}
    739 +
    740  void
    741  tag(const Arg *arg)
    742  {
    743 @@ -1806,12 +2299,24 @@ unmapnotify(XEvent *e)
    744  {
    745  	Client *c;
    746  	XUnmapEvent *ev = &e->xunmap;
    747 +	int type;
    748  
    749 -	if ((c = wintoclient(ev->window))) {
    750 -		if (ev->send_event)
    751 -			setclientstate(c, WithdrawnState);
    752 -		else
    753 -			unmanage(c, 0);
    754 +	type = wintoclient2(ev->window, &c, NULL);
    755 +	if (type && ev->send_event) {
    756 +		setclientstate(c, WithdrawnState);
    757 +		return;
    758 +	}
    759 +	switch (type) {
    760 +	case ClientRegular:
    761 +		unmanage(c, 0);
    762 +		break;
    763 +	case ClientSwallowee:
    764 +		swalstop(c, NULL);
    765 +		unmanage(c, 0);
    766 +		break;
    767 +	case ClientSwallower:
    768 +		/* Swallowers are never mapped. Nothing to do. */
    769 +		break;
    770  	}
    771  }
    772  
    773 @@ -1853,15 +2358,19 @@ updatebarpos(Monitor *m)
    774  void
    775  updateclientlist()
    776  {
    777 -	Client *c;
    778 +	Client *c, *d;
    779  	Monitor *m;
    780  
    781  	XDeleteProperty(dpy, root, netatom[NetClientList]);
    782 -	for (m = mons; m; m = m->next)
    783 -		for (c = m->clients; c; c = c->next)
    784 -			XChangeProperty(dpy, root, netatom[NetClientList],
    785 -				XA_WINDOW, 32, PropModeAppend,
    786 -				(unsigned char *) &(c->win), 1);
    787 +	for (m = mons; m; m = m->next) {
    788 +		for (c = m->clients; c; c = c->next) {
    789 +			for (d = c; d; d = d->swer) {
    790 +				XChangeProperty(dpy, root, netatom[NetClientList],
    791 +					XA_WINDOW, 32, PropModeAppend,
    792 +					(unsigned char *) &(c->win), 1);
    793 +			}
    794 +		}
    795 +	}
    796  }
    797  
    798  int
    799 @@ -2075,6 +2584,43 @@ wintoclient(Window w)
    800  	return NULL;
    801  }
    802  
    803 +/*
    804 + * Writes client managing window 'w' into 'pc' and returns type of client. If
    805 + * no client is found NULL is written to 'pc' and zero is returned. If a client
    806 + * is found and is a swallower (ClientSwallower) and proot is not NULL the root
    807 + * client of the swallow chain is written to 'proot'.
    808 + */
    809 +int
    810 +wintoclient2(Window w, Client **pc, Client **proot)
    811 +{
    812 +	Monitor *m;
    813 +	Client *c, *d;
    814 +
    815 +	for (m = mons; m; m = m->next) {
    816 +		for (c = m->clients; c; c = c->next) {
    817 +			if (c->win == w) {
    818 +				*pc = c;
    819 +				if (c->swer)
    820 +					return ClientSwallowee;
    821 +				else
    822 +					return ClientRegular;
    823 +			}
    824 +			else {
    825 +				for (d = c->swer; d; d = d->swer) {
    826 +					if (d->win == w) {
    827 +						if (proot)
    828 +							*proot = c;
    829 +						*pc = d;
    830 +						return ClientSwallower;
    831 +					}
    832 +				}
    833 +			}
    834 +		}
    835 +	}
    836 +	*pc = NULL;
    837 +	return 0;
    838 +}
    839 +
    840  Monitor *
    841  wintomon(Window w)
    842  {
    843 diff --git a/dwmswallow b/dwmswallow
    844 new file mode 100644
    845 index 0000000..eaab3bb
    846 --- /dev/null
    847 +++ b/dwmswallow
    848 @@ -0,0 +1,120 @@
    849 +#!/usr/bin/env sh
    850 +
    851 +# Separator and command prefix, as defined in dwm.c:fakesignal()
    852 +SEP='###'
    853 +PREFIX='#!'
    854 +
    855 +# Asserts that all arguments are valid X11 window IDs, i.e. positive integers.
    856 +# For the purpose of this script 0 is declared invalid.
    857 +is_winid() {
    858 +	while :; do
    859 +		# Given input incompatible to %d, some implementations of printf return
    860 +		# an error while others silently evaluate the expression to 0.
    861 +		if ! wid=$(printf '%d' "$1" 2>/dev/null) || [ "$wid" -le 0 ]; then
    862 +			return 1
    863 +		fi
    864 +
    865 +		[ -n "$2" ] && shift || break
    866 +	done
    867 +}
    868 +
    869 +# Prints usage help. If "$1" is provided, function exits script after
    870 +# execution.
    871 +usage() {
    872 +	[ -t 1 ] && myprintf=printf || myprintf=true
    873 +	msg="$(cat <<-EOF
    874 +	dwm window swallowing command-line interface. Usage:
    875 +
    876 +	  $($myprintf "\033[1m")dwmswallow $($myprintf "\033[3m")SWALLOWER [-c CLASS] [-i INSTANCE] [-t TITLE]$($myprintf "\033[0m")
    877 +	    Register window $($myprintf "\033[3m")SWALLOWER$($myprintf "\033[0m") to swallow the next future window whose attributes
    878 +	    match the $($myprintf "\033[3m")CLASS$($myprintf "\033[0m") name, $($myprintf "\033[3m")INSTANCE$($myprintf "\033[0m") name and window $($myprintf "\033[3m")TITLE$($myprintf "\033[0m") filters using basic
    879 +	    string-matching. An omitted filter will match anything.
    880 +
    881 +	  $($myprintf "\033[1m")dwmswallow $($myprintf "\033[3m")SWALLOWER -d$($myprintf "\033[0m")
    882 +	    Deregister queued swallow for window $($myprintf "\033[3m")SWALLOWER$($myprintf "\033[0m"). Inverse of above signature.
    883 +
    884 +	  $($myprintf "\033[1m")dwmswallow $($myprintf "\033[3m")SWALLOWER SWALLOWEE$($myprintf "\033[0m")
    885 +	    Perform immediate swallow of window $($myprintf "\033[3m")SWALLOWEE$($myprintf "\033[0m") by window $($myprintf "\033[3m")SWALLOWER$($myprintf "\033[0m").
    886 +
    887 +	  $($myprintf "\033[1m")dwmswallow $($myprintf "\033[3m")SWALLOWEE -s$($myprintf "\033[0m")
    888 +	    Stop swallow of window $($myprintf "\033[3m")SWALLOWEE$($myprintf "\033[0m"). Inverse of the above signature. Visible
    889 +	    windows only.
    890 +
    891 +	  $($myprintf "\033[1m")dwmswallow -h$($myprintf "\033[0m")
    892 +	    Show this usage information.
    893 +	EOF
    894 +	)"
    895 +
    896 +	if [ -n "$1" ]; then
    897 +		echo "$msg" >&2
    898 +		exit "$1"
    899 +	else
    900 +		echo "$msg"
    901 +	fi
    902 +}
    903 +
    904 +# Determine number of leading positional arguments
    905 +arg1="$1" # save for later
    906 +arg2="$2" # save for later
    907 +num_pargs=0
    908 +while :; do
    909 +	case "$1" in
    910 +	-*|"") break ;;
    911 +	*) num_pargs=$((num_pargs + 1)); shift ;;
    912 +	esac
    913 +done
    914 +
    915 +case "$num_pargs" in
    916 +1)
    917 +	! is_winid "$arg1" && usage 1
    918 +
    919 +	widswer="$arg1"
    920 +	if [ "$1" = "-d" ] && [ "$#" -eq 1 ]; then
    921 +		if name="$(printf "${PREFIX}swalunreg${SEP}%u" "$widswer" 2>/dev/null)"; then
    922 +			xsetroot -name "$name"
    923 +		else
    924 +			usage 1
    925 +		fi
    926 +	elif [ "$1" = "-s" ] && [ "$#" -eq 1 ]; then
    927 +		widswee="$arg1"
    928 +		if name="$(printf "${PREFIX}swalstop${SEP}%u" "$widswee" 2>/dev/null)"; then
    929 +			xsetroot -name "$name"
    930 +		else
    931 +			usage 1
    932 +		fi
    933 +	else
    934 +		while :; do
    935 +			case "$1" in
    936 +			-c) [ -n "$2" ] && { class="$2"; shift 2; } || usage 1 ;;
    937 +			-i) [ -n "$2" ] && { instance="$2"; shift 2; } || usage 1 ;;
    938 +			-t) [ -n "$2" ] && { title="$2"; shift 2; } || usage 1 ;;
    939 +			"") break ;;
    940 +			*)	usage 1 ;;
    941 +			esac
    942 +		done
    943 +		widswer="$arg1"
    944 +		if name="$(printf "${PREFIX}swalreg${SEP}%u${SEP}%s${SEP}%s${SEP}%s" "$widswer" "$class" "$instance" "$title" 2>/dev/null)"; then
    945 +			xsetroot -name "$name"
    946 +		else
    947 +			usage 1
    948 +		fi
    949 +	fi
    950 +	;;
    951 +2)
    952 +	! is_winid "$arg1" "$arg2" || [ -n "$1" ] && usage 1
    953 +
    954 +	widswer="$arg1"
    955 +	widswee="$arg2"
    956 +	if name="$(printf "${PREFIX}swal${SEP}%u${SEP}%u" "$widswer" "$widswee" 2>/dev/null)"; then
    957 +		xsetroot -name "$name"
    958 +	else
    959 +		usage 1
    960 +	fi
    961 +	;;
    962 +*)
    963 +	if [ "$arg1" = "-h" ] && [ $# -eq 1 ]; then
    964 +		usage
    965 +	else
    966 +		usage 1
    967 +	fi
    968 +esac
    969 diff --git a/util.c b/util.c
    970 index 96b82c9..3af7b6c 100644
    971 --- a/util.c
    972 +++ b/util.c
    973 @@ -24,6 +24,35 @@ die(const char *fmt, ...)
    974  
    975  	exit(1);
    976  }
    977 +/*
    978 + * Splits a string into segments according to a separator. A '\0' is written to
    979 + * the end of every segment. The beginning of every segment is written to
    980 + * 'pbegin'. Only the first 'maxcount' segments will be written if
    981 + * maxcount > 0. Inspired by python's split.
    982 + *
    983 + * Used exclusively by fakesignal() to split arguments.
    984 + */
    985 +size_t
    986 +split(char *s, const char* sep, char **pbegin, size_t maxcount) {
    987 +
    988 +	char *p, *q;
    989 +	const size_t seplen = strlen(sep);
    990 +	size_t count = 0;
    991 +
    992 +	maxcount = maxcount == 0 ? (size_t)-1 : maxcount;
    993 +	p = s;
    994 +	while ((q = strstr(p, sep)) != NULL && count < maxcount) {
    995 +		pbegin[count] = p;
    996 +		*q = '\0';
    997 +		p = q + seplen;
    998 +		count++;
    999 +	}
   1000 +	if (count < maxcount) {
   1001 +		pbegin[count] = p;
   1002 +		count++;
   1003 +	}
   1004 +	return count;
   1005 +}
   1006  
   1007  void *
   1008  ecalloc(size_t nmemb, size_t size)
   1009 diff --git a/util.h b/util.h
   1010 index f633b51..670345f 100644
   1011 --- a/util.h
   1012 +++ b/util.h
   1013 @@ -6,3 +6,4 @@
   1014  
   1015  void die(const char *fmt, ...);
   1016  void *ecalloc(size_t nmemb, size_t size);
   1017 +size_t split(char *s, const char* sep, char **pbegin, size_t maxcount);
   1018 -- 
   1019 2.44.0
   1020