foxygit / sxbar Log in
commit 87a042691daa33f74d177ac8ac9aba98f9b68eaa
Author:     MrJensK <jens.se@icloud.com>
AuthorDate: Tue Jul 28 19:56:23 2026 +0200
Commit:     MrJensK <jens.se@icloud.com>
CommitDate: Tue Jul 28 19:56:23 2026 +0200

    lots of updates
---
 README.md      | 37 +++++++++++++++++++++++++++++++++++++
 default_sxbarc | 12 ++++++++++++
 src/defs.h     |  1 +
 src/parser.c   | 32 ++++++++++++++++++++++++++++++++
 src/sxbar.c    | 38 +++++++++++++++++++++++++++++---------
 5 files changed, 111 insertions(+), 9 deletions(-)

diff --git a/README.md b/README.md
index dbf5c57..90fd51f 100644
--- a/README.md
+++ b/README.md
@@ -97,6 +97,43 @@ click  : temp : "xterm -e 'watch -n1 sensors; read'"
 - Left-clicking any enabled module runs its assigned command.
 - The command launches detached in the background — sxbar remains responsive.

+## Module prefixes / icons
+
+Any module — built-in or custom — can have text prepended to its output,
+e.g. a Nerd Font glyph.
+
+### Changes
+- Added `prefix` field to the `Module` struct.
+- Added `prefix` directive to the config parser (`icon` is accepted as an alias).
+- Drawing and click-hit-testing now use the module's prefixed text so
+  click regions stay aligned with what's rendered.
+
+### Config syntax
+
+```
+prefix : module_name : "icon text"
+```
+
+Requires a Nerd Font set via `font` to render icon glyphs. For custom
+modules, `prefix` must be declared after the module's `custom` line.
+
+### Examples
+
+```
+font     : JetBrainsMono Nerd Font:size=10
+
+prefix : battery : "  "
+prefix : volume  : "  "
+prefix : cpu     : "  "
+```
+
+### Result
+
+- Built-in modules (`clock`, `date`, `battery`, `volume`, `cpu`) can show an
+  icon without touching their hardcoded command.
+- Custom modules could already embed an icon directly in their command, but
+  `prefix` keeps that separate from the command itself.
+
 ## Disk footprint

 - `sxbar` is very lightweight: the compiled binary is about 36 KB.
diff --git a/default_sxbarc b/default_sxbarc
index 86e4f2f..aa90609 100644
--- a/default_sxbarc
+++ b/default_sxbarc
@@ -42,6 +42,18 @@ module : cpu     : false : 3
 # custom : network  : "~/.config/sxbar/scripts/network.sh" : 5
 # custom : updates  : "checkupdates | wc -l | tr -d ' '" : 300

+# Prefix / icon — text prepended to a module's output (works for both
+# built-in and custom modules). Needs a Nerd Font set via `font` above to
+# render icon glyphs correctly.
+# prefix : module_name : "icon text"
+# (icon : ... is accepted as an alias for prefix)
+#
+# Examples:
+# prefix : battery : "  "
+# prefix : volume  : "  "
+# prefix : cpu     : "  "
+# prefix : clock   : "  "
+
 # Per-module text colour (overrides global foreground_colour for that module)
 # colour : module_name : #rrggbb
 #
diff --git a/src/defs.h b/src/defs.h
index b8e7947..41be285 100644
--- a/src/defs.h
+++ b/src/defs.h
@@ -17,6 +17,7 @@ typedef struct Module {
 	char *scroll_up_command;
 	char *scroll_down_command;
 	char *colour;
+	char *prefix;
 	XftColor xft_colour;
 	int has_colour;
 	int enabled;
diff --git a/src/parser.c b/src/parser.c
index ea25126..9f302ab 100644
--- a/src/parser.c
+++ b/src/parser.c
@@ -252,6 +252,7 @@ int parse_config(Config *cfg)
 			}

 			Module *m         = &cfg->modules[cfg->module_count++];
+			memset(m, 0, sizeof *m);
 			m->name           = strdup(name);
 			m->command        = expand_home(cmd_start);
 			m->enabled        = 1;
@@ -315,6 +316,37 @@ int parse_config(Config *cfg)
 				free(m->scroll_down_command);
 				m->scroll_down_command = expand_home(cmd_start);
 			}
+		} else if (!strcmp(key, "prefix") || !strcmp(key, "icon")) {
+			/* prefix/icon : module_name : "text" -- prepended to the module's output, e.g. a Nerd Font glyph */
+			char *p1 = strchr(rest, ':');
+			if (!p1) {
+				fprintf(stderr, "sxbarc:%d: %s missing name and text\n", lineno, key);
+				continue;
+			}
+			*p1 = '\0';
+			char *name  = strip(rest);
+			char *after = strip(p1 + 1);
+
+			if (*after != '"' && *after != '\'') {
+				fprintf(stderr, "sxbarc:%d: %s text must be quoted\n", lineno, key);
+				continue;
+			}
+			char q = *after;
+			char *text_start = after + 1;
+			char *closing     = strchr(text_start, q);
+			if (!closing) {
+				fprintf(stderr, "sxbarc:%d: %s text missing closing quote\n", lineno, key);
+				continue;
+			}
+			*closing = '\0';
+
+			Module *m = find_module(cfg, name);
+			if (!m) {
+				fprintf(stderr, "sxbarc:%d: %s: unknown module '%s'\n", lineno, key, name);
+				continue;
+			}
+			free(m->prefix);
+			m->prefix = strdup(text_start);
 		} else {
 			fprintf(stderr, "sxbarc:%d: unknown option '%s'\n", lineno, key);
 		}
diff --git a/src/sxbar.c b/src/sxbar.c
index 248b369..f0cf8e7 100644
--- a/src/sxbar.c
+++ b/src/sxbar.c
@@ -93,6 +93,16 @@ static int text_width(const char *str)
 	return ext.xOff;
 }

+/* module's cached output with its configured prefix (e.g. a Nerd Font glyph) prepended */
+static const char *module_text(Module *m, char *buf, size_t bufsz)
+{
+	if (m->prefix && *m->prefix) {
+		snprintf(buf, bufsz, "%s%s", m->prefix, m->cached_output);
+		return buf;
+	}
+	return m->cached_output;
+}
+
 static void pixel_to_xftcolor(unsigned long pixel, XftColor *out)
 {
 	XColor xc = {0};
@@ -130,6 +140,7 @@ void cleanup_modules(void)
 		free(config.modules[i].scroll_up_command);
 		free(config.modules[i].scroll_down_command);
 		free(config.modules[i].colour);
+		free(config.modules[i].prefix);
 		if (config.modules[i].has_colour)
 			XftColorFree(dpy, vis, cmap, &config.modules[i].xft_colour);
 		free(config.modules[i].cached_output);
@@ -345,11 +356,12 @@ static void draw_bar_into(Drawable draw, int monitor_index)
 	}

 	/* modules */
+	char mbuf[256];
 	int total_mw = 0;
 	for (int i = 0; i < config.module_count; i++) {
 		if (!config.modules[i].enabled || !config.modules[i].cached_output)
 			continue;
-		total_mw += text_width(config.modules[i].cached_output) + mod_sp;
+		total_mw += text_width(module_text(&config.modules[i], mbuf, sizeof mbuf)) + mod_sp;
 	}
 	int ver_w = config.show_version ? text_width(config.version_text) : 0;
 	int mx = w - total_mw - ver_w - 2 * config.text_padding - 2 * pad;
@@ -357,7 +369,7 @@ static void draw_bar_into(Drawable draw, int monitor_index)
 	for (int i = 0; i < config.module_count; i++) {
 		if (!config.modules[i].enabled || !config.modules[i].cached_output)
 			continue;
-		char *out = config.modules[i].cached_output;
+		const char *out = module_text(&config.modules[i], mbuf, sizeof mbuf);
 		int tw = text_width(out);
 		XftColor *col = config.modules[i].has_colour
 		    ? &config.modules[i].xft_colour : &xft_fg;
@@ -452,11 +464,12 @@ void hdl_button(XEvent *xev)
 	int w = monitors[idx].width - 2 * config.horizontal_padding;
 	const int pad = 5, mod_sp = 20;

+	char mbuf[256];
 	int total_mw = 0;
 	for (int i = 0; i < config.module_count; i++) {
 		if (!config.modules[i].enabled || !config.modules[i].cached_output)
 			continue;
-		total_mw += text_width(config.modules[i].cached_output) + mod_sp;
+		total_mw += text_width(module_text(&config.modules[i], mbuf, sizeof mbuf)) + mod_sp;
 	}
 	int ver_w = config.show_version ? text_width(config.version_text) : 0;
 	int mx = w - total_mw - ver_w - 2 * config.text_padding - 2 * pad;
@@ -464,7 +477,7 @@ void hdl_button(XEvent *xev)
 	for (int i = 0; i < config.module_count; i++) {
 		if (!config.modules[i].enabled || !config.modules[i].cached_output)
 			continue;
-		char *out = config.modules[i].cached_output;
+		const char *out = module_text(&config.modules[i], mbuf, sizeof mbuf);
 		int tw = text_width(out);
 		if (x_click >= mx && x_click < mx + tw + mod_sp) {
 			if (btn == Button1 && config.modules[i].click_command)
@@ -550,8 +563,9 @@ void init_modules(void)
 	/* battery */
 	config.modules[config.module_count++] =
 	    (Module){.name = strdup("battery"),
-	             .command = "cat /sys/class/power_supply/BAT0/capacity 2>/dev/null | sed "
-	                        "'s/$/%/' || echo 'N/A'",
+	             /* BAT0 on some machines, BAT1 on others -- glob for whichever exists */
+	             .command = "cat /sys/class/power_supply/BAT*/capacity 2>/dev/null | "
+	                        "head -n1 | sed 's/$/%/'",
 	             .enabled = False,
 	             .refresh_interval = 30,
 	             .last_update = 0,
@@ -559,7 +573,9 @@ void init_modules(void)
 	/* volume */
 	config.modules[config.module_count++] =
 	    (Module){.name = strdup("volume"),
-	             .command = "LC_ALL=C wpctl get-volume @DEFAULT_AUDIO_SINK@ 2>/dev/null | awk '/Volume:/ {print $2}' | xargs -I{} bash -c 'echo \"{} * 100 / 1\" | bc | awk \"{print \\$1 \\\"%\\\"}\"'",
+	             /* awk does the arithmetic itself; no bc/bash/xargs needed */
+	             .command = "LC_ALL=C wpctl get-volume @DEFAULT_AUDIO_SINK@ 2>/dev/null | "
+	                        "LC_ALL=C awk '/Volume:/ {printf \"%d%%\\n\", $2 * 100}'",
 	             .enabled = True,
 	             .refresh_interval = 5,
 	             .last_update = 0,
@@ -567,8 +583,12 @@ void init_modules(void)
 	/* cpu */
 	config.modules[config.module_count++] =
 		(Module){.name = strdup("cpu"),
-	             .command = "top -bn1 | grep 'Cpu(s)' | sed 's/.*, *\\([0-9.]*\\)%* id.*/\\1/' "
-	                        "| awk '{print 100-$1\"%\"}'",
+	             /* two /proc/stat samples -- top(1)'s output is localised and unparseable */
+	             .command = "{ grep -m1 '^cpu ' /proc/stat; sleep 0.2; "
+	                        "grep -m1 '^cpu ' /proc/stat; } | LC_ALL=C awk "
+	                        "'NR==1{for(i=2;i<=8;i++)t1+=$i; d1=$5+$6} "
+	                        "NR==2{for(i=2;i<=8;i++)t2+=$i; d2=$5+$6; d=t2-t1; "
+	                        "printf \"%d%%\\n\", (d>0 ? (1-(d2-d1)/d)*100 : 0)}'",
 	             .enabled = False,
 	             .refresh_interval = 3,
 	             .last_update = 0,