foxygit / sxbar Log in
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&gt;/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&gt;/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);
 }