Index | Thread | Search

From:
akira.sato@keemail.me
Subject:
Adding Tilling to cwm
To:
Tech <tech@openbsd.org>
Date:
Thu, 20 Aug 2026 12:18:32 +0200

Download raw body.

Thread
  • akira.sato@keemail.me:

    Adding Tilling to cwm

Hello tech@,

Currently cwm have window-htile and window-vtile, but these only work
for one time you press key, after that if you open new window or close
window, tiling is not keep automatically, you must press key again
every time. For my daily usage (many xterm + firefox open/close all
day) this is bit annoying, so I make patch to add automatic mode.

What diff does, briefly:

- new tilemode directive in cwmrc, can be "none", "vtile" or "htile".
   This become default tile mode for group when group is created.

- each group remember own tile_last (none/vtile/htile), not only
   global setting. So group 1 can be vtile and group 2 can be none,
   independent.

- when new window is mapped, or a window is closed, if the group's
   tile mode is not none, cwm will retile automatically, call same
   client_htile()/client_vtile() as the manual bind. Master window
   selection prefer whatever window currently has focus, so it dont
   jump around randomly when you close something.

- new bind function window-tile-none, to turn off auto tiling for
   current window's group.

- new bind function group-tile-toggle, cycle group between
   none -> vtile -> htile -> none, and retile imediately. I bind this
   to CM-t myself, very convenient, one key change whole layout.

- gap(top bottom left right) is now also using for spacing BETWEEN
   tiled windows, not only edge of screen. left value used for
   horizontal space between windows side by side, bottom value used
   for vertical space (master to rest, and between stacked windows).
   top/right not touched, because top usually big number for status
   bar so dont want that much space between every window. If your
   gap is "0 0 0 0" like default, nothing change, tiled windows still
   touch each other same as before.

- fix small bug: calling window-htile/window-vtile on a window with
   no group (nogroup, gc == NULL) would crash cwm, NULL deref. Now
   just maximize the window instead, dont touch tile_last since there
   is no group to remember it in.

How to configure, on ~/.cwmrc:

    # make every new group start in vertical tile mode by default
    tilemode vtile

    # optional, percent of screen master window take (already exist
    # option before my diff, just mention here because work together)
    vtile 50
    htile 50

    # gap around screen edge AND between tiled windows now, see above
    # top bottom left right
    gap 35 5 5 5

    # manual one-shot tile of current window, same as before my diff
    bind-key M-h window-htile
    bind-key M-v window-vtile

    # stop auto tiling for the group of current window
    bind-key M-n window-tile-none

    # cycle WHOLE GROUP thru none -> vtile -> htile -> none, dont
    # need window focus for this one, act on active group
    bind-key CM-t group-tile-toggle


Tested on amd64, compile clean with no new warning.

Thanks.-- 
 Secured with Tuta Mail: 
 https://tuta.com/free-email
--- calmwm.h
+++ calmwm.h
@@ -178,11 +178,18 @@
 };
 TAILQ_HEAD(client_q, client_ctx);
 
+enum cwm_tilemode {
+	TILE_NONE = 0,
+	TILE_VTILE = 1,
+	TILE_HTILE = 2
+};
+
 struct group_ctx {
 	TAILQ_ENTRY(group_ctx)	 entry;
 	struct screen_ctx	*sc;
 	char			*name;
 	int			 num;
+	enum cwm_tilemode	 tile_last;
 };
 TAILQ_HEAD(group_q, group_ctx);
 
@@ -296,6 +303,7 @@
 	int			 snapdist;
 	int			 htile;
 	int			 vtile;
+	enum cwm_tilemode	 tilemode;
 	struct gap		 gap;
 	char			*color[CWM_COLOR_NITEMS];
 	char			*font;
@@ -421,10 +429,12 @@
 void			 client_raise(struct client_ctx *);
 void			 client_remove(struct client_ctx *);
 void			 client_resize(struct client_ctx *, int);
+void			 client_retile_group(struct group_ctx *);
 void			 client_set_active(struct client_ctx *);
 void			 client_set_name(struct client_ctx *);
 void			 client_show(struct client_ctx *);
 int			 client_snapcalc(int, int, int, int, int);
+void			 client_tile_none(struct client_ctx *);
 void			 client_toggle_hidden(struct client_ctx *);
 void			 client_toggle_hmaximize(struct client_ctx *);
 void			 client_toggle_fullscreen(struct client_ctx *);
@@ -451,6 +461,7 @@
 void			 group_close(struct screen_ctx *, int);
 int			 group_restore(struct client_ctx *);
 void			 group_show(struct group_ctx *);
+void			 group_tile_toggle(struct screen_ctx *);
 void			 group_toggle(struct screen_ctx *, int);
 void			 group_toggle_all(struct screen_ctx *);
 void			 group_toggle_membership(struct client_ctx *);
@@ -508,6 +519,7 @@
 void			 kbfunc_client_toggle_vmaximize(void *, struct cargs *);
 void 			 kbfunc_client_htile(void *, struct cargs *);
 void 			 kbfunc_client_vtile(void *, struct cargs *);
+void			 kbfunc_client_tile_none(void *, struct cargs *);
 void			 kbfunc_client_cycle(void *, struct cargs *);
 void			 kbfunc_client_toggle_group(void *, struct cargs *);
 void			 kbfunc_client_movetogroup(void *, struct cargs *);
@@ -517,6 +529,7 @@
 void			 kbfunc_group_close(void *, struct cargs *);
 void			 kbfunc_group_cycle(void *, struct cargs *);
 void			 kbfunc_group_toggle_all(void *, struct cargs *);
+void			 kbfunc_group_tile_toggle(void *, struct cargs *);
 void			 kbfunc_menu_client(void *, struct cargs *);
 void			 kbfunc_menu_cmd(void *, struct cargs *);
 void			 kbfunc_menu_group(void *, struct cargs *);
--- client.c
+++ client.c
@@ -935,25 +935,29 @@
 			continue;
 		n++;
 	}
-	if (n == 0)
-		return;
-
-	if (cc->flags & CLIENT_VMAXIMIZED ||
-	    cc->geom.h + (cc->bwidth * 2) >= area.h)
-		return;
-
-	cc->flags &= ~CLIENT_HMAXIMIZED;
+	cc->flags &= ~(CLIENT_HMAXIMIZED | CLIENT_VMAXIMIZED);
 	cc->geom.x = area.x;
 	cc->geom.y = area.y;
 	cc->geom.w = area.w - (cc->bwidth * 2);
+	if (n == 0) {
+		/* Alone in its group (or no group): just fill the area. */
+		cc->geom.h = area.h - (cc->bwidth * 2);
+		if (cc->gc != NULL)
+			cc->gc->tile_last = TILE_HTILE;
+		client_resize(cc, 1);
+		client_ptr_warp(cc);
+		return;
+	}
 	if (Conf.htile > 0)
 		cc->geom.h = ((area.h - (cc->bwidth * 2)) * Conf.htile) / 100;
+	if (cc->gc != NULL)
+		cc->gc->tile_last = TILE_HTILE;
 	client_resize(cc, 1);
 	client_ptr_warp(cc);
 
-	mh = cc->geom.h + (cc->bwidth * 2);
+	mh = cc->geom.h + (cc->bwidth * 2) + Conf.gap.bottom;
 	x = area.x;
-	w = area.w / n;
+	w = (area.w - (n - 1) * Conf.gap.left) / n;
 	h = area.h - mh;
 	TAILQ_FOREACH(ci, &sc->clientq, entry) {
 		if (ci->gc != cc->gc)
@@ -973,7 +977,7 @@
 		if (i + 1 == n)
 			ci->geom.w = area.x + area.w -
 			    ci->geom.x - (ci->bwidth * 2);
-		x += w;
+		x += w + Conf.gap.left;
 		i++;
 		client_resize(ci, 1);
 	}
@@ -1004,25 +1008,29 @@
 			continue;
 		n++;
 	}
-	if (n == 0)
-		return;
-
-	if (cc->flags & CLIENT_HMAXIMIZED ||
-	    cc->geom.w + (cc->bwidth * 2) >= area.w)
-		return;
-
-	cc->flags &= ~CLIENT_VMAXIMIZED;
+	cc->flags &= ~(CLIENT_VMAXIMIZED | CLIENT_HMAXIMIZED);
 	cc->geom.x = area.x;
 	cc->geom.y = area.y;
+	cc->geom.h = area.h - (cc->bwidth * 2);
+	if (n == 0) {
+		/* Alone in its group (or no group): just fill the area. */
+		cc->geom.w = area.w - (cc->bwidth * 2);
+		if (cc->gc != NULL)
+			cc->gc->tile_last = TILE_VTILE;
+		client_resize(cc, 1);
+		client_ptr_warp(cc);
+		return;
+	}
 	if (Conf.vtile > 0)
 		cc->geom.w = ((area.w - (cc->bwidth * 2)) * Conf.vtile) / 100;
-	cc->geom.h = area.h - (cc->bwidth * 2);
+	if (cc->gc != NULL)
+		cc->gc->tile_last = TILE_VTILE;
 	client_resize(cc, 1);
 	client_ptr_warp(cc);
 
-	mw = cc->geom.w + (cc->bwidth * 2);
+	mw = cc->geom.w + (cc->bwidth * 2) + Conf.gap.left;
 	y = area.y;
-	h = area.h / n;
+	h = (area.h - (n - 1) * Conf.gap.bottom) / n;
 	w = area.w - mw;
 	TAILQ_FOREACH(ci, &sc->clientq, entry) {
 		if (ci->gc != cc->gc)
@@ -1042,8 +1050,57 @@
 		if (i + 1 == n)
 			ci->geom.h = area.y + area.h -
 			    ci->geom.y - (ci->bwidth * 2);
-		y += h;
+		y += h + Conf.gap.bottom;
 		i++;
 		client_resize(ci, 1);
 	}
 }
+
+/*
+ * Re-run the group's last tile action, e.g. after a window was opened
+ * or closed within it.  The master is chosen as follows: prefer the
+ * client that currently has focus (CLIENT_ACTIVE) within the group,
+ * since that is the window the user is looking at; if none does
+ * (e.g. the client that just triggered the retile was the active one
+ * and got removed before we got here, or a brand-new window hasn't
+ * been focused yet), fall back to the first eligible client in
+ * clientq, which for a freshly mapped window means the pre-existing
+ * master keeps its place.
+ */
+void
+client_retile_group(struct group_ctx *gc)
+{
+	struct screen_ctx	*sc = gc->sc;
+	struct client_ctx	*cc, *first;
+
+	if (gc->tile_last == TILE_NONE)
+		return;
+
+	first = NULL;
+	TAILQ_FOREACH(cc, &sc->clientq, entry) {
+		if (cc->gc != gc)
+			continue;
+		if (cc->flags & (CLIENT_HIDDEN | CLIENT_IGNORE))
+			continue;
+		if (first == NULL)
+			first = cc;
+		if (cc->flags & CLIENT_ACTIVE)
+			break;
+	}
+	if (cc == NULL)
+		cc = first;
+	if (cc == NULL)
+		return;
+
+	if (gc->tile_last == TILE_VTILE)
+		client_vtile(cc);
+	else
+		client_htile(cc);
+}
+
+void
+client_tile_none(struct client_ctx *cc)
+{
+	if (cc->gc != NULL)
+		cc->gc->tile_last = TILE_NONE;
+}
--- group.c
+++ group.c
@@ -131,6 +131,7 @@
 	gc->sc = sc;
 	gc->name = xstrdup(name);
 	gc->num = num;
+	gc->tile_last = Conf.tilemode;
 	TAILQ_INSERT_TAIL(&sc->groupq, gc, entry);
 
 	if (num == 1)
@@ -255,6 +256,40 @@
 	sc->hideall = !sc->hideall;
 }
 
+/*
+ * Cycle the active group's automatic tile mode: none -> vtile ->
+ * htile -> none.  Unlike window-htile/window-vtile (which act on
+ * the currently focused client), this acts on the active group as
+ * a whole, so it works even when no client currently has focus.
+ * When the new mode isn't TILE_NONE, retile immediately so the
+ * change is visible right away instead of waiting for the next
+ * window open/close.
+ */
+void
+group_tile_toggle(struct screen_ctx *sc)
+{
+	struct group_ctx	*gc = sc->group_active;
+
+	if (gc == NULL)
+		return;
+
+	switch (gc->tile_last) {
+	case TILE_NONE:
+		gc->tile_last = TILE_VTILE;
+		break;
+	case TILE_VTILE:
+		gc->tile_last = TILE_HTILE;
+		break;
+	case TILE_HTILE:
+	default:
+		gc->tile_last = TILE_NONE;
+		break;
+	}
+
+	if (gc->tile_last != TILE_NONE)
+		client_retile_group(gc);
+}
+
 void
 group_close(struct screen_ctx *sc, int idx)
 {
--- xevents.c
+++ xevents.c
@@ -94,6 +94,9 @@
 
 	if ((cc != NULL) && (!(cc->flags & CLIENT_IGNORE)))
 		client_ptr_warp(cc);
+
+	if (cc != NULL && cc->gc != NULL)
+		client_retile_group(cc->gc);
 }
 
 static void
@@ -101,6 +104,7 @@
 {
 	XUnmapEvent		*e = &ee->xunmap;
 	struct client_ctx	*cc;
+	struct group_ctx	*gc;
 
 	LOG_DEBUG3("window: 0x%lx", e->window);
 
@@ -108,8 +112,12 @@
 		if (e->send_event) {
 			xu_set_wm_state(cc->win, WithdrawnState);
 		} else {
-			if (!(cc->flags & CLIENT_HIDDEN))
+			if (!(cc->flags & CLIENT_HIDDEN)) {
+				gc = cc->gc;
 				client_remove(cc);
+				if (gc != NULL)
+					client_retile_group(gc);
+			}
 		}
 	}
 }
@@ -119,11 +127,16 @@
 {
 	XDestroyWindowEvent	*e = &ee->xdestroywindow;
 	struct client_ctx	*cc;
+	struct group_ctx	*gc;
 
 	LOG_DEBUG3("window: 0x%lx", e->window);
 
-	if ((cc = client_find(e->window)) != NULL)
+	if ((cc = client_find(e->window)) != NULL) {
+		gc = cc->gc;
 		client_remove(cc);
+		if (gc != NULL)
+			client_retile_group(gc);
+	}
 }
 
 static void
--- conf.c
+++ conf.c
@@ -86,6 +86,7 @@
 	{ FUNC_CC(window-delete, client_close, 0) },
 	{ FUNC_CC(window-htile, client_htile, 0) },
 	{ FUNC_CC(window-vtile, client_vtile, 0) },
+	{ FUNC_CC(window-tile-none, client_tile_none, 0) },
 	{ FUNC_CC(window-stick, client_toggle_sticky, 0) },
 	{ FUNC_CC(window-fullscreen, client_toggle_fullscreen, 0) },
 	{ FUNC_CC(window-maximize, client_toggle_maximize, 0) },
@@ -145,6 +146,7 @@
 	{ FUNC_SC(group-cycle, group_cycle, (CWM_CYCLE_FORWARD)) },
 	{ FUNC_SC(group-rcycle, group_cycle, (CWM_CYCLE_REVERSE)) },
 	{ FUNC_SC(group-last, group_last, 0) },
+	{ FUNC_SC(group-tile-toggle, group_tile_toggle, 0) },
 	{ FUNC_SC(group-toggle-all, group_toggle_all, 0) },
 	{ FUNC_SC(group-toggle-1, group_toggle, 1) },
 	{ FUNC_SC(group-toggle-2, group_toggle, 2) },
@@ -291,6 +293,7 @@
 	c->mamount = 1;
 	c->htile = 50;
 	c->vtile = 50;
+	c->tilemode = TILE_NONE;
 	c->snapdist = 0;
 	c->ngroups = 0;
 	c->nameqlen = 5;
--- parse.y
+++ parse.y
@@ -71,7 +71,7 @@
 %token	BINDKEY UNBINDKEY BINDMOUSE UNBINDMOUSE
 %token	FONTNAME STICKY GAP
 %token	AUTOGROUP COMMAND IGNORE WM
-%token	YES NO BORDERWIDTH MOVEAMOUNT HTILE VTILE
+%token	YES NO BORDERWIDTH MOVEAMOUNT HTILE VTILE TILEMODE
 %token	COLOR SNAPDIST
 %token	ACTIVEBORDER INACTIVEBORDER URGENCYBORDER
 %token	GROUPBORDER UNGROUPBORDER
@@ -147,6 +147,22 @@
 			}
 			conf->vtile = $2;
 		}
+		| TILEMODE STRING {
+			if (strcmp($2, "none") == 0)
+				conf->tilemode = TILE_NONE;
+			else {
+				yyerror("invalid tilemode");
+				free($2);
+				YYERROR;
+			}
+			free($2);
+		}
+		| TILEMODE VTILE {
+			conf->tilemode = TILE_VTILE;
+		}
+		| TILEMODE HTILE {
+			conf->tilemode = TILE_HTILE;
+		}
 		| MOVEAMOUNT NUMBER {
 			if ($2 < 0 || $2 > INT_MAX) {
 				yyerror("invalid movemount");
@@ -351,6 +367,7 @@
 		{ "selfont", 		FONTSELCOLOR},
 		{ "snapdist",		SNAPDIST},
 		{ "sticky",		STICKY},
+		{ "tilemode",		TILEMODE},
 		{ "unbind-key",		UNBINDKEY},
 		{ "unbind-mouse",	UNBINDMOUSE},
 		{ "ungroupborder",	UNGROUPBORDER},
--- kbfunc.c
+++ kbfunc.c
@@ -406,6 +406,12 @@
 }
 
 void
+kbfunc_client_tile_none(void *ctx, struct cargs *cargs)
+{
+	client_tile_none(ctx);
+}
+
+void
 kbfunc_client_cycle(void *ctx, struct cargs *cargs)
 {
 	struct screen_ctx	*sc = ctx;
@@ -517,6 +523,12 @@
 }
 
 void
+kbfunc_group_tile_toggle(void *ctx, struct cargs *cargs)
+{
+	group_tile_toggle(ctx);
+}
+
+void
 kbfunc_group_close(void *ctx, struct cargs *cargs)
 {
 	group_close(ctx, cargs->flag);
--- cwmrc.5
+++ cwmrc.5
@@ -183,6 +183,29 @@
 can be used for applications such as
 .Xr xclock 1 ,
 where the user may wish to remain visible.
+.Pp
+When tiling
+.Po
+.Ic window-htile ,
+.Ic window-vtile ,
+or automatic
+.Ic tilemode
+.Pc ,
+.Ar left
+and
+.Ar bottom
+are reused as the spacing between tiled windows: horizontally-adjacent
+windows are separated by
+.Ar left ,
+and vertically-adjacent windows (including the gap between the master
+window and the rest) are separated by
+.Ar bottom .
+.Ar top
+and
+.Ar right
+are not used for inter-window spacing, since
+.Ar top
+is typically reserved for a status bar.
 .It Ic htile Ar percent
 Set the percentage of screen the master window should occupy
 after calling
@@ -230,6 +253,32 @@
 If set to 0, the vertical size of the master window will
 remain unchanged.
 The default is 50.
+.It Ic tilemode Ar vtile No | Ar htile No | Ar none
+Set the default automatic tile mode used by newly created groups.
+When set to
+.Ar vtile ,
+each new window is placed on the left of the screen and existing
+windows share the remaining space, as with
+.Ic window-vtile .
+When set to
+.Ar htile ,
+each new window is placed at the top of the screen and existing
+windows share the remaining space, as with
+.Ic window-htile .
+Opening or closing a window causes the remaining windows in its
+group to reflow and fill the screen.
+The tile mode of the current window's group may also be changed
+at runtime using the
+.Ic window-vtile ,
+.Ic window-htile ,
+and
+.Ic window-tile-none
+bind functions;
+this only affects that group, and does not change the
+.Ic tilemode
+default applied to groups created afterwards.
+The default is
+.Ar none .
 .It Ic wm Ar name path
 Every
 .Ar name
@@ -279,6 +328,21 @@
 Close all windows in group n, where n is 1-9.
 .It group-toggle-all
 Toggle visibility of all groups.
+.It group-tile-toggle
+Cycle the active group's automatic tile mode, in order:
+.Ar none ,
+.Ar vtile ,
+.Ar htile ,
+back to
+.Ar none .
+Acts on the group as a whole, so unlike
+.Ic window-htile
+and
+.Ic window-vtile
+it works even when no window currently has focus.
+See
+.Ic tilemode
+for details on each mode.
 .It window-group
 Toggle group membership of current window.
 .It window-movetogroup-[n]
@@ -327,12 +391,16 @@
 .Ar htile
 (default half) of the vertical screen space.
 Other windows in its group share remaining screen space.
+Enables automatic tiling for the current group.
 .It window-vtile
 Current window is placed on the left of the screen, maximized vertically
 and resized to
 .Ar vtile
 (default half) of the horizontal screen space.
 Other windows in its group share remaining screen space.
+Enables automatic tiling for the current group.
+.It window-tile-none
+Disable automatic tiling for the current group.
 .It window-move
 Move current window.
 .It window-resize