foxygit / sxbar Log in
commit 4b25403b117b17598441e5cef0992c3cbf6902f7
Author:     Abhinav <abhinav.prsai@gmail.com>
AuthorDate: Wed Jul 2 23:49:04 2025 +0100
Commit:     Abhinav <abhinav.prsai@gmail.com>
CommitDate: Wed Jul 2 23:49:04 2025 +0100

    add modules support
---
 src/defs.h  |  30 +++--
 src/sxbar.c | 411 ++++++++++++++++++++++++++++++++++++++++--------------------
 2 files changed, 297 insertions(+), 144 deletions(-)

diff --git a/src/defs.h b/src/defs.h
index e2e3a75..804c845 100644
--- a/src/defs.h
+++ b/src/defs.h
@@ -1,6 +1,7 @@
 #pragma once

 #include <X11/Xlib.h>
+#include <time.h>

 #define SXBAR_VERSION	"sxbar ver. 1.0"
 #define SXBAR_AUTHOR	"(C) Abhinav Prasai 2025"
@@ -8,21 +9,30 @@

 #define MAX_MONITORS 32

-typedef struct {
-	Bool bottom_bar;
-	Bool border;
-	unsigned long border_colour;
-	unsigned long background_colour;
-	unsigned long foreground_colour;
+typedef struct Module {
+	char *name;
+	char *command;
+	int enabled;
+	int refresh_interval;
+	time_t last_update;
+	char *cached_output;
+} Module;
+
+typedef struct Config {
+	int bottom_bar;
 	int height;
 	int vertical_padding;
 	int horizontal_padding;
-	int border_width;
 	int text_padding;
-	int workspace_spacing;
+	int border;
+	int border_width;
+	unsigned long background_colour;
+	unsigned long foreground_colour;
+	unsigned long border_colour;
 	char *font;
-	char *workspace_highlight_left;
-	char *workspace_highlight_right;
+	Module *modules;
+	int module_count;
+	int max_modules;
 } Config;

 typedef void (*EventHandler)(XEvent *);
diff --git a/src/sxbar.c b/src/sxbar.c
index 16168cd..9f84b57 100644
--- a/src/sxbar.c
+++ b/src/sxbar.c
@@ -3,6 +3,8 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <time.h>
+#include <unistd.h>

 #include <X11/Xatom.h>
 #include <X11/Xlib.h>
@@ -11,8 +13,11 @@

 #include "defs.h"

+void cleanup_modules(void);
+void cleanup_resources(void);
 void create_bars(void);
-void draw_bar_content(Window win, int monitor_index);
+static void draw_bar_into(Drawable draw, int monitor_index);
+static void redraw_monitor(int monitor_index);
 int find_window_monitor(Window win);
 int get_current_workspace(void);
 char **get_workspace_name(int *count);
@@ -20,34 +25,73 @@ void hdl_dummy(XEvent *xev);
 void hdl_expose(XEvent *xev);
 void hdl_property(XEvent *xev);
 void init_defaults(void);
+void init_modules(void);
 unsigned long parse_col(const char *hex);
 void run(void);
+char *run_command(const char *cmd);
 void setup(void);
+void update_modules(void);

 EventHandler evtable[LASTEvent];
 XFontStruct *font;
 Display *dpy;
 Window root;
 Window *wins = NULL;
-int nmonitors = 0;
 XineramaScreenInfo *monitors = NULL;
 GC gc;
-int scr;
 Config config;
+Pixmap *buffers = NULL;
+int nmonitors = 0;
+int scr;

-void create_bars(void)
+void cleanup_modules(void)
 {
-	int xin_active = 0;
+	for (int i = 0; i < config.module_count; i++) {
+		free(config.modules[i].name);
+		free(config.modules[i].command);
+		free(config.modules[i].cached_output);
+	}
+	free(config.modules);
+}

+void cleanup_resources(void)
+{
+	if (buffers) {
+		for (int i = 0; i < nmonitors; i++) {
+			XFreePixmap(dpy, buffers[i]);
+		}
+		free(buffers);
+	}
+	if (wins) {
+		for (int i = 0; i < nmonitors; i++) {
+			XDestroyWindow(dpy, wins[i]);
+		}
+		free(wins);
+	}
+	if (monitors) {
+		XFree(monitors);
+	}
+	if (font) {
+		XFreeFont(dpy, font);
+	}
+	if (gc) {
+		XFreeGC(dpy, gc);
+	}
+	if (dpy) {
+		XCloseDisplay(dpy);
+	}
+}
+
+void create_bars(void)
+{
+	int xin = 0;
 	if (XineramaIsActive(dpy)) {
 		monitors = XineramaQueryScreens(dpy, &nmonitors);
-		xin_active = 1;
+		xin = 1;
 	}
-
-	/* fallback to single monitor if Xinerama is not available */
-	if (!xin_active || nmonitors <= 0) {
+	if (!xin || nmonitors <= 0) {
 		nmonitors = 1;
-		monitors = malloc(sizeof(XineramaScreenInfo));
+		monitors = malloc(sizeof *monitors);
 		monitors[0].screen_number = 0;
 		monitors[0].x_org = 0;
 		monitors[0].y_org = 0;
@@ -55,24 +99,17 @@ void create_bars(void)
 		monitors[0].height = DisplayHeight(dpy, scr);
 	}

-	/* TODO: ADD SUPPORT FOR SELECTING MONITORS */
-	wins = malloc(nmonitors * sizeof(Window));
+	wins = malloc(nmonitors * sizeof *wins);
+	buffers = malloc(nmonitors * sizeof *buffers);

 	for (int i = 0; i < nmonitors; i++) {
 		int bw = config.border ? config.border_width : 0;
-		/* window content width (excluding border) */
-		int w = monitors[i].width - (2 * config.horizontal_padding);
-		/* padding from screen edge */
-		int x = monitors[i].x_org + config.horizontal_padding;
+		int w = monitors[i].width - 2 * config.horizontal_padding;
 		int h = config.height;
-		int y;
-
-		if (config.bottom_bar) {
-			y = monitors[i].y_org + monitors[i].height - h - config.vertical_padding - bw;
-		}
-		else {
-			y = monitors[i].y_org + config.vertical_padding;
-		}
+		int x = monitors[i].x_org + config.horizontal_padding;
+		int y = config.bottom_bar
+		            ? monitors[i].y_org + monitors[i].height - h - config.vertical_padding - bw
+		            : monitors[i].y_org + config.vertical_padding;

 		XSetWindowAttributes wa = {.background_pixel = config.background_colour,
 		                           .border_pixel = config.border_colour,
@@ -83,11 +120,8 @@ void create_bars(void)
 		                        CWBackPixel | CWBorderPixel | CWEventMask, &wa);

 		XStoreName(dpy, wins[i], "sxbar");
-
-		XClassHint class_hint;
-		class_hint.res_name = "sxbar";
-		class_hint.res_class = "sxbar";
-		XSetClassHint(dpy, wins[i], &class_hint);
+		XClassHint ch = {"sxbar", "sxbar"};
+		XSetClassHint(dpy, wins[i], &ch);

 		Atom A_WM_TYPE = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE", False);
 		Atom A_WM_TYPE_DOCK = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_DOCK", False);
@@ -96,21 +130,20 @@ void create_bars(void)

 		Atom A_STRUT = XInternAtom(dpy, "_NET_WM_STRUT_PARTIAL", False);
 		long strut[12] = {0};
-
 		if (config.bottom_bar) {
 			strut[3] = h + bw;
 			strut[10] = x;
-			strut[11] = x + w + (2 * bw) - 1;
+			strut[11] = x + w + 2 * bw - 1;
 		}
 		else {
 			strut[2] = y + h + bw;
 			strut[8] = x;
-			strut[9] = x + w + (2 * bw) - 1;
+			strut[9] = x + w + 2 * bw - 1;
 		}
-
 		XChangeProperty(dpy, wins[i], A_STRUT, XA_CARDINAL, 32, PropModeReplace,
 		                (unsigned char *)strut, 12);

+		buffers[i] = XCreatePixmap(dpy, wins[i], w, h, DefaultDepth(dpy, scr));
 		XMapRaised(dpy, wins[i]);
 	}

@@ -118,91 +151,105 @@ void create_bars(void)
 	XSetForeground(dpy, gc, config.foreground_colour);
 	font = XLoadQueryFont(dpy, config.font);
 	if (!font) {
-		errx(1, "could not load font");
+		errx(1, "could not load font %s", config.font);
 	}
 	XSetFont(dpy, gc, font->fid);
 }

-void draw_bar_content(Window win, int monitor_index)
+static void draw_bar_into(Drawable draw, int monitor_index)
 {
-	XClearWindow(dpy, win);
+	int w = monitors[monitor_index].width - 2 * config.horizontal_padding;
+	int h = config.height;
+	/* clear */
+	XSetForeground(dpy, gc, config.background_colour);
+	XFillRectangle(dpy, draw, gc, 0, 0, w, h);

 	int current_ws = get_current_workspace();
 	int name_count = 0;
 	char **names = get_workspace_name(&name_count);

-	unsigned int text_y = (config.height + font->ascent - font->descent) / 2;
-	const int bg_padding = 5;
-	const int workspace_spacing = 10;
+	unsigned text_y = (h + font->ascent - font->descent) / 2;
+	const int pad = 5, ws_sp = 10, mod_sp = 20;
+	unsigned cur_x = config.text_padding + pad;

+	/* workspaces */
 	if (names) {
-		unsigned int *positions = malloc(name_count * sizeof(unsigned int));
-		unsigned int *widths = malloc(name_count * sizeof(unsigned int));
-		unsigned int text_x = config.text_padding + bg_padding;
-
-		/* calculate consistent positions and widths */
-		for (int i = 0; i < name_count; ++i) {
-			char padded_label[64];
-			snprintf(padded_label, sizeof(padded_label), " %s ", names[i]);
-
-			int label_width = XTextWidth(font, padded_label, strlen(padded_label));
-
-			positions[i] = text_x;
-			widths[i] = label_width;
-
-			text_x += label_width + workspace_spacing;
+		unsigned *pos = malloc(name_count * sizeof *pos);
+		unsigned *wd = malloc(name_count * sizeof *wd);
+		for (int i = 0; i < name_count; i++) {
+			char tmp[64];
+			snprintf(tmp, sizeof tmp, " %s ", names[i]);
+			wd[i] = XTextWidth(font, tmp, strlen(tmp));
+			pos[i] = cur_x;
+			cur_x += wd[i] + ws_sp;
 		}
-
-		/* draw backgrounds and text */
-		for (int i = 0; i < name_count; ++i) {
-			char label[64];
-			/* TODO: ADD ABILITY TO CHANGE WORKSPACE STRING */
-			snprintf(label, sizeof(label), " %s ", names[i]);
-
+		for (int i = 0; i < name_count; i++) {
+			char tmp[64];
+			snprintf(tmp, sizeof tmp, " %s ", names[i]);
 			if (i == current_ws) {
-				/* draw highlight background for active workspace */
-				/* TODO: ADD OPTION TO CHANGE SPECIFIC HILIGHT COLOUR */
 				XSetForeground(dpy, gc, config.foreground_colour);
-				XFillRectangle(dpy, win, gc, positions[i] - bg_padding,
-				               text_y - font->ascent - bg_padding, widths[i] + (2 * bg_padding),
-				               font->ascent + font->descent + (2 * bg_padding));
-
-				/* TODO: ADD CUSTOM TEXT COLOURS */
+				XFillRectangle(dpy, draw, gc, pos[i] - pad, text_y - font->ascent - pad,
+				               wd[i] + 2 * pad, font->ascent + font->descent + 2 * pad);
 				XSetForeground(dpy, gc, config.background_colour);
 			}
 			else {
 				XSetForeground(dpy, gc, config.foreground_colour);
 			}
-
-			XDrawString(dpy, win, gc, positions[i], text_y, label, strlen(label));
+			XDrawString(dpy, draw, gc, pos[i], text_y, tmp, strlen(tmp));
 			free(names[i]);
 		}
-
-		free(positions);
-		free(widths);
 		free(names);
+		free(pos);
+		free(wd);
 	}

 	XSetForeground(dpy, gc, config.foreground_colour);

-	/* use window content width for version string positioning */
-	int version_width = XTextWidth(font, SXBAR_VERSION, strlen(SXBAR_VERSION));
-	int window_width = monitors[monitor_index].width - (2 * config.horizontal_padding);
-	int version_x = window_width - version_width - config.text_padding - bg_padding;
+	/* modules */
+	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 = XTextWidth(font, SXBAR_VERSION, strlen(SXBAR_VERSION));
+	int mx = w - total_mw - ver_w - 2 * config.text_padding - 2 * pad;

-	XDrawString(dpy, win, gc, version_x, text_y, SXBAR_VERSION, strlen(SXBAR_VERSION));
+	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));
+		XDrawString(dpy, draw, gc, mx, text_y, out, strlen(out));
+		mx += tw + mod_sp;
+	}
+
+	/* version */
+	int vx = w - ver_w - config.text_padding - pad;
+	XDrawString(dpy, draw, gc, vx, text_y, SXBAR_VERSION, strlen(SXBAR_VERSION));
+}
+
+static void redraw_monitor(int i)
+{
+	int w = monitors[i].width - 2 * config.horizontal_padding;
+	int h = config.height;
+	draw_bar_into(buffers[i], i);
+	XCopyArea(dpy, buffers[i], wins[i], gc, 0, 0, w, h, 0, 0);
 }

 int get_current_workspace(void)
 {
-	Atom actual_type;
-	int actual_format;
-	unsigned long nitems, bytes_after;
+	Atom at = XInternAtom(dpy, "_NET_CURRENT_DESKTOP", False);
+	Atom ret_type;
+	int fmt;
+	unsigned long n, after;
 	unsigned char *data = NULL;
-	Atom prop = XInternAtom(dpy, "_NET_CURRENT_DESKTOP", False);
-
-	if (XGetWindowProperty(dpy, root, prop, 0, 1, False, XA_CARDINAL, &actual_type,
-	                       &actual_format, &nitems, &bytes_after, &data) == Success &&
+	if (XGetWindowProperty(dpy, root, at, 0, 1, False, XA_CARDINAL, &ret_type, &fmt, &n, &after,
+	                       &data) == Success &&
 	    data) {
 		int ws = *(unsigned long *)data;
 		XFree(data);
@@ -213,28 +260,25 @@ int get_current_workspace(void)

 char **get_workspace_name(int *count)
 {
-	Atom actual_type;
-	int actual_format;
-	unsigned long nitems, bytes_after;
-	unsigned char *data = NULL;
-
-	Atom prop = XInternAtom(dpy, "_NET_DESKTOP_NAMES", False);
+	Atom at = XInternAtom(dpy, "_NET_DESKTOP_NAMES", False);
 	Atom utf8 = XInternAtom(dpy, "UTF8_STRING", False);
-
-	if (XGetWindowProperty(dpy, root, prop, 0, (~0L), False, utf8, &actual_type, &actual_format,
-	                       &nitems, &bytes_after, &data) == Success &&
+	Atom ret_type;
+	int fmt;
+	unsigned long n, after;
+	unsigned char *data = NULL;
+	if (XGetWindowProperty(dpy, root, at, 0, (~0L), False, utf8, &ret_type, &fmt, &n, &after,
+	                       &data) == Success &&
 	    data) {
-
 		char **names = NULL;
-		int n = 0;
+		int idx = 0;
 		char *p = (char *)data;
-		while (n < MAX_MONITORS && p < (char *)data + nitems) {
-			names = realloc(names, (n + 1) * sizeof(char *));
-			names[n++] = strdup(p);
+		while (p < (char *)data + n && idx < MAX_MONITORS) {
+			names = realloc(names, (idx + 1) * sizeof *names);
+			names[idx++] = strdup(p);
 			p += strlen(p) + 1;
 		}
 		XFree(data);
-		*count = n;
+		*count = idx;
 		return names;
 	}
 	*count = 0;
@@ -248,22 +292,19 @@ void hdl_dummy(XEvent *xev)

 void hdl_expose(XEvent *xev)
 {
-	Window win = xev->xexpose.window;
-	int monitor_index = find_window_monitor(win);
-	draw_bar_content(win, monitor_index);
+	int idx = find_window_monitor(xev->xexpose.window);
+	redraw_monitor(idx);
 }

 void hdl_property(XEvent *xev)
 {
 	if (xev->xproperty.atom == XInternAtom(dpy, "_NET_CURRENT_DESKTOP", False)) {
-		/* redraw all bars */
 		for (int i = 0; i < nmonitors; i++) {
-			draw_bar_content(wins[i], i);
+			redraw_monitor(i);
 		}
 	}
 }

-
 void init_defaults(void)
 {
 	config.bottom_bar = True;
@@ -276,7 +317,8 @@ void init_defaults(void)
 	config.background_colour = parse_col("#000000");
 	config.foreground_colour = parse_col("#7abccd");
 	config.border_colour = parse_col("#005577");
-	config.font = strdup("fixed");
+	config.font = strdup("Bm437 IBM VGA 8x16");
+	init_modules();
 }

 int find_window_monitor(Window win)
@@ -286,53 +328,152 @@ int find_window_monitor(Window win)
 			return i;
 		}
 	}
-	return 0; /* fallback to first monitor */
+	return 0;
+}
+
+void init_modules(void)
+{
+	config.max_modules = 10;
+	config.modules = malloc(config.max_modules * sizeof(Module));
+	config.module_count = 0;
+
+	/* clock */
+	config.modules[config.module_count++] = (Module){.name = strdup("clock"),
+	                                                 .command = "date '+%H:%M:%S'",
+	                                                 .enabled = True,
+	                                                 .refresh_interval = 1,
+	                                                 .last_update = 0,
+	                                                 .cached_output = NULL};
+	/* date */
+	config.modules[config.module_count++] = (Module){.name = strdup("date"),
+	                                                 .command = "date '+%Y-%m-%d'",
+	                                                 .enabled = True,
+	                                                 .refresh_interval = 60,
+	                                                 .last_update = 0,
+	                                                 .cached_output = NULL};
+	/* 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'",
+	             .enabled = False,
+	             .refresh_interval = 30,
+	             .last_update = 0,
+	             .cached_output = NULL};
+	/* volume */
+	config.modules[config.module_count++] =
+	    (Module){.name = strdup("volume"),
+	             .command = "amixer get Master | grep -o '[0-9]*%' | head -1 || echo 'N/A'",
+	             .enabled = True,
+	             .refresh_interval = 5,
+	             .last_update = 0,
+	             .cached_output = NULL};
+	/* 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\"%\"}'",
+	             .enabled = True,
+	             .refresh_interval = 3,
+	             .last_update = 0,
+	             .cached_output = NULL};
 }

 unsigned long parse_col(const char *hex)
 {
 	XColor col;
-	Colormap cmap = DefaultColormap(dpy, DefaultScreen(dpy));
-
-	if (!XParseColor(dpy, cmap, hex, &col)) {
-		fprintf(stderr, "sxwm: cannot parse color %s\n", hex);
-		return WhitePixel(dpy, DefaultScreen(dpy));
-	}
-
-	if (!XAllocColor(dpy, cmap, &col)) {
-		fprintf(stderr, "sxwm: cannot allocate color %s\n", hex);
-		return WhitePixel(dpy, DefaultScreen(dpy));
+	Colormap cmap = DefaultColormap(dpy, scr);
+	if (!XParseColor(dpy, cmap, hex, &col) || !XAllocColor(dpy, cmap, &col)) {
+		fprintf(stderr, "sxbar: cannot parse/color %s\n", hex);
+		return WhitePixel(dpy, scr);
 	}
-
 	return col.pixel;
 }

 void run(void)
 {
 	XEvent xev;
+	time_t last = 0;

-	for (;;) {
-		XNextEvent(dpy, &xev);
-		evtable[xev.type](&xev);
+	while (True) {
+		while (XPending(dpy)) {
+			XNextEvent(dpy, &xev);
+			evtable[xev.type](&xev);
+		}
+		time_t now = time(NULL);
+		if (now - last >= 1) {
+			update_modules();
+			for (int i = 0; i < nmonitors; i++) {
+				redraw_monitor(i);
+			}
+			last = now;
+		}
+		struct timespec ts = {0, 100000000};
+		nanosleep(&ts, NULL);
+	}
+}
+
+char *run_command(const char *cmd)
+{
+	FILE *fp = popen(cmd, "r");
+	if (!fp) {
+		return strdup("N/A");
+	}
+
+	char buffer[1024];
+	char *res = NULL;
+	size_t len = 0;
+
+	while (fgets(buffer, sizeof buffer, fp)) {
+		size_t l = strlen(buffer);
+		if (buffer[l - 1] == '\n') {
+			buffer[--l] = '\0';
+		}
+		if (!res) {
+			res = malloc(l + 1);
+			strcpy(res, buffer);
+			len = l;
+		}
+		else {
+			res = realloc(res, len + l + 2);
+			strcat(res, " ");
+			strcat(res, buffer);
+			len += l + 1;
+		}
+	}
+	pclose(fp);
+	return res ? res : strdup("");
+}
+
+void update_modules(void)
+{
+	time_t now = time(NULL);
+	for (int i = 0; i < config.module_count; i++) {
+		Module *m = &config.modules[i];
+		if (!m->enabled) {
+			continue;
+		}
+		if (now - m->last_update >= m->refresh_interval) {
+			free(m->cached_output);
+			m->cached_output = run_command(m->command);
+			m->last_update = now;
+		}
 	}
 }

 void setup(void)
 {
-	if ((dpy = XOpenDisplay(NULL)) == 0) {
-		errx(0, "can't open display. quitting...");
+	if (!(dpy = XOpenDisplay(NULL))) {
+		errx(1, "can't open display");
 	}
 	root = XDefaultRootWindow(dpy);
 	scr = DefaultScreen(dpy);

-	/* init all events with a dummy */
-	for (int i = 0; i < LASTEvent; ++i) {
+	for (int i = 0; i < LASTEvent; i++) {
 		evtable[i] = hdl_dummy;
 	}
-
 	evtable[Expose] = hdl_expose;
 	evtable[PropertyNotify] = hdl_property;
-
 	XSelectInput(dpy, root, PropertyChangeMask);

 	init_defaults();
@@ -342,15 +483,17 @@ void setup(void)
 int main(int ac, char **av)
 {
 	if (ac > 1) {
-		if (strcmp(av[1], "-v") == 0 || strcmp(av[1], "--version") == 0) {
-			errx(0, "%s\n%s\n%s", SXBAR_VERSION, SXBAR_AUTHOR, SXBAR_LICINFO);
-		}
-		else {
-			errx(0, "usage:\n[-v || --version]: See the version of sxbar");
+		if (!strcmp(av[1], "-v") || !strcmp(av[1], "--version")) {
+			printf("%s\n%s\n%s\n", SXBAR_VERSION, SXBAR_AUTHOR, SXBAR_LICINFO);
+			return 0;
 		}
+		errx(1, "usage: sxbar [-v|--version]");
 	}
-
 	setup();
 	run();
+
+	/* TODO?: never reached */
+	cleanup_modules();
+	cleanup_resources();
 	return 0;
 }