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", ¬s, &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