#define _POSIX_C_SOURCE 200809L
#include <ctype.h>
#include <err.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <X11/Xatom.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/extensions/Xinerama.h>
#include "defs.h"
#include "parser.h"
/* vendored, public domain (see src/stb_image.h) -- decodes popup_image
* rows (e.g. album art) without pulling in a full image-loading library
* (Imlib2 et al.) as a runtime dependency; the decoder compiles straight
* into this binary instead */
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
void cleanup_modules(void);
void cleanup_resources(void);
void create_bars(void);
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);
void hdl_button_release(XEvent *xev);
void hdl_crossing(XEvent *xev);
void hdl_dummy(XEvent *xev);
void hdl_expose(XEvent *xev);
void hdl_motion(XEvent *xev);
void hdl_property(XEvent *xev);
void hdl_visibility(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);
static void popup_close(void);
EventHandler evtable[LASTEvent];
XftFont *font;
XftColor xft_fg;
XftColor xft_bg;
Display *dpy;
Window root;
XineramaScreenInfo *monitors = NULL;
GC gc;
Config config;
Bar *bars = NULL;
int nbars = 0;
int nmonitors = 0;
int scr;
static Popup popup;
/* Windows pulled from _NET_CLIENT_LIST (taskbar, workspace dots, ...) are
* routinely destroyed between that snapshot and a follow-up property/attr
* query on them -- a normal race, not a bug. Xlib's default error handler
* would exit() the whole bar over it; swallow just that race instead and
* let callers' existing "did this call fail" checks handle it. */
static int xerror(Display *d, XErrorEvent *ee)
{
(void)d;
if (ee->error_code == BadWindow || ee->error_code == BadDrawable ||
ee->error_code == BadMatch)
return 0;
warnx("X error: request %d.%d, error code %d",
ee->request_code, ee->minor_code, ee->error_code);
return 0;
}
int window_on_monitor(Window win, int monitor_index) {
XWindowAttributes attr;
if (!XGetWindowAttributes(dpy, win, &attr))
return 0;
int mx = monitors[monitor_index].x_org;
int my = monitors[monitor_index].y_org;
int mw = monitors[monitor_index].width;
int mh = monitors[monitor_index].height;
// Kontrollera om fönstrets position är inom monitorns rektangel
return (attr.x >= mx && attr.x < mx + mw && attr.y >= my && attr.y < my + mh);
}
int workspace_has_window(int ws) {
Atom at = XInternAtom(dpy, "_NET_CLIENT_LIST", False);
Atom ret_type;
int fmt;
unsigned long n, after;
unsigned char *data = NULL;
if (XGetWindowProperty(dpy, root, at, 0, (~0L), False, XA_WINDOW, &ret_type, &fmt, &n, &after, &data) == Success && data) {
Atom ws_atom = XInternAtom(dpy, "_NET_WM_DESKTOP", False);
for (unsigned long i = 0; i < n; i++) {
Window win = ((Window *)data)[i];
unsigned char *ws_data = NULL;
if (XGetWindowProperty(dpy, win, ws_atom, 0, 1, False, XA_CARDINAL, &ret_type, &fmt, &n, &after, &ws_data) == Success && ws_data) {
int win_ws = *(unsigned long *)ws_data;
XFree(ws_data);
if (win_ws == ws)
return 1;
}
}
XFree(data);
}
return 0;
}
static int text_width(const char *str)
{
XGlyphInfo ext;
XftTextExtentsUtf8(dpy, font, (const FcChar8 *)str, strlen(str), &ext);
return ext.xOff;
}
/* a workspace's displayed label: its configured workspace_icon override
* (e.g. a Nerd Font glyph) if one matches its _NET_DESKTOP_NAMES string,
* else that string unchanged */
static const char *workspace_display_name(const char *name)
{
for (int i = 0; i < config.workspace_icon_count; i++) {
if (!strcmp(config.workspace_icons[i].name, name))
return config.workspace_icons[i].icon;
}
return name;
}
/* x to actually draw a " %s "-wrapped workspace label at (instead of
* plain origin_x) so `inner`'s visual ink sits centered within the
* padded string's full advance width `box_w`, given `lead_w` (the
* advance of the one literal leading space). Several Nerd Font icon
* glyphs -- especially outside the "Mono" variant of a given font --
* report a narrow advance (e.g. one monospace cell) while their ink is
* considerably wider and shifted, which throws off centering inside a
* fixed box like the workspace switcher's highlight pill.
*
* Measuring the *padded* string's own extents directly doesn't work: at
* least for JetBrainsMono Nerd Font, XftTextExtentsUtf8() reports the
* padded string's bounding box as if it always spans its full advance
* width (x=0, width=xOff) regardless of where the inner glyph's ink
* actually sits, silently producing a zero shift for exactly the glyphs
* that most need one. Measuring `inner` alone instead (correctly
* asymmetric) and offsetting by the known leading space width sidesteps
* that. This leaves box_w itself (and therefore layout/box sizing
* elsewhere) untouched -- only where within it we draw shifts. */
static int ink_centered_x(int origin_x, int box_w, int lead_w, const char *inner)
{
XGlyphInfo in;
XftTextExtentsUtf8(dpy, font, (const FcChar8 *)inner, strlen(inner), &in);
int ink_center = lead_w + in.x + in.width / 2;
int box_center = box_w / 2;
return origin_x + (box_center - ink_center);
}
/* 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)
{
const char *pre = m->prefix_command ? m->prefix_cached : m->prefix;
if (m->icon_only)
return (pre && *pre) ? pre : "";
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). Capped at max_width instead, if set and the text overflows it --
* that text scrolls (marquee) within the fixed slot rather than growing it. */
static int module_slot_width(Module *m, int text_w)
{
if (m->max_width > 0 && text_w > m->max_width)
return m->max_width;
return text_w > m->min_width ? text_w : m->min_width;
}
#define MARQUEE_GAP " " /* separates the wrap point in a scrolling ticker */
/* draws `text` at (x, text_y); if it's narrower than max_w (or max_w <= 0,
* meaning "no cap") this is just XftDrawStringUtf8, same as always. If it's
* wider, the text is clipped to max_w and drawn twice back to back
* (text+gap, text+gap) offset by *offset, so it reads as one continuously-
* wrapping ticker rather than jumping at the seam. Shared by the bar's own
* module text (driven by a Module's max_width/scroll_offset) and a popup's
* text/button rows (driven by a PopupItem's own scroll_offset, capped to
* the popup's actual width) -- callers own advancing *offset over time. */
static void draw_ticker(XftDraw *d, XftColor *col, int x, int text_y, int max_w, const char *text, int tw, int offset)
{
if (max_w <= 0 || tw <= max_w) {
XftDrawStringUtf8(d, col, font, x, text_y, (const FcChar8 *)text, strlen(text));
return;
}
char rep[512];
snprintf(rep, sizeof rep, "%s%s", text, MARQUEE_GAP);
int rep_w = text_width(rep);
int off = rep_w > 0 ? offset % rep_w : 0;
XRectangle clip = {.x = (short)x, .y = (short)(text_y - font->ascent),
.width = (unsigned short)max_w,
.height = (unsigned short)(font->ascent + font->descent)};
XftDrawSetClipRectangles(d, 0, 0, &clip, 1);
XftDrawStringUtf8(d, col, font, x - off, text_y, (const FcChar8 *)rep, strlen(rep));
XftDrawStringUtf8(d, col, font, x - off + rep_w, text_y, (const FcChar8 *)rep, strlen(rep));
XftDrawSetClip(d, NULL);
}
static void draw_module_text(XftDraw *d, XftColor *col, int x, int text_y, Module *m, const char *out, int tw)
{
draw_ticker(d, col, x, text_y, m->max_width, out, tw, m->scroll_offset);
}
#define MARQUEE_STEP_PX 2 /* pixels advanced per redraw tick while scrolling */
/* advance scroll_offset for every enabled module whose text currently
* overflows its max_width, and report (via return value) whether any of
* them did -- run() uses that to decide how often to redraw: fast while
* something's actually animating, the normal cadence otherwise */
static int advance_marquees(void)
{
int any = 0;
char mbuf[256];
for (int i = 0; i < config.module_count; i++) {
Module *m = &config.modules[i];
if (!m->enabled || !m->cached_output || m->max_width <= 0)
continue;
int tw = text_width(module_text(m, mbuf, sizeof mbuf));
if (tw <= m->max_width)
continue;
any = 1;
m->scroll_offset += MARQUEE_STEP_PX;
}
return any;
}
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) {
if (XftColorAllocName(dpy, vis, cmap,
config.modules[i].colour,
&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);
}
}
}
}
/* release an IMAGE row's loaded XImage (if any), so popup_open() can
* safely reload it on every popup open without leaking the previous one */
static void free_popup_image(PopupItem *it)
{
if (!it->image)
return;
XDestroyImage((XImage *)it->image);
it->image = NULL;
it->image_w = it->image_h = 0;
}
/* box-filter downscale of an RGBA8 buffer (4 bytes/pixel, no row padding)
* from sw x sh to dw x dh -- averages every source pixel that falls under
* each destination pixel, rather than nearest-neighbour/point sampling,
* so shrinking a photo-sized album art down to popup size doesn't alias.
* Caller frees the returned buffer with free(). NULL on OOM. */
static unsigned char *scale_image_rgba(const unsigned char *src, int sw, int sh, int dw, int dh)
{
unsigned char *dst = malloc((size_t)dw * dh * 4);
if (!dst)
return NULL;
for (int y = 0; y < dh; y++) {
int sy0 = y * sh / dh;
int sy1 = (y + 1) * sh / dh;
if (sy1 <= sy0) sy1 = sy0 + 1;
if (sy1 > sh) sy1 = sh;
for (int x = 0; x < dw; x++) {
int sx0 = x * sw / dw;
int sx1 = (x + 1) * sw / dw;
if (sx1 <= sx0) sx1 = sx0 + 1;
if (sx1 > sw) sx1 = sw;
long r = 0, g = 0, b = 0, a = 0, n = 0;
for (int yy = sy0; yy < sy1; yy++) {
const unsigned char *row = src + ((size_t)yy * sw + sx0) * 4;
for (int xx = sx0; xx < sx1; xx++, row += 4) {
r += row[0]; g += row[1]; b += row[2]; a += row[3];
n++;
}
}
unsigned char *out = dst + ((size_t)y * dw + x) * 4;
out[0] = (unsigned char)(r / n);
out[1] = (unsigned char)(g / n);
out[2] = (unsigned char)(b / n);
out[3] = (unsigned char)(a / n);
}
}
return dst;
}
/* number of trailing zero bits in a visual channel mask (e.g. 0x00ff00 -> 8) */
static int mask_shift(unsigned long mask)
{
int shift = 0;
while (mask && !(mask & 1)) { mask >>= 1; shift++; }
return shift;
}
/* number of set bits in a visual channel mask (e.g. 0x00ff00 -> 8) */
static int mask_bits(unsigned long mask)
{
int bits = 0;
while (mask) { bits += (int)(mask & 1); mask >>= 1; }
if (bits > 8) bits = 8; /* no real visual exceeds 8 bits/channel */
return bits;
}
/* pack an RGBA8 buffer into a freshly allocated XImage matching the
* default visual's actual channel masks/depth (not just assumed 24-bit
* TrueColor), so this renders correctly on any visual. Returns NULL on
* failure; caller owns the result (free with XDestroyImage()). */
static XImage *rgba_to_ximage(const unsigned char *rgba, int w, int h)
{
Visual *vis = DefaultVisual(dpy, scr);
int depth = DefaultDepth(dpy, scr);
int red_shift = mask_shift(vis->red_mask), red_bits = mask_bits(vis->red_mask);
int green_shift = mask_shift(vis->green_mask), green_bits = mask_bits(vis->green_mask);
int blue_shift = mask_shift(vis->blue_mask), blue_bits = mask_bits(vis->blue_mask);
XImage *img = XCreateImage(dpy, vis, depth, ZPixmap, 0, NULL, w, h, 32, 0);
if (!img)
return NULL;
img->data = malloc((size_t)img->bytes_per_line * h);
if (!img->data) {
XFree(img);
return NULL;
}
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
const unsigned char *px = rgba + ((size_t)y * w + x) * 4;
unsigned long rv = px[0] >> (8 - red_bits);
unsigned long gv = px[1] >> (8 - green_bits);
unsigned long bv = px[2] >> (8 - blue_bits);
unsigned long pixel = (rv << red_shift) | (gv << green_shift) | (bv << blue_shift);
XPutPixel(img, x, y, pixel);
}
}
return img;
}
/* decode `path` (any format stb_image supports -- JPEG/PNG/GIF/BMP/...),
* scale it (preserving aspect ratio, up or down) to fit within a
* `box`-pixel square, and return it as an XImage ready for XPutImage().
* NULL on any failure (missing/corrupt/unreadable file). */
static void *load_scaled_image(const char *path, int box, int *out_w, int *out_h)
{
int iw, ih, comp;
unsigned char *pixels = stbi_load(path, &iw, &ih, &comp, 4);
if (!pixels)
return NULL;
double scale = iw > ih ? (double)box / iw : (double)box / ih;
int dw = (int)(iw * scale); if (dw < 1) dw = 1;
int dh = (int)(ih * scale); if (dh < 1) dh = 1;
unsigned char *final = pixels;
int scaled_ourselves = 0;
if (dw != iw || dh != ih) {
final = scale_image_rgba(pixels, iw, ih, dw, dh);
stbi_image_free(pixels);
if (!final)
return NULL;
scaled_ourselves = 1;
}
XImage *img = rgba_to_ximage(final, dw, dh);
if (scaled_ourselves)
free(final);
else
stbi_image_free(final);
if (!img)
return NULL;
*out_w = dw;
*out_h = dh;
return img;
}
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);
free(config.modules[i].prefix);
free(config.modules[i].prefix_command);
free(config.modules[i].prefix_cached);
for (int j = 0; j < config.modules[i].popup_item_count; j++) {
free(config.modules[i].popup_items[j].label);
free(config.modules[i].popup_items[j].command);
free(config.modules[i].popup_items[j].label_command);
free(config.modules[i].popup_items[j].set_command);
free(config.modules[i].popup_items[j].image_command);
free_popup_image(&config.modules[i].popup_items[j]);
for (int b = 0; b < config.modules[i].popup_items[j].button_count; b++) {
free(config.modules[i].popup_items[j].buttons[b].label);
free(config.modules[i].popup_items[j].buttons[b].command);
}
free(config.modules[i].popup_items[j].buttons);
}
free(config.modules[i].popup_items);
for (int j = 0; j < config.modules[i].taskbar_entry_count; j++) {
free(config.modules[i].taskbar_entries[j].label);
free(config.modules[i].taskbar_entries[j].command);
}
free(config.modules[i].taskbar_entries);
if (config.modules[i].has_colour)
XftColorFree(dpy, vis, cmap, &config.modules[i].xft_colour);
free(config.modules[i].cached_output);
}
free(config.modules);
}
void cleanup_resources(void)
{
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(bars);
}
if (monitors) {
XFree(monitors);
}
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);
}
}
void create_bars(void)
{
int xin = 0;
if (XineramaIsActive(dpy)) {
monitors = XineramaQueryScreens(dpy, &nmonitors);
xin = 1;
}
if (!xin || nmonitors <= 0) {
nmonitors = 1;
monitors = malloc(sizeof *monitors);
monitors[0].screen_number = 0;
monitors[0].x_org = 0;
monitors[0].y_org = 0;
monitors[0].width = DisplayWidth(dpy, scr);
monitors[0].height = DisplayHeight(dpy, scr);
}
/* 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++) {
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 |
PointerMotionMask | LeaveWindowMask};
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] = DisplayHeight(dpy, scr) - y + 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);
bars[bidx].monitor = i;
bars[bidx].is_secondary = is_secondary;
bars[bidx].win = win;
bars[bidx].buffer = buf;
bidx++;
}
}
/* sxwm only re-tiles/recalculates reserved screen space in reaction
* to a _NET_WM_STRUT_PARTIAL PropertyNotify on the root window; it
* doesn't do that on its own when a dock window first maps. Without
* this, windows only reflow around a (re)started bar the next time
* something unrelated happens to trigger a retile. Touch that same
* atom on root ourselves so it happens immediately -- the value is
* irrelevant, sxwm rescans all dock windows itself when it sees this. */
Atom A_ROOT_RETILE_KICK = XInternAtom(dpy, "_NET_WM_STRUT_PARTIAL", False);
long retile_kick = 0;
XChangeProperty(dpy, root, A_ROOT_RETILE_KICK, XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)&retile_kick, 1);
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);
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);
for (int i = 0; i < nbars; i++)
bars[i].xft_draw = XftDrawCreate(dpy, bars[i].buffer, vis, cmap);
}
/* which of m's taskbar_entries live on the given monitor, so a taskbar on
* a multi-monitor setup only ever shows that monitor's own windows
* instead of duplicating the same full list on every bar. The actual
* window-to-monitor matching happens in the script (see update_taskbar()'s
* geometry args and taskbar.sh's `list` case) -- entries just carry
* whatever monitor index the script tagged them with, and -1 (a script
* that doesn't tag at all) always qualifies so unmodified custom taskbar
* scripts keep their old show-everywhere behaviour. Caller frees the
* returned array; *out_n receives its length (may be 0). */
static int *taskbar_indices_for_monitor(Module *m, int monitor_index, int *out_n)
{
int *idx = malloc(m->taskbar_entry_count * sizeof *idx);
int n = 0;
for (int e = 0; e < m->taskbar_entry_count; e++) {
int em = m->taskbar_entries[e].monitor;
if (em < 0 || em == monitor_index)
idx[n++] = e;
}
*out_n = n;
return idx;
}
static void draw_bar_into(int bar_idx)
{
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;
/* clear */
XSetForeground(dpy, gc, config.background_colour);
XFillRectangle(dpy, draw, gc, 0, 0, w, h);
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);
int *wd = malloc(name_count * sizeof *wd);
for (int i = 0; i < name_count; i++) {
char tmp[64];
snprintf(tmp, sizeof tmp, " %s ", workspace_display_name(names[i]));
wd[i] = text_width(tmp);
pos[i] = cur_x;
cur_x += wd[i] + ws_sp;
}
int lead_w = text_width(" ");
for (int i = 0; i < name_count; i++) {
char tmp[64];
const char *inner = workspace_display_name(names[i]);
snprintf(tmp, sizeof tmp, " %s ", inner);
int draw_x = ink_centered_x(pos[i], wd[i], lead_w, inner);
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);
XftDrawStringUtf8(d, &xft_bg, font, draw_x, text_y,
(const FcChar8 *)tmp, strlen(tmp));
} else {
XftDrawStringUtf8(d, &xft_fg, font, draw_x, text_y,
(const FcChar8 *)tmp, strlen(tmp));
}
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;
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);
free(pos);
free(wd);
}
/* modules: split into left/center/right groups, each laid out and
* anchored independently of the other two */
char mbuf[256];
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;
draw_module_text(d, col, lx, text_y, m, out, tw);
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++) {
Module *m = &config.modules[i];
if (!m->enabled || !m->cached_output || m->on_secondary != is_secondary || m->align != ALIGN_CENTER)
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;
draw_module_text(d, col, cx, text_y, m, out, tw);
cx += module_slot_width(m, tw) + mod_sp;
}
/* 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++) {
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(m, mbuf, sizeof mbuf);
int tw = text_width(out);
XftColor *col = m->has_colour ? &m->xft_colour : &xft_fg;
draw_module_text(d, col, rx, text_y, m, out, tw);
rx += module_slot_width(m, tw) + mod_sp;
}
/* taskbar: one clickable, equal-width segment per current-workspace
* window, filling whatever's left between the left and right groups
* above. Its own cached_output is deliberately left NULL by
* update_taskbar() (src/sxbar.c), so it's automatically excluded from
* every loop above (all of them skip modules with no cached_output)
* -- this is its only rendering path. Ignores its own `align` (a fill
* module has no single anchor side); at most one taskbar module can
* ever exist (it's a fixed built-in name), hence the early exit once
* found. */
for (int i = 0; i < config.module_count; i++) {
Module *m = &config.modules[i];
if (strcmp(m->name, "taskbar") || !m->enabled || m->on_secondary != is_secondary)
continue;
int tb_x = cur_x + total_left;
int tb_end = w - total_right - ver_w - 2 * config.text_padding - 2 * pad;
int tb_w = tb_end - tb_x;
int idx_n;
int *idx = taskbar_indices_for_monitor(m, monitor_index, &idx_n);
if (tb_w <= 0 || idx_n <= 0) {
free(idx);
break;
}
Window active = None;
{
Atom at = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False);
Atom ret_type;
int fmt;
unsigned long nitems, after;
unsigned char *adata = NULL;
if (XGetWindowProperty(dpy, root, at, 0, 1, False, XA_WINDOW,
&ret_type, &fmt, &nitems, &after, &adata) == Success && adata) {
active = *(Window *)adata;
XFree(adata);
}
}
int seg_w = tb_w / idx_n;
for (int e = 0; e < idx_n; e++) {
TaskbarEntry *ent = &m->taskbar_entries[idx[e]];
int seg_x = tb_x + e * seg_w;
int this_w = (e == idx_n - 1) ? (tb_x + tb_w - seg_x) : seg_w;
const char *label = ent->label ? ent->label : "";
int lw = text_width(label);
int avail = this_w - 2 * pad;
if (ent->id == active) {
XSetForeground(dpy, gc, config.foreground_colour);
XFillRectangle(dpy, draw, gc, seg_x, text_y - font->ascent - pad,
this_w, font->ascent + font->descent + 2 * pad);
draw_ticker(d, &xft_bg, seg_x + pad, text_y, avail, label, lw, 0);
} else {
draw_ticker(d, &xft_fg, seg_x + pad, text_y, avail, label, lw, 0);
}
}
free(idx);
break;
}
/* 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,
strlen(config.version_text));
}
}
static void redraw_bar(int bar_idx)
{
Bar *bar = &bars[bar_idx];
int w = monitors[bar->monitor].width - 2 * config.horizontal_padding;
int h = config.height;
draw_bar_into(bar_idx);
XCopyArea(dpy, bar->buffer, bar->win, gc, 0, 0, w, h, 0, 0);
}
int get_current_workspace(void)
{
Atom at = XInternAtom(dpy, "_NET_CURRENT_DESKTOP", False);
Atom ret_type;
int fmt;
unsigned long n, after;
unsigned char *data = NULL;
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);
return ws;
}
return -1;
}
char **get_workspace_name(int *count)
{
Atom at = XInternAtom(dpy, "_NET_DESKTOP_NAMES", False);
Atom utf8 = XInternAtom(dpy, "UTF8_STRING", False);
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 idx = 0;
char *p = (char *)data;
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 = idx;
return names;
}
*count = 0;
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 ", workspace_display_name(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();
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);
}
/* find the module (if any) whose slot contains x_click on the given bar,
* mirroring draw_bar_into()'s three-group layout exactly. Returns NULL if
* x_click doesn't land on any enabled module's reserved slot. out_mx (if
* non-NULL) receives the slot's left edge, in the bar window's own
* coordinates. */
static Module *module_at_x(int bar_idx, int x_click, int *out_mx)
{
Bar *bar = &bars[bar_idx];
int is_secondary = bar->is_secondary;
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_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;
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 (out_mx) *out_mx = mx;
return m;
}
mx += slot + mod_sp;
}
}
return NULL;
}
/* find which taskbar entry (if any) x_click lands on, mirroring the
* bounds computed in draw_bar_into()'s dedicated taskbar block without
* doing any of that block's drawing work -- same pattern as
* module_at_x()/workspace_end_x() above */
static TaskbarEntry *taskbar_entry_at_x(int bar_idx, int x_click)
{
Bar *bar = &bars[bar_idx];
int is_secondary = bar->is_secondary;
int w = monitors[bar->monitor].width - 2 * config.horizontal_padding;
const int pad = 5, ws_sp = 10, mod_sp = 20;
Module *tb = NULL;
for (int i = 0; i < config.module_count; i++) {
if (!strcmp(config.modules[i].name, "taskbar")) {
tb = &config.modules[i];
break;
}
}
if (!tb || !tb->enabled || tb->on_secondary != is_secondary || tb->taskbar_entry_count <= 0)
return NULL;
char mbuf[256];
int total_left = 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_right += slot;
}
int ver_w = (!is_secondary && config.show_version) ? text_width(config.version_text) : 0;
int tb_x = workspace_end_x(is_secondary, pad, ws_sp) + total_left;
int tb_end = w - total_right - ver_w - 2 * config.text_padding - 2 * pad;
if (x_click < tb_x || x_click >= tb_end)
return NULL;
int idx_n;
int *idx = taskbar_indices_for_monitor(tb, bar->monitor, &idx_n);
if (idx_n <= 0) {
free(idx);
return NULL;
}
int seg_w = (tb_end - tb_x) / idx_n;
int e = (x_click - tb_x) / (seg_w > 0 ? seg_w : 1);
if (e >= idx_n)
e = idx_n - 1;
TaskbarEntry *ent = &tb->taskbar_entries[idx[e]];
free(idx);
return ent;
}
/* root-space geometry of a bar window, replicating create_bars()'s formula.
* Kept in sync by hand -- there is no shared helper with create_bars(). */
static void bar_root_geometry(int bar_idx, int *ox, int *oy, int *ow, int *oh)
{
Bar *bar = &bars[bar_idx];
int i = bar->monitor;
int is_secondary = bar->is_secondary;
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;
*ox = x; *oy = y; *ow = w + 2 * bw; *oh = h + 2 * bw;
}
#define POPUP_PAD 8
#define POPUP_ROW_PAD 6
#define POPUP_MIN_W 140
#define POPUP_SLIDER_W 170
#define POPUP_TRACK_H 10
#define POPUP_GAP 4
#define POPUP_IMAGE_SIZE 160 /* IMAGE rows scale to fit within this square box */
static int popup_row_height(void)
{
return font->ascent + font->descent + 2 * POPUP_ROW_PAD;
}
static int popup_item_height(PopupItem *it, int img_size)
{
if (it->type == POPUP_ROW_SLIDER)
return popup_row_height() + POPUP_ROW_PAD + POPUP_TRACK_H + POPUP_ROW_PAD;
if (it->type == POPUP_ROW_IMAGE)
return (it->image_h > 0 ? it->image_h : img_size) + 2 * POPUP_ROW_PAD;
return popup_row_height();
}
/* total popup content height across all of a module's rows (text, button
* and slider rows can all be mixed, each with its own height) */
static int popup_total_height(Module *m)
{
int img_size = m->popup_image_size > 0 ? m->popup_image_size : POPUP_IMAGE_SIZE;
int h = 0;
for (int i = 0; i < m->popup_item_count; i++)
h += popup_item_height(&m->popup_items[i], img_size);
return h > 0 ? h : popup_row_height();
}
/* y offset (popup-window-relative) where row idx starts */
static int popup_row_y(Module *m, int idx)
{
int img_size = m->popup_image_size > 0 ? m->popup_image_size : POPUP_IMAGE_SIZE;
int y = 0;
for (int i = 0; i < idx; i++)
y += popup_item_height(&m->popup_items[i], img_size);
return y;
}
/* which row (if any) contains popup-window-relative y, or -1 */
static int popup_row_at_y(Module *m, int y)
{
int img_size = m->popup_image_size > 0 ? m->popup_image_size : POPUP_IMAGE_SIZE;
int cy = 0;
for (int i = 0; i < m->popup_item_count; i++) {
int rh = popup_item_height(&m->popup_items[i], img_size);
if (y >= cy && y < cy + rh)
return i;
cy += rh;
}
return -1;
}
/* leading run of digits in a module's cached output (e.g. "45%" -> 45),
* used to seed a slider row with the module's current reading */
static int extract_pct(const char *s)
{
if (!s)
return 0;
while (*s && !isdigit((unsigned char)*s))
s++;
int v = atoi(s);
if (v < 0) v = 0;
if (v > 100) v = 100;
return v;
}
static void popup_close(void)
{
if (!popup.open)
return;
XUngrabPointer(dpy, CurrentTime);
XftDrawDestroy(popup.xft_draw);
XFreePixmap(dpy, popup.buffer);
XDestroyWindow(dpy, popup.win);
memset(&popup, 0, sizeof popup);
}
static void popup_draw(void)
{
Module *m = popup.module;
XSetForeground(dpy, gc, config.background_colour);
XFillRectangle(dpy, popup.buffer, gc, 0, 0, popup.w, popup.h);
if (config.border) {
XSetForeground(dpy, gc, config.border_colour);
XDrawRectangle(dpy, popup.buffer, gc, 0, 0, popup.w - 1, popup.h - 1);
}
for (int i = 0; i < m->popup_item_count; i++) {
PopupItem *it = &m->popup_items[i];
int ry = popup_row_y(m, i);
int text_y = ry + (popup_row_height() + font->ascent - font->descent) / 2;
if (it->type == POPUP_ROW_SLIDER) {
char label[64];
snprintf(label, sizeof label, "%s: %d%%", m->name, it->value);
XftDrawStringUtf8(popup.xft_draw, &xft_fg, font, POPUP_PAD, text_y,
(const FcChar8 *)label, strlen(label));
int track_x = POPUP_PAD;
int track_y = ry + popup_row_height() + POPUP_ROW_PAD;
int track_w = popup.w - 2 * POPUP_PAD;
XSetForeground(dpy, gc, config.foreground_colour);
XDrawRectangle(dpy, popup.buffer, gc, track_x, track_y, track_w, POPUP_TRACK_H);
int fill_w = track_w * it->value / 100;
if (fill_w > 0)
XFillRectangle(dpy, popup.buffer, gc, track_x, track_y, fill_w, POPUP_TRACK_H);
continue;
}
if (it->type == POPUP_ROW_IMAGE) {
if (it->image) {
int img_x = (popup.w - it->image_w) / 2;
int img_y = ry + POPUP_ROW_PAD;
XPutImage(dpy, popup.buffer, gc, (XImage *)it->image, 0, 0,
img_x, img_y, it->image_w, it->image_h);
}
continue;
}
if (it->type == POPUP_ROW_BUTTONS) {
int n = it->button_count > 0 ? it->button_count : 1;
int seg_w = popup.w / n;
for (int b = 0; b < it->button_count; b++) {
int seg_x = b * seg_w;
int this_w = (b == n - 1) ? (popup.w - seg_x) : seg_w;
const char *blabel = it->buttons[b].label ? it->buttons[b].label : "";
int lw = text_width(blabel);
int lx = seg_x + (this_w - lw) / 2;
if (i == popup.hover_row && b == popup.hover_col) {
XSetForeground(dpy, gc, config.foreground_colour);
XFillRectangle(dpy, popup.buffer, gc, seg_x, ry, this_w, popup_row_height());
XftDrawStringUtf8(popup.xft_draw, &xft_bg, font, lx, text_y,
(const FcChar8 *)blabel, strlen(blabel));
} else {
XftDrawStringUtf8(popup.xft_draw, &xft_fg, font, lx, text_y,
(const FcChar8 *)blabel, strlen(blabel));
}
}
continue;
}
const char *label = it->label ? it->label : "";
int avail = popup.w - 2 * POPUP_PAD;
int lw = text_width(label);
if (it->type == POPUP_ROW_BUTTON && i == popup.hover_row) {
int row_h = popup_item_height(it, m->popup_image_size > 0 ? m->popup_image_size : POPUP_IMAGE_SIZE);
XSetForeground(dpy, gc, config.foreground_colour);
XFillRectangle(dpy, popup.buffer, gc, 0, ry, popup.w, row_h);
draw_ticker(popup.xft_draw, &xft_bg, POPUP_PAD, text_y, avail, label, lw, it->scroll_offset);
} else {
draw_ticker(popup.xft_draw, &xft_fg, POPUP_PAD, text_y, avail, label, lw, it->scroll_offset);
}
}
XCopyArea(dpy, popup.buffer, popup.win, gc, 0, 0, popup.w, popup.h, 0, 0);
}
/* advance scroll_offset for every row in the *currently open* popup whose
* label overflows the popup's actual width, redraw if any did, and report
* that back -- same idea as advance_marquees(), just scoped to whichever
* one popup is open right now instead of every bar module */
static int advance_popup_marquee(void)
{
if (!popup.open)
return 0;
Module *m = popup.module;
int avail = popup.w - 2 * POPUP_PAD;
int any = 0;
for (int i = 0; i < m->popup_item_count; i++) {
PopupItem *it = &m->popup_items[i];
if (it->type != POPUP_ROW_TEXT && it->type != POPUP_ROW_BUTTON)
continue;
if (text_width(it->label ? it->label : "") <= avail)
continue;
any = 1;
it->scroll_offset += MARQUEE_STEP_PX;
}
if (any)
popup_draw();
return any;
}
/* recompute a slider row's value from a pointer x (popup-window-relative)
* and, if it changed, spawn its set_command with the new value (as "NN%") */
static void popup_slider_set_from_x(int row, int x)
{
PopupItem *it = &popup.module->popup_items[row];
int track_w = popup.w - 2 * POPUP_PAD;
int v = (x - POPUP_PAD) * 100 / (track_w > 0 ? track_w : 1);
if (v < 0) v = 0;
if (v > 100) v = 100;
it->value = v;
if (v != it->last_spawned && it->set_command) {
char pct[8];
snprintf(pct, sizeof pct, "%d%%", v);
size_t len = strlen(it->set_command) + strlen(pct) + 2;
char *full = malloc(len);
snprintf(full, len, "%s %s", it->set_command, pct);
spawn(full);
free(full);
it->last_spawned = v;
}
popup_draw();
}
static void popup_open(int bar_idx, Module *m, int anchor_x)
{
if (popup.open) {
if (popup.module == m)
return;
popup_close();
}
int bx, by, bw, bh;
bar_root_geometry(bar_idx, &bx, &by, &bw, &bh);
int is_secondary = bars[bar_idx].is_secondary;
int bottom_bar = is_secondary ? !config.bottom_bar : config.bottom_bar;
/* an IMAGE/SLIDER/BUTTONS row "anchors" the popup's width -- when one
* is present, plain TEXT/BUTTON rows (e.g. a track title) no longer
* get to stretch the popup wider than that; instead their text is
* capped to whatever width the anchor established and scrolls
* (marquee) if it doesn't fit -- see popup_draw(). With no such row
* present, behaviour is unchanged: the widest row's text sets the
* popup's width, same as always. */
int has_anchor = 0;
for (int i = 0; i < m->popup_item_count; i++) {
int t = m->popup_items[i].type;
if (t == POPUP_ROW_IMAGE || t == POPUP_ROW_SLIDER || t == POPUP_ROW_BUTTONS) {
has_anchor = 1;
break;
}
}
int w = POPUP_MIN_W;
for (int i = 0; i < m->popup_item_count; i++) {
PopupItem *it = &m->popup_items[i];
if (it->type == POPUP_ROW_SLIDER) {
it->value = extract_pct(m->cached_output);
it->last_spawned = it->value;
if (POPUP_SLIDER_W > w) w = POPUP_SLIDER_W;
continue;
}
if (it->type == POPUP_ROW_IMAGE) {
free_popup_image(it);
char *path = it->image_command ? run_command(it->image_command) : NULL;
if (path && *path) {
int iw, ih;
int img_size = m->popup_image_size > 0 ? m->popup_image_size : POPUP_IMAGE_SIZE;
it->image = load_scaled_image(path, img_size, &iw, &ih);
if (it->image) {
it->image_w = iw;
it->image_h = ih;
}
}
free(path);
if (it->image_w + 2 * POPUP_PAD > w) w = it->image_w + 2 * POPUP_PAD;
continue;
}
if (it->type == POPUP_ROW_BUTTONS) {
int total = 0;
for (int b = 0; b < it->button_count; b++)
total += text_width(it->buttons[b].label ? it->buttons[b].label : "") + 2 * POPUP_PAD;
if (total > w) w = total;
continue;
}
if (it->label_command) {
free(it->label);
it->label = run_command(it->label_command);
}
it->scroll_offset = 0; /* restart any marquee fresh on each open */
if (!has_anchor) {
int tw = text_width(it->label ? it->label : "") + 2 * POPUP_PAD;
if (tw > w) w = tw;
}
}
int h = popup_total_height(m);
int mon = bars[bar_idx].monitor;
int x = bx + anchor_x;
int y = bottom_bar ? by - h - POPUP_GAP : by + bh + POPUP_GAP;
if (x + w > monitors[mon].x_org + monitors[mon].width)
x = monitors[mon].x_org + monitors[mon].width - w;
if (x < monitors[mon].x_org)
x = monitors[mon].x_org;
XSetWindowAttributes wa = {
.override_redirect = True,
.background_pixel = config.background_colour,
.event_mask = ExposureMask | ButtonPressMask | ButtonReleaseMask |
PointerMotionMask | LeaveWindowMask | VisibilityChangeMask,
};
Window win = XCreateWindow(dpy, root, x, y, w, h, 0, CopyFromParent, InputOutput,
DefaultVisual(dpy, scr),
CWOverrideRedirect | CWBackPixel | CWEventMask, &wa);
Atom A_WM_TYPE = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE", False);
Atom A_WM_TYPE_POPUP = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_POPUP_MENU", False);
XChangeProperty(dpy, win, A_WM_TYPE, XA_ATOM, 32, PropModeReplace,
(unsigned char *)&A_WM_TYPE_POPUP, 1);
Visual *vis = DefaultVisual(dpy, scr);
Colormap cmap = DefaultColormap(dpy, scr);
Pixmap buf = XCreatePixmap(dpy, win, w, h, DefaultDepth(dpy, scr));
popup.open = 1;
popup.win = win;
popup.buffer = buf;
popup.xft_draw = XftDrawCreate(dpy, buf, vis, cmap);
popup.module = m;
popup.bar_idx = bar_idx;
popup.trigger = m->popup_trigger;
popup.x = x; popup.y = y; popup.w = w; popup.h = h;
popup.hover_row = -1;
popup.hover_col = -1;
popup.dragging_row = -1;
XMapRaised(dpy, win);
/* owner_events=True: clicks that land on the bar or the popup itself
* still behave normally; a click anywhere else on screen is reported
* to us instead (as a click on `win`), letting hdl_button dismiss the
* popup like a regular dropdown menu closes on an outside click */
XGrabPointer(dpy, win, True,
ButtonPressMask | ButtonReleaseMask | PointerMotionMask,
GrabModeAsync, GrabModeAsync, None, None, CurrentTime);
popup_draw();
}
static void popup_handle_button(XEvent *xev)
{
int x = xev->xbutton.x, y = xev->xbutton.y;
if (x < 0 || y < 0 || x >= popup.w || y >= popup.h) {
popup_close();
return;
}
Module *m = popup.module;
int row = popup_row_at_y(m, y);
if (row < 0) {
popup_close();
return;
}
PopupItem *it = &m->popup_items[row];
switch (it->type) {
case POPUP_ROW_BUTTON:
/* run the command but leave the popup open -- it only closes once
* the pointer actually leaves the popup/bar area (hdl_crossing) or
* the user clicks outside it (hdl_button) */
if (it->command && *it->command)
spawn(it->command);
break;
case POPUP_ROW_SLIDER:
popup.dragging_row = row;
popup_slider_set_from_x(row, x);
break;
case POPUP_ROW_BUTTONS: {
int n = it->button_count > 0 ? it->button_count : 1;
int seg = x / (popup.w / n);
if (seg >= it->button_count)
seg = it->button_count - 1;
if (seg >= 0 && it->buttons[seg].command && *it->buttons[seg].command)
spawn(it->buttons[seg].command);
break;
}
default:
/* POPUP_ROW_TEXT: purely informational, not clickable at all --
* the popup stays open, nothing happens */
break;
}
}
void hdl_button(XEvent *xev)
{
Window win = xev->xbutton.window;
if (popup.open && win == popup.win) {
popup_handle_button(xev);
return;
}
if (popup.open) {
/* a click anywhere else -- another bar module, empty bar space,
* or (via the pointer grab above) some other window entirely --
* just dismisses the popup, same as any ordinary dropdown menu */
popup_close();
return;
}
unsigned int btn = xev->xbutton.button;
if (btn != Button1 && btn != Button4 && btn != Button5)
return;
int bar_idx = find_bar(win);
if (bars[bar_idx].win != win)
return;
int mx;
Module *m = module_at_x(bar_idx, xev->xbutton.x, &mx);
if (!m) {
if (btn == Button1) {
TaskbarEntry *ent = taskbar_entry_at_x(bar_idx, xev->xbutton.x);
if (ent && ent->command && *ent->command)
spawn(ent->command);
}
return;
}
if (btn == Button1 && m->popup_type != POPUP_NONE && m->popup_trigger == POPUP_TRIGGER_CLICK) {
popup_open(bar_idx, m, mx);
return;
}
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);
}
void hdl_button_release(XEvent *xev)
{
if (popup.open && xev->xbutton.window == popup.win)
popup.dragging_row = -1;
}
void hdl_motion(XEvent *xev)
{
Window win = xev->xmotion.window;
if (popup.open && win == popup.win) {
int x = xev->xmotion.x, y = xev->xmotion.y;
Module *m = popup.module;
if (popup.dragging_row >= 0) {
int cx = x;
if (cx < 0) cx = 0;
if (cx >= popup.w) cx = popup.w - 1;
popup_slider_set_from_x(popup.dragging_row, cx);
return;
}
int row = (x >= 0 && y >= 0 && x < popup.w && y < popup.h) ? popup_row_at_y(m, y) : -1;
int col = -1;
if (row >= 0) {
int type = m->popup_items[row].type;
if (type == POPUP_ROW_BUTTONS) {
PopupItem *it = &m->popup_items[row];
int n = it->button_count > 0 ? it->button_count : 1;
col = x / (popup.w / n);
if (col >= it->button_count)
col = it->button_count - 1;
} else if (type != POPUP_ROW_BUTTON) {
row = -1; /* only BUTTON/BUTTONS rows get a hover highlight */
}
}
if (row != popup.hover_row || col != popup.hover_col) {
popup.hover_row = row;
popup.hover_col = col;
popup_draw();
}
return;
}
int bar_idx = find_bar(win);
if (bars[bar_idx].win != win)
return;
int mx;
Module *m = module_at_x(bar_idx, xev->xmotion.x, &mx);
if (m && m->popup_type != POPUP_NONE && m->popup_trigger == POPUP_TRIGGER_HOVER) {
if (!popup.open || popup.module != m)
popup_open(bar_idx, m, mx);
} else if (popup.open && popup.trigger == POPUP_TRIGGER_HOVER && popup.bar_idx == bar_idx) {
popup_close();
}
}
void hdl_crossing(XEvent *xev)
{
if (xev->xcrossing.mode != NotifyNormal)
return; /* ignore grab-related pseudo crossings during a slider drag */
if (!popup.open || popup.trigger != POPUP_TRIGGER_HOVER)
return;
Window win = xev->xcrossing.window;
if (win != popup.win && win != bars[popup.bar_idx].win)
return;
/* leaving the bar downward (or the popup upward) crosses into the
* other one of this pair -- only close once the pointer has actually
* left both, not just whichever window it happened to leave first */
Window root_ret, child_ret;
int root_x, root_y, win_x, win_y;
unsigned int mask;
if (XQueryPointer(dpy, root, &root_ret, &child_ret, &root_x, &root_y, &win_x, &win_y, &mask)) {
if (root_x >= popup.x && root_x < popup.x + popup.w &&
root_y >= popup.y && root_y < popup.y + popup.h)
return;
int bx, by, bw, bh;
bar_root_geometry(popup.bar_idx, &bx, &by, &bw, &bh);
if (root_x >= bx && root_x < bx + bw && root_y >= by && root_y < by + bh)
return;
/* the thin POPUP_GAP strip between the bar and the popup -- an
* imprecise/fast mouse move can land there for a frame on its way
* from one to the other. Tolerate it (same x-range as the popup)
* instead of treating it as "left both", so the visual gap stays
* but doesn't act as dead space that closes the popup. */
int gap_y0, gap_y1;
if (popup.y + popup.h <= by) {
gap_y0 = popup.y + popup.h;
gap_y1 = by;
} else {
gap_y0 = by + bh;
gap_y1 = popup.y;
}
if (root_x >= popup.x && root_x < popup.x + popup.w &&
root_y >= gap_y0 && root_y < gap_y1)
return;
}
popup_close();
}
void hdl_dummy(XEvent *xev)
{
(void)xev;
}
void hdl_expose(XEvent *xev)
{
if (popup.open && xev->xexpose.window == popup.win) {
/* re-present the already-drawn buffer -- don't call popup_draw()
* here, which would re-run every row's label_command from
* scratch; Expose just means "show what you already have again",
* and recomputing risked a visibly different frame (marquee
* offset, a command's output) flashing in right after the
* correct one from popup_open()/popup_draw(). */
XCopyArea(dpy, popup.buffer, popup.win, gc, 0, 0, popup.w, popup.h, 0, 0);
return;
}
int idx = find_bar(xev->xexpose.window);
redraw_bar(idx);
}
/* the popup is an override-redirect window, so the window manager never
* restacks it on our behalf -- if some other (WM-managed) window gets
* raised while the popup is open (e.g. focus-follows-mouse raising the
* window the pointer just passed over on its way to the next module),
* that window ends up on top of the popup even though we raised it first.
* Re-raise whenever we notice we've been covered. */
void hdl_visibility(XEvent *xev)
{
if (!popup.open || xev->xvisibility.window != popup.win)
return;
if (xev->xvisibility.state != VisibilityUnobscured)
XRaiseWindow(dpy, popup.win);
}
void hdl_property(XEvent *xev)
{
if (xev->xproperty.atom == XInternAtom(dpy, "_NET_CURRENT_DESKTOP", False)) {
for (int i = 0; i < nbars; i++) {
redraw_bar(i);
}
}
}
void init_defaults(void)
{
config.bottom_bar = False;
config.height = 19;
config.vertical_padding = 0;
config.horizontal_padding = 0;
config.text_padding = 0;
config.border = False;
config.border_width = 0;
config.background_colour = parse_col("#000000");
config.foreground_colour = parse_col("#7abccd");
config.border_colour = parse_col("#005577");
config.font = strdup("monospace:size=10");
config.show_version = True;
config.version_text = strdup(SXBAR_VERSION);
config.secondary_bar = False;
init_modules();
}
int find_bar(Window win)
{
for (int i = 0; i < nbars; i++) {
if (bars[i].win == win) {
return i;
}
}
return 0;
}
/* every module -- whether sxbar ships a script for it or the user wrote
* their own -- is created on demand from sxbarc's `module :` directive
* (see parser.c: resolve_script(), and the `module` directive handling in
* parse_config()). There's no hardcoded built-in list any more: a config
* with no `module :` lines at all starts with zero modules. */
void init_modules(void)
{
config.max_modules = 16; /* grow_modules() (parser.c) takes over from
* here as sxbarc's `module :` lines create
* more than this fits */
config.modules = malloc(config.max_modules * sizeof(Module));
config.module_count = 0;
}
unsigned long parse_col(const char *hex)
{
XColor col;
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;
while (True) {
while (XPending(dpy)) {
XNextEvent(dpy, &xev);
evtable[xev.type](&xev);
}
time_t now = time(NULL);
int due = now - last >= 1;
if (due)
update_modules();
/* redraw every ~100ms tick while any module's text is actively
* scrolling (marquee), so it animates smoothly; otherwise just
* once a second like before -- advance_marquees() only ever
* reports true for modules with max_width set and overflowing, so
* this changes nothing for a config that doesn't use it */
int scrolling = advance_marquees();
if (due || scrolling) {
for (int i = 0; i < nbars; i++) {
redraw_bar(i);
}
}
/* same idea for whichever popup is currently open, if any -- it
* redraws itself directly (it's a separate window from the bars) */
advance_popup_marquee();
if (due)
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("");
}
/* 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;
}
static void free_taskbar_entries(Module *m)
{
for (int i = 0; i < m->taskbar_entry_count; i++) {
free(m->taskbar_entries[i].label);
free(m->taskbar_entries[i].command);
}
free(m->taskbar_entries);
m->taskbar_entries = NULL;
m->taskbar_entry_count = 0;
}
/* parses one "A" : "B" : "C" [ : MON ] line -- the internal wire format
* scripts/taskbar.sh's listing uses, not a user-facing sxbarc directive,
* so this doesn't reuse parser.c's quote-parsing (scoped to config-file
* directives). The trailing MON field is a bare (unquoted) monitor index
* the script computed by matching the window's geometry against the
* monitor rectangles update_taskbar() passed it -- optional so a custom
* taskbar.sh that doesn't emit it still parses fine, just with *mon left
* at -1 (see taskbar_indices_for_monitor()'s handling of that sentinel).
* Outputs a/b/c are malloc'd (strdup) on success (0); returns -1 without
* allocating anything if the line doesn't match, so the caller can just
* skip it. */
static int parse_three_quoted(char *line, char **a, char **b, char **c, int *mon)
{
char *p = line;
char *fields[3];
for (int i = 0; i < 3; i++) {
while (*p == ' ' || *p == '\t')
p++;
if (*p != '"')
return -1;
p++;
char *start = p;
char *end = strchr(p, '"');
if (!end)
return -1;
*end = '\0';
fields[i] = start;
p = end + 1;
while (*p == ' ' || *p == '\t')
p++;
if (i < 2) {
if (*p != ':')
return -1;
p++;
}
}
*a = strdup(fields[0]);
*b = strdup(fields[1]);
*c = strdup(fields[2]);
while (*p == ' ' || *p == '\t' || *p == ':')
p++;
*mon = (*p == '\0') ? -1 : (int)strtol(p, NULL, 10);
return 0;
}
/* refreshes the built-in `taskbar` module's window-entry list by running
* its script with "list" plus one geometry arg per monitor ("x,y,w,h",
* in the same order as monitors[]/bars[].monitor) and parsing each line
* as "Title" : "0xWindowID" : "command" : monitor -- see scripts/taskbar.sh,
* which does the actual window-to-monitor matching itself (in awk,
* against those geometry args) rather than sxbar re-deriving it via X
* property queries; this is called once for all bars, not once per bar,
* so the monitor tag on each entry is what lets draw_bar_into()/
* taskbar_entry_at_x() show/click only the entries for their own bar.
* Unlike every other module, m->cached_output is deliberately left NULL:
* every generic per-module code path (draw_bar_into()'s layout/rendering
* loops, module_at_x(), advance_marquees()) already skips modules with no
* cached_output, which is exactly what we want here, since taskbar
* renders itself in its own dedicated block/click-handling instead. */
static void update_taskbar(Module *m)
{
char geoms[MAX_MONITORS * 32] = "";
for (int i = 0; i < nmonitors; i++) {
char part[32];
snprintf(part, sizeof part, " %d,%d,%d,%d", monitors[i].x_org, monitors[i].y_org,
monitors[i].width, monitors[i].height);
strncat(geoms, part, sizeof geoms - strlen(geoms) - 1);
}
char cmd[PATH_MAX + sizeof geoms + 8];
snprintf(cmd, sizeof cmd, "%s list%s", m->command, geoms);
FILE *fp = popen(cmd, "r");
if (!fp)
return;
free_taskbar_entries(m);
int max = 0;
char line[1024];
while (fgets(line, sizeof line, fp)) {
char *nl = strchr(line, '\n');
if (nl)
*nl = '\0';
char *title, *idstr, *action;
int mon;
if (parse_three_quoted(line, &title, &idstr, &action, &mon) < 0)
continue;
if (m->taskbar_entry_count >= max) {
int newmax = max ? max * 2 : 4;
TaskbarEntry *tmp = realloc(m->taskbar_entries, newmax * sizeof *tmp);
if (!tmp) {
free(title);
free(idstr);
free(action);
break;
}
m->taskbar_entries = tmp;
max = newmax;
}
TaskbarEntry *e = &m->taskbar_entries[m->taskbar_entry_count++];
e->label = title;
e->command = action;
e->id = (Window)strtoul(idstr, NULL, 0); /* base 0: "0x..." parses as hex */
e->monitor = mon;
free(idstr);
}
pclose(fp);
}
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) {
if (!strcmp(m->name, "taskbar")) {
update_taskbar(m);
m->last_update = now;
continue;
}
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;
}
}
}
void setup(void)
{
if (!(dpy = XOpenDisplay(NULL))) {
errx(1, "can't open display");
}
XSetErrorHandler(xerror);
root = XDefaultRootWindow(dpy);
scr = DefaultScreen(dpy);
for (int i = 0; i < LASTEvent; i++) {
evtable[i] = hdl_dummy;
}
evtable[Expose] = hdl_expose;
evtable[ButtonPress] = hdl_button;
evtable[ButtonRelease] = hdl_button_release;
evtable[MotionNotify] = hdl_motion;
evtable[LeaveNotify] = hdl_crossing;
evtable[PropertyNotify] = hdl_property;
evtable[VisibilityNotify] = hdl_visibility;
XSelectInput(dpy, root, PropertyChangeMask);
init_defaults();
parse_config(&config);
create_bars();
resolve_module_colours();
update_modules();
}
int main(int ac, char **av)
{
if (ac > 1) {
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]");
}
/* sxwm spawns us without setsid(), so we start out in its process
* group -- along with every other program it launches. Some apps
* (Electron ones observed in practice) signal their own process
* group on shutdown to clean up child processes, which then takes
* sxbar down as collateral. Detach into our own session so we're
* immune regardless of what else shares that group. */
setsid();
setup();
run();
/* TODO?: never reached */
cleanup_modules();
cleanup_resources();
return 0;
}