2026-11-08 14:46:04 +01:00
|
|
|
#include "config.h"
|
|
|
|
|
|
|
|
|
|
#include <stdio.h>
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
#include <strings.h>
|
|
|
|
|
#include <sys/stat.h>
|
|
|
|
|
#include <sys/types.h>
|
|
|
|
|
#include <unistd.h>
|
|
|
|
|
#include <libgen.h>
|
|
|
|
|
|
|
|
|
|
#include "toml.h"
|
|
|
|
|
|
|
|
|
|
static char *trim(char *s) {
|
|
|
|
|
while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++;
|
|
|
|
|
char *end = s + strlen(s);
|
|
|
|
|
while (end > s && (end[-1] == ' ' || end[-1] == '\t' || end[-1] == '\n' || end[-1] == '\r')) end--;
|
|
|
|
|
*end = '\0';
|
|
|
|
|
return s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void config_ensure_capacity(config_t *cfg) {
|
|
|
|
|
if (cfg->count < cfg->capacity) return;
|
|
|
|
|
int newcap = cfg->capacity ? cfg->capacity * 2 : 16;
|
|
|
|
|
binding_t *nb = realloc(cfg->items, newcap * sizeof(binding_t));
|
|
|
|
|
if (!nb) return;
|
|
|
|
|
cfg->items = nb;
|
|
|
|
|
cfg->capacity = newcap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void config_set_binding(config_t *cfg, uint32_t mods, xkb_keysym_t keysym, action_t action) {
|
|
|
|
|
for (int i = 0; i < cfg->count; i++) {
|
|
|
|
|
if (cfg->items[i].mods == mods && cfg->items[i].keysym == keysym) {
|
|
|
|
|
cfg->items[i].action = action;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
config_ensure_capacity(cfg);
|
|
|
|
|
if (cfg->count >= cfg->capacity) return;
|
|
|
|
|
cfg->items[cfg->count].mods = mods;
|
|
|
|
|
cfg->items[cfg->count].keysym = keysym;
|
|
|
|
|
cfg->items[cfg->count].action = action;
|
|
|
|
|
cfg->count++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int config_parse_mods(const char *s, uint32_t *out_mods, char **out_key) {
|
|
|
|
|
if (!s || !*s || !out_mods || !out_key) return -1;
|
|
|
|
|
char *copy = strdup(s);
|
|
|
|
|
if (!copy) return -1;
|
|
|
|
|
/* count tokens */
|
|
|
|
|
int n = 0;
|
|
|
|
|
char *p = copy;
|
|
|
|
|
/* first pass count */
|
|
|
|
|
char *tmp = strdup(s);
|
|
|
|
|
char *save = NULL;
|
|
|
|
|
char *t = strtok_r(tmp, "+", &save);
|
|
|
|
|
/* collect */
|
|
|
|
|
char **toks = NULL;
|
|
|
|
|
int cap = 0;
|
|
|
|
|
while (t) {
|
|
|
|
|
char *tt = trim(t);
|
|
|
|
|
if (*tt) {
|
2026-11-08 15:47:46 +01:00
|
|
|
char *alias = tt;
|
|
|
|
|
if (strcmp(tt, "+") == 0) alias = "plus";
|
|
|
|
|
else if (strcmp(tt, "-") == 0) alias = "minus";
|
2026-11-08 14:46:04 +01:00
|
|
|
if (n == cap) {
|
|
|
|
|
cap = cap ? cap * 2 : 4;
|
|
|
|
|
toks = realloc(toks, cap * sizeof(char *));
|
|
|
|
|
}
|
2026-11-08 15:47:46 +01:00
|
|
|
toks[n++] = strdup(alias);
|
2026-11-08 14:46:04 +01:00
|
|
|
}
|
|
|
|
|
t = strtok_r(NULL, "+", &save);
|
|
|
|
|
}
|
|
|
|
|
free(tmp);
|
|
|
|
|
if (n == 0) { free(copy); free(toks); return -1; }
|
|
|
|
|
|
|
|
|
|
uint32_t mods = 0;
|
|
|
|
|
for (int i = 0; i < n - 1; i++) {
|
|
|
|
|
if (strcasecmp(toks[i], "ctrl") == 0 || strcasecmp(toks[i], "control") == 0) mods |= MOD_CTRL;
|
|
|
|
|
else if (strcasecmp(toks[i], "shift") == 0) mods |= MOD_SHIFT;
|
|
|
|
|
else if (strcasecmp(toks[i], "alt") == 0) mods |= MOD_ALT;
|
|
|
|
|
else if (strcasecmp(toks[i], "super") == 0 || strcasecmp(toks[i], "meta") == 0 || strcasecmp(toks[i], "win") == 0) mods |= MOD_SUPER;
|
|
|
|
|
else {
|
|
|
|
|
/* unknown modifier -> fail */
|
|
|
|
|
for (int j = 0; j < n; j++) free(toks[j]);
|
|
|
|
|
free(toks);
|
|
|
|
|
free(copy);
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
free(toks[i]);
|
|
|
|
|
}
|
|
|
|
|
*out_mods = mods;
|
|
|
|
|
*out_key = toks[n - 1];
|
|
|
|
|
/* free remaining token strings except key (transferred) */
|
|
|
|
|
for (int i = 0; i < n - 1; i++) { /* already freed */ }
|
|
|
|
|
free(toks);
|
|
|
|
|
free(copy);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
action_t config_parse_action(const char *s) {
|
|
|
|
|
if (!s) return ACTION_NONE;
|
|
|
|
|
char buf[128];
|
|
|
|
|
snprintf(buf, sizeof(buf), "%s", s);
|
|
|
|
|
trim(buf);
|
|
|
|
|
if (strcmp(buf, "quit") == 0) return ACTION_QUIT;
|
|
|
|
|
if (strcmp(buf, "cancel") == 0) return ACTION_CANCEL;
|
|
|
|
|
if (strcmp(buf, "rotate") == 0) return ACTION_ROTATE;
|
|
|
|
|
if (strcmp(buf, "clear") == 0) return ACTION_CLEAR;
|
|
|
|
|
if (strcmp(buf, "pause") == 0) return ACTION_PAUSE;
|
|
|
|
|
if (strcmp(buf, "step") == 0) return ACTION_STEP;
|
|
|
|
|
if (strcmp(buf, "speed_up") == 0) return ACTION_SPEED_UP;
|
|
|
|
|
if (strcmp(buf, "speed_down") == 0) return ACTION_SPEED_DOWN;
|
|
|
|
|
if (strcmp(buf, "toggle_grid") == 0) return ACTION_TOGGLE_GRID;
|
|
|
|
|
if (strncmp(buf, "select:", 7) == 0) {
|
|
|
|
|
const char *pat = buf + 7;
|
|
|
|
|
if (strcmp(pat, "glider") == 0) return ACTION_SELECT_GLIDER;
|
|
|
|
|
if (strcmp(pat, "blinker") == 0) return ACTION_SELECT_BLINKER;
|
|
|
|
|
if (strcmp(pat, "block") == 0) return ACTION_SELECT_BLOCK;
|
|
|
|
|
if (strcmp(pat, "toad") == 0) return ACTION_SELECT_TOAD;
|
|
|
|
|
}
|
|
|
|
|
return ACTION_NONE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
config_t config_defaults(void) {
|
|
|
|
|
config_t cfg = {0};
|
|
|
|
|
struct { const char *key; action_t action; } defs[] = {
|
|
|
|
|
{"1", ACTION_SELECT_GLIDER},
|
|
|
|
|
{"2", ACTION_SELECT_BLINKER},
|
|
|
|
|
{"3", ACTION_SELECT_BLOCK},
|
|
|
|
|
{"4", ACTION_SELECT_TOAD},
|
|
|
|
|
{"space", ACTION_ROTATE},
|
|
|
|
|
{"escape", ACTION_CANCEL},
|
|
|
|
|
{"ctrl+q", ACTION_QUIT},
|
2026-11-08 15:47:46 +01:00
|
|
|
{"p", ACTION_PAUSE},
|
|
|
|
|
{"n", ACTION_STEP},
|
|
|
|
|
{"+", ACTION_SPEED_UP},
|
|
|
|
|
{"-", ACTION_SPEED_DOWN},
|
2026-11-08 14:46:04 +01:00
|
|
|
};
|
|
|
|
|
int ndef = sizeof(defs) / sizeof(defs[0]);
|
|
|
|
|
for (int i = 0; i < ndef; i++) {
|
|
|
|
|
uint32_t mods = 0;
|
|
|
|
|
char *key = NULL;
|
|
|
|
|
if (config_parse_mods(defs[i].key, &mods, &key) != 0) continue;
|
|
|
|
|
xkb_keysym_t sym = xkb_keysym_from_name(key, XKB_KEYSYM_CASE_INSENSITIVE);
|
|
|
|
|
free(key);
|
|
|
|
|
if (sym == XKB_KEY_NoSymbol) continue;
|
|
|
|
|
config_set_binding(&cfg, mods, sym, defs[i].action);
|
|
|
|
|
}
|
|
|
|
|
return cfg;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const char *default_file_content =
|
|
|
|
|
"# wlgol key bindings\n"
|
|
|
|
|
"[bindings]\n"
|
|
|
|
|
"\"1\" = \"select:glider\"\n"
|
|
|
|
|
"\"2\" = \"select:blinker\"\n"
|
|
|
|
|
"\"3\" = \"select:block\"\n"
|
|
|
|
|
"\"4\" = \"select:toad\"\n"
|
|
|
|
|
"\"space\" = \"rotate\"\n"
|
|
|
|
|
"\"escape\" = \"cancel\"\n"
|
2026-11-08 15:47:46 +01:00
|
|
|
"\"ctrl+q\" = \"quit\"\n"
|
|
|
|
|
"\"p\" = \"pause\"\n"
|
|
|
|
|
"\"n\" = \"step\"\n"
|
|
|
|
|
"\"+\" = \"speed_up\"\n"
|
|
|
|
|
"\"-\" = \"speed_down\"\n";
|
2026-11-08 14:46:04 +01:00
|
|
|
|
|
|
|
|
int config_ensure_default_file(const char *path) {
|
|
|
|
|
if (!path) return -1;
|
|
|
|
|
struct stat st;
|
|
|
|
|
if (stat(path, &st) == 0) return 0;
|
|
|
|
|
char *parent = strdup(path);
|
|
|
|
|
char *grand = strdup(path);
|
|
|
|
|
dirname(parent);
|
|
|
|
|
/* parent of parent */
|
|
|
|
|
char *p2 = strdup(parent);
|
|
|
|
|
dirname(p2);
|
|
|
|
|
mkdir(p2, 0755);
|
|
|
|
|
mkdir(parent, 0755);
|
|
|
|
|
free(p2);
|
|
|
|
|
|
|
|
|
|
FILE *fp = fopen(path, "w");
|
|
|
|
|
if (!fp) { free(parent); free(grand); return -1; }
|
|
|
|
|
fputs(default_file_content, fp);
|
|
|
|
|
fclose(fp);
|
|
|
|
|
free(parent);
|
|
|
|
|
free(grand);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
config_t config_load(const char *path) {
|
|
|
|
|
config_t cfg = config_defaults();
|
|
|
|
|
config_ensure_default_file(path);
|
|
|
|
|
FILE *fp = fopen(path, "r");
|
|
|
|
|
if (!fp) return cfg;
|
|
|
|
|
char errbuf[512];
|
|
|
|
|
toml_table_t *tab = toml_parse_file(fp, errbuf, sizeof(errbuf));
|
|
|
|
|
fclose(fp);
|
|
|
|
|
if (!tab) return cfg;
|
|
|
|
|
toml_table_t *bindings = toml_table_in(tab, "bindings");
|
|
|
|
|
if (bindings) {
|
|
|
|
|
int n = toml_table_nkval(bindings);
|
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
|
|
|
const char *keystr = toml_key_in(bindings, i);
|
|
|
|
|
if (!keystr) break;
|
|
|
|
|
toml_datum_t d = toml_string_in(bindings, keystr);
|
|
|
|
|
if (!d.ok) { if (d.u.s) free(d.u.s); continue; }
|
|
|
|
|
uint32_t mods = 0;
|
|
|
|
|
char *key = NULL;
|
|
|
|
|
if (config_parse_mods(keystr, &mods, &key) != 0) { free(d.u.s); continue; }
|
|
|
|
|
xkb_keysym_t sym = xkb_keysym_from_name(key, XKB_KEYSYM_CASE_INSENSITIVE);
|
|
|
|
|
free(key);
|
|
|
|
|
if (sym == XKB_KEY_NoSymbol) { free(d.u.s); continue; }
|
|
|
|
|
action_t act = config_parse_action(d.u.s);
|
|
|
|
|
free(d.u.s);
|
|
|
|
|
if (act == ACTION_NONE) continue;
|
|
|
|
|
config_set_binding(&cfg, mods, sym, act);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
toml_free(tab);
|
|
|
|
|
return cfg;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void config_free(config_t *cfg) {
|
|
|
|
|
if (!cfg) return;
|
|
|
|
|
free(cfg->items);
|
|
|
|
|
cfg->items = NULL;
|
|
|
|
|
cfg->count = 0;
|
|
|
|
|
cfg->capacity = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
action_t config_lookup(const config_t *cfg, uint32_t mods, xkb_keysym_t keysym) {
|
|
|
|
|
if (!cfg) return ACTION_NONE;
|
|
|
|
|
for (int i = 0; i < cfg->count; i++) {
|
|
|
|
|
if (cfg->items[i].mods == mods && cfg->items[i].keysym == keysym) {
|
|
|
|
|
return cfg->items[i].action;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ACTION_NONE;
|
|
|
|
|
}
|