foxygit / sxbar Log in
commit 7a9b8269022161cd688dd3b1ed5e7a641575af7f
Author:     MrJensK <jens.se@icloud.com>
AuthorDate: Thu May 14 12:40:42 2026 +0200
Commit:     MrJensK <jens.se@icloud.com>
CommitDate: Thu May 14 12:40:42 2026 +0200

    Enhance module color support and update font handling in configuration
---
 Makefile       |   4 +-
 default_sxbarc |  15 +++-
 src/defs.h     |   4 ++
 src/parser.c   |  18 +++++
 src/sxbar.c    | 218 ++++++++++++++++++++++++++++++++++++---------------------
 5 files changed, 175 insertions(+), 84 deletions(-)

diff --git a/Makefile b/Makefile
index 2de826c..a44d8fc 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
 CC      ?= gcc
-CFLAGS  ?= -std=c99 -Wall -Wextra -O3 -Isrc
-LDFLAGS ?= -lX11 -lXinerama
+CFLAGS  ?= -std=c99 -Wall -Wextra -O3 -Isrc $(shell pkg-config --cflags xft 2>/dev/null || echo -I/usr/include/freetype2)
+LDFLAGS ?= -lX11 -lXinerama -lXft

 PREFIX  ?= /usr/local
 BIN     := sxbar
diff --git a/default_sxbarc b/default_sxbarc
index 4036cef..2a459ed 100644
--- a/default_sxbarc
+++ b/default_sxbarc
@@ -17,7 +17,11 @@ border_width        : 1
 background_colour   : #000000
 foreground_colour   : #7abccd
 border_colour       : #005577
-font                : fixed
+# Xft font name format: "FamilyName:size=N[:style=Bold]"
+# For Nerd Font symbols use your Nerd Font family name, e.g.:
+#   font : JetBrainsMonoNerdFont:size=10
+#   font : Hack Nerd Font:size=10
+font                : monospace:size=8

 # Built-in modules
 # module : name : enabled : refresh_interval_seconds
@@ -38,6 +42,15 @@ module : cpu     : false : 3
 # custom : network  : "~/.config/sxbar/scripts/network.sh" : 5
 # custom : updates  : "checkupdates | wc -l | tr -d ' '" : 300

+# Per-module text colour (overrides global foreground_colour for that module)
+# colour : module_name : #rrggbb
+#
+# Examples:
+# colour : clock   : #50fa7b
+# colour : volume  : #ff79c6
+# colour : battery : #ffb86c
+# colour : date    : #8be9fd
+
 # Click commands — run a command when a module is left-clicked
 # Works for both built-in modules and custom modules
 # click : module_name : "command"
diff --git a/src/defs.h b/src/defs.h
index 01d4168..b8e7947 100644
--- a/src/defs.h
+++ b/src/defs.h
@@ -1,6 +1,7 @@
 #pragma once

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

 #define SXBAR_VERSION	"sxbar ver. 1.1"
@@ -15,6 +16,9 @@ typedef struct Module {
 	char *click_command;
 	char *scroll_up_command;
 	char *scroll_down_command;
+	char *colour;
+	XftColor xft_colour;
+	int has_colour;
 	int enabled;
 	int refresh_interval;
 	time_t last_update;
diff --git a/src/parser.c b/src/parser.c
index 0c56eea..ea25126 100644
--- a/src/parser.c
+++ b/src/parser.c
@@ -258,6 +258,24 @@ int parse_config(Config *cfg)
 			m->refresh_interval = interval;
 			m->last_update    = 0;
 			m->cached_output  = NULL;
+		} else if (!strcmp(key, "colour") || !strcmp(key, "color")) {
+			/* colour : module_name : #hex */
+			char *p1 = strchr(rest, ':');
+			if (!p1) {
+				fprintf(stderr, "sxbarc:%d: colour missing name and value\n", lineno);
+				continue;
+			}
+			*p1 = '\0';
+			char *name = strip(rest);
+			char *val  = strip(p1 + 1);
+			strip_comment(val);
+			Module *m = find_module(cfg, name);
+			if (!m) {
+				fprintf(stderr, "sxbarc:%d: colour: unknown module '%s'\n", lineno, name);
+				continue;
+			}
+			free(m->colour);
+			m->colour = strdup(val);
 		} else if (!strcmp(key, "click") || !strcmp(key, "scroll_up") || !strcmp(key, "scroll_down")) {
 			/* click/scroll_up/scroll_down : module_name : "command" */
 			char *p1 = strchr(rest, ':');
diff --git a/src/sxbar.c b/src/sxbar.c
index 8831f00..248b369 100644
--- a/src/sxbar.c
+++ b/src/sxbar.c
@@ -37,7 +37,10 @@ void setup(void);
 void update_modules(void);

 EventHandler evtable[LASTEvent];
-XFontStruct *font;
+XftFont  *font;
+XftDraw **xft_draws;
+XftColor  xft_fg;
+XftColor  xft_bg;
 Display *dpy;
 Window root;
 Window *wins = NULL;
@@ -83,14 +86,52 @@ int workspace_has_window(int ws) {
     return 0;
 }

+static int text_width(const char *str)
+{
+	XGlyphInfo ext;
+	XftTextExtentsUtf8(dpy, font, (const FcChar8 *)str, strlen(str), &ext);
+	return ext.xOff;
+}
+
+static void pixel_to_xftcolor(unsigned long pixel, XftColor *out)
+{
+	XColor xc = {0};
+	xc.pixel = pixel;
+	XQueryColor(dpy, DefaultColormap(dpy, scr), &xc);
+	out->color.red   = xc.red;
+	out->color.green = xc.green;
+	out->color.blue  = xc.blue;
+	out->color.alpha = 0xffff;
+	out->pixel = pixel;
+}
+
+static void resolve_module_colours(void)
+{
+	Visual  *vis  = DefaultVisual(dpy, scr);
+	Colormap cmap = DefaultColormap(dpy, scr);
+	for (int i = 0; i < config.module_count; i++) {
+		if (config.modules[i].colour) {
+			XftColorAllocName(dpy, vis, cmap,
+			                  config.modules[i].colour,
+			                  &config.modules[i].xft_colour);
+			config.modules[i].has_colour = 1;
+		}
+	}
+}
+
 void cleanup_modules(void)
 {
+	Visual  *vis  = DefaultVisual(dpy, scr);
+	Colormap cmap = DefaultColormap(dpy, scr);
 	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].scroll_up_command);
 		free(config.modules[i].scroll_down_command);
+		free(config.modules[i].colour);
+		if (config.modules[i].has_colour)
+			XftColorFree(dpy, vis, cmap, &config.modules[i].xft_colour);
 		free(config.modules[i].cached_output);
 	}
 	free(config.modules);
@@ -113,13 +154,20 @@ void cleanup_resources(void)
 	if (monitors) {
 		XFree(monitors);
 	}
-	if (font) {
-		XFreeFont(dpy, font);
+	if (xft_draws) {
+		for (int i = 0; i < nmonitors; i++)
+			XftDrawDestroy(xft_draws[i]);
+		free(xft_draws);
 	}
-	if (gc) {
+	if (font)
+		XftFontClose(dpy, font);
+	if (gc)
 		XFreeGC(dpy, gc);
-	}
 	if (dpy) {
+		Visual  *vis  = DefaultVisual(dpy, scr);
+		Colormap cmap = DefaultColormap(dpy, scr);
+		XftColorFree(dpy, vis, cmap, &xft_fg);
+		XftColorFree(dpy, vis, cmap, &xft_bg);
 		XCloseDisplay(dpy);
 	}
 }
@@ -189,19 +237,27 @@ void create_bars(void)
 		XMapRaised(dpy, wins[i]);
 	}

-	gc = XCreateGC(dpy, wins[0], 0, NULL);
-	XSetForeground(dpy, gc, config.foreground_colour);
-	font = XLoadQueryFont(dpy, config.font);
-	if (!font) {
+	gc   = XCreateGC(dpy, wins[0], 0, NULL);
+	font = XftFontOpenName(dpy, scr, config.font);
+	if (!font)
 		errx(1, "could not load font %s", config.font);
-	}
-	XSetFont(dpy, gc, font->fid);
+
+	Visual  *vis  = DefaultVisual(dpy, scr);
+	Colormap cmap = DefaultColormap(dpy, scr);
+	pixel_to_xftcolor(config.foreground_colour, &xft_fg);
+	pixel_to_xftcolor(config.background_colour, &xft_bg);
+
+	xft_draws = malloc(nmonitors * sizeof *xft_draws);
+	for (int i = 0; i < nmonitors; i++)
+		xft_draws[i] = XftDrawCreate(dpy, buffers[i], vis, cmap);
 }

 static void draw_bar_into(Drawable draw, int monitor_index)
 {
+	XftDraw *d = xft_draws[monitor_index];
 	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);
@@ -210,18 +266,18 @@ static void draw_bar_into(Drawable draw, int monitor_index)
 	int name_count = 0;
 	char **names = get_workspace_name(&name_count);

-	unsigned text_y = (h + font->ascent - font->descent) / 2;
+	int 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;
+	int cur_x = config.text_padding + pad;

 	/* workspaces */
 	if (names) {
-		unsigned *pos = malloc(name_count * sizeof *pos);
-		unsigned *wd = malloc(name_count * sizeof *wd);
+		int *pos = malloc(name_count * sizeof *pos);
+		int *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));
+			wd[i]  = text_width(tmp);
 			pos[i] = cur_x;
 			cur_x += wd[i] + ws_sp;
 		}
@@ -230,54 +286,57 @@ static void draw_bar_into(Drawable draw, int monitor_index)
 			snprintf(tmp, sizeof tmp, " %s ", names[i]);
 			if (i == current_ws) {
 				XSetForeground(dpy, gc, config.foreground_colour);
-				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);
+				XFillRectangle(dpy, draw, gc, pos[i] - pad,
+				               text_y - font->ascent - pad,
+				               wd[i] + 2 * pad,
+				               font->ascent + font->descent + 2 * pad);
+				XftDrawStringUtf8(d, &xft_bg, font, pos[i], text_y,
+				                  (const FcChar8 *)tmp, strlen(tmp));
+			} else {
+				XftDrawStringUtf8(d, &xft_fg, font, pos[i], text_y,
+				                  (const FcChar8 *)tmp, strlen(tmp));
 			}
-			else {
-				XSetForeground(dpy, gc, config.foreground_colour);
-			}
-			XDrawString(dpy, draw, gc, pos[i], text_y, tmp, strlen(tmp));
-
- 			int max_boxes = 4;
-            int box_size = 5;
-            int box_spacing = 2;
-			int win_count = 0;
+
+			int max_boxes = 4, box_size = 5, box_spacing = 2, win_count = 0;
 			{
-    		Atom at = XInternAtom(dpy, "_NET_CLIENT_LIST", False);
-    		Atom ret_type;
-    		int fmt;
-    		unsigned long nclients, afterclients;
-    		unsigned char *clients_data = NULL;
-    		if (XGetWindowProperty(dpy, root, at, 0, (~0L), False, XA_WINDOW, &ret_type, &fmt, &nclients, &afterclients, &clients_data) == Success && clients_data) {
-        		Atom ws_atom = XInternAtom(dpy, "_NET_WM_DESKTOP", False);
-        		for (unsigned long j = 0; j < nclients; j++) {
-            		Window win = ((Window *)clients_data)[j];
-            		unsigned long ndesk, afterdesk;
-            		unsigned char *ws_data = NULL;
-            		if (XGetWindowProperty(dpy, win, ws_atom, 0, 1, False, XA_CARDINAL, &ret_type, &fmt, &ndesk, &afterdesk, &ws_data) == Success && ws_data) {
-                		unsigned long win_ws = *(unsigned long *)ws_data;
-                		XFree(ws_data);
-                			if ((int)win_ws == i && window_on_monitor(win, monitor_index))
-                    			win_count++;
-           }
-		        }
-        		XFree(clients_data);
-    		}
-		}
-            if (win_count > 0) {
-                if (win_count > max_boxes) win_count = max_boxes;
-                // Färg: svart om markerad, annars foreground
-                unsigned long box_col = (i == current_ws) ? parse_col("#000000") : config.foreground_colour;
-                XSetForeground(dpy, gc, box_col);
-                for (int b = 0; b < win_count; b++) {
-                    int box_x = pos[i] - pad + 1 + b * (box_size + box_spacing); // 1 pixel från vänster kant
-					int box_y = text_y - font->ascent - pad + 1; // 1 pixel från toppen
-                    XFillRectangle(dpy, draw, gc, box_x, box_y, box_size, box_size);
-                }
-                // Återställ foreground
-                XSetForeground(dpy, gc, (i == current_ws) ? config.background_colour : config.foreground_colour);
-            }
+				Atom at = XInternAtom(dpy, "_NET_CLIENT_LIST", False);
+				Atom ret_type;
+				int fmt;
+				unsigned long nclients, afterclients;
+				unsigned char *clients_data = NULL;
+				if (XGetWindowProperty(dpy, root, at, 0, (~0L), False,
+				                       XA_WINDOW, &ret_type, &fmt,
+				                       &nclients, &afterclients,
+				                       &clients_data) == Success && clients_data) {
+					Atom ws_atom = XInternAtom(dpy, "_NET_WM_DESKTOP", False);
+					for (unsigned long j = 0; j < nclients; j++) {
+						Window win = ((Window *)clients_data)[j];
+						unsigned long ndesk, afterdesk;
+						unsigned char *ws_data = NULL;
+						if (XGetWindowProperty(dpy, win, ws_atom, 0, 1, False,
+						                       XA_CARDINAL, &ret_type, &fmt,
+						                       &ndesk, &afterdesk,
+						                       &ws_data) == Success && ws_data) {
+							unsigned long win_ws = *(unsigned long *)ws_data;
+							XFree(ws_data);
+							if ((int)win_ws == i && window_on_monitor(win, monitor_index))
+								win_count++;
+						}
+					}
+					XFree(clients_data);
+				}
+			}
+			if (win_count > 0) {
+				if (win_count > max_boxes) win_count = max_boxes;
+				unsigned long box_col = (i == current_ws)
+				    ? parse_col("#000000") : config.foreground_colour;
+				XSetForeground(dpy, gc, box_col);
+				for (int b = 0; b < win_count; b++) {
+					int box_x = pos[i] - pad + 1 + b * (box_size + box_spacing);
+					int box_y = text_y - font->ascent - pad + 1;
+					XFillRectangle(dpy, draw, gc, box_x, box_y, box_size, box_size);
+				}
+			}
 			free(names[i]);
 		}
 		free(names);
@@ -285,35 +344,33 @@ static void draw_bar_into(Drawable draw, int monitor_index)
 		free(wd);
 	}

-	XSetForeground(dpy, gc, config.foreground_colour);
-
 	/* modules */
 	int total_mw = 0;
 	for (int i = 0; i < config.module_count; i++) {
-		if (!config.modules[i].enabled || !config.modules[i].cached_output) {
+		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;
+		total_mw += text_width(config.modules[i].cached_output) + mod_sp;
 	}
-	int ver_w = config.show_version ? XTextWidth(font, config.version_text, strlen(config.version_text)) : 0;
+	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;

 	for (int i = 0; i < config.module_count; i++) {
-		if (!config.modules[i].enabled || !config.modules[i].cached_output) {
+		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));
+		int tw = text_width(out);
+		XftColor *col = config.modules[i].has_colour
+		    ? &config.modules[i].xft_colour : &xft_fg;
+		XftDrawStringUtf8(d, col, font, mx, text_y, (const FcChar8 *)out, strlen(out));
 		mx += tw + mod_sp;
 	}

 	/* version */
 	if (config.show_version) {
 		int vx = w - ver_w - config.text_padding - pad;
-		XDrawString(dpy, draw, gc, vx, text_y, config.version_text, strlen(config.version_text));
+		XftDrawStringUtf8(d, &xft_fg, font, vx, text_y,
+		                  (const FcChar8 *)config.version_text,
+		                  strlen(config.version_text));
 	}
 }

@@ -399,18 +456,16 @@ 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;
-		total_mw += XTextWidth(font, config.modules[i].cached_output,
-		                       strlen(config.modules[i].cached_output)) + mod_sp;
+		total_mw += text_width(config.modules[i].cached_output) + mod_sp;
 	}
-	int ver_w = config.show_version
-	    ? XTextWidth(font, config.version_text, strlen(config.version_text)) : 0;
+	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;

 	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));
+		int tw = text_width(out);
 		if (x_click >= mx && x_click < mx + tw + mod_sp) {
 			if (btn == Button1 && config.modules[i].click_command)
 				spawn(config.modules[i].click_command);
@@ -456,7 +511,7 @@ 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("monospace:size=10");
 	config.show_version = True;
 	config.version_text = strdup(SXBAR_VERSION);
 	init_modules();
@@ -621,6 +676,7 @@ void setup(void)
 	init_defaults();
 	parse_config(&config);
 	create_bars();
+	resolve_module_colours();
 	update_modules();
 }