mirror of
https://codeberg.org/dnkl/foot.git
synced 2026-02-04 04:06:06 -05:00
Before this patch, we always blended towards black when dimming text. However, with light color themes, it usually looks better if we dim towards white instead. This option allows you to choose which color to blend towards. The default is 'black' in '[colors]', and 'white' in '[colors2]'. Closes #2187
4109 lines
125 KiB
C
4109 lines
125 KiB
C
#include "config.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <stdbool.h>
|
|
#include <ctype.h>
|
|
#include <unistd.h>
|
|
#include <errno.h>
|
|
#include <pwd.h>
|
|
#include <fcntl.h>
|
|
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
|
|
#include <linux/input-event-codes.h>
|
|
#include <xkbcommon/xkbcommon.h>
|
|
#include <fontconfig/fontconfig.h>
|
|
|
|
#define LOG_MODULE "config"
|
|
#define LOG_ENABLE_DBG 0
|
|
#include "log.h"
|
|
#include "char32.h"
|
|
#include "debug.h"
|
|
#include "input.h"
|
|
#include "key-binding.h"
|
|
#include "macros.h"
|
|
#include "tokenize.h"
|
|
#include "util.h"
|
|
#include "xmalloc.h"
|
|
#include "xsnprintf.h"
|
|
|
|
static const uint32_t default_foreground = 0xffffff;
|
|
static const uint32_t default_background = 0x242424;
|
|
|
|
static const size_t min_csd_border_width = 5;
|
|
|
|
#define cube6(r, g) \
|
|
r|g|0x00, r|g|0x5f, r|g|0x87, r|g|0xaf, r|g|0xd7, r|g|0xff
|
|
|
|
#define cube36(r) \
|
|
cube6(r, 0x0000), \
|
|
cube6(r, 0x5f00), \
|
|
cube6(r, 0x8700), \
|
|
cube6(r, 0xaf00), \
|
|
cube6(r, 0xd700), \
|
|
cube6(r, 0xff00)
|
|
|
|
static const uint32_t default_color_table[256] = {
|
|
// Regular
|
|
0x242424,
|
|
0xf62b5a,
|
|
0x47b413,
|
|
0xe3c401,
|
|
0x24acd4,
|
|
0xf2affd,
|
|
0x13c299,
|
|
0xe6e6e6,
|
|
|
|
// Bright
|
|
0x616161,
|
|
0xff4d51,
|
|
0x35d450,
|
|
0xe9e836,
|
|
0x5dc5f8,
|
|
0xfeabf2,
|
|
0x24dfc4,
|
|
0xffffff,
|
|
|
|
// 6x6x6 RGB cube
|
|
// (color channels = i ? i*40+55 : 0, where i = 0..5)
|
|
cube36(0x000000),
|
|
cube36(0x5f0000),
|
|
cube36(0x870000),
|
|
cube36(0xaf0000),
|
|
cube36(0xd70000),
|
|
cube36(0xff0000),
|
|
|
|
// 24 shades of gray
|
|
// (color channels = i*10+8, where i = 0..23)
|
|
0x080808, 0x121212, 0x1c1c1c, 0x262626,
|
|
0x303030, 0x3a3a3a, 0x444444, 0x4e4e4e,
|
|
0x585858, 0x626262, 0x6c6c6c, 0x767676,
|
|
0x808080, 0x8a8a8a, 0x949494, 0x9e9e9e,
|
|
0xa8a8a8, 0xb2b2b2, 0xbcbcbc, 0xc6c6c6,
|
|
0xd0d0d0, 0xdadada, 0xe4e4e4, 0xeeeeee
|
|
};
|
|
|
|
/* VT330/VT340 Programmer Reference Manual - Table 2-3 VT340 Default Color Map */
|
|
static const uint32_t default_sixel_colors[16] = {
|
|
0xff000000,
|
|
0xff3333cc,
|
|
0xffcc2121,
|
|
0xff33cc33,
|
|
0xffcc33cc,
|
|
0xff33cccc,
|
|
0xffcccc33,
|
|
0xff878787,
|
|
0xff424242,
|
|
0xff545499,
|
|
0xff994242,
|
|
0xff549954,
|
|
0xff995499,
|
|
0xff549999,
|
|
0xff999954,
|
|
0xffcccccc,
|
|
};
|
|
|
|
static const char *const binding_action_map[] = {
|
|
[BIND_ACTION_NONE] = NULL,
|
|
[BIND_ACTION_NOOP] = "noop",
|
|
[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_SCROLLBACK_HOME] = "scrollback-home",
|
|
[BIND_ACTION_SCROLLBACK_END] = "scrollback-end",
|
|
[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",
|
|
[BIND_ACTION_PIPE_COMMAND_OUTPUT] = "pipe-command-output",
|
|
[BIND_ACTION_SHOW_URLS_COPY] = "show-urls-copy",
|
|
[BIND_ACTION_SHOW_URLS_LAUNCH] = "show-urls-launch",
|
|
[BIND_ACTION_SHOW_URLS_PERSISTENT] = "show-urls-persistent",
|
|
[BIND_ACTION_TEXT_BINDING] = "text-binding",
|
|
[BIND_ACTION_PROMPT_PREV] = "prompt-prev",
|
|
[BIND_ACTION_PROMPT_NEXT] = "prompt-next",
|
|
[BIND_ACTION_UNICODE_INPUT] = "unicode-input",
|
|
[BIND_ACTION_QUIT] = "quit",
|
|
[BIND_ACTION_REGEX_LAUNCH] = "regex-launch",
|
|
[BIND_ACTION_REGEX_COPY] = "regex-copy",
|
|
[BIND_ACTION_THEME_SWITCH_1] = "color-theme-switch-1",
|
|
[BIND_ACTION_THEME_SWITCH_2] = "color-theme-switch-2",
|
|
[BIND_ACTION_THEME_TOGGLE] = "color-theme-toggle",
|
|
|
|
/* Mouse-specific actions */
|
|
[BIND_ACTION_SCROLLBACK_UP_MOUSE] = "scrollback-up-mouse",
|
|
[BIND_ACTION_SCROLLBACK_DOWN_MOUSE] = "scrollback-down-mouse",
|
|
[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_QUOTE] = "select-quote",
|
|
[BIND_ACTION_SELECT_ROW] = "select-row",
|
|
};
|
|
|
|
static const char *const search_binding_action_map[] = {
|
|
[BIND_ACTION_SEARCH_NONE] = NULL,
|
|
[BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE] = "scrollback-up-page",
|
|
[BIND_ACTION_SEARCH_SCROLLBACK_UP_HALF_PAGE] = "scrollback-up-half-page",
|
|
[BIND_ACTION_SEARCH_SCROLLBACK_UP_LINE] = "scrollback-up-line",
|
|
[BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE] = "scrollback-down-page",
|
|
[BIND_ACTION_SEARCH_SCROLLBACK_DOWN_HALF_PAGE] = "scrollback-down-half-page",
|
|
[BIND_ACTION_SEARCH_SCROLLBACK_DOWN_LINE] = "scrollback-down-line",
|
|
[BIND_ACTION_SEARCH_SCROLLBACK_HOME] = "scrollback-home",
|
|
[BIND_ACTION_SEARCH_SCROLLBACK_END] = "scrollback-end",
|
|
[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_DELETE_TO_START] = "delete-to-start",
|
|
[BIND_ACTION_SEARCH_DELETE_TO_END] = "delete-to-end",
|
|
[BIND_ACTION_SEARCH_EXTEND_CHAR] = "extend-char",
|
|
[BIND_ACTION_SEARCH_EXTEND_WORD] = "extend-to-word-boundary",
|
|
[BIND_ACTION_SEARCH_EXTEND_WORD_WS] = "extend-to-next-whitespace",
|
|
[BIND_ACTION_SEARCH_EXTEND_LINE_DOWN] = "extend-line-down",
|
|
[BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR] = "extend-backward-char",
|
|
[BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD] = "extend-backward-to-word-boundary",
|
|
[BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD_WS] = "extend-backward-to-next-whitespace",
|
|
[BIND_ACTION_SEARCH_EXTEND_LINE_UP] = "extend-line-up",
|
|
[BIND_ACTION_SEARCH_CLIPBOARD_PASTE] = "clipboard-paste",
|
|
[BIND_ACTION_SEARCH_PRIMARY_PASTE] = "primary-paste",
|
|
[BIND_ACTION_SEARCH_UNICODE_INPUT] = "unicode-input",
|
|
};
|
|
|
|
static const char *const url_binding_action_map[] = {
|
|
[BIND_ACTION_URL_NONE] = NULL,
|
|
[BIND_ACTION_URL_CANCEL] = "cancel",
|
|
[BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL] = "toggle-url-visible",
|
|
};
|
|
|
|
static_assert(ALEN(binding_action_map) == BIND_ACTION_COUNT,
|
|
"binding action map size mismatch");
|
|
static_assert(ALEN(search_binding_action_map) == BIND_ACTION_SEARCH_COUNT,
|
|
"search binding action map size mismatch");
|
|
static_assert(ALEN(url_binding_action_map) == BIND_ACTION_URL_COUNT,
|
|
"URL binding action map size mismatch");
|
|
|
|
struct context {
|
|
struct config *conf;
|
|
const char *section;
|
|
const char *section_suffix;
|
|
const char *key;
|
|
const char *value;
|
|
|
|
const char *path;
|
|
unsigned lineno;
|
|
|
|
bool errors_are_fatal;
|
|
};
|
|
|
|
static const enum user_notification_kind log_class_to_notify_kind[LOG_CLASS_COUNT] = {
|
|
[LOG_CLASS_WARNING] = USER_NOTIFICATION_WARNING,
|
|
[LOG_CLASS_ERROR] = USER_NOTIFICATION_ERROR,
|
|
};
|
|
|
|
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)
|
|
{
|
|
xassert(log_class < ALEN(log_class_to_notify_kind));
|
|
enum user_notification_kind kind = log_class_to_notify_kind[log_class];
|
|
|
|
if (kind == 0) {
|
|
BUG("unsupported log class: %d", (int)log_class);
|
|
return;
|
|
}
|
|
|
|
char *formatted_msg = xvasprintf(fmt, va);
|
|
log_msg(log_class, LOG_MODULE, file, lineno, "%s", formatted_msg);
|
|
user_notification_add(&conf->notifications, kind, formatted_msg);
|
|
}
|
|
|
|
static void NOINLINE PRINTF(5)
|
|
log_and_notify(struct config *conf, enum log_class log_class,
|
|
const char *file, int lineno, const char *fmt, ...)
|
|
{
|
|
va_list va;
|
|
va_start(va, fmt);
|
|
log_and_notify_va(conf, log_class, file, lineno, fmt, va);
|
|
va_end(va);
|
|
}
|
|
|
|
static void NOINLINE PRINTF(5)
|
|
log_contextual(struct context *ctx, enum log_class log_class,
|
|
const char *file, int lineno, const char *fmt, ...)
|
|
{
|
|
va_list va;
|
|
va_start(va, fmt);
|
|
char *formatted_msg = xvasprintf(fmt, va);
|
|
va_end(va);
|
|
|
|
const bool print_dot = ctx->key != NULL;
|
|
const bool print_colon = ctx->value != NULL;
|
|
const bool print_section_suffix = ctx->section_suffix != NULL;
|
|
|
|
if (!print_dot)
|
|
ctx->key = "";
|
|
|
|
if (!print_colon)
|
|
ctx->value = "";
|
|
|
|
if (!print_section_suffix)
|
|
ctx->section_suffix = "";
|
|
|
|
log_and_notify(
|
|
ctx->conf, log_class, file, lineno, "%s:%d: [%s%s%s]%s%s%s%s: %s",
|
|
ctx->path, ctx->lineno, ctx->section,
|
|
print_section_suffix ? ":" : "", ctx->section_suffix,
|
|
print_dot ? "." : "", ctx->key, print_colon ? ": " : "",
|
|
ctx->value, formatted_msg);
|
|
free(formatted_msg);
|
|
}
|
|
|
|
|
|
static void NOINLINE VPRINTF(4)
|
|
log_and_notify_errno_va(struct config *conf, const char *file, int lineno,
|
|
const char *fmt, va_list va)
|
|
{
|
|
int errno_copy = errno;
|
|
char *formatted_msg = xvasprintf(fmt, va);
|
|
log_and_notify(
|
|
conf, LOG_CLASS_ERROR, file, lineno,
|
|
"%s: %s", formatted_msg, strerror(errno_copy));
|
|
free(formatted_msg);
|
|
}
|
|
|
|
static void NOINLINE PRINTF(4)
|
|
log_and_notify_errno(struct config *conf, const char *file, int lineno,
|
|
const char *fmt, ...)
|
|
{
|
|
va_list va;
|
|
va_start(va, fmt);
|
|
log_and_notify_errno_va(conf, file, lineno, fmt, va);
|
|
va_end(va);
|
|
}
|
|
|
|
static void NOINLINE PRINTF(4)
|
|
log_contextual_errno(struct context *ctx, const char *file, int lineno,
|
|
const char *fmt, ...)
|
|
{
|
|
va_list va;
|
|
va_start(va, fmt);
|
|
char *formatted_msg = xvasprintf(fmt, va);
|
|
va_end(va);
|
|
|
|
bool print_dot = ctx->key != NULL;
|
|
bool print_colon = ctx->value != NULL;
|
|
|
|
if (!print_dot)
|
|
ctx->key = "";
|
|
|
|
if (!print_colon)
|
|
ctx->value = "";
|
|
|
|
log_and_notify_errno(
|
|
ctx->conf, file, lineno, "%s:%d: [%s]%s%s%s%s: %s",
|
|
ctx->path, ctx->lineno, ctx->section, print_dot ? "." : "",
|
|
ctx->key, print_colon ? ": " : "", ctx->value, formatted_msg);
|
|
|
|
free(formatted_msg);
|
|
}
|
|
|
|
#define LOG_CONTEXTUAL_ERR(...) \
|
|
log_contextual(ctx, LOG_CLASS_ERROR, __FILE__, __LINE__, __VA_ARGS__)
|
|
|
|
#define LOG_CONTEXTUAL_WARN(...) \
|
|
log_contextual(ctx, LOG_CLASS_WARNING, __FILE__, __LINE__, __VA_ARGS__)
|
|
|
|
#define LOG_CONTEXTUAL_ERRNO(...) \
|
|
log_contextual_errno(ctx, __FILE__, __LINE__, __VA_ARGS__)
|
|
|
|
#define LOG_AND_NOTIFY_ERR(...) \
|
|
log_and_notify(conf, LOG_CLASS_ERROR, __FILE__, __LINE__, __VA_ARGS__)
|
|
|
|
#define LOG_AND_NOTIFY_WARN(...) \
|
|
log_and_notify(conf, LOG_CLASS_WARNING, __FILE__, __LINE__, __VA_ARGS__)
|
|
|
|
#define LOG_AND_NOTIFY_ERRNO(...) \
|
|
log_and_notify_errno(conf, __FILE__, __LINE__, __VA_ARGS__)
|
|
|
|
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 */
|
|
};
|
|
|
|
static struct config_file
|
|
open_config(void)
|
|
{
|
|
char *path = NULL;
|
|
struct config_file ret = {.path = NULL, .fd = -1};
|
|
|
|
const char *xdg_config_home = getenv("XDG_CONFIG_HOME");
|
|
const char *xdg_config_dirs = getenv("XDG_CONFIG_DIRS");
|
|
const char *home_dir = getenv("HOME");
|
|
char *xdg_config_dirs_copy = NULL;
|
|
|
|
/* First, check XDG_CONFIG_HOME (or .config, if unset) */
|
|
if (xdg_config_home != NULL && xdg_config_home[0] != '\0')
|
|
path = xstrjoin(xdg_config_home, "/foot/foot.ini");
|
|
else if (home_dir != NULL)
|
|
path = xstrjoin(home_dir, "/.config/foot/foot.ini");
|
|
|
|
if (path != NULL) {
|
|
LOG_DBG("checking for %s", path);
|
|
int fd = open(path, O_RDONLY | O_CLOEXEC);
|
|
|
|
if (fd >= 0) {
|
|
ret = (struct config_file) {.path = path, .fd = fd};
|
|
path = NULL;
|
|
goto done;
|
|
}
|
|
}
|
|
|
|
xdg_config_dirs_copy = xdg_config_dirs != NULL && xdg_config_dirs[0] != '\0'
|
|
? strdup(xdg_config_dirs)
|
|
: strdup("/etc/xdg");
|
|
|
|
if (xdg_config_dirs_copy == NULL || xdg_config_dirs_copy[0] == '\0')
|
|
goto done;
|
|
|
|
for (const char *conf_dir = strtok(xdg_config_dirs_copy, ":");
|
|
conf_dir != NULL;
|
|
conf_dir = strtok(NULL, ":"))
|
|
{
|
|
free(path);
|
|
path = xstrjoin(conf_dir, "/foot/foot.ini");
|
|
|
|
LOG_DBG("checking for %s", path);
|
|
int fd = open(path, O_RDONLY | O_CLOEXEC);
|
|
if (fd >= 0) {
|
|
ret = (struct config_file){.path = path, .fd = fd};
|
|
path = NULL;
|
|
goto done;
|
|
}
|
|
}
|
|
|
|
done:
|
|
free(xdg_config_dirs_copy);
|
|
free(path);
|
|
return ret;
|
|
}
|
|
|
|
static bool
|
|
str_has_prefix(const char *str, const char *prefix)
|
|
{
|
|
return strncmp(str, prefix, strlen(prefix)) == 0;
|
|
}
|
|
|
|
static bool NOINLINE
|
|
value_to_bool(struct context *ctx, bool *res)
|
|
{
|
|
static const char *const yes[] = {"on", "true", "yes", "1"};
|
|
static const char *const no[] = {"off", "false", "no", "0"};
|
|
|
|
for (size_t i = 0; i < ALEN(yes); i++) {
|
|
if (strcasecmp(ctx->value, yes[i]) == 0) {
|
|
*res = true;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
for (size_t i = 0; i < ALEN(no); i++) {
|
|
if (strcasecmp(ctx->value, no[i]) == 0) {
|
|
*res = false;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
LOG_CONTEXTUAL_ERR("invalid boolean value");
|
|
return false;
|
|
}
|
|
|
|
|
|
static bool NOINLINE
|
|
str_to_ulong(const char *s, int base, unsigned long *res)
|
|
{
|
|
if (s == NULL)
|
|
return false;
|
|
|
|
errno = 0;
|
|
char *end = NULL;
|
|
|
|
unsigned long v = strtoul(s, &end, base);
|
|
if (!(errno == 0 && *end == '\0'))
|
|
return false;
|
|
|
|
*res = v;
|
|
return true;
|
|
}
|
|
|
|
static bool NOINLINE
|
|
str_to_uint32(const char *s, int base, uint32_t *res)
|
|
{
|
|
unsigned long v;
|
|
bool ret = str_to_ulong(s, base, &v);
|
|
if (v > UINT32_MAX)
|
|
return false;
|
|
*res = v;
|
|
return ret;
|
|
}
|
|
|
|
static bool NOINLINE
|
|
str_to_uint16(const char *s, int base, uint16_t *res)
|
|
{
|
|
unsigned long v;
|
|
bool ret = str_to_ulong(s, base, &v);
|
|
if (v > UINT16_MAX)
|
|
return false;
|
|
*res = v;
|
|
return ret;
|
|
}
|
|
|
|
static bool NOINLINE
|
|
value_to_uint16(struct context *ctx, int base, uint16_t *res)
|
|
{
|
|
if (!str_to_uint16(ctx->value, base, res)) {
|
|
LOG_CONTEXTUAL_ERR(
|
|
"invalid integer value, or outside range 0-%u", UINT16_MAX);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool NOINLINE
|
|
value_to_uint32(struct context *ctx, int base, uint32_t *res)
|
|
{
|
|
if (!str_to_uint32(ctx->value, base, res)){
|
|
LOG_CONTEXTUAL_ERR(
|
|
"invalid integer value, or outside range 0-%u", UINT32_MAX);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool NOINLINE
|
|
value_to_dimensions(struct context *ctx, uint32_t *x, uint32_t *y)
|
|
{
|
|
if (sscanf(ctx->value, "%ux%u", x, y) != 2) {
|
|
LOG_CONTEXTUAL_ERR("invalid dimensions (must be in the form AxB)");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool NOINLINE
|
|
value_to_float(struct context *ctx, float *res)
|
|
{
|
|
const char *s = ctx->value;
|
|
|
|
if (s == NULL)
|
|
return false;
|
|
|
|
errno = 0;
|
|
char *end = NULL;
|
|
|
|
float v = strtof(s, &end);
|
|
if (!(errno == 0 && *end == '\0')) {
|
|
LOG_CONTEXTUAL_ERR("invalid decimal value");
|
|
return false;
|
|
}
|
|
|
|
*res = v;
|
|
return true;
|
|
}
|
|
|
|
static bool NOINLINE
|
|
value_to_str(struct context *ctx, char **res)
|
|
{
|
|
char *copy = xstrdup(ctx->value);
|
|
char *end = copy + strlen(copy) - 1;
|
|
|
|
/* Un-quote
|
|
*
|
|
* Note: this is very simple; we only support the *entire* value
|
|
* being quoted. That is, no mid-value quotes. Both double and
|
|
* single quotes are supported.
|
|
*
|
|
* - key="value" OK
|
|
* - key=abc "quote" def NOT OK
|
|
* - key='value' OK
|
|
*
|
|
* Finally, we support escaping the quote character, and the
|
|
* escape character itself:
|
|
*
|
|
* - key="value \"quotes\""
|
|
* - key="backslash: \\"
|
|
*
|
|
* ONLY the "current" quote character can be escaped:
|
|
*
|
|
* key="value \'" NOt OK (both backslash and single quote is kept)
|
|
*/
|
|
|
|
if ((copy[0] == '"' && *end == '"') ||
|
|
(copy[0] == '\'' && *end == '\''))
|
|
{
|
|
const char quote = copy[0];
|
|
*end = '\0';
|
|
|
|
memmove(copy, copy + 1, end - copy);
|
|
|
|
/* Un-escape */
|
|
for (char *p = copy; *p != '\0'; p++) {
|
|
if (p[0] == '\\' && (p[1] == '\\' || p[1] == quote)) {
|
|
memmove(p, p + 1, end - p);
|
|
}
|
|
}
|
|
}
|
|
|
|
free(*res);
|
|
*res = copy;
|
|
return true;
|
|
}
|
|
|
|
static bool NOINLINE
|
|
value_to_wchars(struct context *ctx, char32_t **res)
|
|
{
|
|
char32_t *s = ambstoc32(ctx->value);
|
|
if (s == NULL) {
|
|
LOG_CONTEXTUAL_ERR("not a valid string value");
|
|
return false;
|
|
}
|
|
|
|
free(*res);
|
|
*res = s;
|
|
return true;
|
|
}
|
|
|
|
static bool NOINLINE
|
|
value_to_enum(struct context *ctx, const char **value_map, int *res)
|
|
{
|
|
size_t str_len = 0;
|
|
size_t count = 0;
|
|
|
|
for (; value_map[count] != NULL; count++) {
|
|
if (strcasecmp(value_map[count], ctx->value) == 0) {
|
|
*res = count;
|
|
return true;
|
|
}
|
|
str_len += strlen(value_map[count]);
|
|
}
|
|
|
|
const size_t size = str_len + count * 4 + 1;
|
|
char valid_values[512];
|
|
size_t idx = 0;
|
|
xassert(size < sizeof(valid_values));
|
|
|
|
for (size_t i = 0; i < count; i++)
|
|
idx += xsnprintf(&valid_values[idx], size - idx, "'%s', ", value_map[i]);
|
|
|
|
if (count > 0)
|
|
valid_values[idx - 2] = '\0';
|
|
|
|
LOG_CONTEXTUAL_ERR("not one of %s", valid_values);
|
|
return false;
|
|
}
|
|
|
|
static bool NOINLINE
|
|
value_to_color(struct context *ctx, uint32_t *result, bool allow_alpha)
|
|
{
|
|
uint32_t color;
|
|
const size_t len = strlen(ctx->value);
|
|
const size_t component_count = len / 2;
|
|
|
|
if (!(len == 6 || (allow_alpha && len == 8)) ||
|
|
!str_to_uint32(ctx->value, 16, &color))
|
|
{
|
|
if (allow_alpha) {
|
|
LOG_CONTEXTUAL_ERR("color must be in either RGB or ARGB format");
|
|
} else {
|
|
LOG_CONTEXTUAL_ERR("color must be in RGB format");
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if (allow_alpha && component_count == 3) {
|
|
/* If user left out the alpha component, assume non-transparency */
|
|
color |= 0xff000000;
|
|
}
|
|
|
|
*result = color;
|
|
return true;
|
|
}
|
|
|
|
static bool NOINLINE
|
|
value_to_two_colors(struct context *ctx,
|
|
uint32_t *first, uint32_t *second, bool allow_alpha)
|
|
{
|
|
bool ret = false;
|
|
const char *original_value = ctx->value;
|
|
|
|
/* TODO: do this without strdup() */
|
|
char *value_copy = xstrdup(ctx->value);
|
|
const char *first_as_str = strtok(value_copy, " ");
|
|
const char *second_as_str = strtok(NULL, " ");
|
|
|
|
if (first_as_str == NULL || second_as_str == NULL) {
|
|
LOG_CONTEXTUAL_ERR("invalid double color value");
|
|
goto out;
|
|
}
|
|
|
|
uint32_t a, b;
|
|
|
|
ctx->value = first_as_str;
|
|
if (!value_to_color(ctx, &a, allow_alpha))
|
|
goto out;
|
|
|
|
ctx->value = second_as_str;
|
|
if (!value_to_color(ctx, &b, allow_alpha))
|
|
goto out;
|
|
|
|
*first = a;
|
|
*second = b;
|
|
ret = true;
|
|
|
|
out:
|
|
free(value_copy);
|
|
ctx->value = original_value;
|
|
return ret;
|
|
}
|
|
|
|
static bool NOINLINE
|
|
value_to_pt_or_px(struct context *ctx, struct pt_or_px *res)
|
|
{
|
|
const char *s = ctx->value;
|
|
|
|
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 (!(len > 2 && errno == 0 && end == s + len - 2)) {
|
|
LOG_CONTEXTUAL_ERR("invalid px value (must be in the form 12px)");
|
|
return false;
|
|
}
|
|
res->pt = 0;
|
|
res->px = value;
|
|
} else {
|
|
float value;
|
|
if (!value_to_float(ctx, &value))
|
|
return false;
|
|
res->pt = value;
|
|
res->px = 0;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static struct config_font_list NOINLINE
|
|
value_to_fonts(struct context *ctx)
|
|
{
|
|
size_t count = 0;
|
|
size_t size = 0;
|
|
struct config_font *fonts = NULL;
|
|
|
|
char *copy = xstrdup(ctx->value);
|
|
for (const char *font = strtok(copy, ",");
|
|
font != NULL;
|
|
font = strtok(NULL, ","))
|
|
{
|
|
/* Trim spaces, strictly speaking not necessary, but looks nice :) */
|
|
while (isspace(font[0]))
|
|
font++;
|
|
|
|
if (font[0] == '\0')
|
|
continue;
|
|
|
|
struct config_font font_data;
|
|
if (!config_font_parse(font, &font_data)) {
|
|
ctx->value = font;
|
|
LOG_CONTEXTUAL_ERR("invalid font specification");
|
|
goto err;
|
|
}
|
|
|
|
if (count + 1 > size) {
|
|
size += 4;
|
|
fonts = xrealloc(fonts, size * sizeof(fonts[0]));
|
|
}
|
|
|
|
xassert(count + 1 <= size);
|
|
fonts[count++] = font_data;
|
|
}
|
|
|
|
free(copy);
|
|
return (struct config_font_list){.arr = fonts, .count = count};
|
|
|
|
err:
|
|
free(copy);
|
|
free(fonts);
|
|
return (struct config_font_list){.arr = NULL, .count = 0};
|
|
}
|
|
|
|
static void NOINLINE
|
|
free_argv(struct argv *argv)
|
|
{
|
|
if (argv->args == NULL)
|
|
return;
|
|
for (char **a = argv->args; *a != NULL; a++)
|
|
free(*a);
|
|
free(argv->args);
|
|
argv->args = NULL;
|
|
}
|
|
|
|
static void NOINLINE
|
|
clone_argv(struct argv *dst, const struct argv *src)
|
|
{
|
|
if (src->args == NULL) {
|
|
dst->args = NULL;
|
|
return;
|
|
}
|
|
|
|
size_t count = 0;
|
|
for (char **args = src->args; *args != NULL; args++)
|
|
count++;
|
|
|
|
dst->args = xmalloc((count + 1) * sizeof(dst->args[0]));
|
|
for (char **args_src = src->args, **args_dst = dst->args;
|
|
*args_src != NULL; args_src++,
|
|
args_dst++)
|
|
{
|
|
*args_dst = xstrdup(*args_src);
|
|
}
|
|
dst->args[count] = NULL;
|
|
}
|
|
|
|
static void
|
|
spawn_template_free(struct config_spawn_template *template)
|
|
{
|
|
free_argv(&template->argv);
|
|
}
|
|
|
|
static void
|
|
spawn_template_clone(struct config_spawn_template *dst,
|
|
const struct config_spawn_template *src)
|
|
{
|
|
clone_argv(&dst->argv, &src->argv);
|
|
}
|
|
|
|
static bool NOINLINE
|
|
value_to_spawn_template(struct context *ctx,
|
|
struct config_spawn_template *template)
|
|
{
|
|
spawn_template_free(template);
|
|
|
|
char **argv = NULL;
|
|
|
|
if (ctx->value[0] == '"' && ctx->value[1] == '"' && ctx->value[2] == '\0') {
|
|
template->argv.args = NULL;
|
|
return true;
|
|
}
|
|
|
|
if (!tokenize_cmdline(ctx->value, &argv)) {
|
|
LOG_CONTEXTUAL_ERR("syntax error in command line");
|
|
return false;
|
|
}
|
|
|
|
template->argv.args = argv;
|
|
return true;
|
|
}
|
|
|
|
static bool parse_config_file(
|
|
FILE *f, struct config *conf, const char *path, bool errors_are_fatal);
|
|
|
|
static bool
|
|
parse_section_main(struct context *ctx)
|
|
{
|
|
struct config *conf = ctx->conf;
|
|
const char *key = ctx->key;
|
|
const char *value = ctx->value;
|
|
bool errors_are_fatal = ctx->errors_are_fatal;
|
|
|
|
if (streq(key, "include")) {
|
|
char *_include_path = NULL;
|
|
const char *include_path = NULL;
|
|
|
|
if (value[0] == '~' && value[1] == '/') {
|
|
const char *home_dir = getenv("HOME");
|
|
|
|
if (home_dir == NULL) {
|
|
LOG_CONTEXTUAL_ERRNO("failed to expand '~'");
|
|
return false;
|
|
}
|
|
|
|
_include_path = xstrjoin3(home_dir, "/", value + 2);
|
|
include_path = _include_path;
|
|
} else
|
|
include_path = value;
|
|
|
|
if (include_path[0] != '/') {
|
|
LOG_CONTEXTUAL_ERR("not an absolute path");
|
|
free(_include_path);
|
|
return false;
|
|
}
|
|
|
|
FILE *include = fopen(include_path, "r");
|
|
|
|
if (include == NULL) {
|
|
LOG_CONTEXTUAL_ERRNO("failed to open");
|
|
free(_include_path);
|
|
return false;
|
|
}
|
|
|
|
bool ret = parse_config_file(
|
|
include, conf, include_path, errors_are_fatal);
|
|
fclose(include);
|
|
|
|
LOG_INFO("imported sub-configuration from %s", include_path);
|
|
free(_include_path);
|
|
return ret;
|
|
}
|
|
|
|
else if (streq(key, "term"))
|
|
return value_to_str(ctx, &conf->term);
|
|
|
|
else if (streq(key, "shell"))
|
|
return value_to_str(ctx, &conf->shell);
|
|
|
|
else if (streq(key, "login-shell"))
|
|
return value_to_bool(ctx, &conf->login_shell);
|
|
|
|
else if (streq(key, "title"))
|
|
return value_to_str(ctx, &conf->title);
|
|
|
|
else if (streq(key, "locked-title"))
|
|
return value_to_bool(ctx, &conf->locked_title);
|
|
|
|
else if (streq(key, "app-id"))
|
|
return value_to_str(ctx, &conf->app_id);
|
|
|
|
else if (streq(key, "initial-window-size-pixels")) {
|
|
if (!value_to_dimensions(ctx, &conf->size.width, &conf->size.height))
|
|
return false;
|
|
|
|
conf->size.type = CONF_SIZE_PX;
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "initial-window-size-chars")) {
|
|
if (!value_to_dimensions(ctx, &conf->size.width, &conf->size.height))
|
|
return false;
|
|
|
|
conf->size.type = CONF_SIZE_CELLS;
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "pad")) {
|
|
unsigned x, y;
|
|
char mode[64] = {0};
|
|
int ret = sscanf(value, "%ux%u %63s", &x, &y, mode);
|
|
|
|
enum center_when center = CENTER_NEVER;
|
|
|
|
if (ret == 3) {
|
|
if (strcasecmp(mode, "center") == 0)
|
|
center = CENTER_ALWAYS;
|
|
else if (strcasecmp(mode, "center-when-fullscreen") == 0)
|
|
center = CENTER_FULLSCREEN;
|
|
else if (strcasecmp(mode, "center-when-maximized-and-fullscreen") == 0)
|
|
center = CENTER_MAXIMIZED_AND_FULLSCREEN;
|
|
else
|
|
center = CENTER_INVALID;
|
|
}
|
|
|
|
if ((ret != 2 && ret != 3) || center == CENTER_INVALID) {
|
|
LOG_CONTEXTUAL_ERR(
|
|
"invalid padding (must be in the form PAD_XxPAD_Y "
|
|
"[center|"
|
|
"center-when-fullscreen|"
|
|
"center-when-maximized-and-fullscreen])");
|
|
return false;
|
|
}
|
|
|
|
conf->pad_x = x;
|
|
conf->pad_y = y;
|
|
conf->center_when = ret == 2 ? CENTER_NEVER : center;
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "resize-delay-ms"))
|
|
return value_to_uint16(ctx, 10, &conf->resize_delay_ms);
|
|
|
|
else if (streq(key, "resize-by-cells"))
|
|
return value_to_bool(ctx, &conf->resize_by_cells);
|
|
|
|
else if (streq(key, "resize-keep-grid"))
|
|
return value_to_bool(ctx, &conf->resize_keep_grid);
|
|
|
|
else if (streq(key, "bold-text-in-bright")) {
|
|
if (streq(value, "palette-based")) {
|
|
conf->bold_in_bright.enabled = true;
|
|
conf->bold_in_bright.palette_based = true;
|
|
} else {
|
|
if (!value_to_bool(ctx, &conf->bold_in_bright.enabled))
|
|
return false;
|
|
conf->bold_in_bright.palette_based = false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "initial-window-mode")) {
|
|
_Static_assert(sizeof(conf->startup_mode) == sizeof(int),
|
|
"enum is not 32-bit");
|
|
|
|
return value_to_enum(
|
|
ctx,
|
|
(const char *[]){"windowed", "maximized", "fullscreen", NULL},
|
|
(int *)&conf->startup_mode);
|
|
}
|
|
|
|
else if (streq(key, "font") ||
|
|
streq(key, "font-bold") ||
|
|
streq(key, "font-italic") ||
|
|
streq(key, "font-bold-italic"))
|
|
|
|
{
|
|
size_t idx =
|
|
streq(key, "font") ? 0 :
|
|
streq(key, "font-bold") ? 1 :
|
|
streq(key, "font-italic") ? 2 : 3;
|
|
|
|
struct config_font_list new_list = value_to_fonts(ctx);
|
|
if (new_list.arr == NULL)
|
|
return false;
|
|
|
|
config_font_list_destroy(&conf->fonts[idx]);
|
|
conf->fonts[idx] = new_list;
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "font-size-adjustment")) {
|
|
const size_t len = strlen(ctx->value);
|
|
if (len >= 1 && ctx->value[len - 1] == '%') {
|
|
errno = 0;
|
|
char *end = NULL;
|
|
|
|
float percent = strtof(ctx->value, &end);
|
|
if (!(len > 1 && errno == 0 && end == ctx->value + len - 1)) {
|
|
LOG_CONTEXTUAL_ERR(
|
|
"invalid percent value (must be in the form 10.5%%)");
|
|
return false;
|
|
}
|
|
|
|
conf->font_size_adjustment.percent = percent / 100.;
|
|
conf->font_size_adjustment.pt_or_px.pt = 0;
|
|
conf->font_size_adjustment.pt_or_px.px = 0;
|
|
return true;
|
|
} else {
|
|
bool ret = value_to_pt_or_px(ctx, &conf->font_size_adjustment.pt_or_px);
|
|
if (ret)
|
|
conf->font_size_adjustment.percent = 0.;
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
else if (streq(key, "line-height"))
|
|
return value_to_pt_or_px(ctx, &conf->line_height);
|
|
|
|
else if (streq(key, "letter-spacing"))
|
|
return value_to_pt_or_px(ctx, &conf->letter_spacing);
|
|
|
|
else if (streq(key, "horizontal-letter-offset"))
|
|
return value_to_pt_or_px(ctx, &conf->horizontal_letter_offset);
|
|
|
|
else if (streq(key, "vertical-letter-offset"))
|
|
return value_to_pt_or_px(ctx, &conf->vertical_letter_offset);
|
|
|
|
else if (streq(key, "underline-offset")) {
|
|
if (!value_to_pt_or_px(ctx, &conf->underline_offset))
|
|
return false;
|
|
conf->use_custom_underline_offset = true;
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "underline-thickness"))
|
|
return value_to_pt_or_px(ctx, &conf->underline_thickness);
|
|
|
|
else if (streq(key, "strikeout-thickness"))
|
|
return value_to_pt_or_px(ctx, &conf->strikeout_thickness);
|
|
|
|
else if (streq(key, "dpi-aware"))
|
|
return value_to_bool(ctx, &conf->dpi_aware);
|
|
|
|
else if (streq(key, "workers"))
|
|
return value_to_uint16(ctx, 10, &conf->render_worker_count);
|
|
|
|
else if (streq(key, "word-delimiters"))
|
|
return value_to_wchars(ctx, &conf->word_delimiters);
|
|
|
|
else if (streq(key, "selection-target")) {
|
|
_Static_assert(sizeof(conf->selection_target) == sizeof(int),
|
|
"enum is not 32-bit");
|
|
|
|
return value_to_enum(
|
|
ctx,
|
|
(const char *[]){"none", "primary", "clipboard", "both", NULL},
|
|
(int *)&conf->selection_target);
|
|
}
|
|
|
|
else if (streq(key, "box-drawings-uses-font-glyphs"))
|
|
return value_to_bool(ctx, &conf->box_drawings_uses_font_glyphs);
|
|
|
|
else if (streq(key, "utmp-helper")) {
|
|
if (!value_to_str(ctx, &conf->utmp_helper_path))
|
|
return false;
|
|
|
|
if (streq(conf->utmp_helper_path, "none")) {
|
|
free(conf->utmp_helper_path);
|
|
conf->utmp_helper_path = NULL;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "gamma-correct-blending"))
|
|
return value_to_bool(ctx, &conf->gamma_correct);
|
|
|
|
else if (streq(key, "initial-color-theme")) {
|
|
_Static_assert(
|
|
sizeof(conf->initial_color_theme) == sizeof(int),
|
|
"enum is not 32-bit");
|
|
|
|
return value_to_enum(ctx, (const char*[]){"1", "2", NULL},
|
|
(int *)&conf->initial_color_theme);
|
|
}
|
|
|
|
else if (streq(key, "uppercase-regex-insert"))
|
|
return value_to_bool(ctx, &conf->uppercase_regex_insert);
|
|
|
|
else {
|
|
LOG_CONTEXTUAL_ERR("not a valid option: %s", key);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static bool
|
|
parse_section_security(struct context *ctx)
|
|
{
|
|
struct config *conf = ctx->conf;
|
|
const char *key = ctx->key;
|
|
|
|
if (streq(key, "osc52")) {
|
|
_Static_assert(sizeof(conf->security.osc52) == sizeof(int),
|
|
"enum is not 32-bit");
|
|
return value_to_enum(
|
|
ctx,
|
|
(const char *[]){"disabled", "copy-enabled", "paste-enabled", "enabled", NULL},
|
|
(int *)&conf->security.osc52);
|
|
} else {
|
|
LOG_CONTEXTUAL_ERR("not a valid option: %s", key);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static bool
|
|
parse_section_bell(struct context *ctx)
|
|
{
|
|
struct config *conf = ctx->conf;
|
|
const char *key = ctx->key;
|
|
|
|
if (streq(key, "urgent"))
|
|
return value_to_bool(ctx, &conf->bell.urgent);
|
|
else if (streq(key, "notify"))
|
|
return value_to_bool(ctx, &conf->bell.notify);
|
|
else if (streq(key, "system"))
|
|
return value_to_bool(ctx, &conf->bell.system_bell);
|
|
else if (streq(key, "visual"))
|
|
return value_to_bool(ctx, &conf->bell.flash);
|
|
else if (streq(key, "command"))
|
|
return value_to_spawn_template(ctx, &conf->bell.command);
|
|
else if (streq(key, "command-focused"))
|
|
return value_to_bool(ctx, &conf->bell.command_focused);
|
|
else {
|
|
LOG_CONTEXTUAL_ERR("not a valid option: %s", key);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static bool
|
|
parse_section_desktop_notifications(struct context *ctx)
|
|
{
|
|
struct config *conf = ctx->conf;
|
|
const char *key = ctx->key;
|
|
|
|
if (streq(key, "command"))
|
|
return value_to_spawn_template(
|
|
ctx, &conf->desktop_notifications.command);
|
|
else if (streq(key, "command-action-argument"))
|
|
return value_to_spawn_template(
|
|
ctx, &conf->desktop_notifications.command_action_arg);
|
|
else if (streq(key, "close"))
|
|
return value_to_spawn_template(
|
|
ctx, &conf->desktop_notifications.close);
|
|
else if (streq(key, "inhibit-when-focused"))
|
|
return value_to_bool(
|
|
ctx, &conf->desktop_notifications.inhibit_when_focused);
|
|
else {
|
|
LOG_CONTEXTUAL_ERR("not a valid option: %s", key);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static bool
|
|
parse_section_scrollback(struct context *ctx)
|
|
{
|
|
struct config *conf = ctx->conf;
|
|
const char *key = ctx->key;
|
|
const char *value = ctx->value;
|
|
|
|
if (streq(key, "lines"))
|
|
return value_to_uint32(ctx, 10, &conf->scrollback.lines);
|
|
|
|
else if (streq(key, "indicator-position")) {
|
|
_Static_assert(
|
|
sizeof(conf->scrollback.indicator.position) == sizeof(int),
|
|
"enum is not 32-bit");
|
|
|
|
return value_to_enum(
|
|
ctx,
|
|
(const char *[]){"none", "fixed", "relative", NULL},
|
|
(int *)&conf->scrollback.indicator.position);
|
|
}
|
|
|
|
else if (streq(key, "indicator-format")) {
|
|
if (streq(value, "percentage")) {
|
|
conf->scrollback.indicator.format
|
|
= SCROLLBACK_INDICATOR_FORMAT_PERCENTAGE;
|
|
return true;
|
|
} else if (streq(value, "line")) {
|
|
conf->scrollback.indicator.format
|
|
= SCROLLBACK_INDICATOR_FORMAT_LINENO;
|
|
return true;
|
|
} else
|
|
return value_to_wchars(ctx, &conf->scrollback.indicator.text);
|
|
}
|
|
|
|
else if (streq(key, "multiplier"))
|
|
return value_to_float(ctx, &conf->scrollback.multiplier);
|
|
|
|
else {
|
|
LOG_CONTEXTUAL_ERR("not a valid option: %s", key);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static bool
|
|
parse_section_url(struct context *ctx)
|
|
{
|
|
struct config *conf = ctx->conf;
|
|
const char *key = ctx->key;
|
|
|
|
if (streq(key, "launch"))
|
|
return value_to_spawn_template(ctx, &conf->url.launch);
|
|
|
|
else if (streq(key, "label-letters"))
|
|
return value_to_wchars(ctx, &conf->url.label_letters);
|
|
|
|
else if (streq(key, "osc8-underline")) {
|
|
_Static_assert(sizeof(conf->url.osc8_underline) == sizeof(int),
|
|
"enum is not 32-bit");
|
|
|
|
return value_to_enum(
|
|
ctx,
|
|
(const char *[]){"url-mode", "always", NULL},
|
|
(int *)&conf->url.osc8_underline);
|
|
}
|
|
|
|
else if (streq(key, "regex")) {
|
|
const char *regex = ctx->value;
|
|
regex_t preg;
|
|
|
|
int r = regcomp(&preg, regex, REG_EXTENDED);
|
|
|
|
if (r != 0) {
|
|
char err_buf[128];
|
|
regerror(r, &preg, err_buf, sizeof(err_buf));
|
|
LOG_CONTEXTUAL_ERR("invalid regex: %s", err_buf);
|
|
return false;
|
|
}
|
|
|
|
if (preg.re_nsub == 0) {
|
|
LOG_CONTEXTUAL_ERR("invalid regex: no marked subexpression(s)");
|
|
regfree(&preg);
|
|
return false;
|
|
}
|
|
|
|
regfree(&conf->url.preg);
|
|
free(conf->url.regex);
|
|
|
|
conf->url.regex = xstrdup(regex);
|
|
conf->url.preg = preg;
|
|
return true;
|
|
}
|
|
|
|
else {
|
|
LOG_CONTEXTUAL_ERR("not a valid option: %s", key);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static bool
|
|
parse_section_regex(struct context *ctx)
|
|
{
|
|
struct config *conf = ctx->conf;
|
|
const char *key = ctx->key;
|
|
|
|
const char *regex_name =
|
|
ctx->section_suffix != NULL ? ctx->section_suffix : "";
|
|
|
|
struct custom_regex *regex = NULL;
|
|
tll_foreach(conf->custom_regexes, it) {
|
|
if (streq(it->item.name, regex_name)) {
|
|
regex = &it->item;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (streq(key, "regex")) {
|
|
const char *regex_string = ctx->value;
|
|
regex_t preg;
|
|
|
|
int r = regcomp(&preg, regex_string, REG_EXTENDED);
|
|
|
|
if (r != 0) {
|
|
char err_buf[128];
|
|
regerror(r, &preg, err_buf, sizeof(err_buf));
|
|
LOG_CONTEXTUAL_ERR("invalid regex: %s", err_buf);
|
|
return false;
|
|
}
|
|
|
|
if (preg.re_nsub == 0) {
|
|
LOG_CONTEXTUAL_ERR("invalid regex: no marked subexpression(s)");
|
|
regfree(&preg);
|
|
return false;
|
|
}
|
|
|
|
if (regex == NULL) {
|
|
tll_push_back(conf->custom_regexes,
|
|
((struct custom_regex){.name = xstrdup(regex_name)}));
|
|
regex = &tll_back(conf->custom_regexes);
|
|
}
|
|
|
|
regfree(®ex->preg);
|
|
free(regex->regex);
|
|
|
|
regex->regex = xstrdup(regex_string);
|
|
regex->preg = preg;
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "launch")) {
|
|
struct config_spawn_template launch = {NULL};
|
|
if (!value_to_spawn_template(ctx, &launch))
|
|
return false;
|
|
|
|
if (regex == NULL) {
|
|
tll_push_back(conf->custom_regexes,
|
|
((struct custom_regex){.name = xstrdup(regex_name)}));
|
|
regex = &tll_back(conf->custom_regexes);
|
|
}
|
|
|
|
spawn_template_free(®ex->launch);
|
|
regex->launch = launch;
|
|
return true;
|
|
}
|
|
|
|
else {
|
|
LOG_CONTEXTUAL_ERR("not a valid option: %s", key);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static bool NOINLINE
|
|
parse_color_theme(struct context *ctx, struct color_theme *theme)
|
|
{
|
|
const char *key = ctx->key;
|
|
|
|
size_t key_len = strlen(key);
|
|
uint8_t last_digit = (unsigned char)key[key_len - 1] - '0';
|
|
uint32_t *color = NULL;
|
|
|
|
if (isdigit(key[0])) {
|
|
unsigned long index;
|
|
if (!str_to_ulong(key, 0, &index) || index >= ALEN(theme->table)) {
|
|
LOG_CONTEXTUAL_ERR(
|
|
"invalid color palette index: %s (not in range 0-%zu)",
|
|
key, ALEN(theme->table));
|
|
return false;
|
|
}
|
|
color = &theme->table[index];
|
|
}
|
|
|
|
else if (key_len == 8 && str_has_prefix(key, "regular") && last_digit < 8)
|
|
color = &theme->table[last_digit];
|
|
|
|
else if (key_len == 7 && str_has_prefix(key, "bright") && last_digit < 8)
|
|
color = &theme->table[8 + last_digit];
|
|
|
|
else if (key_len == 4 && str_has_prefix(key, "dim") && last_digit < 8) {
|
|
if (!value_to_color(ctx, &theme->dim[last_digit], false))
|
|
return false;
|
|
|
|
theme->use_custom.dim |= 1 << last_digit;
|
|
return true;
|
|
}
|
|
|
|
else if (str_has_prefix(key, "sixel") &&
|
|
((key_len == 6 && last_digit < 10) ||
|
|
(key_len == 7 && key[5] == '1' && last_digit < 6)))
|
|
{
|
|
size_t idx = key_len == 6 ? last_digit : 10 + last_digit;
|
|
return value_to_color(ctx, &theme->sixel[idx], false);
|
|
}
|
|
|
|
else if (streq(key, "flash")) color = &theme->flash;
|
|
else if (streq(key, "foreground")) color = &theme->fg;
|
|
else if (streq(key, "background")) color = &theme->bg;
|
|
else if (streq(key, "selection-foreground")) color = &theme->selection_fg;
|
|
else if (streq(key, "selection-background")) color = &theme->selection_bg;
|
|
|
|
else if (streq(key, "jump-labels")) {
|
|
if (!value_to_two_colors(
|
|
ctx,
|
|
&theme->jump_label.fg,
|
|
&theme->jump_label.bg,
|
|
false))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
theme->use_custom.jump_label = true;
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "scrollback-indicator")) {
|
|
if (!value_to_two_colors(
|
|
ctx,
|
|
&theme->scrollback_indicator.fg,
|
|
&theme->scrollback_indicator.bg,
|
|
false))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
theme->use_custom.scrollback_indicator = true;
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "search-box-no-match")) {
|
|
if (!value_to_two_colors(
|
|
ctx,
|
|
&theme->search_box.no_match.fg,
|
|
&theme->search_box.no_match.bg,
|
|
false))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
theme->use_custom.search_box_no_match = true;
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "search-box-match")) {
|
|
if (!value_to_two_colors(
|
|
ctx,
|
|
&theme->search_box.match.fg,
|
|
&theme->search_box.match.bg,
|
|
false))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
theme->use_custom.search_box_match = true;
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "cursor")) {
|
|
if (!value_to_two_colors(
|
|
ctx,
|
|
&theme->cursor.text,
|
|
&theme->cursor.cursor,
|
|
false))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
theme->use_custom.cursor = true;
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "urls")) {
|
|
if (!value_to_color(ctx, &theme->url, false))
|
|
return false;
|
|
|
|
theme->use_custom.url = true;
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "alpha")) {
|
|
float alpha;
|
|
if (!value_to_float(ctx, &alpha))
|
|
return false;
|
|
|
|
if (alpha < 0. || alpha > 1.) {
|
|
LOG_CONTEXTUAL_ERR("not in range 0.0-1.0");
|
|
return false;
|
|
}
|
|
|
|
theme->alpha = alpha * 65535.;
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "flash-alpha")) {
|
|
float alpha;
|
|
if (!value_to_float(ctx, &alpha))
|
|
return false;
|
|
|
|
if (alpha < 0. || alpha > 1.) {
|
|
LOG_CONTEXTUAL_ERR("not in range 0.0-1.0");
|
|
return false;
|
|
}
|
|
|
|
theme->flash_alpha = alpha * 65535.;
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "alpha-mode")) {
|
|
_Static_assert(sizeof(theme->alpha_mode) == sizeof(int),
|
|
"enum is not 32-bit");
|
|
|
|
return value_to_enum(
|
|
ctx,
|
|
(const char *[]){"default", "matching", "all", NULL},
|
|
(int *)&theme->alpha_mode);
|
|
}
|
|
|
|
else if (streq(key, "dim-blend-towards")) {
|
|
_Static_assert(sizeof(theme->dim_blend_towards) == sizeof(int),
|
|
"enum is not 32-bit");
|
|
|
|
return value_to_enum(
|
|
ctx,
|
|
(const char *[]){"black", "white", NULL},
|
|
(int *)&theme->dim_blend_towards);
|
|
}
|
|
|
|
else {
|
|
LOG_CONTEXTUAL_ERR("not valid option");
|
|
return false;
|
|
}
|
|
|
|
uint32_t color_value;
|
|
if (!value_to_color(ctx, &color_value, false))
|
|
return false;
|
|
|
|
*color = color_value;
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
parse_section_colors(struct context *ctx)
|
|
{
|
|
return parse_color_theme(ctx, &ctx->conf->colors);
|
|
}
|
|
|
|
static bool
|
|
parse_section_colors2(struct context *ctx)
|
|
{
|
|
return parse_color_theme(ctx, &ctx->conf->colors2);
|
|
}
|
|
|
|
static bool
|
|
parse_section_cursor(struct context *ctx)
|
|
{
|
|
struct config *conf = ctx->conf;
|
|
const char *key = ctx->key;
|
|
|
|
if (streq(key, "style")) {
|
|
_Static_assert(sizeof(conf->cursor.style) == sizeof(int),
|
|
"enum is not 32-bit");
|
|
|
|
return value_to_enum(
|
|
ctx,
|
|
(const char *[]){"block", "underline", "beam", "hollow", NULL},
|
|
(int *)&conf->cursor.style);
|
|
}
|
|
|
|
else if (streq(key, "unfocused-style")) {
|
|
_Static_assert(sizeof(conf->cursor.unfocused_style) == sizeof(int),
|
|
"enum is not 32-bit");
|
|
|
|
return value_to_enum(
|
|
ctx,
|
|
(const char *[]){"unchanged", "hollow", "none", NULL},
|
|
(int *)&conf->cursor.unfocused_style);
|
|
}
|
|
|
|
else if (streq(key, "blink"))
|
|
return value_to_bool(ctx, &conf->cursor.blink.enabled);
|
|
|
|
else if (streq(key, "blink-rate"))
|
|
return value_to_uint32(ctx, 10, &conf->cursor.blink.rate_ms);
|
|
|
|
else if (streq(key, "color")) {
|
|
LOG_WARN("%s:%d: cursor.color: deprecated; use colors.cursor instead",
|
|
ctx->path, ctx->lineno);
|
|
|
|
user_notification_add(
|
|
&conf->notifications,
|
|
USER_NOTIFICATION_DEPRECATED,
|
|
xstrdup("cursor.color: use colors.cursor instead"));
|
|
|
|
if (!value_to_two_colors(
|
|
ctx,
|
|
&conf->colors.cursor.text,
|
|
&conf->colors.cursor.cursor,
|
|
false))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
conf->colors.use_custom.cursor = true;
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "beam-thickness"))
|
|
return value_to_pt_or_px(ctx, &conf->cursor.beam_thickness);
|
|
|
|
else if (streq(key, "underline-thickness"))
|
|
return value_to_pt_or_px(ctx, &conf->cursor.underline_thickness);
|
|
|
|
else {
|
|
LOG_CONTEXTUAL_ERR("not a valid option: %s", key);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static bool
|
|
parse_section_mouse(struct context *ctx)
|
|
{
|
|
struct config *conf = ctx->conf;
|
|
const char *key = ctx->key;
|
|
|
|
if (streq(key, "hide-when-typing"))
|
|
return value_to_bool(ctx, &conf->mouse.hide_when_typing);
|
|
|
|
else if (streq(key, "alternate-scroll-mode"))
|
|
return value_to_bool(ctx, &conf->mouse.alternate_scroll_mode);
|
|
|
|
else {
|
|
LOG_CONTEXTUAL_ERR("not a valid option: %s", key);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static bool
|
|
parse_section_csd(struct context *ctx)
|
|
{
|
|
struct config *conf = ctx->conf;
|
|
const char *key = ctx->key;
|
|
|
|
if (streq(key, "preferred")) {
|
|
_Static_assert(sizeof(conf->csd.preferred) == sizeof(int),
|
|
"enum is not 32-bit");
|
|
|
|
return value_to_enum(
|
|
ctx,
|
|
(const char *[]){"none", "server", "client", NULL},
|
|
(int *)&conf->csd.preferred);
|
|
}
|
|
|
|
else if (streq(key, "font")) {
|
|
struct config_font_list new_list = value_to_fonts(ctx);
|
|
if (new_list.arr == NULL)
|
|
return false;
|
|
|
|
config_font_list_destroy(&conf->csd.font);
|
|
conf->csd.font = new_list;
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "color")) {
|
|
uint32_t color;
|
|
if (!value_to_color(ctx, &color, true))
|
|
return false;
|
|
|
|
conf->csd.color.title_set = true;
|
|
conf->csd.color.title = color;
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "size"))
|
|
return value_to_uint16(ctx, 10, &conf->csd.title_height);
|
|
|
|
else if (streq(key, "button-width"))
|
|
return value_to_uint16(ctx, 10, &conf->csd.button_width);
|
|
|
|
else if (streq(key, "button-color")) {
|
|
if (!value_to_color(ctx, &conf->csd.color.buttons, true))
|
|
return false;
|
|
|
|
conf->csd.color.buttons_set = true;
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "button-minimize-color")) {
|
|
if (!value_to_color(ctx, &conf->csd.color.minimize, true))
|
|
return false;
|
|
|
|
conf->csd.color.minimize_set = true;
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "button-maximize-color")) {
|
|
if (!value_to_color(ctx, &conf->csd.color.maximize, true))
|
|
return false;
|
|
|
|
conf->csd.color.maximize_set = true;
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "button-close-color")) {
|
|
if (!value_to_color(ctx, &conf->csd.color.quit, true))
|
|
return false;
|
|
|
|
conf->csd.color.close_set = true;
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "border-color")) {
|
|
if (!value_to_color(ctx, &conf->csd.color.border, true))
|
|
return false;
|
|
|
|
conf->csd.color.border_set = true;
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "border-width"))
|
|
return value_to_uint16(ctx, 10, &conf->csd.border_width_visible);
|
|
|
|
else if (streq(key, "hide-when-maximized"))
|
|
return value_to_bool(ctx, &conf->csd.hide_when_maximized);
|
|
|
|
else if (streq(key, "double-click-to-maximize"))
|
|
return value_to_bool(ctx, &conf->csd.double_click_to_maximize);
|
|
|
|
else {
|
|
LOG_CONTEXTUAL_ERR("not a valid action: %s", key);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static void
|
|
free_binding_aux(struct binding_aux *aux)
|
|
{
|
|
if (!aux->master_copy)
|
|
return;
|
|
|
|
switch (aux->type) {
|
|
case BINDING_AUX_NONE: break;
|
|
case BINDING_AUX_PIPE: free_argv(&aux->pipe); break;
|
|
case BINDING_AUX_TEXT: free(aux->text.data); break;
|
|
case BINDING_AUX_REGEX: free(aux->regex_name); break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
free_key_binding(struct config_key_binding *binding)
|
|
{
|
|
free_binding_aux(&binding->aux);
|
|
tll_free_and_free(binding->modifiers, free);
|
|
}
|
|
|
|
static void NOINLINE
|
|
free_key_binding_list(struct config_key_binding_list *bindings)
|
|
{
|
|
struct config_key_binding *binding = &bindings->arr[0];
|
|
|
|
for (size_t i = 0; i < bindings->count; i++, binding++)
|
|
free_key_binding(binding);
|
|
free(bindings->arr);
|
|
|
|
bindings->arr = NULL;
|
|
bindings->count = 0;
|
|
}
|
|
|
|
static void NOINLINE
|
|
parse_modifiers(const char *text, size_t len, config_modifier_list_t *modifiers)
|
|
{
|
|
tll_free_and_free(*modifiers, free);
|
|
|
|
/* Handle "none" separately because e.g. none+shift is nonsense */
|
|
if (strncmp(text, "none", len) == 0)
|
|
return;
|
|
|
|
char *copy = xstrndup(text, len);
|
|
|
|
for (char *ctx = NULL, *key = strtok_r(copy, "+", &ctx);
|
|
key != NULL;
|
|
key = strtok_r(NULL, "+", &ctx))
|
|
{
|
|
tll_push_back(*modifiers, xstrdup(key));
|
|
}
|
|
|
|
free(copy);
|
|
tll_sort(*modifiers, strcmp);
|
|
}
|
|
|
|
static int NOINLINE
|
|
argv_compare(const struct argv *argv1, const struct argv *argv2)
|
|
{
|
|
if (argv1->args == NULL && argv2->args == NULL)
|
|
return 0;
|
|
|
|
if (argv1->args == NULL)
|
|
return -1;
|
|
if (argv2->args == NULL)
|
|
return 1;
|
|
|
|
for (size_t i = 0; ; i++) {
|
|
if (argv1->args[i] == NULL && argv2->args[i] == NULL)
|
|
return 0;
|
|
if (argv1->args[i] == NULL)
|
|
return -1;
|
|
if (argv2->args[i] == NULL)
|
|
return 1;
|
|
|
|
int ret = strcmp(argv1->args[i], argv2->args[i]);
|
|
if (ret != 0)
|
|
return ret;
|
|
}
|
|
|
|
BUG("unexpected loop break");
|
|
return 1;
|
|
}
|
|
|
|
static bool NOINLINE
|
|
binding_aux_equal(const struct binding_aux *a,
|
|
const struct binding_aux *b)
|
|
{
|
|
if (a->type != b->type)
|
|
return false;
|
|
|
|
switch (a->type) {
|
|
case BINDING_AUX_NONE:
|
|
return true;
|
|
|
|
case BINDING_AUX_PIPE:
|
|
return argv_compare(&a->pipe, &b->pipe) == 0;
|
|
|
|
case BINDING_AUX_TEXT:
|
|
return a->text.len == b->text.len &&
|
|
memcmp(a->text.data, b->text.data, a->text.len) == 0;
|
|
|
|
case BINDING_AUX_REGEX:
|
|
return streq(a->regex_name, b->regex_name);
|
|
}
|
|
|
|
BUG("invalid AUX type: %d", a->type);
|
|
return false;
|
|
}
|
|
|
|
static void NOINLINE
|
|
remove_from_key_bindings_list(struct config_key_binding_list *bindings,
|
|
int action, const struct binding_aux *aux)
|
|
{
|
|
size_t remove_first_idx = 0;
|
|
size_t remove_count = 0;
|
|
|
|
for (size_t i = 0; i < bindings->count; i++) {
|
|
struct config_key_binding *binding = &bindings->arr[i];
|
|
|
|
if (binding->action != action)
|
|
continue;
|
|
|
|
if (binding_aux_equal(&binding->aux, aux)) {
|
|
if (remove_count++ == 0)
|
|
remove_first_idx = i;
|
|
|
|
xassert(remove_first_idx + remove_count - 1 == i);
|
|
free_key_binding(binding);
|
|
}
|
|
}
|
|
|
|
if (remove_count == 0)
|
|
return;
|
|
|
|
size_t move_count = bindings->count - (remove_first_idx + remove_count);
|
|
|
|
memmove(
|
|
&bindings->arr[remove_first_idx],
|
|
&bindings->arr[remove_first_idx + remove_count],
|
|
move_count * sizeof(bindings->arr[0]));
|
|
bindings->count -= remove_count;
|
|
}
|
|
|
|
static const struct {
|
|
const char *name;
|
|
int code;
|
|
} button_map[] = {
|
|
/* System defined */
|
|
{"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},
|
|
|
|
/* Foot custom, to be able to map scroll events to mouse bindings */
|
|
{"BTN_WHEEL_BACK", BTN_WHEEL_BACK},
|
|
{"BTN_WHEEL_FORWARD", BTN_WHEEL_FORWARD},
|
|
{"BTN_WHEEL_LEFT", BTN_WHEEL_LEFT},
|
|
{"BTN_WHEEL_RIGHT", BTN_WHEEL_RIGHT},
|
|
};
|
|
|
|
static int
|
|
mouse_button_name_to_code(const char *name)
|
|
{
|
|
for (size_t i = 0; i < ALEN(button_map); i++) {
|
|
if (streq(button_map[i].name, name))
|
|
return button_map[i].code;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static const char*
|
|
mouse_button_code_to_name(int code)
|
|
{
|
|
for (size_t i = 0; i < ALEN(button_map); i++) {
|
|
if (code == button_map[i].code)
|
|
return button_map[i].name;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static bool NOINLINE
|
|
value_to_key_combos(struct context *ctx, int action,
|
|
struct binding_aux *aux,
|
|
struct config_key_binding_list *bindings,
|
|
enum key_binding_type type)
|
|
{
|
|
if (strcasecmp(ctx->value, "none") == 0) {
|
|
remove_from_key_bindings_list(bindings, action, aux);
|
|
return true;
|
|
}
|
|
|
|
/* Count number of combinations */
|
|
size_t combo_count = 1;
|
|
size_t used_combos = 1; /* For error handling */
|
|
for (const char *p = strchr(ctx->value, ' ');
|
|
p != NULL;
|
|
p = strchr(p + 1, ' '))
|
|
{
|
|
combo_count++;
|
|
}
|
|
|
|
struct config_key_binding new_combos[combo_count];
|
|
|
|
char *copy = xstrdup(ctx->value);
|
|
size_t idx = 0;
|
|
|
|
for (char *tok_ctx = NULL, *combo = strtok_r(copy, " ", &tok_ctx);
|
|
combo != NULL;
|
|
combo = strtok_r(NULL, " ", &tok_ctx),
|
|
idx++, used_combos++)
|
|
{
|
|
struct config_key_binding *new_combo = &new_combos[idx];
|
|
new_combo->action = action;
|
|
new_combo->aux = *aux;
|
|
new_combo->aux.master_copy = idx == 0;
|
|
#if 0
|
|
new_combo->aux.type = BINDING_AUX_PIPE;
|
|
new_combo->aux.master_copy = idx == 0;
|
|
new_combo->aux.pipe = *argv;
|
|
#endif
|
|
memset(&new_combo->modifiers, 0, sizeof(new_combo->modifiers));
|
|
new_combo->path = ctx->path;
|
|
new_combo->lineno = ctx->lineno;
|
|
|
|
char *key = strrchr(combo, '+');
|
|
|
|
if (key == NULL) {
|
|
/* No modifiers */
|
|
key = combo;
|
|
} else {
|
|
*key = '\0';
|
|
parse_modifiers(combo, key - combo, &new_combo->modifiers);
|
|
key++; /* Skip past the '+' */
|
|
}
|
|
|
|
switch (type) {
|
|
case KEY_BINDING:
|
|
/* Translate key name to symbol */
|
|
new_combo->k.sym = xkb_keysym_from_name(key, 0);
|
|
if (new_combo->k.sym == XKB_KEY_NoSymbol) {
|
|
LOG_CONTEXTUAL_ERR("not a valid XKB key name: %s", key);
|
|
goto err;
|
|
}
|
|
break;
|
|
|
|
case MOUSE_BINDING: {
|
|
new_combo->m.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_CONTEXTUAL_ERRNO("invalid click count: %s", _count);
|
|
else
|
|
LOG_CONTEXTUAL_ERR("invalid click count: %s", _count);
|
|
goto err;
|
|
}
|
|
|
|
new_combo->m.count = value;
|
|
}
|
|
|
|
new_combo->m.button = mouse_button_name_to_code(key);
|
|
if (new_combo->m.button < 0) {
|
|
LOG_CONTEXTUAL_ERR("invalid mouse button name: %s", key);
|
|
goto err;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if (idx == 0) {
|
|
LOG_CONTEXTUAL_ERR(
|
|
"empty binding not allowed (set to 'none' to unmap)");
|
|
free(copy);
|
|
return false;
|
|
}
|
|
|
|
remove_from_key_bindings_list(bindings, action, aux);
|
|
|
|
bindings->arr = xrealloc(
|
|
bindings->arr,
|
|
(bindings->count + combo_count) * sizeof(bindings->arr[0]));
|
|
|
|
memcpy(&bindings->arr[bindings->count],
|
|
new_combos,
|
|
combo_count * sizeof(bindings->arr[0]));
|
|
bindings->count += combo_count;
|
|
|
|
free(copy);
|
|
return true;
|
|
|
|
err:
|
|
for (size_t i = 0; i < used_combos; i++)
|
|
free_key_binding(&new_combos[i]);
|
|
free(copy);
|
|
return false;
|
|
}
|
|
|
|
static bool
|
|
modifiers_equal(const config_modifier_list_t *mods1,
|
|
const config_modifier_list_t *mods2)
|
|
{
|
|
if (tll_length(*mods1) != tll_length(*mods2))
|
|
return false;
|
|
|
|
size_t count = 0;
|
|
tll_foreach(*mods1, it1) {
|
|
size_t skip = count;
|
|
tll_foreach(*mods2, it2) {
|
|
if (skip > 0) {
|
|
skip--;
|
|
continue;
|
|
}
|
|
|
|
if (strcmp(it1->item, it2->item) != 0)
|
|
return false;
|
|
break;
|
|
}
|
|
|
|
count++;
|
|
}
|
|
|
|
return true;
|
|
/*
|
|
* bool shift = mods1->shift == mods2->shift;
|
|
* bool alt = mods1->alt == mods2->alt;
|
|
* bool ctrl = mods1->ctrl == mods2->ctrl;
|
|
* bool super = mods1->super == mods2->super;
|
|
* return shift && alt && ctrl && super;
|
|
*/
|
|
}
|
|
|
|
UNITTEST
|
|
{
|
|
config_modifier_list_t mods1 = tll_init();
|
|
config_modifier_list_t mods2 = tll_init();
|
|
|
|
tll_push_back(mods1, xstrdup("foo"));
|
|
tll_push_back(mods1, xstrdup("bar"));
|
|
|
|
tll_push_back(mods2, xstrdup("foo"));
|
|
xassert(!modifiers_equal(&mods1, &mods2));
|
|
|
|
tll_push_back(mods2, xstrdup("zoo"));
|
|
xassert(!modifiers_equal(&mods1, &mods2));
|
|
|
|
free(tll_pop_back(mods2));
|
|
tll_push_back(mods2, xstrdup("bar"));
|
|
xassert(modifiers_equal(&mods1, &mods2));
|
|
|
|
tll_free_and_free(mods1, free);
|
|
tll_free_and_free(mods2, free);
|
|
}
|
|
|
|
static bool
|
|
modifiers_disjoint(const config_modifier_list_t *mods1,
|
|
const config_modifier_list_t *mods2)
|
|
{
|
|
return !modifiers_equal(mods1, mods2);
|
|
}
|
|
|
|
static char * NOINLINE
|
|
modifiers_to_str(const config_modifier_list_t *mods, bool strip_last_plus)
|
|
{
|
|
size_t len = tll_length(*mods); /* '+' separator */
|
|
tll_foreach(*mods, it)
|
|
len += strlen(it->item);
|
|
|
|
char *ret = xmalloc(len + 1);
|
|
size_t idx = 0;
|
|
tll_foreach(*mods, it) {
|
|
idx += snprintf(&ret[idx], len - idx, "%s", it->item);
|
|
ret[idx++] = '+';
|
|
}
|
|
|
|
if (strip_last_plus)
|
|
idx--;
|
|
|
|
ret[idx] = '\0';
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Parses a key binding value in the form
|
|
* "[cmd-to-exec arg1 arg2] Mods+Key"
|
|
*
|
|
* and extracts 'cmd-to-exec' and its arguments.
|
|
*
|
|
* Input:
|
|
* - value: raw string, in the form mentioned 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 that were stripped 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: allocated array containing {"cmd", "arg1", "arg2", NULL}. Caller frees.
|
|
*/
|
|
static ssize_t NOINLINE
|
|
pipe_argv_from_value(struct context *ctx, struct argv *argv)
|
|
{
|
|
argv->args = NULL;
|
|
|
|
if (ctx->value[0] != '[')
|
|
return 0;
|
|
|
|
const char *pipe_cmd_end = strrchr(ctx->value, ']');
|
|
if (pipe_cmd_end == NULL) {
|
|
LOG_CONTEXTUAL_ERR("unclosed '['");
|
|
return -1;
|
|
}
|
|
|
|
size_t pipe_len = pipe_cmd_end - ctx->value - 1;
|
|
char *cmd = xstrndup(&ctx->value[1], pipe_len);
|
|
|
|
if (!tokenize_cmdline(cmd, &argv->args)) {
|
|
LOG_CONTEXTUAL_ERR("syntax error in command line");
|
|
free(cmd);
|
|
return -1;
|
|
}
|
|
|
|
ssize_t remove_len = pipe_cmd_end + 1 - ctx->value;
|
|
ctx->value = pipe_cmd_end + 1;
|
|
while (isspace(*ctx->value)) {
|
|
ctx->value++;
|
|
remove_len++;
|
|
}
|
|
|
|
free(cmd);
|
|
return remove_len;
|
|
}
|
|
|
|
static ssize_t NOINLINE
|
|
regex_name_from_value(struct context *ctx, char **regex_name)
|
|
{
|
|
*regex_name = NULL;
|
|
|
|
if (ctx->value[0] != '[')
|
|
return 0;
|
|
|
|
const char *regex_end = strrchr(ctx->value, ']');
|
|
if (regex_end == NULL) {
|
|
LOG_CONTEXTUAL_ERR("unclosed '['");
|
|
return -1;
|
|
}
|
|
|
|
size_t regex_len = regex_end - ctx->value - 1;
|
|
*regex_name = xstrndup(&ctx->value[1], regex_len);
|
|
|
|
ssize_t remove_len = regex_end + 1 - ctx->value;
|
|
ctx->value = regex_end + 1;
|
|
while (isspace(*ctx->value)) {
|
|
ctx->value++;
|
|
remove_len++;
|
|
}
|
|
|
|
return remove_len;
|
|
}
|
|
|
|
|
|
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)
|
|
{
|
|
for (int action = 0; action < action_count; action++) {
|
|
if (action_map[action] == NULL)
|
|
continue;
|
|
|
|
if (!streq(ctx->key, action_map[action]))
|
|
continue;
|
|
|
|
struct binding_aux aux = {.type = BINDING_AUX_NONE, .master_copy = true};
|
|
|
|
/* TODO: this is ugly... */
|
|
if (action_map == binding_action_map &&
|
|
action >= BIND_ACTION_PIPE_SCROLLBACK &&
|
|
action <= BIND_ACTION_PIPE_COMMAND_OUTPUT)
|
|
{
|
|
ssize_t pipe_remove_len = pipe_argv_from_value(ctx, &aux.pipe);
|
|
if (pipe_remove_len <= 0)
|
|
return false;
|
|
|
|
aux.type = BINDING_AUX_PIPE;
|
|
aux.master_copy = true;
|
|
} else if (action_map == binding_action_map &&
|
|
action >= BIND_ACTION_REGEX_LAUNCH &&
|
|
action <= BIND_ACTION_REGEX_COPY)
|
|
{
|
|
char *regex_name = NULL;
|
|
ssize_t regex_remove_len = regex_name_from_value(ctx, ®ex_name);
|
|
if (regex_remove_len <= 0)
|
|
return false;
|
|
|
|
aux.type = BINDING_AUX_REGEX;
|
|
aux.master_copy = true;
|
|
aux.regex_name = regex_name;
|
|
}
|
|
|
|
if (!value_to_key_combos(ctx, action, &aux, bindings, KEY_BINDING)) {
|
|
free_binding_aux(&aux);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
LOG_CONTEXTUAL_ERR("not a valid action: %s", ctx->key);
|
|
return false;
|
|
}
|
|
|
|
UNITTEST
|
|
{
|
|
enum test_actions {
|
|
TEST_ACTION_NONE,
|
|
TEST_ACTION_FOO,
|
|
TEST_ACTION_BAR,
|
|
TEST_ACTION_COUNT,
|
|
};
|
|
|
|
const char *const map[] = {
|
|
[TEST_ACTION_NONE] = NULL,
|
|
[TEST_ACTION_FOO] = "foo",
|
|
[TEST_ACTION_BAR] = "bar",
|
|
};
|
|
|
|
struct config conf = {0};
|
|
struct config_key_binding_list bindings = {0};
|
|
|
|
struct context ctx = {
|
|
.conf = &conf,
|
|
.section = "",
|
|
.key = "foo",
|
|
.value = "Escape",
|
|
.path = "",
|
|
};
|
|
|
|
/*
|
|
* ADD foo=Escape
|
|
*
|
|
* This verifies we can bind a single key combo to an action.
|
|
*/
|
|
xassert(parse_key_binding_section(&ctx, ALEN(map), map, &bindings));
|
|
xassert(bindings.count == 1);
|
|
xassert(bindings.arr[0].action == TEST_ACTION_FOO);
|
|
xassert(bindings.arr[0].k.sym == XKB_KEY_Escape);
|
|
|
|
/*
|
|
* ADD bar=Control+g Control+Shift+x
|
|
*
|
|
* This verifies we can bind multiple key combos to an action.
|
|
*/
|
|
ctx.key = "bar";
|
|
ctx.value = "Control+g Control+Shift+x";
|
|
xassert(parse_key_binding_section(&ctx, ALEN(map), map, &bindings));
|
|
xassert(bindings.count == 3);
|
|
xassert(bindings.arr[0].action == TEST_ACTION_FOO);
|
|
xassert(bindings.arr[1].action == TEST_ACTION_BAR);
|
|
xassert(bindings.arr[1].k.sym == XKB_KEY_g);
|
|
xassert(tll_length(bindings.arr[1].modifiers) == 1);
|
|
xassert(strcmp(tll_front(bindings.arr[1].modifiers), XKB_MOD_NAME_CTRL) == 0);
|
|
xassert(bindings.arr[2].action == TEST_ACTION_BAR);
|
|
xassert(bindings.arr[2].k.sym == XKB_KEY_x);
|
|
xassert(tll_length(bindings.arr[2].modifiers) == 2);
|
|
xassert(strcmp(tll_front(bindings.arr[2].modifiers), XKB_MOD_NAME_CTRL) == 0);
|
|
xassert(strcmp(tll_back(bindings.arr[2].modifiers), XKB_MOD_NAME_SHIFT) == 0);
|
|
|
|
/*
|
|
* REPLACE foo with foo=Mod+v Shift+q
|
|
*
|
|
* This verifies we can update a single-combo action with multiple
|
|
* key combos.
|
|
*/
|
|
ctx.key = "foo";
|
|
ctx.value = "Mod1+v Shift+q";
|
|
xassert(parse_key_binding_section(&ctx, ALEN(map), map, &bindings));
|
|
xassert(bindings.count == 4);
|
|
xassert(bindings.arr[0].action == TEST_ACTION_BAR);
|
|
xassert(bindings.arr[1].action == TEST_ACTION_BAR);
|
|
xassert(bindings.arr[2].action == TEST_ACTION_FOO);
|
|
xassert(bindings.arr[2].k.sym == XKB_KEY_v);
|
|
xassert(tll_length(bindings.arr[2].modifiers) == 1);
|
|
xassert(strcmp(tll_front(bindings.arr[2].modifiers), XKB_MOD_NAME_ALT) == 0);
|
|
xassert(bindings.arr[3].action == TEST_ACTION_FOO);
|
|
xassert(bindings.arr[3].k.sym == XKB_KEY_q);
|
|
xassert(tll_length(bindings.arr[3].modifiers) == 1);
|
|
xassert(strcmp(tll_front(bindings.arr[3].modifiers), XKB_MOD_NAME_SHIFT) == 0);
|
|
|
|
/*
|
|
* REMOVE bar
|
|
*/
|
|
ctx.key = "bar";
|
|
ctx.value = "none";
|
|
xassert(parse_key_binding_section(&ctx, ALEN(map), map, &bindings));
|
|
xassert(bindings.count == 2);
|
|
xassert(bindings.arr[0].action == TEST_ACTION_FOO);
|
|
xassert(bindings.arr[1].action == TEST_ACTION_FOO);
|
|
|
|
/*
|
|
* REMOVE foo
|
|
*/
|
|
ctx.key = "foo";
|
|
ctx.value = "none";
|
|
xassert(parse_key_binding_section(&ctx, ALEN(map), map, &bindings));
|
|
xassert(bindings.count == 0);
|
|
|
|
free(bindings.arr);
|
|
}
|
|
|
|
static bool
|
|
parse_section_key_bindings(struct context *ctx)
|
|
{
|
|
return parse_key_binding_section(
|
|
ctx,
|
|
BIND_ACTION_KEY_COUNT, binding_action_map,
|
|
&ctx->conf->bindings.key);
|
|
}
|
|
|
|
static bool
|
|
parse_section_search_bindings(struct context *ctx)
|
|
{
|
|
return parse_key_binding_section(
|
|
ctx,
|
|
BIND_ACTION_SEARCH_COUNT, search_binding_action_map,
|
|
&ctx->conf->bindings.search);
|
|
}
|
|
|
|
static bool
|
|
parse_section_url_bindings(struct context *ctx)
|
|
{
|
|
return parse_key_binding_section(
|
|
ctx,
|
|
BIND_ACTION_URL_COUNT, url_binding_action_map,
|
|
&ctx->conf->bindings.url);
|
|
}
|
|
|
|
static bool NOINLINE
|
|
resolve_key_binding_collisions(struct config *conf, const char *section_name,
|
|
const char *const action_map[],
|
|
struct config_key_binding_list *bindings,
|
|
enum key_binding_type type)
|
|
{
|
|
bool ret = true;
|
|
|
|
for (size_t i = 1; i < bindings->count; i++) {
|
|
enum {COLLISION_NONE,
|
|
COLLISION_OVERRIDE,
|
|
COLLISION_BINDING} collision_type = COLLISION_NONE;
|
|
const struct config_key_binding *collision_binding = NULL;
|
|
|
|
struct config_key_binding *binding1 = &bindings->arr[i];
|
|
xassert(binding1->action != BIND_ACTION_NONE);
|
|
|
|
const config_modifier_list_t *mods1 = &binding1->modifiers;
|
|
|
|
/* Does our modifiers collide with the selection override mods? */
|
|
if (type == MOUSE_BINDING &&
|
|
!modifiers_disjoint(
|
|
mods1, &conf->mouse.selection_override_modifiers))
|
|
{
|
|
collision_type = COLLISION_OVERRIDE;
|
|
}
|
|
|
|
/* Does our binding collide with another binding? */
|
|
for (ssize_t j = i - 1;
|
|
collision_type == COLLISION_NONE && j >= 0;
|
|
j--)
|
|
{
|
|
const struct config_key_binding *binding2 = &bindings->arr[j];
|
|
xassert(binding2->action != BIND_ACTION_NONE);
|
|
|
|
if (binding2->action == binding1->action &&
|
|
binding_aux_equal(&binding1->aux, &binding2->aux))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const config_modifier_list_t *mods2 = &binding2->modifiers;
|
|
|
|
bool mods_equal = modifiers_equal(mods1, mods2);
|
|
bool sym_equal;
|
|
|
|
switch (type) {
|
|
case KEY_BINDING:
|
|
sym_equal = binding1->k.sym == binding2->k.sym;
|
|
break;
|
|
|
|
case MOUSE_BINDING:
|
|
sym_equal = (binding1->m.button == binding2->m.button &&
|
|
binding1->m.count == binding2->m.count);
|
|
break;
|
|
|
|
default:
|
|
BUG("unhandled key binding type");
|
|
}
|
|
|
|
if (!mods_equal || !sym_equal)
|
|
continue;
|
|
|
|
collision_binding = binding2;
|
|
collision_type = COLLISION_BINDING;
|
|
break;
|
|
}
|
|
|
|
if (collision_type != COLLISION_NONE) {
|
|
char *modifier_names = modifiers_to_str(mods1, false);
|
|
char sym_name[64];
|
|
|
|
switch (type){
|
|
case KEY_BINDING:
|
|
xkb_keysym_get_name(binding1->k.sym, sym_name, sizeof(sym_name));
|
|
break;
|
|
|
|
case MOUSE_BINDING: {
|
|
const char *button_name =
|
|
mouse_button_code_to_name(binding1->m.button);
|
|
|
|
if (binding1->m.count > 1) {
|
|
snprintf(sym_name, sizeof(sym_name), "%s-%d",
|
|
button_name, binding1->m.count);
|
|
} else
|
|
strcpy(sym_name, button_name);
|
|
break;
|
|
}
|
|
}
|
|
|
|
switch (collision_type) {
|
|
case COLLISION_NONE:
|
|
break;
|
|
|
|
case COLLISION_BINDING: {
|
|
bool has_pipe = collision_binding->aux.type == BINDING_AUX_PIPE;
|
|
LOG_AND_NOTIFY_ERR(
|
|
"%s:%d: [%s].%s: %s%s already mapped to '%s%s%s%s'",
|
|
binding1->path, binding1->lineno, section_name,
|
|
action_map[binding1->action],
|
|
modifier_names, sym_name,
|
|
action_map[collision_binding->action],
|
|
has_pipe ? " [" : "",
|
|
has_pipe ? collision_binding->aux.pipe.args[0] : "",
|
|
has_pipe ? "]" : "");
|
|
ret = false;
|
|
break;
|
|
}
|
|
|
|
case COLLISION_OVERRIDE: {
|
|
char *override_names = modifiers_to_str(
|
|
&conf->mouse.selection_override_modifiers, true);
|
|
|
|
if (override_names[0] != '\0')
|
|
override_names[strlen(override_names) - 1] = '\0';
|
|
|
|
LOG_AND_NOTIFY_ERR(
|
|
"%s:%d: [%s].%s: %s%s: "
|
|
"modifiers conflict with 'selection-override-modifiers=%s'",
|
|
binding1->path != NULL ? binding1->path : "(default)",
|
|
binding1->lineno, section_name,
|
|
action_map[binding1->action],
|
|
modifier_names, sym_name, override_names);
|
|
ret = false;
|
|
free(override_names);
|
|
break;
|
|
}
|
|
}
|
|
|
|
free(modifier_names);
|
|
|
|
if (binding1->aux.master_copy && i + 1 < bindings->count) {
|
|
struct config_key_binding *next = &bindings->arr[i + 1];
|
|
|
|
if (next->action == binding1->action &&
|
|
binding_aux_equal(&binding1->aux, &next->aux))
|
|
{
|
|
/* Transfer ownership to next binding */
|
|
next->aux.master_copy = true;
|
|
binding1->aux.master_copy = false;
|
|
}
|
|
}
|
|
|
|
free_key_binding(binding1);
|
|
|
|
/* Remove the most recent binding */
|
|
size_t move_count = bindings->count - (i + 1);
|
|
memmove(&bindings->arr[i], &bindings->arr[i + 1],
|
|
move_count * sizeof(bindings->arr[0]));
|
|
bindings->count--;
|
|
|
|
i--;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static bool
|
|
parse_section_mouse_bindings(struct context *ctx)
|
|
{
|
|
struct config *conf = ctx->conf;
|
|
const char *key = ctx->key;
|
|
const char *value = ctx->value;
|
|
|
|
if (streq(key, "selection-override-modifiers")) {
|
|
parse_modifiers(
|
|
ctx->value, strlen(value),
|
|
&conf->mouse.selection_override_modifiers);
|
|
return true;
|
|
}
|
|
|
|
struct binding_aux aux;
|
|
|
|
ssize_t pipe_remove_len = pipe_argv_from_value(ctx, &aux.pipe);
|
|
if (pipe_remove_len < 0)
|
|
return false;
|
|
|
|
aux.type = pipe_remove_len == 0 ? BINDING_AUX_NONE : BINDING_AUX_PIPE;
|
|
aux.master_copy = true;
|
|
|
|
for (enum bind_action_normal action = 0;
|
|
action < BIND_ACTION_COUNT;
|
|
action++)
|
|
{
|
|
if (binding_action_map[action] == NULL)
|
|
continue;
|
|
|
|
if (!streq(key, binding_action_map[action]))
|
|
continue;
|
|
|
|
if (!value_to_key_combos(
|
|
ctx, action, &aux, &conf->bindings.mouse, MOUSE_BINDING))
|
|
{
|
|
free_binding_aux(&aux);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
LOG_CONTEXTUAL_ERR("not a valid option: %s", key);
|
|
free_binding_aux(&aux);
|
|
return false;
|
|
}
|
|
|
|
static bool
|
|
parse_section_text_bindings(struct context *ctx)
|
|
{
|
|
struct config *conf = ctx->conf;
|
|
const char *key = ctx->key;
|
|
|
|
const size_t key_len = strlen(key);
|
|
|
|
uint8_t *data = xmalloc(key_len + 1);
|
|
size_t data_len = 0;
|
|
bool esc = false;
|
|
|
|
for (size_t i = 0; i < key_len; i++) {
|
|
if (key[i] == '\\') {
|
|
if (i + 1 >= key_len) {
|
|
ctx->value = "";
|
|
LOG_CONTEXTUAL_ERR("trailing backslash");
|
|
goto err;
|
|
}
|
|
|
|
esc = true;
|
|
}
|
|
|
|
else if (esc) {
|
|
if (key[i] != 'x') {
|
|
ctx->value = "";
|
|
LOG_CONTEXTUAL_ERR("invalid escaped character: %c", key[i]);
|
|
goto err;
|
|
}
|
|
if (i + 2 >= key_len) {
|
|
ctx->value = "";
|
|
LOG_CONTEXTUAL_ERR("\\x sequence too short");
|
|
goto err;
|
|
}
|
|
|
|
const uint8_t nib1 = hex2nibble(key[i + 1]);
|
|
const uint8_t nib2 = hex2nibble(key[i + 2]);
|
|
|
|
if (nib1 >= HEX_DIGIT_INVALID || nib2 >= HEX_DIGIT_INVALID) {
|
|
ctx->value = "";
|
|
LOG_CONTEXTUAL_ERR("invalid \\x sequence: \\x%c%c",
|
|
key[i + 1], key[i + 2]);
|
|
goto err;
|
|
}
|
|
|
|
data[data_len++] = nib1 << 4 | nib2;
|
|
esc = false;
|
|
i += 2;
|
|
}
|
|
|
|
else
|
|
data[data_len++] = key[i];
|
|
}
|
|
|
|
struct binding_aux aux = {
|
|
.type = BINDING_AUX_TEXT,
|
|
.text = {
|
|
.data = data, /* data is now owned by value_to_key_combos() */
|
|
.len = data_len,
|
|
},
|
|
};
|
|
|
|
if (!value_to_key_combos(ctx, BIND_ACTION_TEXT_BINDING, &aux,
|
|
&conf->bindings.key, KEY_BINDING))
|
|
{
|
|
/* Do *not* free(data) - it is handled by value_to_key_combos() */
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
|
|
err:
|
|
free(data);
|
|
return false;
|
|
}
|
|
|
|
static bool
|
|
parse_section_environment(struct context *ctx)
|
|
{
|
|
struct config *conf = ctx->conf;
|
|
const char *key = ctx->key;
|
|
|
|
/* Check for pre-existing env variable */
|
|
tll_foreach(conf->env_vars, it) {
|
|
if (streq(it->item.name, key))
|
|
return value_to_str(ctx, &it->item.value);
|
|
}
|
|
|
|
/*
|
|
* No pre-existing variable - allocate a new one
|
|
*/
|
|
|
|
char *value = NULL;
|
|
if (!value_to_str(ctx, &value))
|
|
return false;
|
|
|
|
tll_push_back(conf->env_vars, ((struct env_var){xstrdup(key), value}));
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
parse_section_tweak(struct context *ctx)
|
|
{
|
|
struct config *conf = ctx->conf;
|
|
const char *key = ctx->key;
|
|
|
|
if (streq(key, "scaling-filter")) {
|
|
static const char *filters[] = {
|
|
[FCFT_SCALING_FILTER_NONE] = "none",
|
|
[FCFT_SCALING_FILTER_NEAREST] = "nearest",
|
|
[FCFT_SCALING_FILTER_BILINEAR] = "bilinear",
|
|
|
|
[FCFT_SCALING_FILTER_IMPULSE] = "impulse",
|
|
[FCFT_SCALING_FILTER_BOX] = "box",
|
|
[FCFT_SCALING_FILTER_LINEAR] = "linear",
|
|
[FCFT_SCALING_FILTER_CUBIC] = "cubic",
|
|
[FCFT_SCALING_FILTER_GAUSSIAN] = "gaussian",
|
|
[FCFT_SCALING_FILTER_LANCZOS2] = "lanczos2",
|
|
[FCFT_SCALING_FILTER_LANCZOS3] = "lanczos3",
|
|
[FCFT_SCALING_FILTER_LANCZOS3_STRETCHED] = "lanczos3-stretched",
|
|
NULL,
|
|
};
|
|
|
|
_Static_assert(sizeof(conf->tweak.fcft_filter) == sizeof(int),
|
|
"enum is not 32-bit");
|
|
|
|
return value_to_enum(ctx, filters, (int *)&conf->tweak.fcft_filter);
|
|
}
|
|
|
|
else if (streq(key, "overflowing-glyphs"))
|
|
return value_to_bool(ctx, &conf->tweak.overflowing_glyphs);
|
|
|
|
else if (streq(key, "damage-whole-window"))
|
|
return value_to_bool(ctx, &conf->tweak.damage_whole_window);
|
|
|
|
else if (streq(key, "grapheme-shaping")) {
|
|
if (!value_to_bool(ctx, &conf->tweak.grapheme_shaping))
|
|
return false;
|
|
|
|
#if !defined(FOOT_GRAPHEME_CLUSTERING)
|
|
if (conf->tweak.grapheme_shaping) {
|
|
LOG_CONTEXTUAL_WARN(
|
|
"foot was not compiled with support for grapheme shaping");
|
|
conf->tweak.grapheme_shaping = false;
|
|
}
|
|
#endif
|
|
|
|
if (conf->tweak.grapheme_shaping && !conf->can_shape_grapheme) {
|
|
LOG_WARN(
|
|
"fcft was not compiled with support for grapheme shaping");
|
|
|
|
/* Keep it enabled though - this will cause us to do
|
|
* grapheme-clustering at least */
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "grapheme-width-method")) {
|
|
_Static_assert(sizeof(conf->tweak.grapheme_width_method) == sizeof(int),
|
|
"enum is not 32-bit");
|
|
|
|
return value_to_enum(
|
|
ctx,
|
|
(const char *[]){"wcswidth", "double-width", "max", NULL},
|
|
(int *)&conf->tweak.grapheme_width_method);
|
|
}
|
|
|
|
else if (streq(key, "render-timer")) {
|
|
_Static_assert(sizeof(conf->tweak.render_timer) == sizeof(int),
|
|
"enum is not 32-bit");
|
|
|
|
return value_to_enum(
|
|
ctx,
|
|
(const char *[]){"none", "osd", "log", "both", NULL},
|
|
(int *)&conf->tweak.render_timer);
|
|
}
|
|
|
|
else if (streq(key, "delayed-render-lower")) {
|
|
uint32_t ns;
|
|
if (!value_to_uint32(ctx, 10, &ns))
|
|
return false;
|
|
|
|
if (ns > 16666666) {
|
|
LOG_CONTEXTUAL_ERR("timeout must not exceed 16ms");
|
|
return false;
|
|
}
|
|
|
|
conf->tweak.delayed_render_lower_ns = ns;
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "delayed-render-upper")) {
|
|
uint32_t ns;
|
|
if (!value_to_uint32(ctx, 10, &ns))
|
|
return false;
|
|
|
|
if (ns > 16666666) {
|
|
LOG_CONTEXTUAL_ERR("timeout must not exceed 16ms");
|
|
return false;
|
|
}
|
|
|
|
conf->tweak.delayed_render_upper_ns = ns;
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "max-shm-pool-size-mb")) {
|
|
uint32_t mb;
|
|
if (!value_to_uint32(ctx, 10, &mb))
|
|
return false;
|
|
|
|
conf->tweak.max_shm_pool_size = min((int32_t)mb * 1024 * 1024, INT32_MAX);
|
|
return true;
|
|
}
|
|
|
|
else if (streq(key, "box-drawing-base-thickness"))
|
|
return value_to_float(ctx, &conf->tweak.box_drawing_base_thickness);
|
|
|
|
else if (streq(key, "box-drawing-solid-shades"))
|
|
return value_to_bool(ctx, &conf->tweak.box_drawing_solid_shades);
|
|
|
|
else if (streq(key, "font-monospace-warn"))
|
|
return value_to_bool(ctx, &conf->tweak.font_monospace_warn);
|
|
|
|
else if (streq(key, "sixel"))
|
|
return value_to_bool(ctx, &conf->tweak.sixel);
|
|
|
|
else if (streq(key, "dim-amount"))
|
|
return value_to_float(ctx, &conf->dim.amount);
|
|
|
|
else if (streq(key, "bold-text-in-bright-amount"))
|
|
return value_to_float(ctx, &conf->bold_in_bright.amount);
|
|
|
|
else if (streq(key, "surface-bit-depth")) {
|
|
_Static_assert(sizeof(conf->tweak.surface_bit_depth) == sizeof(int),
|
|
"enum is not 32-bit");
|
|
|
|
#if defined(HAVE_PIXMAN_RGBA_16)
|
|
return value_to_enum(
|
|
ctx,
|
|
(const char *[]){"auto", "8-bit", "10-bit", "16-bit", NULL},
|
|
(int *)&conf->tweak.surface_bit_depth);
|
|
#else
|
|
return value_to_enum(
|
|
ctx,
|
|
(const char *[]){"auto", "8-bit", "10-bit", NULL},
|
|
(int *)&conf->tweak.surface_bit_depth);
|
|
#endif
|
|
}
|
|
|
|
else if (streq(key, "min-stride-alignment"))
|
|
return value_to_uint32(ctx, 10, &conf->tweak.min_stride_alignment);
|
|
|
|
else if (streq(key, "pre-apply-damage"))
|
|
return value_to_bool(ctx, &conf->tweak.preapply_damage);
|
|
|
|
else {
|
|
LOG_CONTEXTUAL_ERR("not a valid option: %s", key);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static bool
|
|
parse_section_touch(struct context *ctx) {
|
|
struct config *conf = ctx->conf;
|
|
const char *key = ctx->key;
|
|
|
|
if (streq(key, "long-press-delay"))
|
|
return value_to_uint32(ctx, 10, &conf->touch.long_press_delay);
|
|
|
|
else {
|
|
LOG_CONTEXTUAL_ERR("not a valid option: %s", key);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static bool
|
|
parse_key_value(char *kv, char **section, const char **key, const char **value)
|
|
{
|
|
bool section_is_needed = section != NULL;
|
|
|
|
/* Strip leading whitespace */
|
|
while (isspace(kv[0]))
|
|
++kv;
|
|
|
|
if (section_is_needed)
|
|
*section = "main";
|
|
|
|
if (kv[0] == '=')
|
|
return false;
|
|
|
|
*key = kv;
|
|
*value = NULL;
|
|
|
|
size_t kvlen = strlen(kv);
|
|
|
|
/* Strip trailing whitespace */
|
|
while (isspace(kv[kvlen - 1]))
|
|
kvlen--;
|
|
kv[kvlen] = '\0';
|
|
|
|
for (size_t i = 0; i < kvlen; ++i) {
|
|
if (kv[i] == '.' && section_is_needed) {
|
|
section_is_needed = false;
|
|
*section = kv;
|
|
kv[i] = '\0';
|
|
if (i == kvlen - 1 || kv[i + 1] == '=') {
|
|
*key = NULL;
|
|
return false;
|
|
}
|
|
*key = &kv[i + 1];
|
|
} else if (kv[i] == '=') {
|
|
kv[i] = '\0';
|
|
if (i != kvlen - 1)
|
|
*value = &kv[i + 1];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (*value == NULL)
|
|
return false;
|
|
|
|
/* Strip trailing whitespace from key (leading stripped earlier) */
|
|
{
|
|
xassert(!isspace(*key[0]));
|
|
|
|
char *end = (char *)*key + strlen(*key) - 1;
|
|
while (isspace(end[0]))
|
|
end--;
|
|
end[1] = '\0';
|
|
}
|
|
|
|
/* Strip leading whitespace from value (trailing stripped earlier) */
|
|
while (isspace(*value[0]))
|
|
++*value;
|
|
|
|
return true;
|
|
}
|
|
|
|
enum section {
|
|
SECTION_MAIN,
|
|
SECTION_SECURITY,
|
|
SECTION_BELL,
|
|
SECTION_DESKTOP_NOTIFICATIONS,
|
|
SECTION_SCROLLBACK,
|
|
SECTION_URL,
|
|
SECTION_REGEX,
|
|
SECTION_COLORS,
|
|
SECTION_COLORS2,
|
|
SECTION_CURSOR,
|
|
SECTION_MOUSE,
|
|
SECTION_CSD,
|
|
SECTION_KEY_BINDINGS,
|
|
SECTION_SEARCH_BINDINGS,
|
|
SECTION_URL_BINDINGS,
|
|
SECTION_MOUSE_BINDINGS,
|
|
SECTION_TEXT_BINDINGS,
|
|
SECTION_ENVIRONMENT,
|
|
SECTION_TWEAK,
|
|
SECTION_TOUCH,
|
|
SECTION_COUNT,
|
|
};
|
|
|
|
/* Function pointer, called for each key/value line */
|
|
typedef bool (*parser_fun_t)(struct context *ctx);
|
|
|
|
static const struct {
|
|
parser_fun_t fun;
|
|
const char *name;
|
|
bool allow_colon_suffix;
|
|
} section_info[] = {
|
|
[SECTION_MAIN] = {&parse_section_main, "main"},
|
|
[SECTION_SECURITY] = {&parse_section_security, "security"},
|
|
[SECTION_BELL] = {&parse_section_bell, "bell"},
|
|
[SECTION_DESKTOP_NOTIFICATIONS] = {&parse_section_desktop_notifications, "desktop-notifications"},
|
|
[SECTION_SCROLLBACK] = {&parse_section_scrollback, "scrollback"},
|
|
[SECTION_URL] = {&parse_section_url, "url"},
|
|
[SECTION_REGEX] = {&parse_section_regex, "regex", true},
|
|
[SECTION_COLORS] = {&parse_section_colors, "colors"},
|
|
[SECTION_COLORS2] = {&parse_section_colors2, "colors2"},
|
|
[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_URL_BINDINGS] = {&parse_section_url_bindings, "url-bindings"},
|
|
[SECTION_MOUSE_BINDINGS] = {&parse_section_mouse_bindings, "mouse-bindings"},
|
|
[SECTION_TEXT_BINDINGS] = {&parse_section_text_bindings, "text-bindings"},
|
|
[SECTION_ENVIRONMENT] = {&parse_section_environment, "environment"},
|
|
[SECTION_TWEAK] = {&parse_section_tweak, "tweak"},
|
|
[SECTION_TOUCH] = {&parse_section_touch, "touch"},
|
|
};
|
|
|
|
static_assert(ALEN(section_info) == SECTION_COUNT, "section info array size mismatch");
|
|
|
|
static enum section
|
|
str_to_section(char *str, char **suffix)
|
|
{
|
|
*suffix = NULL;
|
|
|
|
for (enum section section = SECTION_MAIN; section < SECTION_COUNT; ++section) {
|
|
const char *name = section_info[section].name;
|
|
|
|
if (streq(str, name))
|
|
return section;
|
|
|
|
else if (section_info[section].allow_colon_suffix) {
|
|
const size_t str_len = strlen(str);
|
|
const size_t name_len = strlen(name);
|
|
|
|
/* At least "section:" chars? */
|
|
if (str_len > name_len + 1) {
|
|
if (strncmp(str, name, name_len) == 0 && str[name_len] == ':') {
|
|
str[name_len] = '\0';
|
|
*suffix = &str[name_len + 1];
|
|
return section;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return SECTION_COUNT;
|
|
}
|
|
|
|
static bool
|
|
parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_are_fatal)
|
|
{
|
|
enum section section = SECTION_MAIN;
|
|
|
|
char *_line = NULL;
|
|
size_t count = 0;
|
|
bool ret = true;
|
|
|
|
#define error_or_continue() \
|
|
{ \
|
|
if (errors_are_fatal) { \
|
|
ret = false; \
|
|
goto done; \
|
|
} else \
|
|
continue; \
|
|
}
|
|
|
|
char *section_name = xstrdup("main");
|
|
char *section_suffix = NULL;
|
|
|
|
struct context context = {
|
|
.conf = conf,
|
|
.section = section_name,
|
|
.section_suffix = section_suffix,
|
|
.path = path,
|
|
.lineno = 0,
|
|
.errors_are_fatal = errors_are_fatal,
|
|
};
|
|
struct context *ctx = &context; /* For LOG_AND_*() */
|
|
|
|
errno = 0;
|
|
ssize_t len;
|
|
|
|
while ((len = getline(&_line, &count, f)) != -1) {
|
|
context.key = NULL;
|
|
context.value = NULL;
|
|
context.lineno++;
|
|
|
|
char *line = _line;
|
|
|
|
/* Strip leading whitespace */
|
|
while (isspace(line[0])) {
|
|
line++;
|
|
len--;
|
|
}
|
|
|
|
/* Empty line, or comment */
|
|
if (line[0] == '\0' || line[0] == '#')
|
|
continue;
|
|
|
|
/* Strip the trailing newline - may be absent on the last line */
|
|
if (line[len - 1] == '\n')
|
|
line[--len] = '\0';
|
|
|
|
/* Split up into key/value pair + trailing comment separated by blank */
|
|
char *key_value = line;
|
|
char *kv_trailing = &line[len - 1];
|
|
char *comment = &line[1];
|
|
while (comment[1] != '\0') {
|
|
if (isblank(comment[0]) && comment[1] == '#') {
|
|
comment[1] = '\0'; /* Terminate key/value pair */
|
|
kv_trailing = comment++;
|
|
break;
|
|
}
|
|
comment++;
|
|
}
|
|
comment++;
|
|
|
|
/* Strip trailing whitespace */
|
|
while (isspace(kv_trailing[0]))
|
|
kv_trailing--;
|
|
kv_trailing[1] = '\0';
|
|
|
|
/* Check for new section */
|
|
if (key_value[0] == '[') {
|
|
key_value++;
|
|
|
|
if (key_value[0] == ']') {
|
|
LOG_CONTEXTUAL_ERR("empty section name");
|
|
section = SECTION_COUNT;
|
|
error_or_continue();
|
|
}
|
|
|
|
char *end = strchr(key_value, ']');
|
|
|
|
if (end == NULL) {
|
|
context.section = key_value;
|
|
LOG_CONTEXTUAL_ERR("syntax error: no closing ']'");
|
|
context.section = section_name;
|
|
section = SECTION_COUNT;
|
|
error_or_continue();
|
|
}
|
|
|
|
end[0] = '\0';
|
|
|
|
if (end[1] != '\0') {
|
|
context.section = key_value;
|
|
LOG_CONTEXTUAL_ERR("section declaration contains trailing "
|
|
"characters");
|
|
context.section = section_name;
|
|
section = SECTION_COUNT;
|
|
error_or_continue();
|
|
}
|
|
|
|
char *maybe_section_suffix;
|
|
section = str_to_section(key_value, &maybe_section_suffix);
|
|
if (section == SECTION_COUNT) {
|
|
context.section = key_value;
|
|
LOG_CONTEXTUAL_ERR("invalid section name: %s", key_value);
|
|
context.section = section_name;
|
|
error_or_continue();
|
|
}
|
|
|
|
free(section_name);
|
|
free(section_suffix);
|
|
section_name = xstrdup(key_value);
|
|
section_suffix = maybe_section_suffix != NULL ? xstrdup(maybe_section_suffix) : NULL;
|
|
context.section = section_name;
|
|
context.section_suffix = section_suffix;
|
|
|
|
/* Process next line */
|
|
continue;
|
|
}
|
|
|
|
if (section >= SECTION_COUNT) {
|
|
/* Last section name was invalid; ignore all keys in it */
|
|
continue;
|
|
}
|
|
|
|
if (!parse_key_value(key_value, NULL, &context.key, &context.value)) {
|
|
LOG_CONTEXTUAL_ERR("syntax error: key/value pair has no %s",
|
|
context.key == NULL ? "key" : "value");
|
|
error_or_continue();
|
|
}
|
|
|
|
LOG_DBG("section=%s, key='%s', value='%s', comment='%s'",
|
|
section_info[section].name, context.key, context.value, comment);
|
|
|
|
xassert(section >= 0 && section < SECTION_COUNT);
|
|
|
|
parser_fun_t section_parser = section_info[section].fun;
|
|
xassert(section_parser != NULL);
|
|
|
|
if (!section_parser(ctx))
|
|
error_or_continue();
|
|
|
|
/* For next iteration of getline() */
|
|
errno = 0;
|
|
}
|
|
|
|
if (errno != 0) {
|
|
LOG_AND_NOTIFY_ERRNO("failed to read from configuration");
|
|
if (errors_are_fatal)
|
|
ret = false;
|
|
}
|
|
|
|
done:
|
|
free(section_name);
|
|
free(section_suffix);
|
|
free(_line);
|
|
return ret;
|
|
}
|
|
|
|
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 xstrjoin(xdg_runtime, "/foot.sock");
|
|
}
|
|
|
|
return xasprintf("%s/foot-%s.sock", xdg_runtime, wayland_display);
|
|
}
|
|
|
|
static config_modifier_list_t
|
|
m(const char *text)
|
|
{
|
|
config_modifier_list_t ret = tll_init();
|
|
parse_modifiers(text, strlen(text), &ret);
|
|
return ret;
|
|
}
|
|
|
|
static void
|
|
add_default_key_bindings(struct config *conf)
|
|
{
|
|
const struct config_key_binding bindings[] = {
|
|
{BIND_ACTION_SCROLLBACK_UP_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Prior}}},
|
|
{BIND_ACTION_SCROLLBACK_UP_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_KP_Prior}}},
|
|
{BIND_ACTION_SCROLLBACK_DOWN_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Next}}},
|
|
{BIND_ACTION_SCROLLBACK_DOWN_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_KP_Next}}},
|
|
{BIND_ACTION_CLIPBOARD_COPY, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_c}}},
|
|
{BIND_ACTION_CLIPBOARD_COPY, m("none"), {{XKB_KEY_XF86Copy}}},
|
|
{BIND_ACTION_CLIPBOARD_PASTE, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_v}}},
|
|
{BIND_ACTION_CLIPBOARD_PASTE, m("none"), {{XKB_KEY_XF86Paste}}},
|
|
{BIND_ACTION_PRIMARY_PASTE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Insert}}},
|
|
{BIND_ACTION_SEARCH_START, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_r}}},
|
|
{BIND_ACTION_FONT_SIZE_UP, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_plus}}},
|
|
{BIND_ACTION_FONT_SIZE_UP, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_equal}}},
|
|
{BIND_ACTION_FONT_SIZE_UP, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_KP_Add}}},
|
|
{BIND_ACTION_FONT_SIZE_DOWN, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_minus}}},
|
|
{BIND_ACTION_FONT_SIZE_DOWN, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_KP_Subtract}}},
|
|
{BIND_ACTION_FONT_SIZE_RESET, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_0}}},
|
|
{BIND_ACTION_FONT_SIZE_RESET, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_KP_0}}},
|
|
{BIND_ACTION_SPAWN_TERMINAL, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_n}}},
|
|
{BIND_ACTION_SHOW_URLS_LAUNCH, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_o}}},
|
|
{BIND_ACTION_UNICODE_INPUT, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_u}}},
|
|
{BIND_ACTION_PROMPT_PREV, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_z}}},
|
|
{BIND_ACTION_PROMPT_NEXT, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_x}}},
|
|
};
|
|
|
|
conf->bindings.key.count = ALEN(bindings);
|
|
conf->bindings.key.arr = xmemdup(bindings, sizeof(bindings));
|
|
}
|
|
|
|
|
|
static void
|
|
add_default_search_bindings(struct config *conf)
|
|
{
|
|
const struct config_key_binding bindings[] = {
|
|
{BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Prior}}},
|
|
{BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_KP_Prior}}},
|
|
{BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Next}}},
|
|
{BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_KP_Next}}},
|
|
{BIND_ACTION_SEARCH_CANCEL, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_c}}},
|
|
{BIND_ACTION_SEARCH_CANCEL, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_g}}},
|
|
{BIND_ACTION_SEARCH_CANCEL, m("none"), {{XKB_KEY_Escape}}},
|
|
{BIND_ACTION_SEARCH_COMMIT, m("none"), {{XKB_KEY_Return}}},
|
|
{BIND_ACTION_SEARCH_COMMIT, m("none"), {{XKB_KEY_KP_Enter}}},
|
|
{BIND_ACTION_SEARCH_FIND_PREV, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_r}}},
|
|
{BIND_ACTION_SEARCH_FIND_NEXT, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_s}}},
|
|
{BIND_ACTION_SEARCH_EDIT_LEFT, m("none"), {{XKB_KEY_Left}}},
|
|
{BIND_ACTION_SEARCH_EDIT_LEFT, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_b}}},
|
|
{BIND_ACTION_SEARCH_EDIT_LEFT_WORD, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_Left}}},
|
|
{BIND_ACTION_SEARCH_EDIT_LEFT_WORD, m(XKB_MOD_NAME_ALT), {{XKB_KEY_b}}},
|
|
{BIND_ACTION_SEARCH_EDIT_RIGHT, m("none"), {{XKB_KEY_Right}}},
|
|
{BIND_ACTION_SEARCH_EDIT_RIGHT, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_f}}},
|
|
{BIND_ACTION_SEARCH_EDIT_RIGHT_WORD, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_Right}}},
|
|
{BIND_ACTION_SEARCH_EDIT_RIGHT_WORD, m(XKB_MOD_NAME_ALT), {{XKB_KEY_f}}},
|
|
{BIND_ACTION_SEARCH_EDIT_HOME, m("none"), {{XKB_KEY_Home}}},
|
|
{BIND_ACTION_SEARCH_EDIT_HOME, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_a}}},
|
|
{BIND_ACTION_SEARCH_EDIT_END, m("none"), {{XKB_KEY_End}}},
|
|
{BIND_ACTION_SEARCH_EDIT_END, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_e}}},
|
|
{BIND_ACTION_SEARCH_DELETE_PREV, m("none"), {{XKB_KEY_BackSpace}}},
|
|
{BIND_ACTION_SEARCH_DELETE_PREV_WORD, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_BackSpace}}},
|
|
{BIND_ACTION_SEARCH_DELETE_PREV_WORD, m(XKB_MOD_NAME_ALT), {{XKB_KEY_BackSpace}}},
|
|
{BIND_ACTION_SEARCH_DELETE_NEXT, m("none"), {{XKB_KEY_Delete}}},
|
|
{BIND_ACTION_SEARCH_DELETE_NEXT_WORD, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_Delete}}},
|
|
{BIND_ACTION_SEARCH_DELETE_NEXT_WORD, m(XKB_MOD_NAME_ALT), {{XKB_KEY_d}}},
|
|
{BIND_ACTION_SEARCH_DELETE_TO_START, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_u}}},
|
|
{BIND_ACTION_SEARCH_DELETE_TO_END, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_k}}},
|
|
{BIND_ACTION_SEARCH_EXTEND_CHAR, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Right}}},
|
|
{BIND_ACTION_SEARCH_EXTEND_WORD, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_Right}}},
|
|
{BIND_ACTION_SEARCH_EXTEND_WORD, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_w}}},
|
|
{BIND_ACTION_SEARCH_EXTEND_WORD_WS, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_w}}},
|
|
{BIND_ACTION_SEARCH_EXTEND_LINE_DOWN, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Down}}},
|
|
{BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Left}}},
|
|
{BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_Left}}},
|
|
{BIND_ACTION_SEARCH_EXTEND_LINE_UP, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Up}}},
|
|
{BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_v}}},
|
|
{BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_v}}},
|
|
{BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_y}}},
|
|
{BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m("none"), {{XKB_KEY_XF86Paste}}},
|
|
{BIND_ACTION_SEARCH_PRIMARY_PASTE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Insert}}},
|
|
};
|
|
|
|
conf->bindings.search.count = ALEN(bindings);
|
|
conf->bindings.search.arr = xmemdup(bindings, sizeof(bindings));
|
|
}
|
|
|
|
static void
|
|
add_default_url_bindings(struct config *conf)
|
|
{
|
|
const struct config_key_binding bindings[] = {
|
|
{BIND_ACTION_URL_CANCEL, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_c}}},
|
|
{BIND_ACTION_URL_CANCEL, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_g}}},
|
|
{BIND_ACTION_URL_CANCEL, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_d}}},
|
|
{BIND_ACTION_URL_CANCEL, m("none"), {{XKB_KEY_Escape}}},
|
|
{BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL, m("none"), {{XKB_KEY_t}}},
|
|
};
|
|
|
|
conf->bindings.url.count = ALEN(bindings);
|
|
conf->bindings.url.arr = xmemdup(bindings, sizeof(bindings));
|
|
}
|
|
|
|
static void
|
|
add_default_mouse_bindings(struct config *conf)
|
|
{
|
|
const struct config_key_binding bindings[] = {
|
|
{BIND_ACTION_SCROLLBACK_UP_MOUSE, m("none"), {.m = {BTN_WHEEL_BACK, 1}}},
|
|
{BIND_ACTION_SCROLLBACK_DOWN_MOUSE, m("none"), {.m = {BTN_WHEEL_FORWARD, 1}}},
|
|
{BIND_ACTION_PRIMARY_PASTE, m("none"), {.m = {BTN_MIDDLE, 1}}},
|
|
{BIND_ACTION_SELECT_BEGIN, m("none"), {.m = {BTN_LEFT, 1}}},
|
|
{BIND_ACTION_SELECT_BEGIN_BLOCK, m(XKB_MOD_NAME_CTRL), {.m = {BTN_LEFT, 1}}},
|
|
{BIND_ACTION_SELECT_EXTEND, m("none"), {.m = {BTN_RIGHT, 1}}},
|
|
{BIND_ACTION_SELECT_EXTEND_CHAR_WISE, m(XKB_MOD_NAME_CTRL), {.m = {BTN_RIGHT, 1}}},
|
|
{BIND_ACTION_SELECT_WORD, m("none"), {.m = {BTN_LEFT, 2}}},
|
|
{BIND_ACTION_SELECT_WORD_WS, m(XKB_MOD_NAME_CTRL), {.m = {BTN_LEFT, 2}}},
|
|
{BIND_ACTION_SELECT_QUOTE, m("none"), {.m = {BTN_LEFT, 3}}},
|
|
{BIND_ACTION_SELECT_ROW, m("none"), {.m = {BTN_LEFT, 4}}},
|
|
{BIND_ACTION_FONT_SIZE_UP, m("Control"), {.m = {BTN_WHEEL_BACK, 1}}},
|
|
{BIND_ACTION_FONT_SIZE_DOWN, m("Control"), {.m = {BTN_WHEEL_FORWARD, 1}}},
|
|
};
|
|
|
|
conf->bindings.mouse.count = ALEN(bindings);
|
|
conf->bindings.mouse.arr = xmemdup(bindings, sizeof(bindings));
|
|
}
|
|
|
|
static void NOINLINE
|
|
config_font_list_clone(struct config_font_list *dst,
|
|
const struct config_font_list *src)
|
|
{
|
|
dst->count = src->count;
|
|
dst->arr = xmalloc(dst->count * sizeof(dst->arr[0]));
|
|
|
|
for (size_t j = 0; j < dst->count; j++) {
|
|
dst->arr[j].pt_size = src->arr[j].pt_size;
|
|
dst->arr[j].px_size = src->arr[j].px_size;
|
|
dst->arr[j].pattern = xstrdup(src->arr[j].pattern);
|
|
}
|
|
}
|
|
|
|
bool
|
|
config_load(struct config *conf, const char *conf_path,
|
|
user_notifications_t *initial_user_notifications,
|
|
config_override_t *overrides, bool errors_are_fatal,
|
|
bool as_server)
|
|
{
|
|
bool ret = true;
|
|
enum fcft_capabilities fcft_caps = fcft_capabilities();
|
|
|
|
*conf = (struct config) {
|
|
.term = xstrdup(FOOT_DEFAULT_TERM),
|
|
.shell = get_shell(),
|
|
.title = xstrdup("foot"),
|
|
.app_id = (as_server ? xstrdup("footclient") : xstrdup("foot")),
|
|
.word_delimiters = xc32dup(U",│`|:\"'()[]{}<>"),
|
|
.size = {
|
|
.type = CONF_SIZE_PX,
|
|
.width = 700,
|
|
.height = 500,
|
|
},
|
|
.pad_x = 0,
|
|
.pad_y = 0,
|
|
.center_when = CENTER_MAXIMIZED_AND_FULLSCREEN,
|
|
.resize_by_cells = true,
|
|
.resize_keep_grid = true,
|
|
.resize_delay_ms = 100,
|
|
.dim = { .amount = 1.5 },
|
|
.bold_in_bright = {
|
|
.enabled = false,
|
|
.palette_based = false,
|
|
.amount = 1.3,
|
|
},
|
|
.startup_mode = STARTUP_WINDOWED,
|
|
.fonts = {{0}},
|
|
.font_size_adjustment = {.percent = 0., .pt_or_px = {.pt = 0.5, .px = 0}},
|
|
.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},
|
|
.use_custom_underline_offset = false,
|
|
.box_drawings_uses_font_glyphs = false,
|
|
.underline_thickness = {.pt = 0., .px = -1},
|
|
.strikeout_thickness = {.pt = 0., .px = -1},
|
|
.dpi_aware = false,
|
|
.gamma_correct = false,
|
|
.uppercase_regex_insert = true,
|
|
.security = {
|
|
.osc52 = OSC52_ENABLED,
|
|
},
|
|
.bell = {
|
|
.urgent = false,
|
|
.notify = false,
|
|
.flash = false,
|
|
.system_bell = true,
|
|
.command = {
|
|
.argv = {.args = NULL},
|
|
},
|
|
.command_focused = false,
|
|
},
|
|
.url = {
|
|
.label_letters = xc32dup(U"sadfjklewcmpgh"),
|
|
.osc8_underline = OSC8_UNDERLINE_URL_MODE,
|
|
},
|
|
.custom_regexes = tll_init(),
|
|
.can_shape_grapheme = fcft_caps & FCFT_CAPABILITY_GRAPHEME_SHAPING,
|
|
.scrollback = {
|
|
.lines = 1000,
|
|
.indicator = {
|
|
.position = SCROLLBACK_INDICATOR_POSITION_RELATIVE,
|
|
.format = SCROLLBACK_INDICATOR_FORMAT_TEXT,
|
|
.text = xc32dup(U""),
|
|
},
|
|
.multiplier = 3.,
|
|
},
|
|
.colors = {
|
|
.fg = default_foreground,
|
|
.bg = default_background,
|
|
.flash = 0x7f7f00,
|
|
.flash_alpha = 0x7fff,
|
|
.alpha = 0xffff,
|
|
.alpha_mode = ALPHA_MODE_DEFAULT,
|
|
.dim_blend_towards = DIM_BLEND_TOWARDS_BLACK,
|
|
.selection_fg = 0x80000000, /* Use default bg */
|
|
.selection_bg = 0x80000000, /* Use default fg */
|
|
.cursor = {
|
|
.text = 0,
|
|
.cursor = 0,
|
|
},
|
|
.use_custom = {
|
|
.jump_label = false,
|
|
.scrollback_indicator = false,
|
|
.url = false,
|
|
},
|
|
},
|
|
.initial_color_theme = COLOR_THEME1,
|
|
.cursor = {
|
|
.style = CURSOR_BLOCK,
|
|
.unfocused_style = CURSOR_UNFOCUSED_HOLLOW,
|
|
.blink = {
|
|
.enabled = false,
|
|
.rate_ms = 500,
|
|
},
|
|
.beam_thickness = {.pt = 1.5},
|
|
.underline_thickness = {.pt = 0., .px = -1},
|
|
},
|
|
.mouse = {
|
|
.hide_when_typing = false,
|
|
.alternate_scroll_mode = true,
|
|
.selection_override_modifiers = tll_init(),
|
|
},
|
|
.csd = {
|
|
.preferred = CONF_CSD_PREFER_SERVER,
|
|
.font = {0},
|
|
.hide_when_maximized = false,
|
|
.double_click_to_maximize = true,
|
|
.title_height = 26,
|
|
.border_width = 5,
|
|
.border_width_visible = 0,
|
|
.button_width = 26,
|
|
},
|
|
|
|
.render_worker_count = sysconf(_SC_NPROCESSORS_ONLN),
|
|
.server_socket_path = get_server_socket_path(),
|
|
.presentation_timings = false,
|
|
.selection_target = SELECTION_TARGET_PRIMARY,
|
|
.hold_at_exit = false,
|
|
.desktop_notifications = {
|
|
.command = {
|
|
.argv = {.args = NULL},
|
|
},
|
|
.command_action_arg = {
|
|
.argv = {.args = NULL},
|
|
},
|
|
.close = {
|
|
.argv = {.args = NULL},
|
|
},
|
|
.inhibit_when_focused = true,
|
|
},
|
|
|
|
.tweak = {
|
|
.fcft_filter = FCFT_SCALING_FILTER_LANCZOS3,
|
|
.overflowing_glyphs = true,
|
|
#if defined(FOOT_GRAPHEME_CLUSTERING) && FOOT_GRAPHEME_CLUSTERING
|
|
.grapheme_shaping = fcft_caps & FCFT_CAPABILITY_GRAPHEME_SHAPING,
|
|
#endif
|
|
.grapheme_width_method = GRAPHEME_WIDTH_DOUBLE,
|
|
.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 = RENDER_TIMER_NONE,
|
|
.damage_whole_window = false,
|
|
.box_drawing_base_thickness = 0.04,
|
|
.box_drawing_solid_shades = true,
|
|
.font_monospace_warn = true,
|
|
.sixel = true,
|
|
.surface_bit_depth = SHM_BITS_AUTO,
|
|
.min_stride_alignment = 256,
|
|
.preapply_damage = true,
|
|
},
|
|
|
|
.touch = {
|
|
.long_press_delay = 400,
|
|
},
|
|
|
|
.env_vars = tll_init(),
|
|
#if defined(UTMP_DEFAULT_HELPER_PATH)
|
|
.utmp_helper_path = ((strlen(UTMP_DEFAULT_HELPER_PATH) > 0 &&
|
|
access(UTMP_DEFAULT_HELPER_PATH, X_OK) == 0)
|
|
? xstrdup(UTMP_DEFAULT_HELPER_PATH)
|
|
: NULL),
|
|
#endif
|
|
.notifications = tll_init(),
|
|
};
|
|
|
|
memcpy(conf->colors.table, default_color_table, sizeof(default_color_table));
|
|
memcpy(conf->colors.sixel, default_sixel_colors, sizeof(default_sixel_colors));
|
|
memcpy(&conf->colors2, &conf->colors, sizeof(conf->colors));
|
|
conf->colors2.dim_blend_towards = DIM_BLEND_TOWARDS_WHITE;
|
|
|
|
parse_modifiers(XKB_MOD_NAME_SHIFT, 5, &conf->mouse.selection_override_modifiers);
|
|
|
|
tokenize_cmdline(
|
|
"notify-send --wait --app-name ${app-id} --icon ${app-id} --category ${category} --urgency ${urgency} --expire-time ${expire-time} --hint STRING:image-path:${icon} --hint BOOLEAN:suppress-sound:${muted} --hint STRING:sound-name:${sound-name} --replace-id ${replace-id} ${action-argument} --print-id -- ${title} ${body}",
|
|
&conf->desktop_notifications.command.argv.args);
|
|
tokenize_cmdline("--action ${action-name}=${action-label}", &conf->desktop_notifications.command_action_arg.argv.args);
|
|
tokenize_cmdline("xdg-open ${url}", &conf->url.launch.argv.args);
|
|
|
|
{
|
|
const char *url_regex_string =
|
|
"("
|
|
"("
|
|
"(https?://|mailto:|ftp://|file:|ssh:|ssh://|git://|tel:|magnet:|ipfs://|ipns://|gemini://|gopher://|news:)"
|
|
"|"
|
|
"www\\."
|
|
")"
|
|
"("
|
|
/* Safe + reserved + some unsafe characters parenthesis and double quotes omitted (we only allow them when balanced) */
|
|
"[0-9a-zA-Z:/?#@!$&*+,;=.~_%^\\-]+"
|
|
"|"
|
|
/* Balanced "(...)". Content is same as above, plus all _other_ characters we require to be balanced */
|
|
"\\([]\\[\"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\\)"
|
|
"|"
|
|
/* Balanced "[...]". Content is same as above, plus all _other_ characters we require to be balanced */
|
|
"\\[[\\(\\)\"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\\]"
|
|
"|"
|
|
/* Balanced '"..."'. Content is same as above, plus all _other_ characters we require to be balanced */
|
|
"\"[]\\[\\(\\)0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\""
|
|
"|"
|
|
/* Balanced "'...'". Content is same as above, plus all _other_ characters we require to be balanced */
|
|
"'[]\\[\\(\\)0-9a-zA-Z:/?#@!$&*+,;=.~_%^\\-]*'"
|
|
")+"
|
|
"("
|
|
/* Same as above, except :?!,;. are excluded */
|
|
"[0-9a-zA-Z/#@$&*+=~_%^\\-]"
|
|
"|"
|
|
/* Balanced "(...)". Content is same as above, plus all _other_ characters we require to be balanced */
|
|
"\\([]\\[\"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\\)"
|
|
"|"
|
|
/* Balanced "[...]". Content is same as above, plus all _other_ characters we require to be balanced */
|
|
"\\[[\\(\\)\"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\\]"
|
|
"|"
|
|
/* Balanced '"..."'. Content is same as above, plus all _other_ characters we require to be balanced */
|
|
"\"[]\\[\\(\\)0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\""
|
|
"|"
|
|
/* Balanced "'...'". Content is same as above, plus all _other_ characters we require to be balanced */
|
|
"'[]\\[\\(\\)0-9a-zA-Z:/?#@!$&*+,;=.~_%^\\-]*'"
|
|
")"
|
|
")";
|
|
|
|
int r = regcomp(&conf->url.preg, url_regex_string, REG_EXTENDED);
|
|
xassert(r == 0);
|
|
conf->url.regex = xstrdup(url_regex_string);
|
|
xassert(conf->url.preg.re_nsub >= 1);
|
|
}
|
|
|
|
tll_foreach(*initial_user_notifications, it) {
|
|
tll_push_back(conf->notifications, it->item);
|
|
tll_remove(*initial_user_notifications, it);
|
|
}
|
|
|
|
add_default_key_bindings(conf);
|
|
add_default_search_bindings(conf);
|
|
add_default_url_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;
|
|
} else {
|
|
conf_file.path = xstrdup(conf_path);
|
|
conf_file.fd = fd;
|
|
}
|
|
} else {
|
|
conf_file = open_config();
|
|
if (conf_file.fd < 0) {
|
|
LOG_WARN("no configuration found, using defaults");
|
|
ret = !errors_are_fatal;
|
|
}
|
|
}
|
|
|
|
if (conf_file.path && 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;
|
|
} else {
|
|
if (!parse_config_file(f, conf, conf_file.path, errors_are_fatal))
|
|
ret = !errors_are_fatal;
|
|
|
|
fclose(f);
|
|
conf_file.fd = -1;
|
|
}
|
|
}
|
|
|
|
if (!config_override_apply(conf, overrides, errors_are_fatal))
|
|
ret = !errors_are_fatal;
|
|
|
|
if (ret && conf->fonts[0].count == 0) {
|
|
struct config_font font;
|
|
if (!config_font_parse("monospace", &font)) {
|
|
LOG_ERR("failed to load font 'monospace' - no fonts installed?");
|
|
ret = false;
|
|
} else {
|
|
conf->fonts[0].count = 1;
|
|
conf->fonts[0].arr = xmalloc(sizeof(font));
|
|
conf->fonts[0].arr[0] = font;
|
|
}
|
|
}
|
|
|
|
if (ret && conf->csd.font.count == 0)
|
|
config_font_list_clone(&conf->csd.font, &conf->fonts[0]);
|
|
|
|
#if defined(_DEBUG)
|
|
for (size_t i = 0; i < conf->bindings.key.count; i++)
|
|
xassert(conf->bindings.key.arr[i].action != BIND_ACTION_NONE);
|
|
for (size_t i = 0; i < conf->bindings.search.count; i++)
|
|
xassert(conf->bindings.search.arr[i].action != BIND_ACTION_SEARCH_NONE);
|
|
for (size_t i = 0; i < conf->bindings.url.count; i++)
|
|
xassert(conf->bindings.url.arr[i].action != BIND_ACTION_URL_NONE);
|
|
#endif
|
|
|
|
free(conf_file.path);
|
|
if (conf_file.fd >= 0)
|
|
close(conf_file.fd);
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool
|
|
config_override_apply(struct config *conf, config_override_t *overrides,
|
|
bool errors_are_fatal)
|
|
{
|
|
char *section_name = NULL;
|
|
|
|
struct context context = {
|
|
.conf = conf,
|
|
.path = "override",
|
|
.lineno = 0,
|
|
.errors_are_fatal = errors_are_fatal,
|
|
};
|
|
struct context *ctx = &context;
|
|
|
|
tll_foreach(*overrides, it) {
|
|
context.lineno++;
|
|
|
|
if (!parse_key_value(it->item, §ion_name, &context.key, &context.value))
|
|
{
|
|
LOG_CONTEXTUAL_ERR("syntax error: key/value pair has no %s",
|
|
context.key == NULL ? "key" : "value");
|
|
if (errors_are_fatal)
|
|
return false;
|
|
continue;
|
|
}
|
|
|
|
if (section_name[0] == '\0') {
|
|
LOG_CONTEXTUAL_ERR("empty section name");
|
|
if (errors_are_fatal)
|
|
return false;
|
|
continue;
|
|
}
|
|
|
|
char *maybe_section_suffix = NULL;
|
|
enum section section = str_to_section(section_name, &maybe_section_suffix);
|
|
|
|
context.section = section_name;
|
|
context.section_suffix = maybe_section_suffix;
|
|
|
|
if (section == SECTION_COUNT) {
|
|
LOG_CONTEXTUAL_ERR("invalid section name: %s", section_name);
|
|
if (errors_are_fatal)
|
|
return false;
|
|
continue;
|
|
}
|
|
|
|
parser_fun_t section_parser = section_info[section].fun;
|
|
xassert(section_parser != NULL);
|
|
|
|
if (!section_parser(ctx)) {
|
|
if (errors_are_fatal)
|
|
return false;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
conf->csd.border_width = max(
|
|
min_csd_border_width, conf->csd.border_width_visible);
|
|
|
|
return
|
|
resolve_key_binding_collisions(
|
|
conf, section_info[SECTION_KEY_BINDINGS].name,
|
|
binding_action_map, &conf->bindings.key, KEY_BINDING) &&
|
|
resolve_key_binding_collisions(
|
|
conf, section_info[SECTION_SEARCH_BINDINGS].name,
|
|
search_binding_action_map, &conf->bindings.search, KEY_BINDING) &&
|
|
resolve_key_binding_collisions(
|
|
conf, section_info[SECTION_URL_BINDINGS].name,
|
|
url_binding_action_map, &conf->bindings.url, KEY_BINDING) &&
|
|
resolve_key_binding_collisions(
|
|
conf, section_info[SECTION_MOUSE_BINDINGS].name,
|
|
binding_action_map, &conf->bindings.mouse, MOUSE_BINDING);
|
|
}
|
|
|
|
static void NOINLINE
|
|
key_binding_list_clone(struct config_key_binding_list *dst,
|
|
const struct config_key_binding_list *src)
|
|
{
|
|
struct argv *last_master_argv = NULL;
|
|
uint8_t *last_master_text_data = NULL;
|
|
size_t last_master_text_len = 0;
|
|
char *last_master_regex_name = NULL;
|
|
|
|
dst->count = src->count;
|
|
dst->arr = xmalloc(src->count * sizeof(dst->arr[0]));
|
|
|
|
for (size_t i = 0; i < src->count; i++) {
|
|
const struct config_key_binding *old = &src->arr[i];
|
|
struct config_key_binding *new = &dst->arr[i];
|
|
|
|
*new = *old;
|
|
memset(&new->modifiers, 0, sizeof(new->modifiers));
|
|
tll_foreach(old->modifiers, it)
|
|
tll_push_back(new->modifiers, xstrdup(it->item));
|
|
|
|
switch (old->aux.type) {
|
|
case BINDING_AUX_NONE:
|
|
last_master_argv = NULL;
|
|
last_master_text_data = NULL;
|
|
last_master_text_len = 0;
|
|
break;
|
|
|
|
case BINDING_AUX_PIPE:
|
|
if (old->aux.master_copy) {
|
|
clone_argv(&new->aux.pipe, &old->aux.pipe);
|
|
last_master_argv = &new->aux.pipe;
|
|
} else {
|
|
xassert(last_master_argv != NULL);
|
|
new->aux.pipe = *last_master_argv;
|
|
}
|
|
last_master_text_data = NULL;
|
|
last_master_text_len = 0;
|
|
break;
|
|
|
|
case BINDING_AUX_TEXT:
|
|
if (old->aux.master_copy) {
|
|
const size_t len = old->aux.text.len;
|
|
new->aux.text.len = len;
|
|
new->aux.text.data = xmemdup(old->aux.text.data, len);
|
|
|
|
last_master_text_len = len;
|
|
last_master_text_data = new->aux.text.data;
|
|
} else {
|
|
xassert(last_master_text_data != NULL);
|
|
new->aux.text.len = last_master_text_len;
|
|
new->aux.text.data = last_master_text_data;
|
|
}
|
|
last_master_argv = NULL;
|
|
break;
|
|
|
|
case BINDING_AUX_REGEX:
|
|
if (old->aux.master_copy) {
|
|
new->aux.regex_name = xstrdup(old->aux.regex_name);
|
|
last_master_regex_name = new->aux.regex_name;
|
|
} else {
|
|
xassert(last_master_regex_name != NULL);
|
|
new->aux.regex_name = last_master_regex_name;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
struct config *
|
|
config_clone(const struct config *old)
|
|
{
|
|
struct config *conf = xmalloc(sizeof(*conf));
|
|
*conf = *old;
|
|
|
|
conf->term = xstrdup(old->term);
|
|
conf->shell = xstrdup(old->shell);
|
|
conf->title = xstrdup(old->title);
|
|
conf->app_id = xstrdup(old->app_id);
|
|
conf->word_delimiters = xc32dup(old->word_delimiters);
|
|
conf->scrollback.indicator.text = xc32dup(old->scrollback.indicator.text);
|
|
conf->server_socket_path = xstrdup(old->server_socket_path);
|
|
spawn_template_clone(&conf->bell.command, &old->bell.command);
|
|
spawn_template_clone(&conf->desktop_notifications.command,
|
|
&old->desktop_notifications.command);
|
|
spawn_template_clone(&conf->desktop_notifications.command_action_arg,
|
|
&old->desktop_notifications.command_action_arg);
|
|
spawn_template_clone(&conf->desktop_notifications.close,
|
|
&old->desktop_notifications.close);
|
|
|
|
for (size_t i = 0; i < ALEN(conf->fonts); i++)
|
|
config_font_list_clone(&conf->fonts[i], &old->fonts[i]);
|
|
config_font_list_clone(&conf->csd.font, &old->csd.font);
|
|
|
|
conf->url.label_letters = xc32dup(old->url.label_letters);
|
|
spawn_template_clone(&conf->url.launch, &old->url.launch);
|
|
conf->url.regex = xstrdup(old->url.regex);
|
|
regcomp(&conf->url.preg, conf->url.regex, REG_EXTENDED);
|
|
|
|
memset(&conf->custom_regexes, 0, sizeof(conf->custom_regexes));
|
|
tll_foreach(old->custom_regexes, it) {
|
|
const struct custom_regex *old_regex = &it->item;
|
|
|
|
tll_push_back(conf->custom_regexes,
|
|
((struct custom_regex){.name = xstrdup(old_regex->name),
|
|
.regex = xstrdup(old_regex->regex)}));
|
|
|
|
|
|
struct custom_regex *new_regex = &tll_back(conf->custom_regexes);
|
|
regcomp(&new_regex->preg, new_regex->regex, REG_EXTENDED);
|
|
spawn_template_clone(&new_regex->launch, &old_regex->launch);
|
|
}
|
|
|
|
key_binding_list_clone(&conf->bindings.key, &old->bindings.key);
|
|
key_binding_list_clone(&conf->bindings.search, &old->bindings.search);
|
|
key_binding_list_clone(&conf->bindings.url, &old->bindings.url);
|
|
key_binding_list_clone(&conf->bindings.mouse, &old->bindings.mouse);
|
|
|
|
conf->env_vars.length = 0;
|
|
conf->env_vars.head = conf->env_vars.tail = NULL;
|
|
|
|
memset(&conf->mouse.selection_override_modifiers, 0, sizeof(conf->mouse.selection_override_modifiers));
|
|
tll_foreach(old->mouse.selection_override_modifiers, it)
|
|
tll_push_back(conf->mouse.selection_override_modifiers, xstrdup(it->item));
|
|
|
|
tll_foreach(old->env_vars, it) {
|
|
struct env_var copy = {
|
|
.name = xstrdup(it->item.name),
|
|
.value = xstrdup(it->item.value),
|
|
};
|
|
tll_push_back(conf->env_vars, copy);
|
|
}
|
|
|
|
conf->utmp_helper_path =
|
|
old->utmp_helper_path != NULL ? xstrdup(old->utmp_helper_path) : NULL;
|
|
|
|
conf->notifications.length = 0;
|
|
conf->notifications.head = conf->notifications.tail = 0;
|
|
tll_foreach(old->notifications, it) {
|
|
char *text = xstrdup(it->item.text);
|
|
user_notification_add(&conf->notifications, it->item.kind, text);
|
|
}
|
|
|
|
return conf;
|
|
}
|
|
|
|
UNITTEST
|
|
{
|
|
struct config original;
|
|
user_notifications_t nots = tll_init();
|
|
config_override_t overrides = tll_init();
|
|
|
|
fcft_init(FCFT_LOG_COLORIZE_NEVER, false, FCFT_LOG_CLASS_NONE);
|
|
|
|
bool ret = config_load(&original, "/dev/null", ¬s, &overrides, false, false);
|
|
xassert(ret);
|
|
|
|
//struct config *clone = config_clone(&original);
|
|
//xassert(clone != NULL);
|
|
//xassert(clone != &original);
|
|
|
|
config_free(&original);
|
|
//config_free(clone);
|
|
//free(clone);
|
|
|
|
fcft_fini();
|
|
|
|
tll_free(overrides);
|
|
tll_free(nots);
|
|
}
|
|
|
|
void
|
|
config_free(struct config *conf)
|
|
{
|
|
free(conf->term);
|
|
free(conf->shell);
|
|
free(conf->title);
|
|
free(conf->app_id);
|
|
free(conf->word_delimiters);
|
|
spawn_template_free(&conf->bell.command);
|
|
free(conf->scrollback.indicator.text);
|
|
spawn_template_free(&conf->desktop_notifications.command);
|
|
spawn_template_free(&conf->desktop_notifications.command_action_arg);
|
|
spawn_template_free(&conf->desktop_notifications.close);
|
|
for (size_t i = 0; i < ALEN(conf->fonts); i++)
|
|
config_font_list_destroy(&conf->fonts[i]);
|
|
free(conf->server_socket_path);
|
|
|
|
config_font_list_destroy(&conf->csd.font);
|
|
|
|
free(conf->url.label_letters);
|
|
spawn_template_free(&conf->url.launch);
|
|
regfree(&conf->url.preg);
|
|
free(conf->url.regex);
|
|
|
|
tll_foreach(conf->custom_regexes, it) {
|
|
struct custom_regex *regex = &it->item;
|
|
free(regex->name);
|
|
free(regex->regex);
|
|
regfree(®ex->preg);
|
|
spawn_template_free(®ex->launch);
|
|
tll_remove(conf->custom_regexes, it);
|
|
}
|
|
|
|
free_key_binding_list(&conf->bindings.key);
|
|
free_key_binding_list(&conf->bindings.search);
|
|
free_key_binding_list(&conf->bindings.url);
|
|
free_key_binding_list(&conf->bindings.mouse);
|
|
tll_free_and_free(conf->mouse.selection_override_modifiers, free);
|
|
|
|
tll_foreach(conf->env_vars, it) {
|
|
free(it->item.name);
|
|
free(it->item.value);
|
|
tll_remove(conf->env_vars, it);
|
|
}
|
|
|
|
free(conf->utmp_helper_path);
|
|
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;
|
|
|
|
/*
|
|
* First look for user specified {pixel}size option
|
|
* e.g. "font-name:size=12"
|
|
*/
|
|
|
|
double pt_size = -1.0;
|
|
FcResult have_pt_size = FcPatternGetDouble(pat, FC_SIZE, 0, &pt_size);
|
|
|
|
int px_size = -1;
|
|
FcResult have_px_size = FcPatternGetInteger(pat, FC_PIXEL_SIZE, 0, &px_size);
|
|
|
|
if (have_pt_size != FcResultMatch && have_px_size != FcResultMatch) {
|
|
/*
|
|
* Apply fontconfig config. Can't do that until we've first
|
|
* checked for a user provided size, since we may end up with
|
|
* both "size" and "pixelsize" being set, and we don't know
|
|
* which one takes priority.
|
|
*/
|
|
FcConfig *fc_conf = FcConfigCreate();
|
|
FcPattern *pat_copy = FcPatternDuplicate(pat);
|
|
if (pat_copy == NULL ||
|
|
!FcConfigSubstitute(fc_conf, pat_copy, FcMatchPattern))
|
|
{
|
|
LOG_WARN("%s: failed to do config substitution", pattern);
|
|
} else {
|
|
have_pt_size = FcPatternGetDouble(pat_copy, FC_SIZE, 0, &pt_size);
|
|
have_px_size = FcPatternGetInteger(pat_copy, FC_PIXEL_SIZE, 0, &px_size);
|
|
}
|
|
|
|
FcPatternDestroy(pat_copy);
|
|
FcConfigDestroy(fc_conf);
|
|
|
|
if (have_pt_size != FcResultMatch && have_px_size != FcResultMatch)
|
|
pt_size = 8.0;
|
|
}
|
|
|
|
FcPatternRemove(pat, FC_SIZE, 0);
|
|
FcPatternRemove(pat, FC_PIXEL_SIZE, 0);
|
|
|
|
char *stripped_pattern = (char *)FcNameUnparse(pat);
|
|
FcPatternDestroy(pat);
|
|
|
|
LOG_DBG("%s: pt-size=%.2f, px-size=%d", stripped_pattern, pt_size, px_size);
|
|
|
|
if (stripped_pattern == NULL) {
|
|
LOG_ERR("failed to convert font pattern to string");
|
|
return false;
|
|
}
|
|
|
|
*font = (struct config_font){
|
|
.pattern = stripped_pattern,
|
|
.pt_size = pt_size,
|
|
.px_size = px_size
|
|
};
|
|
return true;
|
|
}
|
|
|
|
void
|
|
config_font_list_destroy(struct config_font_list *font_list)
|
|
{
|
|
for (size_t i = 0; i < font_list->count; i++)
|
|
free(font_list->arr[i].pattern);
|
|
free(font_list->arr);
|
|
font_list->count = 0;
|
|
font_list->arr = NULL;
|
|
}
|
|
|
|
|
|
bool
|
|
check_if_font_is_monospaced(const char *pattern,
|
|
user_notifications_t *notifications)
|
|
{
|
|
struct fcft_font *f = fcft_from_name(
|
|
1, (const char *[]){pattern}, ":size=8");
|
|
|
|
if (f == NULL)
|
|
return true;
|
|
|
|
static const char32_t chars[] = {U'a', U'i', U'l', U'M', U'W'};
|
|
|
|
bool is_monospaced = true;
|
|
int last_width = -1;
|
|
|
|
for (size_t i = 0; i < sizeof(chars) / sizeof(chars[0]); i++) {
|
|
const struct fcft_glyph *g = fcft_rasterize_char_utf32(
|
|
f, chars[i], FCFT_SUBPIXEL_NONE);
|
|
|
|
if (g == NULL)
|
|
continue;
|
|
|
|
if (last_width >= 0 && g->advance.x != last_width) {
|
|
const char *font_name = f->name != NULL
|
|
? f->name
|
|
: pattern;
|
|
|
|
LOG_WARN("%s: font does not appear to be monospace; "
|
|
"check your config, or disable this warning by "
|
|
"setting [tweak].font-monospace-warn=no",
|
|
font_name);
|
|
|
|
static const char fmt[] =
|
|
"%s: font does not appear to be monospace; "
|
|
"check your config, or disable this warning by "
|
|
"setting \033[1m[tweak].font-monospace-warn=no\033[22m";
|
|
|
|
user_notification_add_fmt(
|
|
notifications, USER_NOTIFICATION_WARNING, fmt, font_name);
|
|
|
|
is_monospaced = false;
|
|
break;
|
|
}
|
|
|
|
last_width = g->advance.x;
|
|
}
|
|
|
|
fcft_destroy(f);
|
|
return is_monospaced;
|
|
}
|
|
|
|
#if 0
|
|
xkb_mod_mask_t
|
|
conf_modifiers_to_mask(const struct seat *seat,
|
|
const struct config_key_modifiers *modifiers)
|
|
{
|
|
xkb_mod_mask_t mods = 0;
|
|
if (seat->kbd.mod_shift != XKB_MOD_INVALID)
|
|
mods |= modifiers->shift << seat->kbd.mod_shift;
|
|
if (seat->kbd.mod_ctrl != XKB_MOD_INVALID)
|
|
mods |= modifiers->ctrl << seat->kbd.mod_ctrl;
|
|
if (seat->kbd.mod_alt != XKB_MOD_INVALID)
|
|
mods |= modifiers->alt << seat->kbd.mod_alt;
|
|
if (seat->kbd.mod_super != XKB_MOD_INVALID)
|
|
mods |= modifiers->super << seat->kbd.mod_super;
|
|
return mods;
|
|
}
|
|
#endif
|