#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;
}

