commit e0c13f69e2e6ff3fad32d03524ed35b16fa5ff79
Author: MrJensK <jens.se@icloud.com>
AuthorDate: Tue Jul 28 19:56:58 2026 +0200
Commit: MrJensK <jens.se@icloud.com>
CommitDate: Tue Jul 28 19:56:58 2026 +0200
lots and lots of changes, including a new battery icon and volume icon script, as well as updates to the Makefile, README, and source files.
---
Makefile | 5 +
README.md | 230 ++++++++++++++++++++++++++++
default_sxbarc | 74 ++++++++-
scripts/battery_icon.sh | 20 +++
scripts/volume_icon.sh | 17 +++
src/defs.h | 23 +++
src/parser.c | 91 ++++++++++-
src/sxbar.c | 394 ++++++++++++++++++++++++++++++++----------------
8 files changed, 716 insertions(+), 138 deletions(-)
diff --git a/Makefile b/Makefile
index a44d8fc..9957087 100644
--- a/Makefile
+++ b/Makefile
@@ -40,6 +40,9 @@ install: all
@echo "Copying default configuration to $(DESTDIR)$(PREFIX)/share/sxbarc..."
@mkdir -p "$(DESTDIR)$(PREFIX)/share"
@install -m 644 default_sxbarc "$(DESTDIR)$(PREFIX)/share/sxbarc"
+ @echo "Installing example prefix_cmd scripts to $(DESTDIR)$(PREFIX)/share/sxbar/scripts..."
+ @mkdir -p "$(DESTDIR)$(PREFIX)/share/sxbar/scripts"
+ @install -m 755 scripts/*.sh "$(DESTDIR)$(PREFIX)/share/sxbar/scripts/"
@echo "Installation complete."
uninstall:
@@ -47,6 +50,8 @@ uninstall:
@rm -f "$(DESTDIR)$(PREFIX)/bin/$(BIN)"
@echo "Uninstalling man page from $(DESTDIR)$(MAN_DIR)..."
@rm -f $(DESTDIR)$(MAN_DIR)/$(MAN)
+ @echo "Uninstalling example scripts from $(DESTDIR)$(PREFIX)/share/sxbar..."
+ @rm -rf "$(DESTDIR)$(PREFIX)/share/sxbar"
@echo "Uninstallation complete."
.PHONY: all clean install uninstall
diff --git a/README.md b/README.md
index 90fd51f..467caac 100644
--- a/README.md
+++ b/README.md
@@ -134,6 +134,236 @@ prefix : cpu : " "
- Custom modules could already embed an icon directly in their command, but
`prefix` keeps that separate from the command itself.
+## Dynamic module prefixes (prefix_cmd)
+
+`prefix` is static text. `prefix_cmd` lets a script pick the icon instead,
+so it can reflect module state — e.g. a battery icon that changes with
+charge level and charging status, or a volume icon that shows muted.
+
+### Changes
+- Added `prefix_command` and `prefix_cached` fields to the `Module` struct.
+- Added `prefix_cmd` directive to the config parser (`icon_cmd` is an alias),
+ sharing the same quoted-command parsing as `click`/`scroll_up`/`scroll_down`.
+- `update_modules()` re-runs the prefix command every time the module itself
+ refreshes (same `refresh_interval`), passing the module's freshly fetched
+ output as `$1`, shell-quoted to avoid injection.
+- `module_text()` prefers the live `prefix_cached` output over the static
+ `prefix` when a module has a `prefix_cmd`.
+
+### Config syntax
+
+```
+prefix_cmd : module_name : "command or script path"
+```
+
+The script receives the module's current output (e.g. `"83%"`) as `$1`.
+Built-in module commands don't expose extra state like charging or muted
+beyond that string, so a script that needs it re-checks the system itself
+(e.g. `/sys/class/power_supply/BAT*/status`, `wpctl get-volume ... | grep
+MUTED`). The script's stdout becomes the prefix verbatim — include your own
+trailing space if you want one before the value.
+
+### Example
+
+`~/.config/sxbar/scripts/battery_icon.sh`:
+
+```sh
+#!/bin/sh
+pct=$(printf '%s' "$1" | tr -dc '0-9')
+pct=${pct:-0}
+status=$(cat /sys/class/power_supply/BAT*/status 2>/dev/null | head -n1)
+
+if [ "$status" = "Charging" ]; then
+ printf '%s ' '' # bolt glyph
+ exit 0
+fi
+
+if [ "$pct" -ge 90 ]; then printf '%s ' '' # battery-full
+elif [ "$pct" -ge 50 ]; then printf '%s ' '' # battery-three-quarters
+elif [ "$pct" -ge 20 ]; then printf '%s ' '' # battery-half
+elif [ "$pct" -ge 10 ]; then printf '%s ' '' # battery-quarter
+else printf '%s ' '' # battery-empty
+fi
+```
+
+```
+prefix_cmd : battery : "~/.config/sxbar/scripts/battery_icon.sh"
+```
+
+This and a matching `volume_icon.sh` live under `scripts/` in this repo and
+are installed by `make install` to `$(PREFIX)/share/sxbar/scripts/` as
+reference copies — `sxbarc` still points at your own copy under
+`~/.config/sxbar/scripts/`, so copy and edit from there rather than the
+installed ones.
+
+### Result
+
+- Icons can reflect live state instead of being fixed per module.
+- Logic lives in an ordinary shell script, not in sxbar's C source.
+
+## Fixed-width modules
+
+Right-aligned layout means every module's on-screen position depends on
+the total width of everything to its right. Without a fixed width, a
+module like `cpu` shifts the entire bar each time its digit count changes
+(e.g. `9%` -> `16%` -> `100%`).
+
+### Changes
+- Added `min_width` field to the `Module` struct.
+- Added `width` directive to the config parser.
+- Added `module_slot_width()`, used in both `draw_bar_into()` and
+ `hdl_button()` in place of the raw text width when computing layout
+ positions and click-hit regions — the module's own text is still drawn
+ and measured normally, only the space reserved for it in the layout
+ changes.
+
+### Config syntax
+
+```
+width : module_name : min_pixels
+```
+
+The module's text is never truncated — if it's wider than `min_width` it
+just uses its natural width, same as before. Pick a value wide enough for
+the largest expected reading (e.g. the pixel width of `"100%"` plus its
+icon, in your configured font/size).
+
+### Example
+
+```
+width : battery : 48
+width : volume : 48
+width : cpu : 48
+```
+
+### Result
+
+- Modules with a `width` set no longer shift their neighbours around when
+ their value's digit count changes.
+- Click regions stay aligned with the reserved (not just the rendered) width.
+
+## Secondary bar (two bars, one top, one bottom)
+
+A second bar can be enabled on the edge opposite the primary one. It is
+modules-only: no workspace switcher, no version text — just whichever
+modules are tagged onto it.
+
+### Changes
+- Replaced the parallel per-monitor arrays (`wins`, `buffers`, `xft_draws`)
+ with a single `Bar` array (`src/defs.h`), each entry holding its monitor
+ index, whether it's the secondary variant, and its own window/pixmap/
+ Xft draw context. `nbars = nmonitors * (secondary_bar ? 2 : 1)`.
+- `create_bars()` creates one bar per monitor, or two (opposite edges) per
+ monitor when `secondary_bar` is on. Each bar computes its own geometry
+ and `_NET_WM_STRUT_PARTIAL` independently, so both reserve their own
+ screen edge correctly.
+- `draw_bar_into()`, `hdl_button()` now take a bar index and filter the
+ module list by `on_secondary`; the workspace switcher and version text
+ are skipped entirely for secondary bars.
+- Added `on_secondary` field to `Module` and the `bar` config directive.
+- Renamed `find_window_monitor`/`redraw_monitor` to `find_bar`/`redraw_bar`
+ (they now index into `bars`, not `monitors`, since bar count and
+ monitor count differ once a secondary bar exists).
+
+### Config syntax
+
+```
+secondary_bar : true
+
+bar : module_name : primary|secondary
+```
+
+`bar` defaults to `primary` — only set it for modules moving to the
+secondary bar. The secondary bar always sits on the edge opposite
+`bottom_bar`, and currently shares the primary bar's font/colours/height
+(no independent per-bar styling yet).
+
+### Example
+
+```
+bottom_bar : false
+secondary_bar : true
+
+module : battery : true : 30
+module : volume : true : 5
+module : cpu : true : 3
+
+bar : battery : secondary
+bar : volume : secondary
+bar : cpu : secondary
+```
+
+Clock, date and the workspace switcher stay on the primary bar (top);
+battery, volume and cpu move to a plain modules-only bar (bottom).
+
+### Result
+
+- Two bar windows, one per screen edge, coexist without clobbering each
+ other's strut reservation.
+- Existing single-bar configs are unaffected — `secondary_bar` defaults to
+ off and every module defaults to the primary bar.
+
+## Module alignment (left / center / right)
+
+Modules used to always draw as one right-anchored cluster. They can now be
+split into three independently-anchored groups per bar.
+
+### Changes
+- Added `align` field to `Module` (`ALIGN_LEFT`/`ALIGN_CENTER`/`ALIGN_RIGHT`
+ in `src/defs.h`; `ALIGN_RIGHT` is `0` so unset modules keep today's
+ behaviour with no config changes needed).
+- Added `align` config directive.
+- `draw_bar_into()`'s single module loop became three: left continues on
+ from wherever the workspace switcher ended, center is centered across
+ the full bar width, right is anchored before `version_text` exactly like
+ before.
+- `hdl_button()` mirrors the same three groups for click-hit-testing.
+ Added `workspace_end_x()` so it can compute the left group's start
+ position without duplicating the workspace-drawing loop.
+
+### Config syntax
+
+```
+align : module_name : left|center|right
+```
+
+### Example
+
+```
+align : clock : left
+align : date : left
+align : cpu : center
+```
+
+battery/volume (no `align` set) stay in the default right-anchored group.
+
+### Result
+
+- Modules can be grouped into left/center/right clusters, e.g. workspaces
+ and clock on the left, a custom module centered, system stats on the
+ right.
+- Existing configs are unaffected — no `align` lines means every module
+ stays in the right cluster, identical to the old single-group layout.
+
+### Bug fix found along the way
+
+Testing this against a real config with `colour : module_name : #hex`
+lines turned up a pre-existing parser bug: `strip_comment()` truncated at
+the *first* `#` in a value, so a hex colour like `#50fa7b` — which starts
+with `#` — was stripped down to an empty string before `strdup`, silently
+making every coloured module invisible (`XftColorAllocName` was also not
+checked, so a failed allocation gave up transparent text). Fixed to only
+treat a `#` as a comment marker when it's not the first character.
+
+**Known limitation, not fixed:** `background_colour`, `foreground_colour`
+and `border_colour` are parsed with `parse_col(rest)` directly and never
+call `strip_comment()` at all (unlike the per-module `colour` directive,
+`font`, `version_text`, etc.) — confirmed correct for plain values via
+pixel sampling (`#000000`/`#7abccd` rendered exact), but a trailing
+`# comment` on one of those three lines would get passed straight into
+`XParseColor` and fail (falls back to white, with a stderr warning).
+Comment on its own line instead for now.
+
## Disk footprint
- `sxbar` is very lightweight: the compiled binary is about 36 KB.
diff --git a/default_sxbarc b/default_sxbarc
index aa90609..2cd2464 100644
--- a/default_sxbarc
+++ b/default_sxbarc
@@ -9,11 +9,19 @@ version_text : sxbar ver. 1.1
# Appearance
height : 20
bottom_bar : false
+# secondary_bar: adds a second, modules-only bar on the edge opposite
+# `bottom_bar` (no workspace switcher, no version text). Tag which modules
+# go on it further down with `bar : module_name : secondary` -- everything
+# else stays on the primary bar. See that section below for details.
+secondary_bar : false
vertical_padding : 0
horizontal_padding : 0
text_padding : 5
border : false
border_width : 1
+# Note: unlike the per-module `colour` directive and most other keys
+# below, these three don't support a trailing "# comment" on the same
+# line -- put comments on their own line instead.
background_colour : #000000
foreground_colour : #7abccd
border_colour : #005577
@@ -49,10 +57,68 @@ module : cpu : false : 3
# (icon : ... is accepted as an alias for prefix)
#
# Examples:
-# prefix : battery : " "
-# prefix : volume : " "
-# prefix : cpu : " "
-# prefix : clock : " "
+# prefix : clock : " "
+# prefix : date : " "
+# prefix : battery : " "
+# prefix : volume : " "
+# prefix : cpu : " "
+
+# Prefix command -- like `prefix`, but the icon comes from an external
+# script instead of fixed text, so it can change with module state (e.g.
+# battery charging vs discharging, volume muted vs not). The script's own
+# stdout (trailing newline stripped) becomes the prefix; it is re-run on
+# the module's normal refresh_interval, and it must include its own
+# trailing space/separator if you want one before the value.
+# The module's current output (e.g. "83%") is passed in as $1, since the
+# built-in module commands don't expose state like charging/muted beyond
+# that string -- scripts that need it re-check the system themselves.
+# prefix_cmd : module_name : "command or script path"
+# (icon_cmd : ... is accepted as an alias for prefix_cmd)
+#
+# Examples:
+# prefix_cmd : battery : "~/.config/sxbar/scripts/battery_icon.sh"
+# prefix_cmd : volume : "~/.config/sxbar/scripts/volume_icon.sh"
+#
+# Reference copies of these two scripts ship in this repo's scripts/ dir
+# and are installed to $(PREFIX)/share/sxbar/scripts/ -- copy them to
+# ~/.config/sxbar/scripts/ (the path above) and edit from there.
+
+# Width -- reserves a minimum pixel width for a module's slot, so the rest
+# of the bar doesn't shift when its text width changes (e.g. cpu going
+# from one digit to two, or three, wide). The module's own text is never
+# truncated; if it's wider than this it just uses its natural width.
+# width : module_name : min_pixels
+#
+# Examples (picking a value wide enough for the largest expected reading,
+# e.g. the width of "100%" plus its icon in your font/size):
+# width : battery : 48
+# width : volume : 48
+# width : cpu : 48
+
+# Bar assignment -- which bar a module is drawn on when secondary_bar is
+# enabled above. Defaults to primary; only needs setting for modules you
+# want to move to the secondary bar. The secondary bar has no workspace
+# switcher and never shows version_text, regardless of show_version --
+# it only ever draws the modules tagged secondary here.
+# bar : module_name : primary|secondary
+#
+# Example (secondary_bar : true, bottom_bar : false puts battery/volume/cpu
+# on their own bar at the bottom, clock/date/workspaces stay on top):
+# bar : battery : secondary
+# bar : volume : secondary
+# bar : cpu : secondary
+
+# Alignment -- which side of the bar a module is anchored to. Defaults to
+# right, so existing configs (which never set this) are unaffected. Each
+# group (left/center/right) is laid out independently -- left continues on
+# from the workspace switcher, center is centered in the bar, right is
+# anchored before version_text, same as today's default cluster.
+# align : module_name : left|center|right
+#
+# Example (workspaces+clock on the left, date centered, the rest on the
+# right where they already are by default):
+# align : clock : left
+# align : date : center
# Per-module text colour (overrides global foreground_colour for that module)
# colour : module_name : #rrggbb
diff --git a/scripts/battery_icon.sh b/scripts/battery_icon.sh
new file mode 100755
index 0000000..2dc8299
--- /dev/null
+++ b/scripts/battery_icon.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+# prefix_cmd for the battery module -- picks a Nerd Font glyph based on
+# charge level and charging status.
+# $1 is the battery module's own output (e.g. "88%"); charging status isn't
+# in that string, so we re-check /sys ourselves.
+pct=$(printf '%s' "$1" | tr -dc '0-9')
+pct=${pct:-0}
+status=$(cat /sys/class/power_supply/BAT*/status 2>/dev/null | head -n1)
+
+if [ "$status" = "Charging" ]; then
+ printf '%s ' ''
+ exit 0
+fi
+
+if [ "$pct" -ge 90 ]; then printf '%s ' ''
+elif [ "$pct" -ge 50 ]; then printf '%s ' ''
+elif [ "$pct" -ge 20 ]; then printf '%s ' ''
+elif [ "$pct" -ge 10 ]; then printf '%s ' ''
+else printf '%s ' ''
+fi
diff --git a/scripts/volume_icon.sh b/scripts/volume_icon.sh
new file mode 100755
index 0000000..7177b74
--- /dev/null
+++ b/scripts/volume_icon.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+# prefix_cmd for the volume module -- picks a Nerd Font glyph based on level
+# and mute state.
+# $1 is the volume module's own output (e.g. "40%"); the built-in command
+# strips the [MUTED] marker to keep the number clean, so we re-check wpctl
+# ourselves here.
+pct=$(printf '%s' "$1" | tr -dc '0-9')
+pct=${pct:-0}
+muted=$(LC_ALL=C wpctl get-volume @DEFAULT_AUDIO_SINK@ 2>/dev/null | grep -c MUTED)
+
+if [ "$muted" -gt 0 ]; then
+ printf '%s ' ''
+elif [ "$pct" -ge 50 ]; then
+ printf '%s ' ''
+else
+ printf '%s ' ''
+fi
diff --git a/src/defs.h b/src/defs.h
index 41be285..9f44fa1 100644
--- a/src/defs.h
+++ b/src/defs.h
@@ -10,6 +10,12 @@
#define MAX_MONITORS 32
+/* module horizontal alignment groups -- RIGHT is 0 so existing configs
+ * (which never set align) keep today's right-anchored-cluster behaviour */
+#define ALIGN_RIGHT 0
+#define ALIGN_LEFT 1
+#define ALIGN_CENTER 2
+
typedef struct Module {
char *name;
char *command;
@@ -18,6 +24,11 @@ typedef struct Module {
char *scroll_down_command;
char *colour;
char *prefix;
+ char *prefix_command;
+ char *prefix_cached;
+ int min_width;
+ int on_secondary;
+ int align;
XftColor xft_colour;
int has_colour;
int enabled;
@@ -43,6 +54,18 @@ typedef struct Config {
Module *modules;
int module_count;
int max_modules;
+ int secondary_bar;
} Config;
+/* one on-screen bar window: either the primary bar (workspaces + all
+ * primary-tagged modules + version) or the secondary bar (modules tagged
+ * `bar : module_name : secondary` only, on the opposite edge) */
+typedef struct Bar {
+ int monitor;
+ int is_secondary;
+ Window win;
+ Pixmap buffer;
+ XftDraw *xft_draw;
+} Bar;
+
typedef void (*EventHandler)(XEvent *);
diff --git a/src/parser.c b/src/parser.c
index 9f302ab..14fe50e 100644
--- a/src/parser.c
+++ b/src/parser.c
@@ -25,7 +25,11 @@ static char *strip(char *s)
static char *strip_comment(char *s)
{
- char *c = strchr(s, '#');
+ /* a '#' as the very first character is a value (e.g. a hex colour like
+ * #50fa7b), not a comment marker -- only a later '#' starts a trailing
+ * comment (e.g. "#50fa7b # my favourite green") */
+ char *scan = (*s == '#') ? s + 1 : s;
+ char *c = strchr(scan, '#');
if (c)
*c = '\0';
return strip(s);
@@ -175,6 +179,10 @@ int parse_config(Config *cfg)
strip_comment(rest);
free(cfg->version_text);
cfg->version_text = strdup(rest);
+ } else if (!strcmp(key, "secondary_bar")) {
+ /* enable a second bar on the opposite edge from `bottom_bar`,
+ * showing only modules tagged `bar : module_name : secondary` */
+ cfg->secondary_bar = parse_bool(rest);
} else if (!strcmp(key, "module")) {
/* module : name : enabled : interval */
char *p1 = strchr(rest, ':');
@@ -277,8 +285,80 @@ int parse_config(Config *cfg)
}
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" */
+ } else if (!strcmp(key, "width")) {
+ /* width : module_name : min_pixels -- reserves at least this much
+ * horizontal space for the module so its neighbours don't shift
+ * when its text width changes (e.g. cpu going from 9% to 16%) */
+ char *p1 = strchr(rest, ':');
+ if (!p1) {
+ fprintf(stderr, "sxbarc:%d: width 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: width: unknown module '%s'\n", lineno, name);
+ continue;
+ }
+ m->min_width = atoi(val);
+ } else if (!strcmp(key, "bar")) {
+ /* bar : module_name : primary|secondary -- which bar the module
+ * is drawn on when secondary_bar is enabled (default: primary) */
+ char *p1 = strchr(rest, ':');
+ if (!p1) {
+ fprintf(stderr, "sxbarc:%d: bar 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: bar: unknown module '%s'\n", lineno, name);
+ continue;
+ }
+ if (!strcmp(val, "secondary")) {
+ m->on_secondary = 1;
+ } else if (!strcmp(val, "primary")) {
+ m->on_secondary = 0;
+ } else {
+ fprintf(stderr, "sxbarc:%d: bar: value must be 'primary' or 'secondary', got '%s'\n",
+ lineno, val);
+ }
+ } else if (!strcmp(key, "align")) {
+ /* align : module_name : left|center|right -- which side of the
+ * bar the module is anchored to (default: right) */
+ char *p1 = strchr(rest, ':');
+ if (!p1) {
+ fprintf(stderr, "sxbarc:%d: align 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: align: unknown module '%s'\n", lineno, name);
+ continue;
+ }
+ if (!strcmp(val, "left")) {
+ m->align = ALIGN_LEFT;
+ } else if (!strcmp(val, "center") || !strcmp(val, "centre")) {
+ m->align = ALIGN_CENTER;
+ } else if (!strcmp(val, "right")) {
+ m->align = ALIGN_RIGHT;
+ } else {
+ fprintf(stderr, "sxbarc:%d: align: value must be 'left', 'center' or 'right', got '%s'\n",
+ lineno, val);
+ }
+ } else if (!strcmp(key, "click") || !strcmp(key, "scroll_up") || !strcmp(key, "scroll_down") ||
+ !strcmp(key, "prefix_cmd") || !strcmp(key, "icon_cmd")) {
+ /* click/scroll_up/scroll_down/prefix_cmd/icon_cmd : module_name : "command" */
char *p1 = strchr(rest, ':');
if (!p1) {
fprintf(stderr, "sxbarc:%d: %s missing name and command\n", lineno, key);
@@ -312,9 +392,12 @@ int parse_config(Config *cfg)
} else if (!strcmp(key, "scroll_up")) {
free(m->scroll_up_command);
m->scroll_up_command = expand_home(cmd_start);
- } else {
+ } else if (!strcmp(key, "scroll_down")) {
free(m->scroll_down_command);
m->scroll_down_command = expand_home(cmd_start);
+ } else {
+ free(m->prefix_command);
+ m->prefix_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 */
diff --git a/src/sxbar.c b/src/sxbar.c
index f0cf8e7..62e21c6 100644
--- a/src/sxbar.c
+++ b/src/sxbar.c
@@ -19,9 +19,9 @@
void cleanup_modules(void);
void cleanup_resources(void);
void create_bars(void);
-static void draw_bar_into(Drawable draw, int monitor_index);
-static void redraw_monitor(int monitor_index);
-int find_window_monitor(Window win);
+static void draw_bar_into(int bar_idx);
+static void redraw_bar(int bar_idx);
+int find_bar(Window win);
int get_current_workspace(void);
char **get_workspace_name(int *count);
void hdl_button(XEvent *xev);
@@ -38,16 +38,15 @@ void update_modules(void);
EventHandler evtable[LASTEvent];
XftFont *font;
-XftDraw **xft_draws;
XftColor xft_fg;
XftColor xft_bg;
Display *dpy;
Window root;
-Window *wins = NULL;
XineramaScreenInfo *monitors = NULL;
GC gc;
Config config;
-Pixmap *buffers = NULL;
+Bar *bars = NULL;
+int nbars = 0;
int nmonitors = 0;
int scr;
@@ -93,16 +92,26 @@ 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 */
+/* module's cached output with its configured prefix (static text, or the
+ * live output of a prefix_cmd script) 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);
+ const char *pre = m->prefix_command ? m->prefix_cached : m->prefix;
+ if (pre && *pre) {
+ snprintf(buf, bufsz, "%s%s", pre, m->cached_output);
return buf;
}
return m->cached_output;
}
+/* space a module reserves in the layout: its text width, or its configured
+ * minimum, whichever is larger -- keeps everything else on the bar from
+ * shifting when the text width changes (e.g. cpu going from one digit to two) */
+static int module_slot_width(Module *m, int text_w)
+{
+ return text_w > m->min_width ? text_w : m->min_width;
+}
+
static void pixel_to_xftcolor(unsigned long pixel, XftColor *out)
{
XColor xc = {0};
@@ -121,10 +130,14 @@ static void resolve_module_colours(void)
Colormap cmap = DefaultColormap(dpy, scr);
for (int i = 0; i < config.module_count; i++) {
if (config.modules[i].colour) {
- XftColorAllocName(dpy, vis, cmap,
+ if (XftColorAllocName(dpy, vis, cmap,
config.modules[i].colour,
- &config.modules[i].xft_colour);
- config.modules[i].has_colour = 1;
+ &config.modules[i].xft_colour)) {
+ config.modules[i].has_colour = 1;
+ } else {
+ fprintf(stderr, "sxbar: cannot parse/color %s for module %s\n",
+ config.modules[i].colour, config.modules[i].name);
+ }
}
}
}
@@ -141,6 +154,8 @@ void cleanup_modules(void)
free(config.modules[i].scroll_down_command);
free(config.modules[i].colour);
free(config.modules[i].prefix);
+ free(config.modules[i].prefix_command);
+ free(config.modules[i].prefix_cached);
if (config.modules[i].has_colour)
XftColorFree(dpy, vis, cmap, &config.modules[i].xft_colour);
free(config.modules[i].cached_output);
@@ -150,26 +165,17 @@ void cleanup_modules(void)
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]);
+ if (bars) {
+ for (int i = 0; i < nbars; i++) {
+ XftDrawDestroy(bars[i].xft_draw);
+ XFreePixmap(dpy, bars[i].buffer);
+ XDestroyWindow(dpy, bars[i].win);
}
- free(wins);
+ free(bars);
}
if (monitors) {
XFree(monitors);
}
- if (xft_draws) {
- for (int i = 0; i < nmonitors; i++)
- XftDrawDestroy(xft_draws[i]);
- free(xft_draws);
- }
if (font)
XftFontClose(dpy, font);
if (gc)
@@ -200,55 +206,71 @@ void create_bars(void)
monitors[0].height = DisplayHeight(dpy, scr);
}
- wins = malloc(nmonitors * sizeof *wins);
- buffers = malloc(nmonitors * sizeof *buffers);
+ /* one bar per monitor, or two (primary + secondary, opposite edges) if
+ * secondary_bar is enabled */
+ int variants = config.secondary_bar ? 2 : 1;
+ nbars = nmonitors * variants;
+ bars = malloc(nbars * sizeof *bars);
+ int bidx = 0;
for (int i = 0; i < nmonitors; i++) {
- int bw = config.border ? config.border_width : 0;
- int w = monitors[i].width - 2 * config.horizontal_padding;
- int h = config.height;
- 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,
- .event_mask = ExposureMask | ButtonPressMask};
-
- wins[i] = XCreateWindow(dpy, root, x, y, w, h, bw, CopyFromParent, InputOutput,
- DefaultVisual(dpy, scr),
- CWBackPixel | CWBorderPixel | CWEventMask, &wa);
-
- XStoreName(dpy, wins[i], "sxbar");
- 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);
- XChangeProperty(dpy, wins[i], A_WM_TYPE, XA_ATOM, 32, PropModeReplace,
- (unsigned char *)&A_WM_TYPE_DOCK, 1);
-
- 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;
- }
- else {
- strut[2] = y + h + bw;
- strut[8] = x;
- strut[9] = x + w + 2 * bw - 1;
- }
- XChangeProperty(dpy, wins[i], A_STRUT, XA_CARDINAL, 32, PropModeReplace,
- (unsigned char *)strut, 12);
+ for (int v = 0; v < variants; v++) {
+ int is_secondary = v;
+ /* secondary bar sits on the edge opposite the primary bar */
+ int bottom_bar = is_secondary ? !config.bottom_bar : config.bottom_bar;
+
+ int bw = config.border ? config.border_width : 0;
+ int w = monitors[i].width - 2 * config.horizontal_padding;
+ int h = config.height;
+ int x = monitors[i].x_org + config.horizontal_padding;
+ int y = 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,
+ .event_mask = ExposureMask | ButtonPressMask};
+
+ Window win = XCreateWindow(dpy, root, x, y, w, h, bw, CopyFromParent, InputOutput,
+ DefaultVisual(dpy, scr),
+ CWBackPixel | CWBorderPixel | CWEventMask, &wa);
+
+ XStoreName(dpy, win, is_secondary ? "sxbar-secondary" : "sxbar");
+ XClassHint ch = {"sxbar", is_secondary ? "sxbar-secondary" : "sxbar"};
+ XSetClassHint(dpy, win, &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);
+ XChangeProperty(dpy, win, A_WM_TYPE, XA_ATOM, 32, PropModeReplace,
+ (unsigned char *)&A_WM_TYPE_DOCK, 1);
+
+ Atom A_STRUT = XInternAtom(dpy, "_NET_WM_STRUT_PARTIAL", False);
+ long strut[12] = {0};
+ if (bottom_bar) {
+ strut[3] = h + bw;
+ strut[10] = x;
+ strut[11] = x + w + 2 * bw - 1;
+ }
+ else {
+ strut[2] = y + h + bw;
+ strut[8] = x;
+ strut[9] = x + w + 2 * bw - 1;
+ }
+ XChangeProperty(dpy, win, A_STRUT, XA_CARDINAL, 32, PropModeReplace,
+ (unsigned char *)strut, 12);
+
+ Pixmap buf = XCreatePixmap(dpy, win, w, h, DefaultDepth(dpy, scr));
+ XMapRaised(dpy, win);
- buffers[i] = XCreatePixmap(dpy, wins[i], w, h, DefaultDepth(dpy, scr));
- XMapRaised(dpy, wins[i]);
+ bars[bidx].monitor = i;
+ bars[bidx].is_secondary = is_secondary;
+ bars[bidx].win = win;
+ bars[bidx].buffer = buf;
+ bidx++;
+ }
}
- gc = XCreateGC(dpy, wins[0], 0, NULL);
+ gc = XCreateGC(dpy, bars[0].win, 0, NULL);
font = XftFontOpenName(dpy, scr, config.font);
if (!font)
errx(1, "could not load font %s", config.font);
@@ -258,14 +280,17 @@ void create_bars(void)
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);
+ for (int i = 0; i < nbars; i++)
+ bars[i].xft_draw = XftDrawCreate(dpy, bars[i].buffer, vis, cmap);
}
-static void draw_bar_into(Drawable draw, int monitor_index)
+static void draw_bar_into(int bar_idx)
{
- XftDraw *d = xft_draws[monitor_index];
+ Bar *bar = &bars[bar_idx];
+ int monitor_index = bar->monitor;
+ int is_secondary = bar->is_secondary;
+ Drawable draw = bar->buffer;
+ XftDraw *d = bar->xft_draw;
int w = monitors[monitor_index].width - 2 * config.horizontal_padding;
int h = config.height;
@@ -273,14 +298,16 @@ static void draw_bar_into(Drawable draw, int monitor_index)
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);
-
int text_y = (h + font->ascent - font->descent) / 2;
const int pad = 5, ws_sp = 10, mod_sp = 20;
int cur_x = config.text_padding + pad;
+ /* the secondary bar only ever shows modules tagged `bar : ... : secondary` --
+ * no workspace switcher, no version text */
+ int current_ws = is_secondary ? -1 : get_current_workspace();
+ int name_count = 0;
+ char **names = is_secondary ? NULL : get_workspace_name(&name_count);
+
/* workspaces */
if (names) {
int *pos = malloc(name_count * sizeof *pos);
@@ -355,30 +382,63 @@ static void draw_bar_into(Drawable draw, int monitor_index)
free(wd);
}
- /* modules */
+ /* modules: split into left/center/right groups, each laid out and
+ * anchored independently of the other two */
char mbuf[256];
- int total_mw = 0;
+ int total_left = 0, total_center = 0, total_right = 0;
+ for (int i = 0; i < config.module_count; i++) {
+ Module *m = &config.modules[i];
+ if (!m->enabled || !m->cached_output || m->on_secondary != is_secondary)
+ continue;
+ int tw = text_width(module_text(m, mbuf, sizeof mbuf));
+ int slot = module_slot_width(m, tw) + mod_sp;
+ if (m->align == ALIGN_LEFT) total_left += slot;
+ else if (m->align == ALIGN_CENTER) total_center += slot;
+ else total_right += slot;
+ }
+ int ver_w = (!is_secondary && config.show_version) ? text_width(config.version_text) : 0;
+
+ /* left group: continues on from wherever the workspace switcher ended */
+ int lx = cur_x;
+ for (int i = 0; i < config.module_count; i++) {
+ Module *m = &config.modules[i];
+ if (!m->enabled || !m->cached_output || m->on_secondary != is_secondary || m->align != ALIGN_LEFT)
+ continue;
+ const char *out = module_text(m, mbuf, sizeof mbuf);
+ int tw = text_width(out);
+ XftColor *col = m->has_colour ? &m->xft_colour : &xft_fg;
+ XftDrawStringUtf8(d, col, font, lx, text_y, (const FcChar8 *)out, strlen(out));
+ lx += module_slot_width(m, tw) + mod_sp;
+ }
+
+ /* center group: centered across the full bar width */
+ int cx = (w - total_center) / 2;
for (int i = 0; i < config.module_count; i++) {
- if (!config.modules[i].enabled || !config.modules[i].cached_output)
+ Module *m = &config.modules[i];
+ if (!m->enabled || !m->cached_output || m->on_secondary != is_secondary || m->align != ALIGN_CENTER)
continue;
- total_mw += text_width(module_text(&config.modules[i], mbuf, sizeof mbuf)) + mod_sp;
+ const char *out = module_text(m, mbuf, sizeof mbuf);
+ int tw = text_width(out);
+ XftColor *col = m->has_colour ? &m->xft_colour : &xft_fg;
+ XftDrawStringUtf8(d, col, font, cx, text_y, (const FcChar8 *)out, strlen(out));
+ cx += module_slot_width(m, tw) + 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;
+ /* right group: anchored to the right edge, before version text */
+ int rx = w - total_right - 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)
+ Module *m = &config.modules[i];
+ if (!m->enabled || !m->cached_output || m->on_secondary != is_secondary || m->align != ALIGN_RIGHT)
continue;
- const char *out = module_text(&config.modules[i], mbuf, sizeof mbuf);
+ const char *out = module_text(m, mbuf, sizeof mbuf);
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;
+ XftColor *col = m->has_colour ? &m->xft_colour : &xft_fg;
+ XftDrawStringUtf8(d, col, font, rx, text_y, (const FcChar8 *)out, strlen(out));
+ rx += module_slot_width(m, tw) + mod_sp;
}
- /* version */
- if (config.show_version) {
+ /* version (primary bar only) */
+ if (!is_secondary && config.show_version) {
int vx = w - ver_w - config.text_padding - pad;
XftDrawStringUtf8(d, &xft_fg, font, vx, text_y,
(const FcChar8 *)config.version_text,
@@ -386,12 +446,13 @@ static void draw_bar_into(Drawable draw, int monitor_index)
}
}
-static void redraw_monitor(int i)
+static void redraw_bar(int bar_idx)
{
- int w = monitors[i].width - 2 * config.horizontal_padding;
+ Bar *bar = &bars[bar_idx];
+ int w = monitors[bar->monitor].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);
+ draw_bar_into(bar_idx);
+ XCopyArea(dpy, bar->buffer, bar->win, gc, 0, 0, w, h, 0, 0);
}
int get_current_workspace(void)
@@ -438,6 +499,30 @@ char **get_workspace_name(int *count)
return NULL;
}
+/* x position where the workspace switcher ends (and left-aligned modules
+ * begin) for a bar -- mirrors the accumulation in draw_bar_into()'s
+ * workspace loop without doing any of that loop's drawing work */
+static int workspace_end_x(int is_secondary, int pad, int ws_sp)
+{
+ int cur_x = config.text_padding + pad;
+ if (is_secondary)
+ return cur_x;
+
+ int name_count = 0;
+ char **names = get_workspace_name(&name_count);
+ if (!names)
+ return cur_x;
+
+ for (int i = 0; i < name_count; i++) {
+ char tmp[64];
+ snprintf(tmp, sizeof tmp, " %s ", names[i]);
+ cur_x += text_width(tmp) + ws_sp;
+ free(names[i]);
+ }
+ free(names);
+ return cur_x;
+}
+
static void spawn(const char *cmd)
{
pid_t pid = fork();
@@ -459,36 +544,53 @@ void hdl_button(XEvent *xev)
if (btn != Button1 && btn != Button4 && btn != Button5)
return;
- int idx = find_window_monitor(xev->xbutton.window);
+ int bar_idx = find_bar(xev->xbutton.window);
+ Bar *bar = &bars[bar_idx];
+ int is_secondary = bar->is_secondary;
int x_click = xev->xbutton.x;
- int w = monitors[idx].width - 2 * config.horizontal_padding;
- const int pad = 5, mod_sp = 20;
+ int w = monitors[bar->monitor].width - 2 * config.horizontal_padding;
+ const int pad = 5, ws_sp = 10, mod_sp = 20;
char mbuf[256];
- int total_mw = 0;
+ int total_left = 0, total_center = 0, total_right = 0;
for (int i = 0; i < config.module_count; i++) {
- if (!config.modules[i].enabled || !config.modules[i].cached_output)
+ Module *m = &config.modules[i];
+ if (!m->enabled || !m->cached_output || m->on_secondary != is_secondary)
continue;
- total_mw += text_width(module_text(&config.modules[i], mbuf, sizeof mbuf)) + mod_sp;
+ int tw = text_width(module_text(m, mbuf, sizeof mbuf));
+ int slot = module_slot_width(m, tw) + mod_sp;
+ if (m->align == ALIGN_LEFT) total_left += slot;
+ else if (m->align == ALIGN_CENTER) total_center += slot;
+ else total_right += slot;
}
- 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;
- 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)
- spawn(config.modules[i].click_command);
- else if (btn == Button4 && config.modules[i].scroll_up_command)
- spawn(config.modules[i].scroll_up_command);
- else if (btn == Button5 && config.modules[i].scroll_down_command)
- spawn(config.modules[i].scroll_down_command);
- return;
+ int ver_w = (!is_secondary && config.show_version) ? text_width(config.version_text) : 0;
+
+ /* starting x for each group, matching draw_bar_into() exactly */
+ int group_x[3];
+ group_x[ALIGN_LEFT] = workspace_end_x(is_secondary, pad, ws_sp);
+ group_x[ALIGN_CENTER] = (w - total_center) / 2;
+ group_x[ALIGN_RIGHT] = w - total_right - ver_w - 2 * config.text_padding - 2 * pad;
+
+ for (int align = 0; align < 3; align++) {
+ int mx = group_x[align];
+ for (int i = 0; i < config.module_count; i++) {
+ Module *m = &config.modules[i];
+ if (!m->enabled || !m->cached_output || m->on_secondary != is_secondary || m->align != align)
+ continue;
+ const char *out = module_text(m, mbuf, sizeof mbuf);
+ int tw = text_width(out);
+ int slot = module_slot_width(m, tw);
+ if (x_click >= mx && x_click < mx + slot + mod_sp) {
+ if (btn == Button1 && m->click_command)
+ spawn(m->click_command);
+ else if (btn == Button4 && m->scroll_up_command)
+ spawn(m->scroll_up_command);
+ else if (btn == Button5 && m->scroll_down_command)
+ spawn(m->scroll_down_command);
+ return;
+ }
+ mx += slot + mod_sp;
}
- mx += tw + mod_sp;
}
}
@@ -499,15 +601,15 @@ void hdl_dummy(XEvent *xev)
void hdl_expose(XEvent *xev)
{
- int idx = find_window_monitor(xev->xexpose.window);
- redraw_monitor(idx);
+ int idx = find_bar(xev->xexpose.window);
+ redraw_bar(idx);
}
void hdl_property(XEvent *xev)
{
if (xev->xproperty.atom == XInternAtom(dpy, "_NET_CURRENT_DESKTOP", False)) {
- for (int i = 0; i < nmonitors; i++) {
- redraw_monitor(i);
+ for (int i = 0; i < nbars; i++) {
+ redraw_bar(i);
}
}
}
@@ -527,13 +629,14 @@ void init_defaults(void)
config.font = strdup("monospace:size=10");
config.show_version = True;
config.version_text = strdup(SXBAR_VERSION);
+ config.secondary_bar = False;
init_modules();
}
-int find_window_monitor(Window win)
+int find_bar(Window win)
{
- for (int i = 0; i < nmonitors; i++) {
- if (wins[i] == win) {
+ for (int i = 0; i < nbars; i++) {
+ if (bars[i].win == win) {
return i;
}
}
@@ -619,8 +722,8 @@ void run(void)
time_t now = time(NULL);
if (now - last >= 1) {
update_modules();
- for (int i = 0; i < nmonitors; i++) {
- redraw_monitor(i);
+ for (int i = 0; i < nbars; i++) {
+ redraw_bar(i);
}
last = now;
}
@@ -661,6 +764,25 @@ char *run_command(const char *cmd)
return res ? res : strdup("");
}
+/* wrap s in single quotes for safe embedding in a shell command line */
+static char *shell_quote(const char *s)
+{
+ size_t len = strlen(s);
+ char *out = malloc(len * 4 + 3);
+ char *p = out;
+ *p++ = '\'';
+ for (size_t i = 0; i < len; i++) {
+ if (s[i] == '\'') {
+ *p++ = '\''; *p++ = '\\'; *p++ = '\''; *p++ = '\'';
+ } else {
+ *p++ = s[i];
+ }
+ }
+ *p++ = '\'';
+ *p = '\0';
+ return out;
+}
+
void update_modules(void)
{
time_t now = time(NULL);
@@ -672,6 +794,18 @@ void update_modules(void)
if (now - m->last_update >= m->refresh_interval) {
free(m->cached_output);
m->cached_output = run_command(m->command);
+
+ if (m->prefix_command) {
+ char *quoted = shell_quote(m->cached_output);
+ size_t cmdlen = strlen(m->prefix_command) + strlen(quoted) + 2;
+ char *full = malloc(cmdlen);
+ snprintf(full, cmdlen, "%s %s", m->prefix_command, quoted);
+ free(quoted);
+ free(m->prefix_cached);
+ m->prefix_cached = run_command(full);
+ free(full);
+ }
+
m->last_update = now;
}
}