commit 36c837d0d60d0adb2930047614fbdf1b07488269
Author: MrJensK <jens.se@icloud.com>
AuthorDate: Sat Aug 1 16:45:00 2026 +0200
Commit: MrJensK <jens.se@icloud.com>
CommitDate: Sat Aug 1 16:45:00 2026 +0200
added live button funktion
---
README.md | 47 ++++++++++++++++++++++++++++++++
docs/wiki.html | 25 +++++++++++++++--
src/parser.c | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/sxbar.c | 10 +++++++
4 files changed, 164 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 2b4152b..806d331 100644
--- a/README.md
+++ b/README.md
@@ -1114,4 +1114,51 @@ for every module.
`popup_image`/`media` support, or roughly 220 KB with the vendored
`stb_image.h` decoder compiled in -- still just the one binary, no
extra shared library installed alongside it.
+
+## Live button labels (`popup_live_item`)
+
+`popup_item` buttons have always had a fixed label, set once when the
+config (or a module script's `menu` output) is parsed. Showing a button's
+own current state (e.g. a toggle whose label reads "Lid-suspend: OFF")
+meant pairing a separate `popup_info` text row with a `popup_item` button
+next to it, since only `popup_info`/`popup_image` rows re-run their
+command and refresh on every popup open. `popup_live_item` collapses that
+into one row: same click behaviour as `popup_item`, but the label is a
+shell command re-run fresh every time the popup opens, same live
+convention as `popup_info`.
+
+### Changes
+- Added `apply_popup_live_item()` (`src/parser.c`), parsed the same way as
+ `apply_popup_item()` -- two quoted fields -- except the first field is
+ stored as `label_command` (re-run on every `popup_open()`, see
+ `popup_info`) instead of a static `label`. The click field still becomes
+ `command`, unchanged from `popup_item`.
+- No changes needed in `sxbar.c`: `popup_open()`'s per-row refresh already
+ keyed off `label_command` being set rather than the row's type, so a
+ `POPUP_ROW_BUTTON` with a `label_command` was already handled correctly
+ once the parser could produce one.
+- Wired into both directive paths, same as every other popup row: sxbarc
+ (`popup_live_item : module_name : "label command" : "command"`) and a
+ module script's own `menu` subcommand (`popup_live_item : "label
+ command" : "command"`, no module-name field).
+- The first `popup_item`/`popup_live_item`/`popup_info`/`popup_image`/
+ `popup_buttons` line for a module still clears its built-in default
+ rows -- `popup_live_item` joins that shared group.
+
+### Config syntax
+```
+popup_item : module_name : "Label" : "command" # button row, fixed label
+popup_live_item : module_name : "label command" : "command" # button row, label re-run on every open
+```
+
+### Example
+```
+popup_live_item : lidsuspend : "lidsuspend status | grep -q '^ON' && echo 'Lid-suspend: ON' || echo 'Lid-suspend: OFF'" : "lidsuspend status | grep -q '^ON' && lidsuspend off || lidsuspend on"
+```
+
+### Result
+- A single button row can now show its own live status instead of needing
+ a separate informational row next to it.
+- Existing `popup_item` rows are unaffected -- `popup_live_item` is a new,
+ additive directive.
- The source tree is also small, so `sxbar` is ideal for minimal Xorg setups.
diff --git a/docs/wiki.html b/docs/wiki.html
index 279d7d1..e004a4e 100644
--- a/docs/wiki.html
+++ b/docs/wiki.html
@@ -876,7 +876,18 @@ 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 on click; the popup stays open.</p>
+ <p>Runs its own command on click; the popup stays open. Label is fixed
+ text, set once.</p>
+ </div>
+ <div class="card">
+ <div class="eyebrow">button</div>
+ <h4>popup_live_item</h4>
+ <p>Same as <code class="inline">popup_item</code>, but the label is a
+ shell command's output instead of fixed text, re-run fresh every time
+ the popup opens (like <code class="inline">popup_info</code>) — still
+ clickable, its own click command spawns independently of the label.
+ Use this for a single button that shows its own current state, e.g.
+ "Lid-suspend: OFF" that toggles when clicked.</p>
</div>
<div class="card">
<div class="eyebrow">slider</div>
@@ -906,19 +917,29 @@ status=$(cat /sys/class/power_supply/BAT*/status 2>/dev/null | head -n1)
</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_live_item</span> : module_name : <span class="s">"label command"</span> : <span class="s">"command"</span> <span class="c"># button row, label re-run on every open</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>
- <p>The first <code class="inline">popup_item</code>/<code class="inline">popup_info</code>/
+ <p>The first <code class="inline">popup_item</code>/<code class="inline">popup_live_item</code>/
+ <code class="inline">popup_info</code>/
<code class="inline">popup_image</code>/<code class="inline">popup_buttons</code> line for
a module replaces its built-in default rows (if any, including a
<code class="inline">popup_set</code> slider row) — later lines append. Add
<code class="inline">popup_set</code> again afterwards if you cleared a built-in
slider this way and still want it back.</p>
+ <p>A module script's own <code class="inline">menu</code> subcommand can emit
+ <code class="inline">popup_live_item</code> the same way (no module-name field,
+ same as its other popup directives):</p>
+ <pre><span class="k">popup_live_item</span> : <span class="s">"~/.config/sxbar/scripts/lidsuspend.sh status"</span> : <span class="s">"~/.config/sxbar/scripts/lidsuspend.sh toggle"</span></pre>
+ <p>The label command's output becomes the button text on every popup open
+ (e.g. <code class="inline">"Lid-suspend: OFF"</code>), and clicking it runs the
+ second command regardless of what the label currently says.</p>
+
<p>Example combining all five on one module (<code class="inline">mymodule</code>
resolving to your own <code class="inline">~/.config/sxbar/scripts/mymodule.sh</code>):</p>
<pre><span class="k">module</span> : mymodule : <span class="v">true</span> : 5
diff --git a/src/parser.c b/src/parser.c
index ff8b411..d1ab803 100644
--- a/src/parser.c
+++ b/src/parser.c
@@ -292,6 +292,69 @@ static int apply_popup_item(Module *m, char *rest, const char *ctx, int lineno,
return 0;
}
+/* popup_live_item : "label command" : "click command" -- a BUTTON row whose
+ * label is re-run fresh (via label_command) every time the popup opens,
+ * same live convention as popup_info, but still clickable: its own
+ * click command spawns independently of the label. Use this instead of
+ * popup_item when the row needs to show live status on the button itself
+ * rather than (or in addition to) a separate popup_info row. */
+static int apply_popup_live_item(Module *m, char *rest, const char *ctx, int lineno, int from_sxbarc)
+{
+ char *after = strip(rest);
+ if (*after != '"' && *after != '\'') {
+ fprintf(stderr, "%s:%d: popup_live_item label command must be quoted\n", ctx, lineno);
+ return -1;
+ }
+ char q = *after;
+ char *label_start = after + 1;
+ char *closing = strchr(label_start, q);
+ if (!closing) {
+ fprintf(stderr, "%s:%d: popup_live_item label command missing closing quote\n", ctx, lineno);
+ return -1;
+ }
+ *closing = '\0';
+
+ char *tail = strip(closing + 1);
+ if (*tail != ':') {
+ fprintf(stderr, "%s:%d: popup_live_item missing command\n", ctx, lineno);
+ return -1;
+ }
+ tail = strip(tail + 1);
+ if (*tail != '"' && *tail != '\'') {
+ fprintf(stderr, "%s:%d: popup_live_item command must be quoted\n", ctx, lineno);
+ return -1;
+ }
+ q = *tail;
+ char *cmd_start = tail + 1;
+ closing = strchr(cmd_start, q);
+ if (!closing) {
+ fprintf(stderr, "%s:%d: popup_live_item command missing closing quote\n", ctx, lineno);
+ return -1;
+ }
+ *closing = '\0';
+
+ if (from_sxbarc)
+ clear_builtin_popup_items(m);
+ if (grow_popup_items(m) < 0) {
+ fprintf(stderr, "%s: out of memory\n", ctx);
+ return -1;
+ }
+ m->popup_items[m->popup_item_count].type = POPUP_ROW_BUTTON;
+ m->popup_items[m->popup_item_count].label = NULL;
+ m->popup_items[m->popup_item_count].command = expand_home(cmd_start);
+ m->popup_items[m->popup_item_count].label_command = expand_home(label_start);
+ m->popup_items[m->popup_item_count].set_command = NULL;
+ m->popup_items[m->popup_item_count].image_command = NULL;
+ m->popup_items[m->popup_item_count].image = NULL;
+ m->popup_items[m->popup_item_count].image_w = 0;
+ m->popup_items[m->popup_item_count].image_h = 0;
+ m->popup_items[m->popup_item_count].buttons = NULL;
+ m->popup_items[m->popup_item_count].button_count = 0;
+ m->popup_items[m->popup_item_count].scroll_offset = 0;
+ m->popup_item_count++;
+ return 0;
+}
+
static int apply_popup_info(Module *m, char *rest, const char *ctx, int lineno, int from_sxbarc)
{
char *after = strip(rest);
@@ -577,6 +640,8 @@ void load_popup_from_script(Module *m, const char *script_path)
apply_popup(m, rest, ctx, lineno);
} else if (!strcmp(key, "popup_item")) {
apply_popup_item(m, rest, ctx, lineno, 0);
+ } else if (!strcmp(key, "popup_live_item")) {
+ apply_popup_live_item(m, rest, ctx, lineno, 0);
} else if (!strcmp(key, "popup_info")) {
apply_popup_info(m, rest, ctx, lineno, 0);
} else if (!strcmp(key, "popup_image")) {
@@ -906,6 +971,25 @@ int parse_config(Config *cfg)
continue;
}
apply_popup_item(m, after, "sxbarc", lineno, 1);
+ } else if (!strcmp(key, "popup_live_item")) {
+ /* popup_live_item : module_name : "label command" : "click
+ * command" -- a BUTTON row whose label is re-run fresh every
+ * time the popup opens (like popup_info), but that still
+ * spawns its own click command (like popup_item). */
+ char *p1 = strchr(rest, ':');
+ if (!p1) {
+ fprintf(stderr, "sxbarc:%d: popup_live_item missing fields\n", lineno);
+ continue;
+ }
+ *p1 = '\0';
+ char *name = strip(rest);
+ char *after = strip(p1 + 1);
+ Module *m = find_module(cfg, name);
+ if (!m) {
+ fprintf(stderr, "sxbarc:%d: popup_live_item: unknown module '%s'\n", lineno, name);
+ continue;
+ }
+ apply_popup_live_item(m, after, "sxbarc", lineno, 1);
} else if (!strcmp(key, "popup_info")) {
/* popup_info : module_name : "shell command" -- one purely
* informational row: its label is this command's output,
diff --git a/src/sxbar.c b/src/sxbar.c
index 639cfde..9929f4d 100644
--- a/src/sxbar.c
+++ b/src/sxbar.c
@@ -1531,6 +1531,16 @@ void hdl_dummy(XEvent *xev)
void hdl_expose(XEvent *xev)
{
+ if (popup.open && xev->xexpose.window == popup.win) {
+ /* re-present the already-drawn buffer -- don't call popup_draw()
+ * here, which would re-run every row's label_command from
+ * scratch; Expose just means "show what you already have again",
+ * and recomputing risked a visibly different frame (marquee
+ * offset, a command's output) flashing in right after the
+ * correct one from popup_open()/popup_draw(). */
+ XCopyArea(dpy, popup.buffer, popup.win, gc, 0, 0, popup.w, popup.h, 0, 0);
+ return;
+ }
int idx = find_bar(xev->xexpose.window);
redraw_bar(idx);
}