foxygit / sxbar Log in
commit 9f5eba2cd34b87439d0949e998d70ceafac408ce
Author:     MrJensK <jens.se@icloud.com>
AuthorDate: Wed May 13 18:02:48 2026 +0200
Commit:     MrJensK <jens.se@icloud.com>
CommitDate: Wed May 13 18:02:48 2026 +0200

    Add clickable module functionality with command execution
---
 README.md      | 40 ++++++++++++++++++++++++++++++++++++++++
 default_sxbarc | 34 ++++++++++++++++++++++++++++++++++
 src/defs.h     |  1 +
 src/parser.c   | 31 +++++++++++++++++++++++++++++++
 src/sxbar.c    | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 160 insertions(+)

diff --git a/README.md b/README.md
index 9755449..dbf5c57 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,46 @@ A new config file format is now supported via `default_sxbarc`.
 - Users can configure sxbar without recompiling.
 - Bash scripts and shell commands can produce bar text dynamically.

+## Clickable modules
+
+Modules in the bar can be left-clicked to run a command.
+
+### Changes
+- Added `click_command` field to the `Module` struct.
+- Added `hdl_button` event handler that maps a click's X position to the correct module.
+- Registered the handler for `ButtonPress` events (the event mask was already set).
+- Commands are launched in the background via double-fork so sxbar never blocks.
+- Added `click` directive to the config parser.
+
+### Config syntax
+
+```
+click : module_name : "command"
+```
+
+Works for both built-in modules (`clock`, `date`, `battery`, `volume`, `cpu`) and custom modules.
+
+### Examples
+
+```
+click : volume  : "pavucontrol"
+click : clock   : "gsimplecal"
+click : battery : "xterm -e 'upower -i /org/freedesktop/UPower/devices/battery_BAT0; read'"
+click : network : "xterm -e nmtui"
+```
+
+Custom module with a click command:
+
+```
+custom : temp : "sensors | grep 'Package' | awk '{print $4}'" : 5
+click  : temp : "xterm -e 'watch -n1 sensors; read'"
+```
+
+### Result
+
+- Left-clicking any enabled module runs its assigned command.
+- The command launches detached in the background — sxbar remains responsive.
+
 ## Disk footprint

 - `sxbar` is very lightweight: the compiled binary is about 36 KB.
diff --git a/default_sxbarc b/default_sxbarc
index 160e44e..c612c61 100644
--- a/default_sxbarc
+++ b/default_sxbarc
@@ -37,3 +37,37 @@ module : cpu     : false : 3
 # custom : mem      : "free -h | awk '/^Mem:/{print $3\"/\"$2}'" : 10
 # custom : network  : "~/.config/sxbar/scripts/network.sh" : 5
 # custom : updates  : "checkupdates | wc -l | tr -d ' '" : 300
+
+# Click commands — run a command when a module is left-clicked
+# Works for both built-in modules and custom modules
+# click : module_name : "command"
+#
+# Examples:
+# click : volume  : "pavucontrol"
+# click : clock   : "xclock"
+# click : battery : "xterm -e 'upower -i /org/freedesktop/UPower/devices/battery_BAT0; read'"
+# click : network : "xterm -e nmtui"
+
+# Media controller example (requires playerctl)
+# playerctl uses the MPRIS2 protocol and works with most players automatically:
+# Firefox (YouTube, Spotify Web), Spotify, VLC, mpv, rhythmbox, etc.
+# It will control whichever player was most recently active.
+#
+# Shows:  <<   >   >>   Artist - Title
+#
+# custom : media_prev  : "echo ' << '"                                                                    : 999
+# custom : media_play  : "playerctl status 2>/dev/null | sed 's/Playing/ > /;s/Paused/ || /;s/Stopped/ [] /'" : 1
+# custom : media_next  : "echo ' >> '"                                                                    : 999
+# custom : media_track : "playerctl metadata --format ' {{artist}} - {{title}} ' 2>/dev/null || echo ' - '" : 2
+#
+# click : media_prev  : "playerctl previous"
+# click : media_play  : "playerctl play-pause"
+# click : media_next  : "playerctl next"
+#
+# If you have multiple players running at the same time and want to always
+# control the one that is currently playing (not just the most recent):
+#
+# custom : media_play : "playerctl -p $(playerctl -l 2>/dev/null | head -1) status 2>/dev/null | sed 's/Playing/ > /;s/Paused/ || /;s/Stopped/ [] /'" : 1
+#
+# List all available players:  playerctl -l
+
diff --git a/src/defs.h b/src/defs.h
index bd6c25e..03533a2 100644
--- a/src/defs.h
+++ b/src/defs.h
@@ -12,6 +12,7 @@
 typedef struct Module {
 	char *name;
 	char *command;
+	char *click_command;
 	int enabled;
 	int refresh_interval;
 	time_t last_update;
diff --git a/src/parser.c b/src/parser.c
index 6a43b5c..2c0ae2e 100644
--- a/src/parser.c
+++ b/src/parser.c
@@ -258,6 +258,37 @@ int parse_config(Config *cfg)
 			m->refresh_interval = interval;
 			m->last_update    = 0;
 			m->cached_output  = NULL;
+		} else if (!strcmp(key, "click")) {
+			/* click : module_name : "command" */
+			char *p1 = strchr(rest, ':');
+			if (!p1) {
+				fprintf(stderr, "sxbarc:%d: click missing name and command\n", lineno);
+				continue;
+			}
+			*p1 = '\0';
+			char *name  = strip(rest);
+			char *after = strip(p1 + 1);
+
+			if (*after != '"' && *after != '\'') {
+				fprintf(stderr, "sxbarc:%d: click command must be quoted\n", lineno);
+				continue;
+			}
+			char q = *after;
+			char *cmd_start = after + 1;
+			char *closing   = strchr(cmd_start, q);
+			if (!closing) {
+				fprintf(stderr, "sxbarc:%d: click command missing closing quote\n", lineno);
+				continue;
+			}
+			*closing = '\0';
+
+			Module *m = find_module(cfg, name);
+			if (!m) {
+				fprintf(stderr, "sxbarc:%d: click: unknown module '%s'\n", lineno, name);
+				continue;
+			}
+			free(m->click_command);
+			m->click_command = expand_home(cmd_start);
 		} else {
 			fprintf(stderr, "sxbarc:%d: unknown option '%s'\n", lineno, key);
 		}
diff --git a/src/sxbar.c b/src/sxbar.c
index faa9b34..0d67ab1 100644
--- a/src/sxbar.c
+++ b/src/sxbar.c
@@ -3,6 +3,7 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <sys/wait.h>
 #include <time.h>
 #include <unistd.h>

@@ -23,6 +24,7 @@ static void redraw_monitor(int monitor_index);
 int find_window_monitor(Window win);
 int get_current_workspace(void);
 char **get_workspace_name(int *count);
+void hdl_button(XEvent *xev);
 void hdl_dummy(XEvent *xev);
 void hdl_expose(XEvent *xev);
 void hdl_property(XEvent *xev);
@@ -86,6 +88,7 @@ void cleanup_modules(void)
 	for (int i = 0; i < config.module_count; i++) {
 		free(config.modules[i].name);
 		free(config.modules[i].command);
+		free(config.modules[i].click_command);
 		free(config.modules[i].cached_output);
 	}
 	free(config.modules);
@@ -364,6 +367,56 @@ char **get_workspace_name(int *count)
 	return NULL;
 }

+static void spawn(const char *cmd)
+{
+	pid_t pid = fork();
+	if (pid == 0) {
+		if (fork() == 0) {
+			setsid();
+			execl("/bin/sh", "sh", "-c", cmd, NULL);
+			_exit(127);
+		}
+		_exit(0);
+	}
+	if (pid > 0)
+		waitpid(pid, NULL, 0);
+}
+
+void hdl_button(XEvent *xev)
+{
+	if (xev->xbutton.button != Button1)
+		return;
+
+	int idx = find_window_monitor(xev->xbutton.window);
+	int x_click = xev->xbutton.x;
+	int w = monitors[idx].width - 2 * config.horizontal_padding;
+	const int pad = 5, mod_sp = 20;
+
+	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 += XTextWidth(font, config.modules[i].cached_output,
+		                       strlen(config.modules[i].cached_output)) + mod_sp;
+	}
+	int ver_w = config.show_version
+	    ? XTextWidth(font, config.version_text, strlen(config.version_text)) : 0;
+	int mx = w - total_mw - ver_w - 2 * config.text_padding - 2 * pad;
+
+	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;
+		int tw = XTextWidth(font, out, strlen(out));
+		if (x_click >= mx && x_click < mx + tw + mod_sp) {
+			if (config.modules[i].click_command)
+				spawn(config.modules[i].click_command);
+			return;
+		}
+		mx += tw + mod_sp;
+	}
+}
+
 void hdl_dummy(XEvent *xev)
 {
 	(void)xev;
@@ -554,6 +607,7 @@ void setup(void)
 		evtable[i] = hdl_dummy;
 	}
 	evtable[Expose] = hdl_expose;
+	evtable[ButtonPress] = hdl_button;
 	evtable[PropertyNotify] = hdl_property;
 	XSelectInput(dpy, root, PropertyChangeMask);