1 #include "config.h"
2 
3 #include <stdlib.h>
4 #include <stdio.h>
5 #include <string.h>
6 #include <stdbool.h>
7 #include <ctype.h>
8 #include <unistd.h>
9 #include <errno.h>
10 #include <pwd.h>
11 #include <fcntl.h>
12 
13 #include <sys/types.h>
14 #include <sys/stat.h>
15 
16 #include <linux/input-event-codes.h>
17 #include <xkbcommon/xkbcommon.h>
18 #include <fontconfig/fontconfig.h>
19 
20 #define LOG_MODULE "config"
21 #define LOG_ENABLE_DBG 0
22 #include "log.h"
23 #include "debug.h"
24 #include "input.h"
25 #include "macros.h"
26 #include "tokenize.h"
27 #include "util.h"
28 #include "wayland.h"
29 #include "xmalloc.h"
30 #include "xsnprintf.h"
31 
32 static const uint32_t default_foreground = 0xdcdccc;
33 static const uint32_t default_background = 0x111111;
34 
35 #define cube6(r, g) \
36     r|g|0x00, r|g|0x5f, r|g|0x87, r|g|0xaf, r|g|0xd7, r|g|0xff
37 
38 #define cube36(r) \
39     cube6(r, 0x0000), \
40     cube6(r, 0x5f00), \
41     cube6(r, 0x8700), \
42     cube6(r, 0xaf00), \
43     cube6(r, 0xd700), \
44     cube6(r, 0xff00)
45 
46 static const uint32_t default_color_table[256] = {
47     // Regular
48     0x222222,
49     0xcc9393,
50     0x7f9f7f,
51     0xd0bf8f,
52     0x6ca0a3,
53     0xdc8cc3,
54     0x93e0e3,
55     0xdcdccc,
56 
57     // Bright
58     0x666666,
59     0xdca3a3,
60     0xbfebbf,
61     0xf0dfaf,
62     0x8cd0d3,
63     0xfcace3,
64     0xb3ffff,
65     0xffffff,
66 
67     // 6x6x6 RGB cube
68     // (color channels = i ? i*40+55 : 0, where i = 0..5)
69     cube36(0x000000),
70     cube36(0x5f0000),
71     cube36(0x870000),
72     cube36(0xaf0000),
73     cube36(0xd70000),
74     cube36(0xff0000),
75 
76     // 24 shades of gray
77     // (color channels = i*10+8, where i = 0..23)
78     0x080808, 0x121212, 0x1c1c1c, 0x262626,
79     0x303030, 0x3a3a3a, 0x444444, 0x4e4e4e,
80     0x585858, 0x626262, 0x6c6c6c, 0x767676,
81     0x808080, 0x8a8a8a, 0x949494, 0x9e9e9e,
82     0xa8a8a8, 0xb2b2b2, 0xbcbcbc, 0xc6c6c6,
83     0xd0d0d0, 0xdadada, 0xe4e4e4, 0xeeeeee
84 };
85 
86 static const char *const binding_action_map[] = {
87     [BIND_ACTION_NONE] = NULL,
88     [BIND_ACTION_NOOP] = "noop",
89     [BIND_ACTION_SCROLLBACK_UP_PAGE] = "scrollback-up-page",
90     [BIND_ACTION_SCROLLBACK_UP_HALF_PAGE] = "scrollback-up-half-page",
91     [BIND_ACTION_SCROLLBACK_UP_LINE] = "scrollback-up-line",
92     [BIND_ACTION_SCROLLBACK_DOWN_PAGE] = "scrollback-down-page",
93     [BIND_ACTION_SCROLLBACK_DOWN_HALF_PAGE] = "scrollback-down-half-page",
94     [BIND_ACTION_SCROLLBACK_DOWN_LINE] = "scrollback-down-line",
95     [BIND_ACTION_CLIPBOARD_COPY] = "clipboard-copy",
96     [BIND_ACTION_CLIPBOARD_PASTE] = "clipboard-paste",
97     [BIND_ACTION_PRIMARY_PASTE] = "primary-paste",
98     [BIND_ACTION_SEARCH_START] = "search-start",
99     [BIND_ACTION_FONT_SIZE_UP] = "font-increase",
100     [BIND_ACTION_FONT_SIZE_DOWN] = "font-decrease",
101     [BIND_ACTION_FONT_SIZE_RESET] = "font-reset",
102     [BIND_ACTION_SPAWN_TERMINAL] = "spawn-terminal",
103     [BIND_ACTION_MINIMIZE] = "minimize",
104     [BIND_ACTION_MAXIMIZE] = "maximize",
105     [BIND_ACTION_FULLSCREEN] = "fullscreen",
106     [BIND_ACTION_PIPE_SCROLLBACK] = "pipe-scrollback",
107     [BIND_ACTION_PIPE_VIEW] = "pipe-visible",
108     [BIND_ACTION_PIPE_SELECTED] = "pipe-selected",
109     [BIND_ACTION_SHOW_URLS_COPY] = "show-urls-copy",
110     [BIND_ACTION_SHOW_URLS_LAUNCH] = "show-urls-launch",
111 
112     /* Mouse-specific actions */
113     [BIND_ACTION_SELECT_BEGIN] = "select-begin",
114     [BIND_ACTION_SELECT_BEGIN_BLOCK] = "select-begin-block",
115     [BIND_ACTION_SELECT_EXTEND] = "select-extend",
116     [BIND_ACTION_SELECT_EXTEND_CHAR_WISE] = "select-extend-character-wise",
117     [BIND_ACTION_SELECT_WORD] = "select-word",
118     [BIND_ACTION_SELECT_WORD_WS] = "select-word-whitespace",
119     [BIND_ACTION_SELECT_ROW] = "select-row",
120 };
121 
122 static_assert(ALEN(binding_action_map) == BIND_ACTION_COUNT,
123               "binding action map size mismatch");
124 
125 struct context {
126     struct config *conf;
127     const char *section;
128     const char *key;
129     const char *value;
130 
131     const char *path;
132     unsigned lineno;
133 
134     bool errors_are_fatal;
135 };
136 
137 static const enum user_notification_kind log_class_to_notify_kind[LOG_CLASS_COUNT] = {
138     [LOG_CLASS_WARNING] = USER_NOTIFICATION_WARNING,
139     [LOG_CLASS_ERROR] = USER_NOTIFICATION_ERROR,
140 };
141 
142 static void NOINLINE VPRINTF(5)
log_and_notify_va(struct config * conf,enum log_class log_class,const char * file,int lineno,const char * fmt,va_list va)143 log_and_notify_va(struct config *conf, enum log_class log_class,
144                   const char *file, int lineno, const char *fmt, va_list va)
145 {
146     xassert(log_class < ALEN(log_class_to_notify_kind));
147     enum user_notification_kind kind = log_class_to_notify_kind[log_class];
148 
149     if (kind == 0) {
150         BUG("unsupported log class: %d", (int)log_class);
151         return;
152     }
153 
154     char *formatted_msg = xvasprintf(fmt, va);
155     log_msg(log_class, LOG_MODULE, file, lineno, "%s", formatted_msg);
156     user_notification_add(&conf->notifications, kind, formatted_msg);
157 }
158 
159 static void NOINLINE PRINTF(5)
log_and_notify(struct config * conf,enum log_class log_class,const char * file,int lineno,const char * fmt,...)160 log_and_notify(struct config *conf, enum log_class log_class,
161                const char *file, int lineno, const char *fmt, ...)
162 {
163     va_list va;
164     va_start(va, fmt);
165     log_and_notify_va(conf, log_class, file, lineno, fmt, va);
166     va_end(va);
167 }
168 
169 static void NOINLINE PRINTF(5)
log_contextual(struct context * ctx,enum log_class log_class,const char * file,int lineno,const char * fmt,...)170 log_contextual(struct context *ctx, enum log_class log_class,
171                const char *file, int lineno, const char *fmt, ...)
172 {
173     va_list va;
174     va_start(va, fmt);
175     char *formatted_msg = xvasprintf(fmt, va);
176     va_end(va);
177 
178     log_and_notify(
179         ctx->conf, log_class, file, lineno, "%s:%d: [%s].%s: %s: %s",
180         ctx->path, ctx->lineno, ctx->section, ctx->key, ctx->value,
181         formatted_msg);
182     free(formatted_msg);
183 }
184 
185 
186 static void NOINLINE VPRINTF(4)
log_and_notify_errno_va(struct config * conf,const char * file,int lineno,const char * fmt,va_list va)187 log_and_notify_errno_va(struct config *conf, const char *file, int lineno,
188                      const char *fmt, va_list va)
189 {
190     int errno_copy = errno;
191     char *formatted_msg = xvasprintf(fmt, va);
192     log_and_notify(
193         conf, LOG_CLASS_ERROR, file, lineno,
194         "%s: %s", formatted_msg, strerror(errno_copy));
195     free(formatted_msg);
196 }
197 
198 static void NOINLINE PRINTF(4)
log_and_notify_errno(struct config * conf,const char * file,int lineno,const char * fmt,...)199 log_and_notify_errno(struct config *conf, const char *file, int lineno,
200                      const char *fmt, ...)
201 {
202     va_list va;
203     va_start(va, fmt);
204     log_and_notify_errno_va(conf, file, lineno, fmt, va);
205     va_end(va);
206 }
207 
208 static void NOINLINE PRINTF(4)
log_contextual_errno(struct context * ctx,const char * file,int lineno,const char * fmt,...)209 log_contextual_errno(struct context *ctx, const char *file, int lineno,
210                      const char *fmt, ...)
211 {
212     va_list va;
213     va_start(va, fmt);
214     char *formatted_msg = xvasprintf(fmt, va);
215     va_end(va);
216 
217     log_and_notify_errno(
218         ctx->conf, file, lineno, "%s:%d: [%s].%s: %s: %s",
219         ctx->path, ctx->lineno, ctx->section, ctx->key, ctx->value,
220         formatted_msg);
221 
222     free(formatted_msg);
223 }
224 
225 #define LOG_CONTEXTUAL_ERR(...) \
226     log_contextual(ctx, LOG_CLASS_ERROR, __FILE__, __LINE__, __VA_ARGS__)
227 
228 #define LOG_CONTEXTUAL_WARN(...) \
229     log_contextual(ctx, LOG_CLASS_WARNING, __FILE__, __LINE__, __VA_ARGS__)
230 
231 #define LOG_CONTEXTUAL_ERRNO(...) \
232     log_contextual_errno(ctx, __FILE__, __LINE__, __VA_ARGS__)
233 
234 #define LOG_AND_NOTIFY_ERR(...) \
235     log_and_notify(conf, LOG_CLASS_ERROR, __FILE__, __LINE__, __VA_ARGS__)
236 
237 #define LOG_AND_NOTIFY_WARN(...) \
238     log_and_notify(conf, LOG_CLASS_WARNING, __FILE__, __LINE__, __VA_ARGS__)
239 
240 #define LOG_AND_NOTIFY_ERRNO(...) \
241     log_and_notify_errno(conf, __FILE__, __LINE__, __VA_ARGS__)
242 
243 static char *
get_shell(void)244 get_shell(void)
245 {
246     const char *shell = getenv("SHELL");
247 
248     if (shell == NULL) {
249         struct passwd *passwd = getpwuid(getuid());
250         if (passwd == NULL) {
251             LOG_ERRNO("failed to lookup user: falling back to 'sh'");
252             shell = "sh";
253         } else
254             shell = passwd->pw_shell;
255     }
256 
257     LOG_DBG("user's shell: %s", shell);
258     return xstrdup(shell);
259 }
260 
261 struct config_file {
262     char *path;       /* Full, absolute, path */
263     int fd;           /* FD of file, O_RDONLY */
264 };
265 
266 struct path_component {
267     const char *component;
268     int fd;
269 };
270 typedef tll(struct path_component) path_components_t;
271 
272 static void NOINLINE
path_component_add(path_components_t * components,const char * comp,int fd)273 path_component_add(path_components_t *components, const char *comp, int fd)
274 {
275     xassert(comp != NULL);
276     xassert(fd >= 0);
277 
278     struct path_component pc = {.component = comp, .fd = fd};
279     tll_push_back(*components, pc);
280 }
281 
282 static void NOINLINE
path_component_destroy(struct path_component * component)283 path_component_destroy(struct path_component *component)
284 {
285     xassert(component->fd >= 0);
286     close(component->fd);
287 }
288 
289 static void NOINLINE
path_components_destroy(path_components_t * components)290 path_components_destroy(path_components_t *components)
291 {
292     tll_foreach(*components, it) {
293         path_component_destroy(&it->item);
294         tll_remove(*components, it);
295     }
296 }
297 
298 static struct config_file
path_components_to_config_file(const path_components_t * components)299 path_components_to_config_file(const path_components_t *components)
300 {
301     if (tll_length(*components) == 0)
302         goto err;
303 
304     size_t len = 0;
305     tll_foreach(*components, it)
306         len += strlen(it->item.component) + 1;
307 
308     char *path = malloc(len);
309     if (path == NULL)
310         goto err;
311 
312     size_t idx = 0;
313     tll_foreach(*components, it) {
314         strcpy(&path[idx], it->item.component);
315         idx += strlen(it->item.component);
316         path[idx++] = '/';
317     }
318     path[idx - 1] = '\0';  /* Strip last ’/’ */
319 
320     int fd_copy = dup(tll_back(*components).fd);
321     if (fd_copy < 0) {
322         free(path);
323         goto err;
324     }
325 
326     return (struct config_file){.path = path, .fd = fd_copy};
327 
328 err:
329     return (struct config_file){.path = NULL, .fd = -1};
330 }
331 
332 static const char *
get_user_home_dir(void)333 get_user_home_dir(void)
334 {
335     const struct passwd *passwd = getpwuid(getuid());
336     if (passwd == NULL)
337         return NULL;
338     return passwd->pw_dir;
339 }
340 
341 static bool
try_open_file(path_components_t * components,const char * name)342 try_open_file(path_components_t *components, const char *name)
343 {
344     int parent_fd = tll_back(*components).fd;
345 
346     struct stat st;
347     if (fstatat(parent_fd, name, &st, 0) == 0 && S_ISREG(st.st_mode)) {
348         int fd = openat(parent_fd, name, O_RDONLY);
349         if (fd >= 0) {
350             path_component_add(components, name, fd);
351             return true;
352         }
353     }
354 
355     return false;
356 }
357 
358 static struct config_file
open_config(void)359 open_config(void)
360 {
361     struct config_file ret = {.path = NULL, .fd = -1};
362 
363     path_components_t components = tll_init();
364 
365     const char *xdg_config_home = getenv("XDG_CONFIG_HOME");
366     const char *user_home_dir = get_user_home_dir();
367     char *xdg_config_dirs_copy = NULL;
368 
369     /* Use XDG_CONFIG_HOME, or ~/.config */
370     if (xdg_config_home != NULL) {
371         int fd = open(xdg_config_home, O_RDONLY);
372         if (fd >= 0)
373             path_component_add(&components, xdg_config_home, fd);
374     } else if (user_home_dir != NULL) {
375         int home_fd = open(user_home_dir, O_RDONLY);
376         if (home_fd >= 0) {
377             int config_fd = openat(home_fd, ".config", O_RDONLY);
378             if (config_fd >= 0) {
379                 path_component_add(&components, user_home_dir, home_fd);
380                 path_component_add(&components, ".config", config_fd);
381             } else
382                 close(home_fd);
383         }
384     }
385 
386     /* First look for foot/foot.ini */
387     if (tll_length(components) > 0) {
388         int foot_fd = openat(tll_back(components).fd, "foot", O_RDONLY);
389         if (foot_fd >= 0) {
390             path_component_add(&components, "foot", foot_fd);
391 
392             if (try_open_file(&components, "foot.ini"))
393                 goto done;
394 
395             struct path_component pc = tll_pop_back(components);
396             path_component_destroy(&pc);
397         }
398     }
399 
400     /* Finally, try foot/foot.ini in all XDG_CONFIG_DIRS */
401     const char *xdg_config_dirs = getenv("XDG_CONFIG_DIRS");
402     xdg_config_dirs_copy = xdg_config_dirs != NULL
403         ? strdup(xdg_config_dirs) : NULL;
404 
405     if (xdg_config_dirs_copy != NULL) {
406         for (char *save = NULL,
407                  *xdg_dir = strtok_r(xdg_config_dirs_copy, ":", &save);
408              xdg_dir != NULL;
409              xdg_dir = strtok_r(NULL, ":", &save))
410         {
411             path_components_destroy(&components);
412 
413             int xdg_fd = open(xdg_dir, O_RDONLY);
414             if (xdg_fd < 0)
415                 continue;
416 
417             int foot_fd = openat(xdg_fd, "foot", O_RDONLY);
418             if (foot_fd < 0) {
419                 close(xdg_fd);
420                 continue;
421             }
422 
423             xassert(tll_length(components) == 0);
424             path_component_add(&components, xdg_dir, xdg_fd);
425             path_component_add(&components, "foot", foot_fd);
426 
427             if (try_open_file(&components, "foot.ini"))
428                 goto done;
429         }
430     }
431 
432 out:
433     path_components_destroy(&components);
434     free(xdg_config_dirs_copy);
435     return ret;
436 
437 done:
438     xassert(tll_length(components) > 0);
439     ret = path_components_to_config_file(&components);
440     goto out;
441 }
442 
443 static int
wccmp(const void * _a,const void * _b)444 wccmp(const void *_a, const void *_b)
445 {
446     const wchar_t *a = _a;
447     const wchar_t *b = _b;
448     return *a - *b;
449 }
450 
451 static bool
str_has_prefix(const char * str,const char * prefix)452 str_has_prefix(const char *str, const char *prefix)
453 {
454     return strncmp(str, prefix, strlen(prefix)) == 0;
455 }
456 
457 static bool NOINLINE
value_to_bool(struct context * ctx,bool * res)458 value_to_bool(struct context *ctx, bool *res)
459 {
460     static const char *const yes[] = {"on", "true", "yes", "1"};
461     static const char *const  no[] = {"off", "false", "no", "0"};
462 
463     for (size_t i = 0; i < ALEN(yes); i++) {
464         if (strcasecmp(ctx->value, yes[i]) == 0) {
465             *res = true;
466             return true;
467         }
468     }
469 
470     for (size_t i = 0; i < ALEN(no); i++) {
471         if (strcasecmp(ctx->value, no[i]) == 0) {
472             *res = false;
473             return true;
474         }
475     }
476 
477     LOG_CONTEXTUAL_ERR("invalid boolean value");
478     return false;
479 }
480 
481 
482 static bool NOINLINE
str_to_ulong(const char * s,int base,unsigned long * res)483 str_to_ulong(const char *s, int base, unsigned long *res)
484 {
485     if (s == NULL)
486         return false;
487 
488     errno = 0;
489     char *end = NULL;
490 
491     *res = strtoul(s, &end, base);
492     return errno == 0 && *end == '\0';
493 }
494 
495 static bool NOINLINE
str_to_uint32(const char * s,int base,uint32_t * res)496 str_to_uint32(const char *s, int base, uint32_t *res)
497 {
498     unsigned long v;
499     bool ret = str_to_ulong(s, base, &v);
500     if (v > UINT32_MAX)
501         return false;
502     *res = v;
503     return ret;
504 }
505 
506 static bool NOINLINE
str_to_uint16(const char * s,int base,uint16_t * res)507 str_to_uint16(const char *s, int base, uint16_t *res)
508 {
509     unsigned long v;
510     bool ret = str_to_ulong(s, base, &v);
511     if (v > UINT16_MAX)
512         return false;
513     *res = v;
514     return ret;
515 }
516 
517 static bool NOINLINE
value_to_uint16(struct context * ctx,int base,uint16_t * res)518 value_to_uint16(struct context *ctx, int base, uint16_t *res)
519 {
520     if (!str_to_uint16(ctx->value, base, res)) {
521         LOG_CONTEXTUAL_ERR(
522             "invalid integer value, or outside range 0-%u", UINT16_MAX);
523         return false;
524     }
525     return true;
526 }
527 
528 static bool NOINLINE
value_to_uint32(struct context * ctx,int base,uint32_t * res)529 value_to_uint32(struct context *ctx, int base, uint32_t *res)
530 {
531     if (!str_to_uint32(ctx->value, base, res)){
532         LOG_CONTEXTUAL_ERR(
533             "invalid integer value, or outside range 0-%u", UINT32_MAX);
534         return false;
535     }
536     return true;
537 }
538 
539 static bool NOINLINE
value_to_dimensions(struct context * ctx,uint32_t * x,uint32_t * y)540 value_to_dimensions(struct context *ctx, uint32_t *x, uint32_t *y)
541 {
542     if (sscanf(ctx->value, "%ux%u", x, y) != 2) {
543         LOG_CONTEXTUAL_ERR("invalid dimensions (must be on the form AxB)");
544         return false;
545     }
546 
547     return true;
548 }
549 
550 static bool NOINLINE
value_to_double(struct context * ctx,float * res)551 value_to_double(struct context *ctx, float *res)
552 {
553     const char *s = ctx->value;
554 
555     if (s == NULL)
556         return false;
557 
558     errno = 0;
559     char *end = NULL;
560 
561     *res = strtof(s, &end);
562     if (!(errno == 0 && *end == '\0')) {
563         LOG_CONTEXTUAL_ERR("invalid decimal value");
564         return false;
565     }
566 
567     return true;
568 }
569 
570 static bool NOINLINE
value_to_str(struct context * ctx,char ** res)571 value_to_str(struct context *ctx, char **res)
572 {
573     free(*res);
574     *res = xstrdup(ctx->value);
575     return true;
576 }
577 
578 static bool NOINLINE
value_to_wchars(struct context * ctx,wchar_t ** res)579 value_to_wchars(struct context *ctx, wchar_t **res)
580 {
581     size_t chars = mbstowcs(NULL, ctx->value, 0);
582     if (chars == (size_t)-1) {
583         LOG_CONTEXTUAL_ERR("not a valid string value");
584         return false;
585     }
586 
587     free(*res);
588     *res = xmalloc((chars + 1) * sizeof(wchar_t));
589     mbstowcs(*res, ctx->value, chars + 1);
590     return true;
591 }
592 
593 static bool NOINLINE
value_to_enum(struct context * ctx,const char ** value_map,int * res)594 value_to_enum(struct context *ctx, const char **value_map, int *res)
595 {
596     size_t str_len = 0;
597     size_t count = 0;
598 
599     for (; value_map[count] != NULL; count++) {
600         if (strcasecmp(value_map[count], ctx->value) == 0) {
601             *res = count;
602             return true;
603         }
604         str_len += strlen(value_map[count]);
605     }
606 
607     const size_t size = str_len + count * 4 + 1;
608     char valid_values[512];
609     size_t idx = 0;
610     xassert(size < sizeof(valid_values));
611 
612     for (size_t i = 0; i < count; i++)
613         idx += xsnprintf(&valid_values[idx], size - idx, "'%s', ", value_map[i]);
614 
615     if (count > 0)
616         valid_values[idx - 2] = '\0';
617 
618     LOG_CONTEXTUAL_ERR("not one of %s", valid_values);
619     *res = -1;
620     return false;
621 }
622 
623 static bool NOINLINE
value_to_color(struct context * ctx,uint32_t * color,bool allow_alpha)624 value_to_color(struct context *ctx, uint32_t *color, bool allow_alpha)
625 {
626     if (!str_to_uint32(ctx->value, 16, color)) {
627         LOG_CONTEXTUAL_ERR("not a valid color value");
628         return false;
629     }
630 
631     if (!allow_alpha && (*color & 0xff000000) != 0) {
632         LOG_CONTEXTUAL_ERR("color value must not have an alpha component");
633         return false;
634     }
635 
636     return true;
637 }
638 
639 static bool NOINLINE
value_to_two_colors(struct context * ctx,uint32_t * first,uint32_t * second,bool allow_alpha)640 value_to_two_colors(struct context *ctx,
641                     uint32_t *first, uint32_t *second, bool allow_alpha)
642 {
643     bool ret = false;
644     const char *original_value = ctx->value;
645 
646     /* TODO: do this without strdup() */
647     char *value_copy = xstrdup(ctx->value);
648     const char *first_as_str = strtok(value_copy, " ");
649     const char *second_as_str = strtok(NULL, " ");
650 
651     if (first_as_str == NULL || second_as_str == NULL) {
652         LOG_CONTEXTUAL_ERR("invalid double color value");
653         goto out;
654     }
655 
656     ctx->value = first_as_str;
657     if (!value_to_color(ctx, first, allow_alpha))
658         goto out;
659 
660     ctx->value = second_as_str;
661     if (!value_to_color(ctx, second, allow_alpha))
662         goto out;
663 
664     ret = true;
665 
666 out:
667     free(value_copy);
668     ctx->value = original_value;
669     return ret;
670 }
671 
672 static bool NOINLINE
value_to_pt_or_px(struct context * ctx,struct pt_or_px * res)673 value_to_pt_or_px(struct context *ctx, struct pt_or_px *res)
674 {
675     const char *s = ctx->value;
676 
677     size_t len = s != NULL ? strlen(s) : 0;
678     if (len >= 2 && s[len - 2] == 'p' && s[len - 1] == 'x') {
679         errno = 0;
680         char *end = NULL;
681 
682         long value = strtol(s, &end, 10);
683         if (!(errno == 0 && end == s + len - 2)) {
684             LOG_CONTEXTUAL_ERR("invalid px value (must be on the form 12px)");
685             return false;
686         }
687         res->pt = 0;
688         res->px = value;
689     } else {
690         float value;
691         if (!value_to_double(ctx, &value))
692             return false;
693         res->pt = value;
694         res->px = 0;
695     }
696 
697     return true;
698 }
699 
700 static struct config_font_list NOINLINE
value_to_fonts(struct context * ctx)701 value_to_fonts(struct context *ctx)
702 {
703     size_t count = 0;
704     size_t size = 0;
705     struct config_font *fonts = NULL;
706 
707     char *copy = xstrdup(ctx->value);
708     for (const char *font = strtok(copy, ",");
709          font != NULL;
710          font = strtok(NULL, ","))
711     {
712         /* Trim spaces, strictly speaking not necessary, but looks nice :) */
713         while (*font != '\0' && isspace(*font))
714             font++;
715 
716         if (font[0] == '\0')
717             continue;
718 
719         struct config_font font_data;
720         if (!config_font_parse(font, &font_data)) {
721             ctx->value = font;
722             LOG_CONTEXTUAL_ERR("invalid font specification");
723             goto err;
724         }
725 
726         if (count + 1 > size) {
727             size += 4;
728             fonts = xrealloc(fonts, size * sizeof(fonts[0]));
729         }
730 
731         xassert(count + 1 <= size);
732         fonts[count++] = font_data;
733     }
734 
735     free(copy);
736     return (struct config_font_list){.arr = fonts, .count = count};
737 
738 err:
739     free(copy);
740     free(fonts);
741     return (struct config_font_list){.arr = NULL, .count = 0};
742 }
743 
744 static void NOINLINE
free_argv(struct argv * argv)745 free_argv(struct argv *argv)
746 {
747     if (argv->args == NULL)
748         return;
749     for (char **a = argv->args; *a != NULL; a++)
750         free(*a);
751     free(argv->args);
752     argv->args = NULL;
753 }
754 
755 static void NOINLINE
clone_argv(struct argv * dst,const struct argv * src)756 clone_argv(struct argv *dst, const struct argv *src)
757 {
758     if (src->args == NULL) {
759         dst->args = NULL;
760         return;
761     }
762 
763     size_t count = 0;
764     for (char **args = src->args; *args != NULL; args++)
765         count++;
766 
767     dst->args = xmalloc((count + 1) * sizeof(dst->args[0]));
768     for (char **args_src = src->args, **args_dst = dst->args;
769          *args_src != NULL; args_src++,
770              args_dst++)
771     {
772         *args_dst = xstrdup(*args_src);
773     }
774     dst->args[count] = NULL;
775 }
776 
777 static void
spawn_template_free(struct config_spawn_template * template)778 spawn_template_free(struct config_spawn_template *template)
779 {
780     free_argv(&template->argv);
781 }
782 
783 static void
spawn_template_clone(struct config_spawn_template * dst,const struct config_spawn_template * src)784 spawn_template_clone(struct config_spawn_template *dst,
785                      const struct config_spawn_template *src)
786 {
787     clone_argv(&dst->argv, &src->argv);
788 }
789 
790 static bool NOINLINE
value_to_spawn_template(struct context * ctx,struct config_spawn_template * template)791 value_to_spawn_template(struct context *ctx,
792                         struct config_spawn_template *template)
793 {
794     spawn_template_free(template);
795 
796     char **argv = NULL;
797 
798     if (!tokenize_cmdline(ctx->value, &argv)) {
799         LOG_CONTEXTUAL_ERR("syntax error in command line");
800         return false;
801     }
802 
803     template->argv.args = argv;
804     return true;
805 }
806 
807 static bool parse_config_file(
808     FILE *f, struct config *conf, const char *path, bool errors_are_fatal);
809 
810 static bool
parse_section_main(struct context * ctx)811 parse_section_main(struct context *ctx)
812 {
813     struct config *conf = ctx->conf;
814     const char *key = ctx->key;
815     const char *value = ctx->value;
816     bool errors_are_fatal = ctx->errors_are_fatal;
817 
818     if (strcmp(key, "include") == 0) {
819         char *_include_path = NULL;
820         const char *include_path = NULL;
821 
822         if (value[0] == '~' && value[1] == '/') {
823             const char *home_dir = get_user_home_dir();
824 
825             if (home_dir == NULL) {
826                 LOG_CONTEXTUAL_ERRNO("failed to expand '~'");
827                 return false;
828             }
829 
830             _include_path = xasprintf("%s/%s", home_dir, value + 2);
831             include_path = _include_path;
832         } else
833             include_path = value;
834 
835         if (include_path[0] != '/') {
836             LOG_CONTEXTUAL_ERR("not an absolute path");
837             free(_include_path);
838             return false;
839         }
840 
841         FILE *include = fopen(include_path, "r");
842 
843         if (include == NULL) {
844             LOG_CONTEXTUAL_ERRNO("failed to open");
845             free(_include_path);
846             return false;
847         }
848 
849         bool ret = parse_config_file(
850             include, conf, include_path, errors_are_fatal);
851         fclose(include);
852 
853         LOG_INFO("imported sub-configuration from %s", include_path);
854         free(_include_path);
855         return ret;
856     }
857 
858     else if (strcmp(key, "term") == 0)
859         return value_to_str(ctx, &conf->term);
860 
861     else if (strcmp(key, "shell") == 0)
862         return value_to_str(ctx, &conf->shell);
863 
864     else if (strcmp(key, "login-shell") == 0)
865         return value_to_bool(ctx, &conf->login_shell);
866 
867     else if (strcmp(key, "title") == 0)
868         return value_to_str(ctx, &conf->title);
869 
870     else if (strcmp(key, "locked-title") == 0)
871         return value_to_bool(ctx, &conf->locked_title);
872 
873     else if (strcmp(key, "app-id") == 0)
874         return value_to_str(ctx, &conf->app_id);
875 
876     else if (strcmp(key, "initial-window-size-pixels") == 0) {
877         if (!value_to_dimensions(ctx, &conf->size.width, &conf->size.height))
878             return false;
879 
880         conf->size.type = CONF_SIZE_PX;
881         return true;
882     }
883 
884     else if (strcmp(key, "initial-window-size-chars") == 0) {
885         if (!value_to_dimensions(ctx, &conf->size.width, &conf->size.height))
886             return false;
887 
888         conf->size.type = CONF_SIZE_CELLS;
889         return true;
890     }
891 
892     else if (strcmp(key, "pad") == 0) {
893         unsigned x, y;
894         char mode[16] = {0};
895 
896         int ret = sscanf(value, "%ux%u %15s", &x, &y, mode);
897         bool center = strcasecmp(mode, "center") == 0;
898         bool invalid_mode = !center && mode[0] != '\0';
899 
900         if ((ret != 2 && ret != 3) || invalid_mode) {
901             LOG_CONTEXTUAL_ERR(
902                 "invalid padding (must be on the form PAD_XxPAD_Y [center])");
903             return false;
904         }
905 
906         conf->pad_x = x;
907         conf->pad_y = y;
908         conf->center = center;
909         return true;
910     }
911 
912     else if (strcmp(key, "resize-delay-ms") == 0)
913         return value_to_uint16(ctx, 10, &conf->resize_delay_ms);
914 
915     else if (strcmp(key, "bold-text-in-bright") == 0) {
916         if (strcmp(value, "palette-based") == 0) {
917             conf->bold_in_bright.enabled = true;
918             conf->bold_in_bright.palette_based = true;
919         } else {
920             if (!value_to_bool(ctx, &conf->bold_in_bright.enabled))
921                 return false;
922             conf->bold_in_bright.palette_based = false;
923         }
924         return true;
925     }
926 
927     else if (strcmp(key, "initial-window-mode") == 0) {
928         _Static_assert(sizeof(conf->startup_mode) == sizeof(int),
929             "enum is not 32-bit");
930 
931         return value_to_enum(
932                 ctx,
933                 (const char *[]){"windowed", "maximized", "fullscreen", NULL},
934                 (int *)&conf->startup_mode);
935     }
936 
937     else if (strcmp(key, "font") == 0 ||
938              strcmp(key, "font-bold") == 0 ||
939              strcmp(key, "font-italic") == 0 ||
940              strcmp(key, "font-bold-italic") == 0)
941 
942     {
943         size_t idx =
944             strcmp(key, "font") == 0 ? 0 :
945             strcmp(key, "font-bold") == 0 ? 1 :
946             strcmp(key, "font-italic") == 0 ? 2 : 3;
947 
948         struct config_font_list new_list = value_to_fonts(ctx);
949         if (new_list.arr == NULL)
950             return false;
951 
952         config_font_list_destroy(&conf->fonts[idx]);
953         conf->fonts[idx] = new_list;
954         return true;
955     }
956 
957     else if (strcmp(key, "line-height") == 0)
958         return value_to_pt_or_px(ctx, &conf->line_height);
959 
960     else if (strcmp(key, "letter-spacing") == 0)
961         return value_to_pt_or_px(ctx, &conf->letter_spacing);
962 
963     else if (strcmp(key, "horizontal-letter-offset") == 0)
964         return value_to_pt_or_px(ctx, &conf->horizontal_letter_offset);
965 
966     else if (strcmp(key, "vertical-letter-offset") == 0)
967         return value_to_pt_or_px(ctx, &conf->vertical_letter_offset);
968 
969     else if (strcmp(key, "underline-offset") == 0) {
970         if (!value_to_pt_or_px(ctx, &conf->underline_offset))
971             return false;
972         conf->use_custom_underline_offset = true;
973         return true;
974     }
975 
976     else if (strcmp(key, "dpi-aware") == 0) {
977         if (strcmp(value, "auto") == 0)
978             conf->dpi_aware = DPI_AWARE_AUTO;
979         else {
980             bool value;
981             if (!value_to_bool(ctx, &value))
982                 return false;
983             conf->dpi_aware = value ? DPI_AWARE_YES : DPI_AWARE_NO;
984         }
985         return true;
986     }
987 
988     else if (strcmp(key, "workers") == 0)
989         return value_to_uint16(ctx, 10, &conf->render_worker_count);
990 
991     else if (strcmp(key, "word-delimiters") == 0)
992         return value_to_wchars(ctx, &conf->word_delimiters);
993 
994     else if (strcmp(key, "notify") == 0)
995         return value_to_spawn_template(ctx, &conf->notify);
996 
997     else if (strcmp(key, "notify-focus-inhibit") == 0)
998         return value_to_bool(ctx, &conf->notify_focus_inhibit);
999 
1000     else if (strcmp(key, "selection-target") == 0) {
1001         _Static_assert(sizeof(conf->selection_target) == sizeof(int),
1002                        "enum is not 32-bit");
1003 
1004         return value_to_enum(
1005             ctx,
1006             (const char *[]){"none", "primary", "clipboard", "both", NULL},
1007             (int *)&conf->selection_target);
1008     }
1009 
1010     else if (strcmp(key, "box-drawings-uses-font-glyphs") == 0)
1011         return value_to_bool(ctx, &conf->box_drawings_uses_font_glyphs);
1012 
1013     else {
1014         LOG_CONTEXTUAL_ERR("not a valid option: %s", key);
1015         return false;
1016     }
1017 }
1018 
1019 static bool
parse_section_bell(struct context * ctx)1020 parse_section_bell(struct context *ctx)
1021 {
1022     struct config *conf = ctx->conf;
1023     const char *key = ctx->key;
1024 
1025     if (strcmp(key, "urgent") == 0)
1026         return value_to_bool(ctx, &conf->bell.urgent);
1027     else if (strcmp(key, "notify") == 0)
1028         return value_to_bool(ctx, &conf->bell.notify);
1029     else if (strcmp(key, "command") == 0) {
1030         if (!value_to_spawn_template(ctx, &conf->bell.command))
1031             return false;
1032     } else if (strcmp(key, "command-focused") == 0)
1033         return value_to_bool(ctx, &conf->bell.command_focused);
1034     else {
1035         LOG_CONTEXTUAL_ERR("not a valid option: %s", key);
1036         return false;
1037     }
1038 
1039     return true;
1040 }
1041 
1042 static bool
parse_section_scrollback(struct context * ctx)1043 parse_section_scrollback(struct context *ctx)
1044 {
1045     struct config *conf = ctx->conf;
1046     const char *key = ctx->key;
1047     const char *value = ctx->value;
1048 
1049     if (strcmp(key, "lines") == 0)
1050         value_to_uint32(ctx, 10, &conf->scrollback.lines);
1051 
1052     else if (strcmp(key, "indicator-position") == 0) {
1053         _Static_assert(
1054             sizeof(conf->scrollback.indicator.position) == sizeof(int),
1055             "enum is not 32-bit");
1056 
1057         return value_to_enum(
1058             ctx,
1059             (const char *[]){"none", "fixed", "relative", NULL},
1060             (int *)&conf->scrollback.indicator.position);
1061     }
1062 
1063     else if (strcmp(key, "indicator-format") == 0) {
1064         if (strcmp(value, "percentage") == 0) {
1065             conf->scrollback.indicator.format
1066                 = SCROLLBACK_INDICATOR_FORMAT_PERCENTAGE;
1067         } else if (strcmp(value, "line") == 0) {
1068             conf->scrollback.indicator.format
1069                 = SCROLLBACK_INDICATOR_FORMAT_LINENO;
1070         } else
1071             return value_to_wchars(ctx, &conf->scrollback.indicator.text);
1072     }
1073 
1074     else if (strcmp(key, "multiplier") == 0)
1075         return value_to_double(ctx, &conf->scrollback.multiplier);
1076 
1077     else {
1078         LOG_CONTEXTUAL_ERR("not a valid option: %s", key);
1079         return false;
1080     }
1081 
1082     return true;
1083 }
1084 
1085 static bool
parse_section_url(struct context * ctx)1086 parse_section_url(struct context *ctx)
1087 {
1088     struct config *conf = ctx->conf;
1089     const char *key = ctx->key;
1090     const char *value = ctx->value;
1091 
1092     if (strcmp(key, "launch") == 0) {
1093         if (!value_to_spawn_template(ctx, &conf->url.launch))
1094             return false;
1095     }
1096 
1097     else if (strcmp(key, "label-letters") == 0)
1098         return value_to_wchars(ctx, &conf->url.label_letters);
1099 
1100     else if (strcmp(key, "osc8-underline") == 0) {
1101         _Static_assert(sizeof(conf->url.osc8_underline) == sizeof(int),
1102                        "enum is not 32-bit");
1103 
1104         return value_to_enum(
1105             ctx,
1106             (const char *[]){"url-mode", "always", NULL},
1107             (int *)&conf->url.osc8_underline);
1108     }
1109 
1110     else if (strcmp(key, "protocols") == 0) {
1111         for (size_t i = 0; i < conf->url.prot_count; i++)
1112             free(conf->url.protocols[i]);
1113         free(conf->url.protocols);
1114 
1115         conf->url.max_prot_len = 0;
1116         conf->url.prot_count = 0;
1117         conf->url.protocols = NULL;
1118 
1119         char *copy = xstrdup(value);
1120 
1121         for (char *prot = strtok(copy, ",");
1122              prot != NULL;
1123              prot = strtok(NULL, ","))
1124         {
1125 
1126             /* Strip leading whitespace */
1127             while (isspace(*prot))
1128                 prot++;
1129 
1130             /* Strip trailing whitespace */
1131             size_t len = strlen(prot);
1132             while (len > 0 && isspace(prot[len - 1]))
1133                 prot[--len] = '\0';
1134 
1135             size_t chars = mbstowcs(NULL, prot, 0);
1136             if (chars == (size_t)-1) {
1137                 ctx->value = prot;
1138                 LOG_CONTEXTUAL_ERRNO("invalid protocol");
1139                 return false;
1140             }
1141 
1142             conf->url.prot_count++;
1143             conf->url.protocols = xrealloc(
1144                 conf->url.protocols,
1145                 conf->url.prot_count * sizeof(conf->url.protocols[0]));
1146 
1147             size_t idx = conf->url.prot_count - 1;
1148             conf->url.protocols[idx] = xmalloc((chars + 1 + 3) * sizeof(wchar_t));
1149             mbstowcs(conf->url.protocols[idx], prot, chars + 1);
1150             wcscpy(&conf->url.protocols[idx][chars], L"://");
1151 
1152             chars += 3;  /* Include the "://" */
1153             if (chars > conf->url.max_prot_len)
1154                 conf->url.max_prot_len = chars;
1155         }
1156 
1157         free(copy);
1158     }
1159 
1160     else if (strcmp(key, "uri-characters") == 0) {
1161         if (!value_to_wchars(ctx, &conf->url.uri_characters))
1162             return false;
1163 
1164         qsort(
1165             conf->url.uri_characters,
1166             wcslen(conf->url.uri_characters),
1167             sizeof(conf->url.uri_characters[0]),
1168             &wccmp);
1169         return true;
1170     }
1171 
1172     else {
1173         LOG_CONTEXTUAL_ERR("not a valid option: %s", key);
1174         return false;
1175     }
1176 
1177     return true;
1178 }
1179 
1180 static bool
parse_section_colors(struct context * ctx)1181 parse_section_colors(struct context *ctx)
1182 {
1183     struct config *conf = ctx->conf;
1184     const char *key = ctx->key;
1185 
1186     size_t key_len = strlen(key);
1187     uint8_t last_digit = (unsigned char)key[key_len - 1] - '0';
1188     uint32_t *color = NULL;
1189 
1190     if (isdigit(key[0])) {
1191         unsigned long index;
1192         if (!str_to_ulong(key, 0, &index) ||
1193             index >= ALEN(conf->colors.table))
1194         {
1195             LOG_CONTEXTUAL_ERR(
1196                 "invalid color palette index: %s (not in range 0-%zu)",
1197                 key, ALEN(conf->colors.table));
1198             return false;
1199         }
1200         color = &conf->colors.table[index];
1201     }
1202 
1203     else if (key_len == 8 && str_has_prefix(key, "regular") && last_digit < 8)
1204         color = &conf->colors.table[last_digit];
1205 
1206     else if (key_len == 7 && str_has_prefix(key, "bright") && last_digit < 8)
1207         color = &conf->colors.table[8 + last_digit];
1208 
1209     else if (key_len == 4 && str_has_prefix(key, "dim") && last_digit < 8) {
1210         if (!value_to_color(ctx, &conf->colors.dim[last_digit], false))
1211             return false;
1212 
1213         conf->colors.use_custom.dim |= 1 << last_digit;
1214         return true;
1215     }
1216 
1217     else if (strcmp(key, "foreground") == 0) color = &conf->colors.fg;
1218     else if (strcmp(key, "background") == 0) color = &conf->colors.bg;
1219     else if (strcmp(key, "selection-foreground") == 0) color = &conf->colors.selection_fg;
1220     else if (strcmp(key, "selection-background") == 0) color = &conf->colors.selection_bg;
1221 
1222     else if (strcmp(key, "jump-labels") == 0) {
1223         if (!value_to_two_colors(
1224                 ctx,
1225                 &conf->colors.jump_label.fg,
1226                 &conf->colors.jump_label.bg,
1227                 false))
1228         {
1229             return false;
1230         }
1231 
1232         conf->colors.use_custom.jump_label = true;
1233         return true;
1234     }
1235 
1236     else if (strcmp(key, "scrollback-indicator") == 0) {
1237         if (!value_to_two_colors(
1238                 ctx,
1239                 &conf->colors.scrollback_indicator.fg,
1240                 &conf->colors.scrollback_indicator.bg,
1241                 false))
1242         {
1243             return false;
1244         }
1245 
1246         conf->colors.use_custom.scrollback_indicator = true;
1247         return true;
1248     }
1249 
1250     else if (strcmp(key, "urls") == 0) {
1251         if (!value_to_color(ctx, &conf->colors.url, false))
1252             return false;
1253 
1254         conf->colors.use_custom.url = true;
1255         return true;
1256     }
1257 
1258     else if (strcmp(key, "alpha") == 0) {
1259         float alpha;
1260         if (!value_to_double(ctx, &alpha))
1261             return false;
1262 
1263         if (alpha < 0. || alpha > 1.) {
1264             LOG_CONTEXTUAL_ERR("not in range 0.0-1.0");
1265             return false;
1266         }
1267 
1268         conf->colors.alpha = alpha * 65535.;
1269         return true;
1270     }
1271 
1272     else {
1273         LOG_CONTEXTUAL_ERR("not valid option");
1274         return false;
1275     }
1276 
1277     uint32_t color_value;
1278     if (!value_to_color(ctx, &color_value, false))
1279         return false;
1280 
1281     *color = color_value;
1282     return true;
1283 }
1284 
1285 static bool
parse_section_cursor(struct context * ctx)1286 parse_section_cursor(struct context *ctx)
1287 {
1288     struct config *conf = ctx->conf;
1289     const char *key = ctx->key;
1290 
1291     if (strcmp(key, "style") == 0) {
1292         _Static_assert(sizeof(conf->cursor.style) == sizeof(int),
1293                        "enum is not 32-bit");
1294 
1295         return value_to_enum(
1296             ctx,
1297             (const char *[]){"block", "underline", "beam", NULL},
1298             (int *)&conf->cursor.style);
1299     }
1300 
1301     else if (strcmp(key, "blink") == 0)
1302         return value_to_bool(ctx, &conf->cursor.blink);
1303 
1304     else if (strcmp(key, "color") == 0) {
1305         if (!value_to_two_colors(
1306                 ctx,
1307                 &conf->cursor.color.text,
1308                 &conf->cursor.color.cursor,
1309                 false))
1310         {
1311             return false;
1312         }
1313 
1314         conf->cursor.color.text |= 1u << 31;
1315         conf->cursor.color.cursor |= 1u << 31;
1316     }
1317 
1318     else if (strcmp(key, "beam-thickness") == 0) {
1319         if (!value_to_pt_or_px(ctx, &conf->cursor.beam_thickness))
1320             return false;
1321     }
1322 
1323     else if (strcmp(key, "underline-thickness") == 0) {
1324         if (!value_to_pt_or_px(ctx, &conf->cursor.underline_thickness))
1325             return false;
1326     }
1327 
1328     else {
1329         LOG_CONTEXTUAL_ERR("not a valid option: %s", key);
1330         return false;
1331     }
1332 
1333     return true;
1334 }
1335 
1336 static bool
parse_section_mouse(struct context * ctx)1337 parse_section_mouse(struct context *ctx)
1338 {
1339     struct config *conf = ctx->conf;
1340     const char *key = ctx->key;
1341 
1342     if (strcmp(key, "hide-when-typing") == 0)
1343         return value_to_bool(ctx, &conf->mouse.hide_when_typing);
1344 
1345     else if (strcmp(key, "alternate-scroll-mode") == 0)
1346         return value_to_bool(ctx, &conf->mouse.alternate_scroll_mode);
1347 
1348     else {
1349         LOG_CONTEXTUAL_ERR("not a valid option: %s", key);
1350         return false;
1351     }
1352 
1353     return true;
1354 }
1355 
1356 static bool
parse_section_csd(struct context * ctx)1357 parse_section_csd(struct context *ctx)
1358 {
1359     struct config *conf = ctx->conf;
1360     const char *key = ctx->key;
1361 
1362     if (strcmp(key, "preferred") == 0) {
1363         _Static_assert(sizeof(conf->csd.preferred) == sizeof(int),
1364                        "enum is not 32-bit");
1365 
1366         return value_to_enum(
1367             ctx,
1368             (const char *[]){"none", "server", "client", NULL},
1369             (int *)&conf->csd.preferred);
1370     }
1371 
1372     else if (strcmp(key, "font") == 0) {
1373         struct config_font_list new_list = value_to_fonts(ctx);
1374         if (new_list.arr == NULL)
1375             return false;
1376 
1377         config_font_list_destroy(&conf->csd.font);
1378         conf->csd.font = new_list;
1379     }
1380 
1381     else if (strcmp(key, "color") == 0) {
1382         uint32_t color;
1383         if (!value_to_color(ctx, &color, true))
1384             return false;
1385 
1386         conf->csd.color.title_set = true;
1387         conf->csd.color.title = color;
1388     }
1389 
1390     else if (strcmp(key, "size") == 0)
1391         return value_to_uint16(ctx, 10, &conf->csd.title_height);
1392 
1393     else if (strcmp(key, "button-width") == 0)
1394         return value_to_uint16(ctx, 10, &conf->csd.button_width);
1395 
1396     else if (strcmp(key, "button-color") == 0) {
1397         if (!value_to_color(ctx, &conf->csd.color.buttons, true))
1398             return false;
1399 
1400         conf->csd.color.buttons_set = true;
1401     }
1402 
1403     else if (strcmp(key, "button-minimize-color") == 0) {
1404         if (!value_to_color(ctx, &conf->csd.color.minimize, true))
1405             return false;
1406 
1407         conf->csd.color.minimize_set = true;
1408     }
1409 
1410     else if (strcmp(key, "button-maximize-color") == 0) {
1411         if (!value_to_color(ctx, &conf->csd.color.maximize, true))
1412             return false;
1413 
1414         conf->csd.color.maximize_set = true;
1415     }
1416 
1417     else if (strcmp(key, "button-close-color") == 0) {
1418         if (!value_to_color(ctx, &conf->csd.color.close, true))
1419             return false;
1420 
1421         conf->csd.color.close_set = true;
1422     }
1423 
1424     else if (strcmp(key, "border-color") == 0) {
1425         if (!value_to_color(ctx, &conf->csd.color.border, true))
1426             return false;
1427 
1428         conf->csd.color.border_set = true;
1429     }
1430 
1431     else if (strcmp(key, "border-width") == 0)
1432         return value_to_uint16(ctx, 10, &conf->csd.border_width_visible);
1433 
1434     else {
1435         LOG_CONTEXTUAL_ERR("not a valid action: %s", key);
1436         return false;
1437     }
1438 
1439     return true;
1440 }
1441 
1442 /* Struct that holds temporary key/mouse binding parsed data */
1443 struct key_combo {
1444     char *text;          /* Raw text, e.g. "Control+Shift+V" */
1445     struct config_key_modifiers modifiers;
1446     union {
1447         xkb_keysym_t sym;    /* Key converted to an XKB symbol, e.g. XKB_KEY_V */
1448         struct {
1449             int button;
1450             int count;
1451         } m;
1452     };
1453 };
1454 
1455 struct key_combo_list {
1456     size_t count;
1457     struct key_combo *combos;
1458 };
1459 
1460 static void NOINLINE
free_key_combo_list(struct key_combo_list * key_combos)1461 free_key_combo_list(struct key_combo_list *key_combos)
1462 {
1463     for (size_t i = 0; i < key_combos->count; i++)
1464         free(key_combos->combos[i].text);
1465     free(key_combos->combos);
1466     key_combos->count = 0;
1467     key_combos->combos = NULL;
1468 }
1469 
1470 static bool
parse_modifiers(struct context * ctx,const char * text,size_t len,struct config_key_modifiers * modifiers)1471 parse_modifiers(struct context *ctx, const char *text, size_t len,
1472                 struct config_key_modifiers *modifiers)
1473 {
1474     bool ret = false;
1475 
1476     *modifiers = (struct config_key_modifiers){0};
1477     char *copy = xstrndup(text, len);
1478 
1479     for (char *tok_ctx = NULL, *key = strtok_r(copy, "+", &tok_ctx);
1480          key != NULL;
1481          key = strtok_r(NULL, "+", &tok_ctx))
1482     {
1483         if (strcmp(key, XKB_MOD_NAME_SHIFT) == 0)
1484             modifiers->shift = true;
1485         else if (strcmp(key, XKB_MOD_NAME_CTRL) == 0)
1486             modifiers->ctrl = true;
1487         else if (strcmp(key, XKB_MOD_NAME_ALT) == 0)
1488             modifiers->alt = true;
1489         else if (strcmp(key, XKB_MOD_NAME_LOGO) == 0)
1490             modifiers->meta = true;
1491         else {
1492             LOG_CONTEXTUAL_ERR("not a valid modifier name: %s", key);
1493             goto out;
1494         }
1495     }
1496 
1497     ret = true;
1498 
1499 out:
1500     free(copy);
1501     return ret;
1502 }
1503 
1504 static bool
value_to_key_combos(struct context * ctx,struct key_combo_list * key_combos)1505 value_to_key_combos(struct context *ctx, struct key_combo_list *key_combos)
1506 {
1507     xassert(key_combos != NULL);
1508     xassert(key_combos->count == 0 && key_combos->combos == NULL);
1509 
1510     size_t size = 0;  /* Size of ‘combos’ array in the key-combo list */
1511 
1512     char *copy = xstrdup(ctx->value);
1513 
1514     for (char *tok_ctx = NULL, *combo = strtok_r(copy, " ", &tok_ctx);
1515          combo != NULL;
1516          combo = strtok_r(NULL, " ", &tok_ctx))
1517     {
1518         struct config_key_modifiers modifiers = {0};
1519         char *key = strrchr(combo, '+');
1520 
1521         if (key == NULL) {
1522             /* No modifiers */
1523             key = combo;
1524         } else {
1525             if (!parse_modifiers(ctx, combo, key - combo, &modifiers))
1526                 goto err;
1527             key++;  /* Skip past the '+' */
1528         }
1529 
1530         /* Translate key name to symbol */
1531         xkb_keysym_t sym = xkb_keysym_from_name(key, 0);
1532         if (sym == XKB_KEY_NoSymbol) {
1533             LOG_CONTEXTUAL_ERR("not a valid XKB key name: %s", key);
1534             goto err;
1535         }
1536 
1537         if (key_combos->count + 1 > size) {
1538             size += 4;
1539             key_combos->combos = xrealloc(
1540                 key_combos->combos, size * sizeof(key_combos->combos[0]));
1541         }
1542 
1543         xassert(key_combos->count + 1 <= size);
1544         key_combos->combos[key_combos->count++] = (struct key_combo){
1545             .text = xstrdup(combo),
1546             .modifiers = modifiers,
1547             .sym = sym,
1548         };
1549     }
1550 
1551     free(copy);
1552     return true;
1553 
1554 err:
1555     free_key_combo_list(key_combos);
1556     free(copy);
1557     return false;
1558 }
1559 
1560 static int
argv_compare(const struct argv * argv1,const struct argv * argv2)1561 argv_compare(const struct argv *argv1, const struct argv *argv2)
1562 {
1563     if (argv1->args == NULL && argv2->args == NULL)
1564         return 0;
1565 
1566     if (argv1->args == NULL)
1567         return -1;
1568     if (argv2->args == NULL)
1569         return 1;
1570 
1571     for (size_t i = 0; ; i++) {
1572         if (argv1->args[i] == NULL && argv2->args[i] == NULL)
1573             return 0;
1574         if (argv1->args[i] == NULL)
1575             return -1;
1576         if (argv2->args[i] == NULL)
1577             return 1;
1578 
1579         int ret = strcmp(argv1->args[i], argv2->args[i]);
1580         if (ret != 0)
1581             return ret;
1582     }
1583 
1584     BUG("unexpected loop break");
1585     return 1;
1586 }
1587 
1588 static bool
has_key_binding_collisions(struct context * ctx,int action,const char * const action_map[],const struct config_key_binding_list * bindings,const struct key_combo_list * key_combos,const struct argv * pipe_argv)1589 has_key_binding_collisions(struct context *ctx,
1590                            int action, const char *const action_map[],
1591                            const struct config_key_binding_list *bindings,
1592                            const struct key_combo_list *key_combos,
1593                            const struct argv *pipe_argv)
1594 {
1595     for (size_t j = 0; j < bindings->count; j++) {
1596         const struct config_key_binding *combo1 = &bindings->arr[j];
1597 
1598         if (combo1->action == BIND_ACTION_NONE)
1599             continue;
1600 
1601         if (combo1->action == action) {
1602             if (argv_compare(&combo1->pipe.argv, pipe_argv) == 0)
1603                 continue;
1604         }
1605 
1606         for (size_t i = 0; i < key_combos->count; i++) {
1607             const struct key_combo *combo2 = &key_combos->combos[i];
1608 
1609             const struct config_key_modifiers *mods1 = &combo1->modifiers;
1610             const struct config_key_modifiers *mods2 = &combo2->modifiers;
1611 
1612             bool shift = mods1->shift == mods2->shift;
1613             bool alt = mods1->alt == mods2->alt;
1614             bool ctrl = mods1->ctrl == mods2->ctrl;
1615             bool meta = mods1->meta == mods2->meta;
1616             bool sym = combo1->sym == combo2->sym;
1617 
1618             if (shift && alt && ctrl && meta && sym) {
1619                 bool has_pipe = combo1->pipe.argv.args != NULL;
1620                 LOG_CONTEXTUAL_ERR("%s already mapped to '%s%s%s%s'",
1621                                    combo2->text,
1622                                    action_map[combo1->action],
1623                                    has_pipe ? " [" : "",
1624                                    has_pipe ? combo1->pipe.argv.args[0] : "",
1625                                    has_pipe ? "]" : "");
1626                 return true;
1627             }
1628         }
1629     }
1630 
1631     return false;
1632 }
1633 
1634 /*
1635  * Parses a key binding value on the form
1636  *  "[cmd-to-exec arg1 arg2] Mods+Key"
1637  *
1638  * and extracts 'cmd-to-exec' and its arguments.
1639  *
1640  * Input:
1641  *  - value: raw string, on the form mention above
1642  *  - cmd: pointer to string to will be allocated and filled with
1643  *        'cmd-to-exec arg1 arg2'
1644  *  - argv: point to array of string. Array will be allocated. Will be
1645  *          filled with {'cmd-to-exec', 'arg1', 'arg2', NULL}
1646  *
1647  * Returns:
1648  *  - ssize_t, number of bytes that were stripped from 'value' to remove the '[]'
1649  *    enclosed cmd and its arguments, including any subsequent
1650  *    whitespace characters. I.e. if 'value' is "[cmd] BTN_RIGHT", the
1651  *    return value is 6 (strlen("[cmd] ")).
1652  *  - cmd: allocated string containing "cmd arg1 arg2...". Caller frees.
1653  *  - argv: allocated array containing {"cmd", "arg1", "arg2", NULL}. Caller frees.
1654  */
1655 static ssize_t
pipe_argv_from_value(struct context * ctx,struct argv * argv)1656 pipe_argv_from_value(struct context *ctx, struct argv *argv)
1657 {
1658     argv->args = NULL;
1659 
1660     if (ctx->value[0] != '[')
1661         return 0;
1662 
1663     const char *pipe_cmd_end = strrchr(ctx->value, ']');
1664     if (pipe_cmd_end == NULL) {
1665         LOG_CONTEXTUAL_ERR("unclosed '['");
1666         return -1;
1667     }
1668 
1669     size_t pipe_len = pipe_cmd_end - ctx->value - 1;
1670     char *cmd = xstrndup(&ctx->value[1], pipe_len);
1671 
1672     if (!tokenize_cmdline(cmd, &argv->args)) {
1673         LOG_CONTEXTUAL_ERR("syntax error in command line");
1674         free(cmd);
1675         return -1;
1676     }
1677 
1678     ssize_t remove_len = pipe_cmd_end + 1 - ctx->value;
1679     ctx->value = pipe_cmd_end + 1;
1680     while (isspace(*ctx->value)) {
1681         ctx->value++;
1682         remove_len++;
1683     }
1684 
1685     free(cmd);
1686     return remove_len;
1687 }
1688 
1689 static void NOINLINE
remove_action_from_key_bindings_list(struct config_key_binding_list * bindings,int action,const struct argv * pipe_argv)1690 remove_action_from_key_bindings_list(struct config_key_binding_list *bindings,
1691                                      int action, const struct argv *pipe_argv)
1692 {
1693     size_t remove_first_idx = 0;
1694     size_t remove_count = 0;
1695 
1696     for (size_t i = 0; i < bindings->count; i++) {
1697         struct config_key_binding *binding = &bindings->arr[i];
1698 
1699         if (binding->action != action)
1700             continue;
1701 
1702         if (argv_compare(&binding->pipe.argv, pipe_argv) == 0) {
1703             if (remove_count++ == 0)
1704                 remove_first_idx = i;
1705 
1706             xassert(remove_first_idx + remove_count - 1 == i);
1707 
1708             if (binding->pipe.master_copy)
1709                 free_argv(&binding->pipe.argv);
1710         }
1711     }
1712 
1713     if (remove_count == 0)
1714         return;
1715 
1716     size_t move_count = bindings->count - (remove_first_idx + remove_count);
1717 
1718     memmove(
1719         &bindings->arr[remove_first_idx],
1720         &bindings->arr[remove_first_idx + remove_count],
1721         move_count * sizeof(bindings->arr[0]));
1722     bindings->count -= remove_count;
1723 }
1724 
1725 static bool NOINLINE
parse_key_binding_section(struct context * ctx,int action_count,const char * const action_map[static action_count],struct config_key_binding_list * bindings)1726 parse_key_binding_section(struct context *ctx,
1727                           int action_count,
1728                           const char *const action_map[static action_count],
1729                           struct config_key_binding_list *bindings)
1730 {
1731     struct argv pipe_argv;
1732 
1733     ssize_t pipe_remove_len = pipe_argv_from_value(ctx, &pipe_argv);
1734     if (pipe_remove_len < 0)
1735         return false;
1736 
1737     for (int action = 0; action < action_count; action++) {
1738         if (action_map[action] == NULL)
1739             continue;
1740 
1741         if (strcmp(ctx->key, action_map[action]) != 0)
1742             continue;
1743 
1744         /* Unset binding */
1745         if (strcasecmp(ctx->value, "none") == 0) {
1746             remove_action_from_key_bindings_list(bindings, action, &pipe_argv);
1747             free_argv(&pipe_argv);
1748             return true;
1749         }
1750 
1751         struct key_combo_list key_combos = {0};
1752         if (!value_to_key_combos(ctx, &key_combos) ||
1753             has_key_binding_collisions(
1754                 ctx, action, action_map, bindings, &key_combos, &pipe_argv))
1755         {
1756             free_argv(&pipe_argv);
1757             free_key_combo_list(&key_combos);
1758             return false;
1759         }
1760 
1761         remove_action_from_key_bindings_list(bindings, action, &pipe_argv);
1762 
1763         /* Emit key bindings */
1764         size_t ofs = bindings->count;
1765         bindings->count += key_combos.count;
1766         bindings->arr = xrealloc(
1767             bindings->arr, bindings->count * sizeof(bindings->arr[0]));
1768 
1769         bool first = true;
1770         for (size_t i = 0; i < key_combos.count; i++) {
1771             const struct key_combo *combo = &key_combos.combos[i];
1772             struct config_key_binding binding = {
1773                 .action = action,
1774                 .modifiers = combo->modifiers,
1775                 .sym = combo->sym,
1776                 .pipe = {
1777                     .argv = pipe_argv,
1778                     .master_copy = first,
1779                 },
1780             };
1781 
1782             /* TODO: we could re-use free:d slots */
1783             bindings->arr[ofs + i] = binding;
1784             first = false;
1785         }
1786 
1787         free_key_combo_list(&key_combos);
1788         return true;
1789     }
1790 
1791     LOG_CONTEXTUAL_ERR("not a valid action: %s", ctx->key);
1792     free_argv(&pipe_argv);
1793     return false;
1794 }
1795 
1796 UNITTEST
1797 {
1798     enum test_actions {
1799         TEST_ACTION_NONE,
1800         TEST_ACTION_FOO,
1801         TEST_ACTION_BAR,
1802         TEST_ACTION_COUNT,
1803     };
1804 
1805     const char *const map[] = {
1806         [TEST_ACTION_NONE] = NULL,
1807         [TEST_ACTION_FOO] = "foo",
1808         [TEST_ACTION_BAR] = "bar",
1809     };
1810 
1811     struct config conf = {0};
1812     struct config_key_binding_list bindings = {0};
1813 
1814     struct context ctx = {
1815         .conf = &conf,
1816         .section = "",
1817         .key = "foo",
1818         .value = "Escape",
1819         .path = "",
1820     };
1821 
1822     /*
1823      * ADD foo=Escape
1824      *
1825      * This verifies we can bind a single key combo to an action.
1826      */
1827     xassert(parse_key_binding_section(&ctx, ALEN(map), map, &bindings));
1828     xassert(bindings.count == 1);
1829     xassert(bindings.arr[0].action == TEST_ACTION_FOO);
1830     xassert(bindings.arr[0].sym == XKB_KEY_Escape);
1831 
1832     /*
1833      * ADD bar=Control+g Control+Shift+x
1834      *
1835      * This verifies we can bind multiple key combos to an action.
1836      */
1837     ctx.key = "bar";
1838     ctx.value = "Control+g Control+Shift+x";
1839     xassert(parse_key_binding_section(&ctx, ALEN(map), map, &bindings));
1840     xassert(bindings.count == 3);
1841     xassert(bindings.arr[0].action == TEST_ACTION_FOO);
1842     xassert(bindings.arr[1].action == TEST_ACTION_BAR);
1843     xassert(bindings.arr[1].sym == XKB_KEY_g);
1844     xassert(bindings.arr[1].modifiers.ctrl);
1845     xassert(bindings.arr[2].action == TEST_ACTION_BAR);
1846     xassert(bindings.arr[2].sym == XKB_KEY_x);
1847     xassert(bindings.arr[2].modifiers.ctrl && bindings.arr[2].modifiers.shift);
1848 
1849     /*
1850      * REPLACE foo with foo=Mod+v Shift+q
1851      *
1852      * This verifies we can update a single-combo action with multiple
1853      * key combos.
1854      */
1855     ctx.key = "foo";
1856     ctx.value = "Mod1+v Shift+q";
1857     xassert(parse_key_binding_section(&ctx, ALEN(map), map, &bindings));
1858     xassert(bindings.count == 4);
1859     xassert(bindings.arr[0].action == TEST_ACTION_BAR);
1860     xassert(bindings.arr[1].action == TEST_ACTION_BAR);
1861     xassert(bindings.arr[2].action == TEST_ACTION_FOO);
1862     xassert(bindings.arr[2].sym == XKB_KEY_v);
1863     xassert(bindings.arr[2].modifiers.alt);
1864     xassert(bindings.arr[3].action == TEST_ACTION_FOO);
1865     xassert(bindings.arr[3].sym == XKB_KEY_q);
1866     xassert(bindings.arr[3].modifiers.shift);
1867 
1868     /*
1869      * REMOVE bar
1870      */
1871     ctx.key = "bar";
1872     ctx.value = "none";
1873     xassert(parse_key_binding_section(&ctx, ALEN(map), map, &bindings));
1874     xassert(bindings.count == 2);
1875     xassert(bindings.arr[0].action == TEST_ACTION_FOO);
1876     xassert(bindings.arr[1].action == TEST_ACTION_FOO);
1877 
1878     /*
1879      * REMOVE foo
1880      */
1881     ctx.key = "foo";
1882     ctx.value = "none";
1883     xassert(parse_key_binding_section(&ctx, ALEN(map), map, &bindings));
1884     xassert(bindings.count == 0);
1885 
1886     free(bindings.arr);
1887 }
1888 
1889 static bool
parse_section_key_bindings(struct context * ctx)1890 parse_section_key_bindings(struct context *ctx)
1891 {
1892     return parse_key_binding_section(
1893         ctx,
1894         BIND_ACTION_KEY_COUNT, binding_action_map,
1895         &ctx->conf->bindings.key);
1896 }
1897 
1898 static bool
parse_section_search_bindings(struct context * ctx)1899 parse_section_search_bindings(struct context *ctx)
1900 {
1901     static const char *const search_binding_action_map[] = {
1902         [BIND_ACTION_SEARCH_NONE] = NULL,
1903         [BIND_ACTION_SEARCH_CANCEL] = "cancel",
1904         [BIND_ACTION_SEARCH_COMMIT] = "commit",
1905         [BIND_ACTION_SEARCH_FIND_PREV] = "find-prev",
1906         [BIND_ACTION_SEARCH_FIND_NEXT] = "find-next",
1907         [BIND_ACTION_SEARCH_EDIT_LEFT] = "cursor-left",
1908         [BIND_ACTION_SEARCH_EDIT_LEFT_WORD] = "cursor-left-word",
1909         [BIND_ACTION_SEARCH_EDIT_RIGHT] = "cursor-right",
1910         [BIND_ACTION_SEARCH_EDIT_RIGHT_WORD] = "cursor-right-word",
1911         [BIND_ACTION_SEARCH_EDIT_HOME] = "cursor-home",
1912         [BIND_ACTION_SEARCH_EDIT_END] = "cursor-end",
1913         [BIND_ACTION_SEARCH_DELETE_PREV] = "delete-prev",
1914         [BIND_ACTION_SEARCH_DELETE_PREV_WORD] = "delete-prev-word",
1915         [BIND_ACTION_SEARCH_DELETE_NEXT] = "delete-next",
1916         [BIND_ACTION_SEARCH_DELETE_NEXT_WORD] = "delete-next-word",
1917         [BIND_ACTION_SEARCH_EXTEND_WORD] = "extend-to-word-boundary",
1918         [BIND_ACTION_SEARCH_EXTEND_WORD_WS] = "extend-to-next-whitespace",
1919         [BIND_ACTION_SEARCH_CLIPBOARD_PASTE] = "clipboard-paste",
1920         [BIND_ACTION_SEARCH_PRIMARY_PASTE] = "primary-paste",
1921     };
1922 
1923     static_assert(ALEN(search_binding_action_map) == BIND_ACTION_SEARCH_COUNT,
1924                   "search binding action map size mismatch");
1925 
1926     return parse_key_binding_section(
1927         ctx,
1928         BIND_ACTION_SEARCH_COUNT, search_binding_action_map,
1929         &ctx->conf->bindings.search);
1930 }
1931 
1932 static bool
parse_section_url_bindings(struct context * ctx)1933 parse_section_url_bindings(struct context *ctx)
1934 {
1935     static const char *const url_binding_action_map[] = {
1936         [BIND_ACTION_URL_NONE] = NULL,
1937         [BIND_ACTION_URL_CANCEL] = "cancel",
1938         [BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL] = "toggle-url-visible",
1939     };
1940 
1941     static_assert(ALEN(url_binding_action_map) == BIND_ACTION_URL_COUNT,
1942                   "URL binding action map size mismatch");
1943 
1944     return parse_key_binding_section(
1945         ctx,
1946         BIND_ACTION_URL_COUNT, url_binding_action_map,
1947         &ctx->conf->bindings.url);
1948 }
1949 
1950 static bool
value_to_mouse_combos(struct context * ctx,struct key_combo_list * key_combos)1951 value_to_mouse_combos(struct context *ctx, struct key_combo_list *key_combos)
1952 {
1953     xassert(key_combos != NULL);
1954     xassert(key_combos->count == 0 && key_combos->combos == NULL);
1955 
1956     size_t size = 0;  /* Size of the ‘combos’ array in key_combos */
1957 
1958     char *copy = xstrdup(ctx->value);
1959 
1960     for (char *tok_ctx = NULL, *combo = strtok_r(copy, " ", &tok_ctx);
1961          combo != NULL;
1962          combo = strtok_r(NULL, " ", &tok_ctx))
1963     {
1964         struct config_key_modifiers modifiers = {0};
1965         char *key = strrchr(combo, '+');
1966 
1967         if (key == NULL) {
1968             /* No modifiers */
1969             key = combo;
1970         } else {
1971             *key = '\0';
1972             if (!parse_modifiers(ctx, combo, key - combo, &modifiers))
1973                 goto err;
1974             if (modifiers.shift) {
1975                 LOG_CONTEXTUAL_ERR("Shift cannot be used in mouse bindings");
1976                 goto err;
1977             }
1978             key++;  /* Skip past the '+' */
1979         }
1980 
1981         size_t count = 1;
1982         {
1983             char *_count = strrchr(key, '-');
1984             if (_count != NULL) {
1985                 *_count = '\0';
1986                 _count++;
1987 
1988                 errno = 0;
1989                 char *end;
1990                 unsigned long value = strtoul(_count, &end, 10);
1991                 if (_count[0] == '\0' || *end != '\0' || errno != 0) {
1992                     if (errno != 0)
1993                         LOG_CONTEXTUAL_ERRNO("invalid click count: %s", _count);
1994                     else
1995                         LOG_CONTEXTUAL_ERR("invalid click count: %s", _count);
1996                     goto err;
1997                 }
1998                 count = value;
1999             }
2000         }
2001 
2002         static const struct {
2003             const char *name;
2004             int code;
2005         } map[] = {
2006             {"BTN_LEFT", BTN_LEFT},
2007             {"BTN_RIGHT", BTN_RIGHT},
2008             {"BTN_MIDDLE", BTN_MIDDLE},
2009             {"BTN_SIDE", BTN_SIDE},
2010             {"BTN_EXTRA", BTN_EXTRA},
2011             {"BTN_FORWARD", BTN_FORWARD},
2012             {"BTN_BACK", BTN_BACK},
2013             {"BTN_TASK", BTN_TASK},
2014         };
2015 
2016         int button = 0;
2017         for (size_t i = 0; i < ALEN(map); i++) {
2018             if (strcmp(key, map[i].name) == 0) {
2019                 button = map[i].code;
2020                 break;
2021             }
2022         }
2023 
2024         if (button == 0) {
2025             LOG_CONTEXTUAL_ERR("invalid mouse button name: %s", key);
2026             goto err;
2027         }
2028 
2029         struct key_combo new = {
2030             .text = xstrdup(combo),
2031             .modifiers = modifiers,
2032             .m = {
2033                 .button = button,
2034                 .count = count,
2035             },
2036         };
2037 
2038         if (key_combos->count + 1 > size) {
2039             size += 4;
2040             key_combos->combos = xrealloc(
2041                 key_combos->combos, size * sizeof(key_combos->combos[0]));
2042         }
2043 
2044         xassert(key_combos->count + 1 <= size);
2045         key_combos->combos[key_combos->count++] = new;
2046     }
2047 
2048     free(copy);
2049     return true;
2050 
2051 err:
2052     free_key_combo_list(key_combos);
2053     free(copy);
2054     return false;
2055 }
2056 
2057 static bool
has_mouse_binding_collisions(struct context * ctx,const struct key_combo_list * key_combos)2058 has_mouse_binding_collisions(struct context *ctx,
2059                              const struct key_combo_list *key_combos)
2060 {
2061     struct config *conf = ctx->conf;
2062 
2063     for (size_t j = 0; j < conf->bindings.mouse.count; j++) {
2064         const struct config_mouse_binding *combo1 = &conf->bindings.mouse.arr[j];
2065         if (combo1->action == BIND_ACTION_NONE)
2066             continue;
2067 
2068         for (size_t i = 0; i < key_combos->count; i++) {
2069             const struct key_combo *combo2 = &key_combos->combos[i];
2070 
2071             const struct config_key_modifiers *mods1 = &combo1->modifiers;
2072             const struct config_key_modifiers *mods2 = &combo2->modifiers;
2073 
2074             bool shift = mods1->shift == mods2->shift;
2075             bool alt = mods1->alt == mods2->alt;
2076             bool ctrl = mods1->ctrl == mods2->ctrl;
2077             bool meta = mods1->meta == mods2->meta;
2078             bool button = combo1->button == combo2->m.button;
2079             bool count = combo1->count == combo2->m.count;
2080 
2081             if (shift && alt && ctrl && meta && button && count) {
2082                 bool has_pipe = combo1->pipe.argv.args != NULL;
2083                 LOG_CONTEXTUAL_ERR("%s already mapped to '%s%s%s%s'",
2084                                    combo2->text,
2085                                    binding_action_map[combo1->action],
2086                                    has_pipe ? " [" : "",
2087                                    has_pipe ? combo1->pipe.argv.args[0] : "",
2088                                    has_pipe ? "]" : "");
2089                 return true;
2090             }
2091         }
2092     }
2093 
2094     return false;
2095 }
2096 
2097 
2098 static bool
parse_section_mouse_bindings(struct context * ctx)2099 parse_section_mouse_bindings(struct context *ctx)
2100 {
2101     struct config *conf = ctx->conf;
2102     const char *key = ctx->key;
2103     const char *value = ctx->value;
2104 
2105     struct argv pipe_argv;
2106 
2107     ssize_t pipe_remove_len = pipe_argv_from_value(ctx, &pipe_argv);
2108     if (pipe_remove_len < 0)
2109         return false;
2110 
2111     for (enum bind_action_normal action = 0;
2112          action < BIND_ACTION_COUNT;
2113          action++)
2114     {
2115         if (binding_action_map[action] == NULL)
2116             continue;
2117 
2118         if (strcmp(key, binding_action_map[action]) != 0)
2119             continue;
2120 
2121         /* Unset binding */
2122         if (strcasecmp(value, "none") == 0) {
2123             for (size_t i = 0; i < conf->bindings.mouse.count; i++) {
2124                 struct config_mouse_binding *binding =
2125                     &conf->bindings.mouse.arr[i];
2126 
2127                 if (binding->action == action) {
2128                     if (binding->pipe.master_copy)
2129                         free_argv(&binding->pipe.argv);
2130                     binding->action = BIND_ACTION_NONE;
2131                 }
2132             }
2133             free_argv(&pipe_argv);
2134             return true;
2135         }
2136 
2137         struct key_combo_list key_combos = {0};
2138         if (!value_to_mouse_combos(ctx, &key_combos) ||
2139             has_mouse_binding_collisions(ctx, &key_combos))
2140         {
2141             free_argv(&pipe_argv);
2142             free_key_combo_list(&key_combos);
2143             return false;
2144         }
2145 
2146         /* Remove existing bindings for this action */
2147         for (size_t i = 0; i < conf->bindings.mouse.count; i++) {
2148             struct config_mouse_binding *binding = &conf->bindings.mouse.arr[i];
2149 
2150             if (binding->action != action)
2151                 continue;
2152 
2153             if (argv_compare(&binding->pipe.argv, &pipe_argv) == 0) {
2154                 if (binding->pipe.master_copy)
2155                     free_argv(&binding->pipe.argv);
2156                 binding->action = BIND_ACTION_NONE;
2157             }
2158         }
2159 
2160         /* Emit mouse bindings */
2161         size_t ofs = conf->bindings.mouse.count;
2162         conf->bindings.mouse.count += key_combos.count;
2163         conf->bindings.mouse.arr = xrealloc(
2164             conf->bindings.mouse.arr,
2165             conf->bindings.mouse.count * sizeof(conf->bindings.mouse.arr[0]));
2166 
2167         bool first = true;
2168         for (size_t i = 0; i < key_combos.count; i++) {
2169             const struct key_combo *combo = &key_combos.combos[i];
2170             struct config_mouse_binding binding = {
2171                 .action = action,
2172                 .modifiers = combo->modifiers,
2173                 .button = combo->m.button,
2174                 .count = combo->m.count,
2175                 .pipe = {
2176                     .argv = pipe_argv,
2177                     .master_copy = first,
2178                 },
2179             };
2180 
2181             conf->bindings.mouse.arr[ofs + i] = binding;
2182             first = false;
2183         }
2184 
2185         free_key_combo_list(&key_combos);
2186         return true;
2187     }
2188 
2189     LOG_CONTEXTUAL_ERR("not a valid option: %s", key);
2190     free_argv(&pipe_argv);
2191     return false;
2192 }
2193 
2194 static bool
parse_section_tweak(struct context * ctx)2195 parse_section_tweak(struct context *ctx)
2196 {
2197     struct config *conf = ctx->conf;
2198     const char *key = ctx->key;
2199 
2200     if (strcmp(key, "scaling-filter") == 0) {
2201         static const char *filters[] = {
2202             [FCFT_SCALING_FILTER_NONE] = "none",
2203             [FCFT_SCALING_FILTER_NEAREST] = "nearest",
2204             [FCFT_SCALING_FILTER_BILINEAR] = "bilinear",
2205             [FCFT_SCALING_FILTER_CUBIC] = "cubic",
2206             [FCFT_SCALING_FILTER_LANCZOS3] = "lanczos3",
2207             NULL,
2208         };
2209 
2210         _Static_assert(sizeof(conf->tweak.fcft_filter) == sizeof(int),
2211                        "enum is not 32-bit");
2212 
2213         return value_to_enum(ctx, filters, (int *)&conf->tweak.fcft_filter);
2214     }
2215 
2216     else if (strcmp(key, "overflowing-glyphs") == 0)
2217         return value_to_bool(ctx, &conf->tweak.overflowing_glyphs);
2218 
2219     else if (strcmp(key, "damage-whole-window") == 0)
2220         return value_to_bool(ctx, &conf->tweak.damage_whole_window);
2221 
2222     else if (strcmp(key, "grapheme-shaping") == 0) {
2223         if (!value_to_bool(ctx, &conf->tweak.grapheme_shaping))
2224             return false;
2225 
2226 #if !defined(FOOT_GRAPHEME_CLUSTERING)
2227         if (conf->tweak.grapheme_shaping) {
2228             LOG_CONTEXTUAL_WARN(
2229                 "foot was not compiled with support for grapheme shaping");
2230             conf->tweak.grapheme_shaping = false;
2231         }
2232 #endif
2233 
2234         if (conf->tweak.grapheme_shaping && !conf->can_shape_grapheme) {
2235             LOG_WARN(
2236                 "fcft was not compiled with support for grapheme shaping");
2237 
2238             /* Keep it enabled though - this will cause us to do
2239              * grapheme-clustering at least */
2240         }
2241 
2242         return true;
2243     }
2244 
2245     else if (strcmp(key, "grapheme-width-method") == 0) {
2246         _Static_assert(sizeof(conf->tweak.grapheme_width_method) == sizeof(int),
2247                        "enum is not 32-bit");
2248 
2249         return value_to_enum(
2250             ctx,
2251             (const char *[]){"wcswidth", "double-width", "max", NULL},
2252             (int *)&conf->tweak.grapheme_width_method);
2253     }
2254 
2255     else if (strcmp(key, "render-timer") == 0) {
2256         int mode;
2257 
2258         if (!value_to_enum(
2259                 ctx,
2260                 (const char *[]){"none", "osd", "log", "both", NULL},
2261                 &mode))
2262         {
2263             return false;
2264         }
2265 
2266         xassert(0 <= mode && mode <= 3);
2267         conf->tweak.render_timer_osd = mode == 1 || mode == 3;
2268         conf->tweak.render_timer_log = mode == 2 || mode == 3;
2269         return true;
2270     }
2271 
2272     else if (strcmp(key, "delayed-render-lower") == 0) {
2273         uint32_t ns;
2274         if (!value_to_uint32(ctx, 10, &ns))
2275             return false;
2276 
2277         if (ns > 16666666) {
2278             LOG_CONTEXTUAL_ERR("timeout must not exceed 16ms");
2279             return false;
2280         }
2281 
2282         conf->tweak.delayed_render_lower_ns = ns;
2283         return true;
2284     }
2285 
2286     else if (strcmp(key, "delayed-render-upper") == 0) {
2287         uint32_t ns;
2288         if (!value_to_uint32(ctx, 10, &ns))
2289             return false;
2290 
2291         if (ns > 16666666) {
2292             LOG_CONTEXTUAL_ERR("timeout must not exceed 16ms");
2293             return false;
2294         }
2295 
2296         conf->tweak.delayed_render_upper_ns = ns;
2297         return true;
2298     }
2299 
2300     else if (strcmp(key, "max-shm-pool-size-mb") == 0) {
2301         uint32_t mb;
2302         if (!value_to_uint32(ctx, 10, &mb))
2303             return false;
2304 
2305         conf->tweak.max_shm_pool_size = min((int32_t)mb * 1024 * 1024, INT32_MAX);
2306         return true;
2307     }
2308 
2309     else if (strcmp(key, "box-drawing-base-thickness") == 0)
2310         return value_to_double(ctx, &conf->tweak.box_drawing_base_thickness);
2311 
2312     else if (strcmp(key, "box-drawing-solid-shades") == 0)
2313         return value_to_bool(ctx, &conf->tweak.box_drawing_solid_shades);
2314 
2315     else if (strcmp(key, "font-monospace-warn") == 0)
2316         return value_to_bool(ctx, &conf->tweak.font_monospace_warn);
2317 
2318     else {
2319         LOG_CONTEXTUAL_ERR("not a valid option: %s", key);
2320         return false;
2321     }
2322 }
2323 
2324 static bool
parse_key_value(char * kv,const char ** section,const char ** key,const char ** value)2325 parse_key_value(char *kv, const char **section, const char **key, const char **value)
2326 {
2327     /*strip leading whitespace*/
2328     while (*kv && isspace(*kv))
2329         ++kv;
2330 
2331     if (section != NULL)
2332         *section = NULL;
2333     *key = kv;
2334     *value = NULL;
2335 
2336     size_t kvlen = strlen(kv);
2337     for (size_t i = 0; i < kvlen; ++i) {
2338         if (kv[i] == '.') {
2339             if (section != NULL && *section == NULL) {
2340                 *section = kv;
2341                 kv[i] = '\0';
2342                 *key = &kv[i + 1];
2343             }
2344         } else if (kv[i] == '=') {
2345             if (section != NULL && *section == NULL)
2346                 *section = "main";
2347             kv[i] = '\0';
2348             *value = &kv[i + 1];
2349             break;
2350         }
2351     }
2352     if (*value == NULL)
2353         return false;
2354 
2355     /* Strip trailing whitespace from key (leading stripped earlier) */
2356     {
2357         xassert(!isspace(**key));
2358 
2359         char *end = (char *)*key + strlen(*key) - 1;
2360         while (isspace(*end))
2361             end--;
2362         *(end + 1) = '\0';
2363     }
2364 
2365     /* Strip leading+trailing whitespace from valueue */
2366     {
2367         while (isspace(**value))
2368             ++*value;
2369 
2370         if (*value[0] != '\0') {
2371             char *end = (char *)*value + strlen(*value) - 1;
2372             while (isspace(*end))
2373                 end--;
2374             *(end + 1) = '\0';
2375         }
2376     }
2377     return true;
2378 }
2379 
2380 enum section {
2381     SECTION_MAIN,
2382     SECTION_BELL,
2383     SECTION_SCROLLBACK,
2384     SECTION_URL,
2385     SECTION_COLORS,
2386     SECTION_CURSOR,
2387     SECTION_MOUSE,
2388     SECTION_CSD,
2389     SECTION_KEY_BINDINGS,
2390     SECTION_SEARCH_BINDINGS,
2391     SECTION_URL_BINDINGS,
2392     SECTION_MOUSE_BINDINGS,
2393     SECTION_TWEAK,
2394     SECTION_COUNT,
2395 };
2396 
2397 /* Function pointer, called for each key/value line */
2398 typedef bool (*parser_fun_t)(struct context *ctx);
2399 
2400 static const struct {
2401     parser_fun_t fun;
2402     const char *name;
2403 } section_info[] = {
2404     [SECTION_MAIN] =            {&parse_section_main, "main"},
2405     [SECTION_BELL] =            {&parse_section_bell, "bell"},
2406     [SECTION_SCROLLBACK] =      {&parse_section_scrollback, "scrollback"},
2407     [SECTION_URL] =             {&parse_section_url, "url"},
2408     [SECTION_COLORS] =          {&parse_section_colors, "colors"},
2409     [SECTION_CURSOR] =          {&parse_section_cursor, "cursor"},
2410     [SECTION_MOUSE] =           {&parse_section_mouse, "mouse"},
2411     [SECTION_CSD] =             {&parse_section_csd, "csd"},
2412     [SECTION_KEY_BINDINGS] =    {&parse_section_key_bindings, "key-bindings"},
2413     [SECTION_SEARCH_BINDINGS] = {&parse_section_search_bindings, "search-bindings"},
2414     [SECTION_URL_BINDINGS] =    {&parse_section_url_bindings, "url-bindings"},
2415     [SECTION_MOUSE_BINDINGS] =  {&parse_section_mouse_bindings, "mouse-bindings"},
2416     [SECTION_TWEAK] =           {&parse_section_tweak, "tweak"},
2417 };
2418 
2419 static_assert(ALEN(section_info) == SECTION_COUNT, "section info array size mismatch");
2420 
2421 static enum section
str_to_section(const char * str)2422 str_to_section(const char *str)
2423 {
2424     for (enum section section = SECTION_MAIN; section < SECTION_COUNT; ++section) {
2425         if (strcmp(str, section_info[section].name) == 0)
2426             return section;
2427     }
2428     return SECTION_COUNT;
2429 }
2430 
2431 static bool
parse_config_file(FILE * f,struct config * conf,const char * path,bool errors_are_fatal)2432 parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_are_fatal)
2433 {
2434     enum section section = SECTION_MAIN;
2435 
2436     char *_line = NULL;
2437     size_t count = 0;
2438 
2439 #define error_or_continue()                     \
2440     {                                           \
2441         if (errors_are_fatal)                   \
2442             goto err;                           \
2443         else                                    \
2444             continue;                           \
2445     }
2446 
2447     char *section_name = xstrdup("main");
2448 
2449     struct context context = {
2450         .conf = conf,
2451         .section = section_name,
2452         .path = path,
2453         .lineno = 0,
2454         .errors_are_fatal = errors_are_fatal,
2455     };
2456     struct context *ctx = &context;  /* For LOG_AND_*() */
2457 
2458     while (true) {
2459         errno = 0;
2460         context.lineno++;
2461 
2462         ssize_t ret = getline(&_line, &count, f);
2463 
2464         if (ret < 0) {
2465             if (errno != 0) {
2466                 LOG_AND_NOTIFY_ERRNO("failed to read from configuration");
2467                 if (errors_are_fatal)
2468                     goto err;
2469             }
2470             break;
2471         }
2472 
2473         /* Strip leading whitespace */
2474         char *line = _line;
2475         {
2476             while (isspace(*line))
2477                 line++;
2478             if (line[0] != '\0') {
2479                 char *end = line + strlen(line) - 1;
2480                 while (isspace(*end))
2481                     end--;
2482                 *(end + 1) = '\0';
2483             }
2484         }
2485 
2486         /* Empty line, or comment */
2487         if (line[0] == '\0' || line[0] == '#')
2488             continue;
2489 
2490         /* Split up into key/value pair + trailing comment separated by blank */
2491         char *key_value = line;
2492         char *comment = line;
2493         while (comment[0] != '\0') {
2494             const char c = comment[0];
2495             comment++;
2496             if (isblank(c) && comment[0] == '#') {
2497                 comment[0] = '\0'; /* Terminate key/value pair */
2498                 comment++;
2499                 break;
2500             }
2501         }
2502 
2503         /* Check for new section */
2504         if (key_value[0] == '[') {
2505             char *end = strchr(key_value, ']');
2506             if (end == NULL) {
2507                 LOG_CONTEXTUAL_ERR("syntax error: no closing ']'");
2508                 error_or_continue();
2509             }
2510 
2511             *end = '\0';
2512 
2513             section = str_to_section(&key_value[1]);
2514             if (section == SECTION_COUNT) {
2515                 LOG_CONTEXTUAL_ERR("invalid section name: %s", &key_value[1]);
2516                 error_or_continue();
2517             }
2518 
2519             free(section_name);
2520             section_name = xstrdup(&key_value[1]);
2521             context.section = section_name;
2522 
2523             /* Process next line */
2524             continue;
2525         }
2526 
2527         if (section >= SECTION_COUNT) {
2528             /* Last section name was invalid; ignore all keys in it */
2529             continue;
2530         }
2531 
2532         if (!parse_key_value(key_value, NULL, &context.key, &context.value)) {
2533             LOG_CONTEXTUAL_ERR("syntax error: key/value pair has no value");
2534             if (errors_are_fatal)
2535                 goto err;
2536             break;
2537         }
2538 
2539         LOG_DBG("section=%s, key='%s', value='%s', comment='%s'",
2540                 section_info[section].name, key, value, comment);
2541 
2542         xassert(section >= 0 && section < SECTION_COUNT);
2543 
2544         parser_fun_t section_parser = section_info[section].fun;
2545         xassert(section_parser != NULL);
2546 
2547         if (!section_parser(ctx))
2548             error_or_continue();
2549     }
2550 
2551     free(section_name);
2552     free(_line);
2553     return true;
2554 
2555 err:
2556     free(section_name);
2557     free(_line);
2558     return false;
2559 }
2560 
2561 static char *
get_server_socket_path(void)2562 get_server_socket_path(void)
2563 {
2564     const char *xdg_runtime = getenv("XDG_RUNTIME_DIR");
2565     if (xdg_runtime == NULL)
2566         return xstrdup("/tmp/foot.sock");
2567 
2568     const char *wayland_display = getenv("WAYLAND_DISPLAY");
2569     if (wayland_display == NULL) {
2570         return xasprintf("%s/foot.sock", xdg_runtime);
2571     }
2572 
2573     return xasprintf("%s/foot-%s.sock", xdg_runtime, wayland_display);
2574 }
2575 
2576 #define m_none       {0}
2577 #define m_alt        {.alt = true}
2578 #define m_ctrl       {.ctrl = true}
2579 #define m_shift      {.shift = true}
2580 #define m_ctrl_shift {.ctrl = true, .shift = true}
2581 
2582 static void
add_default_key_bindings(struct config * conf)2583 add_default_key_bindings(struct config *conf)
2584 {
2585     static const struct config_key_binding bindings[] = {
2586         {BIND_ACTION_SCROLLBACK_UP_PAGE, m_shift, XKB_KEY_Page_Up},
2587         {BIND_ACTION_SCROLLBACK_DOWN_PAGE, m_shift, XKB_KEY_Page_Down},
2588         {BIND_ACTION_CLIPBOARD_COPY, m_ctrl_shift, XKB_KEY_c},
2589         {BIND_ACTION_CLIPBOARD_PASTE, m_ctrl_shift, XKB_KEY_v},
2590         {BIND_ACTION_PRIMARY_PASTE, m_shift, XKB_KEY_Insert},
2591         {BIND_ACTION_SEARCH_START, m_ctrl_shift, XKB_KEY_r},
2592         {BIND_ACTION_FONT_SIZE_UP, m_ctrl, XKB_KEY_plus},
2593         {BIND_ACTION_FONT_SIZE_UP, m_ctrl, XKB_KEY_equal},
2594         {BIND_ACTION_FONT_SIZE_UP, m_ctrl, XKB_KEY_KP_Add},
2595         {BIND_ACTION_FONT_SIZE_DOWN, m_ctrl, XKB_KEY_minus},
2596         {BIND_ACTION_FONT_SIZE_DOWN, m_ctrl, XKB_KEY_KP_Subtract},
2597         {BIND_ACTION_FONT_SIZE_RESET, m_ctrl, XKB_KEY_0},
2598         {BIND_ACTION_FONT_SIZE_RESET, m_ctrl, XKB_KEY_KP_0},
2599         {BIND_ACTION_SPAWN_TERMINAL, m_ctrl_shift, XKB_KEY_n},
2600         {BIND_ACTION_SHOW_URLS_LAUNCH, m_ctrl_shift, XKB_KEY_u},
2601     };
2602 
2603     conf->bindings.key.count = ALEN(bindings);
2604     conf->bindings.key.arr = xmalloc(sizeof(bindings));
2605     memcpy(conf->bindings.key.arr, bindings, sizeof(bindings));
2606 }
2607 
2608 
2609 static void
add_default_search_bindings(struct config * conf)2610 add_default_search_bindings(struct config *conf)
2611 {
2612     static const struct config_key_binding bindings[] = {
2613         {BIND_ACTION_SEARCH_CANCEL, m_ctrl, XKB_KEY_c},
2614         {BIND_ACTION_SEARCH_CANCEL, m_ctrl, XKB_KEY_g},
2615         {BIND_ACTION_SEARCH_CANCEL, m_none, XKB_KEY_Escape},
2616         {BIND_ACTION_SEARCH_COMMIT, m_none, XKB_KEY_Return},
2617         {BIND_ACTION_SEARCH_FIND_PREV, m_ctrl, XKB_KEY_r},
2618         {BIND_ACTION_SEARCH_FIND_NEXT, m_ctrl, XKB_KEY_s},
2619         {BIND_ACTION_SEARCH_EDIT_LEFT, m_none, XKB_KEY_Left},
2620         {BIND_ACTION_SEARCH_EDIT_LEFT, m_ctrl, XKB_KEY_b},
2621         {BIND_ACTION_SEARCH_EDIT_LEFT_WORD, m_ctrl, XKB_KEY_Left},
2622         {BIND_ACTION_SEARCH_EDIT_LEFT_WORD, m_alt, XKB_KEY_b},
2623         {BIND_ACTION_SEARCH_EDIT_RIGHT, m_none, XKB_KEY_Right},
2624         {BIND_ACTION_SEARCH_EDIT_RIGHT, m_ctrl, XKB_KEY_f},
2625         {BIND_ACTION_SEARCH_EDIT_RIGHT_WORD, m_ctrl, XKB_KEY_Right},
2626         {BIND_ACTION_SEARCH_EDIT_RIGHT_WORD, m_alt, XKB_KEY_f},
2627         {BIND_ACTION_SEARCH_EDIT_HOME, m_none, XKB_KEY_Home},
2628         {BIND_ACTION_SEARCH_EDIT_HOME, m_ctrl, XKB_KEY_a},
2629         {BIND_ACTION_SEARCH_EDIT_END, m_none, XKB_KEY_End},
2630         {BIND_ACTION_SEARCH_EDIT_END, m_ctrl, XKB_KEY_e},
2631         {BIND_ACTION_SEARCH_DELETE_PREV, m_none, XKB_KEY_BackSpace},
2632         {BIND_ACTION_SEARCH_DELETE_PREV_WORD, m_ctrl, XKB_KEY_BackSpace},
2633         {BIND_ACTION_SEARCH_DELETE_PREV_WORD, m_alt, XKB_KEY_BackSpace},
2634         {BIND_ACTION_SEARCH_DELETE_NEXT, m_none, XKB_KEY_Delete},
2635         {BIND_ACTION_SEARCH_DELETE_NEXT_WORD, m_ctrl, XKB_KEY_Delete},
2636         {BIND_ACTION_SEARCH_DELETE_NEXT_WORD, m_alt, XKB_KEY_d},
2637         {BIND_ACTION_SEARCH_EXTEND_WORD, m_ctrl, XKB_KEY_w},
2638         {BIND_ACTION_SEARCH_EXTEND_WORD_WS, m_ctrl_shift, XKB_KEY_w},
2639         {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m_ctrl, XKB_KEY_v},
2640         {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m_ctrl, XKB_KEY_y},
2641         {BIND_ACTION_SEARCH_PRIMARY_PASTE, m_shift, XKB_KEY_Insert},
2642     };
2643 
2644     conf->bindings.search.count = ALEN(bindings);
2645     conf->bindings.search.arr = xmalloc(sizeof(bindings));
2646     memcpy(conf->bindings.search.arr, bindings, sizeof(bindings));
2647 }
2648 
2649 static void
add_default_url_bindings(struct config * conf)2650 add_default_url_bindings(struct config *conf)
2651 {
2652     static const struct config_key_binding bindings[] = {
2653         {BIND_ACTION_URL_CANCEL, m_ctrl, XKB_KEY_c},
2654         {BIND_ACTION_URL_CANCEL, m_ctrl, XKB_KEY_g},
2655         {BIND_ACTION_URL_CANCEL, m_ctrl, XKB_KEY_d},
2656         {BIND_ACTION_URL_CANCEL, m_none, XKB_KEY_Escape},
2657         {BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL, m_none, XKB_KEY_t},
2658     };
2659 
2660     conf->bindings.url.count = ALEN(bindings);
2661     conf->bindings.url.arr = xmalloc(sizeof(bindings));
2662     memcpy(conf->bindings.url.arr, bindings, sizeof(bindings));
2663 }
2664 
2665 static void
add_default_mouse_bindings(struct config * conf)2666 add_default_mouse_bindings(struct config *conf)
2667 {
2668     static const struct config_mouse_binding bindings[] = {
2669         {BIND_ACTION_PRIMARY_PASTE, m_none, BTN_MIDDLE, 1},
2670         {BIND_ACTION_SELECT_BEGIN, m_none, BTN_LEFT, 1},
2671         {BIND_ACTION_SELECT_BEGIN_BLOCK, m_ctrl, BTN_LEFT, 1},
2672         {BIND_ACTION_SELECT_EXTEND, m_none, BTN_RIGHT, 1},
2673         {BIND_ACTION_SELECT_EXTEND_CHAR_WISE, m_ctrl, BTN_RIGHT, 1},
2674         {BIND_ACTION_SELECT_WORD, m_none, BTN_LEFT, 2},
2675         {BIND_ACTION_SELECT_WORD_WS, m_ctrl, BTN_LEFT, 2},
2676         {BIND_ACTION_SELECT_ROW, m_none, BTN_LEFT, 3},
2677     };
2678 
2679     conf->bindings.mouse.count = ALEN(bindings);
2680     conf->bindings.mouse.arr = xmalloc(sizeof(bindings));
2681     memcpy(conf->bindings.mouse.arr, bindings, sizeof(bindings));
2682 }
2683 
2684 static void NOINLINE
config_font_list_clone(struct config_font_list * dst,const struct config_font_list * src)2685 config_font_list_clone(struct config_font_list *dst,
2686                        const struct config_font_list *src)
2687 {
2688     dst->count = src->count;
2689     dst->arr = xmalloc(dst->count * sizeof(dst->arr[0]));
2690 
2691     for (size_t j = 0; j < dst->count; j++) {
2692         dst->arr[j].pt_size = src->arr[j].pt_size;
2693         dst->arr[j].px_size = src->arr[j].px_size;
2694         dst->arr[j].pattern = xstrdup(src->arr[j].pattern);
2695     }
2696 }
2697 
2698 bool
config_load(struct config * conf,const char * conf_path,user_notifications_t * initial_user_notifications,config_override_t * overrides,bool errors_are_fatal)2699 config_load(struct config *conf, const char *conf_path,
2700             user_notifications_t *initial_user_notifications,
2701             config_override_t *overrides, bool errors_are_fatal)
2702 {
2703     bool ret = false;
2704     enum fcft_capabilities fcft_caps = fcft_capabilities();
2705 
2706     *conf = (struct config) {
2707         .term = xstrdup(FOOT_DEFAULT_TERM),
2708         .shell = get_shell(),
2709         .title = xstrdup("foot"),
2710         .app_id = xstrdup("foot"),
2711         .word_delimiters = xwcsdup(L",│`|:\"'()[]{}<>"),
2712         .size = {
2713             .type = CONF_SIZE_PX,
2714             .width = 700,
2715             .height = 500,
2716         },
2717         .pad_x = 2,
2718         .pad_y = 2,
2719         .resize_delay_ms = 100,
2720         .bold_in_bright = {
2721             .enabled = false,
2722             .palette_based = false,
2723         },
2724         .startup_mode = STARTUP_WINDOWED,
2725         .fonts = {{0}},
2726         .line_height = {.pt = 0, .px = -1},
2727         .letter_spacing = {.pt = 0, .px = 0},
2728         .horizontal_letter_offset = {.pt = 0, .px = 0},
2729         .vertical_letter_offset = {.pt = 0, .px = 0},
2730         .use_custom_underline_offset = false,
2731         .box_drawings_uses_font_glyphs = false,
2732         .dpi_aware = DPI_AWARE_AUTO, /* DPI-aware when scaling-factor == 1 */
2733         .bell = {
2734             .urgent = false,
2735             .notify = false,
2736             .command = {
2737                 .argv = {.args = NULL},
2738             },
2739             .command_focused = false,
2740         },
2741         .url = {
2742             .label_letters = xwcsdup(L"sadfjklewcmpgh"),
2743             .uri_characters = xwcsdup(L"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.,~:;/?#@!$&%*+=\"'()[]"),
2744             .osc8_underline = OSC8_UNDERLINE_URL_MODE,
2745         },
2746         .can_shape_grapheme = fcft_caps & FCFT_CAPABILITY_GRAPHEME_SHAPING,
2747         .scrollback = {
2748             .lines = 1000,
2749             .indicator = {
2750                 .position = SCROLLBACK_INDICATOR_POSITION_RELATIVE,
2751                 .format = SCROLLBACK_INDICATOR_FORMAT_TEXT,
2752                 .text = wcsdup(L""),
2753             },
2754             .multiplier = 3.,
2755         },
2756         .colors = {
2757             .fg = default_foreground,
2758             .bg = default_background,
2759             .alpha = 0xffff,
2760             .selection_fg = 0x80000000,  /* Use default bg */
2761             .selection_bg = 0x80000000,  /* Use default fg */
2762             .use_custom = {
2763                 .selection = false,
2764                 .jump_label = false,
2765                 .scrollback_indicator = false,
2766                 .url = false,
2767             },
2768         },
2769 
2770         .cursor = {
2771             .style = CURSOR_BLOCK,
2772             .blink = false,
2773             .color = {
2774                 .text = 0,
2775                 .cursor = 0,
2776             },
2777             .beam_thickness = {.pt = 1.5},
2778             .underline_thickness = {.pt = 0., .px = -1},
2779         },
2780         .mouse = {
2781             .hide_when_typing = false,
2782             .alternate_scroll_mode = true,
2783         },
2784         .csd = {
2785             .preferred = CONF_CSD_PREFER_SERVER,
2786             .font = {0},
2787             .title_height = 26,
2788             .border_width = 5,
2789             .button_width = 26,
2790         },
2791 
2792         .render_worker_count = sysconf(_SC_NPROCESSORS_ONLN),
2793         .server_socket_path = get_server_socket_path(),
2794         .presentation_timings = false,
2795         .selection_target = SELECTION_TARGET_PRIMARY,
2796         .hold_at_exit = false,
2797         .notify = {
2798             .argv = {.args = NULL},
2799         },
2800         .notify_focus_inhibit = true,
2801 
2802         .tweak = {
2803             .fcft_filter = FCFT_SCALING_FILTER_LANCZOS3,
2804             .overflowing_glyphs = true,
2805 #if defined(FOOT_GRAPHEME_CLUSTERING) && FOOT_GRAPHEME_CLUSTERING
2806             .grapheme_shaping = fcft_caps & FCFT_CAPABILITY_GRAPHEME_SHAPING,
2807 #endif
2808             .grapheme_width_method = GRAPHEME_WIDTH_WCSWIDTH,
2809             .delayed_render_lower_ns = 500000,         /* 0.5ms */
2810             .delayed_render_upper_ns = 16666666 / 2,   /* half a frame period (60Hz) */
2811             .max_shm_pool_size = 512 * 1024 * 1024,
2812             .render_timer_osd = false,
2813             .render_timer_log = false,
2814             .damage_whole_window = false,
2815             .box_drawing_base_thickness = 0.04,
2816             .box_drawing_solid_shades = true,
2817             .font_monospace_warn = true,
2818         },
2819 
2820         .notifications = tll_init(),
2821     };
2822 
2823     memcpy(conf->colors.table, default_color_table, sizeof(default_color_table));
2824 
2825     tokenize_cmdline("notify-send -a ${app-id} -i ${app-id} ${title} ${body}",
2826                      &conf->notify.argv.args);
2827     tokenize_cmdline("xdg-open ${url}", &conf->url.launch.argv.args);
2828 
2829     static const wchar_t *url_protocols[] = {
2830         L"http://",
2831         L"https://",
2832         L"ftp://",
2833         L"ftps://",
2834         L"file://",
2835         L"gemini://",
2836         L"gopher://",
2837     };
2838     conf->url.protocols = xmalloc(
2839         ALEN(url_protocols) * sizeof(conf->url.protocols[0]));
2840     conf->url.prot_count = ALEN(url_protocols);
2841     conf->url.max_prot_len = 0;
2842 
2843     for (size_t i = 0; i < ALEN(url_protocols); i++) {
2844         size_t len = wcslen(url_protocols[i]);
2845         if (len > conf->url.max_prot_len)
2846             conf->url.max_prot_len = len;
2847         conf->url.protocols[i] = xwcsdup(url_protocols[i]);
2848     }
2849 
2850     qsort(
2851         conf->url.uri_characters,
2852         wcslen(conf->url.uri_characters),
2853         sizeof(conf->url.uri_characters[0]),
2854         &wccmp);
2855 
2856     tll_foreach(*initial_user_notifications, it) {
2857         tll_push_back(conf->notifications, it->item);
2858         tll_remove(*initial_user_notifications, it);
2859     }
2860 
2861     add_default_key_bindings(conf);
2862     add_default_search_bindings(conf);
2863     add_default_url_bindings(conf);
2864     add_default_mouse_bindings(conf);
2865 
2866     struct config_file conf_file = {.path = NULL, .fd = -1};
2867     if (conf_path != NULL) {
2868         int fd = open(conf_path, O_RDONLY);
2869         if (fd < 0) {
2870             LOG_AND_NOTIFY_ERRNO("%s: failed to open", conf_path);
2871             ret = !errors_are_fatal;
2872             goto out;
2873         }
2874 
2875         conf_file.path = xstrdup(conf_path);
2876         conf_file.fd = fd;
2877     } else {
2878         conf_file = open_config();
2879         if (conf_file.fd < 0) {
2880             LOG_WARN("no configuration found, using defaults");
2881             ret = !errors_are_fatal;
2882             goto out;
2883         }
2884     }
2885 
2886     xassert(conf_file.path != NULL);
2887     xassert(conf_file.fd >= 0);
2888     LOG_INFO("loading configuration from %s", conf_file.path);
2889 
2890     FILE *f = fdopen(conf_file.fd, "r");
2891     if (f == NULL) {
2892         LOG_AND_NOTIFY_ERRNO("%s: failed to open", conf_file.path);
2893         ret = !errors_are_fatal;
2894         goto out;
2895     }
2896 
2897     ret = parse_config_file(f, conf, conf_file.path, errors_are_fatal) &&
2898           config_override_apply(conf, overrides, errors_are_fatal);
2899     fclose(f);
2900 
2901     conf->colors.use_custom.selection =
2902         conf->colors.selection_fg >> 24 == 0 &&
2903         conf->colors.selection_bg >> 24 == 0;
2904 
2905 out:
2906     if (ret && conf->fonts[0].count == 0) {
2907         struct config_font font;
2908         if (!config_font_parse("monospace", &font)) {
2909             LOG_ERR("failed to load font 'monospace' - no fonts installed?");
2910             ret = false;
2911         } else {
2912             conf->fonts[0].count = 1;
2913             conf->fonts[0].arr = malloc(sizeof(font));
2914             conf->fonts[0].arr[0] = font;
2915         }
2916     }
2917 
2918     if (ret && conf->csd.font.count == 0)
2919         config_font_list_clone(&conf->csd.font, &conf->fonts[0]);
2920 
2921 #if defined(_DEBUG)
2922     for (size_t i = 0; i < conf->bindings.key.count; i++)
2923         xassert(conf->bindings.key.arr[i].action != BIND_ACTION_NONE);
2924     for (size_t i = 0; i < conf->bindings.search.count; i++)
2925         xassert(conf->bindings.search.arr[i].action != BIND_ACTION_SEARCH_NONE);
2926     for (size_t i = 0; i < conf->bindings.url.count; i++)
2927         xassert(conf->bindings.url.arr[i].action != BIND_ACTION_URL_NONE);
2928 #endif
2929 
2930     free(conf_file.path);
2931     if (conf_file.fd >= 0)
2932         close(conf_file.fd);
2933 
2934     return ret;
2935 }
2936 
2937 bool
config_override_apply(struct config * conf,config_override_t * overrides,bool errors_are_fatal)2938 config_override_apply(struct config *conf, config_override_t *overrides, bool errors_are_fatal)
2939 {
2940     struct context context = {
2941         .conf = conf,
2942         .path = "override",
2943         .lineno = 0,
2944         .errors_are_fatal = errors_are_fatal,
2945     };
2946     struct context *ctx = &context;
2947 
2948     tll_foreach(*overrides, it) {
2949         context.lineno++;
2950 
2951         if (!parse_key_value(
2952                 it->item, &context.section, &context.key, &context.value))
2953         {
2954             LOG_CONTEXTUAL_ERR("syntax error: key/value pair has no value");
2955             if (errors_are_fatal)
2956                 return false;
2957             continue;
2958         }
2959 
2960         enum section section = str_to_section(context.section);
2961         if (section == SECTION_COUNT) {
2962             LOG_CONTEXTUAL_ERR("invalid section name: %s", context.section);
2963             if (errors_are_fatal)
2964                 return false;
2965             continue;
2966         }
2967         parser_fun_t section_parser = section_info[section].fun;
2968         xassert(section_parser != NULL);
2969 
2970         if (!section_parser(ctx)) {
2971             if (errors_are_fatal)
2972                 return false;
2973             continue;
2974         }
2975     }
2976     return true;
2977 }
2978 
2979 static void
binding_pipe_free(struct config_binding_pipe * pipe)2980 binding_pipe_free(struct config_binding_pipe *pipe)
2981 {
2982     if (pipe->master_copy)
2983         free_argv(&pipe->argv);
2984 }
2985 
2986 static void
binding_pipe_clone(struct config_binding_pipe * dst,const struct config_binding_pipe * src)2987 binding_pipe_clone(struct config_binding_pipe *dst,
2988                    const struct config_binding_pipe *src)
2989 {
2990     xassert(src->master_copy);
2991     clone_argv(&dst->argv, &src->argv);
2992 }
2993 
2994 static void NOINLINE
key_binding_list_free(struct config_key_binding_list * bindings)2995 key_binding_list_free(struct config_key_binding_list *bindings)
2996 {
2997     for (size_t i = 0; i < bindings->count; i++)
2998         binding_pipe_free(&bindings->arr[i].pipe);
2999     free(bindings->arr);
3000 }
3001 
3002 static void NOINLINE
key_binding_list_clone(struct config_key_binding_list * dst,const struct config_key_binding_list * src)3003 key_binding_list_clone(struct config_key_binding_list *dst,
3004                        const struct config_key_binding_list *src)
3005 {
3006     struct argv *last_master_argv = NULL;
3007 
3008     dst->count = src->count;
3009     dst->arr = xmalloc(src->count * sizeof(dst->arr[0]));
3010 
3011     for (size_t i = 0; i < src->count; i++) {
3012         const struct config_key_binding *old = &src->arr[i];
3013         struct config_key_binding *new = &dst->arr[i];
3014 
3015         *new = *old;
3016 
3017         if (old->pipe.argv.args == NULL)
3018             continue;
3019 
3020         if (old->pipe.master_copy) {
3021             binding_pipe_clone(&new->pipe, &old->pipe);
3022             last_master_argv = &new->pipe.argv;
3023         } else {
3024             xassert(last_master_argv != NULL);
3025             new->pipe.argv = *last_master_argv;
3026         }
3027     }
3028 }
3029 
3030 static void
mouse_binding_list_free(struct config_mouse_binding_list * bindings)3031 mouse_binding_list_free(struct config_mouse_binding_list *bindings)
3032 {
3033     for (size_t i = 0; i < bindings->count; i++)
3034         binding_pipe_free(&bindings->arr[i].pipe);
3035     free(bindings->arr);
3036 }
3037 
3038 static void NOINLINE
mouse_binding_list_clone(struct config_mouse_binding_list * dst,const struct config_mouse_binding_list * src)3039 mouse_binding_list_clone(struct config_mouse_binding_list *dst,
3040                          const struct config_mouse_binding_list *src)
3041 {
3042     struct argv *last_master_argv = NULL;
3043 
3044     dst->count = src->count;
3045     dst->arr = xmalloc(src->count * sizeof(dst->arr[0]));
3046 
3047     for (size_t i = 0; i < src->count; i++) {
3048         const struct config_mouse_binding *old = &src->arr[i];
3049         struct config_mouse_binding *new = &dst->arr[i];
3050 
3051         *new = *old;
3052 
3053         if (old->pipe.argv.args == NULL)
3054             continue;
3055 
3056         if (old->pipe.master_copy) {
3057             binding_pipe_clone(&new->pipe, &old->pipe);
3058             last_master_argv = &new->pipe.argv;
3059         } else {
3060             xassert(last_master_argv != NULL);
3061             new->pipe.argv = *last_master_argv;
3062         }
3063     }
3064 }
3065 
3066 struct config *
config_clone(const struct config * old)3067 config_clone(const struct config *old)
3068 {
3069     struct config *conf = xmalloc(sizeof(*conf));
3070     *conf = *old;
3071 
3072     conf->term = xstrdup(old->term);
3073     conf->shell = xstrdup(old->shell);
3074     conf->title = xstrdup(old->title);
3075     conf->app_id = xstrdup(old->app_id);
3076     conf->word_delimiters = xwcsdup(old->word_delimiters);
3077     conf->scrollback.indicator.text = xwcsdup(old->scrollback.indicator.text);
3078     conf->server_socket_path = xstrdup(old->server_socket_path);
3079     spawn_template_clone(&conf->bell.command, &old->bell.command);
3080     spawn_template_clone(&conf->notify, &old->notify);
3081 
3082     for (size_t i = 0; i < ALEN(conf->fonts); i++)
3083         config_font_list_clone(&conf->fonts[i], &old->fonts[i]);
3084     config_font_list_clone(&conf->csd.font, &old->csd.font);
3085 
3086     conf->url.label_letters = xwcsdup(old->url.label_letters);
3087     conf->url.uri_characters = xwcsdup(old->url.uri_characters);
3088     spawn_template_clone(&conf->url.launch, &old->url.launch);
3089     conf->url.protocols = xmalloc(
3090         old->url.prot_count * sizeof(conf->url.protocols[0]));
3091     for (size_t i = 0; i < old->url.prot_count; i++)
3092         conf->url.protocols[i] = xwcsdup(old->url.protocols[i]);
3093 
3094     key_binding_list_clone(&conf->bindings.key, &old->bindings.key);
3095     key_binding_list_clone(&conf->bindings.search, &old->bindings.search);
3096     key_binding_list_clone(&conf->bindings.url, &old->bindings.url);
3097     mouse_binding_list_clone(&conf->bindings.mouse, &old->bindings.mouse);
3098 
3099     conf->notifications.length = 0;
3100     conf->notifications.head = conf->notifications.tail = 0;
3101     tll_foreach(old->notifications, it) {
3102         char *text = xstrdup(it->item.text);
3103         user_notification_add(&conf->notifications, it->item.kind, text);
3104     }
3105 
3106     return conf;
3107 }
3108 
3109 UNITTEST
3110 {
3111     struct config original;
3112     user_notifications_t nots = tll_init();
3113     config_override_t overrides = tll_init();
3114 
3115     bool ret = config_load(&original, "/dev/null", &nots, &overrides, false);
3116     xassert(ret);
3117 
3118     struct config *clone = config_clone(&original);
3119     xassert(clone != NULL);
3120     xassert(clone != &original);
3121 
3122     config_free(original);
3123     config_free(*clone);
3124     free(clone);
3125 
3126     tll_free(overrides);
3127     tll_free(nots);
3128 }
3129 
3130 void
config_free(struct config conf)3131 config_free(struct config conf)
3132 {
3133     free(conf.term);
3134     free(conf.shell);
3135     free(conf.title);
3136     free(conf.app_id);
3137     free(conf.word_delimiters);
3138     spawn_template_free(&conf.bell.command);
3139     free(conf.scrollback.indicator.text);
3140     spawn_template_free(&conf.notify);
3141     for (size_t i = 0; i < ALEN(conf.fonts); i++)
3142         config_font_list_destroy(&conf.fonts[i]);
3143     free(conf.server_socket_path);
3144 
3145     config_font_list_destroy(&conf.csd.font);
3146 
3147     free(conf.url.label_letters);
3148     spawn_template_free(&conf.url.launch);
3149     for (size_t i = 0; i < conf.url.prot_count; i++)
3150         free(conf.url.protocols[i]);
3151     free(conf.url.protocols);
3152     free(conf.url.uri_characters);
3153 
3154     key_binding_list_free(&conf.bindings.key);
3155     key_binding_list_free(&conf.bindings.search);
3156     key_binding_list_free(&conf.bindings.url);
3157     mouse_binding_list_free(&conf.bindings.mouse);
3158 
3159     user_notifications_free(&conf.notifications);
3160 }
3161 
3162 bool
config_font_parse(const char * pattern,struct config_font * font)3163 config_font_parse(const char *pattern, struct config_font *font)
3164 {
3165     FcPattern *pat = FcNameParse((const FcChar8 *)pattern);
3166     if (pat == NULL)
3167         return false;
3168 
3169     double pt_size = -1.0;
3170     FcPatternGetDouble(pat, FC_SIZE, 0, &pt_size);
3171     FcPatternRemove(pat, FC_SIZE, 0);
3172 
3173     int px_size = -1;
3174     FcPatternGetInteger(pat, FC_PIXEL_SIZE, 0, &px_size);
3175     FcPatternRemove(pat, FC_PIXEL_SIZE, 0);
3176 
3177     if (pt_size == -1. && px_size == -1)
3178         pt_size = 8.0;
3179 
3180     char *stripped_pattern = (char *)FcNameUnparse(pat);
3181     FcPatternDestroy(pat);
3182 
3183     *font = (struct config_font){
3184         .pattern = stripped_pattern,
3185         .pt_size = pt_size,
3186         .px_size = px_size
3187     };
3188     return true;
3189 }
3190 
3191 void
config_font_list_destroy(struct config_font_list * font_list)3192 config_font_list_destroy(struct config_font_list *font_list)
3193 {
3194     for (size_t i = 0; i < font_list->count; i++)
3195         free(font_list->arr[i].pattern);
3196     free(font_list->arr);
3197     font_list->count = 0;
3198     font_list->arr = NULL;
3199 }
3200 
3201 
3202 bool
check_if_font_is_monospaced(const char * pattern,user_notifications_t * notifications)3203 check_if_font_is_monospaced(const char *pattern,
3204                             user_notifications_t *notifications)
3205 {
3206     struct fcft_font *f = fcft_from_name(
3207         1, (const char *[]){pattern}, ":size=8");
3208 
3209     if (f == NULL)
3210         return true;
3211 
3212     static const wchar_t chars[] = {L'a', L'i', L'l', L'M', L'W'};
3213 
3214     bool is_monospaced = true;
3215     int last_width = -1;
3216 
3217     for (size_t i = 0; i < sizeof(chars) / sizeof(chars[0]); i++) {
3218         const struct fcft_glyph *g = fcft_glyph_rasterize(
3219             f, chars[i], FCFT_SUBPIXEL_NONE);
3220 
3221         if (g == NULL)
3222             continue;
3223 
3224         if (last_width >= 0 && g->advance.x != last_width) {
3225             LOG_WARN("%s: font does not appear to be monospace; "
3226                      "check your config, or disable this warning by "
3227                      "setting [tweak].font-monospace-warn=no",
3228                      pattern);
3229 
3230             static const char fmt[] =
3231                 "%s: font does not appear to be monospace; "
3232                 "check your config, or disable this warning by "
3233                 "setting \033[1m[tweak].font-monospace-warn=no\033[22m";
3234 
3235             user_notification_add_fmt(notifications, USER_NOTIFICATION_WARNING,
3236                 fmt, pattern);
3237 
3238             is_monospaced = false;
3239             break;
3240         }
3241 
3242         last_width = g->advance.x;
3243     }
3244 
3245     fcft_destroy(f);
3246     return is_monospaced;
3247 }
3248