commit 851573b36390f7279b9aea5c34d12e8e49203197
Author: MrJensK <jens.se@icloud.com>
AuthorDate: Thu Jul 30 11:33:25 2026 +0200
Commit: MrJensK <jens.se@icloud.com>
CommitDate: Thu Jul 30 11:33:25 2026 +0200
more!
---
default_sxbarc | 21 ++++++++++-------
docs/wiki.html | 23 ++++++++++++-------
src/defs.h | 5 ++++-
src/parser.c | 35 +++++++++++++++++++++++++++++
src/sxbar.c | 71 +++++++++++++++++++++++++++++++++++++++++++---------------
sxbar.1 | 10 +++++----
6 files changed, 126 insertions(+), 39 deletions(-)
diff --git a/default_sxbarc b/default_sxbarc
index abdf555..e44c983 100644
--- a/default_sxbarc
+++ b/default_sxbarc
@@ -268,7 +268,7 @@ module : taskbar : false : 1
# defaults to `hover` for a consistent feel, but `click` remains available
# if you'd rather a menu only appear on a deliberate click:
# text -- purely informational, not clickable at all (popup_info)
-# button -- runs its own command and closes the popup on click (popup_item)
+# button -- runs its own command on click; the popup stays open (popup_item)
# slider -- a draggable 0-100% track; dragging doesn't close the popup (popup_set)
# image -- renders an image file, e.g. album art; not clickable (popup_image)
# buttons -- one row split into N equal-width button segments side by
@@ -278,10 +278,10 @@ module : taskbar : false : 1
# popup : module_name : hover|click : buttons|slider
# hover opens the popup while the pointer is over the module (and closes
# it when the pointer leaves both the module and the popup); click opens
-# it on left-click and closes it again on a second click, on clicking a
-# button row, or on clicking anywhere else (like an ordinary dropdown
-# menu). The third field only decides "does this module have a popup at
-# all" these days -- popup_item/popup_info/popup_image/popup_buttons/
+# it on left-click and closes it again on a second click, or on clicking
+# anywhere else (like an ordinary dropdown menu). The third field only
+# decides "does this module have a popup at all" these days --
+# popup_item/popup_info/popup_image/popup_buttons/
# popup_set decide what's in it, in any combination, regardless of which
# of `buttons`/`slider` you put here, so either word works.
#
@@ -300,17 +300,22 @@ module : taskbar : false : 1
# popup_image : module_name : "shell command" -- one image row. The
# command's stdout is a path to a local image file, re-run fresh every
# time the popup opens (e.g. current track's album art); scaled to fit a
-# 160px box, preserving aspect ratio. Empty/failed output just means no
-# image that time -- the row stays, it renders blank. Not clickable.
+# 160px box (see popup_image_size below to change that), preserving
+# aspect ratio. Empty/failed output just means no image that time -- the
+# row stays, it renders blank. Not clickable.
# No extra build dependency -- image decoding is a vendored, compiled-in
# library (see README). An image (or slider, or buttons) row anchors
# the popup's width -- any popup_info/popup_item text in the same popup
# is then capped to that width and scrolls (marquee) if it's longer,
# instead of stretching the popup past the image.
#
+# popup_image_size : module_name : pixels -- changes the square box a
+# module's popup_image row(s) scale their art into (default: 160).
+# Example: popup_image_size : media : 220
+#
# popup_buttons : module_name : "Label1" : "cmd1" : "Label2" : "cmd2" ...
# one row split into N equal-width button segments, each running its own
-# command and closing the popup on click -- same behaviour as popup_item
+# command on click; the popup stays open -- same behaviour as popup_item
# but laid out side by side instead of stacked. Needs an even number of
# quoted label/command pairs; glyphs work well as labels here (see the
# media example below) so you get an icon row instead of stacked text.
diff --git a/docs/wiki.html b/docs/wiki.html
index f634ee9..b031c0c 100644
--- a/docs/wiki.html
+++ b/docs/wiki.html
@@ -835,8 +835,8 @@ status=$(cat /sys/class/power_supply/BAT*/status 2>/dev/null | head -n1)
<div class="card">
<div class="eyebrow">click</div>
<h4>Opens on left-click</h4>
- <p>Toggles on click; closes again on a second click, on clicking a button
- row, or on clicking anywhere else — like an ordinary dropdown menu.</p>
+ <p>Toggles on click; closes again on a second click, or on clicking
+ anywhere else — like an ordinary dropdown menu.</p>
</div>
</div>
<div class="note">Hovering only <em>reveals</em> a menu — an actual click on a
@@ -859,7 +859,7 @@ status=$(cat /sys/class/power_supply/BAT*/status 2>/dev/null | head -n1)
<div class="card">
<div class="eyebrow">button</div>
<h4>popup_item</h4>
- <p>Runs its own command and closes the popup on click.</p>
+ <p>Runs its own command on click; the popup stays open.</p>
</div>
<div class="card">
<div class="eyebrow">slider</div>
@@ -871,10 +871,12 @@ status=$(cat /sys/class/power_supply/BAT*/status 2>/dev/null | head -n1)
<div class="eyebrow">image</div>
<h4>popup_image</h4>
<p>Renders an image file, e.g. album art. Command's stdout is a path to a
- local image, re-run fresh every popup open, scaled to fit a 160px box.
- Not clickable. No extra build dependency needed. Anchors the popup's
- width — any text row in the same popup is capped to it and scrolls
- instead of stretching the popup wider than the image.</p>
+ local image, re-run fresh every popup open, scaled to fit a 160px box
+ by default — override per module with
+ <code class="inline">popup_image_size</code>. Not clickable. No extra
+ build dependency needed. Anchors the popup's width — any text row in
+ the same popup is capped to it and scrolls instead of stretching the
+ popup wider than the image.</p>
</div>
<div class="card">
<div class="eyebrow">buttons</div>
@@ -882,13 +884,14 @@ status=$(cat /sys/class/power_supply/BAT*/status 2>/dev/null | head -n1)
<p>One row split into N equal-width button segments side by side (e.g.
media transport controls), instead of N stacked full-width
<code class="inline">popup_item</code> rows. Each segment runs its own
- command and closes the popup on click.</p>
+ command on click; the popup stays open.</p>
</div>
</div>
<pre><span class="k">popup_item</span> : module_name : <span class="s">"Label"</span> : <span class="s">"command"</span> <span class="c"># button row, repeatable</span>
<span class="k">popup_info</span> : module_name : <span class="s">"shell command"</span> <span class="c"># text row, repeatable</span>
<span class="k">popup_image</span> : module_name : <span class="s">"shell command"</span> <span class="c"># image row, stdout = path to an image file</span>
+<span class="k">popup_image_size</span> : module_name : pixels <span class="c"># image row box size, default 160</span>
<span class="k">popup_buttons</span> : module_name : <span class="s">"L1"</span> : <span class="s">"cmd1"</span> : <span class="s">"L2"</span> : <span class="s">"cmd2"</span> ... <span class="c"># one row, N button segments</span>
<span class="k">popup_set</span> : module_name : <span class="s">"command"</span> <span class="c"># slider row, receives new value as $1 (e.g. "45%")</span></pre>
@@ -1005,6 +1008,9 @@ status=$(cat /sys/class/power_supply/BAT*/status 2>/dev/null | head -n1)
<span class="k">popup_image </span>: media : <span class="s">"~/.config/sxbar/scripts/media.sh art"</span>
<span class="k">popup_info </span>: media : <span class="s">"~/.config/sxbar/scripts/media.sh track"</span>
<span class="k">popup_buttons </span>: media : <span class="s">"<step-backward>"</span> : <span class="s">"~/.config/sxbar/scripts/media.sh prev"</span> : <span class="s">"<play>"</span> : <span class="s">"~/.config/sxbar/scripts/media.sh playpause"</span> : <span class="s">"<step-forward>"</span> : <span class="s">"~/.config/sxbar/scripts/media.sh next"</span></pre>
+ <p>Bigger album art? Add
+ <code class="inline">popup_image_size : media : 220</code> alongside the
+ lines above — any pixel size, default is 160.</p>
</section>
<section id="example">
@@ -1078,6 +1084,7 @@ status=$(cat /sys/class/power_supply/BAT*/status 2>/dev/null | head -n1)
<tr><td class="mono">popup_item</td><td class="mono">popup_item : name : "Label" : "command"</td></tr>
<tr><td class="mono">popup_info</td><td class="mono">popup_info : name : "shell command"</td></tr>
<tr><td class="mono">popup_image</td><td class="mono">popup_image : name : "shell command"</td></tr>
+ <tr><td class="mono">popup_image_size</td><td class="mono">popup_image_size : name : pixels</td></tr>
<tr><td class="mono">popup_set</td><td class="mono">popup_set : name : "command"</td></tr>
<tr><td class="mono">height / bottom_bar / *_padding / border*</td><td>global bar geometry — see <a href="#bars">Global & bar options</a></td></tr>
<tr><td class="mono">background_colour / foreground_colour / border_colour / font</td><td>global appearance</td></tr>
diff --git a/src/defs.h b/src/defs.h
index d528e09..d11b2aa 100644
--- a/src/defs.h
+++ b/src/defs.h
@@ -28,7 +28,7 @@
#define POPUP_TRIGGER_HOVER 1
#define POPUP_ROW_TEXT 0 /* purely informational -- not clickable at all */
-#define POPUP_ROW_BUTTON 1 /* spawns command and closes the popup on click */
+#define POPUP_ROW_BUTTON 1 /* spawns command; popup stays open on click */
#define POPUP_ROW_SLIDER 2 /* draggable 0-100% track; doesn't close on click/drag */
#define POPUP_ROW_IMAGE 3 /* renders an image file (e.g. album art); not clickable */
#define POPUP_ROW_BUTTONS 4 /* one row split into N equal-width button segments,
@@ -114,6 +114,9 @@ typedef struct Module {
PopupItem *popup_items;
int popup_item_count;
int popup_item_max;
+ int popup_image_size; /* IMAGE rows scale to fit within this square box,
+ * in pixels; 0 = built-in default (see
+ * POPUP_IMAGE_SIZE in sxbar.c) */
int popup_items_from_config; /* config popup_item/popup_info lines replace the built-in defaults, once */
int slider_item_idx; /* which popup_items[] entry popup_set manages, or -1 */
/* the built-in `taskbar` module only: renders one clickable segment
diff --git a/src/parser.c b/src/parser.c
index 8559200..953b00a 100644
--- a/src/parser.c
+++ b/src/parser.c
@@ -351,6 +351,21 @@ static int apply_popup_image(Module *m, char *rest, const char *ctx, int lineno,
return 0;
}
+static int apply_popup_image_size(Module *m, char *rest, const char *ctx, int lineno)
+{
+ /* rest: pixels -- caps the square box popup_image rows scale their art
+ * into for this module (default: the built-in POPUP_IMAGE_SIZE) */
+ char *val = strip(rest);
+ strip_comment(val);
+ int px = atoi(val);
+ if (px <= 0) {
+ fprintf(stderr, "%s:%d: popup_image_size must be a positive number\n", ctx, lineno);
+ return -1;
+ }
+ m->popup_image_size = px;
+ return 0;
+}
+
/* free a partially- or fully-built button array on a parse error, before
* returning -- nothing has been attached to a Module yet at that point */
static void free_parsed_buttons(PopupButton *btns, int count)
@@ -544,6 +559,8 @@ void load_popup_from_script(Module *m, const char *script_path)
apply_popup_info(m, rest, ctx, lineno, 0);
} else if (!strcmp(key, "popup_image")) {
apply_popup_image(m, rest, ctx, lineno, 0);
+ } else if (!strcmp(key, "popup_image_size")) {
+ apply_popup_image_size(m, rest, ctx, lineno);
} else if (!strcmp(key, "popup_buttons")) {
apply_popup_buttons(m, rest, ctx, lineno, 0);
} else if (!strcmp(key, "popup_set")) {
@@ -941,6 +958,24 @@ int parse_config(Config *cfg)
continue;
}
apply_popup_image(m, after, "sxbarc", lineno, 1);
+ } else if (!strcmp(key, "popup_image_size")) {
+ /* popup_image_size : module_name : pixels -- caps the square box
+ * this module's popup_image rows scale their art into (default:
+ * the built-in POPUP_IMAGE_SIZE, currently 160) */
+ char *p1 = strchr(rest, ':');
+ if (!p1) {
+ fprintf(stderr, "sxbarc:%d: popup_image_size missing name and value\n", lineno);
+ continue;
+ }
+ *p1 = '\0';
+ char *name = strip(rest);
+ char *val = strip(p1 + 1);
+ Module *m = find_module(cfg, name);
+ if (!m) {
+ fprintf(stderr, "sxbarc:%d: popup_image_size: unknown module '%s'\n", lineno, name);
+ continue;
+ }
+ apply_popup_image_size(m, val, "sxbarc", lineno);
} else if (!strcmp(key, "popup_buttons")) {
/* popup_buttons : module_name : "Label1" : "command1" : "Label2" :
* "command2" ... -- one row split into N equal-width button
diff --git a/src/sxbar.c b/src/sxbar.c
index d747710..1bc62e0 100644
--- a/src/sxbar.c
+++ b/src/sxbar.c
@@ -40,6 +40,7 @@ void hdl_dummy(XEvent *xev);
void hdl_expose(XEvent *xev);
void hdl_motion(XEvent *xev);
void hdl_property(XEvent *xev);
+void hdl_visibility(XEvent *xev);
void init_defaults(void);
void init_modules(void);
unsigned long parse_col(const char *hex);
@@ -365,8 +366,8 @@ static XImage *rgba_to_ximage(const unsigned char *rgba, int w, int h)
}
/* decode `path` (any format stb_image supports -- JPEG/PNG/GIF/BMP/...),
- * scale it down (preserving aspect ratio) to fit within a `box`-pixel
- * square if larger, and return it as an XImage ready for XPutImage().
+ * scale it (preserving aspect ratio, up or down) to fit within a
+ * `box`-pixel square, and return it as an XImage ready for XPutImage().
* NULL on any failure (missing/corrupt/unreadable file). */
static void *load_scaled_image(const char *path, int box, int *out_w, int *out_h)
{
@@ -375,12 +376,9 @@ static void *load_scaled_image(const char *path, int box, int *out_w, int *out_h
if (!pixels)
return NULL;
- int dw = iw, dh = ih;
- if (iw > box || ih > box) {
- double scale = iw > ih ? (double)box / iw : (double)box / ih;
- dw = (int)(iw * scale); if (dw < 1) dw = 1;
- dh = (int)(ih * scale); if (dh < 1) dh = 1;
- }
+ double scale = iw > ih ? (double)box / iw : (double)box / ih;
+ int dw = (int)(iw * scale); if (dw < 1) dw = 1;
+ int dh = (int)(ih * scale); if (dh < 1) dh = 1;
unsigned char *final = pixels;
int scaled_ourselves = 0;
@@ -1007,12 +1005,12 @@ static int popup_row_height(void)
return font->ascent + font->descent + 2 * POPUP_ROW_PAD;
}
-static int popup_item_height(PopupItem *it)
+static int popup_item_height(PopupItem *it, int img_size)
{
if (it->type == POPUP_ROW_SLIDER)
return popup_row_height() + POPUP_ROW_PAD + POPUP_TRACK_H + POPUP_ROW_PAD;
if (it->type == POPUP_ROW_IMAGE)
- return (it->image_h > 0 ? it->image_h : POPUP_IMAGE_SIZE) + 2 * POPUP_ROW_PAD;
+ return (it->image_h > 0 ? it->image_h : img_size) + 2 * POPUP_ROW_PAD;
return popup_row_height();
}
@@ -1020,27 +1018,30 @@ static int popup_item_height(PopupItem *it)
* and slider rows can all be mixed, each with its own height) */
static int popup_total_height(Module *m)
{
+ int img_size = m->popup_image_size > 0 ? m->popup_image_size : POPUP_IMAGE_SIZE;
int h = 0;
for (int i = 0; i < m->popup_item_count; i++)
- h += popup_item_height(&m->popup_items[i]);
+ h += popup_item_height(&m->popup_items[i], img_size);
return h > 0 ? h : popup_row_height();
}
/* y offset (popup-window-relative) where row idx starts */
static int popup_row_y(Module *m, int idx)
{
+ int img_size = m->popup_image_size > 0 ? m->popup_image_size : POPUP_IMAGE_SIZE;
int y = 0;
for (int i = 0; i < idx; i++)
- y += popup_item_height(&m->popup_items[i]);
+ y += popup_item_height(&m->popup_items[i], img_size);
return y;
}
/* which row (if any) contains popup-window-relative y, or -1 */
static int popup_row_at_y(Module *m, int y)
{
+ int img_size = m->popup_image_size > 0 ? m->popup_image_size : POPUP_IMAGE_SIZE;
int cy = 0;
for (int i = 0; i < m->popup_item_count; i++) {
- int rh = popup_item_height(&m->popup_items[i]);
+ int rh = popup_item_height(&m->popup_items[i], img_size);
if (y >= cy && y < cy + rh)
return i;
cy += rh;
@@ -1142,7 +1143,7 @@ static void popup_draw(void)
int avail = popup.w - 2 * POPUP_PAD;
int lw = text_width(label);
if (it->type == POPUP_ROW_BUTTON && i == popup.hover_row) {
- int row_h = popup_item_height(it);
+ int row_h = popup_item_height(it, m->popup_image_size > 0 ? m->popup_image_size : POPUP_IMAGE_SIZE);
XSetForeground(dpy, gc, config.foreground_colour);
XFillRectangle(dpy, popup.buffer, gc, 0, ry, popup.w, row_h);
draw_ticker(popup.xft_draw, &xft_bg, POPUP_PAD, text_y, avail, label, lw, it->scroll_offset);
@@ -1247,7 +1248,8 @@ static void popup_open(int bar_idx, Module *m, int anchor_x)
char *path = it->image_command ? run_command(it->image_command) : NULL;
if (path && *path) {
int iw, ih;
- it->image = load_scaled_image(path, POPUP_IMAGE_SIZE, &iw, &ih);
+ int img_size = m->popup_image_size > 0 ? m->popup_image_size : POPUP_IMAGE_SIZE;
+ it->image = load_scaled_image(path, img_size, &iw, &ih);
if (it->image) {
it->image_w = iw;
it->image_h = ih;
@@ -1288,7 +1290,7 @@ static void popup_open(int bar_idx, Module *m, int anchor_x)
.override_redirect = True,
.background_pixel = config.background_colour,
.event_mask = ExposureMask | ButtonPressMask | ButtonReleaseMask |
- PointerMotionMask | LeaveWindowMask,
+ PointerMotionMask | LeaveWindowMask | VisibilityChangeMask,
};
Window win = XCreateWindow(dpy, root, x, y, w, h, 0, CopyFromParent, InputOutput,
DefaultVisual(dpy, scr),
@@ -1345,9 +1347,11 @@ static void popup_handle_button(XEvent *xev)
PopupItem *it = &m->popup_items[row];
switch (it->type) {
case POPUP_ROW_BUTTON:
+ /* run the command but leave the popup open -- it only closes once
+ * the pointer actually leaves the popup/bar area (hdl_crossing) or
+ * the user clicks outside it (hdl_button) */
if (it->command && *it->command)
spawn(it->command);
- popup_close();
break;
case POPUP_ROW_SLIDER:
popup.dragging_row = row;
@@ -1360,7 +1364,6 @@ static void popup_handle_button(XEvent *xev)
seg = it->button_count - 1;
if (seg >= 0 && it->buttons[seg].command && *it->buttons[seg].command)
spawn(it->buttons[seg].command);
- popup_close();
break;
}
default:
@@ -1500,6 +1503,23 @@ void hdl_crossing(XEvent *xev)
bar_root_geometry(popup.bar_idx, &bx, &by, &bw, &bh);
if (root_x >= bx && root_x < bx + bw && root_y >= by && root_y < by + bh)
return;
+
+ /* the thin POPUP_GAP strip between the bar and the popup -- an
+ * imprecise/fast mouse move can land there for a frame on its way
+ * from one to the other. Tolerate it (same x-range as the popup)
+ * instead of treating it as "left both", so the visual gap stays
+ * but doesn't act as dead space that closes the popup. */
+ int gap_y0, gap_y1;
+ if (popup.y + popup.h <= by) {
+ gap_y0 = popup.y + popup.h;
+ gap_y1 = by;
+ } else {
+ gap_y0 = by + bh;
+ gap_y1 = popup.y;
+ }
+ if (root_x >= popup.x && root_x < popup.x + popup.w &&
+ root_y >= gap_y0 && root_y < gap_y1)
+ return;
}
popup_close();
}
@@ -1515,6 +1535,20 @@ void hdl_expose(XEvent *xev)
redraw_bar(idx);
}
+/* the popup is an override-redirect window, so the window manager never
+ * restacks it on our behalf -- if some other (WM-managed) window gets
+ * raised while the popup is open (e.g. focus-follows-mouse raising the
+ * window the pointer just passed over on its way to the next module),
+ * that window ends up on top of the popup even though we raised it first.
+ * Re-raise whenever we notice we've been covered. */
+void hdl_visibility(XEvent *xev)
+{
+ if (!popup.open || xev->xvisibility.window != popup.win)
+ return;
+ if (xev->xvisibility.state != VisibilityUnobscured)
+ XRaiseWindow(dpy, popup.win);
+}
+
void hdl_property(XEvent *xev)
{
if (xev->xproperty.atom == XInternAtom(dpy, "_NET_CURRENT_DESKTOP", False)) {
@@ -1875,6 +1909,7 @@ void setup(void)
evtable[MotionNotify] = hdl_motion;
evtable[LeaveNotify] = hdl_crossing;
evtable[PropertyNotify] = hdl_property;
+ evtable[VisibilityNotify] = hdl_visibility;
XSelectInput(dpy, root, PropertyChangeMask);
init_defaults();
diff --git a/sxbar.1 b/sxbar.1
index 7d4355d..0d89393 100644
--- a/sxbar.1
+++ b/sxbar.1
@@ -331,8 +331,8 @@ command. Only one popup is open at a time.
opens the popup while the pointer is over the module, closing it when the
pointer leaves both the module and the popup;
.B click
-opens it on left-click and closes it again on a second click, on clicking a
-button row, or on clicking elsewhere. The third field only decides whether
+opens it on left-click and closes it again on a second click, or on clicking
+elsewhere. The third field only decides whether
the module has a popup at all \(en
.BR popup_item / popup_info / popup_set
below decide what is actually in it, in any combination, so either word
@@ -341,7 +341,9 @@ works there.
.BR "popup_item : name : \(dqLabel\(dq : \(dqcommand\(dq"
A button row: runs
.I command
-(detached) and closes the popup, on click. Repeatable.
+(detached) on click; the popup stays open (see
+.B POPUPS
+above for how it closes). Repeatable.
.TP
.BR "popup_info : name : \(dqcommand\(dq"
A purely informational text row: its label is
@@ -372,7 +374,7 @@ One row split into N equal-width button segments side by side, e.g. media
transport controls, instead of N stacked full-width
.B popup_item
rows. Needs an even number of quoted label/command pairs; each segment
-runs its own command and closes the popup on click, same as
+runs its own command on click and the popup stays open, same as
.BR popup_item .
Glyphs work well as segment labels here.
.TP