foxygit / sxbar Log in
commit 0a58d137200c49c0f6657839f737aa161a55a587
Author:     MrJensK <jens.se@icloud.com>
AuthorDate: Thu Aug 6 18:44:08 2026 +0200
Commit:     MrJensK <jens.se@icloud.com>
CommitDate: Thu Aug 6 18:44:08 2026 +0200

    updates
---
 scripts/taskbar.sh |  55 +++++++++++++++++++++++----
 src/defs.h         |   5 +++
 src/sxbar.c        | 107 ++++++++++++++++++++++++++++++++++++++++-------------
 3 files changed, 135 insertions(+), 32 deletions(-)

diff --git a/scripts/taskbar.sh b/scripts/taskbar.sh
index 73ffa4b..963a7d0 100755
--- a/scripts/taskbar.sh
+++ b/scripts/taskbar.sh
@@ -6,10 +6,15 @@
 # _NET_ACTIVE_WINDOW client message (as of this writing, that means a
 # patched sxwm -- see the "workspace icons"-adjacent commit/PR that adds
 # a _NET_ACTIVE_WINDOW handler to hdl_client_msg()).
-#   taskbar.sh          -> internal listing, read fresh every refresh
-#                          interval -- not meant to be run by hand. One
-#                          line per current-workspace window:
-#                          "Title" : "0xWINDOW_ID" : "command"
+#   taskbar.sh list [x,y,w,h ...]
+#                       -> internal listing, read fresh every refresh
+#                          interval -- not meant to be run by hand. sxbar
+#                          passes one "x,y,w,h" geometry per monitor
+#                          (Xinerama order); this is how the list gets a
+#                          monitor tag per window without sxbar itself
+#                          having to peek at window geometry. One line per
+#                          current-workspace window:
+#                          "Title" : "0xWINDOW_ID" : "command" : monitor
 #   taskbar.sh focus ID -> activates/raises that window (bar click action)
 #
 # No `menu` case: unlike every other built-in module, taskbar renders its
@@ -27,15 +32,51 @@ focus)
 	;;
 list)
 	self="$0"
+	[ $# -gt 0 ] && shift
 	cur=$(wmctrl -d 2>/dev/null | awk '$2 == "*" { print $1 }')
 	[ -z "$cur" ] && exit 0
-	wmctrl -l 2>/dev/null | awk -v cur="$cur" -v self="$self" '
+	wmctrl -lG 2>/dev/null | awk -v cur="$cur" -v self="$self" -v geoms="$*" '
+		BEGIN { ngeom = split(geoms, garr, " ") }
+		# which monitor (0-based, matching sxbar geometry-arg order)
+		# contains window position (wx, wy). -1 only when sxbar gave us no
+		# geometries at all (e.g. a custom script not yet updated for this
+		# arg) -- that is the "show on every bar" sentinel. If geometries
+		# *were* given but wx,wy falls outside all of them (a floating
+		# window dragged past the edge of every monitor, a stale position
+		# from a since-changed monitor layout, etc.), fall back to
+		# whichever monitor centre is nearest, so every window still lands
+		# on exactly one bar instead of every bar.
+		function monitor_of(wx, wy,    i, n, mx, my, mw, mh, best, bestd, cx, cy, dx, dy, d) {
+			if (ngeom == 0)
+				return -1
+			for (i = 1; i <= ngeom; i++) {
+				n = split(garr[i], g, ",")
+				if (n != 4)
+					continue
+				mx = g[1]; my = g[2]; mw = g[3]; mh = g[4]
+				if (wx >= mx && wx < mx + mw && wy >= my && wy < my + mh)
+					return i - 1
+			}
+			best = -1
+			for (i = 1; i <= ngeom; i++) {
+				n = split(garr[i], g, ",")
+				if (n != 4)
+					continue
+				mx = g[1]; my = g[2]; mw = g[3]; mh = g[4]
+				cx = mx + mw / 2; cy = my + mh / 2
+				dx = wx - cx; dy = wy - cy
+				d = dx * dx + dy * dy
+				if (best == -1 || d < bestd) { best = i - 1; bestd = d }
+			}
+			return best
+		}
 		$2 == cur {
 			id = $1
+			mon = monitor_of($3, $4)
 			title = $0
-			sub(/^[^ \t]+[ \t]+[^ \t]+[ \t]+[^ \t]+[ \t]+/, "", title)
+			sub(/^([^ \t]+[ \t]+){7}/, "", title)
 			gsub(/"/, "'"'"'", title)
-			printf "\"%s\" : \"%s\" : \"%s focus %s\"\n", title, id, self, id
+			printf "\"%s\" : \"%s\" : \"%s focus %s\" : %d\n", title, id, self, id, mon
 		}'
 	;;
 esac
diff --git a/src/defs.h b/src/defs.h
index d11b2aa..d2368ce 100644
--- a/src/defs.h
+++ b/src/defs.h
@@ -82,6 +82,11 @@ typedef struct TaskbarEntry {
 	char *label;   /* window title */
 	char *command; /* spawned on click, e.g. "taskbar.sh focus 0x0140..." */
 	Window id;
+	int monitor;   /* which monitor (index into monitors[]) the script placed
+	               * this window on, or -1 if the script didn't say -- see
+	               * update_taskbar()'s geometry args and taskbar.sh's
+	               * `list` case, which does the actual window-to-monitor
+	               * matching in awk */
 } TaskbarEntry;

 typedef struct Module {
diff --git a/src/sxbar.c b/src/sxbar.c
index 9929f4d..e3af39f 100644
--- a/src/sxbar.c
+++ b/src/sxbar.c
@@ -529,7 +529,7 @@ void create_bars(void)
 			Atom A_STRUT = XInternAtom(dpy, "_NET_WM_STRUT_PARTIAL", False);
 			long strut[12] = {0};
 			if (bottom_bar) {
-				strut[3] = h + bw;
+				strut[3] = DisplayHeight(dpy, scr) - y + bw;
 				strut[10] = x;
 				strut[11] = x + w + 2 * bw - 1;
 			}
@@ -566,6 +566,28 @@ void create_bars(void)
 		bars[i].xft_draw = XftDrawCreate(dpy, bars[i].buffer, vis, cmap);
 }

+/* which of m's taskbar_entries live on the given monitor, so a taskbar on
+ * a multi-monitor setup only ever shows that monitor's own windows
+ * instead of duplicating the same full list on every bar. The actual
+ * window-to-monitor matching happens in the script (see update_taskbar()'s
+ * geometry args and taskbar.sh's `list` case) -- entries just carry
+ * whatever monitor index the script tagged them with, and -1 (a script
+ * that doesn't tag at all) always qualifies so unmodified custom taskbar
+ * scripts keep their old show-everywhere behaviour. Caller frees the
+ * returned array; *out_n receives its length (may be 0). */
+static int *taskbar_indices_for_monitor(Module *m, int monitor_index, int *out_n)
+{
+	int *idx = malloc(m->taskbar_entry_count * sizeof *idx);
+	int n = 0;
+	for (int e = 0; e < m->taskbar_entry_count; e++) {
+		int em = m->taskbar_entries[e].monitor;
+		if (em < 0 || em == monitor_index)
+			idx[n++] = e;
+	}
+	*out_n = n;
+	return idx;
+}
+
 static void draw_bar_into(int bar_idx)
 {
 	Bar *bar = &bars[bar_idx];
@@ -739,9 +761,12 @@ static void draw_bar_into(int bar_idx)
 		int tb_x = cur_x + total_left;
 		int tb_end = w - total_right - ver_w - 2 * config.text_padding - 2 * pad;
 		int tb_w = tb_end - tb_x;
-		int n = m->taskbar_entry_count;
-		if (tb_w <= 0 || n <= 0)
+		int idx_n;
+		int *idx = taskbar_indices_for_monitor(m, monitor_index, &idx_n);
+		if (tb_w <= 0 || idx_n <= 0) {
+			free(idx);
 			break;
+		}

 		Window active = None;
 		{
@@ -757,11 +782,11 @@ static void draw_bar_into(int bar_idx)
 			}
 		}

-		int seg_w = tb_w / n;
-		for (int e = 0; e < n; e++) {
-			TaskbarEntry *ent = &m->taskbar_entries[e];
+		int seg_w = tb_w / idx_n;
+		for (int e = 0; e < idx_n; e++) {
+			TaskbarEntry *ent = &m->taskbar_entries[idx[e]];
 			int seg_x = tb_x + e * seg_w;
-			int this_w = (e == n - 1) ? (tb_x + tb_w - seg_x) : seg_w;
+			int this_w = (e == idx_n - 1) ? (tb_x + tb_w - seg_x) : seg_w;
 			const char *label = ent->label ? ent->label : "";
 			int lw = text_width(label);
 			int avail = this_w - 2 * pad;
@@ -774,6 +799,7 @@ static void draw_bar_into(int bar_idx)
 				draw_ticker(d, &xft_fg, seg_x + pad, text_y, avail, label, lw, 0);
 			}
 		}
+		free(idx);
 		break;
 	}

@@ -966,12 +992,19 @@ static TaskbarEntry *taskbar_entry_at_x(int bar_idx, int x_click)
 	if (x_click < tb_x || x_click >= tb_end)
 		return NULL;

-	int n = tb->taskbar_entry_count;
-	int seg_w = (tb_end - tb_x) / n;
+	int idx_n;
+	int *idx = taskbar_indices_for_monitor(tb, bar->monitor, &idx_n);
+	if (idx_n <= 0) {
+		free(idx);
+		return NULL;
+	}
+	int seg_w = (tb_end - tb_x) / idx_n;
 	int e = (x_click - tb_x) / (seg_w > 0 ? seg_w : 1);
-	if (e >= n)
-		e = n - 1;
-	return &tb->taskbar_entries[e];
+	if (e >= idx_n)
+		e = idx_n - 1;
+	TaskbarEntry *ent = &tb->taskbar_entries[idx[e]];
+	free(idx);
+	return ent;
 }

 /* root-space geometry of a bar window, replicating create_bars()'s formula.
@@ -1721,13 +1754,18 @@ static void free_taskbar_entries(Module *m)
 	m->taskbar_entry_count = 0;
 }

-/* parses one "A" : "B" : "C" line -- the internal wire format
+/* parses one "A" : "B" : "C" [ : MON ] line -- the internal wire format
  * scripts/taskbar.sh's listing uses, not a user-facing sxbarc directive,
  * so this doesn't reuse parser.c's quote-parsing (scoped to config-file
- * directives). Outputs a/b/c are malloc'd (strdup) on success (0); returns -1
- * without allocating anything if the line doesn't match, so the caller
- * can just skip it. */
-static int parse_three_quoted(char *line, char **a, char **b, char **c)
+ * directives). The trailing MON field is a bare (unquoted) monitor index
+ * the script computed by matching the window's geometry against the
+ * monitor rectangles update_taskbar() passed it -- optional so a custom
+ * taskbar.sh that doesn't emit it still parses fine, just with *mon left
+ * at -1 (see taskbar_indices_for_monitor()'s handling of that sentinel).
+ * Outputs a/b/c are malloc'd (strdup) on success (0); returns -1 without
+ * allocating anything if the line doesn't match, so the caller can just
+ * skip it. */
+static int parse_three_quoted(char *line, char **a, char **b, char **c, int *mon)
 {
 	char *p = line;
 	char *fields[3];
@@ -1755,21 +1793,38 @@ static int parse_three_quoted(char *line, char **a, char **b, char **c)
 	*a = strdup(fields[0]);
 	*b = strdup(fields[1]);
 	*c = strdup(fields[2]);
+	while (*p == ' ' || *p == '\t' || *p == ':')
+		p++;
+	*mon = (*p == '\0') ? -1 : (int)strtol(p, NULL, 10);
 	return 0;
 }

 /* refreshes the built-in `taskbar` module's window-entry list by running
- * its script with "list" appended and parsing each line as "Title" :
- * "0xWindowID" : "command" -- see scripts/taskbar.sh. Unlike every other
- * module, m->cached_output is deliberately left NULL: every generic
- * per-module code path (draw_bar_into()'s layout/rendering loops,
- * module_at_x(), advance_marquees()) already skips modules with no
+ * its script with "list" plus one geometry arg per monitor ("x,y,w,h",
+ * in the same order as monitors[]/bars[].monitor) and parsing each line
+ * as "Title" : "0xWindowID" : "command" : monitor -- see scripts/taskbar.sh,
+ * which does the actual window-to-monitor matching itself (in awk,
+ * against those geometry args) rather than sxbar re-deriving it via X
+ * property queries; this is called once for all bars, not once per bar,
+ * so the monitor tag on each entry is what lets draw_bar_into()/
+ * taskbar_entry_at_x() show/click only the entries for their own bar.
+ * Unlike every other module, m->cached_output is deliberately left NULL:
+ * every generic per-module code path (draw_bar_into()'s layout/rendering
+ * loops, module_at_x(), advance_marquees()) already skips modules with no
  * cached_output, which is exactly what we want here, since taskbar
  * renders itself in its own dedicated block/click-handling instead. */
 static void update_taskbar(Module *m)
 {
-	char cmd[PATH_MAX + 8];
-	snprintf(cmd, sizeof cmd, "%s list", m->command);
+	char geoms[MAX_MONITORS * 32] = "";
+	for (int i = 0; i < nmonitors; i++) {
+		char part[32];
+		snprintf(part, sizeof part, " %d,%d,%d,%d", monitors[i].x_org, monitors[i].y_org,
+		         monitors[i].width, monitors[i].height);
+		strncat(geoms, part, sizeof geoms - strlen(geoms) - 1);
+	}
+
+	char cmd[PATH_MAX + sizeof geoms + 8];
+	snprintf(cmd, sizeof cmd, "%s list%s", m->command, geoms);

 	FILE *fp = popen(cmd, "r");
 	if (!fp)
@@ -1784,7 +1839,8 @@ static void update_taskbar(Module *m)
 			*nl = '\0';

 		char *title, *idstr, *action;
-		if (parse_three_quoted(line, &title, &idstr, &action) < 0)
+		int mon;
+		if (parse_three_quoted(line, &title, &idstr, &action, &mon) < 0)
 			continue;

 		if (m->taskbar_entry_count >= max) {
@@ -1803,6 +1859,7 @@ static void update_taskbar(Module *m)
 		e->label = title;
 		e->command = action;
 		e->id = (Window)strtoul(idstr, NULL, 0); /* base 0: "0x..." parses as hex */
+		e->monitor = mon;
 		free(idstr);
 	}
 	pclose(fp);