aboutsummaryrefslogtreecommitdiff
path: root/config.c
diff options
context:
space:
mode:
Diffstat (limited to 'config.c')
-rw-r--r--config.c273
1 files changed, 273 insertions, 0 deletions
diff --git a/config.c b/config.c
new file mode 100644
index 0000000..4b930df
--- /dev/null
+++ b/config.c
@@ -0,0 +1,273 @@
+/* config.c - defaults, config file parsing, and shared key/value sets. */
+#include "config.h"
+
+#include <ctype.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+void config_defaults(struct config *c)
+{
+ /* Home row first, then the rest of the top/bottom rows: common cells
+ * get the easiest two-key labels. */
+ strcpy(c->alphabet, "fjdkslaghrueiwoqptyvbcnxmz");
+ c->cell_size = 100;
+ c->cols = 0;
+ c->rows = 0;
+ c->font_scale = 0;
+
+ c->opacity_bg = 33; /* how dark the screen goes (100 = solid) */
+ c->opacity_fg = 65; /* labels, grid and crosshair */
+
+ /* Full-alpha colours (grid deliberately fainter); opacity_bg/fg
+ * scale them once at startup. */
+ c->color_bg = 0xff101014; /* near-black backdrop */
+ c->color_grid = 0x40ffffff; /* faint white grid lines */
+ c->color_label = 0xffffffff; /* white labels */
+ c->color_typed = 0xff52a8ff; /* blue for the typed prefix */
+ c->color_active = 0xffff5252; /* red refine box + crosshair */
+
+ c->k_tl = 'u';
+ c->k_tr = 'i';
+ c->k_bl = 'j';
+ c->k_br = 'k';
+ c->k_left = 'm';
+ c->k_middle = ',';
+ c->k_right = '.';
+ c->k_double = ';';
+ c->k_drag = 'v';
+}
+
+static int parse_color(const char *s, uint32_t *out)
+{
+ if (*s == '#')
+ s++;
+ size_t n = strlen(s);
+ if (n != 6 && n != 8)
+ return -1;
+ for (size_t i = 0; i < n; i++)
+ if (!isxdigit((unsigned char)s[i]))
+ return -1;
+
+ unsigned long v = strtoul(s, NULL, 16);
+ uint32_t r, g, b, a;
+ if (n == 6) {
+ r = (v >> 16) & 0xff;
+ g = (v >> 8) & 0xff;
+ b = v & 0xff;
+ a = 0xff;
+ } else {
+ r = (v >> 24) & 0xff;
+ g = (v >> 16) & 0xff;
+ b = (v >> 8) & 0xff;
+ a = v & 0xff;
+ }
+ *out = (a << 24) | (r << 16) | (g << 8) | b;
+ return 0;
+}
+
+static int parse_int(const char *s, int *out)
+{
+ char *end;
+ errno = 0;
+ long v = strtol(s, &end, 10);
+ if (errno || end == s || *end || v < 0 || v > 100000)
+ return -1;
+ *out = (int)v;
+ return 0;
+}
+
+/* A character the user can actually type at the grid: printable ASCII, no
+ * space. Uppercase is rejected because incoming keys are lowercased (so Caps
+ * Lock cannot break labels) - an uppercase binding would never fire. */
+static int typeable(unsigned char ch)
+{
+ return ch > 0x20 && ch < 0x7f && !(ch >= 'A' && ch <= 'Z');
+}
+
+int config_set(struct config *c, const char *key, const char *val)
+{
+ if (!strcmp(key, "alphabet")) {
+ if (strlen(val) < 2 || strlen(val) >= sizeof c->alphabet)
+ return -2;
+ for (size_t i = 0; val[i]; i++) {
+ if (!typeable((unsigned char)val[i]))
+ return -2;
+ if (strchr(val + i + 1, val[i]))
+ return -2; /* duplicate char breaks labels */
+ }
+ strcpy(c->alphabet, val);
+ return 0;
+ }
+
+ if (!strcmp(key, "opacity_bg")) {
+ int v;
+ if (parse_int(val, &v) || v > 100)
+ return -2;
+ c->opacity_bg = v;
+ return 0;
+ }
+ if (!strcmp(key, "opacity_fg")) {
+ int v;
+ if (parse_int(val, &v) || v > 100)
+ return -2;
+ c->opacity_fg = v;
+ return 0;
+ }
+
+ if (!strcmp(key, "cell_size"))
+ return parse_int(val, &c->cell_size) ? -2 : 0;
+ if (!strcmp(key, "cols"))
+ return parse_int(val, &c->cols) ? -2 : 0;
+ if (!strcmp(key, "rows"))
+ return parse_int(val, &c->rows) ? -2 : 0;
+ if (!strcmp(key, "font_scale"))
+ return parse_int(val, &c->font_scale) ? -2 : 0;
+
+ if (!strcmp(key, "color_bg"))
+ return parse_color(val, &c->color_bg) ? -2 : 0;
+ if (!strcmp(key, "color_grid"))
+ return parse_color(val, &c->color_grid) ? -2 : 0;
+ if (!strcmp(key, "color_label"))
+ return parse_color(val, &c->color_label) ? -2 : 0;
+ if (!strcmp(key, "color_typed"))
+ return parse_color(val, &c->color_typed) ? -2 : 0;
+ if (!strcmp(key, "color_active"))
+ return parse_color(val, &c->color_active) ? -2 : 0;
+
+ /* Single-character key bindings. */
+ struct { const char *name; char *slot; } keys[] = {
+ { "refine_tl", &c->k_tl }, { "refine_tr", &c->k_tr },
+ { "refine_bl", &c->k_bl }, { "refine_br", &c->k_br },
+ { "click_left", &c->k_left }, { "click_middle", &c->k_middle },
+ { "click_right", &c->k_right }, { "click_double", &c->k_double },
+ { "drag", &c->k_drag },
+ };
+ for (size_t i = 0; i < sizeof keys / sizeof keys[0]; i++) {
+ if (strcmp(key, keys[i].name))
+ continue;
+ if (strlen(val) != 1 || !typeable((unsigned char)val[0]))
+ return -2;
+ *keys[i].slot = val[0];
+ return 0;
+ }
+
+ return -1;
+}
+
+const char *config_validate(const struct config *c)
+{
+ if (c->cell_size < 1)
+ return "cell_size must be greater than zero";
+ if ((c->cols > 0) != (c->rows > 0))
+ return "cols and rows must be set together";
+ if (c->cols > MB_MAX_GRID_DIM || c->rows > MB_MAX_GRID_DIM)
+ return "cols and rows must not exceed 200";
+ if (c->font_scale > MB_MAX_FONT_SCALE)
+ return "font_scale must not exceed 64";
+
+ const char keys[] = {
+ c->k_tl, c->k_tr, c->k_bl, c->k_br, c->k_left,
+ c->k_middle, c->k_right, c->k_double, c->k_drag,
+ };
+ for (size_t i = 0; i < sizeof keys; i++)
+ for (size_t j = i + 1; j < sizeof keys; j++)
+ if (keys[i] == keys[j])
+ return "refine and action keys must be unique";
+ return NULL;
+}
+
+void config_apply_opacity(struct config *c)
+{
+ uint32_t *fg[] = { &c->color_grid, &c->color_label,
+ &c->color_typed, &c->color_active };
+ uint32_t a = (c->color_bg >> 24) * (uint32_t)c->opacity_bg / 100;
+ c->color_bg = (a << 24) | (c->color_bg & 0x00ffffff);
+ for (size_t i = 0; i < sizeof fg / sizeof fg[0]; i++) {
+ a = (*fg[i] >> 24) * (uint32_t)c->opacity_fg / 100;
+ *fg[i] = (a << 24) | (*fg[i] & 0x00ffffff);
+ }
+}
+
+static char *trim(char *s)
+{
+ while (*s && isspace((unsigned char)*s))
+ s++;
+ char *e = s + strlen(s);
+ while (e > s && isspace((unsigned char)e[-1]))
+ *--e = 0;
+ return s;
+}
+
+int config_load_file(struct config *c, const char *path)
+{
+ FILE *f = fopen(path, "r");
+ if (!f) {
+ if (errno == ENOENT)
+ return -1;
+ fprintf(stderr, "%s: %s\n", path, strerror(errno));
+ return -2;
+ }
+
+ char line[256];
+ int lineno = 0, rc = 0;
+ while (fgets(line, sizeof line, f)) {
+ lineno++;
+ size_t len = strlen(line);
+ if (len && line[len - 1] != '\n' && !feof(f)) {
+ int ch;
+ while ((ch = fgetc(f)) != '\n' && ch != EOF)
+ ;
+ fprintf(stderr, "%s:%d: line too long\n", path, lineno);
+ rc = -2;
+ continue;
+ }
+ /* Comments are whole lines starting with '#'; this keeps '#'
+ * usable inside values (colours are #RRGGBB). */
+ char *body = trim(line);
+ if (!*body || *body == '#')
+ continue;
+ char *eq = strchr(body, '=');
+ if (!eq) {
+ fprintf(stderr, "%s:%d: missing '='\n", path, lineno);
+ rc = -2;
+ continue;
+ }
+ *eq = 0;
+ char *key = trim(body);
+ char *val = trim(eq + 1);
+ int r = config_set(c, key, val);
+ if (r == -1)
+ fprintf(stderr, "%s:%d: unknown key '%s'\n", path,
+ lineno, key);
+ else if (r == -2)
+ fprintf(stderr, "%s:%d: bad value for '%s'\n", path,
+ lineno, key);
+ if (r)
+ rc = -2;
+ }
+ fclose(f);
+ return rc;
+}
+
+const char *config_default_path(void)
+{
+ static char path[512];
+ const char *xdg = getenv("XDG_CONFIG_HOME");
+ if (xdg && *xdg) {
+ snprintf(path, sizeof path, "%s/mouseboard/config", xdg);
+ return path;
+ }
+ /* Windows sets APPDATA, not HOME; fopen accepts the mixed slashes. */
+ const char *appdata = getenv("APPDATA");
+ if (appdata && *appdata) {
+ snprintf(path, sizeof path, "%s/mouseboard/config", appdata);
+ return path;
+ }
+ const char *home = getenv("HOME");
+ if (!home)
+ home = "";
+ snprintf(path, sizeof path, "%s/.config/mouseboard/config", home);
+ return path;
+}