#define _POSIX_C_SOURCE 200809L
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#include "defs.h"
#include "parser.h"

extern unsigned long parse_col(const char *hex);

static char *strip(char *s)
{
	while (*s && isspace((unsigned char)*s))
		s++;
	if (!*s)
		return s;
	char *e = s + strlen(s) - 1;
	while (e > s && isspace((unsigned char)*e))
		*e-- = '\0';
	return s;
}

static char *strip_comment(char *s)
{
	/* a '#' as the very first character is a value (e.g. a hex colour like
	 * #50fa7b), not a comment marker -- only a later '#' starts a trailing
	 * comment (e.g. "#50fa7b  # my favourite green") */
	char *scan = (*s == '#') ? s + 1 : s;
	char *c = strchr(scan, '#');
	if (c)
		*c = '\0';
	return strip(s);
}

/* true/false with optional trailing whitespace/comment */
static int parse_bool(const char *s)
{
	return strncmp(s, "true", 4) == 0 &&
	       (s[4] == '\0' || isspace((unsigned char)s[4]) || s[4] == '#');
}

/* expand leading ~/ to $HOME/ */
static char *expand_home(const char *s)
{
	if (s[0] != '~' || s[1] != '/')
		return strdup(s);
	const char *home = getenv("HOME");
	if (!home)
		return strdup(s);
	size_t len = strlen(home) + strlen(s + 1) + 1;
	char *out = malloc(len);
	if (!out)
		return strdup(s);
	snprintf(out, len, "%s%s", home, s + 1);
	return out;
}

static FILE *open_config(char *path, size_t pathsz)
{
	const char *home = getenv("HOME");
	if (!home) {
		fputs("sxbarc: HOME not set\n", stderr);
		return NULL;
	}

	const char *xdg = getenv("XDG_CONFIG_HOME");
	if (xdg) {
		snprintf(path, pathsz, "%s/sxbarc", xdg);
		if (access(path, R_OK) == 0)
			goto found;
		snprintf(path, pathsz, "%s/sxbar/sxbarc", xdg);
		if (access(path, R_OK) == 0)
			goto found;
	}

	snprintf(path, pathsz, "%s/.config/sxbarc", home);
	if (access(path, R_OK) == 0)
		goto found;

	snprintf(path, pathsz, "%s/.config/sxbar/sxbarc", home);
	if (access(path, R_OK) == 0)
		goto found;

	snprintf(path, pathsz, "/usr/local/share/sxbarc");
	if (access(path, R_OK) == 0)
		goto found;

	fprintf(stderr, "sxbarc: no configuration file found\n");
	return NULL;

found:
	printf("sxbarc: using %s\n", path);
	FILE *f = fopen(path, "r");
	if (!f)
		fprintf(stderr, "sxbarc: cannot open %s\n", path);
	return f;
}

static Module *find_module(Config *cfg, const char *name)
{
	for (int i = 0; i < cfg->module_count; i++) {
		if (cfg->modules[i].name && !strcmp(cfg->modules[i].name, name))
			return &cfg->modules[i];
	}
	return NULL;
}

static int grow_modules(Config *cfg)
{
	if (cfg->module_count < cfg->max_modules)
		return 0;
	int newmax = cfg->max_modules * 2;
	Module *tmp = realloc(cfg->modules, newmax * sizeof(Module));
	if (!tmp)
		return -1;
	cfg->modules   = tmp;
	cfg->max_modules = newmax;
	return 0;
}

static int mkdir_p(const char *dir)
{
	char tmp[PATH_MAX];
	snprintf(tmp, sizeof tmp, "%s", dir);
	for (char *p = tmp + 1; *p; p++) {
		if (*p != '/')
			continue;
		*p = '\0';
		if (mkdir(tmp, 0755) != 0 && errno != EEXIST)
			return -1;
		*p = '/';
	}
	if (mkdir(tmp, 0755) != 0 && errno != EEXIST)
		return -1;
	return 0;
}

static void copy_script(const char *src_path, const char *dst_path)
{
	FILE *src = fopen(src_path, "rb");
	if (!src)
		return;
	FILE *dst = fopen(dst_path, "wb");
	if (!dst) {
		fclose(src);
		return;
	}
	char buf[4096];
	size_t n;
	while ((n = fread(buf, 1, sizeof buf, src)) > 0)
		fwrite(buf, 1, n, dst);
	fclose(src);
	fclose(dst);
	chmod(dst_path, 0755);
}

/* on a fresh install, $PREFIX/share/sxbar/scripts/ (installed by `make
 * install`) has every built-in module's reference script, but
 * ~/.config/sxbar/scripts/ -- the copy resolve_script() actually prefers,
 * and the one users are told to edit -- starts out empty; nothing ever
 * created it. Seed it once at startup: copy in any reference script whose
 * name isn't already present under ~/.config/sxbar/scripts/, so built-in
 * modules work (and are editable in place) without a manual copy step
 * first. Never overwrites a file that's already there, so existing user
 * edits are untouched; only fills in gaps (e.g. a script for a built-in
 * module added in a later sxbar version). */
static void seed_user_scripts(void)
{
	const char *home = getenv("HOME");
	if (!home)
		return;

	char user_dir[PATH_MAX];
	snprintf(user_dir, sizeof user_dir, "%s/.config/sxbar/scripts", home);

	const char *sys_dir = "/usr/local/share/sxbar/scripts";
	DIR *d = opendir(sys_dir);
	if (!d)
		return; /* not installed system-wide yet -- nothing to seed from */

	int seeded = 0;
	struct dirent *ent;
	while ((ent = readdir(d))) {
		size_t len = strlen(ent->d_name);
		if (len < 4 || strcmp(ent->d_name + len - 3, ".sh"))
			continue;

		char dst_path[PATH_MAX];
		if ((size_t)snprintf(dst_path, sizeof dst_path, "%s/%s", user_dir,
		                      ent->d_name) >= sizeof dst_path)
			continue; /* path too long to ever exist -- skip */
		if (access(dst_path, F_OK) == 0)
			continue; /* user already has (or edited) this one */

		if (!seeded && mkdir_p(user_dir) != 0) {
			closedir(d);
			return;
		}

		char src_path[PATH_MAX];
		snprintf(src_path, sizeof src_path, "%s/%s", sys_dir, ent->d_name);
		copy_script(src_path, dst_path);
		seeded++;
	}
	closedir(d);

	if (seeded)
		printf("sxbarc: seeded %d script%s into %s\n", seeded,
		       seeded == 1 ? "" : "s", user_dir);
}

/* resolve a module name to its script: the user's own edited copy at
 * ~/.config/sxbar/scripts/<name>.sh wins over the installed reference copy
 * at $PREFIX/share/sxbar/scripts/<name>.sh (currently hardcoded to
 * /usr/local, matching the Makefile's default PREFIX), or a harmless
 * shell no-op (`:`) if neither exists yet -- e.g. a freshly built,
 * not-yet-installed checkout. Every module -- whether sxbar ships a
 * script for it or the user wrote their own -- is resolved this same way. */
static char *resolve_script(const char *name)
{
	char path[PATH_MAX];
	const char *home = getenv("HOME");
	if (home) {
		snprintf(path, sizeof path, "%s/.config/sxbar/scripts/%s.sh", home, name);
		if (access(path, X_OK) == 0)
			return strdup(path);
	}
	snprintf(path, sizeof path, "/usr/local/share/sxbar/scripts/%s.sh", name);
	if (access(path, X_OK) == 0)
		return strdup(path);
	return strdup(":");
}

/* the first popup_item/popup_info line for a module wipes its built-in
 * default rows (if any, including a popup_set-managed slider row); later
 * lines from either directive just append. A popup_set line afterwards
 * re-creates the slider row if you still want one alongside your rows. */
static void clear_builtin_popup_items(Module *m)
{
	if (m->popup_items_from_config)
		return;
	for (int i = 0; i < m->popup_item_count; i++) {
		free(m->popup_items[i].label);
		free(m->popup_items[i].command);
		free(m->popup_items[i].label_command);
		free(m->popup_items[i].set_command);
		free(m->popup_items[i].image_command);
		/* .image itself is only ever populated by popup_open() (sxbar.c) at
		 * runtime, never during config parsing -- this always runs before
		 * any popup has opened, so there's nothing loaded yet to free here */
		for (int b = 0; b < m->popup_items[i].button_count; b++) {
			free(m->popup_items[i].buttons[b].label);
			free(m->popup_items[i].buttons[b].command);
		}
		free(m->popup_items[i].buttons);
	}
	m->popup_item_count = 0;
	m->slider_item_idx = -1;
	m->popup_items_from_config = 1;
}

static int grow_popup_items(Module *m)
{
	if (m->popup_item_count < m->popup_item_max)
		return 0;
	int newmax = m->popup_item_max ? m->popup_item_max * 2 : 4;
	PopupItem *tmp = realloc(m->popup_items, newmax * sizeof *tmp);
	if (!tmp)
		return -1;
	m->popup_items = tmp;
	m->popup_item_max = newmax;
	return 0;
}

/* ---- shared popup-directive application ----
 *
 * Each of these takes the directive's value with the module-name field
 * already stripped off (e.g. "hover : buttons" for `popup`, or
 * "\"Label\" : \"command\"" for `popup_item`), plus `ctx`/`lineno` to
 * identify the source in error messages. This lets the exact same parsing
 * be driven both by sxbarc lines (which have a name field to strip first)
 * and by a module script's own `<script> menu` output (which never has a
 * name field, since a script only ever describes itself).
 *
 * `from_sxbarc` on the item/info variants controls whether the built-in
 * default rows get cleared first: sxbarc directives should replace a
 * script's default rows on their first line (same as they used to replace
 * the old compile-time defaults), but a script's own `menu` output *is*
 * the set of default rows, so loading it must not trip that "already
 * customized by sxbarc" flag. */

static int apply_popup(Module *m, char *rest, const char *ctx, int lineno)
{
	char *p2 = strchr(rest, ':');
	if (!p2) {
		fprintf(stderr, "%s:%d: popup missing trigger/type\n", ctx, lineno);
		return -1;
	}
	*p2 = '\0';
	char *trig_s = strip(rest);
	char *type_s = strip(p2 + 1);
	strip_comment(type_s);
	if (!strcmp(trig_s, "hover")) {
		m->popup_trigger = POPUP_TRIGGER_HOVER;
	} else if (!strcmp(trig_s, "click")) {
		m->popup_trigger = POPUP_TRIGGER_CLICK;
	} else {
		fprintf(stderr, "%s:%d: popup: trigger must be 'hover' or 'click', got '%s'\n",
		        ctx, lineno, trig_s);
		return -1;
	}
	/* popups are just a list of rows now (text/button/slider can be mixed
	 * freely via popup_item/popup_info/popup_set) -- this field only still
	 * exists so older configs keep parsing; both values just mean "this
	 * module has a popup" */
	if (!strcmp(type_s, "buttons") || !strcmp(type_s, "slider")) {
		m->popup_type = POPUP_BUTTONS;
	} else {
		fprintf(stderr, "%s:%d: popup: type must be 'buttons' or 'slider', got '%s'\n",
		        ctx, lineno, type_s);
		return -1;
	}
	return 0;
}

static int apply_popup_item(Module *m, char *rest, const char *ctx, int lineno, int from_sxbarc)
{
	char *after = strip(rest);
	if (*after != '"' && *after != '\'') {
		fprintf(stderr, "%s:%d: popup_item label must be quoted\n", ctx, lineno);
		return -1;
	}
	char q = *after;
	char *label_start = after + 1;
	char *closing = strchr(label_start, q);
	if (!closing) {
		fprintf(stderr, "%s:%d: popup_item label missing closing quote\n", ctx, lineno);
		return -1;
	}
	*closing = '\0';

	char *tail = strip(closing + 1);
	if (*tail != ':') {
		fprintf(stderr, "%s:%d: popup_item missing command\n", ctx, lineno);
		return -1;
	}
	tail = strip(tail + 1);
	if (*tail != '"' && *tail != '\'') {
		fprintf(stderr, "%s:%d: popup_item command must be quoted\n", ctx, lineno);
		return -1;
	}
	q = *tail;
	char *cmd_start = tail + 1;
	closing = strchr(cmd_start, q);
	if (!closing) {
		fprintf(stderr, "%s:%d: popup_item command missing closing quote\n", ctx, lineno);
		return -1;
	}
	*closing = '\0';

	if (from_sxbarc)
		clear_builtin_popup_items(m);
	if (grow_popup_items(m) < 0) {
		fprintf(stderr, "%s: out of memory\n", ctx);
		return -1;
	}
	m->popup_items[m->popup_item_count].type          = POPUP_ROW_BUTTON;
	m->popup_items[m->popup_item_count].label         = strdup(label_start);
	m->popup_items[m->popup_item_count].command       = expand_home(cmd_start);
	m->popup_items[m->popup_item_count].label_command = NULL;
	m->popup_items[m->popup_item_count].set_command   = NULL;
	m->popup_items[m->popup_item_count].image_command = NULL;
	m->popup_items[m->popup_item_count].image         = NULL;
	m->popup_items[m->popup_item_count].image_w       = 0;
	m->popup_items[m->popup_item_count].image_h       = 0;
	m->popup_items[m->popup_item_count].buttons       = NULL;
	m->popup_items[m->popup_item_count].button_count  = 0;
	m->popup_items[m->popup_item_count].scroll_offset = 0;
	m->popup_item_count++;
	return 0;
}

/* popup_live_item : "label command" : "click command" -- a BUTTON row whose
 * label is re-run fresh (via label_command) every time the popup opens,
 * same live convention as popup_info, but still clickable: its own
 * click command spawns independently of the label. Use this instead of
 * popup_item when the row needs to show live status on the button itself
 * rather than (or in addition to) a separate popup_info row. */
static int apply_popup_live_item(Module *m, char *rest, const char *ctx, int lineno, int from_sxbarc)
{
	char *after = strip(rest);
	if (*after != '"' && *after != '\'') {
		fprintf(stderr, "%s:%d: popup_live_item label command must be quoted\n", ctx, lineno);
		return -1;
	}
	char q = *after;
	char *label_start = after + 1;
	char *closing = strchr(label_start, q);
	if (!closing) {
		fprintf(stderr, "%s:%d: popup_live_item label command missing closing quote\n", ctx, lineno);
		return -1;
	}
	*closing = '\0';

	char *tail = strip(closing + 1);
	if (*tail != ':') {
		fprintf(stderr, "%s:%d: popup_live_item missing command\n", ctx, lineno);
		return -1;
	}
	tail = strip(tail + 1);
	if (*tail != '"' && *tail != '\'') {
		fprintf(stderr, "%s:%d: popup_live_item command must be quoted\n", ctx, lineno);
		return -1;
	}
	q = *tail;
	char *cmd_start = tail + 1;
	closing = strchr(cmd_start, q);
	if (!closing) {
		fprintf(stderr, "%s:%d: popup_live_item command missing closing quote\n", ctx, lineno);
		return -1;
	}
	*closing = '\0';

	if (from_sxbarc)
		clear_builtin_popup_items(m);
	if (grow_popup_items(m) < 0) {
		fprintf(stderr, "%s: out of memory\n", ctx);
		return -1;
	}
	m->popup_items[m->popup_item_count].type          = POPUP_ROW_BUTTON;
	m->popup_items[m->popup_item_count].label         = NULL;
	m->popup_items[m->popup_item_count].command       = expand_home(cmd_start);
	m->popup_items[m->popup_item_count].label_command = expand_home(label_start);
	m->popup_items[m->popup_item_count].set_command   = NULL;
	m->popup_items[m->popup_item_count].image_command = NULL;
	m->popup_items[m->popup_item_count].image         = NULL;
	m->popup_items[m->popup_item_count].image_w       = 0;
	m->popup_items[m->popup_item_count].image_h       = 0;
	m->popup_items[m->popup_item_count].buttons       = NULL;
	m->popup_items[m->popup_item_count].button_count  = 0;
	m->popup_items[m->popup_item_count].scroll_offset = 0;
	m->popup_item_count++;
	return 0;
}

static int apply_popup_info(Module *m, char *rest, const char *ctx, int lineno, int from_sxbarc)
{
	char *after = strip(rest);
	if (*after != '"' && *after != '\'') {
		fprintf(stderr, "%s:%d: popup_info command must be quoted\n", ctx, lineno);
		return -1;
	}
	char q = *after;
	char *cmd_start = after + 1;
	char *closing = strchr(cmd_start, q);
	if (!closing) {
		fprintf(stderr, "%s:%d: popup_info command missing closing quote\n", ctx, lineno);
		return -1;
	}
	*closing = '\0';

	if (from_sxbarc)
		clear_builtin_popup_items(m);
	if (grow_popup_items(m) < 0) {
		fprintf(stderr, "%s: out of memory\n", ctx);
		return -1;
	}
	m->popup_items[m->popup_item_count].type          = POPUP_ROW_TEXT;
	m->popup_items[m->popup_item_count].label         = NULL;
	m->popup_items[m->popup_item_count].command       = NULL;
	m->popup_items[m->popup_item_count].label_command = expand_home(cmd_start);
	m->popup_items[m->popup_item_count].set_command   = NULL;
	m->popup_items[m->popup_item_count].image_command = NULL;
	m->popup_items[m->popup_item_count].image         = NULL;
	m->popup_items[m->popup_item_count].image_w       = 0;
	m->popup_items[m->popup_item_count].image_h       = 0;
	m->popup_items[m->popup_item_count].buttons       = NULL;
	m->popup_items[m->popup_item_count].button_count  = 0;
	m->popup_items[m->popup_item_count].scroll_offset = 0;
	m->popup_item_count++;
	return 0;
}

static int apply_popup_image(Module *m, char *rest, const char *ctx, int lineno, int from_sxbarc)
{
	/* rest: "command" -- stdout is a path to a local image file (e.g. album
	 * art), re-run fresh every time the popup opens, same convention as
	 * popup_info's label_command. Empty/failed output just means no image
	 * this time -- the row still exists, it renders blank. */
	char *after = strip(rest);
	if (*after != '"' && *after != '\'') {
		fprintf(stderr, "%s:%d: popup_image command must be quoted\n", ctx, lineno);
		return -1;
	}
	char q = *after;
	char *cmd_start = after + 1;
	char *closing = strchr(cmd_start, q);
	if (!closing) {
		fprintf(stderr, "%s:%d: popup_image command missing closing quote\n", ctx, lineno);
		return -1;
	}
	*closing = '\0';

	if (from_sxbarc)
		clear_builtin_popup_items(m);
	if (grow_popup_items(m) < 0) {
		fprintf(stderr, "%s: out of memory\n", ctx);
		return -1;
	}
	PopupItem *it = &m->popup_items[m->popup_item_count];
	it->type          = POPUP_ROW_IMAGE;
	it->label         = NULL;
	it->command       = NULL;
	it->label_command = NULL;
	it->set_command   = NULL;
	it->image_command = expand_home(cmd_start);
	it->image         = NULL;
	it->image_w       = 0;
	it->image_h       = 0;
	it->buttons       = NULL;
	it->button_count  = 0;
	it->scroll_offset = 0;
	m->popup_item_count++;
	return 0;
}

static int apply_popup_image_size(Module *m, char *rest, const char *ctx, int lineno)
{
	/* rest: pixels -- caps the square box popup_image rows scale their art
	 * into for this module (default: the built-in POPUP_IMAGE_SIZE) */
	char *val = strip(rest);
	strip_comment(val);
	int px = atoi(val);
	if (px <= 0) {
		fprintf(stderr, "%s:%d: popup_image_size must be a positive number\n", ctx, lineno);
		return -1;
	}
	m->popup_image_size = px;
	return 0;
}

/* free a partially- or fully-built button array on a parse error, before
 * returning -- nothing has been attached to a Module yet at that point */
static void free_parsed_buttons(PopupButton *btns, int count)
{
	for (int i = 0; i < count; i++) {
		free(btns[i].label);
		free(btns[i].command);
	}
	free(btns);
}

static int apply_popup_buttons(Module *m, char *rest, const char *ctx, int lineno, int from_sxbarc)
{
	/* rest: "Label1" : "command1" : "Label2" : "command2" ... -- one row
	 * split into N equal-width button segments side by side, e.g. media
	 * transport controls (glyphs work well as labels here: prev/play-pause/
	 * next icons instead of stacking three full-width text rows). */
	PopupButton *btns = NULL;
	int count = 0, max = 0;
	char *p = rest;

	for (;;) {
		p = strip(p);
		if (!*p)
			break;
		if (*p != '"' && *p != '\'') {
			fprintf(stderr, "%s:%d: popup_buttons: expected a quoted label or command, got '%s'\n",
			        ctx, lineno, p);
			free_parsed_buttons(btns, count);
			return -1;
		}
		char q = *p;
		char *start = p + 1;
		char *closing = strchr(start, q);
		if (!closing) {
			fprintf(stderr, "%s:%d: popup_buttons: missing closing quote\n", ctx, lineno);
			free_parsed_buttons(btns, count);
			return -1;
		}
		*closing = '\0';
		p = closing + 1;

		if (count % 2 == 0) {
			if (count / 2 >= max) {
				int newmax = max ? max * 2 : 4;
				PopupButton *tmp = realloc(btns, newmax * sizeof *tmp);
				if (!tmp) {
					fprintf(stderr, "%s: out of memory\n", ctx);
					free_parsed_buttons(btns, count);
					return -1;
				}
				btns = tmp;
				max = newmax;
			}
			btns[count / 2].label   = strdup(start);
			btns[count / 2].command = NULL;
		} else {
			btns[count / 2].command = expand_home(start);
		}
		count++;

		p = strip(p);
		if (!*p)
			break;
		if (*p != ':') {
			fprintf(stderr, "%s:%d: popup_buttons: expected ':' after \"%s\"\n", ctx, lineno, start);
			free_parsed_buttons(btns, count);
			return -1;
		}
		p++;
	}

	if (count == 0 || count % 2 != 0) {
		fprintf(stderr, "%s:%d: popup_buttons: needs an even number of \"label\" : \"command\" pairs\n",
		        ctx, lineno);
		free_parsed_buttons(btns, count);
		return -1;
	}

	if (from_sxbarc)
		clear_builtin_popup_items(m);
	if (grow_popup_items(m) < 0) {
		fprintf(stderr, "%s: out of memory\n", ctx);
		free_parsed_buttons(btns, count);
		return -1;
	}
	PopupItem *it = &m->popup_items[m->popup_item_count];
	it->type          = POPUP_ROW_BUTTONS;
	it->label         = NULL;
	it->command       = NULL;
	it->label_command = NULL;
	it->set_command   = NULL;
	it->image_command = NULL;
	it->image         = NULL;
	it->image_w       = 0;
	it->image_h       = 0;
	it->buttons       = btns;
	it->button_count  = count / 2;
	it->scroll_offset = 0;
	m->popup_item_count++;
	return 0;
}

static int apply_popup_set(Module *m, char *rest, const char *ctx, int lineno)
{
	char *after = strip(rest);
	if (*after != '"' && *after != '\'') {
		fprintf(stderr, "%s:%d: popup_set command must be quoted\n", ctx, lineno);
		return -1;
	}
	char q = *after;
	char *cmd_start = after + 1;
	char *closing = strchr(cmd_start, q);
	if (!closing) {
		fprintf(stderr, "%s:%d: popup_set command missing closing quote\n", ctx, lineno);
		return -1;
	}
	*closing = '\0';

	if (m->slider_item_idx >= 0) {
		free(m->popup_items[m->slider_item_idx].set_command);
		m->popup_items[m->slider_item_idx].set_command = expand_home(cmd_start);
	} else {
		if (grow_popup_items(m) < 0) {
			fprintf(stderr, "%s: out of memory\n", ctx);
			return -1;
		}
		PopupItem *it = &m->popup_items[m->popup_item_count];
		it->type          = POPUP_ROW_SLIDER;
		it->label         = NULL;
		it->command       = NULL;
		it->label_command = NULL;
		it->set_command   = expand_home(cmd_start);
		it->image_command = NULL;
		it->image         = NULL;
		it->image_w       = 0;
		it->image_h       = 0;
		it->buttons       = NULL;
		it->button_count  = 0;
		it->scroll_offset = 0;
		it->value         = 0;
		it->last_spawned  = 0;
		m->slider_item_idx = m->popup_item_count;
		m->popup_item_count++;
		m->popup_type = POPUP_BUTTONS; /* "has a popup" */
	}
	return 0;
}

/* run "<script_path> menu" and apply each line of its output the same way
 * sxbarc's own popup/popup_item/popup_info/popup_image/popup_set
 * directives are applied, just without a module-name field to strip off
 * first. These
 * count as default rows, not sxbarc overrides: `popup_items_from_config`
 * is left untouched, so sxbarc can still replace them afterwards exactly
 * like it could replace the old compile-time defaults. */
void load_popup_from_script(Module *m, const char *script_path)
{
	char cmd[PATH_MAX + 8];
	snprintf(cmd, sizeof cmd, "%s menu", script_path);

	FILE *fp = popen(cmd, "r");
	if (!fp)
		return;

	char ctx[PATH_MAX + 8];
	snprintf(ctx, sizeof ctx, "%s menu", script_path);

	char line[1024];
	int lineno = 0;
	while (fgets(line, sizeof line, fp)) {
		lineno++;
		char *s = strip(line);
		if (!*s || *s == '#')
			continue;

		char *sep = strchr(s, ':');
		if (!sep) {
			fprintf(stderr, "%s:%d: missing ':'\n", ctx, lineno);
			continue;
		}
		*sep = '\0';
		char *key  = strip(s);
		char *rest = strip(sep + 1);

		if (!strcmp(key, "popup")) {
			apply_popup(m, rest, ctx, lineno);
		} else if (!strcmp(key, "popup_item")) {
			apply_popup_item(m, rest, ctx, lineno, 0);
		} else if (!strcmp(key, "popup_live_item")) {
			apply_popup_live_item(m, rest, ctx, lineno, 0);
		} else if (!strcmp(key, "popup_info")) {
			apply_popup_info(m, rest, ctx, lineno, 0);
		} else if (!strcmp(key, "popup_image")) {
			apply_popup_image(m, rest, ctx, lineno, 0);
		} else if (!strcmp(key, "popup_image_size")) {
			apply_popup_image_size(m, rest, ctx, lineno);
		} else if (!strcmp(key, "popup_buttons")) {
			apply_popup_buttons(m, rest, ctx, lineno, 0);
		} else if (!strcmp(key, "popup_set")) {
			apply_popup_set(m, rest, ctx, lineno);
		} else {
			fprintf(stderr, "%s:%d: unknown directive '%s'\n", ctx, lineno, key);
		}
	}
	pclose(fp);
}

int parse_config(Config *cfg)
{
	seed_user_scripts();

	char path[PATH_MAX];
	FILE *f = open_config(path, sizeof(path));
	if (!f)
		return -1;

	char line[1024];
	int lineno = 0;

	while (fgets(line, sizeof line, f)) {
		lineno++;
		char *s = strip(line);
		if (!*s || *s == '#')
			continue;

		char *sep = strchr(s, ':');
		if (!sep) {
			fprintf(stderr, "sxbarc:%d: missing ':'\n", lineno);
			continue;
		}
		*sep = '\0';
		char *key  = strip(s);
		char *rest = strip(sep + 1);

		if (!strcmp(key, "height")) {
			cfg->height = atoi(rest);
		} else if (!strcmp(key, "bottom_bar")) {
			cfg->bottom_bar = parse_bool(rest);
		} else if (!strcmp(key, "vertical_padding")) {
			cfg->vertical_padding = atoi(rest);
		} else if (!strcmp(key, "horizontal_padding")) {
			cfg->horizontal_padding = atoi(rest);
		} else if (!strcmp(key, "text_padding")) {
			cfg->text_padding = atoi(rest);
		} else if (!strcmp(key, "border")) {
			cfg->border = parse_bool(rest);
		} else if (!strcmp(key, "border_width")) {
			cfg->border_width = atoi(rest);
		} else if (!strcmp(key, "background_colour") ||
		           !strcmp(key, "background_color")) {
			cfg->background_colour = parse_col(rest);
		} else if (!strcmp(key, "foreground_colour") ||
		           !strcmp(key, "foreground_color")) {
			cfg->foreground_colour = parse_col(rest);
		} else if (!strcmp(key, "border_colour") ||
		           !strcmp(key, "border_color")) {
			cfg->border_colour = parse_col(rest);
		} else if (!strcmp(key, "font")) {
			strip_comment(rest);
			free(cfg->font);
			cfg->font = strdup(rest);
		} else if (!strcmp(key, "show_version")) {
			cfg->show_version = parse_bool(rest);
		} else if (!strcmp(key, "version_text")) {
			strip_comment(rest);
			free(cfg->version_text);
			cfg->version_text = strdup(rest);
		} else if (!strcmp(key, "secondary_bar")) {
			/* enable a second bar on the opposite edge from `bottom_bar`,
			 * showing only modules tagged `bar : module_name : secondary` */
			cfg->secondary_bar = parse_bool(rest);
		} else if (!strcmp(key, "workspace_icon")) {
			/* workspace_icon : name : "icon text" -- replaces a workspace's
			 * displayed label (its _NET_DESKTOP_NAMES string, e.g. "1")
			 * with this text instead, e.g. a Nerd Font glyph. Repeatable,
			 * one per workspace name. */
			char *p1 = strchr(rest, ':');
			if (!p1) {
				fprintf(stderr, "sxbarc:%d: workspace_icon missing name and text\n", lineno);
				continue;
			}
			*p1 = '\0';
			char *name  = strip(rest);
			char *after = strip(p1 + 1);

			if (*after != '"' && *after != '\'') {
				fprintf(stderr, "sxbarc:%d: workspace_icon text must be quoted\n", lineno);
				continue;
			}
			char q = *after;
			char *text_start = after + 1;
			char *closing = strchr(text_start, q);
			if (!closing) {
				fprintf(stderr, "sxbarc:%d: workspace_icon text missing closing quote\n", lineno);
				continue;
			}
			*closing = '\0';

			if (cfg->workspace_icon_count >= cfg->workspace_icon_max) {
				int newmax = cfg->workspace_icon_max ? cfg->workspace_icon_max * 2 : 4;
				WorkspaceIcon *tmp = realloc(cfg->workspace_icons, newmax * sizeof *tmp);
				if (!tmp) {
					fprintf(stderr, "sxbarc: out of memory\n");
					fclose(f);
					return -1;
				}
				cfg->workspace_icons = tmp;
				cfg->workspace_icon_max = newmax;
			}
			cfg->workspace_icons[cfg->workspace_icon_count].name = strdup(name);
			cfg->workspace_icons[cfg->workspace_icon_count].icon = strdup(text_start);
			cfg->workspace_icon_count++;
		} else if (!strcmp(key, "module")) {
			/* module : name : enabled : interval
			 *
			 * There's no separate "custom module" directive -- any name
			 * works here. If a module by this name doesn't exist yet, one
			 * is created on the spot by resolving scripts/<name>.sh
			 * (resolve_script(), below): the user's own copy at
			 * ~/.config/sxbar/scripts/<name>.sh wins over the installed
			 * reference copy at $PREFIX/share/sxbar/scripts/<name>.sh, or
			 * a harmless no-op if neither exists yet. A module the user
			 * wrote themselves is exactly as first-class as one sxbar
			 * ships with -- both are just a script in scripts/, and both
			 * can self-declare a popup via a `menu` subcommand
			 * (load_popup_from_script(), below). */
			char *p1 = strchr(rest, ':');
			if (!p1) {
				fprintf(stderr, "sxbarc:%d: module missing fields\n", lineno);
				continue;
			}
			*p1 = '\0';
			char *name     = strip(rest);
			char *rest2    = strip(p1 + 1);
			char *p2       = strchr(rest2, ':');
			char *enabled_s, *interval_s = NULL;
			if (p2) {
				*p2 = '\0';
				enabled_s  = strip(rest2);
				interval_s = strip(p2 + 1);
			} else {
				enabled_s = strip(rest2);
			}
			Module *m = find_module(cfg, name);
			if (!m) {
				if (grow_modules(cfg) < 0) {
					fprintf(stderr, "sxbarc: out of memory\n");
					fclose(f);
					return -1;
				}
				m = &cfg->modules[cfg->module_count++];
				memset(m, 0, sizeof *m);
				m->name             = strdup(name);
				m->command          = resolve_script(name);
				m->refresh_interval = 5;
				m->slider_item_idx  = -1;
				load_popup_from_script(m, m->command);
			}
			m->enabled = parse_bool(enabled_s);
			if (interval_s && *interval_s)
				m->refresh_interval = atoi(interval_s);
		} else if (!strcmp(key, "colour") || !strcmp(key, "color")) {
			/* colour : module_name : #hex */
			char *p1 = strchr(rest, ':');
			if (!p1) {
				fprintf(stderr, "sxbarc:%d: colour missing name and value\n", lineno);
				continue;
			}
			*p1 = '\0';
			char *name = strip(rest);
			char *val  = strip(p1 + 1);
			strip_comment(val);
			Module *m = find_module(cfg, name);
			if (!m) {
				fprintf(stderr, "sxbarc:%d: colour: unknown module '%s'\n", lineno, name);
				continue;
			}
			free(m->colour);
			m->colour = strdup(val);
		} else if (!strcmp(key, "width")) {
			/* width : module_name : min_pixels -- reserves at least this much
			 * horizontal space for the module so its neighbours don't shift
			 * when its text width changes (e.g. cpu going from 9% to 16%) */
			char *p1 = strchr(rest, ':');
			if (!p1) {
				fprintf(stderr, "sxbarc:%d: width missing name and value\n", lineno);
				continue;
			}
			*p1 = '\0';
			char *name = strip(rest);
			char *val  = strip(p1 + 1);
			strip_comment(val);
			Module *m = find_module(cfg, name);
			if (!m) {
				fprintf(stderr, "sxbarc:%d: width: unknown module '%s'\n", lineno, name);
				continue;
			}
			m->min_width = atoi(val);
		} else if (!strcmp(key, "max_width")) {
			/* max_width : module_name : max_pixels -- caps the module's
			 * rendered text at this width; text wider than it scrolls
			 * (marquee) left instead of stretching the bar */
			char *p1 = strchr(rest, ':');
			if (!p1) {
				fprintf(stderr, "sxbarc:%d: max_width missing name and value\n", lineno);
				continue;
			}
			*p1 = '\0';
			char *name = strip(rest);
			char *val  = strip(p1 + 1);
			strip_comment(val);
			Module *m = find_module(cfg, name);
			if (!m) {
				fprintf(stderr, "sxbarc:%d: max_width: unknown module '%s'\n", lineno, name);
				continue;
			}
			m->max_width = atoi(val);
		} else if (!strcmp(key, "bar")) {
			/* bar : module_name : primary|secondary -- which bar the module
			 * is drawn on when secondary_bar is enabled (default: primary) */
			char *p1 = strchr(rest, ':');
			if (!p1) {
				fprintf(stderr, "sxbarc:%d: bar missing name and value\n", lineno);
				continue;
			}
			*p1 = '\0';
			char *name = strip(rest);
			char *val  = strip(p1 + 1);
			strip_comment(val);
			Module *m = find_module(cfg, name);
			if (!m) {
				fprintf(stderr, "sxbarc:%d: bar: unknown module '%s'\n", lineno, name);
				continue;
			}
			if (!strcmp(val, "secondary")) {
				m->on_secondary = 1;
			} else if (!strcmp(val, "primary")) {
				m->on_secondary = 0;
			} else {
				fprintf(stderr, "sxbarc:%d: bar: value must be 'primary' or 'secondary', got '%s'\n",
				        lineno, val);
			}
		} else if (!strcmp(key, "align")) {
			/* align : module_name : left|center|right -- which side of the
			 * bar the module is anchored to (default: right) */
			char *p1 = strchr(rest, ':');
			if (!p1) {
				fprintf(stderr, "sxbarc:%d: align missing name and value\n", lineno);
				continue;
			}
			*p1 = '\0';
			char *name = strip(rest);
			char *val  = strip(p1 + 1);
			strip_comment(val);
			Module *m = find_module(cfg, name);
			if (!m) {
				fprintf(stderr, "sxbarc:%d: align: unknown module '%s'\n", lineno, name);
				continue;
			}
			if (!strcmp(val, "left")) {
				m->align = ALIGN_LEFT;
			} else if (!strcmp(val, "center") || !strcmp(val, "centre")) {
				m->align = ALIGN_CENTER;
			} else if (!strcmp(val, "right")) {
				m->align = ALIGN_RIGHT;
			} else {
				fprintf(stderr, "sxbarc:%d: align: value must be 'left', 'center' or 'right', got '%s'\n",
				        lineno, val);
			}
		} else if (!strcmp(key, "icon_only")) {
			/* icon_only : module_name : true|false -- show only the prefix/icon,
			 * never the module's own text (default: false) */
			char *p1 = strchr(rest, ':');
			if (!p1) {
				fprintf(stderr, "sxbarc:%d: icon_only missing name and value\n", lineno);
				continue;
			}
			*p1 = '\0';
			char *name = strip(rest);
			char *val  = strip(p1 + 1);
			strip_comment(val);
			Module *m = find_module(cfg, name);
			if (!m) {
				fprintf(stderr, "sxbarc:%d: icon_only: unknown module '%s'\n", lineno, name);
				continue;
			}
			m->icon_only = parse_bool(val);
		} else if (!strcmp(key, "popup")) {
			/* popup : module_name : hover|click : buttons|slider */
			char *p1 = strchr(rest, ':');
			if (!p1) {
				fprintf(stderr, "sxbarc:%d: popup missing fields\n", lineno);
				continue;
			}
			*p1 = '\0';
			char *name  = strip(rest);
			char *rest2 = strip(p1 + 1);
			Module *m = find_module(cfg, name);
			if (!m) {
				fprintf(stderr, "sxbarc:%d: popup: unknown module '%s'\n", lineno, name);
				continue;
			}
			apply_popup(m, rest2, "sxbarc", lineno);
		} else if (!strcmp(key, "popup_item")) {
			/* popup_item : module_name : "Label" : "command" -- one row of a
			 * POPUP_BUTTONS popup. The first popup_item line for a module
			 * replaces its built-in default rows; later lines append. */
			char *p1 = strchr(rest, ':');
			if (!p1) {
				fprintf(stderr, "sxbarc:%d: popup_item missing fields\n", lineno);
				continue;
			}
			*p1 = '\0';
			char *name  = strip(rest);
			char *after = strip(p1 + 1);
			Module *m = find_module(cfg, name);
			if (!m) {
				fprintf(stderr, "sxbarc:%d: popup_item: unknown module '%s'\n", lineno, name);
				continue;
			}
			apply_popup_item(m, after, "sxbarc", lineno, 1);
		} else if (!strcmp(key, "popup_live_item")) {
			/* popup_live_item : module_name : "label command" : "click
			 * command" -- a BUTTON row whose label is re-run fresh every
			 * time the popup opens (like popup_info), but that still
			 * spawns its own click command (like popup_item). */
			char *p1 = strchr(rest, ':');
			if (!p1) {
				fprintf(stderr, "sxbarc:%d: popup_live_item missing fields\n", lineno);
				continue;
			}
			*p1 = '\0';
			char *name  = strip(rest);
			char *after = strip(p1 + 1);
			Module *m = find_module(cfg, name);
			if (!m) {
				fprintf(stderr, "sxbarc:%d: popup_live_item: unknown module '%s'\n", lineno, name);
				continue;
			}
			apply_popup_live_item(m, after, "sxbarc", lineno, 1);
		} else if (!strcmp(key, "popup_info")) {
			/* popup_info : module_name : "shell command" -- one purely
			 * informational row: its label is this command's output,
			 * re-run fresh every time the popup opens. Not clickable at
			 * all -- clicking it does nothing, the popup stays open. */
			char *p1 = strchr(rest, ':');
			if (!p1) {
				fprintf(stderr, "sxbarc:%d: popup_info missing name and command\n", lineno);
				continue;
			}
			*p1 = '\0';
			char *name  = strip(rest);
			char *after = strip(p1 + 1);
			Module *m = find_module(cfg, name);
			if (!m) {
				fprintf(stderr, "sxbarc:%d: popup_info: unknown module '%s'\n", lineno, name);
				continue;
			}
			apply_popup_info(m, after, "sxbarc", lineno, 1);
		} else if (!strcmp(key, "popup_image")) {
			/* popup_image : module_name : "shell command" -- one image row:
			 * the command's stdout is a path to a local image file (e.g.
			 * album art), re-run fresh every time the popup opens. Not
			 * clickable, same as popup_info. */
			char *p1 = strchr(rest, ':');
			if (!p1) {
				fprintf(stderr, "sxbarc:%d: popup_image missing name and command\n", lineno);
				continue;
			}
			*p1 = '\0';
			char *name  = strip(rest);
			char *after = strip(p1 + 1);
			Module *m = find_module(cfg, name);
			if (!m) {
				fprintf(stderr, "sxbarc:%d: popup_image: unknown module '%s'\n", lineno, name);
				continue;
			}
			apply_popup_image(m, after, "sxbarc", lineno, 1);
		} else if (!strcmp(key, "popup_image_size")) {
			/* popup_image_size : module_name : pixels -- caps the square box
			 * this module's popup_image rows scale their art into (default:
			 * the built-in POPUP_IMAGE_SIZE, currently 160) */
			char *p1 = strchr(rest, ':');
			if (!p1) {
				fprintf(stderr, "sxbarc:%d: popup_image_size missing name and value\n", lineno);
				continue;
			}
			*p1 = '\0';
			char *name = strip(rest);
			char *val  = strip(p1 + 1);
			Module *m = find_module(cfg, name);
			if (!m) {
				fprintf(stderr, "sxbarc:%d: popup_image_size: unknown module '%s'\n", lineno, name);
				continue;
			}
			apply_popup_image_size(m, val, "sxbarc", lineno);
		} else if (!strcmp(key, "popup_buttons")) {
			/* popup_buttons : module_name : "Label1" : "command1" : "Label2" :
			 * "command2" ... -- one row split into N equal-width button
			 * segments side by side (e.g. media transport controls), instead
			 * of N separate full-width popup_item rows. */
			char *p1 = strchr(rest, ':');
			if (!p1) {
				fprintf(stderr, "sxbarc:%d: popup_buttons missing name and buttons\n", lineno);
				continue;
			}
			*p1 = '\0';
			char *name  = strip(rest);
			char *after = strip(p1 + 1);
			Module *m = find_module(cfg, name);
			if (!m) {
				fprintf(stderr, "sxbarc:%d: popup_buttons: unknown module '%s'\n", lineno, name);
				continue;
			}
			apply_popup_buttons(m, after, "sxbarc", lineno, 1);
		} else if (!strcmp(key, "popup_set")) {
			/* popup_set : module_name : "command" -- adds (or updates) one
			 * slider row for this module; receives the new value (0-100)
			 * as $1, same convention as prefix_cmd. Can coexist with
			 * popup_item/popup_info rows on the same module -- text,
			 * button and slider rows all mix freely in one popup. */
			char *p1 = strchr(rest, ':');
			if (!p1) {
				fprintf(stderr, "sxbarc:%d: popup_set missing name and command\n", lineno);
				continue;
			}
			*p1 = '\0';
			char *name  = strip(rest);
			char *after = strip(p1 + 1);
			Module *m = find_module(cfg, name);
			if (!m) {
				fprintf(stderr, "sxbarc:%d: popup_set: unknown module '%s'\n", lineno, name);
				continue;
			}
			apply_popup_set(m, after, "sxbarc", lineno);
		} else if (!strcmp(key, "click") || !strcmp(key, "scroll_up") || !strcmp(key, "scroll_down") ||
		           !strcmp(key, "prefix_cmd") || !strcmp(key, "icon_cmd")) {
			/* click/scroll_up/scroll_down/prefix_cmd/icon_cmd : module_name : "command" */
			char *p1 = strchr(rest, ':');
			if (!p1) {
				fprintf(stderr, "sxbarc:%d: %s missing name and command\n", lineno, key);
				continue;
			}
			*p1 = '\0';
			char *name  = strip(rest);
			char *after = strip(p1 + 1);

			if (*after != '"' && *after != '\'') {
				fprintf(stderr, "sxbarc:%d: %s command must be quoted\n", lineno, key);
				continue;
			}
			char q = *after;
			char *cmd_start = after + 1;
			char *closing   = strchr(cmd_start, q);
			if (!closing) {
				fprintf(stderr, "sxbarc:%d: %s command missing closing quote\n", lineno, key);
				continue;
			}
			*closing = '\0';

			Module *m = find_module(cfg, name);
			if (!m) {
				fprintf(stderr, "sxbarc:%d: %s: unknown module '%s'\n", lineno, key, name);
				continue;
			}
			if (!strcmp(key, "click")) {
				free(m->click_command);
				m->click_command = expand_home(cmd_start);
			} else if (!strcmp(key, "scroll_up")) {
				free(m->scroll_up_command);
				m->scroll_up_command = expand_home(cmd_start);
			} else if (!strcmp(key, "scroll_down")) {
				free(m->scroll_down_command);
				m->scroll_down_command = expand_home(cmd_start);
			} else {
				free(m->prefix_command);
				m->prefix_command = expand_home(cmd_start);
			}
		} else if (!strcmp(key, "prefix") || !strcmp(key, "icon")) {
			/* prefix/icon : module_name : "text" -- prepended to the module's output, e.g. a Nerd Font glyph */
			char *p1 = strchr(rest, ':');
			if (!p1) {
				fprintf(stderr, "sxbarc:%d: %s missing name and text\n", lineno, key);
				continue;
			}
			*p1 = '\0';
			char *name  = strip(rest);
			char *after = strip(p1 + 1);

			if (*after != '"' && *after != '\'') {
				fprintf(stderr, "sxbarc:%d: %s text must be quoted\n", lineno, key);
				continue;
			}
			char q = *after;
			char *text_start = after + 1;
			char *closing     = strchr(text_start, q);
			if (!closing) {
				fprintf(stderr, "sxbarc:%d: %s text missing closing quote\n", lineno, key);
				continue;
			}
			*closing = '\0';

			Module *m = find_module(cfg, name);
			if (!m) {
				fprintf(stderr, "sxbarc:%d: %s: unknown module '%s'\n", lineno, key, name);
				continue;
			}
			free(m->prefix);
			m->prefix = strdup(text_start);
		} else {
			fprintf(stderr, "sxbarc:%d: unknown option '%s'\n", lineno, key);
		}
	}

	fclose(f);
	return 0;
}
