Crafting a status bar with dzen2

September 24, 2018

dzen2 is a fork of dzen, which is a general purpose messaging, notification and menuing program for X11. What most people use it for, however, is to create cool-looking and highly functional statusbars/docks. (Just look at earsplit’s desktop to get an idea).

How you go about doing this, is you craft the content of the status line, and pipe it into dzen2. Let’s try a sample “Hello world” to demonstrate. I’ll periodically write the output of date, with a 1 second sleep, to dzen2.

$ while :; do date; sleep 1; done | dzen2

Usually, you’d probably do this in Bash or Python, which is the sane thing to do. I, however, prefer to stick to C. So here we go.

Let’s start with a goal in mind. I want to get a bar which outputs the current music I’m playing and its playing/paused state, the current audio volume, WiFi SSID, battery percentage with colour coding, kernel version, and date/time. So let’s put a framework in place for that. Now, since the different elements are probably going to change at different rates, how I’m going to do this is maintain global buffers for each element, which will be updated at different times, and which I’ll then combine into a consolidated statusline when any element has changed, and pipe this to dzen2.

#include <stdio.h>
#include <stdlib.h>

static char music_buf[128] = { 0 };
static char volume_buf[128] = { 0 };
static char wifi_buf[260] = { 0 };
static char battery_buf[128] = { 0 };
static char kernel_buf[256] = { 0 };
static char datetime_buf[128] = { 0 };

static void read_music() {
	// TODO ..
}

static void read_volume() {
	// TODO ..
}

static void read_wifi() {
	// TODO ..
}

static void read_battery() {
	// TODO ..
}

static void read_kernel() {
	// TODO ..
}

static void read_datetime() {
	// TODO ..
}

int main() {
	FILE *fp;
	if (!(fp = popen("dzen2 -bg '#1d1d1d' -h 18 -w 1920 -fn \"DejaVu Sans Mono-9\" -dock", "w"))) {
		exit(-1);
	}
	read_kernel();
	while (1) {
		read_music();
		read_volume();
		read_battery();
		read_datetime();
		read_wifi();
		fprintf(fp, " %s | %s | %s | %s | %s | %s ", music_buf, volume_buf, wifi_buf, battery_buf, kernel_buf, datetime_buf);
		fflush(fp);
		sleep(1);
	}
	return 0;
}

Now we just need to fill in those functions, right? Well.. not so simple. Okay, some of them are pretty straightforward. Let’s fill in the functions one by one. Starting with read_datetime(), where we just use strftime() to format the current date/time in the desired format of Day, Date Month Hour:Minute. eg. Mon, 24 Sep 23:53.

#include <sys/time.h>

// ...

static void read_datetime() {
	time_t rawtime;
	struct tm *tminfo;
	time(&rawtime);
	tminfo = localtime(&rawtime);
	strftime(datetime_buf, sizeof(datetime_buf), "%a, %d %b %H:%M", tminfo);
}

Next let’s tackle another easy one - read_battery(). Here we first come across the formatting commands you can include within dzen2 input strings. We will use such formatting to colour the battery percentage reading based on the reading itself. The battery status (Charging/Discarging) can be read from /sys/class/power_supply/BAT0/status (Atleast on my system. The specific path might vary on yours), and the current capacity can be read from /sys/class/power_supply/BAT0/capacity. This knowledge leads us to a simple implementation.

static void read_battery() {
	static char status[64];
	unsigned cap;
	FILE *capfp, *statfp;
	if (!(statfp = fopen("/sys/class/power_supply/BAT0/status", "r"))) {
		perror("fopen()");
		exit(-1);
	}
	if (!(capfp = fopen("/sys/class/power_supply/BAT0/capacity", "r"))) {
		perror("fopen()");
		exit(-1);
	}
	fgets(status, sizeof(status), statfp);
	fscanf(capfp, "%u", &cap);
	if (!strncmp(status, "Charging", 8)) {
		snprintf(battery_buf, sizeof(battery_buf), "%u%%", cap);
	} else if (cap == 100) {
		snprintf(battery_buf, sizeof(battery_buf), "^fg(#22ff22)full^fg()");
	} else if (cap >= 90) {
		snprintf(battery_buf, sizeof(battery_buf), "^fg(#22ff22)%u%%^fg()", cap);
	} else if (cap >= 75) {
		snprintf(battery_buf, sizeof(battery_buf), "^fg(#88ff22)%u%%^fg()", cap);
	} else if (cap >= 45) {
		snprintf(battery_buf, sizeof(battery_buf), "^fg(#ffff22)%u%%^fg()", cap);
	} else if (cap >= 15) {
		snprintf(battery_buf, sizeof(battery_buf), "^fg(#ff8844)%u%%^fg()", cap);
	} else {
		snprintf(battery_buf, sizeof(battery_buf), "^fg(#ff1111)%u%%^fg()", cap);
	}
	fclose(statfp);
	fclose(capfp);
}

We can read the kernel version with uname, the WiFi SSID with nmcli and the current volume with amixer, so let’s fill those in. Also, notice how we need to escape out the backslash character in the sed command, in the read_volume() function.

#include <string.h>

// ...

static void read_kernel() {
	size_t len;
	FILE *fp;
	if (!(fp = popen("uname -r", "r"))) {
		perror("popen()");
		exit(-1);
	}
	fgets(kernel_buf, sizeof(kernel_buf), fp);
	pclose(fp);
	len = strlen(kernel_buf);
	if (kernel_buf[len - 1] == '\n') {
		kernel_buf[len - 1] = '\0';
	}
}

static void read_wifi() {
	FILE *fp;
	size_t len;
	if (!(fp = popen("nmcli -t -f active,ssid dev wifi | egrep '^yes' | cut -d ':' -f 2", "r"))) {
		perror("popen()");
		exit(-1);
	}
	if (!fgets(wifi_buf, sizeof(wifi_buf), fp) || wifi_buf[0] == '\0' || wifi_buf[0] == ':') {
		snprintf(wifi_buf, sizeof(wifi_buf), "- wifi down -");
		return;
	}
	len = strlen(wifi_buf);
	if (wifi_buf[len - 1] == '\n') {
		wifi_buf[len - 1] = '\0';
	}
	pclose(fp);
}

static void read_volume() {
	char stat[8];
	unsigned val;
	FILE *fp;
	if (!(fp = popen("amixer get Master | sed -n 'N;s/^.*\\[\\([0-9]\\+\\).*\\[\\([a-z]\\+\\).*$/\\1 \\2/p'", "r"))) {
		perror("popen()");
		exit(-1);
	}
	fscanf(fp, "%u %s", &val, stat);
	pclose(fp);
	if (strcmp(stat, "on")) {
		snprintf(volume_buf, sizeof(volume_buf), "(mute) %u%%", val);
	} else {
		snprintf(volume_buf, sizeof(volume_buf), "%u%%", val);
	}
}

There are a few problems with what we have so far, have you noticed? Basically, you don’t necessarily need to poll the date/time, battery, or WiFi SSID every second. Nor the volume. In fact, checking the volume every second might cause delayed responses to volume change, as compared to a near-instantaneous response to the pressing of one of the volume buttons. We can do with with a signal handler. So, we setup a handler for the SIGUSR1 signal, which triggers a volume update. Otherwise, the volume isn’t read. Also, we arbitrarily set a loop counter for the WiFi, battery, and date/time. So let’s rework our main loop somewhat.

Note: In your system’s keybindings, you’d have to add a command to send SIGUSR1 to this program. So, if your program is called dzen2_ctrl, you could add something like pkill -SIGUSR1 -f dzen2_ctrl.

#include <signal.h>

// ...

static int do_read_volume = 1;

static void signal_handler(int signum) {
	if (signum == SIGUSR1) {
		do_read_volume = 1;
	}
}

// ...

int main() {
	int bat_count = 10, dt_count = 5, wifi_count = 5, vol_changed = 0;
	// ...
	signal(SIGUSR1, signal_handler);
	read_kernel();
	while (1) {
		read_music();
		if (do_read_volume) {
			do_read_volume = 0;
			read_volume();
			vol_changed = 1;
		}
		if (bat_count == 10) {
			bat_count = 0;
			read_battery();
		}
		if (dt_count == 5) {
			dt_count = 0;
			read_datetime();
		}
		if (wifi_count == 5) {
			wifi_count = 0;
			read_wifi();
		}
		if (vol_changed || bat_count == 0 || dt_count == 0 || wifi_count == 0) {
			vol_changed = 0;
			fprintf(fp, " %s | %s | %s | %s | %s | %s ", music_buf, volume_buf, wifi_buf, battery_buf, kernel_buf, datetime_buf);
			fflush(fp);
		}
		sleep(1);
		bat_count++;
		dt_count++;
		wifi_count++;
	}
	return 0;
}

Now finally, we come to music. I’m using MPD as my music player of choice, and it runs as a daemon, providing a TCP-based interface which uses a defined protocol for sending commands and requesting information. It also provides a mechanism to wait for an update in the music state. So how we’re going to do this is - create a socket which connects to this daemon, waits for updates, and then reads the song, artist, and playing/paused state. If the daemon is not running, we decide it is “stopped”.

#include <fcntl.h>
#include <sys/socket.h>
#include <netdb.h>

#define MPD_PORT "6600"

#define MUSIC_STATUS_PLAY  0
#define MUSIC_STATUS_PAUSE 1
#define MUSIC_STATUS_STOP  2
#define MUSIC_STATUS_ERR  -1

// ...

static int get_socket() {
	int ret, sock = -1;
	struct addrinfo hints, *res, *p;
	memset(&hints, 0, sizeof(hints));
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_flags = AI_PASSIVE;
	if ((ret = getaddrinfo("127.0.0.1", MPD_PORT, &hints, &res))) {
		fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ret));
		exit(-1);
	}
	for (p = res; p; p = p->ai_next) {
		if ((sock = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) <= 0) {
			sock = -1;
			continue;
		}
		while (connect(sock, p->ai_addr, p->ai_addrlen) < 0) {
			if (errno == EAGAIN) {
				continue;
			}
			close(sock);
			sock = -1;
			break;
		}
		if (sock < 0) {
			continue;
		}
		freeaddrinfo(res);
		return sock;
	}
	freeaddrinfo(res);
	return sock;
}

static int do_read(int sock, char *buf, int len) {
	int ret;
	while ((ret = read(sock, buf, len)) < 0) {
		if (errno == EAGAIN) {
			continue;
		}
		perror("read()");
		return -1;
	}
	if (ret == 0) {
		fprintf(stderr, "Connection closed'\0'");
	}
	buf[ret] = '\0';
	return ret;
}

static int do_write(int sock, char *buf, int len) {
	int ret;
	while ((ret = write(sock, buf, len)) < 0) {
		if (errno == EAGAIN) {
			continue;
		}
		perror("write()");
		return -1;
	}
	return ret;
}

static void parse_cursong(char *buf, char **p_artist, char **p_song) {
	char *artist, *song, *ptr;
	if ((artist = strstr(buf, "Artist: "))) {
		artist += 8;
	} else {
		artist = NULL;
	}
	if ((song = strstr(buf, "Title: "))) {
		song += 7;
	} else if ((song = strstr(buf, "file: "))) {
		song += 6;
	} else {
		fprintf(stderr, "Error: Should not be asking for song here\n");
		exit(-1);
	}
	if (artist) {
		ptr = strchr(artist, '\n');
		*ptr = '\0';
	}
	ptr = strchr(song,  '\n');
	*ptr = '\0';
	*p_artist = artist;
	*p_song = song;
}

static int parse_state(char *buf) {
	char *ptr;
	if (!(buf = strstr(buf, "\nstate: "))) {
		return MUSIC_STATUS_ERR;
	}
	buf += 8;
	if (!(ptr = strchr(buf, '\n'))) {
		return MUSIC_STATUS_ERR;
	}
	*ptr = '\0';
	if (!strcmp(buf, "play")) {
		return MUSIC_STATUS_PLAY;
	}
	if (!(strcmp(buf, "pause"))) {
		return MUSIC_STATUS_PAUSE;
	}
	if (!(strcmp(buf, "stop"))) {
		return MUSIC_STATUS_STOP;
	}
	return MUSIC_STATUS_ERR;
}

static void fini_music(int sock) {
	close(sock);
}

static int read_music(int sock) {
	int status;
	char buf[1024], *artist, *song;
	if (do_write(sock, "status\n", 7) < 0) {
		return -1;
	}
	if (do_read(sock, buf, sizeof(buf)) <= 0) {
		return -1;
	}
	status = parse_status(buf);
	switch (status) {
	case MUSIC_STATUS_ERR:
		snprintf(music_buf, sizeof(music_buf), "- error -");
		break;
	case MUSIC_STATUS_STOP:
		snprintf(music_buf, sizeof(music_buf), "- stopped -");
		break;
	case MUSIC_STATUS_PLAY:
	case MUSIC_STATUS_PAUSE:
		if (do_write(sock, "currentsong\n", 12) < 0) {
			return -1;
		}
		if (do_read(sock, buf, sizeof(buf)) <= 0) {
			return -1;
		}
		parse_cursong(buf, &artist,  &song);
		if (status == MUSIC_STATUS_PAUSE) {
			if (artist) {
				snprintf(music_buf, sizeof(music_buf), "(pause) %s - %s", artist, song);
			} else {
				snprintf(music_buf, sizeof(music_buf), "(pause) %s", song);
			}
		} else {
			if (artist) {
				snprintf(music_buf, (music_buf), "%s - %s", artist, song);
			} else {
				snprintf(music_buf, sizeof(music_buf), "%s", song);
			}
		}
	}
	// Queue next status update notification
	if (do_write(sock, "idle player\n", 12) < 0) {
		return -1;
	}
	return 0;
}

static int init_music() {
	int sock;
	char buf[1024];
	if ((sock = get_socket()) < 0) {
		fprintf(stderr, "Error: Could not connect to mpd\n");
		return -1;
	}
	// Read initial message
	if (do_read(sock, buf, sizeof(buf)) <= 0 || strncmp(buf, "OK MPD", 6)) {
		fprintf(stderr, "Error: Service on port 6600 is not mpd\n");
		return -1;
	}
	// Get song and status now
	if (read_music(sock) < 0) {
		fini_music(sock);
		return -1;
	}
	return sock;
}

And finally, we need to make a few changed to our main function, so that the main loop waits on select() on the music socket, with a timeout of 1 second, instead of just sleeping. Note that the current code doesn’t support MPD starting after dzen2, so if MPD is killed while this program is running, it won’t be able to detect it coming back to life again. Unless of course this program is restarted.

#include <sys/select.h>

// ...

int main() {
	fd_set rfds;
	int music_fd;
	struct timeval tv;
	tv.tv_sec = 1;
	tv_tv_nsec = 0;
	// ...
	if ((music_fd = init_music()) < 0) {
		snprintf(music_buf, sizeof(music_buf), "- no player -");
	}
	while (1) {
		// ...
		if (music_fd > 0) {
			FD_ZERO(&rfds);
			FD_SET(music_fd, &rfds);
			while (ret = select(&rfds, NULL, NULL, &tv) < 0) {
				if (errno == EAGAIN) {
					continue;
				}
				fini_music(music_fd);
				music_fd = -1;
				break;
			}
			if (music_fd > 0) {
				if (read_music() < 0) {
					fini_music(music_fd);
					music_fd = -1;
				}
			}
			if (music_fd < 0) {
				snprintf(music_buf, sizeof(music_buf), "- no player -");
			}
		} else {
			sleep(1);
		}
	}
	fini_music();
	return 0;
}

And there we have it, a functional status bar. You could go ahead an work on things like icons or on-click popups which dzen2 supports. This post was just intended to give you an idea of what’s possible.