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