#include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define LOG_MODULE "config" #define LOG_ENABLE_DBG 0 #include "log.h" #include "debug.h" #include "input.h" #include "macros.h" #include "tokenize.h" #include "util.h" #include "wayland.h" #include "xmalloc.h" static const uint32_t default_foreground = 0xdcdccc; static const uint32_t default_background = 0x111111; static const uint32_t default_regular[] = { 0x222222, 0xcc9393, 0x7f9f7f, 0xd0bf8f, 0x6ca0a3, 0xdc8cc3, 0x93e0e3, 0xdcdccc, }; static const uint32_t default_bright[] = { 0x666666, 0xdca3a3, 0xbfebbf, 0xf0dfaf, 0x8cd0d3, 0xfcace3, 0xb3ffff, 0xffffff, }; static const char *const binding_action_map[] = { [BIND_ACTION_NONE] = NULL, [BIND_ACTION_SCROLLBACK_UP_PAGE] = "scrollback-up-page", [BIND_ACTION_SCROLLBACK_UP_HALF_PAGE] = "scrollback-up-half-page", [BIND_ACTION_SCROLLBACK_UP_LINE] = "scrollback-up-line", [BIND_ACTION_SCROLLBACK_DOWN_PAGE] = "scrollback-down-page", [BIND_ACTION_SCROLLBACK_DOWN_HALF_PAGE] = "scrollback-down-half-page", [BIND_ACTION_SCROLLBACK_DOWN_LINE] = "scrollback-down-line", [BIND_ACTION_CLIPBOARD_COPY] = "clipboard-copy", [BIND_ACTION_CLIPBOARD_PASTE] = "clipboard-paste", [BIND_ACTION_PRIMARY_PASTE] = "primary-paste", [BIND_ACTION_SEARCH_START] = "search-start", [BIND_ACTION_FONT_SIZE_UP] = "font-increase", [BIND_ACTION_FONT_SIZE_DOWN] = "font-decrease", [BIND_ACTION_FONT_SIZE_RESET] = "font-reset", [BIND_ACTION_SPAWN_TERMINAL] = "spawn-terminal", [BIND_ACTION_MINIMIZE] = "minimize", [BIND_ACTION_MAXIMIZE] = "maximize", [BIND_ACTION_FULLSCREEN] = "fullscreen", [BIND_ACTION_PIPE_SCROLLBACK] = "pipe-scrollback", [BIND_ACTION_PIPE_VIEW] = "pipe-visible", [BIND_ACTION_PIPE_SELECTED] = "pipe-selected", /* Mouse-specific actions */ [BIND_ACTION_SELECT_BEGIN] = "select-begin", [BIND_ACTION_SELECT_BEGIN_BLOCK] = "select-begin-block", [BIND_ACTION_SELECT_EXTEND] = "select-extend", [BIND_ACTION_SELECT_EXTEND_CHAR_WISE] = "select-extend-character-wise", [BIND_ACTION_SELECT_WORD] = "select-word", [BIND_ACTION_SELECT_WORD_WS] = "select-word-whitespace", [BIND_ACTION_SELECT_ROW] = "select-row", }; static_assert(ALEN(binding_action_map) == BIND_ACTION_COUNT, "binding action map size mismatch"); static const char *const search_binding_action_map[] = { [BIND_ACTION_SEARCH_NONE] = NULL, [BIND_ACTION_SEARCH_CANCEL] = "cancel", [BIND_ACTION_SEARCH_COMMIT] = "commit", [BIND_ACTION_SEARCH_FIND_PREV] = "find-prev", [BIND_ACTION_SEARCH_FIND_NEXT] = "find-next", [BIND_ACTION_SEARCH_EDIT_LEFT] = "cursor-left", [BIND_ACTION_SEARCH_EDIT_LEFT_WORD] = "cursor-left-word", [BIND_ACTION_SEARCH_EDIT_RIGHT] = "cursor-right", [BIND_ACTION_SEARCH_EDIT_RIGHT_WORD] = "cursor-right-word", [BIND_ACTION_SEARCH_EDIT_HOME] = "cursor-home", [BIND_ACTION_SEARCH_EDIT_END] = "cursor-end", [BIND_ACTION_SEARCH_DELETE_PREV] = "delete-prev", [BIND_ACTION_SEARCH_DELETE_PREV_WORD] = "delete-prev-word", [BIND_ACTION_SEARCH_DELETE_NEXT] = "delete-next", [BIND_ACTION_SEARCH_DELETE_NEXT_WORD] = "delete-next-word", [BIND_ACTION_SEARCH_EXTEND_WORD] = "extend-to-word-boundary", [BIND_ACTION_SEARCH_EXTEND_WORD_WS] = "extend-to-next-whitespace", [BIND_ACTION_SEARCH_CLIPBOARD_PASTE] = "clipboard-paste", [BIND_ACTION_SEARCH_PRIMARY_PASTE] = "primary-paste", }; static_assert(ALEN(search_binding_action_map) == BIND_ACTION_SEARCH_COUNT, "search binding action map size mismatch"); #define LOG_AND_NOTIFY_ERR(...) \ do { \ LOG_ERR(__VA_ARGS__); \ char *text = xasprintf(__VA_ARGS__); \ struct user_notification notif = { \ .kind = USER_NOTIFICATION_ERROR, \ .text = text, \ }; \ tll_push_back(conf->notifications, notif); \ } while (0) #define LOG_AND_NOTIFY_WARN(...) \ do { \ LOG_WARN(__VA_ARGS__); \ char *text = xasprintf(__VA_ARGS__); \ struct user_notification notif = { \ .kind = USER_NOTIFICATION_WARNING, \ .text = text, \ }; \ tll_push_back(conf->notifications, notif); \ } while (0) #define LOG_AND_NOTIFY_ERRNO(...) \ do { \ int _errno = errno; \ LOG_ERRNO(__VA_ARGS__); \ int len = snprintf(NULL, 0, __VA_ARGS__); \ int errno_len = snprintf(NULL, 0, ": %s", strerror(_errno)); \ char *text = xmalloc(len + errno_len + 1); \ snprintf(text, len + errno_len + 1, __VA_ARGS__); \ snprintf(&text[len], errno_len + 1, ": %s", strerror(_errno)); \ struct user_notification notif = { \ .kind = USER_NOTIFICATION_ERROR, \ .text = text, \ }; \ tll_push_back(conf->notifications, notif); \ } while(0) static char * get_shell(void) { const char *shell = getenv("SHELL"); if (shell == NULL) { struct passwd *passwd = getpwuid(getuid()); if (passwd == NULL) { LOG_ERRNO("failed to lookup user: falling back to 'sh'"); shell = "sh"; } else shell = passwd->pw_shell; } LOG_DBG("user's shell: %s", shell); return xstrdup(shell); } struct config_file { char *path; /* Full, absolute, path */ int fd; /* FD of file, O_RDONLY */ }; struct path_component { const char *component; int fd; }; typedef tll(struct path_component) path_components_t; static void path_component_add(path_components_t *components, const char *comp, int fd) { xassert(comp != NULL); xassert(fd >= 0); struct path_component pc = {.component = comp, .fd = fd}; tll_push_back(*components, pc); } static void path_component_destroy(struct path_component *component) { xassert(component->fd >= 0); close(component->fd); } static void path_components_destroy(path_components_t *components) { tll_foreach(*components, it) { path_component_destroy(&it->item); tll_remove(*components, it); } } static struct config_file path_components_to_config_file(const path_components_t *components) { if (tll_length(*components) == 0) goto err; size_t len = 0; tll_foreach(*components, it) len += strlen(it->item.component) + 1; char *path = malloc(len); if (path == NULL) goto err; size_t idx = 0; tll_foreach(*components, it) { strcpy(&path[idx], it->item.component); idx += strlen(it->item.component); path[idx++] = '/'; } path[idx - 1] = '\0'; /* Strip last ’/’ */ int fd_copy = dup(tll_back(*components).fd); if (fd_copy < 0) { free(path); goto err; } return (struct config_file){.path = path, .fd = fd_copy}; err: return (struct config_file){.path = NULL, .fd = -1}; } static const char * get_user_home_dir(void) { const struct passwd *passwd = getpwuid(getuid()); if (passwd == NULL) return NULL; return passwd->pw_dir; } static bool try_open_file(path_components_t *components, const char *name) { int parent_fd = tll_back(*components).fd; struct stat st; if (fstatat(parent_fd, name, &st, 0) == 0 && S_ISREG(st.st_mode)) { int fd = openat(parent_fd, name, O_RDONLY); if (fd >= 0) { path_component_add(components, name, fd); return true; } } return false; } static struct config_file open_config(struct config *conf) { struct config_file ret = {.path = NULL, .fd = -1}; path_components_t components = tll_init(); const char *xdg_config_home = getenv("XDG_CONFIG_HOME"); const char *user_home_dir = get_user_home_dir(); char *xdg_config_dirs_copy = NULL; /* Use XDG_CONFIG_HOME, or ~/.config */ if (xdg_config_home != NULL) { int fd = open(xdg_config_home, O_RDONLY); if (fd >= 0) path_component_add(&components, xdg_config_home, fd); } else if (user_home_dir != NULL) { int home_fd = open(user_home_dir, O_RDONLY); if (home_fd >= 0) { int config_fd = openat(home_fd, ".config", O_RDONLY); if (config_fd >= 0) { path_component_add(&components, user_home_dir, home_fd); path_component_add(&components, ".config", config_fd); } else close(home_fd); } } /* First look for foot/foot.ini */ if (tll_length(components) > 0) { int foot_fd = openat(tll_back(components).fd, "foot", O_RDONLY); if (foot_fd >= 0) { path_component_add(&components, "foot", foot_fd); if (try_open_file(&components, "foot.ini")) goto done; struct path_component pc = tll_pop_back(components); path_component_destroy(&pc); } } /* Finally, try foot/foot.ini in all XDG_CONFIG_DIRS */ const char *xdg_config_dirs = getenv("XDG_CONFIG_DIRS"); xdg_config_dirs_copy = xdg_config_dirs != NULL ? strdup(xdg_config_dirs) : NULL; if (xdg_config_dirs_copy != NULL) { for (char *save = NULL, *xdg_dir = strtok_r(xdg_config_dirs_copy, ":", &save); xdg_dir != NULL; xdg_dir = strtok_r(NULL, ":", &save)) { path_components_destroy(&components); int xdg_fd = open(xdg_dir, O_RDONLY); if (xdg_fd < 0) continue; int foot_fd = openat(xdg_fd, "foot", O_RDONLY); if (foot_fd < 0) { close(xdg_fd); continue; } xassert(tll_length(components) == 0); path_component_add(&components, xdg_dir, xdg_fd); path_component_add(&components, "foot", foot_fd); if (try_open_file(&components, "foot.ini")) goto done; } } out: path_components_destroy(&components); free(xdg_config_dirs_copy); return ret; done: xassert(tll_length(components) > 0); ret = path_components_to_config_file(&components); goto out; } static bool str_to_bool(const char *s) { return strcasecmp(s, "on") == 0 || strcasecmp(s, "true") == 0 || strcasecmp(s, "yes") == 0 || strtoul(s, NULL, 0) > 0; } static bool str_to_ulong(const char *s, int base, unsigned long *res) { if (s == NULL) return false; errno = 0; char *end = NULL; *res = strtoul(s, &end, base); return errno == 0 && *end == '\0'; } static bool str_to_double(const char *s, double *res) { if (s == NULL) return false; errno = 0; char *end = NULL; *res = strtod(s, &end); return errno == 0 && *end == '\0'; } static bool str_to_color(const char *s, uint32_t *color, bool allow_alpha, struct config *conf, const char *path, int lineno, const char *section, const char *key) { unsigned long value; if (!str_to_ulong(s, 16, &value)) { LOG_AND_NOTIFY_ERRNO( "%s:%d: [%s]: %s: invalid color: %s", path, lineno, section, key, s); return false; } if (!allow_alpha && (value & 0xff000000) != 0) { LOG_AND_NOTIFY_ERR( "%s:%d: [%s]: %s: color value must not have an alpha component: %s", path, lineno, section, key, s); return false; } *color = value; return true; } static bool str_to_pt_or_px(const char *s, struct pt_or_px *res, struct config *conf, const char *path, int lineno, const char *section, const char *key) { size_t len = s != NULL ? strlen(s) : 0; if (len >= 2 && s[len - 2] == 'p' && s[len - 1] == 'x') { errno = 0; char *end = NULL; long value = strtol(s, &end, 10); if (!(errno == 0 && end == s + len - 2)) { LOG_AND_NOTIFY_ERR( "%s:%d: [%s]: %s: " "expected an integer directly followed by 'px', got '%s'", path, lineno, section, key, s); return false; } res->pt = 0; res->px = value; } else { double value; if (!str_to_double(s, &value)) { LOG_AND_NOTIFY_ERR( "%s:%d: [%s]: %s: expected a decimal value, got '%s'", path, lineno, section, key, s); return false; } res->pt = value; res->px = 0; } return true; } static bool parse_section_main(const char *key, const char *value, struct config *conf, const char *path, unsigned lineno) { if (strcmp(key, "term") == 0) { free(conf->term); conf->term = xstrdup(value); } else if (strcmp(key, "shell") == 0) { free(conf->shell); conf->shell = xstrdup(value); } else if (strcmp(key, "login-shell") == 0) { conf->login_shell = str_to_bool(value); } else if (strcmp(key, "title") == 0) { free(conf->title); conf->title = xstrdup(value); } else if (strcmp(key, "app-id") == 0) { free(conf->app_id); conf->app_id = xstrdup(value); } else if (strcmp(key, "initial-window-size-pixels") == 0) { unsigned width, height; if (sscanf(value, "%ux%u", &width, &height) != 2 || width == 0 || height == 0) { LOG_AND_NOTIFY_ERR( "%s: %d: [default]: initial-window-size-pixels: " "expected WIDTHxHEIGHT, where both are positive integers, " "got '%s'", path, lineno, value); return false; } conf->size.type = CONF_SIZE_PX; conf->size.width = width; conf->size.height = height; } else if (strcmp(key, "initial-window-size-chars") == 0) { unsigned width, height; if (sscanf(value, "%ux%u", &width, &height) != 2 || width == 0 || height == 0) { LOG_AND_NOTIFY_ERR( "%s: %d: [default]: initial-window-size-chars: " "expected WIDTHxHEIGHT, where both are positive integers, " "got '%s'", path, lineno, value); return false; } conf->size.type = CONF_SIZE_CELLS; conf->size.width = width; conf->size.height = height; } else if (strcmp(key, "pad") == 0) { unsigned x, y; char mode[16] = {0}; int ret = sscanf(value, "%ux%u %15s", &x, &y, mode); bool center = strcasecmp(mode, "center") == 0; bool invalid_mode = !center && mode[0] != '\0'; if ((ret != 2 && ret != 3) || invalid_mode) { LOG_AND_NOTIFY_ERR( "%s:%d: [default]: pad: expected PAD_XxPAD_Y [center], " "where both are positive integers, got '%s'", path, lineno, value); return false; } conf->pad_x = x; conf->pad_y = y; conf->center = center; } else if (strcmp(key, "bold-text-in-bright") == 0) conf->bold_in_bright = str_to_bool(value); else if (strcmp(key, "bell") == 0) { if (strcmp(value, "set-urgency") == 0) conf->bell_action = BELL_ACTION_URGENT; else if (strcmp(value, "notify") == 0) conf->bell_action = BELL_ACTION_NOTIFY; else if (strcmp(value, "none") == 0) conf->bell_action = BELL_ACTION_NONE; else { LOG_AND_NOTIFY_ERR( "%s:%d: [default]: bell: " "expected either 'set-urgency', 'notify' or 'none'", path, lineno); conf->bell_action = BELL_ACTION_NONE; return false; } } else if (strcmp(key, "initial-window-mode") == 0) { if (strcmp(value, "windowed") == 0) conf->startup_mode = STARTUP_WINDOWED; else if (strcmp(value, "maximized") == 0) conf->startup_mode = STARTUP_MAXIMIZED; else if (strcmp(value, "fullscreen") == 0) conf->startup_mode = STARTUP_FULLSCREEN; else { LOG_AND_NOTIFY_ERR( "%s:%d: [default]: initial-window-mode: expected either " "'windowed', 'maximized' or 'fullscreen'", path, lineno); return false; } } else if (strcmp(key, "font") == 0 || strcmp(key, "font-bold") == 0 || strcmp(key, "font-italic") == 0 || strcmp(key, "font-bold-italic") == 0) { size_t idx = strcmp(key, "font") == 0 ? 0 : strcmp(key, "font-bold") == 0 ? 1 : strcmp(key, "font-italic") == 0 ? 2 : 3; tll_foreach(conf->fonts[idx], it) config_font_destroy(&it->item); tll_free(conf->fonts[idx]); char *copy = xstrdup(value); for (const char *font = strtok(copy, ","); font != NULL; font = strtok(NULL, ",")) { /* Trim spaces, strictly speaking not necessary, but looks nice :) */ while (*font != '\0' && isspace(*font)) font++; if (*font != '\0') { struct config_font font_data; if (!config_font_parse(font, &font_data)) { LOG_ERR("%s:%d: [default]: %s: invalid font specification", path, lineno, key); free(copy); return false; } tll_push_back(conf->fonts[idx], font_data); } } free(copy); } else if (strcmp(key, "line-height") == 0) { if (!str_to_pt_or_px(value, &conf->line_height, conf, path, lineno, "default", "line-height")) return false; } else if (strcmp(key, "letter-spacing") == 0) { if (!str_to_pt_or_px(value, &conf->letter_spacing, conf, path, lineno, "default", "letter-spacing")) return false; } else if (strcmp(key, "horizontal-letter-offset") == 0) { if (!str_to_pt_or_px( value, &conf->horizontal_letter_offset, conf, path, lineno, "default", "horizontal-letter-offset")) return false; } else if (strcmp(key, "vertical-letter-offset") == 0) { if (!str_to_pt_or_px( value, &conf->horizontal_letter_offset, conf, path, lineno, "default", "vertical-letter-offset")) return false; } else if (strcmp(key, "dpi-aware") == 0) { if (strcmp(value, "auto") == 0) conf->dpi_aware = DPI_AWARE_AUTO; else conf->dpi_aware = str_to_bool(value) ? DPI_AWARE_YES : DPI_AWARE_NO; } else if (strcmp(key, "workers") == 0) { unsigned long count; if (!str_to_ulong(value, 10, &count)) { LOG_AND_NOTIFY_ERR( "%s:%d: [default]: workers: expected an integer, got '%s'", path, lineno, value); return false; } conf->render_worker_count = count; } else if (strcmp(key, "word-delimiters") == 0) { size_t chars = mbstowcs(NULL, value, 0); if (chars == (size_t)-1) { LOG_AND_NOTIFY_ERR( "%s:%d: [default]: word-delimiters: invalid string: %s", path, lineno, value); return false; } free(conf->word_delimiters); conf->word_delimiters = xmalloc((chars + 1) * sizeof(wchar_t)); mbstowcs(conf->word_delimiters, value, chars + 1); } else if (strcmp(key, "notify") == 0) { free(conf->notify.raw_cmd); free(conf->notify.argv); conf->notify.raw_cmd = NULL; conf->notify.argv = NULL; char *raw_cmd = xstrdup(value); char **argv = NULL; if (!tokenize_cmdline(raw_cmd, &argv)) { LOG_AND_NOTIFY_ERR( "%s:%d: [default]: notify: syntax error in command line", path, lineno); return false; } conf->notify.raw_cmd = raw_cmd; conf->notify.argv = argv; } else { LOG_AND_NOTIFY_ERR("%s:%u: [default]: %s: invalid key", path, lineno, key); return false; } return true; } static bool parse_section_scrollback(const char *key, const char *value, struct config *conf, const char *path, unsigned lineno) { if (strcmp(key, "lines") == 0) { unsigned long lines; if (!str_to_ulong(value, 10, &lines)) { LOG_AND_NOTIFY_ERR("%s:%d: [scrollback]: lines: expected an integer, got '%s'", path, lineno, value); return false; } conf->scrollback.lines = lines; } else if (strcmp(key, "indicator-position") == 0) { if (strcmp(value, "none") == 0) conf->scrollback.indicator.position = SCROLLBACK_INDICATOR_POSITION_NONE; else if (strcmp(value, "fixed") == 0) conf->scrollback.indicator.position = SCROLLBACK_INDICATOR_POSITION_FIXED; else if (strcmp(value, "relative") == 0) conf->scrollback.indicator.position = SCROLLBACK_INDICATOR_POSITION_RELATIVE; else { LOG_AND_NOTIFY_ERR("%s:%d: [scrollback]: indicator-position must be one of " "'none', 'fixed' or 'relative'", path, lineno); return false; } } else if (strcmp(key, "indicator-format") == 0) { if (strcmp(value, "percentage") == 0) { conf->scrollback.indicator.format = SCROLLBACK_INDICATOR_FORMAT_PERCENTAGE; } else if (strcmp(value, "line") == 0) { conf->scrollback.indicator.format = SCROLLBACK_INDICATOR_FORMAT_LINENO; } else { free(conf->scrollback.indicator.text); conf->scrollback.indicator.text = NULL; size_t len = mbstowcs(NULL, value, 0); if (len == (size_t)-1) { LOG_AND_NOTIFY_ERRNO( "%s:%d: [scrollback]: indicator-format: " "invalid value: %s", path, lineno, value); return false; } conf->scrollback.indicator.text = xcalloc(len + 1, sizeof(wchar_t)); mbstowcs(conf->scrollback.indicator.text, value, len + 1); } } else if (strcmp(key, "multiplier") == 0) { double multiplier; if (!str_to_double(value, &multiplier)) { LOG_AND_NOTIFY_ERR("%s:%d: [scrollback]: multiplier: " "invalid value: %s", path, lineno, value); return false; } conf->scrollback.multiplier = multiplier; } else { LOG_AND_NOTIFY_ERR("%s:%u: [scrollback]: %s: invalid key", path, lineno, key); return false; } return true; } static bool parse_section_colors(const char *key, const char *value, struct config *conf, const char *path, unsigned lineno) { uint32_t *color = NULL; if (strcmp(key, "foreground") == 0) color = &conf->colors.fg; else if (strcmp(key, "background") == 0) color = &conf->colors.bg; else if (strcmp(key, "regular0") == 0) color = &conf->colors.regular[0]; else if (strcmp(key, "regular1") == 0) color = &conf->colors.regular[1]; else if (strcmp(key, "regular2") == 0) color = &conf->colors.regular[2]; else if (strcmp(key, "regular3") == 0) color = &conf->colors.regular[3]; else if (strcmp(key, "regular4") == 0) color = &conf->colors.regular[4]; else if (strcmp(key, "regular5") == 0) color = &conf->colors.regular[5]; else if (strcmp(key, "regular6") == 0) color = &conf->colors.regular[6]; else if (strcmp(key, "regular7") == 0) color = &conf->colors.regular[7]; else if (strcmp(key, "bright0") == 0) color = &conf->colors.bright[0]; else if (strcmp(key, "bright1") == 0) color = &conf->colors.bright[1]; else if (strcmp(key, "bright2") == 0) color = &conf->colors.bright[2]; else if (strcmp(key, "bright3") == 0) color = &conf->colors.bright[3]; else if (strcmp(key, "bright4") == 0) color = &conf->colors.bright[4]; else if (strcmp(key, "bright5") == 0) color = &conf->colors.bright[5]; else if (strcmp(key, "bright6") == 0) color = &conf->colors.bright[6]; else if (strcmp(key, "bright7") == 0) color = &conf->colors.bright[7]; else if (strcmp(key, "selection-foreground") == 0) color = &conf->colors.selection_fg; else if (strcmp(key, "selection-background") == 0) color = &conf->colors.selection_bg; else if (strcmp(key, "alpha") == 0) { double alpha; if (!str_to_double(value, &alpha) || alpha < 0. || alpha > 1.) { LOG_AND_NOTIFY_ERR("%s: %d: [colors]: alpha: expected a value in the range 0.0-1.0", path, lineno); return false; } conf->colors.alpha = alpha * 65535.; return true; } else { LOG_AND_NOTIFY_ERR("%s:%d: [colors]: %s: invalid key", path, lineno, key); return false; } uint32_t color_value; if (!str_to_color(value, &color_value, false, conf, path, lineno, "colors", key)) return false; *color = color_value; return true; } static bool parse_section_cursor(const char *key, const char *value, struct config *conf, const char *path, unsigned lineno) { if (strcmp(key, "style") == 0) { if (strcmp(value, "block") == 0) conf->cursor.style = CURSOR_BLOCK; else if (strcmp(value, "bar") == 0) conf->cursor.style = CURSOR_BAR; else if (strcmp(value, "underline") == 0) conf->cursor.style = CURSOR_UNDERLINE; else { LOG_AND_NOTIFY_ERR("%s:%d: invalid 'style': %s", path, lineno, value); return false; } } else if (strcmp(key, "blink") == 0) conf->cursor.blink = str_to_bool(value); else if (strcmp(key, "color") == 0) { char *value_copy = xstrdup(value); const char *text = strtok(value_copy, " "); const char *cursor = strtok(NULL, " "); uint32_t text_color, cursor_color; if (text == NULL || cursor == NULL || !str_to_color(text, &text_color, false, conf, path, lineno, "cursor", "color") || !str_to_color(cursor, &cursor_color, false, conf, path, lineno, "cursor", "color")) { LOG_AND_NOTIFY_ERR("%s:%d: invalid cursor colors: %s", path, lineno, value); free(value_copy); return false; } conf->cursor.color.text = 1u << 31 | text_color; conf->cursor.color.cursor = 1u << 31 | cursor_color; free(value_copy); } else { LOG_AND_NOTIFY_ERR("%s:%d: [cursor]: %s: invalid key", path, lineno, key); return false; } return true; } static bool parse_section_mouse(const char *key, const char *value, struct config *conf, const char *path, unsigned lineno) { if (strcmp(key, "hide-when-typing") == 0) conf->mouse.hide_when_typing = str_to_bool(value); else if (strcmp(key, "alternate-scroll-mode") == 0) conf->mouse.alternate_scroll_mode = str_to_bool(value); else { LOG_AND_NOTIFY_ERR("%s:%d: [mouse]: %s: invalid key", path, lineno, key); return false; } return true; } static bool parse_section_csd(const char *key, const char *value, struct config *conf, const char *path, unsigned lineno) { if (strcmp(key, "preferred") == 0) { if (strcmp(value, "server") == 0) conf->csd.preferred = CONF_CSD_PREFER_SERVER; else if (strcmp(value, "client") == 0) conf->csd.preferred = CONF_CSD_PREFER_CLIENT; else if (strcmp(value, "none") == 0) conf->csd.preferred = CONF_CSD_PREFER_NONE; else { LOG_AND_NOTIFY_ERR( "%s:%d: csd.preferred: expected either " "'server', 'client' or 'none'", path, lineno); return false; } } else if (strcmp(key, "color") == 0) { uint32_t color; if (!str_to_color(value, &color, true, conf, path, lineno, "csd", "color")) { LOG_AND_NOTIFY_ERR("%s:%d: invalid titlebar-color: %s", path, lineno, value); return false; } conf->csd.color.title_set = true; conf->csd.color.title = color; } else if (strcmp(key, "size") == 0) { unsigned long pixels; if (!str_to_ulong(value, 10, &pixels)) { LOG_AND_NOTIFY_ERR("%s:%d: expected an integer, got '%s'", path, lineno, value); return false; } conf->csd.title_height = pixels; } else if (strcmp(key, "button-width") == 0) { unsigned long pixels; if (!str_to_ulong(value, 10, &pixels)) { LOG_AND_NOTIFY_ERR("%s:%d: expected an integer, got '%s'", path, lineno, value); return false; } conf->csd.button_width = pixels; } else if (strcmp(key, "button-minimize-color") == 0) { uint32_t color; if (!str_to_color(value, &color, true, conf, path, lineno, "csd", "button-minimize-color")) { LOG_AND_NOTIFY_ERR("%s:%d: invalid button-minimize-color: %s", path, lineno, value); return false; } conf->csd.color.minimize_set = true; conf->csd.color.minimize = color; } else if (strcmp(key, "button-maximize-color") == 0) { uint32_t color; if (!str_to_color(value, &color, true, conf, path, lineno, "csd", "button-maximize-color")) { LOG_AND_NOTIFY_ERR("%s:%d: invalid button-maximize-color: %s", path, lineno, value); return false; } conf->csd.color.maximize_set = true; conf->csd.color.maximize = color; } else if (strcmp(key, "button-close-color") == 0) { uint32_t color; if (!str_to_color(value, &color, true, conf, path, lineno, "csd", "button-close-color")) { LOG_AND_NOTIFY_ERR("%s:%d: invalid button-close-color: %s", path, lineno, value); return false; } conf->csd.color.close_set = true; conf->csd.color.close = color; } else { LOG_AND_NOTIFY_ERR("%s:%u: [csd]: %s: invalid action", path, lineno, key); return false; } return true; } /* Struct that holds temporary key/mouse binding parsed data */ struct key_combo { char *text; /* Raw text, e.g. "Control+Shift+V" */ struct config_key_modifiers modifiers; union { xkb_keysym_t sym; /* Key converted to an XKB symbol, e.g. XKB_KEY_V */ struct { int button; int count; } m; }; }; typedef tll(struct key_combo) key_combo_list_t; static void free_key_combo_list(key_combo_list_t *key_combos) { tll_foreach(*key_combos, it) free(it->item.text); tll_free(*key_combos); } static bool parse_modifiers(struct config *conf, const char *text, size_t len, struct config_key_modifiers *modifiers, const char *path, unsigned lineno) { bool ret = false; *modifiers = (struct config_key_modifiers){0}; char *copy = xstrndup(text, len); for (char *tok_ctx = NULL, *key = strtok_r(copy, "+", &tok_ctx); key != NULL; key = strtok_r(NULL, "+", &tok_ctx)) { if (strcmp(key, XKB_MOD_NAME_SHIFT) == 0) modifiers->shift = true; else if (strcmp(key, XKB_MOD_NAME_CTRL) == 0) modifiers->ctrl = true; else if (strcmp(key, XKB_MOD_NAME_ALT) == 0) modifiers->alt = true; else if (strcmp(key, XKB_MOD_NAME_LOGO) == 0) modifiers->meta = true; else { LOG_AND_NOTIFY_ERR("%s:%d: %s: not a valid modifier name", path, lineno, key); goto out; } } ret = true; out: free(copy); return ret; } static bool parse_key_combos(struct config *conf, const char *combos, key_combo_list_t *key_combos, const char *path, unsigned lineno) { xassert(tll_length(*key_combos) == 0); char *copy = xstrdup(combos); for (char *tok_ctx = NULL, *combo = strtok_r(copy, " ", &tok_ctx); combo != NULL; combo = strtok_r(NULL, " ", &tok_ctx)) { struct config_key_modifiers modifiers = {0}; const char *key = strrchr(combo, '+'); if (key == NULL) { /* No modifiers */ key = combo; } else { if (!parse_modifiers(conf, combo, key - combo, &modifiers, path, lineno)) goto err; key++; /* Skip past the '+' */ } /* Translate key name to symbol */ xkb_keysym_t sym = xkb_keysym_from_name(key, 0); if (sym == XKB_KEY_NoSymbol) { LOG_AND_NOTIFY_ERR("%s:%d: %s: key is not a valid XKB key name", path, lineno, key); goto err; } tll_push_back( *key_combos, ((struct key_combo){.text = xstrdup(combo), .modifiers = modifiers, .sym = sym})); } free(copy); return true; err: tll_foreach(*key_combos, it) free(it->item.text); tll_free(*key_combos); free(copy); return false; } static bool has_key_binding_collisions(struct config *conf, enum bind_action_normal action, const key_combo_list_t *key_combos, const char *path, unsigned lineno) { tll_foreach(conf->bindings.key, it) { if (it->item.action == action) continue; tll_foreach(*key_combos, it2) { const struct config_key_modifiers *mods1 = &it->item.modifiers; const struct config_key_modifiers *mods2 = &it2->item.modifiers; bool shift = mods1->shift == mods2->shift; bool alt = mods1->alt == mods2->alt; bool ctrl = mods1->ctrl == mods2->ctrl; bool meta = mods1->meta == mods2->meta; bool sym = it->item.sym == it2->item.sym; if (shift && alt && ctrl && meta && sym) { bool has_pipe = it->item.pipe.cmd != NULL; LOG_AND_NOTIFY_ERR("%s:%d: %s already mapped to '%s%s%s%s'", path, lineno, it2->item.text, binding_action_map[it->item.action], has_pipe ? " [" : "", has_pipe ? it->item.pipe.cmd : "", has_pipe ? "]" : ""); return true; } } } return false; } static bool has_search_binding_collisions(struct config *conf, enum bind_action_search action, const key_combo_list_t *key_combos, const char *path, unsigned lineno) { tll_foreach(conf->bindings.search, it) { if (it->item.action == action) continue; tll_foreach(*key_combos, it2) { const struct config_key_modifiers *mods1 = &it->item.modifiers; const struct config_key_modifiers *mods2 = &it2->item.modifiers; bool shift = mods1->shift == mods2->shift; bool alt = mods1->alt == mods2->alt; bool ctrl = mods1->ctrl == mods2->ctrl; bool meta = mods1->meta == mods2->meta; bool sym = it->item.sym == it2->item.sym; if (shift && alt && ctrl && meta && sym) { LOG_AND_NOTIFY_ERR("%s:%d: %s already mapped to '%s'", path, lineno, it2->item.text, search_binding_action_map[it->item.action]); return true; } } } return false; } static int argv_compare(char *const *argv1, char *const *argv2) { xassert(argv1 != NULL); xassert(argv2 != NULL); for (size_t i = 0; ; i++) { if (argv1[i] == NULL && argv2[i] == NULL) return 0; if (argv1[i] == NULL) return -1; if (argv2[i] == NULL) return 1; int ret = strcmp(argv1[i], argv2[i]); if (ret != 0) return ret; } xassert(false); return 1; } /* * Parses a key binding value on the form * "[cmd-to-exec arg1 arg2] Mods+Key" * * and extracts 'cmd-to-exec' and its arguments. * * Input: * - value: raw string, on the form mention above * - cmd: pointer to string to will be allocated and filled with * 'cmd-to-exec arg1 arg2' * - argv: point to array of string. Array will be allocated. Will be * filled with {'cmd-to-exec', 'arg1', 'arg2', NULL} * * Returns: * - ssize_t, number of bytes to strip from 'value' to remove the '[]' * enclosed cmd and its arguments, including any subsequent * whitespace characters. I.e. if 'value' is "[cmd] BTN_RIGHT", the * return value is 6 (strlen("[cmd] ")). * - cmd: allocated string containing "cmd arg1 arg2...". Caller frees. * - argv: allocatd array containing {"cmd", "arg1", "arg2", NULL}. Caller frees. */ static ssize_t pipe_argv_from_string(const char *value, char **cmd, char ***argv, struct config *conf, const char *path, unsigned lineno) { *cmd = NULL; *argv = NULL; if (value[0] != '[') return 0; const char *pipe_cmd_end = strrchr(value, ']'); if (pipe_cmd_end == NULL) { LOG_AND_NOTIFY_ERR("%s:%d: unclosed '['", path, lineno); return -1; } size_t pipe_len = pipe_cmd_end - value - 1; *cmd = xstrndup(&value[1], pipe_len); if (!tokenize_cmdline(*cmd, argv)) { LOG_AND_NOTIFY_ERR("%s:%d: syntax error in command line", path, lineno); free(*cmd); return -1; } ssize_t remove_len = pipe_cmd_end + 1 - value; value = pipe_cmd_end + 1; while (isspace(*value)) { value++; remove_len++; } return remove_len; } static bool parse_section_key_bindings( const char *key, const char *value, struct config *conf, const char *path, unsigned lineno) { char *pipe_cmd; char **pipe_argv; ssize_t pipe_remove_len = pipe_argv_from_string( value, &pipe_cmd, &pipe_argv, conf, path, lineno); if (pipe_remove_len < 0) return false; value += pipe_remove_len; for (enum bind_action_normal action = 0; action < BIND_ACTION_KEY_COUNT; action++) { if (binding_action_map[action] == NULL) continue; if (strcmp(key, binding_action_map[action]) != 0) continue; /* Unset binding */ if (strcasecmp(value, "none") == 0) { tll_foreach(conf->bindings.key, it) { if (it->item.action == action) { if (it->item.pipe.master_copy) { free(it->item.pipe.cmd); free(it->item.pipe.argv); } tll_remove(conf->bindings.key, it); } } free(pipe_argv); free(pipe_cmd); return true; } key_combo_list_t key_combos = tll_init(); if (!parse_key_combos(conf, value, &key_combos, path, lineno) || has_key_binding_collisions(conf, action, &key_combos, path, lineno)) { free(pipe_argv); free(pipe_cmd); free_key_combo_list(&key_combos); return false; } /* Remove existing bindings for this action+pipe */ tll_foreach(conf->bindings.key, it) { if (it->item.action == action && ((it->item.pipe.argv == NULL && pipe_argv == NULL) || (it->item.pipe.argv != NULL && pipe_argv != NULL && argv_compare(it->item.pipe.argv, pipe_argv) == 0))) { if (it->item.pipe.master_copy) { free(it->item.pipe.cmd); free(it->item.pipe.argv); } tll_remove(conf->bindings.key, it); } } /* Emit key bindings */ bool first = true; tll_foreach(key_combos, it) { struct config_key_binding_normal binding = { .action = action, .modifiers = it->item.modifiers, .sym = it->item.sym, .pipe = { .cmd = pipe_cmd, .argv = pipe_argv, .master_copy = first, }, }; tll_push_back(conf->bindings.key, binding); first = false; } free_key_combo_list(&key_combos); return true; } LOG_AND_NOTIFY_ERR("%s:%u: [key-bindings]: %s: invalid action", path, lineno, key); free(pipe_cmd); free(pipe_argv); return false; } static bool parse_section_search_bindings( const char *key, const char *value, struct config *conf, const char *path, unsigned lineno) { for (enum bind_action_search action = 0; action < BIND_ACTION_SEARCH_COUNT; action++) { if (search_binding_action_map[action] == NULL) continue; if (strcmp(key, search_binding_action_map[action]) != 0) continue; /* Unset binding */ if (strcasecmp(value, "none") == 0) { tll_foreach(conf->bindings.search, it) { if (it->item.action == action) tll_remove(conf->bindings.search, it); } return true; } key_combo_list_t key_combos = tll_init(); if (!parse_key_combos(conf, value, &key_combos, path, lineno) || has_search_binding_collisions(conf, action, &key_combos, path, lineno)) { free_key_combo_list(&key_combos); return false; } /* Remove existing bindings for this action */ tll_foreach(conf->bindings.search, it) { if (it->item.action == action) tll_remove(conf->bindings.search, it); } /* Emit key bindings */ tll_foreach(key_combos, it) { struct config_key_binding_search binding = { .action = action, .modifiers = it->item.modifiers, .sym = it->item.sym, }; tll_push_back(conf->bindings.search, binding); } free_key_combo_list(&key_combos); return true; } LOG_AND_NOTIFY_ERR("%s:%u: [search-bindings]: %s: invalid key", path, lineno, key); return false; } static bool parse_mouse_combos(struct config *conf, const char *combos, key_combo_list_t *key_combos, const char *path, unsigned lineno) { xassert(tll_length(*key_combos) == 0); char *copy = xstrdup(combos); for (char *tok_ctx = NULL, *combo = strtok_r(copy, " ", &tok_ctx); combo != NULL; combo = strtok_r(NULL, " ", &tok_ctx)) { struct config_key_modifiers modifiers = {0}; char *key = strrchr(combo, '+'); if (key == NULL) { /* No modifiers */ key = combo; } else { *key = '\0'; if (!parse_modifiers(conf, combo, key - combo, &modifiers, path, lineno)) goto err; if (modifiers.shift) { LOG_AND_NOTIFY_ERR( "%s:%d: Shift cannot be used in mouse bindings", path, lineno); goto err; } key++; /* Skip past the '+' */ } size_t count = 1; { char *_count = strrchr(key, '-'); if (_count != NULL) { *_count = '\0'; _count++; errno = 0; char *end; unsigned long value = strtoul(_count, &end, 10); if (_count[0] == '\0' || *end != '\0' || errno != 0) { if (errno != 0) LOG_AND_NOTIFY_ERRNO( "%s:%d: %s: invalid click count", path, lineno, _count); else LOG_AND_NOTIFY_ERR( "%s:%d: %s: invalid click count", path, lineno, _count); goto err; } count = value; } } static const struct { const char *name; int code; } map[] = { {"BTN_LEFT", BTN_LEFT}, {"BTN_RIGHT", BTN_RIGHT}, {"BTN_MIDDLE", BTN_MIDDLE}, {"BTN_SIDE", BTN_SIDE}, {"BTN_EXTRA", BTN_EXTRA}, {"BTN_FORWARD", BTN_FORWARD}, {"BTN_BACK", BTN_BACK}, {"BTN_TASK", BTN_TASK}, }; int button = 0; for (size_t i = 0; i < ALEN(map); i++) { if (strcmp(key, map[i].name) == 0) { button = map[i].code; break; } } if (button == 0) { LOG_AND_NOTIFY_ERR("%s:%d: %s: invalid mouse button name", path, lineno, key); goto err; } struct key_combo new = { .text = xstrdup(combo), .modifiers = modifiers, .m = { .button = button, .count = count, }, }; tll_push_back(*key_combos, new); } free(copy); return true; err: tll_foreach(*key_combos, it) free(it->item.text); tll_free(*key_combos); free(copy); return false; } static bool has_mouse_binding_collisions(struct config *conf, const key_combo_list_t *key_combos, const char *path, unsigned lineno) { tll_foreach(conf->bindings.mouse, it) { tll_foreach(*key_combos, it2) { const struct config_key_modifiers *mods1 = &it->item.modifiers; const struct config_key_modifiers *mods2 = &it2->item.modifiers; bool shift = mods1->shift == mods2->shift; bool alt = mods1->alt == mods2->alt; bool ctrl = mods1->ctrl == mods2->ctrl; bool meta = mods1->meta == mods2->meta; bool button = it->item.button == it2->item.m.button; bool count = it->item.count == it2->item.m.count; if (shift && alt && ctrl && meta && button && count) { bool has_pipe = it->item.pipe.cmd != NULL; LOG_AND_NOTIFY_ERR("%s:%d: %s already mapped to '%s%s%s%s'", path, lineno, it2->item.text, binding_action_map[it->item.action], has_pipe ? " [" : "", has_pipe ? it->item.pipe.cmd : "", has_pipe ? "]" : ""); return true; } } } return false; } static bool parse_section_mouse_bindings( const char *key, const char *value, struct config *conf, const char *path, unsigned lineno) { char *pipe_cmd; char **pipe_argv; ssize_t pipe_remove_len = pipe_argv_from_string( value, &pipe_cmd, &pipe_argv, conf, path, lineno); if (pipe_remove_len < 0) return false; value += pipe_remove_len; for (enum bind_action_normal action = 0; action < BIND_ACTION_COUNT; action++) { if (binding_action_map[action] == NULL) continue; if (strcmp(key, binding_action_map[action]) != 0) continue; /* Unset binding */ if (strcasecmp(value, "none") == 0) { tll_foreach(conf->bindings.mouse, it) { if (it->item.action == action) { if (it->item.pipe.master_copy) { free(it->item.pipe.cmd); free(it->item.pipe.argv); } tll_remove(conf->bindings.mouse, it); } } free(pipe_argv); free(pipe_cmd); return true; } key_combo_list_t key_combos = tll_init(); if (!parse_mouse_combos(conf, value, &key_combos, path, lineno) || has_mouse_binding_collisions(conf, &key_combos, path, lineno)) { free(pipe_argv); free(pipe_cmd); free_key_combo_list(&key_combos); return false; } /* Remove existing bindings for this action */ tll_foreach(conf->bindings.mouse, it) { if (it->item.action == action && ((it->item.pipe.argv == NULL && pipe_argv == NULL) || (it->item.pipe.argv != NULL && pipe_argv != NULL && argv_compare(it->item.pipe.argv, pipe_argv) == 0))) { if (it->item.pipe.master_copy) { free(it->item.pipe.cmd); free(it->item.pipe.argv); } tll_remove(conf->bindings.mouse, it); } } /* Emit mouse bindings */ bool first = true; tll_foreach(key_combos, it) { struct config_mouse_binding binding = { .action = action, .modifiers = it->item.modifiers, .button = it->item.m.button, .count = it->item.m.count, .pipe = { .cmd = pipe_cmd, .argv = pipe_argv, .master_copy = first, }, }; tll_push_back(conf->bindings.mouse, binding); first = false; } free_key_combo_list(&key_combos); return true; } LOG_AND_NOTIFY_ERR("%s:%u: [mouse-bindings]: %s: invalid key", path, lineno, key); free(pipe_argv); free(pipe_cmd); return false; } static bool parse_section_tweak( const char *key, const char *value, struct config *conf, const char *path, unsigned lineno) { if (strcmp(key, "scaling-filter") == 0) { static const struct { const char *name; enum fcft_scaling_filter filter; } filters[] = { {"none", FCFT_SCALING_FILTER_NONE}, {"nearest", FCFT_SCALING_FILTER_NEAREST}, {"bilinear", FCFT_SCALING_FILTER_BILINEAR}, {"cubic", FCFT_SCALING_FILTER_CUBIC}, {"lanczos3", FCFT_SCALING_FILTER_LANCZOS3}, }; for (size_t i = 0; i < ALEN(filters); i++) { if (strcmp(value, filters[i].name) == 0) { conf->tweak.fcft_filter = filters[i].filter; LOG_WARN("tweak: scaling-filter=%s", filters[i].name); return true; } } LOG_AND_NOTIFY_ERR( "%s:%d: [tweak]: %s: invalid 'scaling-filter' value, " "expected one of 'none', 'nearest', 'bilinear', 'cubic' or " "'lanczos3'", path, lineno, value); return false; } else if (strcmp(key, "allow-overflowing-double-width-glyphs") == 0) { conf->tweak.allow_overflowing_double_width_glyphs = str_to_bool(value); if (!conf->tweak.allow_overflowing_double_width_glyphs) LOG_WARN("tweak: disabled overflowing double-width glyphs"); } else if (strcmp(key, "damage-whole-window") == 0) { conf->tweak.damage_whole_window = str_to_bool(value); if (conf->tweak.damage_whole_window) LOG_WARN("tweak: damage whole window"); } else if (strcmp(key, "render-timer") == 0) { if (strcmp(value, "none") == 0) { conf->tweak.render_timer_osd = false; conf->tweak.render_timer_log = false; } else if (strcmp(value, "osd") == 0) { conf->tweak.render_timer_osd = true; conf->tweak.render_timer_log = false; } else if (strcmp(value, "log") == 0) { conf->tweak.render_timer_osd = false; conf->tweak.render_timer_log = true; } else if (strcmp(value, "both") == 0) { conf->tweak.render_timer_osd = true; conf->tweak.render_timer_log = true; } else { LOG_AND_NOTIFY_ERR( "%s:%d: [tweak]: %s: invalid 'render-timer' value, " "expected one of 'none', 'osd', 'log' or 'both'", path, lineno, value); return false; } } else if (strcmp(key, "delayed-render-lower") == 0) { unsigned long ns; if (!str_to_ulong(value, 10, &ns)) { LOG_AND_NOTIFY_ERR("%s:%d: expected an integer, got '%s'", path, lineno, value); return false; } if (ns > 16666666) { LOG_AND_NOTIFY_ERR("%s:%d: timeout must not exceed 16ms", path, lineno); return false; } conf->tweak.delayed_render_lower_ns = ns; LOG_WARN("tweak: delayed-render-lower=%lu", ns); } else if (strcmp(key, "delayed-render-upper") == 0) { unsigned long ns; if (!str_to_ulong(value, 10, &ns)) { LOG_AND_NOTIFY_ERR("%s:%d: expected an integer, got '%s'", path, lineno, value); return false; } if (ns > 16666666) { LOG_AND_NOTIFY_ERR("%s:%d: timeout must not exceed 16ms", path, lineno); return false; } conf->tweak.delayed_render_upper_ns = ns; LOG_WARN("tweak: delayed-render-upper=%lu", ns); } else if (strcmp(key, "max-shm-pool-size-mb") == 0) { unsigned long mb; if (!str_to_ulong(value, 10, &mb)) { LOG_AND_NOTIFY_ERR("%s:%d: expected an integer, got '%s'", path, lineno, value); return false; } conf->tweak.max_shm_pool_size = min(mb * 1024 * 1024, INT32_MAX); LOG_WARN("tweak: max-shm-pool-size=%lld bytes", (long long)conf->tweak.max_shm_pool_size); } else if (strcmp(key, "box-drawing-base-thickness") == 0) { double base_thickness; if (!str_to_double(value, &base_thickness)) { LOG_AND_NOTIFY_ERR( "%s:%d: [tweak]: box-drawing-base-thickness: " "expected a decimal value, got '%s'", path, lineno, value); return false; } conf->tweak.box_drawing_base_thickness = base_thickness; LOG_WARN("tweak: box-drawing-base-thickness=%f", conf->tweak.box_drawing_base_thickness); } else { LOG_AND_NOTIFY_ERR("%s:%u: [tweak]: %s: invalid key", path, lineno, key); return false; } return true; } static bool parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_are_fatal) { enum section { SECTION_MAIN, SECTION_SCROLLBACK, SECTION_COLORS, SECTION_CURSOR, SECTION_MOUSE, SECTION_CSD, SECTION_KEY_BINDINGS, SECTION_SEARCH_BINDINGS, SECTION_MOUSE_BINDINGS, SECTION_TWEAK, SECTION_COUNT, } section = SECTION_MAIN; /* Function pointer, called for each key/value line */ typedef bool (*parser_fun_t)( const char *key, const char *value, struct config *conf, const char *path, unsigned lineno); static const struct { parser_fun_t fun; const char *name; } section_info[] = { [SECTION_MAIN] = {&parse_section_main, "main"}, [SECTION_SCROLLBACK] = {&parse_section_scrollback, "scrollback"}, [SECTION_COLORS] = {&parse_section_colors, "colors"}, [SECTION_CURSOR] = {&parse_section_cursor, "cursor"}, [SECTION_MOUSE] = {&parse_section_mouse, "mouse"}, [SECTION_CSD] = {&parse_section_csd, "csd"}, [SECTION_KEY_BINDINGS] = {&parse_section_key_bindings, "key-bindings"}, [SECTION_SEARCH_BINDINGS] = {&parse_section_search_bindings, "search-bindings"}, [SECTION_MOUSE_BINDINGS] = {&parse_section_mouse_bindings, "mouse-bindings"}, [SECTION_TWEAK] = {&parse_section_tweak, "tweak"}, }; static_assert(ALEN(section_info) == SECTION_COUNT, "section info array size mismatch"); unsigned lineno = 0; char *_line = NULL; size_t count = 0; #define error_or_continue() \ { \ if (errors_are_fatal) \ goto err; \ else \ continue; \ } while (true) { errno = 0; lineno++; ssize_t ret = getline(&_line, &count, f); if (ret < 0) { if (errno != 0) { LOG_AND_NOTIFY_ERRNO("failed to read from configuration"); if (errors_are_fatal) goto err; } break; } /* Strip leading whitespace */ char *line = _line; { while (isspace(*line)) line++; if (line[0] != '\0') { char *end = line + strlen(line) - 1; while (isspace(*end)) end--; *(end + 1) = '\0'; } } /* Empty line, or comment */ if (line[0] == '\0' || line[0] == '#') continue; /* Split up into key/value pair + trailing comment separated by blank */ char *key_value = line; char *comment = line; while (comment[0] != '\0') { const char c = comment[0]; comment++; if (isblank(c) && comment[0] == '#') { comment[0] = '\0'; /* Terminate key/value pair */ comment++; break; } } /* Check for new section */ if (key_value[0] == '[') { char *end = strchr(key_value, ']'); if (end == NULL) { LOG_AND_NOTIFY_ERR("%s:%d: syntax error: %s", path, lineno, key_value); error_or_continue(); } *end = '\0'; section = SECTION_COUNT; for (enum section i = 0; i < SECTION_COUNT; i++) { if (strcmp(&key_value[1], section_info[i].name) == 0) { section = i; } } if (section == SECTION_COUNT) { LOG_AND_NOTIFY_ERR("%s:%d: invalid section name: %s", path, lineno, &key_value[1]); error_or_continue(); } /* Process next line */ continue; } if (section >= SECTION_COUNT) { /* Last section name was invalid; ignore all keys in it */ continue; } char *key = strtok(key_value, "="); if (key == NULL) { LOG_AND_NOTIFY_ERR("%s:%d: syntax error: no key specified", path, lineno); error_or_continue(); } char *value = strtok(NULL, "\n"); if (value == NULL) { /* Empty value, i.e. "key=" */ value = key + strlen(key); } /* Strip trailing whitespace from key (leading stripped earlier) */ { xassert(!isspace(*key)); char *end = key + strlen(key) - 1; while (isspace(*end)) end--; *(end + 1) = '\0'; } /* Strip leading+trailing whitespace from value */ { while (isspace(*value)) value++; if (value[0] != '\0') { char *end = value + strlen(value) - 1; while (isspace(*end)) end--; *(end + 1) = '\0'; } } LOG_DBG("section=%s, key='%s', value='%s', comment='%s'", section_info[section].name, key, value, comment); xassert(section >= 0 && section < SECTION_COUNT); parser_fun_t section_parser = section_info[section].fun; xassert(section_parser != NULL); if (!section_parser(key, value, conf, path, lineno)) error_or_continue(); } free(_line); return true; err: free(_line); return false; } static char * get_server_socket_path(void) { const char *xdg_runtime = getenv("XDG_RUNTIME_DIR"); if (xdg_runtime == NULL) return xstrdup("/tmp/foot.sock"); const char *wayland_display = getenv("WAYLAND_DISPLAY"); if (wayland_display == NULL) { return xasprintf("%s/foot.sock", xdg_runtime); } return xasprintf("%s/foot-%s.sock", xdg_runtime, wayland_display); } static void add_default_key_bindings(struct config *conf) { #define add_binding(action, mods, sym) \ do { \ tll_push_back( \ conf->bindings.key, \ ((struct config_key_binding_normal){action, mods, sym})); \ } while (0) const struct config_key_modifiers shift = {.shift = true}; const struct config_key_modifiers ctrl = {.ctrl = true}; const struct config_key_modifiers ctrl_shift = {.ctrl = true, .shift = true}; add_binding(BIND_ACTION_SCROLLBACK_UP_PAGE, shift, XKB_KEY_Page_Up); add_binding(BIND_ACTION_SCROLLBACK_DOWN_PAGE, shift, XKB_KEY_Page_Down); add_binding(BIND_ACTION_CLIPBOARD_COPY, ctrl_shift, XKB_KEY_C); add_binding(BIND_ACTION_CLIPBOARD_PASTE, ctrl_shift, XKB_KEY_V); add_binding(BIND_ACTION_PRIMARY_PASTE, shift, XKB_KEY_Insert); add_binding(BIND_ACTION_SEARCH_START, ctrl_shift, XKB_KEY_R); add_binding(BIND_ACTION_FONT_SIZE_UP, ctrl, XKB_KEY_plus); add_binding(BIND_ACTION_FONT_SIZE_UP, ctrl, XKB_KEY_equal); add_binding(BIND_ACTION_FONT_SIZE_UP, ctrl, XKB_KEY_KP_Add); add_binding(BIND_ACTION_FONT_SIZE_DOWN, ctrl, XKB_KEY_minus); add_binding(BIND_ACTION_FONT_SIZE_DOWN, ctrl, XKB_KEY_KP_Subtract); add_binding(BIND_ACTION_FONT_SIZE_RESET, ctrl, XKB_KEY_0); add_binding(BIND_ACTION_FONT_SIZE_RESET, ctrl, XKB_KEY_KP_0); add_binding(BIND_ACTION_SPAWN_TERMINAL, ctrl_shift, XKB_KEY_N); #undef add_binding } static void add_default_search_bindings(struct config *conf) { #define add_binding(action, mods, sym) \ do { \ tll_push_back( \ conf->bindings.search, \ ((struct config_key_binding_search){action, mods, sym})); \ } while (0) const struct config_key_modifiers none = {0}; const struct config_key_modifiers alt = {.alt = true}; const struct config_key_modifiers ctrl = {.ctrl = true}; const struct config_key_modifiers shift = {.shift = true}; const struct config_key_modifiers ctrl_shift = {.ctrl = true, .shift = true}; add_binding(BIND_ACTION_SEARCH_CANCEL, ctrl, XKB_KEY_g); add_binding(BIND_ACTION_SEARCH_CANCEL, none, XKB_KEY_Escape); add_binding(BIND_ACTION_SEARCH_COMMIT, none, XKB_KEY_Return); add_binding(BIND_ACTION_SEARCH_FIND_PREV, ctrl, XKB_KEY_r); add_binding(BIND_ACTION_SEARCH_FIND_NEXT, ctrl, XKB_KEY_s); add_binding(BIND_ACTION_SEARCH_EDIT_LEFT, none, XKB_KEY_Left); add_binding(BIND_ACTION_SEARCH_EDIT_LEFT, ctrl, XKB_KEY_b); add_binding(BIND_ACTION_SEARCH_EDIT_LEFT_WORD, ctrl, XKB_KEY_Left); add_binding(BIND_ACTION_SEARCH_EDIT_LEFT_WORD, alt, XKB_KEY_b); add_binding(BIND_ACTION_SEARCH_EDIT_RIGHT, none, XKB_KEY_Right); add_binding(BIND_ACTION_SEARCH_EDIT_RIGHT, ctrl, XKB_KEY_f); add_binding(BIND_ACTION_SEARCH_EDIT_RIGHT_WORD, ctrl, XKB_KEY_Right); add_binding(BIND_ACTION_SEARCH_EDIT_RIGHT_WORD, alt, XKB_KEY_f); add_binding(BIND_ACTION_SEARCH_EDIT_HOME, none, XKB_KEY_Home); add_binding(BIND_ACTION_SEARCH_EDIT_HOME, ctrl, XKB_KEY_a); add_binding(BIND_ACTION_SEARCH_EDIT_END, none, XKB_KEY_End); add_binding(BIND_ACTION_SEARCH_EDIT_END, ctrl, XKB_KEY_e); add_binding(BIND_ACTION_SEARCH_DELETE_PREV, none, XKB_KEY_BackSpace); add_binding(BIND_ACTION_SEARCH_DELETE_PREV_WORD, ctrl, XKB_KEY_BackSpace); add_binding(BIND_ACTION_SEARCH_DELETE_PREV_WORD, alt, XKB_KEY_BackSpace); add_binding(BIND_ACTION_SEARCH_DELETE_NEXT, none, XKB_KEY_Delete); add_binding(BIND_ACTION_SEARCH_DELETE_NEXT_WORD, ctrl, XKB_KEY_Delete); add_binding(BIND_ACTION_SEARCH_DELETE_NEXT_WORD, alt, XKB_KEY_d); add_binding(BIND_ACTION_SEARCH_EXTEND_WORD, ctrl, XKB_KEY_w); add_binding(BIND_ACTION_SEARCH_EXTEND_WORD_WS, ctrl_shift, XKB_KEY_W); add_binding(BIND_ACTION_SEARCH_CLIPBOARD_PASTE, ctrl, XKB_KEY_v); add_binding(BIND_ACTION_SEARCH_CLIPBOARD_PASTE, ctrl, XKB_KEY_y); add_binding(BIND_ACTION_SEARCH_PRIMARY_PASTE, shift, XKB_KEY_Insert); #undef add_binding } static void add_default_mouse_bindings(struct config *conf) { #define add_binding(action, mods, btn, count) \ do { \ tll_push_back( \ conf->bindings.mouse, \ ((struct config_mouse_binding){action, mods, btn, count, {0}})); \ } while (0) const struct config_key_modifiers none = {0}; const struct config_key_modifiers ctrl = {.ctrl = true}; add_binding(BIND_ACTION_PRIMARY_PASTE, none, BTN_MIDDLE, 1); add_binding(BIND_ACTION_SELECT_BEGIN, none, BTN_LEFT, 1); add_binding(BIND_ACTION_SELECT_BEGIN_BLOCK, ctrl, BTN_LEFT, 1); add_binding(BIND_ACTION_SELECT_EXTEND, none, BTN_RIGHT, 1); add_binding(BIND_ACTION_SELECT_EXTEND_CHAR_WISE, ctrl, BTN_RIGHT, 1); add_binding(BIND_ACTION_SELECT_WORD, none, BTN_LEFT, 2); add_binding(BIND_ACTION_SELECT_WORD_WS, ctrl, BTN_LEFT, 2); add_binding(BIND_ACTION_SELECT_ROW, none, BTN_LEFT, 3); #undef add_binding } bool config_load(struct config *conf, const char *conf_path, user_notifications_t *initial_user_notifications, bool errors_are_fatal) { bool ret = false; *conf = (struct config) { #ifdef HAVE_TERMINFO .term = xstrdup("foot"), #else .term = xstrdup("xterm-256color"), #endif .shell = get_shell(), .title = xstrdup("foot"), .app_id = xstrdup("foot"), .word_delimiters = xwcsdup(L",│`|:\"'()[]{}<>"), .size = { .type = CONF_SIZE_PX, .width = 700, .height = 500, }, .pad_x = 2, .pad_y = 2, .bold_in_bright = false, .bell_action = BELL_ACTION_NONE, .startup_mode = STARTUP_WINDOWED, .fonts = {tll_init(), tll_init(), tll_init(), tll_init()}, .line_height = { .pt = 0, .px = -1, }, .letter_spacing = { .pt = 0, .px = 0, }, .horizontal_letter_offset = {.pt = 0, .px = 0, }, .vertical_letter_offset = {.pt = 0, .px = 0, }, .dpi_aware = DPI_AWARE_AUTO, /* DPI-aware when scaling-factor == 1 */ .scrollback = { .lines = 1000, .indicator = { .position = SCROLLBACK_INDICATOR_POSITION_RELATIVE, .format = SCROLLBACK_INDICATOR_FORMAT_TEXT, .text = wcsdup(L""), }, .multiplier = 3., }, .colors = { .fg = default_foreground, .bg = default_background, .regular = { default_regular[0], default_regular[1], default_regular[2], default_regular[3], default_regular[4], default_regular[5], default_regular[6], default_regular[7], }, .bright = { default_bright[0], default_bright[1], default_bright[2], default_bright[3], default_bright[4], default_bright[5], default_bright[6], default_bright[7], }, .alpha = 0xffff, .selection_fg = 0x80000000, /* Use default bg */ .selection_bg = 0x80000000, /* Use default fg */ .selection_uses_custom_colors = false, }, .cursor = { .style = CURSOR_BLOCK, .blink = false, .color = { .text = 0, .cursor = 0, }, }, .mouse = { .hide_when_typing = false, .alternate_scroll_mode = true, }, .csd = { .preferred = CONF_CSD_PREFER_SERVER, .title_height = 26, .border_width = 5, .button_width = 26, }, .render_worker_count = sysconf(_SC_NPROCESSORS_ONLN), .server_socket_path = get_server_socket_path(), .presentation_timings = false, .hold_at_exit = false, .notify = { .raw_cmd = NULL, .argv = NULL, }, .tweak = { .fcft_filter = FCFT_SCALING_FILTER_LANCZOS3, .allow_overflowing_double_width_glyphs = true, .delayed_render_lower_ns = 500000, /* 0.5ms */ .delayed_render_upper_ns = 16666666 / 2, /* half a frame period (60Hz) */ .max_shm_pool_size = 512 * 1024 * 1024, .render_timer_osd = false, .render_timer_log = false, .damage_whole_window = false, .box_drawing_base_thickness = 0.04, }, .notifications = tll_init(), }; conf->notify.raw_cmd = xstrdup( "notify-send -a foot -i foot ${title} ${body}"); tokenize_cmdline(conf->notify.raw_cmd, &conf->notify.argv); tll_foreach(*initial_user_notifications, it) tll_push_back(conf->notifications, it->item); tll_free(*initial_user_notifications); add_default_key_bindings(conf); add_default_search_bindings(conf); add_default_mouse_bindings(conf); struct config_file conf_file = {.path = NULL, .fd = -1}; if (conf_path != NULL) { int fd = open(conf_path, O_RDONLY); if (fd < 0) { LOG_AND_NOTIFY_ERRNO("%s: failed to open", conf_path); ret = !errors_are_fatal; goto out; } conf_file.path = xstrdup(conf_path); conf_file.fd = fd; } else { conf_file = open_config(conf); if (conf_file.fd < 0) { LOG_AND_NOTIFY_ERR("no configuration found, using defaults"); ret = !errors_are_fatal; goto out; } } xassert(conf_file.path != NULL); xassert(conf_file.fd >= 0); LOG_INFO("loading configuration from %s", conf_file.path); FILE *f = fdopen(conf_file.fd, "r"); if (f == NULL) { LOG_AND_NOTIFY_ERRNO("%s: failed to open", conf_file.path); ret = !errors_are_fatal; goto out; } ret = parse_config_file(f, conf, conf_file.path, errors_are_fatal); fclose(f); conf->colors.selection_uses_custom_colors = conf->colors.selection_fg >> 24 == 0 && conf->colors.selection_bg >> 24 == 0; out: if (ret && tll_length(conf->fonts[0]) == 0) { struct config_font font; if (!config_font_parse("monospace", &font)) { LOG_ERR("failed to load font 'monospace' - no fonts installed?"); ret = false; } else tll_push_back(conf->fonts[0], font); } free(conf_file.path); if (conf_file.fd >= 0) close(conf_file.fd); return ret; } void config_free(struct config conf) { free(conf.term); free(conf.shell); free(conf.title); free(conf.app_id); free(conf.word_delimiters); free(conf.scrollback.indicator.text); free(conf.notify.raw_cmd); free(conf.notify.argv); for (size_t i = 0; i < ALEN(conf.fonts); i++) { tll_foreach(conf.fonts[i], it) config_font_destroy(&it->item); tll_free(conf.fonts[i]); } free(conf.server_socket_path); tll_foreach(conf.bindings.key, it) { if (it->item.pipe.master_copy) { free(it->item.pipe.cmd); free(it->item.pipe.argv); } } tll_foreach(conf.bindings.mouse, it) { if (it->item.pipe.master_copy) { free(it->item.pipe.cmd); free(it->item.pipe.argv); } } tll_free(conf.bindings.key); tll_free(conf.bindings.mouse); tll_free(conf.bindings.search); user_notifications_free(&conf.notifications); } bool config_font_parse(const char *pattern, struct config_font *font) { FcPattern *pat = FcNameParse((const FcChar8 *)pattern); if (pat == NULL) return false; double pt_size = -1.0; FcPatternGetDouble(pat, FC_SIZE, 0, &pt_size); FcPatternRemove(pat, FC_SIZE, 0); int px_size = -1; FcPatternGetInteger(pat, FC_PIXEL_SIZE, 0, &px_size); FcPatternRemove(pat, FC_PIXEL_SIZE, 0); if (pt_size == -1. && px_size == -1) pt_size = 8.0; char *stripped_pattern = (char *)FcNameUnparse(pat); FcPatternDestroy(pat); *font = (struct config_font){ .pattern = stripped_pattern, .pt_size = pt_size, .px_size = px_size }; return true; } void config_font_destroy(struct config_font *font) { if (font == NULL) return; free(font->pattern); }