/* config.c - defaults, config file parsing, and shared key/value sets. */ #include "config.h" #include #include #include #include #include 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; }