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);