diff --git a/config.c b/config.c index aaae7f2..7ea297e 100644 --- a/config.c +++ b/config.c @@ -1,29 +1,29 @@ #include "config.h" -#include -#include -#include -#include #include -#include #include -#include #include +#include +#include +#include +#include +#include +#include -#include #include +#include +#include #include #include -#include #define LOG_MODULE "config" #define LOG_ENABLE_DBG 0 -#include "log.h" #include "char32.h" #include "debug.h" #include "input.h" #include "key-binding.h" +#include "log.h" #include "macros.h" #include "tokenize.h" #include "util.h" @@ -35,75 +35,41 @@ 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 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) +#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, + 0x242424, 0xf62b5a, 0x47b413, 0xe3c401, 0x24acd4, 0xf2affd, 0x13c299, 0xe6e6e6, // Bright - 0x616161, - 0xff4d51, - 0x35d450, - 0xe9e836, - 0x5dc5f8, - 0xfeabf2, - 0x24dfc4, + 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), + 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 -}; + 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 */ +/* 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, + 0xff000000, 0xff3333cc, 0xffcc2121, 0xff33cc33, 0xffcc33cc, 0xff33cccc, + 0xffcccc33, 0xff878787, 0xff424242, 0xff545499, 0xff994242, 0xff549954, + 0xff995499, 0xff549999, 0xff999954, 0xffcccccc, }; static const char *const binding_action_map[] = { @@ -164,6 +130,11 @@ static const char *const binding_action_map[] = { [BIND_ACTION_TAB_9] = "tab-9", [BIND_ACTION_TAB_OVERVIEW] = "tab-overview", + /* Session actions */ + [BIND_ACTION_SESSION_SAVE] = "session-save", + [BIND_ACTION_SESSION_LOAD] = "session-load", + [BIND_ACTION_SESSION_SAVE_SECURE] = "session-save-secure", + /* Mouse-specific actions */ [BIND_ACTION_SCROLLBACK_UP_MOUSE] = "scrollback-up-mouse", [BIND_ACTION_SCROLLBACK_DOWN_MOUSE] = "scrollback-down-mouse", @@ -183,7 +154,8 @@ static const char *const search_binding_action_map[] = { [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_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", @@ -208,8 +180,10 @@ static const char *const search_binding_action_map[] = { [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_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", @@ -236,1839 +210,1708 @@ 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; + struct config *conf; + const char *section; + const char *section_suffix; + const char *key; + const char *value; - const char *path; - unsigned lineno; + const char *path; + unsigned lineno; - bool errors_are_fatal; + 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 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]; + 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; - } + 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); + 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); + 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); + 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; + 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_dot) + ctx->key = ""; - if (!print_colon) - ctx->value = ""; + if (!print_colon) + ctx->value = ""; - if (!print_section_suffix) - ctx->section_suffix = ""; + 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); + 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); + 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); + 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); + 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; + bool print_dot = ctx->key != NULL; + bool print_colon = ctx->value != NULL; - if (!print_dot) - ctx->key = ""; + if (!print_dot) + ctx->key = ""; - if (!print_colon) - ctx->value = ""; + 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); + 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); + free(formatted_msg); } -#define LOG_CONTEXTUAL_ERR(...) \ - log_contextual(ctx, LOG_CLASS_ERROR, __FILE__, __LINE__, __VA_ARGS__) +#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_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_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_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_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__) +#define LOG_AND_NOTIFY_ERRNO(...) \ + log_and_notify_errno(conf, __FILE__, __LINE__, __VA_ARGS__) -static char * -get_shell(void) -{ - const char *shell = getenv("SHELL"); +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; - } + 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); + 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 */ + 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}; +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; + 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"); + /* 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 (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; - } + 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"); + 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; + 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"); + 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; - } + 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; + 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 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"}; +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(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; - } + 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"); + 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; -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); + unsigned long v = strtoul(s, &end, base); + if (!(errno == 0 && *end == '\0')) return false; + + *res = v; + return true; } -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 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 -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; +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; +} - /* 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, " "); +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; +} - if (first_as_str == NULL || second_as_str == NULL) { - LOG_CONTEXTUAL_ERR("invalid double color value"); - goto out; +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"); } - uint32_t a, b; + return false; + } - ctx->value = first_as_str; - if (!value_to_color(ctx, &a, allow_alpha)) - goto out; + if (allow_alpha && component_count == 3) { + /* If user left out the alpha component, assume non-transparency */ + color |= 0xff000000; + } - ctx->value = second_as_str; - if (!value_to_color(ctx, &b, allow_alpha)) - goto out; + *result = color; + return true; +} - *first = a; - *second = b; - ret = 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; + 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; +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; + 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; + 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; + 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; +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++; + 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; + 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; + struct config_font font_data; + if (!config_font_parse(font, &font_data)) { + ctx->value = font; + LOG_CONTEXTUAL_ERR("invalid font specification"); + goto err; } - free(copy); - return (struct config_font_list){.arr = fonts, .count = count}; + 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}; + 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 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; - } +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++; + 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; + 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_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 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); +static bool NOINLINE value_to_spawn_template( + struct context *ctx, struct config_spawn_template *template) { + spawn_template_free(template); - char **argv = NULL; + 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; + if (ctx->value[0] == '"' && ctx->value[1] == '"' && ctx->value[2] == '\0') { + template->argv.args = NULL; 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, "toplevel-tag")) - return value_to_str(ctx, &conf->toplevel_tag); - - 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, left, top, right, bottom; - char mode[64] = {0}; - int ret = sscanf(value, "%ux%ux%ux%u %63s", &left, &top, &right, &bottom, mode); - enum center_when center = CENTER_NEVER; - - if (ret == 5) { - 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; - } else if (ret < 4) { - ret = sscanf(value, "%ux%u %63s", &x, &y, mode); - if (ret >= 2) { - left = right = x; - top = bottom = y; - 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 > 5) || center == CENTER_INVALID) { - LOG_CONTEXTUAL_ERR( - "invalid padding (must be in the form RIGHTxTOPxLEFTxBOTTOM or XxY " - "[center|" - "center-when-fullscreen|" - "center-when-maximized-and-fullscreen])"); - return false; - } - - conf->pad_left = left; - conf->pad_top = top; - conf->pad_right = right; - conf->pad_bottom = bottom; - conf->center_when = (ret == 4 || 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"); - - if (!value_to_enum(ctx, (const char*[]){ - "dark", "light", "1", "2", NULL}, - (int *)&conf->initial_color_theme)) - return false; - - if (streq(ctx->value, "1")) { - LOG_WARN("%s:%d: [main].initial-color-theme=1 deprecated, " - "use [main].initial-color-theme=dark instead", - ctx->path, ctx->lineno); - - user_notification_add( - &ctx->conf->notifications, - USER_NOTIFICATION_DEPRECATED, - xstrdup("[main].initial-color-theme=1: " - "use [main].initial-color-theme=dark instead")); - - conf->initial_color_theme = COLOR_THEME_DARK; - } - - else if (streq(ctx->value, "2")) { - LOG_WARN("%s:%d: [main].initial-color-theme=2 deprecated, " - "use [main].initial-color-theme=light instead", - ctx->path, ctx->lineno); - - user_notification_add( - &ctx->conf->notifications, - USER_NOTIFICATION_DEPRECATED, - xstrdup("[main].initial-color-theme=2: " - "use [main].initial-color-theme=light instead")); - - conf->initial_color_theme = COLOR_THEME_LIGHT; - } - - return true; - } - - 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, "style")) { - _Static_assert(sizeof(conf->url.style) == sizeof(int), - "enum is not 32-bit"); - return value_to_enum( - ctx, (const char *[]){"none", "single", "double", "curly", "dotted", "dashed", NULL}, - (int *)&conf->url.style); - } - - 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 if (streq(key, "blur")) - return value_to_bool(ctx, &theme->blur); - - 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_dark(struct context *ctx) -{ - return parse_color_theme(ctx, &ctx->conf->colors_dark); -} - -static bool -parse_section_colors_light(struct context *ctx) -{ - return parse_color_theme(ctx, &ctx->conf->colors_light); -} - -static bool -parse_section_colors(struct context *ctx) -{ - LOG_WARN("%s:%d: [colors]: deprecated; use [colors-dark] instead", - ctx->path, ctx->lineno); - - user_notification_add( - &ctx->conf->notifications, - USER_NOTIFICATION_DEPRECATED, - xstrdup("[colors]: use [colors-dark] instead")); - - return parse_color_theme(ctx, &ctx->conf->colors_dark); -} - -static bool -parse_section_colors2(struct context *ctx) -{ - LOG_WARN("%s:%d: [colors2]: deprecated; use [colors-light] instead", - ctx->path, ctx->lineno); - - user_notification_add( - &ctx->conf->notifications, - USER_NOTIFICATION_DEPRECATED, - xstrdup("[colors2]: use [colors-light] instead")); - - return parse_color_theme(ctx, &ctx->conf->colors_light); -} - -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, "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 bool -parse_section_tabs(struct context *ctx) -{ - struct config *conf = ctx->conf; - const char *key = ctx->key; - - if (streq(key, "enabled")) - return value_to_bool(ctx, &conf->tabs.enabled); - - else if (streq(key, "position")) { - _Static_assert(sizeof(conf->tabs.position) == sizeof(int), - "enum is not 32-bit"); - return value_to_enum( - ctx, - (const char *[]){"top", "bottom", NULL}, - (int *)&conf->tabs.position); - } - - else if (streq(key, "style")) { - _Static_assert(sizeof(conf->tabs.style) == sizeof(int), - "enum is not 32-bit"); - return value_to_enum( - ctx, - (const char *[]){"rounded", "square", "gradient", NULL}, - (int *)&conf->tabs.style); - } - - else if (streq(key, "layout")) { - _Static_assert(sizeof(conf->tabs.layout) == sizeof(int), - "enum is not 32-bit"); - return value_to_enum( - ctx, - (const char *[]){"span", "floating", NULL}, - (int *)&conf->tabs.layout); - } - - else if (streq(key, "height")) - return value_to_uint16(ctx, 10, &conf->tabs.height); - - else if (streq(key, "tab-width")) - return value_to_uint16(ctx, 10, &conf->tabs.tab_width); - - else if (streq(key, "tab-padding")) - return value_to_uint16(ctx, 10, &conf->tabs.tab_padding); - - else if (streq(key, "label-padding")) - return value_to_uint16(ctx, 10, &conf->tabs.label_padding); - - else if (streq(key, "margin")) - return value_to_uint16(ctx, 10, &conf->tabs.margin); - - else if (streq(key, "corner-radius")) - return value_to_uint16(ctx, 10, &conf->tabs.corner_radius); - - else if (streq(key, "background")) - return value_to_color(ctx, &conf->tabs.colors.bg, false); - - else if (streq(key, "foreground")) - return value_to_color(ctx, &conf->tabs.colors.fg, false); - - else if (streq(key, "active-background")) - return value_to_color(ctx, &conf->tabs.colors.active_bg, false); - - else if (streq(key, "active-foreground")) - return value_to_color(ctx, &conf->tabs.colors.active_fg, false); - - else if (streq(key, "unread-color")) - return value_to_color(ctx, &conf->tabs.colors.unread_fg, false); - - else if (streq(key, "unread-indicator")) { - free(conf->tabs.unread_indicator); - conf->tabs.unread_indicator = xstrdup(ctx->value); - return true; - } - - else if (streq(key, "inherit-cwd")) - return value_to_bool(ctx, &conf->tabs.inherit_cwd); - - else { - LOG_CONTEXTUAL_ERR("not a valid tabs option: %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); + 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, "toplevel-tag")) + return value_to_str(ctx, &conf->toplevel_tag); + + 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, left, top, right, bottom; + char mode[64] = {0}; + int ret = + sscanf(value, "%ux%ux%ux%u %63s", &left, &top, &right, &bottom, mode); + enum center_when center = CENTER_NEVER; + + if (ret == 5) { + 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; + } else if (ret < 4) { + ret = sscanf(value, "%ux%u %63s", &x, &y, mode); + if (ret >= 2) { + left = right = x; + top = bottom = y; + 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 > 5) || center == CENTER_INVALID) { + LOG_CONTEXTUAL_ERR( + "invalid padding (must be in the form RIGHTxTOPxLEFTxBOTTOM or XxY " + "[center|" + "center-when-fullscreen|" + "center-when-maximized-and-fullscreen])"); + return false; + } + + conf->pad_left = left; + conf->pad_top = top; + conf->pad_right = right; + conf->pad_bottom = bottom; + conf->center_when = (ret == 4 || 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"); + + if (!value_to_enum(ctx, (const char *[]){"dark", "light", "1", "2", NULL}, + (int *)&conf->initial_color_theme)) + return false; + + if (streq(ctx->value, "1")) { + LOG_WARN("%s:%d: [main].initial-color-theme=1 deprecated, " + "use [main].initial-color-theme=dark instead", + ctx->path, ctx->lineno); + + user_notification_add( + &ctx->conf->notifications, USER_NOTIFICATION_DEPRECATED, + xstrdup("[main].initial-color-theme=1: " + "use [main].initial-color-theme=dark instead")); + + conf->initial_color_theme = COLOR_THEME_DARK; + } + + else if (streq(ctx->value, "2")) { + LOG_WARN("%s:%d: [main].initial-color-theme=2 deprecated, " + "use [main].initial-color-theme=light instead", + ctx->path, ctx->lineno); + + user_notification_add( + &ctx->conf->notifications, USER_NOTIFICATION_DEPRECATED, + xstrdup("[main].initial-color-theme=2: " + "use [main].initial-color-theme=light instead")); + + conf->initial_color_theme = COLOR_THEME_LIGHT; + } + + return true; + } + + 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, "style")) { + _Static_assert(sizeof(conf->url.style) == sizeof(int), + "enum is not 32-bit"); + return value_to_enum(ctx, + (const char *[]){"none", "single", "double", "curly", + "dotted", "dashed", NULL}, + (int *)&conf->url.style); + } + + 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 if (streq(key, "blur")) + return value_to_bool(ctx, &theme->blur); + + 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_dark(struct context *ctx) { + return parse_color_theme(ctx, &ctx->conf->colors_dark); +} + +static bool parse_section_colors_light(struct context *ctx) { + return parse_color_theme(ctx, &ctx->conf->colors_light); +} + +static bool parse_section_colors(struct context *ctx) { + LOG_WARN("%s:%d: [colors]: deprecated; use [colors-dark] instead", ctx->path, + ctx->lineno); + + user_notification_add(&ctx->conf->notifications, USER_NOTIFICATION_DEPRECATED, + xstrdup("[colors]: use [colors-dark] instead")); + + return parse_color_theme(ctx, &ctx->conf->colors_dark); +} + +static bool parse_section_colors2(struct context *ctx) { + LOG_WARN("%s:%d: [colors2]: deprecated; use [colors-light] instead", + ctx->path, ctx->lineno); + + user_notification_add(&ctx->conf->notifications, USER_NOTIFICATION_DEPRECATED, + xstrdup("[colors2]: use [colors-light] instead")); + + return parse_color_theme(ctx, &ctx->conf->colors_light); +} + +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, "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 bool parse_section_tabs(struct context *ctx) { + struct config *conf = ctx->conf; + const char *key = ctx->key; + + if (streq(key, "enabled")) + return value_to_bool(ctx, &conf->tabs.enabled); + + else if (streq(key, "position")) { + _Static_assert(sizeof(conf->tabs.position) == sizeof(int), + "enum is not 32-bit"); + return value_to_enum(ctx, (const char *[]){"top", "bottom", NULL}, + (int *)&conf->tabs.position); + } + + else if (streq(key, "style")) { + _Static_assert(sizeof(conf->tabs.style) == sizeof(int), + "enum is not 32-bit"); + return value_to_enum( + ctx, (const char *[]){"rounded", "square", "gradient", NULL}, + (int *)&conf->tabs.style); + } + + else if (streq(key, "layout")) { + _Static_assert(sizeof(conf->tabs.layout) == sizeof(int), + "enum is not 32-bit"); + return value_to_enum(ctx, (const char *[]){"span", "floating", NULL}, + (int *)&conf->tabs.layout); + } + + else if (streq(key, "height")) + return value_to_uint16(ctx, 10, &conf->tabs.height); + + else if (streq(key, "tab-width")) + return value_to_uint16(ctx, 10, &conf->tabs.tab_width); + + else if (streq(key, "tab-padding")) + return value_to_uint16(ctx, 10, &conf->tabs.tab_padding); + + else if (streq(key, "label-padding")) + return value_to_uint16(ctx, 10, &conf->tabs.label_padding); + + else if (streq(key, "margin")) + return value_to_uint16(ctx, 10, &conf->tabs.margin); + + else if (streq(key, "corner-radius")) + return value_to_uint16(ctx, 10, &conf->tabs.corner_radius); + + else if (streq(key, "background")) + return value_to_color(ctx, &conf->tabs.colors.bg, false); + + else if (streq(key, "foreground")) + return value_to_color(ctx, &conf->tabs.colors.fg, false); + + else if (streq(key, "active-background")) + return value_to_color(ctx, &conf->tabs.colors.active_bg, false); + + else if (streq(key, "active-foreground")) + return value_to_color(ctx, &conf->tabs.colors.active_fg, false); + + else if (streq(key, "unread-color")) + return value_to_color(ctx, &conf->tabs.colors.unread_fg, false); + + else if (streq(key, "overview-active-border")) + return value_to_color(ctx, &conf->tabs.colors.overview_active_border, + false); + + else if (streq(key, "overview-select-border")) + return value_to_color(ctx, &conf->tabs.colors.overview_select_border, + false); + + else if (streq(key, "unread-indicator")) { + free(conf->tabs.unread_indicator); + conf->tabs.unread_indicator = xstrdup(ctx->value); + return true; + } + + else if (streq(key, "inherit-cwd")) + return value_to_bool(ctx, &conf->tabs.inherit_cwd); + + else { + LOG_CONTEXTUAL_ERR("not a valid tabs option: %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; + 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]; + for (size_t i = 0; i < bindings->count; i++) { + struct config_key_binding *binding = &bindings->arr[i]; - if (binding->action != action) - continue; + if (binding->action != action) + continue; - if (binding_aux_equal(&binding->aux, aux)) { - if (remove_count++ == 0) - remove_first_idx = i; + 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); - } + xassert(remove_first_idx + remove_count - 1 == i); + free_key_binding(binding); } + } - if (remove_count == 0) - return; + if (remove_count == 0) + return; - size_t move_count = bindings->count - (remove_first_idx + remove_count); + 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; + 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; + const char *name; + int code; } button_map[] = { /* System defined */ {"BTN_LEFT", BTN_LEFT}, @@ -2087,236 +1930,214 @@ static const struct { {"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 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; - } +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; + 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; - } +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++; - } + /* 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]; + struct config_key_binding new_combos[combo_count]; - char *copy = xstrdup(ctx->value); - size_t idx = 0; + 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; + 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; + 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; - } - } + 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 '+' */ } - if (idx == 0) { - LOG_CONTEXTUAL_ERR( - "empty binding not allowed (set to 'none' to unmap)"); - free(copy); - return false; + 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; } + } + } - 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]); + 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)) +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; - - 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++; + break; } - 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; - */ + 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(); +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(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("foo")); + xassert(!modifiers_equal(&mods1, &mods2)); - tll_push_back(mods2, xstrdup("zoo")); - 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)); + 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); + 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 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); +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++] = '+'; - } + 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--; + if (strip_last_plus) + idx--; - ret[idx] = '\0'; - return ret; + ret[idx] = '\0'; + return ret; } /* @@ -2333,2018 +2154,2058 @@ modifiers_to_str(const config_modifier_list_t *mods, bool strip_last_plus) * 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] ")). + * - 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. + * - 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; +static ssize_t NOINLINE pipe_argv_from_value(struct context *ctx, + struct argv *argv) { + argv->args = NULL; - if (ctx->value[0] != '[') - return 0; + 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; - } + 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++; - } + 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 remove_len; + 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; +static ssize_t NOINLINE regex_name_from_value(struct context *ctx, + char **regex_name) { + *regex_name = NULL; - if (ctx->value[0] != '[') - return 0; + if (ctx->value[0] != '[') + return 0; - const char *regex_end = strrchr(ctx->value, ']'); - if (regex_end == NULL) { - LOG_CONTEXTUAL_ERR("unclosed '['"); - return -1; - } + 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); + 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++; - } + 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; + return remove_len; } - static bool NOINLINE -parse_key_binding_section(struct context *ctx, - int action_count, +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; + 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; + if (!streq(ctx->key, action_map[action])) + continue; - struct binding_aux aux = {.type = BINDING_AUX_NONE, .master_copy = true}; + 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 (action_map == binding_action_map && - action >= BIND_ACTION_THEME_SWITCH_1 && - action <= BIND_ACTION_THEME_SWITCH_2) - { - const char *use_instead = - action_map[action == BIND_ACTION_THEME_SWITCH_1 - ? BIND_ACTION_THEME_SWITCH_DARK - : BIND_ACTION_THEME_SWITCH_LIGHT]; - - const char *notif = action == BIND_ACTION_THEME_SWITCH_1 - ? "[key-bindings].color-theme-switch-1: use [key-bindings].color-theme-switch-dark instead" - : "[key-bindings].color-theme-switch-2: use [key-bindings].color-theme-switch-light instead"; - - LOG_WARN("%s:%d: [key-bindings].%s: deprecated, use %s instead", - ctx->path, ctx->lineno, - action_map[action], use_instead); - - user_notification_add( - &ctx->conf->notifications, - USER_NOTIFICATION_DEPRECATED, - xstrdup(notif)); - } - - 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) + /* 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 = 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() */ + 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 (action_map == binding_action_map && + action >= BIND_ACTION_THEME_SWITCH_1 && + action <= BIND_ACTION_THEME_SWITCH_2) { + const char *use_instead = + action_map[action == BIND_ACTION_THEME_SWITCH_1 + ? BIND_ACTION_THEME_SWITCH_DARK + : BIND_ACTION_THEME_SWITCH_LIGHT]; + + const char *notif = + action == BIND_ACTION_THEME_SWITCH_1 + ? "[key-bindings].color-theme-switch-1: use " + "[key-bindings].color-theme-switch-dark instead" + : "[key-bindings].color-theme-switch-2: use " + "[key-bindings].color-theme-switch-light instead"; + + LOG_WARN("%s:%d: [key-bindings].%s: deprecated, use %s instead", + ctx->path, ctx->lineno, action_map[action], use_instead); + + user_notification_add(&ctx->conf->notifications, + USER_NOTIFICATION_DEPRECATED, xstrdup(notif)); + } + + 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); + 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_environment(struct context *ctx) -{ - struct config *conf = ctx->conf; - const char *key = ctx->key; +static bool parse_section_tweak(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); - } + if (streq(key, "scaling-filter")) { + static const char *filters[] = { + [FCFT_SCALING_FILTER_NONE] = "none", + [FCFT_SCALING_FILTER_NEAREST] = "nearest", + [FCFT_SCALING_FILTER_BILINEAR] = "bilinear", - /* - * No pre-existing variable - allocate a new one - */ + [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, + }; - char *value = NULL; - if (!value_to_str(ctx, &value)) - return false; + _Static_assert(sizeof(conf->tweak.fcft_filter) == sizeof(int), + "enum is not 32-bit"); - tll_push_back(conf->env_vars, ((struct env_var){xstrdup(key), value})); - return true; -} + return value_to_enum(ctx, filters, (int *)&conf->tweak.fcft_filter); + } -static bool -parse_section_tweak(struct context *ctx) -{ - struct config *conf = ctx->conf; - const char *key = ctx->key; + else if (streq(key, "overflowing-glyphs")) + return value_to_bool(ctx, &conf->tweak.overflowing_glyphs); - if (streq(key, "scaling-filter")) { - static const char *filters[] = { - [FCFT_SCALING_FILTER_NONE] = "none", - [FCFT_SCALING_FILTER_NEAREST] = "nearest", - [FCFT_SCALING_FILTER_BILINEAR] = "bilinear", + else if (streq(key, "damage-whole-window")) + return value_to_bool(ctx, &conf->tweak.damage_whole_window); - [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; + 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; - } + 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"); + 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; + /* Keep it enabled though - this will cause us to do + * grapheme-clustering at least */ } - 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; + } + + 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_DARK, - SECTION_COLORS_LIGHT, - 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_TABS, + SECTION_MAIN, + SECTION_SECURITY, + SECTION_BELL, + SECTION_DESKTOP_NOTIFICATIONS, + SECTION_SCROLLBACK, + SECTION_URL, + SECTION_REGEX, + SECTION_COLORS_DARK, + SECTION_COLORS_LIGHT, + 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_TABS, - /* Deprecated */ - SECTION_COLORS, - SECTION_COLORS2, + /* Deprecated */ + SECTION_COLORS, + SECTION_COLORS2, - SECTION_COUNT, + 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; + 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_DARK] = {&parse_section_colors_dark, "colors-dark"}, - [SECTION_COLORS_LIGHT] = {&parse_section_colors_light, "colors-light"}, - [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"}, - [SECTION_TABS] = {&parse_section_tabs, "tabs"}, + [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_DARK] = {&parse_section_colors_dark, "colors-dark"}, + [SECTION_COLORS_LIGHT] = {&parse_section_colors_light, "colors-light"}, + [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"}, + [SECTION_TABS] = {&parse_section_tabs, "tabs"}, /* Deprecated */ - [SECTION_COLORS] = {&parse_section_colors, "colors"}, - [SECTION_COLORS2] = {&parse_section_colors2, "colors2"}, + [SECTION_COLORS] = {&parse_section_colors, "colors"}, + [SECTION_COLORS2] = {&parse_section_colors2, "colors2"}, }; -static_assert(ALEN(section_info) == SECTION_COUNT, "section info array size mismatch"); +static_assert(ALEN(section_info) == SECTION_COUNT, + "section info array size mismatch"); -static enum section -str_to_section(char *str, char **suffix) -{ - *suffix = NULL; +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; + for (enum section section = SECTION_MAIN; section < SECTION_COUNT; + ++section) { + const char *name = section_info[section].name; - if (streq(str, name)) - return section; + 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); + 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; - } - } + /* 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; + } + 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; +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; + char *_line = NULL; + size_t count = 0; + bool ret = true; -#define error_or_continue() \ - { \ - if (errors_are_fatal) { \ - ret = false; \ - goto done; \ - } else \ - continue; \ +#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--; } - char *section_name = xstrdup("main"); - char *section_suffix = NULL; + /* Empty line, or comment */ + if (line[0] == '\0' || line[0] == '#') + continue; - 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_*() */ + /* 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; - 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; - } + 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; + 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"); +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"); - } + 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); + 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 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_TAB_NEW, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_t}}}, - {BIND_ACTION_TAB_CLOSE, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_w}}}, - {BIND_ACTION_TAB_NEXT, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_Tab}}}, - {BIND_ACTION_TAB_PREV, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_Tab}}}, - {BIND_ACTION_TAB_OVERVIEW, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_space}}}, - {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}}}, - }; +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_TAB_NEW, + m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), + {{XKB_KEY_t}}}, + {BIND_ACTION_TAB_CLOSE, + m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), + {{XKB_KEY_w}}}, + {BIND_ACTION_TAB_NEXT, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_Tab}}}, + {BIND_ACTION_TAB_PREV, + m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), + {{XKB_KEY_Tab}}}, + {BIND_ACTION_TAB_OVERVIEW, + m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), + {{XKB_KEY_space}}}, + {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)); + 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}}}, + {BIND_ACTION_SEARCH_TOGGLE_CASE, m(XKB_MOD_NAME_ALT), {{XKB_KEY_c}}}, + {BIND_ACTION_SEARCH_TOGGLE_WHOLE_WORD, + m(XKB_MOD_NAME_ALT), + {{XKB_KEY_w}}}, + {BIND_ACTION_SEARCH_TOGGLE_REGEX, m(XKB_MOD_NAME_ALT), {{XKB_KEY_r}}}, + {BIND_ACTION_SEARCH_HISTORY_PREV, m("none"), {{XKB_KEY_Up}}}, + {BIND_ACTION_SEARCH_HISTORY_NEXT, m("none"), {{XKB_KEY_Down}}}, + {BIND_ACTION_SEARCH_COMMIT_LINE, + m(XKB_MOD_NAME_CTRL), + {{XKB_KEY_Return}}}, + }; -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}}}, - {BIND_ACTION_SEARCH_TOGGLE_CASE, m(XKB_MOD_NAME_ALT), {{XKB_KEY_c}}}, - {BIND_ACTION_SEARCH_TOGGLE_WHOLE_WORD, m(XKB_MOD_NAME_ALT), {{XKB_KEY_w}}}, - {BIND_ACTION_SEARCH_TOGGLE_REGEX, m(XKB_MOD_NAME_ALT), {{XKB_KEY_r}}}, - {BIND_ACTION_SEARCH_HISTORY_PREV, m("none"), {{XKB_KEY_Up}}}, - {BIND_ACTION_SEARCH_HISTORY_NEXT, m("none"), {{XKB_KEY_Down}}}, - {BIND_ACTION_SEARCH_COMMIT_LINE, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_Return}}}, - }; - - conf->bindings.search.count = ALEN(bindings); - conf->bindings.search.arr = xmemdup(bindings, sizeof(bindings)); + 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}}}, - }; +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)); + 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}}}, - }; +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)); + 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])); +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); - } + 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(); +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) { - .conf_path = (conf_path ? xstrdup(conf_path) : NULL), - .term = xstrdup(FOOT_DEFAULT_TERM), - .shell = get_shell(), - .title = xstrdup("foot"), - .app_id = (as_server ? xstrdup("footclient") : xstrdup("foot")), - .toplevel_tag = xstrdup(""), - .word_delimiters = xc32dup(U",│`|:\"'()[]{}<>"), - .size = { - .type = CONF_SIZE_PX, - .width = 700, - .height = 500, - }, - .pad_left = 0, - .pad_top = 0, - .pad_right = 0, - .pad_bottom = 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, - .style = UNDERLINE_DOTTED, - }, - .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_dark = { - .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, - }, - .blur = false, - }, - .initial_color_theme = COLOR_THEME_DARK, - .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, - }, + *conf = (struct config){ + .conf_path = (conf_path ? xstrdup(conf_path) : NULL), + .term = xstrdup(FOOT_DEFAULT_TERM), + .shell = get_shell(), + .title = xstrdup("foot"), + .app_id = (as_server ? xstrdup("footclient") : xstrdup("foot")), + .toplevel_tag = xstrdup(""), + .word_delimiters = xc32dup(U",│`|:\"'()[]{}<>"), + .size = + { + .type = CONF_SIZE_PX, + .width = 700, + .height = 500, + }, + .pad_left = 0, + .pad_top = 0, + .pad_right = 0, + .pad_bottom = 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, + .style = UNDERLINE_DOTTED, + }, + .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_dark = + { + .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, + }, + .blur = false, + }, + .initial_color_theme = COLOR_THEME_DARK, + .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, - }, + .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, + .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, + .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, - }, + .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, - }, + .touch = + { + .long_press_delay = 400, + }, - .tabs = { - .enabled = false, - .inherit_cwd = false, - .position = CONF_TABS_POSITION_TOP, - .style = CONF_TABS_STYLE_ROUNDED, - .layout = CONF_TABS_LAYOUT_SPAN, - .height = 26, - .tab_width = 200, - .tab_padding = 8, - .label_padding = 8, - .margin = 4, - .corner_radius = 6, - .unread_indicator = NULL, /* set below to a strdup'd default */ - .colors = { - .bg = 0x1c1c1c, - .fg = 0xb0b0b0, - .active_bg = 0x3a3a3a, - .active_fg = 0xffffff, - .unread_fg = 0xfabd2f, /* warm yellow */ - }, - }, + .tabs = + { + .enabled = false, + .inherit_cwd = false, + .position = CONF_TABS_POSITION_TOP, + .style = CONF_TABS_STYLE_ROUNDED, + .layout = CONF_TABS_LAYOUT_SPAN, + .height = 26, + .tab_width = 200, + .tab_padding = 8, + .label_padding = 8, + .margin = 4, + .corner_radius = 6, + .unread_indicator = NULL, + .colors = + { + .bg = 0x1c1c1c, + .fg = 0xb0b0b0, + .active_bg = 0x3a3a3a, + .active_fg = 0xffffff, + .unread_fg = 0xfabd2f, + .overview_active_border = 0xfabd2f, + .overview_select_border = 0xffffff, + }, + }, - .env_vars = tll_init(), + .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), + .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(), - }; + .notifications = tll_init(), + }; - conf->tabs.unread_indicator = xstrdup("●"); + conf->tabs.unread_indicator = xstrdup("●"); - memcpy(conf->colors_dark.table, default_color_table, sizeof(default_color_table)); - memcpy(conf->colors_dark.sixel, default_sixel_colors, sizeof(default_sixel_colors)); - memcpy(&conf->colors_light, &conf->colors_dark, sizeof(conf->colors_dark)); - conf->colors_light.dim_blend_towards = DIM_BLEND_TOWARDS_WHITE; + memcpy(conf->colors_dark.table, default_color_table, + sizeof(default_color_table)); + memcpy(conf->colors_dark.sixel, default_sixel_colors, + sizeof(default_sixel_colors)); + memcpy(&conf->colors_light, &conf->colors_dark, sizeof(conf->colors_dark)); + conf->colors_light.dim_blend_towards = DIM_BLEND_TOWARDS_WHITE; - parse_modifiers(XKB_MOD_NAME_SHIFT, 5, &conf->mouse.selection_override_modifiers); + 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); + 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:/?#@!$&*+,;=.~_%^\\-]*'" - ")" + "(" + "(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); - } + 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); - } + 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); + 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; - } + 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 = open_config(); - if (conf_file.fd < 0) { - LOG_WARN("no configuration found, using defaults"); - ret = !errors_are_fatal; - } + conf_file.path = xstrdup(conf_path); + conf_file.fd = fd; } - - 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; - } + } else { + conf_file = open_config(); + if (conf_file.fd < 0) { + LOG_WARN("no configuration found, using defaults"); + ret = !errors_are_fatal; } + } - if (!config_override_apply(conf, overrides, 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; - 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; - } + fclose(f); + conf_file.fd = -1; } + } - if (ret && conf->csd.font.count == 0) - config_font_list_clone(&conf->csd.font, &conf->fonts[0]); + 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); + 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); + free(conf_file.path); + if (conf_file.fd >= 0) + close(conf_file.fd); - return ret; + return ret; } -bool -config_override_apply(struct config *conf, config_override_t *overrides, - bool errors_are_fatal) -{ - char *section_name = NULL; +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; + 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++; + 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; - } + 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; } - conf->csd.border_width = max( - min_csd_border_width, conf->csd.border_width_visible); + if (section_name[0] == '\0') { + LOG_CONTEXTUAL_ERR("empty section name"); + if (errors_are_fatal) + return false; + continue; + } - 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); + 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; + 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])); + 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]; + 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)); + *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; + 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_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); + 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; + 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; - } + 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; +struct config *config_clone(const struct config *old) { + struct config *conf = xmalloc(sizeof(*conf)); + *conf = *old; - conf->conf_path = (old->conf_path ? xstrdup(old->conf_path) : NULL); - conf->term = xstrdup(old->term); - conf->shell = xstrdup(old->shell); - conf->title = xstrdup(old->title); - conf->app_id = xstrdup(old->app_id); - conf->toplevel_tag = xstrdup(old->toplevel_tag); - 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); + conf->conf_path = (old->conf_path ? xstrdup(old->conf_path) : NULL); + conf->term = xstrdup(old->term); + conf->shell = xstrdup(old->shell); + conf->title = xstrdup(old->title); + conf->app_id = xstrdup(old->app_id); + conf->toplevel_tag = xstrdup(old->toplevel_tag); + 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); + 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); + 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; + 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)})); + 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); + } - 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); - 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; - 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)); - 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->conf_path); - free(conf->term); - free(conf->shell); - free(conf->title); - free(conf->app_id); - free(conf->toplevel_tag); - 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); - free(conf->tabs.unread_indicator); - 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 + tll_foreach(old->env_vars, it) { + struct env_var copy = { + .name = xstrdup(it->item.name), + .value = xstrdup(it->item.value), }; - return true; + 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; } -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; +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->conf_path); + free(conf->term); + free(conf->shell); + free(conf->title); + free(conf->app_id); + free(conf->toplevel_tag); + 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); -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"); + config_font_list_destroy(&conf->csd.font); - if (f == NULL) - return true; + free(conf->url.label_letters); + spawn_template_free(&conf->url.launch); + regfree(&conf->url.preg); + free(conf->url.regex); - static const char32_t chars[] = {U'a', U'i', U'l', U'M', U'W'}; + 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); + } - bool is_monospaced = true; - int last_width = -1; + 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); - 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); + tll_foreach(conf->env_vars, it) { + free(it->item.name); + free(it->item.value); + tll_remove(conf->env_vars, it); + } - if (g == NULL) - continue; + free(conf->utmp_helper_path); + free(conf->tabs.unread_indicator); + user_notifications_free(&conf->notifications); +} - if (last_width >= 0 && g->advance.x != last_width) { - const char *font_name = f->name != NULL - ? f->name - : pattern; +bool config_font_parse(const char *pattern, struct config_font *font) { + FcPattern *pat = FcNameParse((const FcChar8 *)pattern); + if (pat == NULL) + return false; - 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); + /* + * First look for user specified {pixel}size option + * e.g. "font-name:size=12" + */ - 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"; + double pt_size = -1.0; + FcResult have_pt_size = FcPatternGetDouble(pat, FC_SIZE, 0, &pt_size); - user_notification_add_fmt( - notifications, USER_NOTIFICATION_WARNING, fmt, font_name); + int px_size = -1; + FcResult have_px_size = FcPatternGetInteger(pat, FC_PIXEL_SIZE, 0, &px_size); - is_monospaced = false; - break; - } - - last_width = g->advance.x; + 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); } - fcft_destroy(f); - return is_monospaced; + 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 diff --git a/config.h b/config.h index 2e9c34c..dce0ed4 100644 --- a/config.h +++ b/config.h @@ -495,6 +495,8 @@ struct config { uint32_t active_bg; uint32_t active_fg; uint32_t unread_fg; + uint32_t overview_active_border; /* ring around the currently-active tab in the overview */ + uint32_t overview_select_border; /* ring around keyboard/hover-selected tab in the overview */ } colors; } tabs; diff --git a/csi.c b/csi.c index 87af215..f4f16fd 100644 --- a/csi.c +++ b/csi.c @@ -5,18 +5,18 @@ #include #if defined(_DEBUG) - #include +#include #endif #include #define LOG_MODULE "csi" #define LOG_ENABLE_DBG 0 -#include "log.h" #include "char32.h" #include "config.h" #include "debug.h" #include "grid.h" +#include "log.h" #include "selection.h" #include "sixel.h" #include "util.h" @@ -25,462 +25,479 @@ #include "xmalloc.h" #include "xsnprintf.h" -#define UNHANDLED() LOG_DBG("unhandled: %s", csi_as_string(term, final, -1)) -#define UNHANDLED_SGR(idx) LOG_DBG("unhandled: %s", csi_as_string(term, 'm', idx)) +#define UNHANDLED() LOG_DBG("unhandled: %s", csi_as_string(term, final, -1)) +#define UNHANDLED_SGR(idx) \ + LOG_DBG("unhandled: %s", csi_as_string(term, 'm', idx)) -static void -sgr_reset(struct terminal *term) -{ - term->vt.attrs = (struct attributes){0}; - term->vt.underline = (struct underline_range_data){0}; +static void sgr_reset(struct terminal *term) { + term->vt.attrs = (struct attributes){0}; + term->vt.underline = (struct underline_range_data){0}; - term->bits_affecting_ascii_printer.underline_style = false; - term->bits_affecting_ascii_printer.underline_color = false; - term_update_ascii_printer(term); + term->bits_affecting_ascii_printer.underline_style = false; + term->bits_affecting_ascii_printer.underline_color = false; + term_update_ascii_printer(term); } -static const char * -csi_as_string(struct terminal *term, uint8_t final, int idx) -{ - static char msg[1024]; - int c = snprintf(msg, sizeof(msg), "CSI: "); +static const char *csi_as_string(struct terminal *term, uint8_t final, + int idx) { + static char msg[1024]; + int c = snprintf(msg, sizeof(msg), "CSI: "); - for (size_t i = idx >= 0 ? idx : 0; - i < (idx >= 0 ? idx + 1 : term->vt.params.idx); - i++) - { - c += snprintf(&msg[c], sizeof(msg) - c, "%u", - term->vt.params.v[i].value); + for (size_t i = idx >= 0 ? idx : 0; + i < (idx >= 0 ? idx + 1 : term->vt.params.idx); i++) { + c += snprintf(&msg[c], sizeof(msg) - c, "%u", term->vt.params.v[i].value); - for (size_t j = 0; j < term->vt.params.v[i].sub.idx; j++) { - c += snprintf(&msg[c], sizeof(msg) - c, ":%u", - term->vt.params.v[i].sub.value[j]); - } - - c += snprintf(&msg[c], sizeof(msg) - c, "%s", - i == term->vt.params.idx - 1 ? "" : ";"); + for (size_t j = 0; j < term->vt.params.v[i].sub.idx; j++) { + c += snprintf(&msg[c], sizeof(msg) - c, ":%u", + term->vt.params.v[i].sub.value[j]); } - for (size_t i = 0; i < sizeof(term->vt.private); i++) { - char value = (term->vt.private >> (i * 8)) & 0xff; - if (value == 0) - break; - c += snprintf(&msg[c], sizeof(msg) - c, "%c", value); - } + c += snprintf(&msg[c], sizeof(msg) - c, "%s", + i == term->vt.params.idx - 1 ? "" : ";"); + } - snprintf(&msg[c], sizeof(msg) - c, "%c (%u parameters)", - final, idx >= 0 ? 1 : term->vt.params.idx); - return msg; + for (size_t i = 0; i < sizeof(term->vt.private); i++) { + char value = (term->vt.private >> (i * 8)) & 0xff; + if (value == 0) + break; + c += snprintf(&msg[c], sizeof(msg) - c, "%c", value); + } + + snprintf(&msg[c], sizeof(msg) - c, "%c (%u parameters)", final, + idx >= 0 ? 1 : term->vt.params.idx); + return msg; } -static void -csi_sgr(struct terminal *term) -{ - if (term->vt.params.idx == 0) { - sgr_reset(term); - return; - } +static void csi_sgr(struct terminal *term) { + if (term->vt.params.idx == 0) { + sgr_reset(term); + return; + } - for (size_t i = 0; i < term->vt.params.idx; i++) { - const int param = term->vt.params.v[i].value; - - switch (param) { - case 0: - sgr_reset(term); - break; - - case 1: term->vt.attrs.bold = true; break; - case 2: term->vt.attrs.dim = true; break; - case 3: term->vt.attrs.italic = true; break; - case 4: { - term->vt.attrs.underline = true; - term->vt.underline.style = UNDERLINE_SINGLE; - - if (unlikely(term->vt.params.v[i].sub.idx == 1)) { - enum underline_style style = term->vt.params.v[i].sub.value[0]; - - switch (style) { - default: - case UNDERLINE_NONE: - term->vt.attrs.underline = false; - term->vt.underline.style = UNDERLINE_NONE; - term->bits_affecting_ascii_printer.underline_style = false; - break; - - case UNDERLINE_SINGLE: - case UNDERLINE_DOUBLE: - case UNDERLINE_CURLY: - case UNDERLINE_DOTTED: - case UNDERLINE_DASHED: - term->vt.underline.style = style; - term->bits_affecting_ascii_printer.underline_style = - style > UNDERLINE_SINGLE; - break; - } - } else - term->bits_affecting_ascii_printer.underline_style = false; - term_update_ascii_printer(term); - break; - } - case 5: term->vt.attrs.blink = true; break; - case 6: LOG_WARN("ignored: rapid blink"); break; - case 7: term->vt.attrs.reverse = true; break; - case 8: term->vt.attrs.conceal = true; break; - case 9: term->vt.attrs.strikethrough = true; break; - - case 21: - term->vt.attrs.underline = true; - term->vt.underline.style = UNDERLINE_DOUBLE; - term->bits_affecting_ascii_printer.underline_style = true; - term_update_ascii_printer(term); - break; - - case 22: term->vt.attrs.bold = term->vt.attrs.dim = false; break; - case 23: term->vt.attrs.italic = false; break; - case 24: { - term->vt.attrs.underline = false; - term->vt.underline.style = UNDERLINE_NONE; - term->bits_affecting_ascii_printer.underline_style = false; - term_update_ascii_printer(term); - break; - } - case 25: term->vt.attrs.blink = false; break; - case 26: break; /* rapid blink, ignored */ - case 27: term->vt.attrs.reverse = false; break; - case 28: term->vt.attrs.conceal = false; break; - case 29: term->vt.attrs.strikethrough = false; break; - - /* Regular foreground colors */ - case 30: - case 31: - case 32: - case 33: - case 34: - case 35: - case 36: - case 37: - term->vt.attrs.fg_src = COLOR_BASE16; - term->vt.attrs.fg = param - 30; - break; - - case 38: - case 48: - case 58: { - uint32_t color; - enum color_source src; - - /* Indexed: 38;5; */ - if (term->vt.params.idx - i - 1 >= 2 && - term->vt.params.v[i + 1].value == 5) - { - src = COLOR_BASE256; - color = min(term->vt.params.v[i + 2].value, - ALEN(term->colors.table) - 1); - i += 2; - } - - /* RGB: 38;2;;; */ - else if (term->vt.params.idx - i - 1 >= 4 && - term->vt.params.v[i + 1].value == 2) - { - uint8_t r = term->vt.params.v[i + 2].value; - uint8_t g = term->vt.params.v[i + 3].value; - uint8_t b = term->vt.params.v[i + 4].value; - src = COLOR_RGB; - color = r << 16 | g << 8 | b; - i += 4; - } - - /* Indexed: 38:5: */ - else if (term->vt.params.v[i].sub.idx >= 2 && - term->vt.params.v[i].sub.value[0] == 5) - { - src = COLOR_BASE256; - color = min(term->vt.params.v[i].sub.value[1], - ALEN(term->colors.table) - 1); - } - - /* - * RGB: 38:2::r:g:b[:ignored:tolerance:tolerance-color-space] - * RGB: 38:2:r:g:b - * - * The second version is a "bastard" version - many - * programs "forget" the color space ID - * parameter... *sigh* - */ - else if (term->vt.params.v[i].sub.idx >= 4 && - term->vt.params.v[i].sub.value[0] == 2) - { - const struct vt_param *param = &term->vt.params.v[i]; - bool have_color_space_id = param->sub.idx >= 5; - - /* 0 - color space (ignored) */ - int r_idx = 2 - !have_color_space_id; - int g_idx = 3 - !have_color_space_id; - int b_idx = 4 - !have_color_space_id; - /* 5 - unused */ - /* 6 - CS tolerance */ - /* 7 - color space associated with tolerance */ - - uint8_t r = param->sub.value[r_idx]; - uint8_t g = param->sub.value[g_idx]; - uint8_t b = param->sub.value[b_idx]; - - src = COLOR_RGB; - color = r << 16 | g << 8 | b; - } - - /* Transparent: 38:1 */ - /* CMY: 38:3::c:m:y[:tolerance:tolerance-color-space] */ - /* CMYK: 38:4::c:m:y:k[:tolerance:tolerance-color-space] */ - - /* Unrecognized */ - else { - UNHANDLED_SGR(i); - break; - } - - if (unlikely(param == 58)) { - term->vt.underline.color_src = src; - term->vt.underline.color = color; - term->bits_affecting_ascii_printer.underline_color = true; - term_update_ascii_printer(term); - } else if (param == 38) { - term->vt.attrs.fg_src = src; - term->vt.attrs.fg = color; - } else { - xassert(param == 48); - term->vt.attrs.bg_src = src; - term->vt.attrs.bg = color; - } - break; - } - - case 39: - term->vt.attrs.fg_src = COLOR_DEFAULT; - break; - - /* Regular background colors */ - case 40: - case 41: - case 42: - case 43: - case 44: - case 45: - case 46: - case 47: - term->vt.attrs.bg_src = COLOR_BASE16; - term->vt.attrs.bg = param - 40; - break; - - case 49: - term->vt.attrs.bg_src = COLOR_DEFAULT; - break; - - case 59: - term->vt.underline.color_src = COLOR_DEFAULT; - term->vt.underline.color = 0; - term->bits_affecting_ascii_printer.underline_color = false; - term_update_ascii_printer(term); - break; - - /* Bright foreground colors */ - case 90: - case 91: - case 92: - case 93: - case 94: - case 95: - case 96: - case 97: - term->vt.attrs.fg_src = COLOR_BASE16; - term->vt.attrs.fg = param - 90 + 8; - break; - - /* Bright background colors */ - case 100: - case 101: - case 102: - case 103: - case 104: - case 105: - case 106: - case 107: - term->vt.attrs.bg_src = COLOR_BASE16; - term->vt.attrs.bg = param - 100 + 8; - break; - - default: - UNHANDLED_SGR(i); - break; - } - } -} - -static void -decset_decrst(struct terminal *term, unsigned param, bool enable) -{ -#if defined(_DEBUG) - /* For UNHANDLED() */ - int UNUSED final = enable ? 'h' : 'l'; -#endif - - /* Note: update XTSAVE/XTRESTORE if adding/removing things here */ + for (size_t i = 0; i < term->vt.params.idx; i++) { + const int param = term->vt.params.v[i].value; switch (param) { + case 0: + sgr_reset(term); + break; + case 1: - /* DECCKM */ - term->cursor_keys_mode = - enable ? CURSOR_KEYS_APPLICATION : CURSOR_KEYS_NORMAL; - break; + term->vt.attrs.bold = true; + break; + case 2: + term->vt.attrs.dim = true; + break; + case 3: + term->vt.attrs.italic = true; + break; + case 4: { + term->vt.attrs.underline = true; + term->vt.underline.style = UNDERLINE_SINGLE; + if (unlikely(term->vt.params.v[i].sub.idx == 1)) { + enum underline_style style = term->vt.params.v[i].sub.value[0]; + + switch (style) { + default: + case UNDERLINE_NONE: + term->vt.attrs.underline = false; + term->vt.underline.style = UNDERLINE_NONE; + term->bits_affecting_ascii_printer.underline_style = false; + break; + + case UNDERLINE_SINGLE: + case UNDERLINE_DOUBLE: + case UNDERLINE_CURLY: + case UNDERLINE_DOTTED: + case UNDERLINE_DASHED: + term->vt.underline.style = style; + term->bits_affecting_ascii_printer.underline_style = + style > UNDERLINE_SINGLE; + break; + } + } else + term->bits_affecting_ascii_printer.underline_style = false; + term_update_ascii_printer(term); + break; + } case 5: - /* DECSCNM */ - term->reverse = enable; - term_damage_all(term); - term_damage_margins(term); - break; + term->vt.attrs.blink = true; + break; + case 6: + LOG_WARN("ignored: rapid blink"); + break; + case 7: + term->vt.attrs.reverse = true; + break; + case 8: + term->vt.attrs.conceal = true; + break; + case 9: + term->vt.attrs.strikethrough = true; + break; - case 6: { - /* DECOM */ - term->origin = enable ? ORIGIN_RELATIVE : ORIGIN_ABSOLUTE; - term_cursor_home(term); + case 21: + term->vt.attrs.underline = true; + term->vt.underline.style = UNDERLINE_DOUBLE; + term->bits_affecting_ascii_printer.underline_style = true; + term_update_ascii_printer(term); + break; + + case 22: + term->vt.attrs.bold = term->vt.attrs.dim = false; + break; + case 23: + term->vt.attrs.italic = false; + break; + case 24: { + term->vt.attrs.underline = false; + term->vt.underline.style = UNDERLINE_NONE; + term->bits_affecting_ascii_printer.underline_style = false; + term_update_ascii_printer(term); + break; + } + case 25: + term->vt.attrs.blink = false; + break; + case 26: + break; /* rapid blink, ignored */ + case 27: + term->vt.attrs.reverse = false; + break; + case 28: + term->vt.attrs.conceal = false; + break; + case 29: + term->vt.attrs.strikethrough = false; + break; + + /* Regular foreground colors */ + case 30: + case 31: + case 32: + case 33: + case 34: + case 35: + case 36: + case 37: + term->vt.attrs.fg_src = COLOR_BASE16; + term->vt.attrs.fg = param - 30; + break; + + case 38: + case 48: + case 58: { + uint32_t color; + enum color_source src; + + /* Indexed: 38;5; */ + if (term->vt.params.idx - i - 1 >= 2 && + term->vt.params.v[i + 1].value == 5) { + src = COLOR_BASE256; + color = + min(term->vt.params.v[i + 2].value, ALEN(term->colors.table) - 1); + i += 2; + } + + /* RGB: 38;2;;; */ + else if (term->vt.params.idx - i - 1 >= 4 && + term->vt.params.v[i + 1].value == 2) { + uint8_t r = term->vt.params.v[i + 2].value; + uint8_t g = term->vt.params.v[i + 3].value; + uint8_t b = term->vt.params.v[i + 4].value; + src = COLOR_RGB; + color = r << 16 | g << 8 | b; + i += 4; + } + + /* Indexed: 38:5: */ + else if (term->vt.params.v[i].sub.idx >= 2 && + term->vt.params.v[i].sub.value[0] == 5) { + src = COLOR_BASE256; + color = min(term->vt.params.v[i].sub.value[1], + ALEN(term->colors.table) - 1); + } + + /* + * RGB: 38:2::r:g:b[:ignored:tolerance:tolerance-color-space] + * RGB: 38:2:r:g:b + * + * The second version is a "bastard" version - many + * programs "forget" the color space ID + * parameter... *sigh* + */ + else if (term->vt.params.v[i].sub.idx >= 4 && + term->vt.params.v[i].sub.value[0] == 2) { + const struct vt_param *param = &term->vt.params.v[i]; + bool have_color_space_id = param->sub.idx >= 5; + + /* 0 - color space (ignored) */ + int r_idx = 2 - !have_color_space_id; + int g_idx = 3 - !have_color_space_id; + int b_idx = 4 - !have_color_space_id; + /* 5 - unused */ + /* 6 - CS tolerance */ + /* 7 - color space associated with tolerance */ + + uint8_t r = param->sub.value[r_idx]; + uint8_t g = param->sub.value[g_idx]; + uint8_t b = param->sub.value[b_idx]; + + src = COLOR_RGB; + color = r << 16 | g << 8 | b; + } + + /* Transparent: 38:1 */ + /* CMY: 38:3::c:m:y[:tolerance:tolerance-color-space] + */ + /* CMYK: 38:4::c:m:y:k[:tolerance:tolerance-color-space] */ + + /* Unrecognized */ + else { + UNHANDLED_SGR(i); break; + } + + if (unlikely(param == 58)) { + term->vt.underline.color_src = src; + term->vt.underline.color = color; + term->bits_affecting_ascii_printer.underline_color = true; + term_update_ascii_printer(term); + } else if (param == 38) { + term->vt.attrs.fg_src = src; + term->vt.attrs.fg = color; + } else { + xassert(param == 48); + term->vt.attrs.bg_src = src; + term->vt.attrs.bg = color; + } + break; } - case 7: - /* DECAWM */ - term->auto_margin = enable; - term->grid->cursor.lcf = false; - break; + case 39: + term->vt.attrs.fg_src = COLOR_DEFAULT; + break; - case 9: - if (enable) - LOG_WARN("unimplemented: X10 mouse tracking mode"); + /* Regular background colors */ + case 40: + case 41: + case 42: + case 43: + case 44: + case 45: + case 46: + case 47: + term->vt.attrs.bg_src = COLOR_BASE16; + term->vt.attrs.bg = param - 40; + break; + + case 49: + term->vt.attrs.bg_src = COLOR_DEFAULT; + break; + + case 59: + term->vt.underline.color_src = COLOR_DEFAULT; + term->vt.underline.color = 0; + term->bits_affecting_ascii_printer.underline_color = false; + term_update_ascii_printer(term); + break; + + /* Bright foreground colors */ + case 90: + case 91: + case 92: + case 93: + case 94: + case 95: + case 96: + case 97: + term->vt.attrs.fg_src = COLOR_BASE16; + term->vt.attrs.fg = param - 90 + 8; + break; + + /* Bright background colors */ + case 100: + case 101: + case 102: + case 103: + case 104: + case 105: + case 106: + case 107: + term->vt.attrs.bg_src = COLOR_BASE16; + term->vt.attrs.bg = param - 100 + 8; + break; + + default: + UNHANDLED_SGR(i); + break; + } + } +} + +static void decset_decrst(struct terminal *term, unsigned param, bool enable) { +#if defined(_DEBUG) + /* For UNHANDLED() */ + int UNUSED final = enable ? 'h' : 'l'; +#endif + + /* Note: update XTSAVE/XTRESTORE if adding/removing things here */ + + switch (param) { + case 1: + /* DECCKM */ + term->cursor_keys_mode = + enable ? CURSOR_KEYS_APPLICATION : CURSOR_KEYS_NORMAL; + break; + + case 5: + /* DECSCNM */ + term->reverse = enable; + term_damage_all(term); + term_damage_margins(term); + break; + + case 6: { + /* DECOM */ + term->origin = enable ? ORIGIN_RELATIVE : ORIGIN_ABSOLUTE; + term_cursor_home(term); + break; + } + + case 7: + /* DECAWM */ + term->auto_margin = enable; + term->grid->cursor.lcf = false; + break; + + case 9: + if (enable) + LOG_WARN("unimplemented: X10 mouse tracking mode"); #if 0 else if (term->mouse_tracking == MOUSE_X10) term->mouse_tracking = MOUSE_NONE; #endif - break; + break; - case 12: - term->cursor_blink.decset = enable; - term_cursor_blink_update(term); - break; + case 12: + term->cursor_blink.decset = enable; + term_cursor_blink_update(term); + break; - case 25: - /* DECTCEM */ - term->hide_cursor = !enable; - break; + case 25: + /* DECTCEM */ + term->hide_cursor = !enable; + break; - case 45: - term->reverse_wrap = enable; - break; + case 45: + term->reverse_wrap = enable; + break; - case 66: - /* DECNKM */ - term->keypad_keys_mode = enable ? KEYPAD_APPLICATION : KEYPAD_NUMERICAL; - break; + case 66: + /* DECNKM */ + term->keypad_keys_mode = enable ? KEYPAD_APPLICATION : KEYPAD_NUMERICAL; + break; - case 67: - if (enable) - LOG_WARN("unimplemented: DECBKM"); - break; + case 67: + if (enable) + LOG_WARN("unimplemented: DECBKM"); + break; - case 80: - term->sixel.scrolling = !enable; - break; + case 80: + term->sixel.scrolling = !enable; + break; - case 1000: - if (enable) - term->mouse_tracking = MOUSE_CLICK; - else if (term->mouse_tracking == MOUSE_CLICK) - term->mouse_tracking = MOUSE_NONE; - term_xcursor_update(term); - break; + case 1000: + if (enable) + term->mouse_tracking = MOUSE_CLICK; + else if (term->mouse_tracking == MOUSE_CLICK) + term->mouse_tracking = MOUSE_NONE; + term_xcursor_update(term); + break; - case 1001: - if (enable) - LOG_WARN("unimplemented: highlight mouse tracking"); - break; + case 1001: + if (enable) + LOG_WARN("unimplemented: highlight mouse tracking"); + break; - case 1002: - if (enable) - term->mouse_tracking = MOUSE_DRAG; - else if (term->mouse_tracking == MOUSE_DRAG) - term->mouse_tracking = MOUSE_NONE; - term_xcursor_update(term); - break; + case 1002: + if (enable) + term->mouse_tracking = MOUSE_DRAG; + else if (term->mouse_tracking == MOUSE_DRAG) + term->mouse_tracking = MOUSE_NONE; + term_xcursor_update(term); + break; - case 1003: - if (enable) - term->mouse_tracking = MOUSE_MOTION; - else if (term->mouse_tracking == MOUSE_MOTION) - term->mouse_tracking = MOUSE_NONE; - term_xcursor_update(term); - break; + case 1003: + if (enable) + term->mouse_tracking = MOUSE_MOTION; + else if (term->mouse_tracking == MOUSE_MOTION) + term->mouse_tracking = MOUSE_NONE; + term_xcursor_update(term); + break; - case 1004: - term->focus_events = enable; - if (enable) - term_to_slave(term, term->kbd_focus ? "\033[I" : "\033[O", 3); - break; + case 1004: + term->focus_events = enable; + if (enable) + term_to_slave(term, term->kbd_focus ? "\033[I" : "\033[O", 3); + break; - case 1005: - if (enable) - LOG_WARN("unimplemented: mouse reporting mode: UTF-8"); + case 1005: + if (enable) + LOG_WARN("unimplemented: mouse reporting mode: UTF-8"); #if 0 else if (term->mouse_reporting == MOUSE_UTF8) term->mouse_reporting = MOUSE_NONE; #endif - break; + break; - case 1006: - if (enable) - term->mouse_reporting = MOUSE_SGR; - else if (term->mouse_reporting == MOUSE_SGR) - term->mouse_reporting = MOUSE_NORMAL; - break; + case 1006: + if (enable) + term->mouse_reporting = MOUSE_SGR; + else if (term->mouse_reporting == MOUSE_SGR) + term->mouse_reporting = MOUSE_NORMAL; + break; - case 1007: - term->alt_scrolling = enable; - break; + case 1007: + term->alt_scrolling = enable; + break; - case 1015: - if (enable) - term->mouse_reporting = MOUSE_URXVT; - else if (term->mouse_reporting == MOUSE_URXVT) - term->mouse_reporting = MOUSE_NORMAL; - break; + case 1015: + if (enable) + term->mouse_reporting = MOUSE_URXVT; + else if (term->mouse_reporting == MOUSE_URXVT) + term->mouse_reporting = MOUSE_NORMAL; + break; - case 1016: - if (enable) - term->mouse_reporting = MOUSE_SGR_PIXELS; - else if (term->mouse_reporting == MOUSE_SGR_PIXELS) - term->mouse_reporting = MOUSE_NORMAL; - break; + case 1016: + if (enable) + term->mouse_reporting = MOUSE_SGR_PIXELS; + else if (term->mouse_reporting == MOUSE_SGR_PIXELS) + term->mouse_reporting = MOUSE_NORMAL; + break; - case 1034: - /* smm */ - LOG_DBG("%s 8-bit meta mode", enable ? "enabling" : "disabling"); - term->meta.eight_bit = enable; - break; + case 1034: + /* smm */ + LOG_DBG("%s 8-bit meta mode", enable ? "enabling" : "disabling"); + term->meta.eight_bit = enable; + break; - case 1035: - /* numLock */ - LOG_DBG("%s Num Lock modifier", enable ? "enabling" : "disabling"); - term->num_lock_modifier = enable; - break; + case 1035: + /* numLock */ + LOG_DBG("%s Num Lock modifier", enable ? "enabling" : "disabling"); + term->num_lock_modifier = enable; + break; - case 1036: - /* metaSendsEscape */ - LOG_DBG("%s meta-sends-escape", enable ? "enabling" : "disabling"); - term->meta.esc_prefix = enable; - break; + case 1036: + /* metaSendsEscape */ + LOG_DBG("%s meta-sends-escape", enable ? "enabling" : "disabling"); + term->meta.esc_prefix = enable; + break; - case 1042: - term->bell_action_enabled = enable; - break; + case 1042: + term->bell_action_enabled = enable; + break; #if 0 case 1043: @@ -488,123 +505,116 @@ decset_decrst(struct terminal *term, unsigned param, bool enable) break; #endif - case 1048: - if (enable) - term_save_cursor(term); - else - term_restore_cursor(term, &term->grid->saved_cursor); - break; + case 1048: + if (enable) + term_save_cursor(term); + else + term_restore_cursor(term, &term->grid->saved_cursor); + break; - case 47: - case 1047: - case 1049: - if (enable && term->grid != &term->alt) { - selection_cancel(term); + case 47: + case 1047: + case 1049: + if (enable && term->grid != &term->alt) { + selection_cancel(term); - if (param == 1049) - term_save_cursor(term); + if (param == 1049) + term_save_cursor(term); - term->grid = &term->alt; + term->grid = &term->alt; - /* Cursor retains its position from the normal grid */ - term_cursor_to( - term, - min(term->normal.cursor.point.row, term->rows - 1), - min(term->normal.cursor.point.col, term->cols - 1)); + /* Cursor retains its position from the normal grid */ + term_cursor_to(term, min(term->normal.cursor.point.row, term->rows - 1), + min(term->normal.cursor.point.col, term->cols - 1)); - tll_free(term->normal.scroll_damage); - term_erase(term, 0, 0, term->rows - 1, term->cols - 1); - } - - else if (!enable && term->grid == &term->alt) { - selection_cancel(term); - - term->grid = &term->normal; - - /* Cursor retains its position from the alt grid */ - term_cursor_to( - term, min(term->alt.cursor.point.row, term->rows - 1), - min(term->alt.cursor.point.col, term->cols - 1)); - - if (param == 1049) - term_restore_cursor(term, &term->grid->saved_cursor); - - /* Delete all sixel images on the alt screen */ - tll_foreach(term->alt.sixel_images, it) { - sixel_destroy(&it->item); - tll_remove(term->alt.sixel_images, it); - } - - tll_free(term->alt.scroll_damage); - term_damage_view(term); - } - - term->bits_affecting_ascii_printer.sixels = - tll_length(term->grid->sixel_images) > 0; - term_update_ascii_printer(term); - break; - - case 1070: - term->sixel.use_private_palette = enable; - break; - - case 2004: - term->bracketed_paste = enable; - break; - - case 2026: - if (enable) - term_enable_app_sync_updates(term); - else - term_disable_app_sync_updates(term); - break; - - case 2027: -#if defined(FOOT_GRAPHEME_CLUSTERING) - term->grapheme_shaping = enable; -#endif - break; - - case 2031: - term->report_theme_changes = enable; - break; - - case 2048: - if (enable) - term_enable_size_notifications(term); - else - term_disable_size_notifications(term); - break; - - case 8452: - term->sixel.cursor_right_of_graphics = enable; - break; - - case 737769: - if (enable) - term_ime_enable(term); - else { - term_ime_disable(term); - term->ime_reenable_after_url_mode = false; - } - break; - - default: - UNHANDLED(); - break; + tll_free(term->normal.scroll_damage); + term_erase(term, 0, 0, term->rows - 1, term->cols - 1); } + + else if (!enable && term->grid == &term->alt) { + selection_cancel(term); + + term->grid = &term->normal; + + /* Cursor retains its position from the alt grid */ + term_cursor_to(term, min(term->alt.cursor.point.row, term->rows - 1), + min(term->alt.cursor.point.col, term->cols - 1)); + + if (param == 1049) + term_restore_cursor(term, &term->grid->saved_cursor); + + /* Delete all sixel images on the alt screen */ + tll_foreach(term->alt.sixel_images, it) { + sixel_destroy(&it->item); + tll_remove(term->alt.sixel_images, it); + } + + tll_free(term->alt.scroll_damage); + term_damage_view(term); + } + + term->bits_affecting_ascii_printer.sixels = + tll_length(term->grid->sixel_images) > 0; + term_update_ascii_printer(term); + break; + + case 1070: + term->sixel.use_private_palette = enable; + break; + + case 2004: + term->bracketed_paste = enable; + break; + + case 2026: + if (enable) + term_enable_app_sync_updates(term); + else + term_disable_app_sync_updates(term); + break; + + case 2027: +#if defined(FOOT_GRAPHEME_CLUSTERING) + term->grapheme_shaping = enable; +#endif + break; + + case 2031: + term->report_theme_changes = enable; + break; + + case 2048: + if (enable) + term_enable_size_notifications(term); + else + term_disable_size_notifications(term); + break; + + case 8452: + term->sixel.cursor_right_of_graphics = enable; + break; + + case 737769: + if (enable) + term_ime_enable(term); + else { + term_ime_disable(term); + term->ime_reenable_after_url_mode = false; + } + break; + + default: + UNHANDLED(); + break; + } } -static void -decset(struct terminal *term, unsigned param) -{ - decset_decrst(term, param, true); +static void decset(struct terminal *term, unsigned param) { + decset_decrst(term, param, true); } -static void -decrst(struct terminal *term, unsigned param) -{ - decset_decrst(term, param, false); +static void decrst(struct terminal *term, unsigned param) { + decset_decrst(term, param, false); } /* @@ -612,1623 +622,1841 @@ decrst(struct terminal *term, unsigned param) * as returned in the DECRPM reply to a DECRQM query. */ enum decrpm_status { - DECRPM_NOT_RECOGNIZED = 0, - DECRPM_SET = 1, - DECRPM_RESET = 2, - DECRPM_PERMANENTLY_SET = 3, - DECRPM_PERMANENTLY_RESET = 4, + DECRPM_NOT_RECOGNIZED = 0, + DECRPM_SET = 1, + DECRPM_RESET = 2, + DECRPM_PERMANENTLY_SET = 3, + DECRPM_PERMANENTLY_RESET = 4, }; -static enum decrpm_status -decrpm(bool enabled) -{ - return enabled ? DECRPM_SET : DECRPM_RESET; +static enum decrpm_status decrpm(bool enabled) { + return enabled ? DECRPM_SET : DECRPM_RESET; } -static enum decrpm_status -decrqm(const struct terminal *term, unsigned param) -{ - switch (param) { - case 1: return decrpm(term->cursor_keys_mode == CURSOR_KEYS_APPLICATION); - case 5: return decrpm(term->reverse); - case 6: return decrpm(term->origin); - case 7: return decrpm(term->auto_margin); - case 9: return DECRPM_PERMANENTLY_RESET; /* term->mouse_tracking == MOUSE_X10; */ - case 12: return decrpm(term->cursor_blink.decset); - case 25: return decrpm(!term->hide_cursor); - case 45: return decrpm(term->reverse_wrap); - case 66: return decrpm(term->keypad_keys_mode == KEYPAD_APPLICATION); - case 67: return DECRPM_PERMANENTLY_RESET; /* https://vt100.net/docs/vt510-rm/DECBKM */ - case 80: return decrpm(!term->sixel.scrolling); - case 1000: return decrpm(term->mouse_tracking == MOUSE_CLICK); - case 1001: return DECRPM_PERMANENTLY_RESET; - case 1002: return decrpm(term->mouse_tracking == MOUSE_DRAG); - case 1003: return decrpm(term->mouse_tracking == MOUSE_MOTION); - case 1004: return decrpm(term->focus_events); - case 1005: return DECRPM_PERMANENTLY_RESET; /* term->mouse_reporting == MOUSE_UTF8; */ - case 1006: return decrpm(term->mouse_reporting == MOUSE_SGR); - case 1007: return decrpm(term->alt_scrolling); - case 1015: return decrpm(term->mouse_reporting == MOUSE_URXVT); - case 1016: return decrpm(term->mouse_reporting == MOUSE_SGR_PIXELS); - case 1034: return decrpm(term->meta.eight_bit); - case 1035: return decrpm(term->num_lock_modifier); - case 1036: return decrpm(term->meta.esc_prefix); - case 1042: return decrpm(term->bell_action_enabled); - case 47: /* FALLTHROUGH */ - case 1047: /* FALLTHROUGH */ - case 1049: return decrpm(term->grid == &term->alt); - case 1070: return decrpm(term->sixel.use_private_palette); - case 2004: return decrpm(term->bracketed_paste); - case 2026: return decrpm(term->render.app_sync_updates.enabled); - case 2027: return term->conf->tweak.grapheme_width_method != GRAPHEME_WIDTH_DOUBLE - ? DECRPM_PERMANENTLY_RESET - : decrpm(term->grapheme_shaping); - case 2031: return decrpm(term->report_theme_changes); - case 2048: return decrpm(term->size_notifications); - case 8452: return decrpm(term->sixel.cursor_right_of_graphics); - case 737769: return decrpm(term_ime_is_enabled(term)); +static enum decrpm_status decrqm(const struct terminal *term, unsigned param) { + switch (param) { + case 1: + return decrpm(term->cursor_keys_mode == CURSOR_KEYS_APPLICATION); + case 5: + return decrpm(term->reverse); + case 6: + return decrpm(term->origin); + case 7: + return decrpm(term->auto_margin); + case 9: + return DECRPM_PERMANENTLY_RESET; /* term->mouse_tracking == MOUSE_X10; */ + case 12: + return decrpm(term->cursor_blink.decset); + case 25: + return decrpm(!term->hide_cursor); + case 45: + return decrpm(term->reverse_wrap); + case 66: + return decrpm(term->keypad_keys_mode == KEYPAD_APPLICATION); + case 67: + return DECRPM_PERMANENTLY_RESET; /* https://vt100.net/docs/vt510-rm/DECBKM + */ + case 80: + return decrpm(!term->sixel.scrolling); + case 1000: + return decrpm(term->mouse_tracking == MOUSE_CLICK); + case 1001: + return DECRPM_PERMANENTLY_RESET; + case 1002: + return decrpm(term->mouse_tracking == MOUSE_DRAG); + case 1003: + return decrpm(term->mouse_tracking == MOUSE_MOTION); + case 1004: + return decrpm(term->focus_events); + case 1005: + return DECRPM_PERMANENTLY_RESET; /* term->mouse_reporting == MOUSE_UTF8; */ + case 1006: + return decrpm(term->mouse_reporting == MOUSE_SGR); + case 1007: + return decrpm(term->alt_scrolling); + case 1015: + return decrpm(term->mouse_reporting == MOUSE_URXVT); + case 1016: + return decrpm(term->mouse_reporting == MOUSE_SGR_PIXELS); + case 1034: + return decrpm(term->meta.eight_bit); + case 1035: + return decrpm(term->num_lock_modifier); + case 1036: + return decrpm(term->meta.esc_prefix); + case 1042: + return decrpm(term->bell_action_enabled); + case 47: /* FALLTHROUGH */ + case 1047: /* FALLTHROUGH */ + case 1049: + return decrpm(term->grid == &term->alt); + case 1070: + return decrpm(term->sixel.use_private_palette); + case 2004: + return decrpm(term->bracketed_paste); + case 2026: + return decrpm(term->render.app_sync_updates.enabled); + case 2027: + return term->conf->tweak.grapheme_width_method != GRAPHEME_WIDTH_DOUBLE + ? DECRPM_PERMANENTLY_RESET + : decrpm(term->grapheme_shaping); + case 2031: + return decrpm(term->report_theme_changes); + case 2048: + return decrpm(term->size_notifications); + case 8452: + return decrpm(term->sixel.cursor_right_of_graphics); + case 737769: + return decrpm(term_ime_is_enabled(term)); + } + + return DECRPM_NOT_RECOGNIZED; +} + +static void xtsave(struct terminal *term, unsigned param) { + switch (param) { + case 1: + term->xtsave.application_cursor_keys = + term->cursor_keys_mode == CURSOR_KEYS_APPLICATION; + break; + case 5: + term->xtsave.reverse = term->reverse; + break; + case 6: + term->xtsave.origin = term->origin; + break; + case 7: + term->xtsave.auto_margin = term->auto_margin; + break; + case 9: /* term->xtsave.mouse_x10 = term->mouse_tracking == MOUSE_X10; */ + break; + case 12: + term->xtsave.cursor_blink = term->cursor_blink.decset; + break; + case 25: + term->xtsave.show_cursor = !term->hide_cursor; + break; + case 45: + term->xtsave.reverse_wrap = term->reverse_wrap; + break; + case 47: + term->xtsave.alt_screen = term->grid == &term->alt; + break; + case 66: + term->xtsave.application_keypad_keys = + term->keypad_keys_mode == KEYPAD_APPLICATION; + break; + case 67: + break; + case 80: + term->xtsave.sixel_display_mode = !term->sixel.scrolling; + break; + case 1000: + term->xtsave.mouse_click = term->mouse_tracking == MOUSE_CLICK; + break; + case 1001: + break; + case 1002: + term->xtsave.mouse_drag = term->mouse_tracking == MOUSE_DRAG; + break; + case 1003: + term->xtsave.mouse_motion = term->mouse_tracking == MOUSE_MOTION; + break; + case 1004: + term->xtsave.focus_events = term->focus_events; + break; + case 1005: /* term->xtsave.mouse_utf8 = term->mouse_reporting == MOUSE_UTF8; + */ + break; + case 1006: + term->xtsave.mouse_sgr = term->mouse_reporting == MOUSE_SGR; + break; + case 1007: + term->xtsave.alt_scrolling = term->alt_scrolling; + break; + case 1015: + term->xtsave.mouse_urxvt = term->mouse_reporting == MOUSE_URXVT; + break; + case 1016: + term->xtsave.mouse_sgr_pixels = term->mouse_reporting == MOUSE_SGR_PIXELS; + break; + case 1034: + term->xtsave.meta_eight_bit = term->meta.eight_bit; + break; + case 1035: + term->xtsave.num_lock_modifier = term->num_lock_modifier; + break; + case 1036: + term->xtsave.meta_esc_prefix = term->meta.esc_prefix; + break; + case 1042: + term->xtsave.bell_action_enabled = term->bell_action_enabled; + break; + case 1047: + term->xtsave.alt_screen = term->grid == &term->alt; + break; + case 1048: + term_save_cursor(term); + break; + case 1049: + term->xtsave.alt_screen = term->grid == &term->alt; + break; + case 1070: + term->xtsave.sixel_private_palette = term->sixel.use_private_palette; + break; + case 2004: + term->xtsave.bracketed_paste = term->bracketed_paste; + break; + case 2026: + term->xtsave.app_sync_updates = term->render.app_sync_updates.enabled; + break; + case 2027: + term->xtsave.grapheme_shaping = term->grapheme_shaping; + break; + case 2031: + term->xtsave.report_theme_changes = term->report_theme_changes; + break; + case 2048: + term->xtsave.size_notifications = term->size_notifications; + break; + case 8452: + term->xtsave.sixel_cursor_right_of_graphics = + term->sixel.cursor_right_of_graphics; + break; + case 737769: + term->xtsave.ime = term_ime_is_enabled(term); + break; + } +} + +static void xtrestore(struct terminal *term, unsigned param) { + bool enable; + switch (param) { + case 1: + enable = term->xtsave.application_cursor_keys; + break; + case 5: + enable = term->xtsave.reverse; + break; + case 6: + enable = term->xtsave.origin; + break; + case 7: + enable = term->xtsave.auto_margin; + break; + case 9: /* enable = term->xtsave.mouse_x10; break; */ + return; + case 12: + enable = term->xtsave.cursor_blink; + break; + case 25: + enable = term->xtsave.show_cursor; + break; + case 45: + enable = term->xtsave.reverse_wrap; + break; + case 47: + enable = term->xtsave.alt_screen; + break; + case 66: + enable = term->xtsave.application_keypad_keys; + break; + case 67: + return; + case 80: + enable = term->xtsave.sixel_display_mode; + break; + case 1000: + enable = term->xtsave.mouse_click; + break; + case 1001: + return; + case 1002: + enable = term->xtsave.mouse_drag; + break; + case 1003: + enable = term->xtsave.mouse_motion; + break; + case 1004: + enable = term->xtsave.focus_events; + break; + case 1005: /* enable = term->xtsave.mouse_utf8; break; */ + return; + case 1006: + enable = term->xtsave.mouse_sgr; + break; + case 1007: + enable = term->xtsave.alt_scrolling; + break; + case 1015: + enable = term->xtsave.mouse_urxvt; + break; + case 1016: + enable = term->xtsave.mouse_sgr_pixels; + break; + case 1034: + enable = term->xtsave.meta_eight_bit; + break; + case 1035: + enable = term->xtsave.num_lock_modifier; + break; + case 1036: + enable = term->xtsave.meta_esc_prefix; + break; + case 1042: + enable = term->xtsave.bell_action_enabled; + break; + case 1047: + enable = term->xtsave.alt_screen; + break; + case 1048: + enable = true; + break; + case 1049: + enable = term->xtsave.alt_screen; + break; + case 1070: + enable = term->xtsave.sixel_private_palette; + break; + case 2004: + enable = term->xtsave.bracketed_paste; + break; + case 2026: + enable = term->xtsave.app_sync_updates; + break; + case 2027: + enable = term->xtsave.grapheme_shaping; + break; + case 2031: + enable = term->xtsave.report_theme_changes; + break; + case 2048: + enable = term->xtsave.size_notifications; + break; + case 8452: + enable = term->xtsave.sixel_cursor_right_of_graphics; + break; + case 737769: + enable = term->xtsave.ime; + break; + + default: + return; + } + + decset_decrst(term, param, enable); +} + +static bool params_to_rectangular_area(const struct terminal *term, + int first_idx, int *top, int *left, + int *bottom, int *right) { + int rel_top = vt_param_get(term, first_idx + 0, 1) - 1; + *left = min(vt_param_get(term, first_idx + 1, 1) - 1, term->cols - 1); + int rel_bottom = vt_param_get(term, first_idx + 2, term->rows) - 1; + *right = + min(vt_param_get(term, first_idx + 3, term->cols) - 1, term->cols - 1); + + if (rel_top > rel_bottom || *left > *right) + return false; + + *top = term_row_rel_to_abs(term, rel_top); + *bottom = term_row_rel_to_abs(term, rel_bottom); + + return true; +} + +void csi_dispatch(struct terminal *term, uint8_t final) { + LOG_DBG("%s (%08x)", csi_as_string(term, final, -1), term->vt.private); + + switch (term->vt.private) { + case 0: { + switch (final) { + case 'b': + if (term->vt.last_printed != 0) { + /* + * Note: we never reset 'last-printed'. According to + * ECMA-48, the behaviour is undefined if REP was + * _not_ preceded by a graphical character. + */ + int count = vt_param_get(term, 0, 1); + LOG_DBG("REP: '%lc' %d times", (wint_t)term->vt.last_printed, count); + + int width; + + if (term->vt.last_printed >= CELL_COMB_CHARS_LO) { + const struct composed *comp = composed_lookup( + term->composed, term->vt.last_printed - CELL_COMB_CHARS_LO); + + xassert(comp != NULL); + width = comp->forced_width > 0 ? comp->forced_width : comp->width; + } else + width = c32width(term->vt.last_printed); + + if (width > 0) { + for (int i = 0; i < count; i++) + term_print(term, term->vt.last_printed, width, false); + } + } + break; + + case 'c': { + if (vt_param_get(term, 0, 0) != 0) { + UNHANDLED(); + break; + } + + /* Send Device Attributes (Primary DA) */ + + /* + * Responses: + * - CSI?1;2c vt100 with advanced video option + * - CSI?1;0c vt101 with no options + * - CSI?6c vt102 + * - CSI?62;c vt220 + * - CSI?63;c vt320 + * - CSI?64;c vt420 + * + * Ps (response may contain multiple): + * - 1 132 columns + * - 2 Printer. + * - 3 ReGIS graphics. + * - 4 Sixel graphics. + * - 6 Selective erase. + * - 8 User-defined keys. + * - 9 National Replacement Character sets. + * - 15 Technical characters. + * - 16 Locator port. + * - 17 Terminal state interrogation. + * - 18 User windows. + * - 21 Horizontal scrolling. + * - 22 ANSI color, e.g., VT525. + * - 28 Rectangular editing. + * - 29 ANSI text locator (i.e., DEC Locator mode). + * - 52 Clipboard access + * + * Note: we report ourselves as a VT220, mainly to be able + * to pass parameters, to indicate we support sixel, and + * ANSI colors. + * + * The VT level must be synchronized with the secondary DA + * response. + * + * Note: tertiary DA responds with "FOOT". + */ + char reply[32]; + + int len = + snprintf(reply, sizeof(reply), "\033[?62%s;22;28%sc", + term->conf->tweak.sixel ? ";4" : "", + (term->conf->security.osc52 == OSC52_ENABLED || + term->conf->security.osc52 == OSC52_COPY_ENABLED + ? ";52" + : "")); + + term_to_slave(term, reply, len); + break; } - return DECRPM_NOT_RECOGNIZED; -} - -static void -xtsave(struct terminal *term, unsigned param) -{ - switch (param) { - case 1: term->xtsave.application_cursor_keys = term->cursor_keys_mode == CURSOR_KEYS_APPLICATION; break; - case 5: term->xtsave.reverse = term->reverse; break; - case 6: term->xtsave.origin = term->origin; break; - case 7: term->xtsave.auto_margin = term->auto_margin; break; - case 9: /* term->xtsave.mouse_x10 = term->mouse_tracking == MOUSE_X10; */ break; - case 12: term->xtsave.cursor_blink = term->cursor_blink.decset; break; - case 25: term->xtsave.show_cursor = !term->hide_cursor; break; - case 45: term->xtsave.reverse_wrap = term->reverse_wrap; break; - case 47: term->xtsave.alt_screen = term->grid == &term->alt; break; - case 66: term->xtsave.application_keypad_keys = term->keypad_keys_mode == KEYPAD_APPLICATION; break; - case 67: break; - case 80: term->xtsave.sixel_display_mode = !term->sixel.scrolling; break; - case 1000: term->xtsave.mouse_click = term->mouse_tracking == MOUSE_CLICK; break; - case 1001: break; - case 1002: term->xtsave.mouse_drag = term->mouse_tracking == MOUSE_DRAG; break; - case 1003: term->xtsave.mouse_motion = term->mouse_tracking == MOUSE_MOTION; break; - case 1004: term->xtsave.focus_events = term->focus_events; break; - case 1005: /* term->xtsave.mouse_utf8 = term->mouse_reporting == MOUSE_UTF8; */ break; - case 1006: term->xtsave.mouse_sgr = term->mouse_reporting == MOUSE_SGR; break; - case 1007: term->xtsave.alt_scrolling = term->alt_scrolling; break; - case 1015: term->xtsave.mouse_urxvt = term->mouse_reporting == MOUSE_URXVT; break; - case 1016: term->xtsave.mouse_sgr_pixels = term->mouse_reporting == MOUSE_SGR_PIXELS; break; - case 1034: term->xtsave.meta_eight_bit = term->meta.eight_bit; break; - case 1035: term->xtsave.num_lock_modifier = term->num_lock_modifier; break; - case 1036: term->xtsave.meta_esc_prefix = term->meta.esc_prefix; break; - case 1042: term->xtsave.bell_action_enabled = term->bell_action_enabled; break; - case 1047: term->xtsave.alt_screen = term->grid == &term->alt; break; - case 1048: term_save_cursor(term); break; - case 1049: term->xtsave.alt_screen = term->grid == &term->alt; break; - case 1070: term->xtsave.sixel_private_palette = term->sixel.use_private_palette; break; - case 2004: term->xtsave.bracketed_paste = term->bracketed_paste; break; - case 2026: term->xtsave.app_sync_updates = term->render.app_sync_updates.enabled; break; - case 2027: term->xtsave.grapheme_shaping = term->grapheme_shaping; break; - case 2031: term->xtsave.report_theme_changes = term->report_theme_changes; break; - case 2048: term->xtsave.size_notifications = term->size_notifications; break; - case 8452: term->xtsave.sixel_cursor_right_of_graphics = term->sixel.cursor_right_of_graphics; break; - case 737769: term->xtsave.ime = term_ime_is_enabled(term); break; - } -} - -static void -xtrestore(struct terminal *term, unsigned param) -{ - bool enable; - switch (param) { - case 1: enable = term->xtsave.application_cursor_keys; break; - case 5: enable = term->xtsave.reverse; break; - case 6: enable = term->xtsave.origin; break; - case 7: enable = term->xtsave.auto_margin; break; - case 9: /* enable = term->xtsave.mouse_x10; break; */ return; - case 12: enable = term->xtsave.cursor_blink; break; - case 25: enable = term->xtsave.show_cursor; break; - case 45: enable = term->xtsave.reverse_wrap; break; - case 47: enable = term->xtsave.alt_screen; break; - case 66: enable = term->xtsave.application_keypad_keys; break; - case 67: return; - case 80: enable = term->xtsave.sixel_display_mode; break; - case 1000: enable = term->xtsave.mouse_click; break; - case 1001: return; - case 1002: enable = term->xtsave.mouse_drag; break; - case 1003: enable = term->xtsave.mouse_motion; break; - case 1004: enable = term->xtsave.focus_events; break; - case 1005: /* enable = term->xtsave.mouse_utf8; break; */ return; - case 1006: enable = term->xtsave.mouse_sgr; break; - case 1007: enable = term->xtsave.alt_scrolling; break; - case 1015: enable = term->xtsave.mouse_urxvt; break; - case 1016: enable = term->xtsave.mouse_sgr_pixels; break; - case 1034: enable = term->xtsave.meta_eight_bit; break; - case 1035: enable = term->xtsave.num_lock_modifier; break; - case 1036: enable = term->xtsave.meta_esc_prefix; break; - case 1042: enable = term->xtsave.bell_action_enabled; break; - case 1047: enable = term->xtsave.alt_screen; break; - case 1048: enable = true; break; - case 1049: enable = term->xtsave.alt_screen; break; - case 1070: enable = term->xtsave.sixel_private_palette; break; - case 2004: enable = term->xtsave.bracketed_paste; break; - case 2026: enable = term->xtsave.app_sync_updates; break; - case 2027: enable = term->xtsave.grapheme_shaping; break; - case 2031: enable = term->xtsave.report_theme_changes; break; - case 2048: enable = term->xtsave.size_notifications; break; - case 8452: enable = term->xtsave.sixel_cursor_right_of_graphics; break; - case 737769: enable = term->xtsave.ime; break; - - default: return; + case 'd': { + /* VPA - vertical line position absolute */ + int rel_row = vt_param_get(term, 0, 1) - 1; + int row = term_row_rel_to_abs(term, rel_row); + term_cursor_to(term, row, term->grid->cursor.point.col); + break; } - decset_decrst(term, param, enable); -} + case 'm': + csi_sgr(term); + break; -static bool -params_to_rectangular_area(const struct terminal *term, int first_idx, - int *top, int *left, int *bottom, int *right) -{ - int rel_top = vt_param_get(term, first_idx + 0, 1) - 1; - *left = min(vt_param_get(term, first_idx + 1, 1) - 1, term->cols - 1); - int rel_bottom = vt_param_get(term, first_idx + 2, term->rows) - 1; - *right = min(vt_param_get(term, first_idx + 3, term->cols) - 1, term->cols - 1); + case 'A': + term_cursor_up(term, vt_param_get(term, 0, 1)); + break; - if (rel_top > rel_bottom || *left > *right) - return false; + case 'e': + case 'B': + term_cursor_down(term, vt_param_get(term, 0, 1)); + break; - *top = term_row_rel_to_abs(term, rel_top); - *bottom = term_row_rel_to_abs(term, rel_bottom); + case 'a': + case 'C': + term_cursor_right(term, vt_param_get(term, 0, 1)); + break; - return true; -} + case 'D': + term_cursor_left(term, vt_param_get(term, 0, 1)); + break; -void -csi_dispatch(struct terminal *term, uint8_t final) -{ - LOG_DBG("%s (%08x)", csi_as_string(term, final, -1), term->vt.private); + case 'E': + /* CNL - Cursor Next Line */ + term_cursor_down(term, vt_param_get(term, 0, 1)); + term_cursor_left(term, term->grid->cursor.point.col); + break; - switch (term->vt.private) { - case 0: { - switch (final) { - case 'b': - if (term->vt.last_printed != 0) { - /* - * Note: we never reset 'last-printed'. According to - * ECMA-48, the behaviour is undefined if REP was - * _not_ preceded by a graphical character. - */ - int count = vt_param_get(term, 0, 1); - LOG_DBG("REP: '%lc' %d times", (wint_t)term->vt.last_printed, count); + case 'F': + /* CPL - Cursor Previous Line */ + term_cursor_up(term, vt_param_get(term, 0, 1)); + term_cursor_left(term, term->grid->cursor.point.col); + break; - int width; - - if (term->vt.last_printed >= CELL_COMB_CHARS_LO) { - const struct composed *comp = composed_lookup( - term->composed, term->vt.last_printed - CELL_COMB_CHARS_LO); - - xassert(comp != NULL); - width = comp->forced_width > 0 ? comp->forced_width : comp->width; - } else - width = c32width(term->vt.last_printed); - - if (width > 0) { - for (int i = 0; i < count; i++) - term_print(term, term->vt.last_printed, width, false); - } - } - break; - - case 'c': { - if (vt_param_get(term, 0, 0) != 0) { - UNHANDLED(); - break; - } - - /* Send Device Attributes (Primary DA) */ - - /* - * Responses: - * - CSI?1;2c vt100 with advanced video option - * - CSI?1;0c vt101 with no options - * - CSI?6c vt102 - * - CSI?62;c vt220 - * - CSI?63;c vt320 - * - CSI?64;c vt420 - * - * Ps (response may contain multiple): - * - 1 132 columns - * - 2 Printer. - * - 3 ReGIS graphics. - * - 4 Sixel graphics. - * - 6 Selective erase. - * - 8 User-defined keys. - * - 9 National Replacement Character sets. - * - 15 Technical characters. - * - 16 Locator port. - * - 17 Terminal state interrogation. - * - 18 User windows. - * - 21 Horizontal scrolling. - * - 22 ANSI color, e.g., VT525. - * - 28 Rectangular editing. - * - 29 ANSI text locator (i.e., DEC Locator mode). - * - 52 Clipboard access - * - * Note: we report ourselves as a VT220, mainly to be able - * to pass parameters, to indicate we support sixel, and - * ANSI colors. - * - * The VT level must be synchronized with the secondary DA - * response. - * - * Note: tertiary DA responds with "FOOT". - */ - char reply[32]; - - int len = snprintf( - reply, sizeof(reply), "\033[?62%s;22;28%sc", - term->conf->tweak.sixel ? ";4" : "", - (term->conf->security.osc52 == OSC52_ENABLED || - term->conf->security.osc52 == OSC52_COPY_ENABLED ? ";52" : "")); - - term_to_slave(term, reply, len); + case 'g': { + int param = vt_param_get(term, 0, 0); + switch (param) { + case 0: + /* Clear tab stop at *current* column */ + tll_foreach(term->tab_stops, it) { + if (it->item == term->grid->cursor.point.col) + tll_remove(term->tab_stops, it); + else if (it->item > term->grid->cursor.point.col) break; } - case 'd': { - /* VPA - vertical line position absolute */ - int rel_row = vt_param_get(term, 0, 1) - 1; - int row = term_row_rel_to_abs(term, rel_row); - term_cursor_to(term, row, term->grid->cursor.point.col); + break; + + case 3: + /* Clear *all* tabs */ + tll_free(term->tab_stops); + break; + + default: + UNHANDLED(); + break; + } + break; + } + + case '`': + case 'G': { + /* Cursor horizontal absolute */ + int col = min(vt_param_get(term, 0, 1), term->cols) - 1; + term_cursor_col(term, col); + break; + } + + case 'f': + case 'H': { + /* Move cursor */ + int rel_row = vt_param_get(term, 0, 1) - 1; + int row = term_row_rel_to_abs(term, rel_row); + int col = min(vt_param_get(term, 1, 1), term->cols) - 1; + term_cursor_to(term, row, col); + break; + } + + case 'J': { + /* Erase screen */ + + int param = vt_param_get(term, 0, 0); + switch (param) { + case 0: { + /* From cursor to end of screen */ + const struct coord *cursor = &term->grid->cursor.point; + term_erase(term, cursor->row, cursor->col, term->rows - 1, + term->cols - 1); + term->grid->cursor.lcf = false; + break; + } + + case 1: { + /* From start of screen to cursor */ + const struct coord *cursor = &term->grid->cursor.point; + term_erase(term, 0, 0, cursor->row, cursor->col); + term->grid->cursor.lcf = false; + break; + } + + case 2: + /* Erase entire screen */ + term_erase(term, 0, 0, term->rows - 1, term->cols - 1); + term->grid->cursor.lcf = false; + break; + + case 3: { + /* Erase scrollback */ + term_erase_scrollback(term); + break; + } + + default: + UNHANDLED(); + break; + } + break; + } + + case 'K': { + /* Erase line */ + + int param = vt_param_get(term, 0, 0); + switch (param) { + case 0: { + /* From cursor to end of line */ + const struct coord *cursor = &term->grid->cursor.point; + term_erase(term, cursor->row, cursor->col, cursor->row, term->cols - 1); + term->grid->cursor.lcf = false; + break; + } + + case 1: { + /* From start of line to cursor */ + const struct coord *cursor = &term->grid->cursor.point; + term_erase(term, cursor->row, 0, cursor->row, cursor->col); + term->grid->cursor.lcf = false; + break; + } + + case 2: { + /* Entire line */ + const struct coord *cursor = &term->grid->cursor.point; + term_erase(term, cursor->row, 0, cursor->row, term->cols - 1); + term->grid->cursor.lcf = false; + break; + } + + default: + UNHANDLED(); + break; + } + + break; + } + + case 'L': { /* IL */ + if (term->grid->cursor.point.row < term->scroll_region.start || + term->grid->cursor.point.row >= term->scroll_region.end) + break; + + int count = min(vt_param_get(term, 0, 1), + term->scroll_region.end - term->grid->cursor.point.row); + + term_scroll_reverse_partial( + term, + (struct scroll_region){.start = term->grid->cursor.point.row, + .end = term->scroll_region.end}, + count); + term->grid->cursor.lcf = false; + term->grid->cursor.point.col = 0; + break; + } + + case 'M': { /* DL */ + if (term->grid->cursor.point.row < term->scroll_region.start || + term->grid->cursor.point.row >= term->scroll_region.end) + break; + + int count = min(vt_param_get(term, 0, 1), + term->scroll_region.end - term->grid->cursor.point.row); + + term_scroll_partial( + term, + (struct scroll_region){.start = term->grid->cursor.point.row, + .end = term->scroll_region.end}, + count); + term->grid->cursor.lcf = false; + term->grid->cursor.point.col = 0; + break; + } + + case 'P': { + /* DCH: Delete character(s) */ + + /* Number of characters to delete */ + int count = min(vt_param_get(term, 0, 1), + term->cols - term->grid->cursor.point.col); + + /* Number of characters left after deletion (on current line) */ + int remaining = term->cols - (term->grid->cursor.point.col + count); + + /* 'Delete' characters by moving the remaining ones */ + memmove(&term->grid->cur_row->cells[term->grid->cursor.point.col], + &term->grid->cur_row->cells[term->grid->cursor.point.col + count], + remaining * sizeof(term->grid->cur_row->cells[0])); + + for (size_t c = 0; c < remaining; c++) + term->grid->cur_row->cells[term->grid->cursor.point.col + c] + .attrs.clean = 0; + term->grid->cur_row->dirty = true; + + /* Erase the remainder of the line */ + const struct coord *cursor = &term->grid->cursor.point; + term_erase(term, cursor->row, cursor->col + remaining, cursor->row, + term->cols - 1); + term->grid->cursor.lcf = false; + break; + } + + case '@': { + /* ICH: insert character(s) */ + + /* Number of characters to insert */ + int count = min(vt_param_get(term, 0, 1), + term->cols - term->grid->cursor.point.col); + + /* Characters to move */ + int remaining = term->cols - (term->grid->cursor.point.col + count); + + /* Push existing characters */ + memmove(&term->grid->cur_row->cells[term->grid->cursor.point.col + count], + &term->grid->cur_row->cells[term->grid->cursor.point.col], + remaining * sizeof(term->grid->cur_row->cells[0])); + for (size_t c = 0; c < remaining; c++) + term->grid->cur_row->cells[term->grid->cursor.point.col + count + c] + .attrs.clean = 0; + term->grid->cur_row->dirty = true; + + /* Erase (insert space characters) */ + const struct coord *cursor = &term->grid->cursor.point; + term_erase(term, cursor->row, cursor->col, cursor->row, + cursor->col + count - 1); + term->grid->cursor.lcf = false; + break; + } + + case 'S': { + const struct scroll_region *r = &term->scroll_region; + int amount = min(vt_param_get(term, 0, 1), r->end - r->start); + term_scroll(term, amount); + break; + } + + case 'T': { + const struct scroll_region *r = &term->scroll_region; + int amount = min(vt_param_get(term, 0, 1), r->end - r->start); + term_scroll_reverse(term, amount); + break; + } + + case 'X': { + /* Erase chars */ + int count = min(vt_param_get(term, 0, 1), + term->cols - term->grid->cursor.point.col); + + const struct coord *cursor = &term->grid->cursor.point; + term_erase(term, cursor->row, cursor->col, cursor->row, + cursor->col + count - 1); + term->grid->cursor.lcf = false; + break; + } + + case 'I': { + /* CHT - Tab Forward (param is number of tab stops to move through) */ + for (int i = 0; i < vt_param_get(term, 0, 1); i++) { + int new_col = term->cols - 1; + tll_foreach(term->tab_stops, it) { + if (it->item > term->grid->cursor.point.col) { + new_col = it->item; break; + } + } + xassert(new_col >= term->grid->cursor.point.col); + + bool lcf = term->grid->cursor.lcf; + term_cursor_right(term, new_col - term->grid->cursor.point.col); + term->grid->cursor.lcf = lcf; + } + break; + } + + case 'Z': + /* CBT - Back tab (param is number of tab stops to move back through) */ + for (int i = 0; i < vt_param_get(term, 0, 1); i++) { + int new_col = 0; + tll_rforeach(term->tab_stops, it) { + if (it->item < term->grid->cursor.point.col) { + new_col = it->item; + break; + } + } + xassert(term->grid->cursor.point.col >= new_col); + term_cursor_left(term, term->grid->cursor.point.col - new_col); + } + break; + + case 'h': + case 'l': { + /* Set/Reset Mode (SM/RM) */ + int param = vt_param_get(term, 0, 0); + bool sm = final == 'h'; + if (param == 4) { + /* Insertion Replacement Mode (IRM) */ + term->insert_mode = sm; + term->bits_affecting_ascii_printer.insert_mode = sm; + term_update_ascii_printer(term); + break; + } + + /* + * ECMA-48 defines modes 1-22, all of which were optional + * (§7.1; "may have one state only") and are considered + * deprecated (§7.1) in the latest (5th) edition. xterm only + * documents modes 2, 4, 12 and 20, the last of which was + * outright removed (§8.3.106) in 5th edition ECMA-48. + */ + if (sm) { + LOG_WARN("SM with unimplemented mode: %d", param); + } + break; + } + + case 'r': { + int start = vt_param_get(term, 0, 1); + int end = min(vt_param_get(term, 1, term->rows), term->rows); + + if (end > start) { + + /* 1-based */ + term->scroll_region.start = start - 1; + term->scroll_region.end = end; + term_cursor_home(term); + + LOG_DBG("scroll region: %d-%d", term->scroll_region.start, + term->scroll_region.end); + } + break; + } + + case 's': + term_save_cursor(term); + break; + + case 'u': + term_restore_cursor(term, &term->grid->saved_cursor); + break; + + case 't': { + /* + * Window operations + */ + + const unsigned param = vt_param_get(term, 0, 0); + + switch (param) { + case 1: + LOG_WARN("unimplemented: de-iconify"); + break; + case 2: + LOG_WARN("unimplemented: iconify"); + break; + case 3: + LOG_WARN("unimplemented: move window to pixel position"); + break; + case 4: + LOG_WARN("unimplemented: resize window in pixels"); + break; + case 5: + LOG_WARN("unimplemented: raise window to front of stack"); + break; + case 6: + LOG_WARN("unimplemented: raise window to back of stack"); + break; + case 7: + LOG_WARN("unimplemented: refresh window"); + break; + case 8: + LOG_WARN("unimplemented: resize window in chars"); + break; + case 9: + LOG_WARN("unimplemented: maximize/unmaximize window"); + break; + case 10: + LOG_WARN("unimplemented: to/from full screen"); + break; + case 20: + LOG_WARN("unimplemented: report icon label"); + break; + case 24: + LOG_WARN("unimplemented: resize window (DECSLPP)"); + break; + + case 11: /* report if window is iconified */ + /* We don't know - always report *not* iconified */ + /* 1=not iconified, 2=iconified */ + term_to_slave(term, "\033[1t", 4); + break; + + case 13: { /* report window position */ + /* We don't know our position - always report (0,0) */ + static const char reply[] = "\033[3;0;0t"; + switch (vt_param_get(term, 1, 0)) { + case 0: /* window position */ + case 2: /* text area position */ + term_to_slave(term, reply, sizeof(reply) - 1); + break; + + default: + UNHANDLED(); + break; } - case 'm': - csi_sgr(term); - break; + break; + } - case 'A': - term_cursor_up(term, vt_param_get(term, 0, 1)); - break; + case 14: { /* report window size in pixels */ + int width = -1; + int height = -1; - case 'e': - case 'B': - term_cursor_down(term, vt_param_get(term, 0, 1)); - break; + switch (vt_param_get(term, 1, 0)) { + case 0: + /* text area size */ + width = term->width - term->margins.left - term->margins.right; + height = term->height - term->margins.top - term->margins.bottom; + break; - case 'a': - case 'C': - term_cursor_right(term, vt_param_get(term, 0, 1)); - break; + case 2: + /* window size */ + width = term->width; + height = term->height; + break; - case 'D': - term_cursor_left(term, vt_param_get(term, 0, 1)); - break; - - case 'E': - /* CNL - Cursor Next Line */ - term_cursor_down(term, vt_param_get(term, 0, 1)); - term_cursor_left(term, term->grid->cursor.point.col); - break; - - case 'F': - /* CPL - Cursor Previous Line */ - term_cursor_up(term, vt_param_get(term, 0, 1)); - term_cursor_left(term, term->grid->cursor.point.col); - break; - - case 'g': { - int param = vt_param_get(term, 0, 0); - switch (param) { - case 0: - /* Clear tab stop at *current* column */ - tll_foreach(term->tab_stops, it) { - if (it->item == term->grid->cursor.point.col) - tll_remove(term->tab_stops, it); - else if (it->item > term->grid->cursor.point.col) - break; - } - - break; - - case 3: - /* Clear *all* tabs */ - tll_free(term->tab_stops); - break; - - default: - UNHANDLED(); - break; - } - break; + default: + UNHANDLED(); + break; } - case '`': - case 'G': { - /* Cursor horizontal absolute */ - int col = min(vt_param_get(term, 0, 1), term->cols) - 1; - term_cursor_col(term, col); - break; + if (width >= 0 && height >= 0) { + char reply[64]; + size_t n = + xsnprintf(reply, sizeof(reply), "\033[4;%d;%dt", height, width); + term_to_slave(term, reply, n); + } + break; + } + + case 15: /* report screen size in pixels */ + tll_foreach(term->window->on_outputs, it) { + char reply[64]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[5;%d;%dt", + it->item->dim.px_real.height, + it->item->dim.px_real.width); + term_to_slave(term, reply, n); + break; } - case 'f': - case 'H': { - /* Move cursor */ - int rel_row = vt_param_get(term, 0, 1) - 1; - int row = term_row_rel_to_abs(term, rel_row); - int col = min(vt_param_get(term, 1, 1), term->cols) - 1; - term_cursor_to(term, row, col); - break; + if (tll_length(term->window->on_outputs) == 0) + term_to_slave(term, "\033[5;0;0t", 8); + break; + + case 16: { /* report cell size in pixels */ + char reply[64]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[6;%d;%dt", + term->cell_height, term->cell_width); + term_to_slave(term, reply, n); + break; + } + + case 18: { /* text area size in chars */ + char reply[64]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[8;%d;%dt", term->rows, + term->cols); + term_to_slave(term, reply, n); + break; + } + + case 19: { /* report screen size in chars */ + tll_foreach(term->window->on_outputs, it) { + char reply[64]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[9;%d;%dt", + it->item->dim.px_real.height / term->cell_height, + it->item->dim.px_real.width / term->cell_width); + term_to_slave(term, reply, n); + break; } - case 'J': { - /* Erase screen */ + if (tll_length(term->window->on_outputs) == 0) + term_to_slave(term, "\033[9;0;0t", 8); + break; + } - int param = vt_param_get(term, 0, 0); - switch (param) { - case 0: { - /* From cursor to end of screen */ - const struct coord *cursor = &term->grid->cursor.point; - term_erase( - term, - cursor->row, cursor->col, - term->rows - 1, term->cols - 1); - term->grid->cursor.lcf = false; - break; - } - - case 1: { - /* From start of screen to cursor */ - const struct coord *cursor = &term->grid->cursor.point; - term_erase(term, 0, 0, cursor->row, cursor->col); - term->grid->cursor.lcf = false; - break; - } - - case 2: - /* Erase entire screen */ - term_erase(term, 0, 0, term->rows - 1, term->cols - 1); - term->grid->cursor.lcf = false; - break; - - case 3: { - /* Erase scrollback */ - term_erase_scrollback(term); - break; - } - - default: - UNHANDLED(); - break; - } - break; - } - - case 'K': { - /* Erase line */ - - int param = vt_param_get(term, 0, 0); - switch (param) { - case 0: { - /* From cursor to end of line */ - const struct coord *cursor = &term->grid->cursor.point; - term_erase( - term, - cursor->row, cursor->col, - cursor->row, term->cols - 1); - term->grid->cursor.lcf = false; - break; - } - - case 1: { - /* From start of line to cursor */ - const struct coord *cursor = &term->grid->cursor.point; - term_erase(term, cursor->row, 0, cursor->row, cursor->col); - term->grid->cursor.lcf = false; - break; - } - - case 2: { - /* Entire line */ - const struct coord *cursor = &term->grid->cursor.point; - term_erase(term, cursor->row, 0, cursor->row, term->cols - 1); - term->grid->cursor.lcf = false; - break; - } - - default: - UNHANDLED(); - break; - } - - break; - } - - case 'L': { /* IL */ - if (term->grid->cursor.point.row < term->scroll_region.start || - term->grid->cursor.point.row >= term->scroll_region.end) - break; - - int count = min( - vt_param_get(term, 0, 1), - term->scroll_region.end - term->grid->cursor.point.row); - - term_scroll_reverse_partial( - term, - (struct scroll_region){ - .start = term->grid->cursor.point.row, - .end = term->scroll_region.end}, - count); - term->grid->cursor.lcf = false; - term->grid->cursor.point.col = 0; - break; - } - - case 'M': { /* DL */ - if (term->grid->cursor.point.row < term->scroll_region.start || - term->grid->cursor.point.row >= term->scroll_region.end) - break; - - int count = min( - vt_param_get(term, 0, 1), - term->scroll_region.end - term->grid->cursor.point.row); - - term_scroll_partial( - term, - (struct scroll_region){ - .start = term->grid->cursor.point.row, - .end = term->scroll_region.end}, - count); - term->grid->cursor.lcf = false; - term->grid->cursor.point.col = 0; - break; - } - - case 'P': { - /* DCH: Delete character(s) */ - - /* Number of characters to delete */ - int count = min( - vt_param_get(term, 0, 1), term->cols - term->grid->cursor.point.col); - - /* Number of characters left after deletion (on current line) */ - int remaining = term->cols - (term->grid->cursor.point.col + count); - - /* 'Delete' characters by moving the remaining ones */ - memmove(&term->grid->cur_row->cells[term->grid->cursor.point.col], - &term->grid->cur_row->cells[term->grid->cursor.point.col + count], - remaining * sizeof(term->grid->cur_row->cells[0])); - - for (size_t c = 0; c < remaining; c++) - term->grid->cur_row->cells[term->grid->cursor.point.col + c].attrs.clean = 0; - term->grid->cur_row->dirty = true; - - /* Erase the remainder of the line */ - const struct coord *cursor = &term->grid->cursor.point; - term_erase( - term, - cursor->row, cursor->col + remaining, - cursor->row, term->cols - 1); - term->grid->cursor.lcf = false; - break; - } - - case '@': { - /* ICH: insert character(s) */ - - /* Number of characters to insert */ - int count = min( - vt_param_get(term, 0, 1), term->cols - term->grid->cursor.point.col); - - /* Characters to move */ - int remaining = term->cols - (term->grid->cursor.point.col + count); - - /* Push existing characters */ - memmove(&term->grid->cur_row->cells[term->grid->cursor.point.col + count], - &term->grid->cur_row->cells[term->grid->cursor.point.col], - remaining * sizeof(term->grid->cur_row->cells[0])); - for (size_t c = 0; c < remaining; c++) - term->grid->cur_row->cells[term->grid->cursor.point.col + count + c].attrs.clean = 0; - term->grid->cur_row->dirty = true; - - /* Erase (insert space characters) */ - const struct coord *cursor = &term->grid->cursor.point; - term_erase( - term, - cursor->row, cursor->col, - cursor->row, cursor->col + count - 1); - term->grid->cursor.lcf = false; - break; - } - - case 'S': { - const struct scroll_region *r = &term->scroll_region; - int amount = min(vt_param_get(term, 0, 1), r->end - r->start); - term_scroll(term, amount); - break; - } - - case 'T': { - const struct scroll_region *r = &term->scroll_region; - int amount = min(vt_param_get(term, 0, 1), r->end - r->start); - term_scroll_reverse(term, amount); - break; - } - - case 'X': { - /* Erase chars */ - int count = min( - vt_param_get(term, 0, 1), term->cols - term->grid->cursor.point.col); - - const struct coord *cursor = &term->grid->cursor.point; - term_erase( - term, - cursor->row, cursor->col, - cursor->row, cursor->col + count - 1); - term->grid->cursor.lcf = false; - break; - } - - case 'I': { - /* CHT - Tab Forward (param is number of tab stops to move through) */ - for (int i = 0; i < vt_param_get(term, 0, 1); i++) { - int new_col = term->cols - 1; - tll_foreach(term->tab_stops, it) { - if (it->item > term->grid->cursor.point.col) { - new_col = it->item; - break; - } - } - xassert(new_col >= term->grid->cursor.point.col); - - bool lcf = term->grid->cursor.lcf; - term_cursor_right(term, new_col - term->grid->cursor.point.col); - term->grid->cursor.lcf = lcf; - } - break; - } - - case 'Z': - /* CBT - Back tab (param is number of tab stops to move back through) */ - for (int i = 0; i < vt_param_get(term, 0, 1); i++) { - int new_col = 0; - tll_rforeach(term->tab_stops, it) { - if (it->item < term->grid->cursor.point.col) { - new_col = it->item; - break; - } - } - xassert(term->grid->cursor.point.col >= new_col); - term_cursor_left(term, term->grid->cursor.point.col - new_col); - } - break; - - case 'h': - case 'l': { - /* Set/Reset Mode (SM/RM) */ - int param = vt_param_get(term, 0, 0); - bool sm = final == 'h'; - if (param == 4) { - /* Insertion Replacement Mode (IRM) */ - term->insert_mode = sm; - term->bits_affecting_ascii_printer.insert_mode = sm; - term_update_ascii_printer(term); - break; - } - - /* - * ECMA-48 defines modes 1-22, all of which were optional - * (§7.1; "may have one state only") and are considered - * deprecated (§7.1) in the latest (5th) edition. xterm only - * documents modes 2, 4, 12 and 20, the last of which was - * outright removed (§8.3.106) in 5th edition ECMA-48. - */ - if (sm) { - LOG_WARN("SM with unimplemented mode: %d", param); - } - break; - } - - case 'r': { - int start = vt_param_get(term, 0, 1); - int end = min(vt_param_get(term, 1, term->rows), term->rows); - - if (end > start) { - - /* 1-based */ - term->scroll_region.start = start - 1; - term->scroll_region.end = end; - term_cursor_home(term); - - LOG_DBG("scroll region: %d-%d", - term->scroll_region.start, - term->scroll_region.end); - } - break; - } - - case 's': - term_save_cursor(term); - break; - - case 'u': - term_restore_cursor(term, &term->grid->saved_cursor); - break; - - case 't': { - /* - * Window operations - */ - - const unsigned param = vt_param_get(term, 0, 0); - - switch (param) { - case 1: LOG_WARN("unimplemented: de-iconify"); break; - case 2: LOG_WARN("unimplemented: iconify"); break; - case 3: LOG_WARN("unimplemented: move window to pixel position"); break; - case 4: LOG_WARN("unimplemented: resize window in pixels"); break; - case 5: LOG_WARN("unimplemented: raise window to front of stack"); break; - case 6: LOG_WARN("unimplemented: raise window to back of stack"); break; - case 7: LOG_WARN("unimplemented: refresh window"); break; - case 8: LOG_WARN("unimplemented: resize window in chars"); break; - case 9: LOG_WARN("unimplemented: maximize/unmaximize window"); break; - case 10: LOG_WARN("unimplemented: to/from full screen"); break; - case 20: LOG_WARN("unimplemented: report icon label"); break; - case 24: LOG_WARN("unimplemented: resize window (DECSLPP)"); break; - - case 11: /* report if window is iconified */ - /* We don't know - always report *not* iconified */ - /* 1=not iconified, 2=iconified */ - term_to_slave(term, "\033[1t", 4); - break; - - case 13: { /* report window position */ - /* We don't know our position - always report (0,0) */ - static const char reply[] = "\033[3;0;0t"; - switch (vt_param_get(term, 1, 0)) { - case 0: /* window position */ - case 2: /* text area position */ - term_to_slave(term, reply, sizeof(reply) - 1); - break; - - default: - UNHANDLED(); - break; - } - - break; - } - - case 14: { /* report window size in pixels */ - int width = -1; - int height = -1; - - switch (vt_param_get(term, 1, 0)) { - case 0: - /* text area size */ - width = term->width - term->margins.left - term->margins.right; - height = term->height - term->margins.top - term->margins.bottom; - break; - - case 2: - /* window size */ - width = term->width; - height = term->height; - break; - - default: - UNHANDLED(); - break; - } - - if (width >= 0 && height >= 0) { - char reply[64]; - size_t n = xsnprintf( - reply, sizeof(reply), "\033[4;%d;%dt", height, width); - term_to_slave(term, reply, n); - } - break; - } - - case 15: /* report screen size in pixels */ - tll_foreach(term->window->on_outputs, it) { - char reply[64]; - size_t n = xsnprintf(reply, sizeof(reply), "\033[5;%d;%dt", - it->item->dim.px_real.height, - it->item->dim.px_real.width); - term_to_slave(term, reply, n); - break; - } - - if (tll_length(term->window->on_outputs) == 0) - term_to_slave(term, "\033[5;0;0t", 8); - break; - - case 16: { /* report cell size in pixels */ - char reply[64]; - size_t n = xsnprintf( - reply, sizeof(reply), "\033[6;%d;%dt", - term->cell_height, term->cell_width); - term_to_slave(term, reply, n); - break; - } - - case 18: { /* text area size in chars */ - char reply[64]; - size_t n = xsnprintf(reply, sizeof(reply), "\033[8;%d;%dt", - term->rows, term->cols); - term_to_slave(term, reply, n); - break; - } - - case 19: { /* report screen size in chars */ - tll_foreach(term->window->on_outputs, it) { - char reply[64]; - size_t n = xsnprintf( - reply, sizeof(reply), "\033[9;%d;%dt", - it->item->dim.px_real.height / term->cell_height, - it->item->dim.px_real.width / term->cell_width); - term_to_slave(term, reply, n); - break; - } - - if (tll_length(term->window->on_outputs) == 0) - term_to_slave(term, "\033[9;0;0t", 8); - break; - } - - case 21: { -#if 0 /* Disabled for now, see #1894 */ + case 21: { +#if 0 /* Disabled for now, see #1894 */ char reply[3 + strlen(term->window_title) + 2 + 1]; int chars = xsnprintf( reply, sizeof(reply), "\033]l%s\033\\", term->window_title); term_to_slave(term, reply, chars); #else - LOG_WARN("CSI 21 t (report window title) ignored"); + LOG_WARN("CSI 21 t (report window title) ignored"); #endif - break; - } + break; + } - case 22: { /* push window title */ - /* 0 - icon + title, 1 - icon, 2 - title */ - unsigned what = vt_param_get(term, 1, 0); - if (what == 0 || what == 2) { - tll_push_back( - term->window_title_stack, xstrdup(term->window_title)); - } - break; - } - - case 23: { /* pop window title */ - /* 0 - icon + title, 1 - icon, 2 - title */ - unsigned what = vt_param_get(term, 1, 0); - if (what == 0 || what == 2) { - if (tll_length(term->window_title_stack) > 0) { - char *title = tll_pop_back(term->window_title_stack); - term_set_window_title(term, title); - free(title); - } - } - break; - } - - case 1001: { - } - - default: - LOG_DBG("ignoring %s", csi_as_string(term, final, -1)); - break; - } - break; + case 22: { /* push window title */ + /* 0 - icon + title, 1 - icon, 2 - title */ + unsigned what = vt_param_get(term, 1, 0); + if (what == 0 || what == 2) { + tll_push_back(term->window_title_stack, + xstrdup(term->window_title != NULL + ? term->window_title + : "")); } + break; + } - case 'n': { - if (term->vt.params.idx > 0) { - int param = vt_param_get(term, 0, 0); - switch (param) { - case 5: - /* Query device status */ - term_to_slave(term, "\x1b[0n", 4); /* "Device OK" */ - break; + case 23: { /* pop window title */ + /* 0 - icon + title, 1 - icon, 2 - title */ + unsigned what = vt_param_get(term, 1, 0); + if (what == 0 || what == 2) { + if (tll_length(term->window_title_stack) > 0) { + char *title = tll_pop_back(term->window_title_stack); + term_set_window_title(term, title); + free(title); + } + } + break; + } - case 6: { - /* u7 - cursor position query */ + case 1001: { + } - int row = term->origin == ORIGIN_ABSOLUTE - ? term->grid->cursor.point.row - : term->grid->cursor.point.row - term->scroll_region.start; + default: + LOG_DBG("ignoring %s", csi_as_string(term, final, -1)); + break; + } + break; + } - /* TODO: we use 0-based position, while the xterm - * terminfo says the receiver of the reply should - * decrement, hence we must add 1 */ - char reply[64]; - size_t n = xsnprintf(reply, sizeof(reply), "\x1b[%d;%dR", - row + 1, term->grid->cursor.point.col + 1); - term_to_slave(term, reply, n); - break; - } + case 'n': { + if (term->vt.params.idx > 0) { + int param = vt_param_get(term, 0, 0); + switch (param) { + case 5: + /* Query device status */ + term_to_slave(term, "\x1b[0n", 4); /* "Device OK" */ + break; - default: - UNHANDLED(); - break; - } - } else - UNHANDLED(); + case 6: { + /* u7 - cursor position query */ - break; + int row = + term->origin == ORIGIN_ABSOLUTE + ? term->grid->cursor.point.row + : term->grid->cursor.point.row - term->scroll_region.start; + + /* TODO: we use 0-based position, while the xterm + * terminfo says the receiver of the reply should + * decrement, hence we must add 1 */ + char reply[64]; + size_t n = xsnprintf(reply, sizeof(reply), "\x1b[%d;%dR", row + 1, + term->grid->cursor.point.col + 1); + term_to_slave(term, reply, n); + break; } default: - UNHANDLED(); - break; + UNHANDLED(); + break; } - - break; /* private[0] == 0 */ - } - - case '?': { - switch (final) { - case 'h': - /* DECSET - DEC private mode set */ - for (size_t i = 0; i < term->vt.params.idx; i++) - decset(term, term->vt.params.v[i].value); - break; - - case 'l': - /* DECRST - DEC private mode reset */ - for (size_t i = 0; i < term->vt.params.idx; i++) - decrst(term, term->vt.params.v[i].value); - break; - - case 's': - for (size_t i = 0; i < term->vt.params.idx; i++) - xtsave(term, term->vt.params.v[i].value); - break; - - case 'r': - for (size_t i = 0; i < term->vt.params.idx; i++) - xtrestore(term, term->vt.params.v[i].value); - break; - - case 'S': { - if (!term->conf->tweak.sixel) { - UNHANDLED(); - break; - } - - unsigned target = vt_param_get(term, 0, 0); - unsigned operation = vt_param_get(term, 1, 0); - - switch (target) { - case 1: - switch (operation) { - case 1: sixel_colors_report_current(term); break; - case 2: sixel_colors_reset(term); break; - case 3: sixel_colors_set(term, vt_param_get(term, 2, 0)); break; - case 4: sixel_colors_report_max(term); break; - default: UNHANDLED(); break; - } - break; - - case 2: - switch (operation) { - case 1: sixel_geometry_report_current(term); break; - case 2: sixel_geometry_reset(term); break; - case 3: sixel_geometry_set(term, vt_param_get(term, 2, 0), vt_param_get(term, 3, 0)); break; - case 4: sixel_geometry_report_max(term); break; - default: UNHANDLED(); break; - } - break; - - default: - UNHANDLED(); - break; - } - - break; - } - - case 'm': { - int resource = vt_param_get(term, 0, 0); - int value = -1; - - switch (resource) { - case 0: /* modifyKeyboard */ - value = 0; - break; - - case 1: /* modifyCursorKeys */ - case 2: /* modifyFunctionKeys */ - value = 1; - break; - - case 4: /* modifyOtherKeys */ - value = term->modify_other_keys_2 ? 2 : 1; - break; - - default: - LOG_WARN("XTQMODKEYS: invalid resource '%d' in '%s'", - resource, csi_as_string(term, final, -1)); - break; - } - - if (value >= 0) { - char reply[16] = {0}; - int chars = snprintf(reply, sizeof(reply), - "\033[>%d;%dm", resource, value); - term_to_slave(term, reply, chars); - } - break; - } - - case 'n': { - const int param = vt_param_get(term, 0, 0); - - switch (param) { - case 996: { /* Query current theme mode (see private mode 2031) */ - /* - * 1 - dark mode - * 2 - light mode - * - * In foot, the themes aren't necessarily light/dark, - * but by convention, the primary theme is dark, and - * the alternative theme is light. - */ - char reply[16] = {0}; - int chars = snprintf( - reply, sizeof(reply), - "\033[?997;%dn", - term->colors.active_theme == COLOR_THEME_DARK ? 1 : 2); - - term_to_slave(term, reply, chars); - break; - } - } - break; - } - - case 'p': { - /* - * Request status of ECMA-48/"ANSI" private mode (DECRQM - * for SM/RM modes; see private="?$" case further below for - * DECSET/DECRST modes) - */ - unsigned param = vt_param_get(term, 0, 0); - unsigned status = DECRPM_NOT_RECOGNIZED; - if (param == 4) { - status = decrpm(term->insert_mode); - } - char reply[32]; - size_t n = xsnprintf(reply, sizeof(reply), "\033[%u;%u$y", param, status); - term_to_slave(term, reply, n); - break; - } - - case 'u': { - enum kitty_kbd_flags flags = - term->grid->kitty_kbd.flags[term->grid->kitty_kbd.idx]; - - char reply[8]; - int chars = snprintf(reply, sizeof(reply), "\033[?%uu", flags); - term_to_slave(term, reply, chars); - break; - } - - default: - UNHANDLED(); - break; - } - - break; /* private[0] == '?' */ - } - - case '>': { - switch (final) { - case 'c': - /* Send Device Attributes (Secondary DA) */ - if (vt_param_get(term, 0, 0) != 0) { - UNHANDLED(); - break; - } - - /* - * Param 1 - terminal type: - * 0 - vt100 - * 1 - vt220 - * 2 - vt240 - * 18 - vt330 - * 19 - vt340 - * 24 - vt320 - * 41 - vt420 - * 61 - vt510 - * 64 - vt520 - * 65 - vt525 - * - * Param 2 - firmware version xterm uses its version - * number. We do to, in the format "MAJORMINORPATCH", - * where all three version numbers are always two - * digits. So e.g. 1.25.0 is reported as 012500. - * - * We report ourselves as a VT220. This must be - * synchronized with the primary DA response. - * - * Note: tertiary DA replies with "FOOT". - */ - - static_assert(FOOT_MAJOR < 100, "Major version must not exceed 99"); - static_assert(FOOT_MINOR < 100, "Minor version must not exceed 99"); - static_assert(FOOT_PATCH < 100, "Patch version must not exceed 99"); - - char reply[64]; - size_t n = xsnprintf(reply, sizeof(reply), "\033[>1;%02u%02u%02u;0c", - FOOT_MAJOR, FOOT_MINOR, FOOT_PATCH); - - term_to_slave(term, reply, n); - break; - - case 'm': - if (term->vt.params.idx == 0) { - /* Reset all */ - } else { - int resource = vt_param_get(term, 0, 0); - int value = vt_param_get(term, 1, -1); - - switch (resource) { - case 0: /* modifyKeyboard */ - break; - - case 1: /* modifyCursorKeys */ - case 2: /* modifyFunctionKeys */ - /* Ignored, we always report modifiers */ - if (value != 2 && value != -1) { - LOG_WARN( - "unimplemented: %s = %d", - resource == 1 ? "modifyCursorKeys" : - resource == 2 ? "modifyFunctionKeys" : "", - value); - } - break; - - case 4: /* modifyOtherKeys */ - term->modify_other_keys_2 = value == 2; - LOG_DBG("modifyOtherKeys=%d", value); - break; - - default: - LOG_WARN("XTMODKEYS: invalid resource '%d' in '%s'", - resource, csi_as_string(term, final, -1)); - break; - } - } - break; /* final == 'm' */ - - case 'n': { - int resource = vt_param_get(term, 0, 2); /* Default is modifyFunctionKeys */ - switch (resource) { - case 0: /* modifyKeyboard */ - case 1: /* modifyCursorKeys */ - case 2: /* modifyFunctionKeys */ - break; - - case 4: /* modifyOtherKeys */ - /* We don't support fully disabling modifyOtherKeys, - * but simply revert back to mode '1' */ - term->modify_other_keys_2 = false; - LOG_DBG("modifyOtherKeys=1"); - break; - } - break; - } - - case 'u': { - int flags = vt_param_get(term, 0, 0) & KITTY_KBD_SUPPORTED; - - struct grid *grid = term->grid; - uint8_t idx = grid->kitty_kbd.idx; - - if (idx + 1 >= ALEN(grid->kitty_kbd.flags)) { - /* Stack full, evict oldest by wrapping around */ - idx = 0; - } else - idx++; - - grid->kitty_kbd.flags[idx] = flags; - grid->kitty_kbd.idx = idx; - - LOG_DBG("kitty kbd: pushed new flags: 0x%03x", flags); - break; - } - - case 'q': { - /* XTVERSION */ - if (vt_param_get(term, 0, 0) != 0) { - UNHANDLED(); - break; - } - - char reply[64]; - size_t n = xsnprintf( - reply, sizeof(reply), "\033P>|foot(%u.%u.%u%s%s)\033\\", - FOOT_MAJOR, FOOT_MINOR, FOOT_PATCH, - FOOT_EXTRA[0] != '\0' ? "-" : "", FOOT_EXTRA); - term_to_slave(term, reply, n); - break; - } - - default: - UNHANDLED(); - break; - } - - break; /* private[0] == '>' */ - } - - case '<': { - switch (final) { - case 'u': { - int count = vt_param_get(term, 0, 1); - LOG_DBG("kitty kbd: popping %d levels of flags", count); - - struct grid *grid = term->grid; - uint8_t idx = grid->kitty_kbd.idx; - - for (int i = 0; i < count; i++) { - /* Reset flags. This ensures we get flags=0 when - * over-popping */ - grid->kitty_kbd.flags[idx] = 0; - - if (idx == 0) - idx = ALEN(grid->kitty_kbd.flags) - 1; - else - idx--; - } - - grid->kitty_kbd.idx = idx; - - LOG_DBG("kitty kbd: flags after pop: 0x%03x", - term->grid->kitty_kbd.flags[idx]); - break; - } - } - break; /* private[0] == '<' */ - } - - case ' ': { - switch (final) { - case 'q': { - int param = vt_param_get(term, 0, 0); - switch (param) { - case 0: /* blinking block, but we use it to reset to configured default */ - term->cursor_style = term->conf->cursor.style; - term->cursor_blink.deccsusr = term->conf->cursor.blink.enabled; - term_cursor_blink_update(term); - break; - - case 1: /* blinking block */ - case 2: /* steady block */ - term->cursor_style = term->conf->cursor.style == CURSOR_HOLLOW - ? CURSOR_HOLLOW : CURSOR_BLOCK; - break; - - case 3: /* blinking underline */ - case 4: /* steady underline */ - term->cursor_style = CURSOR_UNDERLINE; - break; - - case 5: /* blinking bar */ - case 6: /* steady bar */ - term->cursor_style = CURSOR_BEAM; - break; - - default: - UNHANDLED(); - break; - } - - if (param > 0 && param <= 6) { - term->cursor_blink.deccsusr = param & 1; - term_cursor_blink_update(term); - } - break; - } - - default: - UNHANDLED(); - break; - } - break; /* private[0] == ' ' */ - } - - case '!': { - if (final == 'p') { - term_reset(term, false); - break; - } - + } else UNHANDLED(); - break; /* private[0] == '!' */ + + break; } - case '=': { - switch (final) { - case 'c': - if (vt_param_get(term, 0, 0) != 0) { - UNHANDLED(); - break; - } - - /* - * Send Device Attributes (Tertiary DA) - * - * Reply format is "DCS ! | DDDDDDDD ST" - * - * D..D is the unit ID of the terminal, consisting of four - * hexadecimal pairs. The first pair represents the - * manufacturing site code. This code can be any - * hexadecimal value from 00 through FF. - */ - - term_to_slave(term, "\033P!|464f4f54\033\\", 14); /* FOOT */ - break; - - case 'u': { - int flag_set = vt_param_get(term, 0, 0) & KITTY_KBD_SUPPORTED; - int mode = vt_param_get(term, 1, 1); - - struct grid *grid = term->grid; - uint8_t idx = grid->kitty_kbd.idx; - - switch (mode) { - case 1: - /* set bits are set, unset bits are reset */ - grid->kitty_kbd.flags[idx] = flag_set; - break; - - case 2: - /* set bits are set, unset bits are left unchanged */ - grid->kitty_kbd.flags[idx] |= flag_set; - break; - - case 3: - /* set bits are reset, unset bits are left unchanged */ - grid->kitty_kbd.flags[idx] &= ~flag_set; - break; - - default: - UNHANDLED(); - break; - } - - LOG_DBG("kitty kbd: flags after update: 0x%03x", - grid->kitty_kbd.flags[idx]); - break; - } - - default: - UNHANDLED(); - break; - } - break; /* private[0] == '=' */ - } - - case '$': { - switch (final) { - case 'r': { /* DECCARA */ - int top, left, bottom, right; - if (!params_to_rectangular_area( - term, 0, &top, &left, &bottom, &right)) - { - break; - } - - for (int r = top; r <= bottom; r++) { - struct row *row = grid_row(term->grid, r); - row->dirty = true; - - for (int c = left; c <= right; c++) { - struct attributes *a = &row->cells[c].attrs; - a->clean = 0; - - for (size_t i = 4; i < term->vt.params.idx; i++) { - const int param = term->vt.params.v[i].value; - - /* DECCARA only supports a sub-set of SGR parameters */ - switch (param) { - case 0: - a->bold = false; - a->underline = false; - a->blink = false; - a->reverse = false; - break; - - case 1: a->bold = true; break; - case 4: a->underline = true; break; - case 5: a->blink = true; break; - case 7: a->reverse = true; break; - - case 22: a->bold = false; break; - case 24: a->underline = false; break; - case 25: a->blink = false; break; - case 27: a->reverse = false; break; - } - } - } - } - break; - } - - case 't': { /* DECRARA */ - int top, left, bottom, right; - if (!params_to_rectangular_area( - term, 0, &top, &left, &bottom, &right)) - { - break; - } - - for (int r = top; r <= bottom; r++) { - struct row *row = grid_row(term->grid, r); - row->dirty = true; - - for (int c = left; c <= right; c++) { - struct attributes *a = &row->cells[c].attrs; - a->clean = 0; - - for (size_t i = 4; i < term->vt.params.idx; i++) { - const int param = term->vt.params.v[i].value; - - /* DECRARA only supports a sub-set of SGR parameters */ - switch (param) { - case 0: - a->bold = !a->bold; - a->underline = !a->underline; - a->blink = !a->blink; - a->reverse = !a->reverse; - break; - - case 1: a->bold = !a->bold; break; - case 4: a->underline = !a->underline; break; - case 5: a->blink = !a->blink; break; - case 7: a->reverse = !a->reverse; break; - } - } - } - } - break; - } - - case 'v': { /* DECCRA */ - int src_top, src_left, src_bottom, src_right; - if (!params_to_rectangular_area( - term, 0, &src_top, &src_left, &src_bottom, &src_right)) - { - break; - } - - int src_page = vt_param_get(term, 4, 1); - - int dst_rel_top = vt_param_get(term, 5, 1) - 1; - int dst_left = vt_param_get(term, 6, 1) - 1; - int dst_page = vt_param_get(term, 7, 1); - - if (unlikely(src_page != 1 || dst_page != 1)) { - /* We don’t support “pages” */ - break; - } - - int dst_rel_bottom = dst_rel_top + (src_bottom - src_top); - int dst_right = min(dst_left + (src_right - src_left), term->cols - 1); - - int dst_top = term_row_rel_to_abs(term, dst_rel_top); - int dst_bottom = term_row_rel_to_abs(term, dst_rel_bottom); - - /* Target area outside the screen is clipped */ - const size_t row_count = min(src_bottom - src_top, - dst_bottom - dst_top) + 1; - const size_t cell_count = min(src_right - src_left, - dst_right - dst_left) + 1; - - sixel_overwrite_by_rectangle( - term, dst_top, dst_left, row_count, cell_count); - - /* - * Copy source area - * - * Note: since source and destination may overlap, we need - * to copy out the entire source region first, and _then_ - * write the destination. I.e. this is similar to how - * memmove() behaves, but adapted to our row/cell - * structure. - */ - struct cell **copy = xmalloc(row_count * sizeof(copy[0])); - for (int r = 0; r < row_count; r++) { - copy[r] = xmalloc(cell_count * sizeof(copy[r][0])); - - const struct row *row = grid_row(term->grid, src_top + r); - const struct cell *cell = &row->cells[src_left]; - memcpy(copy[r], cell, cell_count * sizeof(copy[r][0])); - } - - /* Paste into destination area */ - for (int r = 0; r < row_count; r++) { - struct row *row = grid_row(term->grid, dst_top + r); - row->dirty = true; - - struct cell *cell = &row->cells[dst_left]; - memcpy(cell, copy[r], cell_count * sizeof(copy[r][0])); - free(copy[r]); - - for (;cell < &row->cells[dst_left + cell_count]; cell++) - cell->attrs.clean = 0; - - if (unlikely(row->extra != NULL)) { - /* TODO: technically, we should copy the source URIs... */ - grid_row_uri_range_erase(row, dst_left, dst_right); - } - } - free(copy); - break; - } - - case 'x': { /* DECFRA */ - const uint8_t c = vt_param_get(term, 0, 0); - - if (unlikely(!((c >= 32 && c < 126) || c >= 160))) - break; - - int top, left, bottom, right; - if (!params_to_rectangular_area( - term, 1, &top, &left, &bottom, &right)) - { - break; - } - - /* Erase the entire region at once (MUCH cheaper than - * doing it row by row, or even character by - * character). */ - sixel_overwrite_by_rectangle( - term, top, left, bottom - top + 1, right - left + 1); - - for (int r = top; r <= bottom; r++) - term_fill(term, r, left, c, right - left + 1, true); - - break; - } - - case 'z': { /* DECERA */ - int top, left, bottom, right; - if (!params_to_rectangular_area( - term, 0, &top, &left, &bottom, &right)) - { - break; - } - - /* - * Note: term_erase() _also_ erases sixels, but since - * we’re forced to erase one row at a time, erasing the - * entire sixel here is more efficient. - */ - sixel_overwrite_by_rectangle( - term, top, left, bottom - top + 1, right - left + 1); - - for (int r = top; r <= bottom; r++) - term_erase(term, r, left, r, right); - break; - } - } - - break; /* private[0] == ‘$’ */ - } - - case '#': { - switch (final) { - case 'P': { /* XTPUSHCOLORS */ - int slot = vt_param_get(term, 0, 0); - - /* Pm == 0, "push" (what xterm does is take take the - *current* slot + 1, even if that's in the middle of the - stack, and overwrites whatever is already in that - slot) */ - if (slot == 0) - slot = term->color_stack.idx + 1; - - if (term->color_stack.size < slot) { - const size_t new_size = slot; - term->color_stack.stack = xrealloc( - term->color_stack.stack, - new_size * sizeof(term->color_stack.stack[0])); - - /* Initialize new slots (except the selected slot, - which is done below) */ - xassert(new_size > 0); - for (size_t i = term->color_stack.size; i < new_size - 1; i++) { - memcpy(&term->color_stack.stack[i], &term->colors, - sizeof(term->colors)); - } - term->color_stack.size = new_size; - } - - xassert(slot > 0); - xassert(slot <= term->color_stack.size); - term->color_stack.idx = slot; - memcpy(&term->color_stack.stack[slot - 1], &term->colors, - sizeof(term->colors)); - break; - } - - case 'Q': { /* XTPOPCOLORS */ - int slot = vt_param_get(term, 0, 0); - - /* Pm == 0, "pop" (what xterm does is copy colors from the - *current* slot, *and* decrease the current slot index, - even if that's in the middle of the stack) */ - if (slot == 0) - slot = term->color_stack.idx; - - if (slot > 0 && slot <= term->color_stack.size) { - memcpy(&term->colors, &term->color_stack.stack[slot - 1], - sizeof(term->colors)); - term->color_stack.idx = slot - 1; - - /* Assume a full palette switch *will* affect almost - all cells. The alternative is to call - term_damage_color() for all 256 palette entries - *and* the default fg/bg (256 + 2 calls in total) */ - term_damage_view(term); - term_damage_margins(term); - } else if (slot == 0) { - LOG_ERR("XTPOPCOLORS: cannot pop beyond the first element"); - } else { - LOG_ERR( - "XTPOPCOLORS: invalid color slot: %d " - "(stack has %zu slots, current slot is %zu)", - vt_param_get(term, 0, 0), - term->color_stack.size, term->color_stack.idx); - } - break; - } - - case 'R': { /* XTREPORTCOLORS */ - char reply[64]; - size_t n = xsnprintf(reply, sizeof(reply), "\033[?%zu;%zu#Q", - term->color_stack.idx, term->color_stack.size); - term_to_slave(term, reply, n); - break; - } - } - break; /* private[0] == '#' */ - } - - case 0x243f: /* ?$ */ - switch (final) { - case 'p': { - unsigned param = vt_param_get(term, 0, 0); - - /* - * Request DEC private mode (DECRQM) - * Reply: - * 0 - not recognized - * 1 - set - * 2 - reset - * 3 - permanently set - * 4 - permantently reset - */ - unsigned status = decrqm(term, param); - char reply[32]; - size_t n = xsnprintf(reply, sizeof(reply), "\033[?%u;%u$y", param, status); - term_to_slave(term, reply, n); - break; - - } - - default: - UNHANDLED(); - break; - } - - break; /* private[0] == '?' && private[1] == '$' */ - default: + UNHANDLED(); + break; + } + + break; /* private[0] == 0 */ + } + + case '?': { + switch (final) { + case 'h': + /* DECSET - DEC private mode set */ + for (size_t i = 0; i < term->vt.params.idx; i++) + decset(term, term->vt.params.v[i].value); + break; + + case 'l': + /* DECRST - DEC private mode reset */ + for (size_t i = 0; i < term->vt.params.idx; i++) + decrst(term, term->vt.params.v[i].value); + break; + + case 's': + for (size_t i = 0; i < term->vt.params.idx; i++) + xtsave(term, term->vt.params.v[i].value); + break; + + case 'r': + for (size_t i = 0; i < term->vt.params.idx; i++) + xtrestore(term, term->vt.params.v[i].value); + break; + + case 'S': { + if (!term->conf->tweak.sixel) { UNHANDLED(); break; + } + + unsigned target = vt_param_get(term, 0, 0); + unsigned operation = vt_param_get(term, 1, 0); + + switch (target) { + case 1: + switch (operation) { + case 1: + sixel_colors_report_current(term); + break; + case 2: + sixel_colors_reset(term); + break; + case 3: + sixel_colors_set(term, vt_param_get(term, 2, 0)); + break; + case 4: + sixel_colors_report_max(term); + break; + default: + UNHANDLED(); + break; + } + break; + + case 2: + switch (operation) { + case 1: + sixel_geometry_report_current(term); + break; + case 2: + sixel_geometry_reset(term); + break; + case 3: + sixel_geometry_set(term, vt_param_get(term, 2, 0), + vt_param_get(term, 3, 0)); + break; + case 4: + sixel_geometry_report_max(term); + break; + default: + UNHANDLED(); + break; + } + break; + + default: + UNHANDLED(); + break; + } + + break; } + + case 'm': { + int resource = vt_param_get(term, 0, 0); + int value = -1; + + switch (resource) { + case 0: /* modifyKeyboard */ + value = 0; + break; + + case 1: /* modifyCursorKeys */ + case 2: /* modifyFunctionKeys */ + value = 1; + break; + + case 4: /* modifyOtherKeys */ + value = term->modify_other_keys_2 ? 2 : 1; + break; + + default: + LOG_WARN("XTQMODKEYS: invalid resource '%d' in '%s'", resource, + csi_as_string(term, final, -1)); + break; + } + + if (value >= 0) { + char reply[16] = {0}; + int chars = + snprintf(reply, sizeof(reply), "\033[>%d;%dm", resource, value); + term_to_slave(term, reply, chars); + } + break; + } + + case 'n': { + const int param = vt_param_get(term, 0, 0); + + switch (param) { + case 996: { /* Query current theme mode (see private mode 2031) */ + /* + * 1 - dark mode + * 2 - light mode + * + * In foot, the themes aren't necessarily light/dark, + * but by convention, the primary theme is dark, and + * the alternative theme is light. + */ + char reply[16] = {0}; + int chars = + snprintf(reply, sizeof(reply), "\033[?997;%dn", + term->colors.active_theme == COLOR_THEME_DARK ? 1 : 2); + + term_to_slave(term, reply, chars); + break; + } + } + break; + } + + case 'p': { + /* + * Request status of ECMA-48/"ANSI" private mode (DECRQM + * for SM/RM modes; see private="?$" case further below for + * DECSET/DECRST modes) + */ + unsigned param = vt_param_get(term, 0, 0); + unsigned status = DECRPM_NOT_RECOGNIZED; + if (param == 4) { + status = decrpm(term->insert_mode); + } + char reply[32]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[%u;%u$y", param, status); + term_to_slave(term, reply, n); + break; + } + + case 'u': { + enum kitty_kbd_flags flags = + term->grid->kitty_kbd.flags[term->grid->kitty_kbd.idx]; + + char reply[8]; + int chars = snprintf(reply, sizeof(reply), "\033[?%uu", flags); + term_to_slave(term, reply, chars); + break; + } + + default: + UNHANDLED(); + break; + } + + break; /* private[0] == '?' */ + } + + case '>': { + switch (final) { + case 'c': + /* Send Device Attributes (Secondary DA) */ + if (vt_param_get(term, 0, 0) != 0) { + UNHANDLED(); + break; + } + + /* + * Param 1 - terminal type: + * 0 - vt100 + * 1 - vt220 + * 2 - vt240 + * 18 - vt330 + * 19 - vt340 + * 24 - vt320 + * 41 - vt420 + * 61 - vt510 + * 64 - vt520 + * 65 - vt525 + * + * Param 2 - firmware version xterm uses its version + * number. We do to, in the format "MAJORMINORPATCH", + * where all three version numbers are always two + * digits. So e.g. 1.25.0 is reported as 012500. + * + * We report ourselves as a VT220. This must be + * synchronized with the primary DA response. + * + * Note: tertiary DA replies with "FOOT". + */ + + static_assert(FOOT_MAJOR < 100, "Major version must not exceed 99"); + static_assert(FOOT_MINOR < 100, "Minor version must not exceed 99"); + static_assert(FOOT_PATCH < 100, "Patch version must not exceed 99"); + + char reply[64]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[>1;%02u%02u%02u;0c", + FOOT_MAJOR, FOOT_MINOR, FOOT_PATCH); + + term_to_slave(term, reply, n); + break; + + case 'm': + if (term->vt.params.idx == 0) { + /* Reset all */ + } else { + int resource = vt_param_get(term, 0, 0); + int value = vt_param_get(term, 1, -1); + + switch (resource) { + case 0: /* modifyKeyboard */ + break; + + case 1: /* modifyCursorKeys */ + case 2: /* modifyFunctionKeys */ + /* Ignored, we always report modifiers */ + if (value != 2 && value != -1) { + LOG_WARN("unimplemented: %s = %d", + resource == 1 ? "modifyCursorKeys" + : resource == 2 ? "modifyFunctionKeys" + : "", + value); + } + break; + + case 4: /* modifyOtherKeys */ + term->modify_other_keys_2 = value == 2; + LOG_DBG("modifyOtherKeys=%d", value); + break; + + default: + LOG_WARN("XTMODKEYS: invalid resource '%d' in '%s'", resource, + csi_as_string(term, final, -1)); + break; + } + } + break; /* final == 'm' */ + + case 'n': { + int resource = + vt_param_get(term, 0, 2); /* Default is modifyFunctionKeys */ + switch (resource) { + case 0: /* modifyKeyboard */ + case 1: /* modifyCursorKeys */ + case 2: /* modifyFunctionKeys */ + break; + + case 4: /* modifyOtherKeys */ + /* We don't support fully disabling modifyOtherKeys, + * but simply revert back to mode '1' */ + term->modify_other_keys_2 = false; + LOG_DBG("modifyOtherKeys=1"); + break; + } + break; + } + + case 'u': { + int flags = vt_param_get(term, 0, 0) & KITTY_KBD_SUPPORTED; + + struct grid *grid = term->grid; + uint8_t idx = grid->kitty_kbd.idx; + + if (idx + 1 >= ALEN(grid->kitty_kbd.flags)) { + /* Stack full, evict oldest by wrapping around */ + idx = 0; + } else + idx++; + + grid->kitty_kbd.flags[idx] = flags; + grid->kitty_kbd.idx = idx; + + LOG_DBG("kitty kbd: pushed new flags: 0x%03x", flags); + break; + } + + case 'q': { + /* XTVERSION */ + if (vt_param_get(term, 0, 0) != 0) { + UNHANDLED(); + break; + } + + char reply[64]; + size_t n = xsnprintf( + reply, sizeof(reply), "\033P>|foot(%u.%u.%u%s%s)\033\\", FOOT_MAJOR, + FOOT_MINOR, FOOT_PATCH, FOOT_EXTRA[0] != '\0' ? "-" : "", FOOT_EXTRA); + term_to_slave(term, reply, n); + break; + } + + default: + UNHANDLED(); + break; + } + + break; /* private[0] == '>' */ + } + + case '<': { + switch (final) { + case 'u': { + int count = vt_param_get(term, 0, 1); + LOG_DBG("kitty kbd: popping %d levels of flags", count); + + struct grid *grid = term->grid; + uint8_t idx = grid->kitty_kbd.idx; + + for (int i = 0; i < count; i++) { + /* Reset flags. This ensures we get flags=0 when + * over-popping */ + grid->kitty_kbd.flags[idx] = 0; + + if (idx == 0) + idx = ALEN(grid->kitty_kbd.flags) - 1; + else + idx--; + } + + grid->kitty_kbd.idx = idx; + + LOG_DBG("kitty kbd: flags after pop: 0x%03x", + term->grid->kitty_kbd.flags[idx]); + break; + } + } + break; /* private[0] == '<' */ + } + + case ' ': { + switch (final) { + case 'q': { + int param = vt_param_get(term, 0, 0); + switch (param) { + case 0: /* blinking block, but we use it to reset to configured default */ + term->cursor_style = term->conf->cursor.style; + term->cursor_blink.deccsusr = term->conf->cursor.blink.enabled; + term_cursor_blink_update(term); + break; + + case 1: /* blinking block */ + case 2: /* steady block */ + term->cursor_style = term->conf->cursor.style == CURSOR_HOLLOW + ? CURSOR_HOLLOW + : CURSOR_BLOCK; + break; + + case 3: /* blinking underline */ + case 4: /* steady underline */ + term->cursor_style = CURSOR_UNDERLINE; + break; + + case 5: /* blinking bar */ + case 6: /* steady bar */ + term->cursor_style = CURSOR_BEAM; + break; + + default: + UNHANDLED(); + break; + } + + if (param > 0 && param <= 6) { + term->cursor_blink.deccsusr = param & 1; + term_cursor_blink_update(term); + } + break; + } + + default: + UNHANDLED(); + break; + } + break; /* private[0] == ' ' */ + } + + case '!': { + if (final == 'p') { + term_reset(term, false); + break; + } + + UNHANDLED(); + break; /* private[0] == '!' */ + } + + case '=': { + switch (final) { + case 'c': + if (vt_param_get(term, 0, 0) != 0) { + UNHANDLED(); + break; + } + + /* + * Send Device Attributes (Tertiary DA) + * + * Reply format is "DCS ! | DDDDDDDD ST" + * + * D..D is the unit ID of the terminal, consisting of four + * hexadecimal pairs. The first pair represents the + * manufacturing site code. This code can be any + * hexadecimal value from 00 through FF. + */ + + term_to_slave(term, "\033P!|464f4f54\033\\", 14); /* FOOT */ + break; + + case 'u': { + int flag_set = vt_param_get(term, 0, 0) & KITTY_KBD_SUPPORTED; + int mode = vt_param_get(term, 1, 1); + + struct grid *grid = term->grid; + uint8_t idx = grid->kitty_kbd.idx; + + switch (mode) { + case 1: + /* set bits are set, unset bits are reset */ + grid->kitty_kbd.flags[idx] = flag_set; + break; + + case 2: + /* set bits are set, unset bits are left unchanged */ + grid->kitty_kbd.flags[idx] |= flag_set; + break; + + case 3: + /* set bits are reset, unset bits are left unchanged */ + grid->kitty_kbd.flags[idx] &= ~flag_set; + break; + + default: + UNHANDLED(); + break; + } + + LOG_DBG("kitty kbd: flags after update: 0x%03x", + grid->kitty_kbd.flags[idx]); + break; + } + + default: + UNHANDLED(); + break; + } + break; /* private[0] == '=' */ + } + + case '$': { + switch (final) { + case 'r': { /* DECCARA */ + int top, left, bottom, right; + if (!params_to_rectangular_area(term, 0, &top, &left, &bottom, &right)) { + break; + } + + for (int r = top; r <= bottom; r++) { + struct row *row = grid_row(term->grid, r); + row->dirty = true; + + for (int c = left; c <= right; c++) { + struct attributes *a = &row->cells[c].attrs; + a->clean = 0; + + for (size_t i = 4; i < term->vt.params.idx; i++) { + const int param = term->vt.params.v[i].value; + + /* DECCARA only supports a sub-set of SGR parameters */ + switch (param) { + case 0: + a->bold = false; + a->underline = false; + a->blink = false; + a->reverse = false; + break; + + case 1: + a->bold = true; + break; + case 4: + a->underline = true; + break; + case 5: + a->blink = true; + break; + case 7: + a->reverse = true; + break; + + case 22: + a->bold = false; + break; + case 24: + a->underline = false; + break; + case 25: + a->blink = false; + break; + case 27: + a->reverse = false; + break; + } + } + } + } + break; + } + + case 't': { /* DECRARA */ + int top, left, bottom, right; + if (!params_to_rectangular_area(term, 0, &top, &left, &bottom, &right)) { + break; + } + + for (int r = top; r <= bottom; r++) { + struct row *row = grid_row(term->grid, r); + row->dirty = true; + + for (int c = left; c <= right; c++) { + struct attributes *a = &row->cells[c].attrs; + a->clean = 0; + + for (size_t i = 4; i < term->vt.params.idx; i++) { + const int param = term->vt.params.v[i].value; + + /* DECRARA only supports a sub-set of SGR parameters */ + switch (param) { + case 0: + a->bold = !a->bold; + a->underline = !a->underline; + a->blink = !a->blink; + a->reverse = !a->reverse; + break; + + case 1: + a->bold = !a->bold; + break; + case 4: + a->underline = !a->underline; + break; + case 5: + a->blink = !a->blink; + break; + case 7: + a->reverse = !a->reverse; + break; + } + } + } + } + break; + } + + case 'v': { /* DECCRA */ + int src_top, src_left, src_bottom, src_right; + if (!params_to_rectangular_area(term, 0, &src_top, &src_left, &src_bottom, + &src_right)) { + break; + } + + int src_page = vt_param_get(term, 4, 1); + + int dst_rel_top = vt_param_get(term, 5, 1) - 1; + int dst_left = vt_param_get(term, 6, 1) - 1; + int dst_page = vt_param_get(term, 7, 1); + + if (unlikely(src_page != 1 || dst_page != 1)) { + /* We don’t support “pages” */ + break; + } + + int dst_rel_bottom = dst_rel_top + (src_bottom - src_top); + int dst_right = min(dst_left + (src_right - src_left), term->cols - 1); + + int dst_top = term_row_rel_to_abs(term, dst_rel_top); + int dst_bottom = term_row_rel_to_abs(term, dst_rel_bottom); + + /* Target area outside the screen is clipped */ + const size_t row_count = + min(src_bottom - src_top, dst_bottom - dst_top) + 1; + const size_t cell_count = + min(src_right - src_left, dst_right - dst_left) + 1; + + sixel_overwrite_by_rectangle(term, dst_top, dst_left, row_count, + cell_count); + + /* + * Copy source area + * + * Note: since source and destination may overlap, we need + * to copy out the entire source region first, and _then_ + * write the destination. I.e. this is similar to how + * memmove() behaves, but adapted to our row/cell + * structure. + */ + struct cell **copy = xmalloc(row_count * sizeof(copy[0])); + for (int r = 0; r < row_count; r++) { + copy[r] = xmalloc(cell_count * sizeof(copy[r][0])); + + const struct row *row = grid_row(term->grid, src_top + r); + const struct cell *cell = &row->cells[src_left]; + memcpy(copy[r], cell, cell_count * sizeof(copy[r][0])); + } + + /* Paste into destination area */ + for (int r = 0; r < row_count; r++) { + struct row *row = grid_row(term->grid, dst_top + r); + row->dirty = true; + + struct cell *cell = &row->cells[dst_left]; + memcpy(cell, copy[r], cell_count * sizeof(copy[r][0])); + free(copy[r]); + + for (; cell < &row->cells[dst_left + cell_count]; cell++) + cell->attrs.clean = 0; + + if (unlikely(row->extra != NULL)) { + /* TODO: technically, we should copy the source URIs... */ + grid_row_uri_range_erase(row, dst_left, dst_right); + } + } + free(copy); + break; + } + + case 'x': { /* DECFRA */ + const uint8_t c = vt_param_get(term, 0, 0); + + if (unlikely(!((c >= 32 && c < 126) || c >= 160))) + break; + + int top, left, bottom, right; + if (!params_to_rectangular_area(term, 1, &top, &left, &bottom, &right)) { + break; + } + + /* Erase the entire region at once (MUCH cheaper than + * doing it row by row, or even character by + * character). */ + sixel_overwrite_by_rectangle(term, top, left, bottom - top + 1, + right - left + 1); + + for (int r = top; r <= bottom; r++) + term_fill(term, r, left, c, right - left + 1, true); + + break; + } + + case 'z': { /* DECERA */ + int top, left, bottom, right; + if (!params_to_rectangular_area(term, 0, &top, &left, &bottom, &right)) { + break; + } + + /* + * Note: term_erase() _also_ erases sixels, but since + * we’re forced to erase one row at a time, erasing the + * entire sixel here is more efficient. + */ + sixel_overwrite_by_rectangle(term, top, left, bottom - top + 1, + right - left + 1); + + for (int r = top; r <= bottom; r++) + term_erase(term, r, left, r, right); + break; + } + } + + break; /* private[0] == ‘$’ */ + } + + case '#': { + switch (final) { + case 'P': { /* XTPUSHCOLORS */ + int slot = vt_param_get(term, 0, 0); + + /* Pm == 0, "push" (what xterm does is take take the + *current* slot + 1, even if that's in the middle of the + stack, and overwrites whatever is already in that + slot) */ + if (slot == 0) + slot = term->color_stack.idx + 1; + + if (term->color_stack.size < slot) { + const size_t new_size = slot; + term->color_stack.stack = + xrealloc(term->color_stack.stack, + new_size * sizeof(term->color_stack.stack[0])); + + /* Initialize new slots (except the selected slot, + which is done below) */ + xassert(new_size > 0); + for (size_t i = term->color_stack.size; i < new_size - 1; i++) { + memcpy(&term->color_stack.stack[i], &term->colors, + sizeof(term->colors)); + } + term->color_stack.size = new_size; + } + + xassert(slot > 0); + xassert(slot <= term->color_stack.size); + term->color_stack.idx = slot; + memcpy(&term->color_stack.stack[slot - 1], &term->colors, + sizeof(term->colors)); + break; + } + + case 'Q': { /* XTPOPCOLORS */ + int slot = vt_param_get(term, 0, 0); + + /* Pm == 0, "pop" (what xterm does is copy colors from the + *current* slot, *and* decrease the current slot index, + even if that's in the middle of the stack) */ + if (slot == 0) + slot = term->color_stack.idx; + + if (slot > 0 && slot <= term->color_stack.size) { + memcpy(&term->colors, &term->color_stack.stack[slot - 1], + sizeof(term->colors)); + term->color_stack.idx = slot - 1; + + /* Assume a full palette switch *will* affect almost + all cells. The alternative is to call + term_damage_color() for all 256 palette entries + *and* the default fg/bg (256 + 2 calls in total) */ + term_damage_view(term); + term_damage_margins(term); + } else if (slot == 0) { + LOG_ERR("XTPOPCOLORS: cannot pop beyond the first element"); + } else { + LOG_ERR("XTPOPCOLORS: invalid color slot: %d " + "(stack has %zu slots, current slot is %zu)", + vt_param_get(term, 0, 0), term->color_stack.size, + term->color_stack.idx); + } + break; + } + + case 'R': { /* XTREPORTCOLORS */ + char reply[64]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[?%zu;%zu#Q", + term->color_stack.idx, term->color_stack.size); + term_to_slave(term, reply, n); + break; + } + } + break; /* private[0] == '#' */ + } + + case 0x243f: /* ?$ */ + switch (final) { + case 'p': { + unsigned param = vt_param_get(term, 0, 0); + + /* + * Request DEC private mode (DECRQM) + * Reply: + * 0 - not recognized + * 1 - set + * 2 - reset + * 3 - permanently set + * 4 - permantently reset + */ + unsigned status = decrqm(term, param); + char reply[32]; + size_t n = + xsnprintf(reply, sizeof(reply), "\033[?%u;%u$y", param, status); + term_to_slave(term, reply, n); + break; + } + + default: + UNHANDLED(); + break; + } + + break; /* private[0] == '?' && private[1] == '$' */ + + default: + UNHANDLED(); + break; + } } diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..c98e08e --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1779259093, + "narHash": "sha256-7DKWmH23hL2eYdkxCKeqj2i+yljTKuU+3Nk1UPHOnxc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d99b013d5d1931ad77fe3912ed218170dec5d9a4", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..abcd22c --- /dev/null +++ b/flake.nix @@ -0,0 +1,39 @@ +{ + description = "toes terminal emulator"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + }; + + outputs = + { nixpkgs, ... }: + let + systems = [ + "aarch64-linux" + "x86_64-linux" + ]; + + forAllSystems = nixpkgs.lib.genAttrs systems; + in + { + packages = forAllSystems ( + system: + let + pkgs = import nixpkgs { inherit system; }; + + toes = pkgs.foot.overrideAttrs (prev: { + pname = "toes"; + version = "1.26.1"; + src = ./.; + buildInputs = (prev.buildInputs or [ ]) ++ [ + pkgs.libsodium + ]; + }); + in + { + inherit toes; + default = toes; + } + ); + }; +} diff --git a/foot.ini b/foot.ini index 3d0e5d0..3381f20 100644 --- a/foot.ini +++ b/foot.ini @@ -193,6 +193,8 @@ height=26 # inherit-cwd=no (new tabs open in the active tab's cwd; requires OSC 7 shell support) # unread-indicator=● (string drawn before label when tab has unseen output; empty disables) # unread-color=fabd2f (color of the unread-indicator) +# overview-active-border=fabd2f (ring around the currently-active tab in tab overview) +# overview-select-border=ffffff (ring around the keyboard/hover-selected tab in tab overview) [csd] # preferred=server @@ -249,6 +251,9 @@ height=26 # tab-next=Control+Tab # tab-prev=Control+Shift+Tab # tab-overview=Control+Shift+space +# session-save=Control+Shift+s +# session-load=Control+Shift+l +# session-save-secure=Control+Shift+Alt+s [search-bindings] # cancel=Control+g Control+c Escape diff --git a/input.c b/input.c index 0e5c031..fb02113 100644 --- a/input.c +++ b/input.c @@ -1,34 +1,34 @@ #include "input.h" -#include -#include -#include -#include -#include #include -#include -#include -#include -#include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include -#include -#include #include +#include +#include #include #define LOG_MODULE "input" #define LOG_ENABLE_DBG 0 -#include "log.h" #include "commands.h" #include "config.h" #include "grid.h" #include "keymap.h" #include "kitty-keymap.h" +#include "log.h" #include "macros.h" #include "quirks.h" #include "render.h" @@ -46,1288 +46,1448 @@ #include "xsnprintf.h" struct pipe_context { - char *text; - size_t idx; - size_t left; + char *text; + size_t idx; + size_t left; }; -static bool -fdm_write_pipe(struct fdm *fdm, int fd, int events, void *data) -{ - struct pipe_context *ctx = data; +static bool fdm_write_pipe(struct fdm *fdm, int fd, int events, void *data) { + struct pipe_context *ctx = data; - if (events & EPOLLHUP) - goto pipe_closed; + if (events & EPOLLHUP) + goto pipe_closed; - xassert(events & EPOLLOUT); - ssize_t written = write(fd, &ctx->text[ctx->idx], ctx->left); + xassert(events & EPOLLOUT); + ssize_t written = write(fd, &ctx->text[ctx->idx], ctx->left); - if (written < 0) { - LOG_WARN("failed to write to pipe: %s", strerror(errno)); - goto pipe_closed; - } + if (written < 0) { + LOG_WARN("failed to write to pipe: %s", strerror(errno)); + goto pipe_closed; + } - xassert(written <= ctx->left); - ctx->idx += written; - ctx->left -= written; + xassert(written <= ctx->left); + ctx->idx += written; + ctx->left -= written; - if (ctx->left == 0) - goto pipe_closed; + if (ctx->left == 0) + goto pipe_closed; - return true; + return true; pipe_closed: - free(ctx->text); - free(ctx); - fdm_del(fdm, fd); - return true; + free(ctx->text); + free(ctx); + fdm_del(fdm, fd); + return true; } static void alternate_scroll(struct seat *seat, int amount, int button); -static bool -execute_binding(struct seat *seat, struct terminal *term, - const struct key_binding *binding, uint32_t serial, int amount) -{ - const enum bind_action_normal action = binding->action; +static bool execute_binding(struct seat *seat, struct terminal *term, + const struct key_binding *binding, uint32_t serial, + int amount) { + const enum bind_action_normal action = binding->action; + switch (action) { + case BIND_ACTION_NONE: + return true; + + case BIND_ACTION_NOOP: + return true; + + case BIND_ACTION_SCROLLBACK_UP_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, term->rows); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_UP_HALF_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, max(term->rows / 2, 1)); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_UP_LINE: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, 1); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_UP_MOUSE: + if (term->grid == &term->alt) { + if (term->alt_scrolling) { + alternate_scroll(seat, amount, BTN_BACK); + return true; + } + } else { + cmd_scrollback_up(term, amount); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_DOWN_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, term->rows); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_DOWN_HALF_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, max(term->rows / 2, 1)); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_DOWN_LINE: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, 1); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_DOWN_MOUSE: + if (term->grid == &term->alt) { + if (term->alt_scrolling) { + alternate_scroll(seat, amount, BTN_FORWARD); + return true; + } + } else { + cmd_scrollback_down(term, amount); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_HOME: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, term->grid->num_rows); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_END: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, term->grid->num_rows); + return true; + } + break; + + case BIND_ACTION_CLIPBOARD_COPY: + selection_to_clipboard(seat, term, serial); + return true; + + case BIND_ACTION_CLIPBOARD_PASTE: + selection_from_clipboard(seat, term, serial); + term_reset_view(term); + return true; + + case BIND_ACTION_PRIMARY_PASTE: + selection_from_primary(seat, term); + term_reset_view(term); + return true; + + case BIND_ACTION_SEARCH_START: + search_begin(term); + return true; + + case BIND_ACTION_FONT_SIZE_UP: + term_font_size_increase(term); + return true; + + case BIND_ACTION_FONT_SIZE_DOWN: + term_font_size_decrease(term); + return true; + + case BIND_ACTION_FONT_SIZE_RESET: + term_font_size_reset(term); + return true; + + case BIND_ACTION_SPAWN_TERMINAL: + term_spawn_new(term); + return true; + + case BIND_ACTION_MINIMIZE: + xdg_toplevel_set_minimized(term->window->xdg_toplevel); + return true; + + case BIND_ACTION_MAXIMIZE: + if (term->window->is_fullscreen) + xdg_toplevel_unset_fullscreen(term->window->xdg_toplevel); + if (term->window->is_maximized) + xdg_toplevel_unset_maximized(term->window->xdg_toplevel); + else + xdg_toplevel_set_maximized(term->window->xdg_toplevel); + return true; + + case BIND_ACTION_FULLSCREEN: + if (term->window->is_fullscreen) + xdg_toplevel_unset_fullscreen(term->window->xdg_toplevel); + else + xdg_toplevel_set_fullscreen(term->window->xdg_toplevel, NULL); + return true; + + case BIND_ACTION_PIPE_SCROLLBACK: + if (term->grid == &term->alt) + break; + /* FALLTHROUGH */ + case BIND_ACTION_PIPE_VIEW: + case BIND_ACTION_PIPE_SELECTED: + case BIND_ACTION_PIPE_COMMAND_OUTPUT: { + if (binding->aux->type != BINDING_AUX_PIPE) + return true; + + struct pipe_context *ctx = NULL; + + int pipe_fd[2] = {-1, -1}; + int stdout_fd = -1; + int stderr_fd = -1; + + char *text = NULL; + size_t len = 0; + + if (pipe(pipe_fd) < 0) { + LOG_ERRNO("failed to create pipe"); + goto pipe_err; + } + + stdout_fd = open("/dev/null", O_WRONLY); + stderr_fd = open("/dev/null", O_WRONLY); + + if (stdout_fd < 0 || stderr_fd < 0) { + LOG_ERRNO("failed to open /dev/null"); + goto pipe_err; + } + + bool success; switch (action) { - case BIND_ACTION_NONE: - return true; - - case BIND_ACTION_NOOP: - return true; - - case BIND_ACTION_SCROLLBACK_UP_PAGE: - if (term->grid == &term->normal) { - cmd_scrollback_up(term, term->rows); - return true; - } - break; - - case BIND_ACTION_SCROLLBACK_UP_HALF_PAGE: - if (term->grid == &term->normal) { - cmd_scrollback_up(term, max(term->rows / 2, 1)); - return true; - } - break; - - case BIND_ACTION_SCROLLBACK_UP_LINE: - if (term->grid == &term->normal) { - cmd_scrollback_up(term, 1); - return true; - } - break; - - case BIND_ACTION_SCROLLBACK_UP_MOUSE: - if (term->grid == &term->alt) { - if (term->alt_scrolling) { - alternate_scroll(seat, amount, BTN_BACK); - return true; - } - } else { - cmd_scrollback_up(term, amount); - return true; - } - break; - - case BIND_ACTION_SCROLLBACK_DOWN_PAGE: - if (term->grid == &term->normal) { - cmd_scrollback_down(term, term->rows); - return true; - } - break; - - case BIND_ACTION_SCROLLBACK_DOWN_HALF_PAGE: - if (term->grid == &term->normal) { - cmd_scrollback_down(term, max(term->rows / 2, 1)); - return true; - } - break; - - case BIND_ACTION_SCROLLBACK_DOWN_LINE: - if (term->grid == &term->normal) { - cmd_scrollback_down(term, 1); - return true; - } - break; - - case BIND_ACTION_SCROLLBACK_DOWN_MOUSE: - if (term->grid == &term->alt) { - if (term->alt_scrolling) { - alternate_scroll(seat, amount, BTN_FORWARD); - return true; - } - } else { - cmd_scrollback_down(term, amount); - return true; - } - break; - - case BIND_ACTION_SCROLLBACK_HOME: - if (term->grid == &term->normal) { - cmd_scrollback_up(term, term->grid->num_rows); - return true; - } - break; - - case BIND_ACTION_SCROLLBACK_END: - if (term->grid == &term->normal) { - cmd_scrollback_down(term, term->grid->num_rows); - return true; - } - break; - - case BIND_ACTION_CLIPBOARD_COPY: - selection_to_clipboard(seat, term, serial); - return true; - - case BIND_ACTION_CLIPBOARD_PASTE: - selection_from_clipboard(seat, term, serial); - term_reset_view(term); - return true; - - case BIND_ACTION_PRIMARY_PASTE: - selection_from_primary(seat, term); - term_reset_view(term); - return true; - - case BIND_ACTION_SEARCH_START: - search_begin(term); - return true; - - case BIND_ACTION_FONT_SIZE_UP: - term_font_size_increase(term); - return true; - - case BIND_ACTION_FONT_SIZE_DOWN: - term_font_size_decrease(term); - return true; - - case BIND_ACTION_FONT_SIZE_RESET: - term_font_size_reset(term); - return true; - - case BIND_ACTION_SPAWN_TERMINAL: - term_spawn_new(term); - return true; - - case BIND_ACTION_MINIMIZE: - xdg_toplevel_set_minimized(term->window->xdg_toplevel); - return true; - - case BIND_ACTION_MAXIMIZE: - if (term->window->is_fullscreen) - xdg_toplevel_unset_fullscreen(term->window->xdg_toplevel); - if (term->window->is_maximized) - xdg_toplevel_unset_maximized(term->window->xdg_toplevel); - else - xdg_toplevel_set_maximized(term->window->xdg_toplevel); - return true; - - case BIND_ACTION_FULLSCREEN: - if (term->window->is_fullscreen) - xdg_toplevel_unset_fullscreen(term->window->xdg_toplevel); - else - xdg_toplevel_set_fullscreen(term->window->xdg_toplevel, NULL); - return true; - case BIND_ACTION_PIPE_SCROLLBACK: - if (term->grid == &term->alt) - break; - /* FALLTHROUGH */ + success = term_scrollback_to_text(term, &text, &len); + break; + case BIND_ACTION_PIPE_VIEW: + success = term_view_to_text(term, &text, &len); + break; + case BIND_ACTION_PIPE_SELECTED: - case BIND_ACTION_PIPE_COMMAND_OUTPUT: { - if (binding->aux->type != BINDING_AUX_PIPE) - return true; - - struct pipe_context *ctx = NULL; - - int pipe_fd[2] = {-1, -1}; - int stdout_fd = -1; - int stderr_fd = -1; - - char *text = NULL; - size_t len = 0; - - if (pipe(pipe_fd) < 0) { - LOG_ERRNO("failed to create pipe"); - goto pipe_err; - } - - stdout_fd = open("/dev/null", O_WRONLY); - stderr_fd = open("/dev/null", O_WRONLY); - - if (stdout_fd < 0 || stderr_fd < 0) { - LOG_ERRNO("failed to open /dev/null"); - goto pipe_err; - } - - bool success; - switch (action) { - case BIND_ACTION_PIPE_SCROLLBACK: - success = term_scrollback_to_text(term, &text, &len); - break; - - case BIND_ACTION_PIPE_VIEW: - success = term_view_to_text(term, &text, &len); - break; - - case BIND_ACTION_PIPE_SELECTED: - text = selection_to_text(term); - success = text != NULL; - len = text != NULL ? strlen(text) : 0; - break; - - case BIND_ACTION_PIPE_COMMAND_OUTPUT: - success = term_command_output_to_text(term, &text, &len); - break; - - default: - BUG("Unhandled action type"); - success = false; - break; - } - - if (!success) - goto pipe_err; - - /* Make write-end non-blocking; required by the FDM */ - { - int flags = fcntl(pipe_fd[1], F_GETFL); - if (flags < 0 || - fcntl(pipe_fd[1], F_SETFL, flags | O_NONBLOCK) < 0) - { - LOG_ERRNO("failed to make write-end of pipe non-blocking"); - goto pipe_err; - } - } - - /* Make sure write-end is closed on exec() - or the spawned - * program may not terminate*/ - { - int flags = fcntl(pipe_fd[1], F_GETFD); - if (flags < 0 || - fcntl(pipe_fd[1], F_SETFD, flags | FD_CLOEXEC) < 0) - { - LOG_ERRNO("failed to set FD_CLOEXEC on writeend of pipe"); - goto pipe_err; - } - } - - if (spawn(term->reaper, term->cwd, binding->aux->pipe.args, - pipe_fd[0], stdout_fd, stderr_fd, NULL, NULL, NULL) < 0) - goto pipe_err; - - /* Close read end */ - close(pipe_fd[0]); - - ctx = xmalloc(sizeof(*ctx)); - *ctx = (struct pipe_context){ - .text = text, - .left = len, - }; - - /* Asynchronously write the output to the pipe */ - if (!fdm_add(term->fdm, pipe_fd[1], EPOLLOUT, &fdm_write_pipe, ctx)) - goto pipe_err; - - return true; - - pipe_err: - if (stdout_fd >= 0) - close(stdout_fd); - if (stderr_fd >= 0) - close(stderr_fd); - if (pipe_fd[0] >= 0) - close(pipe_fd[0]); - if (pipe_fd[1] >= 0) - close(pipe_fd[1]); - free(text); - free(ctx); - return true; - } - - case BIND_ACTION_SHOW_URLS_COPY: - case BIND_ACTION_SHOW_URLS_LAUNCH: - case BIND_ACTION_SHOW_URLS_PERSISTENT: { - xassert(!urls_mode_is_active(term)); - - enum url_action url_action = - action == BIND_ACTION_SHOW_URLS_COPY ? URL_ACTION_COPY : - action == BIND_ACTION_SHOW_URLS_LAUNCH ? URL_ACTION_LAUNCH : - URL_ACTION_PERSISTENT; - - urls_collect(term, url_action, &term->conf->url.preg, true, &term->urls); - urls_assign_key_combos(term->conf, &term->urls); - urls_render(term, &term->conf->url.launch); - return true; - } - - case BIND_ACTION_TEXT_BINDING: - xassert(binding->aux->type == BINDING_AUX_TEXT); - term_to_slave(term, binding->aux->text.data, binding->aux->text.len); - return true; - - case BIND_ACTION_PROMPT_PREV: { - if (term->grid != &term->normal) - return false; - - struct grid *grid = term->grid; - const int sb_start = - grid_sb_start_ignore_uninitialized(grid, term->rows); - - /* Check each row from current view-1 (that is, the first - * currently not visible row), up to, and including, the - * scrollback start */ - for (int r_sb_rel = - grid_row_abs_to_sb_precalc_sb_start( - grid, sb_start, grid->view) - 1; - r_sb_rel >= 0; r_sb_rel--) - { - const int r_abs = - grid_row_sb_to_abs_precalc_sb_start(grid, sb_start, r_sb_rel); - - const struct row *row = grid->rows[r_abs]; - xassert(row != NULL); - - if (!row->shell_integration.prompt_marker) - continue; - - grid->view = r_abs; - term_damage_view(term); - render_refresh(term); - break; - } - - return true; - } - - case BIND_ACTION_PROMPT_NEXT: { - if (term->grid != &term->normal) - return false; - - struct grid *grid = term->grid; - const int num_rows = grid->num_rows; - - if (grid->view == grid->offset) { - /* Already at the bottom */ - return true; - } - - /* Check each row from view+1, to the bottom of the scrollback */ - for (int r_abs = (grid->view + 1) & (num_rows - 1); - ; - r_abs = (r_abs + 1) & (num_rows - 1)) - { - const struct row *row = grid->rows[r_abs]; - xassert(row != NULL); - - if (!row->shell_integration.prompt_marker) { - if (r_abs == grid->offset + term->rows - 1) { - /* We've reached the bottom of the scrollback */ - break; - } - continue; - } - - int sb_start = grid_sb_start_ignore_uninitialized(grid, term->rows); - int ofs_sb_rel = - grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, grid->offset); - int new_view_sb_rel = - grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, r_abs); - - new_view_sb_rel = min(ofs_sb_rel, new_view_sb_rel); - grid->view = grid_row_sb_to_abs_precalc_sb_start( - grid, sb_start, new_view_sb_rel); - - term_damage_view(term); - render_refresh(term); - break; - } - - return true; - } - - case BIND_ACTION_UNICODE_INPUT: - unicode_mode_activate(term); - return true; - - case BIND_ACTION_QUIT: - term_shutdown(term); - return true; - - case BIND_ACTION_TAB_NEW: - term_tab_new(term, 0, NULL, NULL, term->shutdown.cb, term->shutdown.cb_data); - return true; - - case BIND_ACTION_TAB_CLOSE: - term_tab_close(term); - return true; - - case BIND_ACTION_TAB_NEXT: { - struct wl_window *win = term->window; - if (win->tab_count > 1) - term_tab_switch(win, (win->active_tab + 1) % win->tab_count); - return true; - } - - case BIND_ACTION_TAB_PREV: { - struct wl_window *win = term->window; - if (win->tab_count > 1) - term_tab_switch(win, - win->active_tab == 0 ? win->tab_count - 1 : win->active_tab - 1); - return true; - } - - case BIND_ACTION_TAB_1: - case BIND_ACTION_TAB_2: - case BIND_ACTION_TAB_3: - case BIND_ACTION_TAB_4: - case BIND_ACTION_TAB_5: - case BIND_ACTION_TAB_6: - case BIND_ACTION_TAB_7: - case BIND_ACTION_TAB_8: - case BIND_ACTION_TAB_9: { - size_t idx = (size_t)(action - BIND_ACTION_TAB_1); - struct wl_window *win = term->window; - if (idx < win->tab_count) - term_tab_switch(win, idx); - return true; - } - - case BIND_ACTION_TAB_OVERVIEW: - tab_overview_toggle(term->window); - return true; - - case BIND_ACTION_REGEX_LAUNCH: - case BIND_ACTION_REGEX_COPY: - if (binding->aux->type != BINDING_AUX_REGEX) - return true; - - tll_foreach(term->conf->custom_regexes, it) { - const struct custom_regex *regex = &it->item; - - if (streq(regex->name, binding->aux->regex_name)) { - xassert(!urls_mode_is_active(term)); - - enum url_action url_action = action == BIND_ACTION_REGEX_LAUNCH - ? URL_ACTION_LAUNCH : URL_ACTION_COPY; - - if (regex->regex == NULL) { - LOG_ERR("regex:%s has no regex defined", regex->name); - return true; - } - if (url_action == URL_ACTION_LAUNCH && regex->launch.argv.args == NULL) { - LOG_ERR("regex:%s has no launch command defined", regex->name); - return true; - } - - urls_collect(term, url_action, ®ex->preg, false, &term->urls); - urls_assign_key_combos(term->conf, &term->urls); - urls_render(term, ®ex->launch); - return true; - } - } - - LOG_ERR( - "no regex section named '%s' defined in the configuration", - binding->aux->regex_name); - - return true; - - case BIND_ACTION_THEME_SWITCH_1: - case BIND_ACTION_THEME_SWITCH_DARK: - term_theme_switch_to_dark(term); - return true; - - case BIND_ACTION_THEME_SWITCH_2: - case BIND_ACTION_THEME_SWITCH_LIGHT: - term_theme_switch_to_light(term); - return true; - - case BIND_ACTION_THEME_TOGGLE: - term_theme_toggle(term); - return true; - - case BIND_ACTION_SELECT_BEGIN: - selection_start( - term, seat->mouse.col, seat->mouse.row, SELECTION_CHAR_WISE, false); - return true; - - case BIND_ACTION_SELECT_BEGIN_BLOCK: - selection_start( - term, seat->mouse.col, seat->mouse.row, SELECTION_BLOCK, false); - return true; - - case BIND_ACTION_SELECT_EXTEND: - selection_extend( - seat, term, seat->mouse.col, seat->mouse.row, term->selection.kind); - return true; - - case BIND_ACTION_SELECT_EXTEND_CHAR_WISE: - if (term->selection.kind != SELECTION_BLOCK) { - selection_extend( - seat, term, seat->mouse.col, seat->mouse.row, SELECTION_CHAR_WISE); - return true; - } - return false; - - case BIND_ACTION_SELECT_WORD: - selection_start( - term, seat->mouse.col, seat->mouse.row, SELECTION_WORD_WISE, false); - return true; - - case BIND_ACTION_SELECT_WORD_WS: - selection_start( - term, seat->mouse.col, seat->mouse.row, SELECTION_WORD_WISE, true); - return true; - - case BIND_ACTION_SELECT_QUOTE: - selection_start( - term, seat->mouse.col, seat->mouse.row, SELECTION_QUOTE_WISE, false); - return true; - - case BIND_ACTION_SELECT_ROW: - selection_start( - term, seat->mouse.col, seat->mouse.row, SELECTION_LINE_WISE, false); - return true; - - case BIND_ACTION_COUNT: - BUG("Invalid action type"); - return false; - } - - return false; -} - -static void -keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, - uint32_t format, int32_t fd, uint32_t size) -{ - LOG_DBG("keyboard_keymap: keyboard=%p (format=%u, size=%u)", - (void *)wl_keyboard, format, size); - - struct seat *seat = data; - struct wayland *wayl = seat->wayl; - - /* - * Free old keymap state - */ - - if (seat->kbd.xkb_keymap != NULL) { - xkb_keymap_unref(seat->kbd.xkb_keymap); - seat->kbd.xkb_keymap = NULL; - } - if (seat->kbd.xkb_state != NULL) { - xkb_state_unref(seat->kbd.xkb_state); - seat->kbd.xkb_state = NULL; - } - - key_binding_unload_keymap(wayl->key_binding_manager, seat); - - /* Verify keymap is in a format we understand */ - switch ((enum wl_keyboard_keymap_format)format) { - case WL_KEYBOARD_KEYMAP_FORMAT_NO_KEYMAP: - goto err; - - case WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1: - break; + text = selection_to_text(term); + success = text != NULL; + len = text != NULL ? strlen(text) : 0; + break; + + case BIND_ACTION_PIPE_COMMAND_OUTPUT: + success = term_command_output_to_text(term, &text, &len); + break; default: - LOG_WARN("unrecognized keymap format: %u", format); - goto err; + BUG("Unhandled action type"); + success = false; + break; } - char *map_str = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0); - if (map_str == MAP_FAILED) { - LOG_ERRNO("failed to mmap keyboard keymap"); - goto err; + if (!success) + goto pipe_err; + + /* Make write-end non-blocking; required by the FDM */ + { + int flags = fcntl(pipe_fd[1], F_GETFL); + if (flags < 0 || fcntl(pipe_fd[1], F_SETFL, flags | O_NONBLOCK) < 0) { + LOG_ERRNO("failed to make write-end of pipe non-blocking"); + goto pipe_err; + } } - while (map_str[size - 1] == '\0') - size--; - - if (seat->kbd.xkb != NULL) { - seat->kbd.xkb_keymap = xkb_keymap_new_from_buffer( - seat->kbd.xkb, map_str, size, XKB_KEYMAP_FORMAT_TEXT_V1, - XKB_KEYMAP_COMPILE_NO_FLAGS); - + /* Make sure write-end is closed on exec() - or the spawned + * program may not terminate*/ + { + int flags = fcntl(pipe_fd[1], F_GETFD); + if (flags < 0 || fcntl(pipe_fd[1], F_SETFD, flags | FD_CLOEXEC) < 0) { + LOG_ERRNO("failed to set FD_CLOEXEC on writeend of pipe"); + goto pipe_err; + } } - munmap(map_str, size); + if (spawn(term->reaper, term->cwd, binding->aux->pipe.args, pipe_fd[0], + stdout_fd, stderr_fd, NULL, NULL, NULL) < 0) + goto pipe_err; - if (seat->kbd.xkb_keymap != NULL) { - seat->kbd.xkb_state = xkb_state_new(seat->kbd.xkb_keymap); + /* Close read end */ + close(pipe_fd[0]); - seat->kbd.mod_shift = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); - seat->kbd.mod_alt = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_ALT) ; - seat->kbd.mod_ctrl = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_CTRL); - seat->kbd.mod_super = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_LOGO); - seat->kbd.mod_caps = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_CAPS); - seat->kbd.mod_num = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_NUM); + ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct pipe_context){ + .text = text, + .left = len, + }; - /* Significant modifiers in the legacy keyboard protocol */ - seat->kbd.legacy_significant = 0; - if (seat->kbd.mod_shift != XKB_MOD_INVALID) - seat->kbd.legacy_significant |= 1 << seat->kbd.mod_shift; - if (seat->kbd.mod_alt != XKB_MOD_INVALID) - seat->kbd.legacy_significant |= 1 << seat->kbd.mod_alt; - if (seat->kbd.mod_ctrl != XKB_MOD_INVALID) - seat->kbd.legacy_significant |= 1 << seat->kbd.mod_ctrl; - if (seat->kbd.mod_super != XKB_MOD_INVALID) - seat->kbd.legacy_significant |= 1 << seat->kbd.mod_super; + /* Asynchronously write the output to the pipe */ + if (!fdm_add(term->fdm, pipe_fd[1], EPOLLOUT, &fdm_write_pipe, ctx)) + goto pipe_err; - /* Significant modifiers in the kitty keyboard protocol */ - seat->kbd.kitty_significant = seat->kbd.legacy_significant; - if (seat->kbd.mod_caps != XKB_MOD_INVALID) - seat->kbd.kitty_significant |= 1 << seat->kbd.mod_caps; - if (seat->kbd.mod_num != XKB_MOD_INVALID) - seat->kbd.kitty_significant |= 1 << seat->kbd.mod_num; + return true; + pipe_err: + if (stdout_fd >= 0) + close(stdout_fd); + if (stderr_fd >= 0) + close(stderr_fd); + if (pipe_fd[0] >= 0) + close(pipe_fd[0]); + if (pipe_fd[1] >= 0) + close(pipe_fd[1]); + free(text); + free(ctx); + return true; + } - /* - * Create a mask of all "virtual" modifiers. Some compositors - * add these *in addition* to the "real" modifiers (Mod1, - * Mod2, etc). - * - * Since our modifier logic (both for internal shortcut - * processing, and e.g. the kitty keyboard protocol) makes - * very few assumptions on available modifiers, which keys map - * to which modifier etc, the presence of virtual modifiers - * causes various things to break. - * - * For example, if a foot shortcut is Mod1+b (i.e. Alt+b), it - * won't match if the compositor _also_ sets the Alt modifier - * (the corresponding shortcut in foot would be Alt+Mod1+b). - * - * See https://codeberg.org/dnkl/foot/issues/2009 - * - * Mutter (GNOME) is known to set the virtual modifiers in - * addtiion to the real modifiers. - * - * As far as I know, there's no compositor that _only_ sets - * virtual modifiers (don't think that's even legal...?) - */ - { - xkb_mod_index_t alt = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_ALT); - xkb_mod_index_t meta = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_META); - xkb_mod_index_t super = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_SUPER); - xkb_mod_index_t hyper = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_HYPER); - xkb_mod_index_t num_lock = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_NUM); - xkb_mod_index_t scroll_lock = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_SCROLL); - xkb_mod_index_t level_three = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_LEVEL3); - xkb_mod_index_t level_five = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_LEVEL5); + case BIND_ACTION_SHOW_URLS_COPY: + case BIND_ACTION_SHOW_URLS_LAUNCH: + case BIND_ACTION_SHOW_URLS_PERSISTENT: { + xassert(!urls_mode_is_active(term)); - xkb_mod_index_t ignore = 0; + enum url_action url_action = + action == BIND_ACTION_SHOW_URLS_COPY ? URL_ACTION_COPY + : action == BIND_ACTION_SHOW_URLS_LAUNCH ? URL_ACTION_LAUNCH + : URL_ACTION_PERSISTENT; - if (alt != XKB_MOD_INVALID) ignore |= 1 << alt; - if (meta != XKB_MOD_INVALID) ignore |= 1 << meta; - if (super != XKB_MOD_INVALID) ignore |= 1 << super; - if (hyper != XKB_MOD_INVALID) ignore |= 1 << hyper; - if (num_lock != XKB_MOD_INVALID) ignore |= 1 << num_lock; - if (scroll_lock != XKB_MOD_INVALID) ignore |= 1 << scroll_lock; - if (level_three != XKB_MOD_INVALID) ignore |= 1 << level_three; - if (level_five != XKB_MOD_INVALID) ignore |= 1 << level_five; + urls_collect(term, url_action, &term->conf->url.preg, true, &term->urls); + urls_assign_key_combos(term->conf, &term->urls); + urls_render(term, &term->conf->url.launch); + return true; + } - seat->kbd.virtual_modifiers = ignore; + case BIND_ACTION_TEXT_BINDING: + xassert(binding->aux->type == BINDING_AUX_TEXT); + term_to_slave(term, binding->aux->text.data, binding->aux->text.len); + return true; + + case BIND_ACTION_PROMPT_PREV: { + if (term->grid != &term->normal) + return false; + + struct grid *grid = term->grid; + const int sb_start = grid_sb_start_ignore_uninitialized(grid, term->rows); + + /* Check each row from current view-1 (that is, the first + * currently not visible row), up to, and including, the + * scrollback start */ + for (int r_sb_rel = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, grid->view) - + 1; + r_sb_rel >= 0; r_sb_rel--) { + const int r_abs = + grid_row_sb_to_abs_precalc_sb_start(grid, sb_start, r_sb_rel); + + const struct row *row = grid->rows[r_abs]; + xassert(row != NULL); + + if (!row->shell_integration.prompt_marker) + continue; + + grid->view = r_abs; + term_damage_view(term); + render_refresh(term); + break; + } + + return true; + } + + case BIND_ACTION_PROMPT_NEXT: { + if (term->grid != &term->normal) + return false; + + struct grid *grid = term->grid; + const int num_rows = grid->num_rows; + + if (grid->view == grid->offset) { + /* Already at the bottom */ + return true; + } + + /* Check each row from view+1, to the bottom of the scrollback */ + for (int r_abs = (grid->view + 1) & (num_rows - 1);; + r_abs = (r_abs + 1) & (num_rows - 1)) { + const struct row *row = grid->rows[r_abs]; + xassert(row != NULL); + + if (!row->shell_integration.prompt_marker) { + if (r_abs == grid->offset + term->rows - 1) { + /* We've reached the bottom of the scrollback */ + break; + } + continue; + } + + int sb_start = grid_sb_start_ignore_uninitialized(grid, term->rows); + int ofs_sb_rel = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, grid->offset); + int new_view_sb_rel = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, r_abs); + + new_view_sb_rel = min(ofs_sb_rel, new_view_sb_rel); + grid->view = + grid_row_sb_to_abs_precalc_sb_start(grid, sb_start, new_view_sb_rel); + + term_damage_view(term); + render_refresh(term); + break; + } + + return true; + } + + case BIND_ACTION_UNICODE_INPUT: + unicode_mode_activate(term); + return true; + + case BIND_ACTION_QUIT: + term_shutdown(term); + return true; + + case BIND_ACTION_TAB_NEW: + term_tab_new(term, 0, NULL, NULL, term->shutdown.cb, + term->shutdown.cb_data); + return true; + + case BIND_ACTION_TAB_CLOSE: + term_tab_close(term); + return true; + + case BIND_ACTION_TAB_NEXT: { + struct wl_window *win = term->window; + if (win->tab_count > 1) + term_tab_switch(win, (win->active_tab + 1) % win->tab_count); + return true; + } + + case BIND_ACTION_TAB_PREV: { + struct wl_window *win = term->window; + if (win->tab_count > 1) + term_tab_switch(win, win->active_tab == 0 ? win->tab_count - 1 + : win->active_tab - 1); + return true; + } + + case BIND_ACTION_TAB_1: + case BIND_ACTION_TAB_2: + case BIND_ACTION_TAB_3: + case BIND_ACTION_TAB_4: + case BIND_ACTION_TAB_5: + case BIND_ACTION_TAB_6: + case BIND_ACTION_TAB_7: + case BIND_ACTION_TAB_8: + case BIND_ACTION_TAB_9: { + size_t idx = (size_t)(action - BIND_ACTION_TAB_1); + struct wl_window *win = term->window; + if (idx < win->tab_count) + term_tab_switch(win, idx); + return true; + } + + case BIND_ACTION_TAB_OVERVIEW: + tab_overview_toggle(term->window); + return true; + + case BIND_ACTION_SESSION_SAVE: + search_begin_session(term, SEARCH_MODE_SESSION_SAVE); + return true; + + case BIND_ACTION_SESSION_LOAD: + search_begin_session(term, SEARCH_MODE_SESSION_LOAD); + return true; + + case BIND_ACTION_SESSION_SAVE_SECURE: + search_begin_session(term, SEARCH_MODE_SESSION_SAVE_SECURE_NAME); + return true; + + case BIND_ACTION_REGEX_LAUNCH: + case BIND_ACTION_REGEX_COPY: + if (binding->aux->type != BINDING_AUX_REGEX) + return true; + + tll_foreach(term->conf->custom_regexes, it) { + const struct custom_regex *regex = &it->item; + + if (streq(regex->name, binding->aux->regex_name)) { + xassert(!urls_mode_is_active(term)); + + enum url_action url_action = action == BIND_ACTION_REGEX_LAUNCH + ? URL_ACTION_LAUNCH + : URL_ACTION_COPY; + + if (regex->regex == NULL) { + LOG_ERR("regex:%s has no regex defined", regex->name); + return true; + } + if (url_action == URL_ACTION_LAUNCH && + regex->launch.argv.args == NULL) { + LOG_ERR("regex:%s has no launch command defined", regex->name); + return true; } - seat->kbd.key_arrow_up = xkb_keymap_key_by_name(seat->kbd.xkb_keymap, "UP"); - seat->kbd.key_arrow_down = xkb_keymap_key_by_name(seat->kbd.xkb_keymap, "DOWN"); + urls_collect(term, url_action, ®ex->preg, false, &term->urls); + urls_assign_key_combos(term->conf, &term->urls); + urls_render(term, ®ex->launch); + return true; + } } - key_binding_load_keymap(wayl->key_binding_manager, seat); + LOG_ERR("no regex section named '%s' defined in the configuration", + binding->aux->regex_name); + + return true; + + case BIND_ACTION_THEME_SWITCH_1: + case BIND_ACTION_THEME_SWITCH_DARK: + term_theme_switch_to_dark(term); + return true; + + case BIND_ACTION_THEME_SWITCH_2: + case BIND_ACTION_THEME_SWITCH_LIGHT: + term_theme_switch_to_light(term); + return true; + + case BIND_ACTION_THEME_TOGGLE: + term_theme_toggle(term); + return true; + + case BIND_ACTION_SELECT_BEGIN: + selection_start(term, seat->mouse.col, seat->mouse.row, SELECTION_CHAR_WISE, + false); + return true; + + case BIND_ACTION_SELECT_BEGIN_BLOCK: + selection_start(term, seat->mouse.col, seat->mouse.row, SELECTION_BLOCK, + false); + return true; + + case BIND_ACTION_SELECT_EXTEND: + selection_extend(seat, term, seat->mouse.col, seat->mouse.row, + term->selection.kind); + return true; + + case BIND_ACTION_SELECT_EXTEND_CHAR_WISE: + if (term->selection.kind != SELECTION_BLOCK) { + selection_extend(seat, term, seat->mouse.col, seat->mouse.row, + SELECTION_CHAR_WISE); + return true; + } + return false; + + case BIND_ACTION_SELECT_WORD: + selection_start(term, seat->mouse.col, seat->mouse.row, SELECTION_WORD_WISE, + false); + return true; + + case BIND_ACTION_SELECT_WORD_WS: + selection_start(term, seat->mouse.col, seat->mouse.row, SELECTION_WORD_WISE, + true); + return true; + + case BIND_ACTION_SELECT_QUOTE: + selection_start(term, seat->mouse.col, seat->mouse.row, + SELECTION_QUOTE_WISE, false); + return true; + + case BIND_ACTION_SELECT_ROW: + selection_start(term, seat->mouse.col, seat->mouse.row, SELECTION_LINE_WISE, + false); + return true; + + case BIND_ACTION_COUNT: + BUG("Invalid action type"); + return false; + } + + return false; +} + +static void keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, + uint32_t format, int32_t fd, uint32_t size) { + LOG_DBG("keyboard_keymap: keyboard=%p (format=%u, size=%u)", + (void *)wl_keyboard, format, size); + + struct seat *seat = data; + struct wayland *wayl = seat->wayl; + + /* + * Free old keymap state + */ + + if (seat->kbd.xkb_keymap != NULL) { + xkb_keymap_unref(seat->kbd.xkb_keymap); + seat->kbd.xkb_keymap = NULL; + } + if (seat->kbd.xkb_state != NULL) { + xkb_state_unref(seat->kbd.xkb_state); + seat->kbd.xkb_state = NULL; + } + + key_binding_unload_keymap(wayl->key_binding_manager, seat); + + /* Verify keymap is in a format we understand */ + switch ((enum wl_keyboard_keymap_format)format) { + case WL_KEYBOARD_KEYMAP_FORMAT_NO_KEYMAP: + goto err; + + case WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1: + break; + + default: + LOG_WARN("unrecognized keymap format: %u", format); + goto err; + } + + char *map_str = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0); + if (map_str == MAP_FAILED) { + LOG_ERRNO("failed to mmap keyboard keymap"); + goto err; + } + + while (map_str[size - 1] == '\0') + size--; + + if (seat->kbd.xkb != NULL) { + seat->kbd.xkb_keymap = xkb_keymap_new_from_buffer( + seat->kbd.xkb, map_str, size, XKB_KEYMAP_FORMAT_TEXT_V1, + XKB_KEYMAP_COMPILE_NO_FLAGS); + } + + munmap(map_str, size); + + if (seat->kbd.xkb_keymap != NULL) { + seat->kbd.xkb_state = xkb_state_new(seat->kbd.xkb_keymap); + + seat->kbd.mod_shift = + xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); + seat->kbd.mod_alt = + xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_ALT); + seat->kbd.mod_ctrl = + xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_CTRL); + seat->kbd.mod_super = + xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_LOGO); + seat->kbd.mod_caps = + xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_CAPS); + seat->kbd.mod_num = + xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_NUM); + + /* Significant modifiers in the legacy keyboard protocol */ + seat->kbd.legacy_significant = 0; + if (seat->kbd.mod_shift != XKB_MOD_INVALID) + seat->kbd.legacy_significant |= 1 << seat->kbd.mod_shift; + if (seat->kbd.mod_alt != XKB_MOD_INVALID) + seat->kbd.legacy_significant |= 1 << seat->kbd.mod_alt; + if (seat->kbd.mod_ctrl != XKB_MOD_INVALID) + seat->kbd.legacy_significant |= 1 << seat->kbd.mod_ctrl; + if (seat->kbd.mod_super != XKB_MOD_INVALID) + seat->kbd.legacy_significant |= 1 << seat->kbd.mod_super; + + /* Significant modifiers in the kitty keyboard protocol */ + seat->kbd.kitty_significant = seat->kbd.legacy_significant; + if (seat->kbd.mod_caps != XKB_MOD_INVALID) + seat->kbd.kitty_significant |= 1 << seat->kbd.mod_caps; + if (seat->kbd.mod_num != XKB_MOD_INVALID) + seat->kbd.kitty_significant |= 1 << seat->kbd.mod_num; + + /* + * Create a mask of all "virtual" modifiers. Some compositors + * add these *in addition* to the "real" modifiers (Mod1, + * Mod2, etc). + * + * Since our modifier logic (both for internal shortcut + * processing, and e.g. the kitty keyboard protocol) makes + * very few assumptions on available modifiers, which keys map + * to which modifier etc, the presence of virtual modifiers + * causes various things to break. + * + * For example, if a foot shortcut is Mod1+b (i.e. Alt+b), it + * won't match if the compositor _also_ sets the Alt modifier + * (the corresponding shortcut in foot would be Alt+Mod1+b). + * + * See https://codeberg.org/dnkl/foot/issues/2009 + * + * Mutter (GNOME) is known to set the virtual modifiers in + * addtiion to the real modifiers. + * + * As far as I know, there's no compositor that _only_ sets + * virtual modifiers (don't think that's even legal...?) + */ + { + xkb_mod_index_t alt = + xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_ALT); + xkb_mod_index_t meta = + xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_META); + xkb_mod_index_t super = + xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_SUPER); + xkb_mod_index_t hyper = + xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_HYPER); + xkb_mod_index_t num_lock = + xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_NUM); + xkb_mod_index_t scroll_lock = + xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_SCROLL); + xkb_mod_index_t level_three = + xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_LEVEL3); + xkb_mod_index_t level_five = + xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_LEVEL5); + + xkb_mod_index_t ignore = 0; + + if (alt != XKB_MOD_INVALID) + ignore |= 1 << alt; + if (meta != XKB_MOD_INVALID) + ignore |= 1 << meta; + if (super != XKB_MOD_INVALID) + ignore |= 1 << super; + if (hyper != XKB_MOD_INVALID) + ignore |= 1 << hyper; + if (num_lock != XKB_MOD_INVALID) + ignore |= 1 << num_lock; + if (scroll_lock != XKB_MOD_INVALID) + ignore |= 1 << scroll_lock; + if (level_three != XKB_MOD_INVALID) + ignore |= 1 << level_three; + if (level_five != XKB_MOD_INVALID) + ignore |= 1 << level_five; + + seat->kbd.virtual_modifiers = ignore; + } + + seat->kbd.key_arrow_up = xkb_keymap_key_by_name(seat->kbd.xkb_keymap, "UP"); + seat->kbd.key_arrow_down = + xkb_keymap_key_by_name(seat->kbd.xkb_keymap, "DOWN"); + } + + key_binding_load_keymap(wayl->key_binding_manager, seat); err: - close(fd); + close(fd); } -static void -keyboard_enter(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, - struct wl_surface *surface, struct wl_array *keys) -{ - xassert(surface != NULL); - xassert(serial != 0); +static void keyboard_enter(void *data, struct wl_keyboard *wl_keyboard, + uint32_t serial, struct wl_surface *surface, + struct wl_array *keys) { + xassert(surface != NULL); + xassert(serial != 0); - struct seat *seat = data; - struct wl_window *win = wl_surface_get_user_data(surface); - struct terminal *term = win->term; + struct seat *seat = data; + struct wl_window *win = wl_surface_get_user_data(surface); + struct terminal *term = win->term; - LOG_DBG("%s: keyboard_enter: keyboard=%p, serial=%u, surface=%p", - seat->name, (void *)wl_keyboard, serial, (void *)surface); + LOG_DBG("%s: keyboard_enter: keyboard=%p, serial=%u, surface=%p", seat->name, + (void *)wl_keyboard, serial, (void *)surface); - term_kbd_focus_in(term); - seat->kbd_focus = term; - seat->kbd.serial = serial; + term_kbd_focus_in(term); + seat->kbd_focus = term; + seat->kbd.serial = serial; } -static bool -start_repeater(struct seat *seat, uint32_t key) -{ - if (seat->kbd.repeat.dont_re_repeat) - return true; - - if (seat->kbd.repeat.rate == 0) - return true; - - struct itimerspec t = { - .it_value = {.tv_sec = 0, .tv_nsec = seat->kbd.repeat.delay * 1000000}, - .it_interval = {.tv_sec = 0, .tv_nsec = 1000000000 / seat->kbd.repeat.rate}, - }; - - if (t.it_value.tv_nsec >= 1000000000) { - t.it_value.tv_sec += t.it_value.tv_nsec / 1000000000; - t.it_value.tv_nsec %= 1000000000; - } - if (t.it_interval.tv_nsec >= 1000000000) { - t.it_interval.tv_sec += t.it_interval.tv_nsec / 1000000000; - t.it_interval.tv_nsec %= 1000000000; - } - if (timerfd_settime(seat->kbd.repeat.fd, 0, &t, NULL) < 0) { - LOG_ERRNO("%s: failed to arm keyboard repeat timer", seat->name); - return false; - } - - seat->kbd.repeat.key = key; +static bool start_repeater(struct seat *seat, uint32_t key) { + if (seat->kbd.repeat.dont_re_repeat) return true; -} - -static bool -stop_repeater(struct seat *seat, uint32_t key) -{ - if (key != -1 && key != seat->kbd.repeat.key) - return true; - - if (timerfd_settime(seat->kbd.repeat.fd, 0, &(struct itimerspec){{0}}, NULL) < 0) { - LOG_ERRNO("%s: failed to disarm keyboard repeat timer", seat->name); - return false; - } + if (seat->kbd.repeat.rate == 0) return true; + + struct itimerspec t = { + .it_value = {.tv_sec = 0, .tv_nsec = seat->kbd.repeat.delay * 1000000}, + .it_interval = {.tv_sec = 0, + .tv_nsec = 1000000000 / seat->kbd.repeat.rate}, + }; + + if (t.it_value.tv_nsec >= 1000000000) { + t.it_value.tv_sec += t.it_value.tv_nsec / 1000000000; + t.it_value.tv_nsec %= 1000000000; + } + if (t.it_interval.tv_nsec >= 1000000000) { + t.it_interval.tv_sec += t.it_interval.tv_nsec / 1000000000; + t.it_interval.tv_nsec %= 1000000000; + } + if (timerfd_settime(seat->kbd.repeat.fd, 0, &t, NULL) < 0) { + LOG_ERRNO("%s: failed to arm keyboard repeat timer", seat->name); + return false; + } + + seat->kbd.repeat.key = key; + return true; } -static void -keyboard_leave(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, - struct wl_surface *surface) -{ - struct seat *seat = data; +static bool stop_repeater(struct seat *seat, uint32_t key) { + if (key != -1 && key != seat->kbd.repeat.key) + return true; - LOG_DBG("keyboard_leave: keyboard=%p, serial=%u, surface=%p", - (void *)wl_keyboard, serial, (void *)surface); + if (timerfd_settime(seat->kbd.repeat.fd, 0, &(struct itimerspec){{0}}, NULL) < + 0) { + LOG_ERRNO("%s: failed to disarm keyboard repeat timer", seat->name); + return false; + } - xassert( - seat->kbd_focus == NULL || - surface == NULL || /* Seen on Sway 1.2 */ - ((const struct wl_window *)wl_surface_get_user_data(surface))->term == seat->kbd_focus - ); + return true; +} - struct terminal *old_focused = seat->kbd_focus; - seat->kbd_focus = NULL; +static void keyboard_leave(void *data, struct wl_keyboard *wl_keyboard, + uint32_t serial, struct wl_surface *surface) { + struct seat *seat = data; - stop_repeater(seat, -1); - seat->kbd.shift = false; - seat->kbd.alt = false; - seat->kbd.ctrl = false; - seat->kbd.super = false; + LOG_DBG("keyboard_leave: keyboard=%p, serial=%u, surface=%p", + (void *)wl_keyboard, serial, (void *)surface); - if (seat->kbd.xkb_compose_state != NULL) - xkb_compose_state_reset(seat->kbd.xkb_compose_state); + xassert(seat->kbd_focus == NULL || surface == NULL || /* Seen on Sway 1.2 */ + ((const struct wl_window *)wl_surface_get_user_data(surface))->term == + seat->kbd_focus); - if (seat->kbd.xkb_state != NULL && seat->kbd.xkb_keymap != NULL) { - const xkb_layout_index_t layout_count = xkb_keymap_num_layouts(seat->kbd.xkb_keymap); + struct terminal *old_focused = seat->kbd_focus; + seat->kbd_focus = NULL; - for (xkb_layout_index_t i = 0; i < layout_count; i++) - xkb_state_update_mask(seat->kbd.xkb_state, 0, 0, 0, i, i, i); - } + stop_repeater(seat, -1); + seat->kbd.shift = false; + seat->kbd.alt = false; + seat->kbd.ctrl = false; + seat->kbd.super = false; - if (old_focused != NULL) { - seat->pointer.hidden = false; - term_xcursor_update_for_seat(old_focused, seat); - term_kbd_focus_out(old_focused); - } else { - /* - * Sway bug - under certain conditions we get a - * keyboard_leave() (and keyboard_key()) without first having - * received a keyboard_enter() - */ - LOG_WARN( - "compositor sent keyboard_leave event without a keyboard_enter " - "event: surface=%p", (void *)surface); - } + if (seat->kbd.xkb_compose_state != NULL) + xkb_compose_state_reset(seat->kbd.xkb_compose_state); + + if (seat->kbd.xkb_state != NULL && seat->kbd.xkb_keymap != NULL) { + const xkb_layout_index_t layout_count = + xkb_keymap_num_layouts(seat->kbd.xkb_keymap); + + for (xkb_layout_index_t i = 0; i < layout_count; i++) + xkb_state_update_mask(seat->kbd.xkb_state, 0, 0, 0, i, i, i); + } + + if (old_focused != NULL) { + seat->pointer.hidden = false; + term_xcursor_update_for_seat(old_focused, seat); + term_kbd_focus_out(old_focused); + } else { + /* + * Sway bug - under certain conditions we get a + * keyboard_leave() (and keyboard_key()) without first having + * received a keyboard_enter() + */ + LOG_WARN("compositor sent keyboard_leave event without a keyboard_enter " + "event: surface=%p", + (void *)surface); + } +} + +static const struct key_data *keymap_data_for_sym(xkb_keysym_t sym, + size_t *count) { + switch (sym) { + case XKB_KEY_Escape: + *count = ALEN(key_escape); + return key_escape; + case XKB_KEY_Return: + *count = ALEN(key_return); + return key_return; + case XKB_KEY_ISO_Left_Tab: + *count = ALEN(key_iso_left_tab); + return key_iso_left_tab; + case XKB_KEY_Tab: + *count = ALEN(key_tab); + return key_tab; + case XKB_KEY_BackSpace: + *count = ALEN(key_backspace); + return key_backspace; + case XKB_KEY_Up: + *count = ALEN(key_up); + return key_up; + case XKB_KEY_Down: + *count = ALEN(key_down); + return key_down; + case XKB_KEY_Right: + *count = ALEN(key_right); + return key_right; + case XKB_KEY_Left: + *count = ALEN(key_left); + return key_left; + case XKB_KEY_Home: + *count = ALEN(key_home); + return key_home; + case XKB_KEY_End: + *count = ALEN(key_end); + return key_end; + case XKB_KEY_Insert: + *count = ALEN(key_insert); + return key_insert; + case XKB_KEY_Delete: + *count = ALEN(key_delete); + return key_delete; + case XKB_KEY_Page_Up: + *count = ALEN(key_pageup); + return key_pageup; + case XKB_KEY_Page_Down: + *count = ALEN(key_pagedown); + return key_pagedown; + case XKB_KEY_F1: + *count = ALEN(key_f1); + return key_f1; + case XKB_KEY_F2: + *count = ALEN(key_f2); + return key_f2; + case XKB_KEY_F3: + *count = ALEN(key_f3); + return key_f3; + case XKB_KEY_F4: + *count = ALEN(key_f4); + return key_f4; + case XKB_KEY_F5: + *count = ALEN(key_f5); + return key_f5; + case XKB_KEY_F6: + *count = ALEN(key_f6); + return key_f6; + case XKB_KEY_F7: + *count = ALEN(key_f7); + return key_f7; + case XKB_KEY_F8: + *count = ALEN(key_f8); + return key_f8; + case XKB_KEY_F9: + *count = ALEN(key_f9); + return key_f9; + case XKB_KEY_F10: + *count = ALEN(key_f10); + return key_f10; + case XKB_KEY_F11: + *count = ALEN(key_f11); + return key_f11; + case XKB_KEY_F12: + *count = ALEN(key_f12); + return key_f12; + case XKB_KEY_F13: + *count = ALEN(key_f13); + return key_f13; + case XKB_KEY_F14: + *count = ALEN(key_f14); + return key_f14; + case XKB_KEY_F15: + *count = ALEN(key_f15); + return key_f15; + case XKB_KEY_F16: + *count = ALEN(key_f16); + return key_f16; + case XKB_KEY_F17: + *count = ALEN(key_f17); + return key_f17; + case XKB_KEY_F18: + *count = ALEN(key_f18); + return key_f18; + case XKB_KEY_F19: + *count = ALEN(key_f19); + return key_f19; + case XKB_KEY_F20: + *count = ALEN(key_f20); + return key_f20; + case XKB_KEY_F21: + *count = ALEN(key_f21); + return key_f21; + case XKB_KEY_F22: + *count = ALEN(key_f22); + return key_f22; + case XKB_KEY_F23: + *count = ALEN(key_f23); + return key_f23; + case XKB_KEY_F24: + *count = ALEN(key_f24); + return key_f24; + case XKB_KEY_F25: + *count = ALEN(key_f25); + return key_f25; + case XKB_KEY_F26: + *count = ALEN(key_f26); + return key_f26; + case XKB_KEY_F27: + *count = ALEN(key_f27); + return key_f27; + case XKB_KEY_F28: + *count = ALEN(key_f28); + return key_f28; + case XKB_KEY_F29: + *count = ALEN(key_f29); + return key_f29; + case XKB_KEY_F30: + *count = ALEN(key_f30); + return key_f30; + case XKB_KEY_F31: + *count = ALEN(key_f31); + return key_f31; + case XKB_KEY_F32: + *count = ALEN(key_f32); + return key_f32; + case XKB_KEY_F33: + *count = ALEN(key_f33); + return key_f33; + case XKB_KEY_F34: + *count = ALEN(key_f34); + return key_f34; + case XKB_KEY_F35: + *count = ALEN(key_f35); + return key_f35; + case XKB_KEY_KP_Up: + *count = ALEN(key_kp_up); + return key_kp_up; + case XKB_KEY_KP_Down: + *count = ALEN(key_kp_down); + return key_kp_down; + case XKB_KEY_KP_Right: + *count = ALEN(key_kp_right); + return key_kp_right; + case XKB_KEY_KP_Left: + *count = ALEN(key_kp_left); + return key_kp_left; + case XKB_KEY_KP_Begin: + *count = ALEN(key_kp_begin); + return key_kp_begin; + case XKB_KEY_KP_Home: + *count = ALEN(key_kp_home); + return key_kp_home; + case XKB_KEY_KP_End: + *count = ALEN(key_kp_end); + return key_kp_end; + case XKB_KEY_KP_Insert: + *count = ALEN(key_kp_insert); + return key_kp_insert; + case XKB_KEY_KP_Delete: + *count = ALEN(key_kp_delete); + return key_kp_delete; + case XKB_KEY_KP_Page_Up: + *count = ALEN(key_kp_pageup); + return key_kp_pageup; + case XKB_KEY_KP_Page_Down: + *count = ALEN(key_kp_pagedown); + return key_kp_pagedown; + case XKB_KEY_KP_Enter: + *count = ALEN(key_kp_enter); + return key_kp_enter; + case XKB_KEY_KP_Divide: + *count = ALEN(key_kp_divide); + return key_kp_divide; + case XKB_KEY_KP_Multiply: + *count = ALEN(key_kp_multiply); + return key_kp_multiply; + case XKB_KEY_KP_Subtract: + *count = ALEN(key_kp_subtract); + return key_kp_subtract; + case XKB_KEY_KP_Add: + *count = ALEN(key_kp_add); + return key_kp_add; + case XKB_KEY_KP_Separator: + *count = ALEN(key_kp_separator); + return key_kp_separator; + case XKB_KEY_KP_Decimal: + *count = ALEN(key_kp_decimal); + return key_kp_decimal; + case XKB_KEY_KP_0: + *count = ALEN(key_kp_0); + return key_kp_0; + case XKB_KEY_KP_1: + *count = ALEN(key_kp_1); + return key_kp_1; + case XKB_KEY_KP_2: + *count = ALEN(key_kp_2); + return key_kp_2; + case XKB_KEY_KP_3: + *count = ALEN(key_kp_3); + return key_kp_3; + case XKB_KEY_KP_4: + *count = ALEN(key_kp_4); + return key_kp_4; + case XKB_KEY_KP_5: + *count = ALEN(key_kp_5); + return key_kp_5; + case XKB_KEY_KP_6: + *count = ALEN(key_kp_6); + return key_kp_6; + case XKB_KEY_KP_7: + *count = ALEN(key_kp_7); + return key_kp_7; + case XKB_KEY_KP_8: + *count = ALEN(key_kp_8); + return key_kp_8; + case XKB_KEY_KP_9: + *count = ALEN(key_kp_9); + return key_kp_9; + } + + return NULL; } static const struct key_data * -keymap_data_for_sym(xkb_keysym_t sym, size_t *count) -{ - switch (sym) { - case XKB_KEY_Escape: *count = ALEN(key_escape); return key_escape; - case XKB_KEY_Return: *count = ALEN(key_return); return key_return; - case XKB_KEY_ISO_Left_Tab: *count = ALEN(key_iso_left_tab); return key_iso_left_tab; - case XKB_KEY_Tab: *count = ALEN(key_tab); return key_tab; - case XKB_KEY_BackSpace: *count = ALEN(key_backspace); return key_backspace; - case XKB_KEY_Up: *count = ALEN(key_up); return key_up; - case XKB_KEY_Down: *count = ALEN(key_down); return key_down; - case XKB_KEY_Right: *count = ALEN(key_right); return key_right; - case XKB_KEY_Left: *count = ALEN(key_left); return key_left; - case XKB_KEY_Home: *count = ALEN(key_home); return key_home; - case XKB_KEY_End: *count = ALEN(key_end); return key_end; - case XKB_KEY_Insert: *count = ALEN(key_insert); return key_insert; - case XKB_KEY_Delete: *count = ALEN(key_delete); return key_delete; - case XKB_KEY_Page_Up: *count = ALEN(key_pageup); return key_pageup; - case XKB_KEY_Page_Down: *count = ALEN(key_pagedown); return key_pagedown; - case XKB_KEY_F1: *count = ALEN(key_f1); return key_f1; - case XKB_KEY_F2: *count = ALEN(key_f2); return key_f2; - case XKB_KEY_F3: *count = ALEN(key_f3); return key_f3; - case XKB_KEY_F4: *count = ALEN(key_f4); return key_f4; - case XKB_KEY_F5: *count = ALEN(key_f5); return key_f5; - case XKB_KEY_F6: *count = ALEN(key_f6); return key_f6; - case XKB_KEY_F7: *count = ALEN(key_f7); return key_f7; - case XKB_KEY_F8: *count = ALEN(key_f8); return key_f8; - case XKB_KEY_F9: *count = ALEN(key_f9); return key_f9; - case XKB_KEY_F10: *count = ALEN(key_f10); return key_f10; - case XKB_KEY_F11: *count = ALEN(key_f11); return key_f11; - case XKB_KEY_F12: *count = ALEN(key_f12); return key_f12; - case XKB_KEY_F13: *count = ALEN(key_f13); return key_f13; - case XKB_KEY_F14: *count = ALEN(key_f14); return key_f14; - case XKB_KEY_F15: *count = ALEN(key_f15); return key_f15; - case XKB_KEY_F16: *count = ALEN(key_f16); return key_f16; - case XKB_KEY_F17: *count = ALEN(key_f17); return key_f17; - case XKB_KEY_F18: *count = ALEN(key_f18); return key_f18; - case XKB_KEY_F19: *count = ALEN(key_f19); return key_f19; - case XKB_KEY_F20: *count = ALEN(key_f20); return key_f20; - case XKB_KEY_F21: *count = ALEN(key_f21); return key_f21; - case XKB_KEY_F22: *count = ALEN(key_f22); return key_f22; - case XKB_KEY_F23: *count = ALEN(key_f23); return key_f23; - case XKB_KEY_F24: *count = ALEN(key_f24); return key_f24; - case XKB_KEY_F25: *count = ALEN(key_f25); return key_f25; - case XKB_KEY_F26: *count = ALEN(key_f26); return key_f26; - case XKB_KEY_F27: *count = ALEN(key_f27); return key_f27; - case XKB_KEY_F28: *count = ALEN(key_f28); return key_f28; - case XKB_KEY_F29: *count = ALEN(key_f29); return key_f29; - case XKB_KEY_F30: *count = ALEN(key_f30); return key_f30; - case XKB_KEY_F31: *count = ALEN(key_f31); return key_f31; - case XKB_KEY_F32: *count = ALEN(key_f32); return key_f32; - case XKB_KEY_F33: *count = ALEN(key_f33); return key_f33; - case XKB_KEY_F34: *count = ALEN(key_f34); return key_f34; - case XKB_KEY_F35: *count = ALEN(key_f35); return key_f35; - case XKB_KEY_KP_Up: *count = ALEN(key_kp_up); return key_kp_up; - case XKB_KEY_KP_Down: *count = ALEN(key_kp_down); return key_kp_down; - case XKB_KEY_KP_Right: *count = ALEN(key_kp_right); return key_kp_right; - case XKB_KEY_KP_Left: *count = ALEN(key_kp_left); return key_kp_left; - case XKB_KEY_KP_Begin: *count = ALEN(key_kp_begin); return key_kp_begin; - case XKB_KEY_KP_Home: *count = ALEN(key_kp_home); return key_kp_home; - case XKB_KEY_KP_End: *count = ALEN(key_kp_end); return key_kp_end; - case XKB_KEY_KP_Insert: *count = ALEN(key_kp_insert); return key_kp_insert; - case XKB_KEY_KP_Delete: *count = ALEN(key_kp_delete); return key_kp_delete; - case XKB_KEY_KP_Page_Up: *count = ALEN(key_kp_pageup); return key_kp_pageup; - case XKB_KEY_KP_Page_Down: *count = ALEN(key_kp_pagedown); return key_kp_pagedown; - case XKB_KEY_KP_Enter: *count = ALEN(key_kp_enter); return key_kp_enter; - case XKB_KEY_KP_Divide: *count = ALEN(key_kp_divide); return key_kp_divide; - case XKB_KEY_KP_Multiply: *count = ALEN(key_kp_multiply); return key_kp_multiply; - case XKB_KEY_KP_Subtract: *count = ALEN(key_kp_subtract); return key_kp_subtract; - case XKB_KEY_KP_Add: *count = ALEN(key_kp_add); return key_kp_add; - case XKB_KEY_KP_Separator: *count = ALEN(key_kp_separator); return key_kp_separator; - case XKB_KEY_KP_Decimal: *count = ALEN(key_kp_decimal); return key_kp_decimal; - case XKB_KEY_KP_0: *count = ALEN(key_kp_0); return key_kp_0; - case XKB_KEY_KP_1: *count = ALEN(key_kp_1); return key_kp_1; - case XKB_KEY_KP_2: *count = ALEN(key_kp_2); return key_kp_2; - case XKB_KEY_KP_3: *count = ALEN(key_kp_3); return key_kp_3; - case XKB_KEY_KP_4: *count = ALEN(key_kp_4); return key_kp_4; - case XKB_KEY_KP_5: *count = ALEN(key_kp_5); return key_kp_5; - case XKB_KEY_KP_6: *count = ALEN(key_kp_6); return key_kp_6; - case XKB_KEY_KP_7: *count = ALEN(key_kp_7); return key_kp_7; - case XKB_KEY_KP_8: *count = ALEN(key_kp_8); return key_kp_8; - case XKB_KEY_KP_9: *count = ALEN(key_kp_9); return key_kp_9; - } +keymap_lookup(struct terminal *term, xkb_keysym_t sym, enum modifier mods) { + size_t count; + const struct key_data *info = keymap_data_for_sym(sym, &count); + if (info == NULL) return NULL; -} -static const struct key_data * -keymap_lookup(struct terminal *term, xkb_keysym_t sym, enum modifier mods) -{ - size_t count; - const struct key_data *info = keymap_data_for_sym(sym, &count); + const enum cursor_keys cursor_keys_mode = term->cursor_keys_mode; + const enum keypad_keys keypad_keys_mode = + term->num_lock_modifier ? KEYPAD_NUMERICAL : term->keypad_keys_mode; - if (info == NULL) - return NULL; + LOG_DBG("keypad mode: %d", keypad_keys_mode); - const enum cursor_keys cursor_keys_mode = term->cursor_keys_mode; - const enum keypad_keys keypad_keys_mode - = term->num_lock_modifier ? KEYPAD_NUMERICAL : term->keypad_keys_mode; + for (size_t j = 0; j < count; j++) { + enum modifier modifiers = info[j].modifiers; - LOG_DBG("keypad mode: %d", keypad_keys_mode); - - for (size_t j = 0; j < count; j++) { - enum modifier modifiers = info[j].modifiers; - - if (modifiers & MOD_MODIFY_OTHER_KEYS_STATE1) { - if (term->modify_other_keys_2) - continue; - modifiers &= ~MOD_MODIFY_OTHER_KEYS_STATE1; - } - if (modifiers & MOD_MODIFY_OTHER_KEYS_STATE2) { - if (!term->modify_other_keys_2) - continue; - modifiers &= ~MOD_MODIFY_OTHER_KEYS_STATE2; - } - - if (modifiers != MOD_ANY && modifiers != mods) - continue; - - if (info[j].cursor_keys_mode != CURSOR_KEYS_DONTCARE && - info[j].cursor_keys_mode != cursor_keys_mode) - continue; - - if (info[j].keypad_keys_mode != KEYPAD_DONTCARE && - info[j].keypad_keys_mode != keypad_keys_mode) - continue; - - return &info[j]; + if (modifiers & MOD_MODIFY_OTHER_KEYS_STATE1) { + if (term->modify_other_keys_2) + continue; + modifiers &= ~MOD_MODIFY_OTHER_KEYS_STATE1; + } + if (modifiers & MOD_MODIFY_OTHER_KEYS_STATE2) { + if (!term->modify_other_keys_2) + continue; + modifiers &= ~MOD_MODIFY_OTHER_KEYS_STATE2; } - return NULL; + if (modifiers != MOD_ANY && modifiers != mods) + continue; + + if (info[j].cursor_keys_mode != CURSOR_KEYS_DONTCARE && + info[j].cursor_keys_mode != cursor_keys_mode) + continue; + + if (info[j].keypad_keys_mode != KEYPAD_DONTCARE && + info[j].keypad_keys_mode != keypad_keys_mode) + continue; + + return &info[j]; + } + + return NULL; } -UNITTEST -{ - struct terminal term = { - .num_lock_modifier = false, - .keypad_keys_mode = KEYPAD_NUMERICAL, - .cursor_keys_mode = CURSOR_KEYS_NORMAL, - }; +UNITTEST { + struct terminal term = { + .num_lock_modifier = false, + .keypad_keys_mode = KEYPAD_NUMERICAL, + .cursor_keys_mode = CURSOR_KEYS_NORMAL, + }; - const struct key_data *info = keymap_lookup(&term, XKB_KEY_ISO_Left_Tab, MOD_SHIFT | MOD_CTRL); - xassert(info != NULL); - xassert(streq(info->seq, "\033[27;6;9~")); + const struct key_data *info = + keymap_lookup(&term, XKB_KEY_ISO_Left_Tab, MOD_SHIFT | MOD_CTRL); + xassert(info != NULL); + xassert(streq(info->seq, "\033[27;6;9~")); } -UNITTEST -{ - struct terminal term = { - .modify_other_keys_2 = false, - }; +UNITTEST { + struct terminal term = { + .modify_other_keys_2 = false, + }; - const struct key_data *info = keymap_lookup(&term, XKB_KEY_Return, MOD_ALT); - xassert(info != NULL); - xassert(streq(info->seq, "\033\r")); + const struct key_data *info = keymap_lookup(&term, XKB_KEY_Return, MOD_ALT); + xassert(info != NULL); + xassert(streq(info->seq, "\033\r")); - term.modify_other_keys_2 = true; - info = keymap_lookup(&term, XKB_KEY_Return, MOD_ALT); - xassert(info != NULL); - xassert(streq(info->seq, "\033[27;3;13~")); + term.modify_other_keys_2 = true; + info = keymap_lookup(&term, XKB_KEY_Return, MOD_ALT); + xassert(info != NULL); + xassert(streq(info->seq, "\033[27;3;13~")); } -void -get_current_modifiers(const struct seat *seat, - xkb_mod_mask_t *effective, - xkb_mod_mask_t *consumed, uint32_t key, - bool filter_locked) -{ - if (unlikely(seat->kbd.xkb_state == NULL)) { - if (effective != NULL) - *effective = 0; - if (consumed != NULL) - *consumed = 0; +void get_current_modifiers(const struct seat *seat, xkb_mod_mask_t *effective, + xkb_mod_mask_t *consumed, uint32_t key, + bool filter_locked) { + if (unlikely(seat->kbd.xkb_state == NULL)) { + if (effective != NULL) + *effective = 0; + if (consumed != NULL) + *consumed = 0; + } + + else { + const xkb_mod_mask_t locked = + xkb_state_serialize_mods(seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); + + if (effective != NULL) { + *effective = xkb_state_serialize_mods(seat->kbd.xkb_state, + XKB_STATE_MODS_EFFECTIVE); + + if (filter_locked) + *effective &= ~locked; } - else { - const xkb_mod_mask_t locked = - xkb_state_serialize_mods(seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); + if (consumed != NULL) { + *consumed = xkb_state_key_get_consumed_mods2(seat->kbd.xkb_state, key, + XKB_CONSUMED_MODE_XKB); - if (effective != NULL) { - *effective = xkb_state_serialize_mods( - seat->kbd.xkb_state, XKB_STATE_MODS_EFFECTIVE); - - if (filter_locked) - *effective &= ~locked; - } - - if (consumed != NULL) { - *consumed = xkb_state_key_get_consumed_mods2( - seat->kbd.xkb_state, key, XKB_CONSUMED_MODE_XKB); - - if (filter_locked) - *consumed &= ~locked; - } + if (filter_locked) + *consumed &= ~locked; } + } } struct kbd_ctx { - xkb_layout_index_t layout; - xkb_keycode_t key; - xkb_keysym_t sym; + xkb_layout_index_t layout; + xkb_keycode_t key; + xkb_keysym_t sym; - struct { - const xkb_keysym_t *syms; - size_t count; - } level0_syms; + struct { + const xkb_keysym_t *syms; + size_t count; + } level0_syms; - xkb_mod_mask_t mods; - xkb_mod_mask_t consumed; + xkb_mod_mask_t mods; + xkb_mod_mask_t consumed; - struct { - const uint8_t *buf; - size_t count; - } utf8; - uint32_t *utf32; + struct { + const uint8_t *buf; + size_t count; + } utf8; + uint32_t *utf32; - enum xkb_compose_status compose_status; - enum wl_keyboard_key_state key_state; + enum xkb_compose_status compose_status; + enum wl_keyboard_key_state key_state; }; -static bool -legacy_kbd_protocol(struct seat *seat, struct terminal *term, - const struct kbd_ctx *ctx) -{ - if (ctx->key_state != WL_KEYBOARD_KEY_STATE_PRESSED) - return false; - if (ctx->compose_status == XKB_COMPOSE_COMPOSING) - return false; +static bool legacy_kbd_protocol(struct seat *seat, struct terminal *term, + const struct kbd_ctx *ctx) { + if (ctx->key_state != WL_KEYBOARD_KEY_STATE_PRESSED) + return false; + if (ctx->compose_status == XKB_COMPOSE_COMPOSING) + return false; - enum modifier keymap_mods = MOD_NONE; - keymap_mods |= seat->kbd.shift ? MOD_SHIFT : MOD_NONE; - keymap_mods |= seat->kbd.alt ? MOD_ALT : MOD_NONE; - keymap_mods |= seat->kbd.ctrl ? MOD_CTRL : MOD_NONE; - keymap_mods |= seat->kbd.super ? MOD_META : MOD_NONE; + enum modifier keymap_mods = MOD_NONE; + keymap_mods |= seat->kbd.shift ? MOD_SHIFT : MOD_NONE; + keymap_mods |= seat->kbd.alt ? MOD_ALT : MOD_NONE; + keymap_mods |= seat->kbd.ctrl ? MOD_CTRL : MOD_NONE; + keymap_mods |= seat->kbd.super ? MOD_META : MOD_NONE; - const xkb_keysym_t sym = ctx->sym; - const size_t count = ctx->utf8.count; - const uint8_t *const utf8 = ctx->utf8.buf; + const xkb_keysym_t sym = ctx->sym; + const size_t count = ctx->utf8.count; + const uint8_t *const utf8 = ctx->utf8.buf; - const struct key_data *keymap = keymap_lookup(term, sym, keymap_mods); - if (keymap != NULL) { - term_to_slave(term, keymap->seq, strlen(keymap->seq)); - return true; - } + const struct key_data *keymap = keymap_lookup(term, sym, keymap_mods); + if (keymap != NULL) { + term_to_slave(term, keymap->seq, strlen(keymap->seq)); + return true; + } - if (count == 0) - return false; + if (count == 0) + return false; #define is_control_key(x) ((x) >= 0x40 && (x) <= 0x7f) #define IS_CTRL(x) ((x) < 0x20 || ((x) >= 0x7f && (x) <= 0x9f)) - //LOG_DBG("term->modify_other_keys=%d, count=%zu, is_ctrl=%d (utf8=0x%02x), sym=%d", - //term->modify_other_keys_2, count, IS_CTRL(utf8[0]), utf8[0], sym); + // LOG_DBG("term->modify_other_keys=%d, count=%zu, is_ctrl=%d (utf8=0x%02x), + // sym=%d", term->modify_other_keys_2, count, IS_CTRL(utf8[0]), utf8[0], sym); - bool ctrl_is_in_effect = (keymap_mods & MOD_CTRL) != 0; - bool ctrl_seq = is_control_key(sym) || (count == 1 && IS_CTRL(utf8[0])); + bool ctrl_is_in_effect = (keymap_mods & MOD_CTRL) != 0; + bool ctrl_seq = is_control_key(sym) || (count == 1 && IS_CTRL(utf8[0])); - bool modify_other_keys2_in_effect = false; + bool modify_other_keys2_in_effect = false; - if (term->modify_other_keys_2) { - /* - * Try to mimic XTerm's behavior, when holding shift: - * - * - if other modifiers are pressed (e.g. Alt), emit a CSI escape - * - upper-case symbols A-Z are encoded as an CSI escape - * - other upper-case symbols (e.g 'Ö') or emitted as is - * - non-upper cased symbols are _mostly_ emitted as is (foot - * always emits as is) - * - * Examples (assuming Swedish layout): - * - Shift-a ('A') emits a CSI - * - Shift-, (';') emits ';' - * - Shift-Alt-, (Alt-;) emits a CSI - * - Shift-ö ('Ö') emits 'Ö' - */ + if (term->modify_other_keys_2) { + /* + * Try to mimic XTerm's behavior, when holding shift: + * + * - if other modifiers are pressed (e.g. Alt), emit a CSI escape + * - upper-case symbols A-Z are encoded as an CSI escape + * - other upper-case symbols (e.g 'Ö') or emitted as is + * - non-upper cased symbols are _mostly_ emitted as is (foot + * always emits as is) + * + * Examples (assuming Swedish layout): + * - Shift-a ('A') emits a CSI + * - Shift-, (';') emits ';' + * - Shift-Alt-, (Alt-;) emits a CSI + * - Shift-ö ('Ö') emits 'Ö' + */ - /* Any modifiers, besides shift active? */ - const xkb_mod_mask_t shift_mask = 1 << seat->kbd.mod_shift; - if ((ctx->mods & ~shift_mask & seat->kbd.legacy_significant) != 0) - modify_other_keys2_in_effect = true; + /* Any modifiers, besides shift active? */ + const xkb_mod_mask_t shift_mask = 1 << seat->kbd.mod_shift; + if ((ctx->mods & ~shift_mask & seat->kbd.legacy_significant) != 0) + modify_other_keys2_in_effect = true; - else { - const xkb_layout_index_t layout_idx = xkb_state_key_get_layout( - seat->kbd.xkb_state, ctx->key); + else { + const xkb_layout_index_t layout_idx = + xkb_state_key_get_layout(seat->kbd.xkb_state, ctx->key); - /* - * Get pressed key's base symbol. - * - for 'A' (shift-a), that's 'a' - * - for ';' (shift-,), that's ',' - */ - const xkb_keysym_t *base_syms = NULL; - size_t base_count = xkb_keymap_key_get_syms_by_level( - seat->kbd.xkb_keymap, ctx->key, layout_idx, 0, &base_syms); + /* + * Get pressed key's base symbol. + * - for 'A' (shift-a), that's 'a' + * - for ';' (shift-,), that's ',' + */ + const xkb_keysym_t *base_syms = NULL; + size_t base_count = xkb_keymap_key_get_syms_by_level( + seat->kbd.xkb_keymap, ctx->key, layout_idx, 0, &base_syms); - /* Check if base symbol(s) is a-z. If so, emit CSI */ - const xkb_keysym_t lower_cased_sym = xkb_keysym_to_lower(ctx->sym); - for (size_t i = 0; i < base_count; i++) { - const xkb_keysym_t s = base_syms[i]; - if (lower_cased_sym == s && s >= XKB_KEY_a && s <= XKB_KEY_z) { - modify_other_keys2_in_effect = true; - break; - } - } + /* Check if base symbol(s) is a-z. If so, emit CSI */ + const xkb_keysym_t lower_cased_sym = xkb_keysym_to_lower(ctx->sym); + for (size_t i = 0; i < base_count; i++) { + const xkb_keysym_t s = base_syms[i]; + if (lower_cased_sym == s && s >= XKB_KEY_a && s <= XKB_KEY_z) { + modify_other_keys2_in_effect = true; + break; } + } + } + } + + if (keymap_mods != MOD_NONE && + (modify_other_keys2_in_effect || (ctrl_is_in_effect && !ctrl_seq))) { + static const int mod_param_map[32] = { + [MOD_SHIFT] = 2, + [MOD_ALT] = 3, + [MOD_SHIFT | MOD_ALT] = 4, + [MOD_CTRL] = 5, + [MOD_SHIFT | MOD_CTRL] = 6, + [MOD_ALT | MOD_CTRL] = 7, + [MOD_SHIFT | MOD_ALT | MOD_CTRL] = 8, + [MOD_META] = 9, + [MOD_META | MOD_SHIFT] = 10, + [MOD_META | MOD_ALT] = 11, + [MOD_META | MOD_SHIFT | MOD_ALT] = 12, + [MOD_META | MOD_CTRL] = 13, + [MOD_META | MOD_SHIFT | MOD_CTRL] = 14, + [MOD_META | MOD_ALT | MOD_CTRL] = 15, + [MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL] = 16, + }; + + xassert(keymap_mods < ALEN(mod_param_map)); + int modify_param = mod_param_map[keymap_mods]; + xassert(modify_param != 0); + + char reply[32]; + size_t n = + xsnprintf(reply, sizeof(reply), "\x1b[27;%d;%d~", modify_param, sym); + term_to_slave(term, reply, n); + } + + else if (keymap_mods & MOD_ALT) { + /* + * When the alt modifier is pressed, we do one out of three things: + * + * 1. we prefix the output bytes with ESC + * 2. we set the 8:th bit in the output byte + * 3. we ignore the alt modifier + * + * #1 is configured with \E[?1036, and is on by default + * + * If #1 has been disabled, we use #2, *if* it's a single byte + * we're emitting. Since this is a UTF-8 terminal, we then + * UTF8-encode the 8-bit character. #2 is configured with + * \E[?1034, and is on by default. + * + * Lastly, if both #1 and #2 have been disabled, the alt + * modifier is ignored. + */ + if (term->meta.esc_prefix) { + term_to_slave(term, "\x1b", 1); + term_to_slave(term, utf8, count); } - if (keymap_mods != MOD_NONE && (modify_other_keys2_in_effect || - (ctrl_is_in_effect && !ctrl_seq))) - { - static const int mod_param_map[32] = { - [MOD_SHIFT] = 2, - [MOD_ALT] = 3, - [MOD_SHIFT | MOD_ALT] = 4, - [MOD_CTRL] = 5, - [MOD_SHIFT | MOD_CTRL] = 6, - [MOD_ALT | MOD_CTRL] = 7, - [MOD_SHIFT | MOD_ALT | MOD_CTRL] = 8, - [MOD_META] = 9, - [MOD_META | MOD_SHIFT] = 10, - [MOD_META | MOD_ALT] = 11, - [MOD_META | MOD_SHIFT | MOD_ALT] = 12, - [MOD_META | MOD_CTRL] = 13, - [MOD_META | MOD_SHIFT | MOD_CTRL] = 14, - [MOD_META | MOD_ALT | MOD_CTRL] = 15, - [MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL] = 16, - }; + else if (term->meta.eight_bit && count == 1) { + const char32_t wc = 0x80 | utf8[0]; - xassert(keymap_mods < ALEN(mod_param_map)); - int modify_param = mod_param_map[keymap_mods]; - xassert(modify_param != 0); + char utf8_meta[MB_CUR_MAX]; + size_t chars = c32rtomb(utf8_meta, wc, &(mbstate_t){0}); - char reply[32]; - size_t n = xsnprintf(reply, sizeof(reply), "\x1b[27;%d;%d~", modify_param, sym); - term_to_slave(term, reply, n); - } - - else if (keymap_mods & MOD_ALT) { - /* - * When the alt modifier is pressed, we do one out of three things: - * - * 1. we prefix the output bytes with ESC - * 2. we set the 8:th bit in the output byte - * 3. we ignore the alt modifier - * - * #1 is configured with \E[?1036, and is on by default - * - * If #1 has been disabled, we use #2, *if* it's a single byte - * we're emitting. Since this is a UTF-8 terminal, we then - * UTF8-encode the 8-bit character. #2 is configured with - * \E[?1034, and is on by default. - * - * Lastly, if both #1 and #2 have been disabled, the alt - * modifier is ignored. - */ - if (term->meta.esc_prefix) { - term_to_slave(term, "\x1b", 1); - term_to_slave(term, utf8, count); - } - - else if (term->meta.eight_bit && count == 1) { - const char32_t wc = 0x80 | utf8[0]; - - char utf8_meta[MB_CUR_MAX]; - size_t chars = c32rtomb(utf8_meta, wc, &(mbstate_t){0}); - - if (chars != (size_t)-1) - term_to_slave(term, utf8_meta, chars); - else - term_to_slave(term, utf8, count); - } - - else { - /* Alt ignored */ - term_to_slave(term, utf8, count); - } - } else + if (chars != (size_t)-1) + term_to_slave(term, utf8_meta, chars); + else term_to_slave(term, utf8, count); - - return true; -} - -UNITTEST -{ - /* Verify the kitty keymap is sorted */ - xkb_keysym_t last = 0; - for (size_t i = 0; i < ALEN(kitty_keymap); i++) { - const struct kitty_key_data *e = &kitty_keymap[i]; - xassert(e->sym > last); - last = e->sym; } + + else { + /* Alt ignored */ + term_to_slave(term, utf8, count); + } + } else + term_to_slave(term, utf8, count); + + return true; } -static int -kitty_search(const void *_key, const void *_e) -{ - const xkb_keysym_t *key = _key; - const struct kitty_key_data *e = _e; - return *key - e->sym; +UNITTEST { + /* Verify the kitty keymap is sorted */ + xkb_keysym_t last = 0; + for (size_t i = 0; i < ALEN(kitty_keymap); i++) { + const struct kitty_key_data *e = &kitty_keymap[i]; + xassert(e->sym > last); + last = e->sym; + } } -static bool -kitty_kbd_protocol(struct seat *seat, struct terminal *term, - const struct kbd_ctx *ctx) -{ - const bool repeating = seat->kbd.repeat.dont_re_repeat; - const bool pressed = ctx->key_state == WL_KEYBOARD_KEY_STATE_PRESSED && !repeating; - const bool released = ctx->key_state == WL_KEYBOARD_KEY_STATE_RELEASED; - const bool composing = ctx->compose_status == XKB_COMPOSE_COMPOSING; - const bool composed = ctx->compose_status == XKB_COMPOSE_COMPOSED; +static int kitty_search(const void *_key, const void *_e) { + const xkb_keysym_t *key = _key; + const struct kitty_key_data *e = _e; + return *key - e->sym; +} - const enum kitty_kbd_flags flags = - term->grid->kitty_kbd.flags[term->grid->kitty_kbd.idx]; +static bool kitty_kbd_protocol(struct seat *seat, struct terminal *term, + const struct kbd_ctx *ctx) { + const bool repeating = seat->kbd.repeat.dont_re_repeat; + const bool pressed = + ctx->key_state == WL_KEYBOARD_KEY_STATE_PRESSED && !repeating; + const bool released = ctx->key_state == WL_KEYBOARD_KEY_STATE_RELEASED; + const bool composing = ctx->compose_status == XKB_COMPOSE_COMPOSING; + const bool composed = ctx->compose_status == XKB_COMPOSE_COMPOSED; - const bool disambiguate = flags & KITTY_KBD_DISAMBIGUATE; - const bool report_events = flags & KITTY_KBD_REPORT_EVENT; - const bool report_alternate = flags & KITTY_KBD_REPORT_ALTERNATE; - const bool report_all_as_escapes = flags & KITTY_KBD_REPORT_ALL; + const enum kitty_kbd_flags flags = + term->grid->kitty_kbd.flags[term->grid->kitty_kbd.idx]; - if (!report_events && released) - return false; + const bool disambiguate = flags & KITTY_KBD_DISAMBIGUATE; + const bool report_events = flags & KITTY_KBD_REPORT_EVENT; + const bool report_alternate = flags & KITTY_KBD_REPORT_ALTERNATE; + const bool report_all_as_escapes = flags & KITTY_KBD_REPORT_ALL; - /* TODO: should we even bother with this, or just say it's not supported? */ - if (!disambiguate && !report_all_as_escapes && pressed) - return legacy_kbd_protocol(seat, term, ctx); + if (!report_events && released) + return false; - const xkb_keysym_t sym = ctx->sym; - const uint32_t *utf32 = ctx->utf32; - const uint8_t *const utf8 = ctx->utf8.buf; - const size_t count = ctx->utf8.count; + /* TODO: should we even bother with this, or just say it's not supported? */ + if (!disambiguate && !report_all_as_escapes && pressed) + return legacy_kbd_protocol(seat, term, ctx); - /* Lookup sym in the pre-defined keysym table */ - const struct kitty_key_data *info = bsearch( - &sym, kitty_keymap, ALEN(kitty_keymap), sizeof(kitty_keymap[0]), - &kitty_search); - xassert(info == NULL || info->sym == sym); + const xkb_keysym_t sym = ctx->sym; + const uint32_t *utf32 = ctx->utf32; + const uint8_t *const utf8 = ctx->utf8.buf; + const size_t count = ctx->utf8.count; - xkb_mod_mask_t mods = 0; - xkb_mod_mask_t locked = 0; - xkb_mod_mask_t consumed = ctx->consumed; + /* Lookup sym in the pre-defined keysym table */ + const struct kitty_key_data *info = + bsearch(&sym, kitty_keymap, ALEN(kitty_keymap), sizeof(kitty_keymap[0]), + &kitty_search); + xassert(info == NULL || info->sym == sym); - if (info != NULL && info->is_modifier) { - /* - * Special-case modifier keys. - * - * Normally, the "current" XKB state reflects the state - * *before* the current key event. In other words, the - * modifiers for key events that affect the modifier state - * (e.g. one of the control keys, or shift keys etc) does - * *not* include the key itself. - * - * Put another way, if you press "control", the modifier set - * is empty in the key press event, but contains "ctrl" in the - * release event. - * - * The kitty protocol mandates the modifier list contain the - * key itself, in *both* the press and release event. - * - * We handle this by updating the XKB state to *include* the - * current key, retrieve the set of modifiers (including the - * set of consumed modifiers), and then revert the XKB update. - */ - xkb_state_update_key( - seat->kbd.xkb_state, ctx->key, pressed ? XKB_KEY_DOWN : XKB_KEY_UP); + xkb_mod_mask_t mods = 0; + xkb_mod_mask_t locked = 0; + xkb_mod_mask_t consumed = ctx->consumed; - get_current_modifiers(seat, &mods, NULL, 0, false); + if (info != NULL && info->is_modifier) { + /* + * Special-case modifier keys. + * + * Normally, the "current" XKB state reflects the state + * *before* the current key event. In other words, the + * modifiers for key events that affect the modifier state + * (e.g. one of the control keys, or shift keys etc) does + * *not* include the key itself. + * + * Put another way, if you press "control", the modifier set + * is empty in the key press event, but contains "ctrl" in the + * release event. + * + * The kitty protocol mandates the modifier list contain the + * key itself, in *both* the press and release event. + * + * We handle this by updating the XKB state to *include* the + * current key, retrieve the set of modifiers (including the + * set of consumed modifiers), and then revert the XKB update. + */ + xkb_state_update_key(seat->kbd.xkb_state, ctx->key, + pressed ? XKB_KEY_DOWN : XKB_KEY_UP); - locked = xkb_state_serialize_mods( - seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); - consumed = xkb_state_key_get_consumed_mods2( - seat->kbd.xkb_state, ctx->key, XKB_CONSUMED_MODE_XKB); + get_current_modifiers(seat, &mods, NULL, 0, false); + + locked = + xkb_state_serialize_mods(seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); + consumed = xkb_state_key_get_consumed_mods2(seat->kbd.xkb_state, ctx->key, + XKB_CONSUMED_MODE_XKB); #if 0 /* @@ -1341,1108 +1501,1155 @@ kitty_kbd_protocol(struct seat *seat, struct terminal *term, xkb_state_update_key( seat->kbd.xkb_state, ctx->key, pressed ? XKB_KEY_UP : XKB_KEY_DOWN); #endif + } else { + /* Same as ctx->mods, but *without* filtering locked modifiers */ + get_current_modifiers(seat, &mods, NULL, 0, false); + locked = + xkb_state_serialize_mods(seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); + } + + mods &= seat->kbd.kitty_significant; + consumed &= seat->kbd.kitty_significant; + + /* + * A note on locked modifiers; they *are* a part of the protocol, + * and *should* be included in the modifier set reported in the + * key event. + * + * However, *only* if the key would result in a CSIu *without* the + * locked modifier being enabled + * + * Translated: if *another* modifier is active, or if + * report-all-keys-as-escapes is enabled, then we include the + * locked modifier in the key event. + * + * But, if the key event would result in plain text output without + * the locked modifier, then we "ignore" the locked modifier and + * emit plain text anyway. + */ + + bool is_text = + count > 0 && utf32 != NULL && (mods & ~locked & ~consumed) == 0; + for (size_t i = 0; utf32[i] != U'\0'; i++) { + if (!isc32print(utf32[i])) { + is_text = false; + break; + } + } + + const bool report_associated_text = + (flags & KITTY_KBD_REPORT_ASSOCIATED) && is_text && !released; + + if (composing) { + /* We never emit anything while composing, *except* modifiers + * (and only in report-all-keys-as-escape-codes mode) */ + if (info != NULL && info->is_modifier) + goto emit_escapes; + + return false; + } + + if (report_all_as_escapes) + goto emit_escapes; + + if ((mods & ~locked & ~consumed) == 0) { + switch (sym) { + case XKB_KEY_Return: + if (!released) + term_to_slave(term, "\r", 1); + return true; + + case XKB_KEY_BackSpace: + if (!released) + term_to_slave(term, "\x7f", 1); + return true; + + case XKB_KEY_Tab: + if (!released) + term_to_slave(term, "\t", 1); + return true; + } + } + + /* Plain-text without modifiers, or commposed text, is emitted as-is */ + if (is_text && !released) { + term_to_slave(term, utf8, count); + return true; + } + +emit_escapes:; + unsigned int encoded_mods = 0; + if (seat->kbd.mod_shift != XKB_MOD_INVALID) + encoded_mods |= mods & (1 << seat->kbd.mod_shift) ? (1 << 0) : 0; + if (seat->kbd.mod_alt != XKB_MOD_INVALID) + encoded_mods |= mods & (1 << seat->kbd.mod_alt) ? (1 << 1) : 0; + if (seat->kbd.mod_ctrl != XKB_MOD_INVALID) + encoded_mods |= mods & (1 << seat->kbd.mod_ctrl) ? (1 << 2) : 0; + if (seat->kbd.mod_super != XKB_MOD_INVALID) + encoded_mods |= mods & (1 << seat->kbd.mod_super) ? (1 << 3) : 0; + if (seat->kbd.mod_caps != XKB_MOD_INVALID) + encoded_mods |= mods & (1 << seat->kbd.mod_caps) ? (1 << 6) : 0; + if (seat->kbd.mod_num != XKB_MOD_INVALID) + encoded_mods |= mods & (1 << seat->kbd.mod_num) ? (1 << 7) : 0; + encoded_mods++; + + /* + * Figure out the main, alternate and base key codes. + * + * The main key is the unshifted version of the generated symbol, + * the alternate key is the shifted version, and base is the + * (unshifted) key assuming the default layout. + * + * For example, the user presses shift+a, then: + * - unshifted = 'a' + * - shifted = 'A' + * - base = 'a' + * + * Base will in many cases be the same as the unshifted key, but + * may differ if the active keyboard layout is non-ASCII (examples + * would be russian, or alternative layouts like neo etc). + * + * The shifted key is what we get from XKB, i.e. the resulting key + * from all active modifiers, plus the pressed key. + */ + int unshifted = -1, shifted = -1, base = -1; + char final; + + if (info != NULL) { + /* Use code from lookup table (cursor keys, enter, tab etc)*/ + if (!info->is_modifier || report_all_as_escapes) { + shifted = info->key; + final = info->final; + } + } else { + /* Use keysym (typically its Unicode codepoint value) */ + + if (composed) + shifted = utf32[0]; /* TODO: what if there are multiple codepoints? */ + else + shifted = xkb_keysym_to_utf32(sym); + + final = 'u'; + } + + if (shifted <= 0) + return false; + + /* Base layout key. I.e the symbol the pressed key produces in + * the base/default layout (layout idx 0) */ + const xkb_keysym_t *base_syms; + int base_sym_count = xkb_keymap_key_get_syms_by_level( + seat->kbd.xkb_keymap, ctx->key, 0, 0, &base_syms); + + if (base_sym_count > 0) + base = xkb_keysym_to_utf32(base_syms[0]); + + /* + * If the keysym is shifted, use its unshifted codepoint + * instead. In other words, ctrl+a and ctrl+shift+a should both + * use the same value for 'key' (97 - i.a. 'a'). + * + * However, don't do this if a non-significant modifier was used + * to generate the symbol. This is needed since we cannot encode + * non-significant modifiers, and thus the "extra" modifier(s) + * would get lost. + * + * Example: + * + * the Swedish layout has '2', QUOTATION MARK ("double quote"), + * '@', and '²' on the same key. '2' is the base symbol. + * + * Shift+2 results in QUOTATION MARK + * AltGr+2 results in '@' + * AltGr+Shift+2 results in '²' + * + * The kitty kbd protocol can't encode AltGr. So, if we always + * used the base symbol ('2'), Alt+Shift+2 would result in the + * same escape sequence as AltGr+Alt+Shift+2. + * + * (yes, this matches what kitty does, as of 0.23.1) + */ + const bool use_level0_sym = (ctx->mods & ~seat->kbd.kitty_significant) == 0 && + ctx->level0_syms.count > 0; + + unshifted = + use_level0_sym ? xkb_keysym_to_utf32(ctx->level0_syms.syms[0]) : 0; + + xassert(encoded_mods >= 1); + + char event[4]; + if (report_events /*&& !pressed*/) { + /* Note: this deviates slightly from Kitty, which omits the + * ":1" subparameter for key press events */ + event[0] = ':'; + event[1] = '0' + (pressed ? 1 : repeating ? 2 : 3); + event[2] = '\0'; + } else + event[0] = '\0'; + + char buf[128], *p = buf; + size_t left = sizeof(buf); + size_t bytes; + + const int key = + unshifted > 0 && isc32print(unshifted) && !composed ? unshifted : shifted; + const int alternate = shifted; + + if (final == 'u' || final == '~') { + bytes = snprintf(p, left, "\x1b[%u", key); + p += bytes; + left -= bytes; + + if (report_alternate) { + bool emit_alternate = alternate > 0 && alternate != key; + bool emit_base = + base > 0 && base != key && base != alternate && isc32print(base); + + if (emit_alternate) { + bytes = snprintf(p, left, ":%u", alternate); + p += bytes; + left -= bytes; + } + + if (emit_base) { + bytes = snprintf(p, left, "%s:%u", !emit_alternate ? ":" : "", base); + p += bytes; + left -= bytes; + } + } + + bool emit_mods = encoded_mods > 1 || event[0] != '\0'; + + if (emit_mods) { + bytes = snprintf(p, left, ";%u%s", encoded_mods, event); + p += bytes; + left -= bytes; + } + + if (report_associated_text) { + bytes = snprintf(p, left, "%s;%u", !emit_mods ? ";" : "", utf32[0]); + p += bytes; + left -= bytes; + + /* Additional text codepoints */ + if (utf32[0] != U'\0') { + for (size_t i = 1; utf32[i] != U'\0'; i++) { + bytes = snprintf(p, left, ":%u", utf32[i]); + p += bytes; + left -= bytes; + } + } + } + + bytes = snprintf(p, left, "%c", final); + p += bytes; + left -= bytes; + } else { + if (encoded_mods > 1 || event[0] != '\0') { + bytes = snprintf(p, left, "\x1b[1;%u%s%c", encoded_mods, event, final); + p += bytes; + left -= bytes; } else { - /* Same as ctx->mods, but *without* filtering locked modifiers */ - get_current_modifiers(seat, &mods, NULL, 0, false); - locked = xkb_state_serialize_mods( - seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); + bytes = snprintf(p, left, "\x1b[%c", final); + p += bytes; + left -= bytes; } + } - mods &= seat->kbd.kitty_significant; - consumed &= seat->kbd.kitty_significant; - - /* - * A note on locked modifiers; they *are* a part of the protocol, - * and *should* be included in the modifier set reported in the - * key event. - * - * However, *only* if the key would result in a CSIu *without* the - * locked modifier being enabled - * - * Translated: if *another* modifier is active, or if - * report-all-keys-as-escapes is enabled, then we include the - * locked modifier in the key event. - * - * But, if the key event would result in plain text output without - * the locked modifier, then we "ignore" the locked modifier and - * emit plain text anyway. - */ - - bool is_text = count > 0 && utf32 != NULL && (mods & ~locked & ~consumed) == 0; - for (size_t i = 0; utf32[i] != U'\0'; i++) { - if (!isc32print(utf32[i])) { - is_text = false; - break; - } - } - - const bool report_associated_text = - (flags & KITTY_KBD_REPORT_ASSOCIATED) && is_text && !released; - - if (composing) { - /* We never emit anything while composing, *except* modifiers - * (and only in report-all-keys-as-escape-codes mode) */ - if (info != NULL && info->is_modifier) - goto emit_escapes; - - return false; - } - - if (report_all_as_escapes) - goto emit_escapes; - - if ((mods & ~locked & ~consumed) == 0) { - switch (sym) { - case XKB_KEY_Return: - if (!released) - term_to_slave(term, "\r", 1); - return true; - - case XKB_KEY_BackSpace: - if (!released) - term_to_slave(term, "\x7f", 1); - return true; - - case XKB_KEY_Tab: - if (!released) - term_to_slave(term, "\t", 1); - return true; - } - } - - /* Plain-text without modifiers, or commposed text, is emitted as-is */ - if (is_text && !released) { - term_to_slave(term, utf8, count); - return true; - } - -emit_escapes: - ; - unsigned int encoded_mods = 0; - if (seat->kbd.mod_shift != XKB_MOD_INVALID) - encoded_mods |= mods & (1 << seat->kbd.mod_shift) ? (1 << 0) : 0; - if (seat->kbd.mod_alt != XKB_MOD_INVALID) - encoded_mods |= mods & (1 << seat->kbd.mod_alt) ? (1 << 1) : 0; - if (seat->kbd.mod_ctrl != XKB_MOD_INVALID) - encoded_mods |= mods & (1 << seat->kbd.mod_ctrl) ? (1 << 2) : 0; - if (seat->kbd.mod_super != XKB_MOD_INVALID) - encoded_mods |= mods & (1 << seat->kbd.mod_super) ? (1 << 3) : 0; - if (seat->kbd.mod_caps != XKB_MOD_INVALID) - encoded_mods |= mods & (1 << seat->kbd.mod_caps) ? (1 << 6) : 0; - if (seat->kbd.mod_num != XKB_MOD_INVALID) - encoded_mods |= mods & (1 << seat->kbd.mod_num) ? (1 << 7) : 0; - encoded_mods++; - - /* - * Figure out the main, alternate and base key codes. - * - * The main key is the unshifted version of the generated symbol, - * the alternate key is the shifted version, and base is the - * (unshifted) key assuming the default layout. - * - * For example, the user presses shift+a, then: - * - unshifted = 'a' - * - shifted = 'A' - * - base = 'a' - * - * Base will in many cases be the same as the unshifted key, but - * may differ if the active keyboard layout is non-ASCII (examples - * would be russian, or alternative layouts like neo etc). - * - * The shifted key is what we get from XKB, i.e. the resulting key - * from all active modifiers, plus the pressed key. - */ - int unshifted = -1, shifted = -1, base = -1; - char final; - - if (info != NULL) { - /* Use code from lookup table (cursor keys, enter, tab etc)*/ - if (!info->is_modifier || report_all_as_escapes) { - shifted = info->key; - final = info->final; - } - } else { - /* Use keysym (typically its Unicode codepoint value) */ - - if (composed) - shifted = utf32[0]; /* TODO: what if there are multiple codepoints? */ - else - shifted = xkb_keysym_to_utf32(sym); - - final = 'u'; - } - - if (shifted <= 0) - return false; - - /* Base layout key. I.e the symbol the pressed key produces in - * the base/default layout (layout idx 0) */ - const xkb_keysym_t *base_syms; - int base_sym_count = xkb_keymap_key_get_syms_by_level( - seat->kbd.xkb_keymap, ctx->key, 0, 0, &base_syms); - - if (base_sym_count > 0) - base = xkb_keysym_to_utf32(base_syms[0]); - - /* - * If the keysym is shifted, use its unshifted codepoint - * instead. In other words, ctrl+a and ctrl+shift+a should both - * use the same value for 'key' (97 - i.a. 'a'). - * - * However, don't do this if a non-significant modifier was used - * to generate the symbol. This is needed since we cannot encode - * non-significant modifiers, and thus the "extra" modifier(s) - * would get lost. - * - * Example: - * - * the Swedish layout has '2', QUOTATION MARK ("double quote"), - * '@', and '²' on the same key. '2' is the base symbol. - * - * Shift+2 results in QUOTATION MARK - * AltGr+2 results in '@' - * AltGr+Shift+2 results in '²' - * - * The kitty kbd protocol can't encode AltGr. So, if we always - * used the base symbol ('2'), Alt+Shift+2 would result in the - * same escape sequence as AltGr+Alt+Shift+2. - * - * (yes, this matches what kitty does, as of 0.23.1) - */ - const bool use_level0_sym = - (ctx->mods & ~seat->kbd.kitty_significant) == 0 && ctx->level0_syms.count > 0; - - unshifted = use_level0_sym ? xkb_keysym_to_utf32(ctx->level0_syms.syms[0]) : 0; - - xassert(encoded_mods >= 1); - - char event[4]; - if (report_events /*&& !pressed*/) { - /* Note: this deviates slightly from Kitty, which omits the - * ":1" subparameter for key press events */ - event[0] = ':'; - event[1] = '0' + (pressed ? 1 : repeating ? 2 : 3); - event[2] = '\0'; - } else - event[0] = '\0'; - - char buf[128], *p = buf; - size_t left = sizeof(buf); - size_t bytes; - - const int key = unshifted > 0 && isc32print(unshifted) && !composed ? unshifted : shifted; - const int alternate = shifted; - - if (final == 'u' || final == '~') { - bytes = snprintf(p, left, "\x1b[%u", key); - p += bytes; left -= bytes; - - if (report_alternate) { - bool emit_alternate = alternate > 0 && alternate != key; - bool emit_base = base > 0 && base != key && base != alternate && isc32print(base); - - if (emit_alternate) { - bytes = snprintf(p, left, ":%u", alternate); - p += bytes; left -= bytes; - } - - if (emit_base) { - bytes = snprintf( - p, left, "%s:%u", !emit_alternate ? ":" : "", base); - p += bytes; left -= bytes; - } - } - - bool emit_mods = encoded_mods > 1 || event[0] != '\0'; - - if (emit_mods) { - bytes = snprintf(p, left, ";%u%s", encoded_mods, event); - p += bytes; left -= bytes; - } - - if (report_associated_text) { - bytes = snprintf(p, left, "%s;%u", !emit_mods ? ";" : "", utf32[0]); - p += bytes; left -= bytes; - - /* Additional text codepoints */ - if (utf32[0] != U'\0') { - for (size_t i = 1; utf32[i] != U'\0'; i++) { - bytes = snprintf(p, left, ":%u", utf32[i]); - p += bytes; left -= bytes; - } - } - } - - bytes = snprintf(p, left, "%c", final); - p += bytes; left -= bytes; - } else { - if (encoded_mods > 1 || event[0] != '\0') { - bytes = snprintf(p, left, "\x1b[1;%u%s%c", encoded_mods, event, final); - p += bytes; left -= bytes; - } else { - bytes = snprintf(p, left, "\x1b[%c", final); - p += bytes; left -= bytes; - } - } - - return term_to_slave(term, buf, sizeof(buf) - left); + return term_to_slave(term, buf, sizeof(buf) - left); } /* Copied from libxkbcommon (internal function) */ -static bool -keysym_is_modifier(xkb_keysym_t keysym) -{ - return - (keysym >= XKB_KEY_Shift_L && keysym <= XKB_KEY_Hyper_R) || - /* libX11 only goes up to XKB_KEY_ISO_Level5_Lock. */ - (keysym >= XKB_KEY_ISO_Lock && keysym <= XKB_KEY_ISO_Last_Group_Lock) || - keysym == XKB_KEY_Mode_switch || - keysym == XKB_KEY_Num_Lock; +static bool keysym_is_modifier(xkb_keysym_t keysym) { + return (keysym >= XKB_KEY_Shift_L && keysym <= XKB_KEY_Hyper_R) || + /* libX11 only goes up to XKB_KEY_ISO_Level5_Lock. */ + (keysym >= XKB_KEY_ISO_Lock && + keysym <= XKB_KEY_ISO_Last_Group_Lock) || + keysym == XKB_KEY_Mode_switch || keysym == XKB_KEY_Num_Lock; } #if defined(_DEBUG) -static void -modifier_string(xkb_mod_mask_t mods, size_t sz, char mod_str[static sz], const struct seat *seat) -{ - if (sz == 0) - return; +static void modifier_string(xkb_mod_mask_t mods, size_t sz, + char mod_str[static sz], const struct seat *seat) { + if (sz == 0) + return; - mod_str[0] = '\0'; + mod_str[0] = '\0'; - for (size_t i = 0; i < sizeof(xkb_mod_mask_t) * 8; i++) { - if (!(mods & (1u << i))) - continue; + for (size_t i = 0; i < sizeof(xkb_mod_mask_t) * 8; i++) { + if (!(mods & (1u << i))) + continue; - strcat(mod_str, xkb_keymap_mod_get_name(seat->kbd.xkb_keymap, i)); - strcat(mod_str, "+"); - } + strcat(mod_str, xkb_keymap_mod_get_name(seat->kbd.xkb_keymap, i)); + strcat(mod_str, "+"); + } - if (mod_str[0] != '\0') { - /* Strip the last '+' */ - mod_str[strlen(mod_str) - 1] = '\0'; - } + if (mod_str[0] != '\0') { + /* Strip the last '+' */ + mod_str[strlen(mod_str) - 1] = '\0'; + } - if (mod_str[0] == '\0') { - strcpy(mod_str, ""); - } + if (mod_str[0] == '\0') { + strcpy(mod_str, ""); + } } #endif -static void -key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, - uint32_t key, uint32_t state) -{ - xassert(serial != 0); +static void key_press_release(struct seat *seat, struct terminal *term, + uint32_t serial, uint32_t key, uint32_t state) { + xassert(serial != 0); - seat->kbd.serial = serial; - if (seat->kbd.xkb == NULL || - seat->kbd.xkb_keymap == NULL || - seat->kbd.xkb_state == NULL) - { - return; - } + seat->kbd.serial = serial; + if (seat->kbd.xkb == NULL || seat->kbd.xkb_keymap == NULL || + seat->kbd.xkb_state == NULL) { + return; + } - const bool pressed = state == WL_KEYBOARD_KEY_STATE_PRESSED; - //const bool repeated = pressed && seat->kbd.repeat.dont_re_repeat; - const bool released = state == WL_KEYBOARD_KEY_STATE_RELEASED; + const bool pressed = state == WL_KEYBOARD_KEY_STATE_PRESSED; + // const bool repeated = pressed && seat->kbd.repeat.dont_re_repeat; + const bool released = state == WL_KEYBOARD_KEY_STATE_RELEASED; - if (released) - stop_repeater(seat, key); + if (released) + stop_repeater(seat, key); + if (pressed) + seat->kbd.last_shortcut_sym = XKB_KEYSYM_MAX + 1; + + bool should_repeat = + pressed && xkb_keymap_key_repeats(seat->kbd.xkb_keymap, key); + + xkb_keysym_t sym = xkb_state_key_get_one_sym(seat->kbd.xkb_state, key); + + if (pressed && term->conf->mouse.hide_when_typing && + !keysym_is_modifier(sym)) { + seat->pointer.hidden = true; + term_xcursor_update_for_seat(term, seat); + } + + enum xkb_compose_status compose_status = XKB_COMPOSE_NOTHING; + + if (seat->kbd.xkb_compose_state != NULL) { if (pressed) - seat->kbd.last_shortcut_sym = XKB_KEYSYM_MAX + 1; + xkb_compose_state_feed(seat->kbd.xkb_compose_state, sym); + compose_status = xkb_compose_state_get_status(seat->kbd.xkb_compose_state); + } - bool should_repeat = - pressed && xkb_keymap_key_repeats(seat->kbd.xkb_keymap, key); + const bool composed = compose_status == XKB_COMPOSE_COMPOSED; - xkb_keysym_t sym = xkb_state_key_get_one_sym(seat->kbd.xkb_state, key); + xkb_mod_mask_t mods, consumed; + get_current_modifiers(seat, &mods, &consumed, key, true); - if (pressed && term->conf->mouse.hide_when_typing && !keysym_is_modifier(sym)) { - seat->pointer.hidden = true; - term_xcursor_update_for_seat(term, seat); + xkb_layout_index_t layout_idx = + xkb_state_key_get_layout(seat->kbd.xkb_state, key); + + const xkb_keysym_t *raw_syms = NULL; + size_t raw_count = xkb_keymap_key_get_syms_by_level(seat->kbd.xkb_keymap, key, + layout_idx, 0, &raw_syms); + + const struct key_binding_set *bindings = + key_binding_for(seat->wayl->key_binding_manager, term->conf, seat); + xassert(bindings != NULL); + + if (term->unicode_mode.active) { + if (pressed) + unicode_mode_input(seat, term, sym); + return; + } + + else if (term->is_searching) { + if (pressed) { + if (should_repeat) + start_repeater(seat, key); + + search_input(seat, term, bindings, key, sym, mods, consumed, raw_syms, + raw_count, serial); } + return; + } - enum xkb_compose_status compose_status = XKB_COMPOSE_NOTHING; + else if (urls_mode_is_active(term)) { + if (pressed) { + if (should_repeat) + start_repeater(seat, key); - if (seat->kbd.xkb_compose_state != NULL) { - if (pressed) - xkb_compose_state_feed(seat->kbd.xkb_compose_state, sym); - compose_status = xkb_compose_state_get_status( - seat->kbd.xkb_compose_state); + urls_input(seat, term, bindings, key, sym, mods, consumed, raw_syms, + raw_count, serial); } + return; + } - const bool composed = compose_status == XKB_COMPOSE_COMPOSED; + /* Tab overview: intercept navigation + activation while open. The + * configured tab-overview binding still fires (handled below), so the + * user can toggle it back off with the same key. */ + if (term->window != NULL && term->window->tab_overview_state.target_open && + pressed) { + struct wl_window *win = term->window; + __typeof__(win->tab_overview_state) *st = &win->tab_overview_state; + const size_t n = win->tab_count; - xkb_mod_mask_t mods, consumed; - get_current_modifiers(seat, &mods, &consumed, key, true); - - xkb_layout_index_t layout_idx = - xkb_state_key_get_layout(seat->kbd.xkb_state, key); - - const xkb_keysym_t *raw_syms = NULL; - size_t raw_count = xkb_keymap_key_get_syms_by_level( - seat->kbd.xkb_keymap, key, layout_idx, 0, &raw_syms); - - const struct key_binding_set *bindings = key_binding_for( - seat->wayl->key_binding_manager, term->conf, seat); - xassert(bindings != NULL); - - if (term->unicode_mode.active) { - if (pressed) - unicode_mode_input(seat, term, sym); - return; - } - - else if (term->is_searching) { - if (pressed) { - if (should_repeat) - start_repeater(seat, key); - - search_input( - seat, term, bindings, key, sym, mods, consumed, - raw_syms, raw_count, serial); + bool handled = false; + switch (sym) { + case XKB_KEY_Escape: + tab_overview_toggle(win); + handled = true; + break; + case XKB_KEY_Return: + case XKB_KEY_KP_Enter: + if (st->sel_idx < n && st->sel_idx != win->active_tab) + term_tab_switch(win, st->sel_idx); + tab_overview_close_instant(win); + handled = true; + break; + case XKB_KEY_Left: + if (n > 0) + st->sel_idx = (st->sel_idx == 0) ? n - 1 : st->sel_idx - 1; + render_refresh(term); + handled = true; + break; + case XKB_KEY_Right: + if (n > 0) + st->sel_idx = (st->sel_idx + 1) % n; + render_refresh(term); + handled = true; + break; + case XKB_KEY_Up: + case XKB_KEY_Down: { + /* Step by one row in the cached card layout */ + if (st->card_count >= 2) { + int cur_y = st->cards[st->sel_idx].y; + int cur_x = st->cards[st->sel_idx].x; + int best = -1; + int best_d = INT_MAX; + for (size_t i = 0; i < st->card_count; i++) { + if (i == st->sel_idx) + continue; + int dy = st->cards[i].y - cur_y; + if (sym == XKB_KEY_Up && dy >= 0) + continue; + if (sym == XKB_KEY_Down && dy <= 0) + continue; + int dx = st->cards[i].x - cur_x; + int d = dx * dx + dy * dy; + if (d < best_d) { + best_d = d; + best = (int)i; + } } - return; + if (best >= 0) + st->sel_idx = (size_t)best; + } + render_refresh(term); + handled = true; + break; + } + default: + if (sym >= XKB_KEY_1 && sym <= XKB_KEY_9) { + size_t idx = (size_t)(sym - XKB_KEY_1); + if (idx < n) { + if (idx != win->active_tab) + term_tab_switch(win, idx); + tab_overview_close_instant(win); + } + handled = true; + } + break; } - else if (urls_mode_is_active(term)) { - if (pressed) { - if (should_repeat) - start_repeater(seat, key); - - urls_input( - seat, term, bindings, key, sym, mods, consumed, - raw_syms, raw_count, serial); - } - return; + if (handled) { + seat->kbd.last_shortcut_sym = sym; + /* Don't try repeat; navigation is one-shot */ + return; } - /* Tab overview: intercept navigation + activation while open. The - * configured tab-overview binding still fires (handled below), so the - * user can toggle it back off with the same key. */ - if (term->window != NULL && - term->window->tab_overview_state.target_open && pressed) { - struct wl_window *win = term->window; - __typeof__(win->tab_overview_state) *st = &win->tab_overview_state; - const size_t n = win->tab_count; - - bool handled = false; - switch (sym) { - case XKB_KEY_Escape: - tab_overview_toggle(win); - handled = true; - break; - case XKB_KEY_Return: - case XKB_KEY_KP_Enter: - if (st->sel_idx < n && st->sel_idx != win->active_tab) - term_tab_switch(win, st->sel_idx); - tab_overview_close_instant(win); - handled = true; - break; - case XKB_KEY_Left: - if (n > 0) - st->sel_idx = (st->sel_idx == 0) ? n - 1 : st->sel_idx - 1; - render_refresh(term); - handled = true; - break; - case XKB_KEY_Right: - if (n > 0) - st->sel_idx = (st->sel_idx + 1) % n; - render_refresh(term); - handled = true; - break; - case XKB_KEY_Up: - case XKB_KEY_Down: { - /* Step by one row in the cached card layout */ - if (st->card_count >= 2) { - int cur_y = st->cards[st->sel_idx].y; - int cur_x = st->cards[st->sel_idx].x; - int best = -1; - int best_d = INT_MAX; - for (size_t i = 0; i < st->card_count; i++) { - if (i == st->sel_idx) continue; - int dy = st->cards[i].y - cur_y; - if (sym == XKB_KEY_Up && dy >= 0) continue; - if (sym == XKB_KEY_Down && dy <= 0) continue; - int dx = st->cards[i].x - cur_x; - int d = dx * dx + dy * dy; - if (d < best_d) { best_d = d; best = (int)i; } - } - if (best >= 0) st->sel_idx = (size_t)best; - } - render_refresh(term); - handled = true; - break; - } - default: - if (sym >= XKB_KEY_1 && sym <= XKB_KEY_9) { - size_t idx = (size_t)(sym - XKB_KEY_1); - if (idx < n) { - if (idx != win->active_tab) - term_tab_switch(win, idx); - tab_overview_close_instant(win); - } - handled = true; - } - break; - } - - if (handled) { - seat->kbd.last_shortcut_sym = sym; - /* Don't try repeat; navigation is one-shot */ - return; - } - - /* Fall through to binding match so the user's tab-overview key - * can still toggle off, but swallow everything else. */ - } + /* Fall through to binding match so the user's tab-overview key + * can still toggle off, but swallow everything else. */ + } #if defined(_DEBUG) && defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG - char sym_name[100]; - xkb_keysym_get_name(sym, sym_name, sizeof(sym_name)); + char sym_name[100]; + xkb_keysym_get_name(sym, sym_name, sizeof(sym_name)); - char active_mods_str[256] = {0}; - char consumed_mods_str[256] = {0}; - char locked_mods_str[256] = {0}; + char active_mods_str[256] = {0}; + char consumed_mods_str[256] = {0}; + char locked_mods_str[256] = {0}; - const xkb_mod_mask_t locked = - xkb_state_serialize_mods(seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); + const xkb_mod_mask_t locked = + xkb_state_serialize_mods(seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); - modifier_string(mods, sizeof(active_mods_str), active_mods_str, seat); - modifier_string(consumed, sizeof(consumed_mods_str), consumed_mods_str, seat); - modifier_string(locked, sizeof(locked_mods_str), locked_mods_str, seat); + modifier_string(mods, sizeof(active_mods_str), active_mods_str, seat); + modifier_string(consumed, sizeof(consumed_mods_str), consumed_mods_str, seat); + modifier_string(locked, sizeof(locked_mods_str), locked_mods_str, seat); - LOG_DBG("%s: %s (%u/0x%x), seat=%s, term=%p, serial=%u, " - "mods=%s (0x%08x), consumed=%s (0x%08x), locked=%s (0x%08x), " - "repeats=%d", - pressed ? "pressed" : "released", sym_name, sym, sym, - seat->name, (void *)term, serial, - active_mods_str, mods, consumed_mods_str, consumed, - locked_mods_str, locked, should_repeat); + LOG_DBG("%s: %s (%u/0x%x), seat=%s, term=%p, serial=%u, " + "mods=%s (0x%08x), consumed=%s (0x%08x), locked=%s (0x%08x), " + "repeats=%d", + pressed ? "pressed" : "released", sym_name, sym, sym, seat->name, + (void *)term, serial, active_mods_str, mods, consumed_mods_str, + consumed, locked_mods_str, locked, should_repeat); #endif - /* - * User configurable bindings - */ - if (pressed) { - /* Match untranslated symbols */ - tll_foreach(bindings->key, it) { - const struct key_binding *bind = &it->item; + /* + * User configurable bindings + */ + if (pressed) { + /* Match untranslated symbols */ + tll_foreach(bindings->key, it) { + const struct key_binding *bind = &it->item; - if (bind->mods != mods || bind->mods == 0) - continue; + if (bind->mods != mods || bind->mods == 0) + continue; - for (size_t i = 0; i < raw_count; i++) { - if (bind->k.sym == raw_syms[i] && - execute_binding(seat, term, bind, serial, 1)) - { - seat->kbd.last_shortcut_sym = sym; - goto maybe_repeat; - } - } - } - - /* Match translated symbol */ - tll_foreach(bindings->key, it) { - const struct key_binding *bind = &it->item; - - if (bind->k.sym == sym && - bind->mods == (mods & ~consumed) && - execute_binding(seat, term, bind, serial, 1)) - { - seat->kbd.last_shortcut_sym = sym; - goto maybe_repeat; - } - } - - /* Match raw key code */ - tll_foreach(bindings->key, it) { - const struct key_binding *bind = &it->item; - - if (bind->mods != mods || bind->mods == 0) - continue; - - tll_foreach(bind->k.key_codes, code) { - if (code->item == key && - execute_binding(seat, term, bind, serial, 1)) - { - seat->kbd.last_shortcut_sym = sym; - goto maybe_repeat; - } - } + for (size_t i = 0; i < raw_count; i++) { + if (bind->k.sym == raw_syms[i] && + execute_binding(seat, term, bind, serial, 1)) { + seat->kbd.last_shortcut_sym = sym; + goto maybe_repeat; } + } } - /* Overview is modal: don't pass un-bound keys through to the PTY */ - if (term->window != NULL && - (term->window->tab_overview_state.target_open || - term->window->tab_overview_state.visible)) - { - return; - } + /* Match translated symbol */ + tll_foreach(bindings->key, it) { + const struct key_binding *bind = &it->item; - if (released && seat->kbd.last_shortcut_sym == sym) { - /* - * Don't process a release event, if it corresponds to a - * triggered shortcut. - * - * 1. If we consumed a key (press) event, we shouldn't emit an - * escape for its release event. - * 2. Ignoring the incorrectness of doing so; this also caused - * us to reset the viewport. - * - * Background: if the kitty keyboard protocol was enabled, - * then the viewport was instantly reset to the bottom, after - * scrolling up. - */ - //seat->kbd.last_shortcut_sym = XKB_KEYSYM_MAX + 1; + if (bind->k.sym == sym && bind->mods == (mods & ~consumed) && + execute_binding(seat, term, bind, serial, 1)) { + seat->kbd.last_shortcut_sym = sym; goto maybe_repeat; + } } + /* Match raw key code */ + tll_foreach(bindings->key, it) { + const struct key_binding *bind = &it->item; + + if (bind->mods != mods || bind->mods == 0) + continue; + + tll_foreach(bind->k.key_codes, code) { + if (code->item == key && execute_binding(seat, term, bind, serial, 1)) { + seat->kbd.last_shortcut_sym = sym; + goto maybe_repeat; + } + } + } + } + + /* Overview is modal: don't pass un-bound keys through to the PTY */ + if (term->window != NULL && (term->window->tab_overview_state.target_open || + term->window->tab_overview_state.visible)) { + return; + } + + if (released && seat->kbd.last_shortcut_sym == sym) { /* - * Keys generating escape sequences + * Don't process a release event, if it corresponds to a + * triggered shortcut. + * + * 1. If we consumed a key (press) event, we shouldn't emit an + * escape for its release event. + * 2. Ignoring the incorrectness of doing so; this also caused + * us to reset the viewport. + * + * Background: if the kitty keyboard protocol was enabled, + * then the viewport was instantly reset to the bottom, after + * scrolling up. */ + // seat->kbd.last_shortcut_sym = XKB_KEYSYM_MAX + 1; + goto maybe_repeat; + } + /* + * Keys generating escape sequences + */ - /* - * Compose, and maybe emit "normal" character - */ + /* + * Compose, and maybe emit "normal" character + */ - xassert(seat->kbd.xkb_compose_state != NULL || !composed); + xassert(seat->kbd.xkb_compose_state != NULL || !composed); - if (compose_status == XKB_COMPOSE_CANCELLED) - goto maybe_repeat; + if (compose_status == XKB_COMPOSE_CANCELLED) + goto maybe_repeat; - int count = composed - ? xkb_compose_state_get_utf8(seat->kbd.xkb_compose_state, NULL, 0) - : xkb_state_key_get_utf8(seat->kbd.xkb_state, key, NULL, 0); + int count = + composed + ? xkb_compose_state_get_utf8(seat->kbd.xkb_compose_state, NULL, 0) + : xkb_state_key_get_utf8(seat->kbd.xkb_state, key, NULL, 0); - /* Buffer for translated key. Use a static buffer in most cases, - * and use a malloc:ed buffer when necessary */ - uint8_t buf[32]; - uint8_t *utf8 = count < sizeof(buf) ? buf : xmalloc(count + 1); - uint32_t *utf32 = NULL; + /* Buffer for translated key. Use a static buffer in most cases, + * and use a malloc:ed buffer when necessary */ + uint8_t buf[32]; + uint8_t *utf8 = count < sizeof(buf) ? buf : xmalloc(count + 1); + uint32_t *utf32 = NULL; - if (composed) { - xkb_compose_state_get_utf8( - seat->kbd.xkb_compose_state, (char *)utf8, count + 1); + if (composed) { + xkb_compose_state_get_utf8(seat->kbd.xkb_compose_state, (char *)utf8, + count + 1); - if (count > 0) - utf32 = ambstoc32((const char *)utf8); - } else { - xkb_state_key_get_utf8( - seat->kbd.xkb_state, key, (char *)utf8, count + 1); + if (count > 0) + utf32 = ambstoc32((const char *)utf8); + } else { + xkb_state_key_get_utf8(seat->kbd.xkb_state, key, (char *)utf8, count + 1); - utf32 = xcalloc(2, sizeof(utf32[0])); - utf32[0] = xkb_state_key_get_utf32(seat->kbd.xkb_state, key); - } + utf32 = xcalloc(2, sizeof(utf32[0])); + utf32[0] = xkb_state_key_get_utf32(seat->kbd.xkb_state, key); + } - struct kbd_ctx ctx = { - .layout = layout_idx, - .key = key, - .sym = sym, - .level0_syms = { - .syms = raw_syms, - .count = raw_count, - }, - .mods = mods, - .consumed = consumed, - .utf8 = { - .buf = utf8, - .count = count, - }, - .utf32 = utf32, - .compose_status = compose_status, - .key_state = state, - }; + struct kbd_ctx ctx = { + .layout = layout_idx, + .key = key, + .sym = sym, + .level0_syms = + { + .syms = raw_syms, + .count = raw_count, + }, + .mods = mods, + .consumed = consumed, + .utf8 = + { + .buf = utf8, + .count = count, + }, + .utf32 = utf32, + .compose_status = compose_status, + .key_state = state, + }; - bool handled = term->grid->kitty_kbd.flags[term->grid->kitty_kbd.idx] != 0 - ? kitty_kbd_protocol(seat, term, &ctx) - : legacy_kbd_protocol(seat, term, &ctx); + bool handled = term->grid->kitty_kbd.flags[term->grid->kitty_kbd.idx] != 0 + ? kitty_kbd_protocol(seat, term, &ctx) + : legacy_kbd_protocol(seat, term, &ctx); - if (composed && released) - xkb_compose_state_reset(seat->kbd.xkb_compose_state); + if (composed && released) + xkb_compose_state_reset(seat->kbd.xkb_compose_state); - if (utf8 != buf) - free(utf8); + if (utf8 != buf) + free(utf8); - if (handled && !keysym_is_modifier(sym)) { - term_reset_view(term); - selection_cancel(term); - } + if (handled && !keysym_is_modifier(sym)) { + term_reset_view(term); + selection_cancel(term); + } - free(utf32); + free(utf32); maybe_repeat: - clock_gettime( - term->wl->presentation_clock_id, &term->render.input_time); + clock_gettime(term->wl->presentation_clock_id, &term->render.input_time); - if (should_repeat) - start_repeater(seat, key); + if (should_repeat) + start_repeater(seat, key); } -static void -keyboard_key(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, - uint32_t time, uint32_t key, uint32_t state) -{ - struct seat *seat = data; - key_press_release(seat, seat->kbd_focus, serial, key + 8, state); +static void keyboard_key(void *data, struct wl_keyboard *wl_keyboard, + uint32_t serial, uint32_t time, uint32_t key, + uint32_t state) { + struct seat *seat = data; + key_press_release(seat, seat->kbd_focus, serial, key + 8, state); } -static void -keyboard_modifiers(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, - uint32_t mods_depressed, uint32_t mods_latched, - uint32_t mods_locked, uint32_t group) -{ - struct seat *seat = data; +static void keyboard_modifiers(void *data, struct wl_keyboard *wl_keyboard, + uint32_t serial, uint32_t mods_depressed, + uint32_t mods_latched, uint32_t mods_locked, + uint32_t group) { + struct seat *seat = data; - mods_depressed &= ~seat->kbd.virtual_modifiers; - mods_latched &= ~seat->kbd.virtual_modifiers; - mods_locked &= ~seat->kbd.virtual_modifiers; + mods_depressed &= ~seat->kbd.virtual_modifiers; + mods_latched &= ~seat->kbd.virtual_modifiers; + mods_locked &= ~seat->kbd.virtual_modifiers; #if defined(_DEBUG) - char depressed[256]; - char latched[256]; - char locked[256]; + char depressed[256]; + char latched[256]; + char locked[256]; - modifier_string(mods_depressed, sizeof(depressed), depressed, seat); - modifier_string(mods_latched, sizeof(latched), latched, seat); - modifier_string(mods_locked, sizeof(locked), locked, seat); + modifier_string(mods_depressed, sizeof(depressed), depressed, seat); + modifier_string(mods_latched, sizeof(latched), latched, seat); + modifier_string(mods_locked, sizeof(locked), locked, seat); - LOG_DBG( - "modifiers: depressed=%s (0x%x), latched=%s (0x%x), locked=%s (0x%x), " - "group=%u", - depressed, mods_depressed, latched, mods_latched, locked, mods_locked, - group); + LOG_DBG( + "modifiers: depressed=%s (0x%x), latched=%s (0x%x), locked=%s (0x%x), " + "group=%u", + depressed, mods_depressed, latched, mods_latched, locked, mods_locked, + group); #endif - if (seat->kbd.xkb_state != NULL) { - xkb_state_update_mask( - seat->kbd.xkb_state, mods_depressed, mods_latched, mods_locked, 0, 0, group); + if (seat->kbd.xkb_state != NULL) { + xkb_state_update_mask(seat->kbd.xkb_state, mods_depressed, mods_latched, + mods_locked, 0, 0, group); - /* Update state of modifiers we're interested in for e.g mouse events */ - seat->kbd.shift = seat->kbd.mod_shift != XKB_MOD_INVALID - ? xkb_state_mod_index_is_active( - seat->kbd.xkb_state, seat->kbd.mod_shift, XKB_STATE_MODS_EFFECTIVE) - : false; - seat->kbd.alt = seat->kbd.mod_alt != XKB_MOD_INVALID - ? xkb_state_mod_index_is_active( - seat->kbd.xkb_state, seat->kbd.mod_alt, XKB_STATE_MODS_EFFECTIVE) - : false; - seat->kbd.ctrl = seat->kbd.mod_ctrl != XKB_MOD_INVALID - ? xkb_state_mod_index_is_active( - seat->kbd.xkb_state, seat->kbd.mod_ctrl, XKB_STATE_MODS_EFFECTIVE) - : false; - seat->kbd.super = seat->kbd.mod_super != XKB_MOD_INVALID - ? xkb_state_mod_index_is_active( - seat->kbd.xkb_state, seat->kbd.mod_super, XKB_STATE_MODS_EFFECTIVE) - : false; - } + /* Update state of modifiers we're interested in for e.g mouse events */ + seat->kbd.shift = seat->kbd.mod_shift != XKB_MOD_INVALID + ? xkb_state_mod_index_is_active( + seat->kbd.xkb_state, seat->kbd.mod_shift, + XKB_STATE_MODS_EFFECTIVE) + : false; + seat->kbd.alt = seat->kbd.mod_alt != XKB_MOD_INVALID + ? xkb_state_mod_index_is_active( + seat->kbd.xkb_state, seat->kbd.mod_alt, + XKB_STATE_MODS_EFFECTIVE) + : false; + seat->kbd.ctrl = seat->kbd.mod_ctrl != XKB_MOD_INVALID + ? xkb_state_mod_index_is_active( + seat->kbd.xkb_state, seat->kbd.mod_ctrl, + XKB_STATE_MODS_EFFECTIVE) + : false; + seat->kbd.super = seat->kbd.mod_super != XKB_MOD_INVALID + ? xkb_state_mod_index_is_active( + seat->kbd.xkb_state, seat->kbd.mod_super, + XKB_STATE_MODS_EFFECTIVE) + : false; + } - if (seat->kbd_focus && seat->kbd_focus->active_surface == TERM_SURF_GRID) - term_xcursor_update_for_seat(seat->kbd_focus, seat); + if (seat->kbd_focus && seat->kbd_focus->active_surface == TERM_SURF_GRID) + term_xcursor_update_for_seat(seat->kbd_focus, seat); } -UNITTEST -{ - int chan[2]; - xassert(pipe2(chan, O_CLOEXEC) == 0); +UNITTEST { + int chan[2]; + xassert(pipe2(chan, O_CLOEXEC) == 0); - xassert(chan[0] >= 0); - xassert(chan[1] >= 0); + xassert(chan[0] >= 0); + xassert(chan[1] >= 0); - struct config conf = {0}; - struct grid grid = {0}; + struct config conf = {0}; + struct grid grid = {0}; - struct terminal term = { - .conf = &conf, - .grid = &grid, - .ptmx = chan[1], - .selection = { - .coords = { - .start = {-1, -1}, - .end = {-1, -1}, - }, - .auto_scroll = { - .fd = -1, - }, - }, - }; + struct terminal term = { + .conf = &conf, + .grid = &grid, + .ptmx = chan[1], + .selection = + { + .coords = + { + .start = {-1, -1}, + .end = {-1, -1}, + }, + .auto_scroll = + { + .fd = -1, + }, + }, + }; - struct key_binding_manager *key_binding_manager = key_binding_manager_new(); + struct key_binding_manager *key_binding_manager = key_binding_manager_new(); - struct wayland wayl = { - .key_binding_manager = key_binding_manager, - .terms = tll_init(), - }; + struct wayland wayl = { + .key_binding_manager = key_binding_manager, + .terms = tll_init(), + }; - struct seat seat = { - .wayl = &wayl, - .name = "unittest", - }; + struct seat seat = { + .wayl = &wayl, + .name = "unittest", + }; - tll_push_back(wayl.terms, &term); - term.wl = &wayl; + tll_push_back(wayl.terms, &term); + term.wl = &wayl; - seat.kbd.xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS); - xassert(seat.kbd.xkb != NULL); + seat.kbd.xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + xassert(seat.kbd.xkb != NULL); - grid.kitty_kbd.flags[0] = KITTY_KBD_DISAMBIGUATE | KITTY_KBD_REPORT_ALTERNATE; + grid.kitty_kbd.flags[0] = KITTY_KBD_DISAMBIGUATE | KITTY_KBD_REPORT_ALTERNATE; - /* Swedish keymap */ - { - seat.kbd.xkb_keymap = xkb_keymap_new_from_names( - seat.kbd.xkb, &(struct xkb_rule_names){.layout = "se"}, XKB_KEYMAP_COMPILE_NO_FLAGS); - if (seat.kbd.xkb_keymap == NULL) { - /* Skip test */ - goto no_keymap; - } - - seat.kbd.xkb_state = xkb_state_new(seat.kbd.xkb_keymap); - xassert(seat.kbd.xkb_state != NULL); - - seat.kbd.mod_shift = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); - seat.kbd.mod_alt = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_ALT) ; - seat.kbd.mod_ctrl = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CTRL); - seat.kbd.mod_super = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_LOGO); - seat.kbd.mod_caps = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CAPS); - seat.kbd.mod_num = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_NUM); - - /* Significant modifiers in the legacy keyboard protocol */ - seat.kbd.legacy_significant = 0; - if (seat.kbd.mod_shift != XKB_MOD_INVALID) - seat.kbd.legacy_significant |= 1 << seat.kbd.mod_shift; - if (seat.kbd.mod_alt != XKB_MOD_INVALID) - seat.kbd.legacy_significant |= 1 << seat.kbd.mod_alt; - if (seat.kbd.mod_ctrl != XKB_MOD_INVALID) - seat.kbd.legacy_significant |= 1 << seat.kbd.mod_ctrl; - if (seat.kbd.mod_super != XKB_MOD_INVALID) - seat.kbd.legacy_significant |= 1 << seat.kbd.mod_super; - - /* Significant modifiers in the kitty keyboard protocol */ - seat.kbd.kitty_significant = seat.kbd.legacy_significant; - if (seat.kbd.mod_caps != XKB_MOD_INVALID) - seat.kbd.kitty_significant |= 1 << seat.kbd.mod_caps; - if (seat.kbd.mod_num != XKB_MOD_INVALID) - seat.kbd.kitty_significant |= 1 << seat.kbd.mod_num; - - key_binding_new_for_seat(key_binding_manager, &seat); - key_binding_load_keymap(key_binding_manager, &seat); - - { - xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_ctrl; - keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); - key_press_release(&seat, &term, 1337, KEY_A + 8, WL_KEYBOARD_KEY_STATE_PRESSED); - - char escape[64] = {0}; - ssize_t count = read(chan[0], escape, sizeof(escape)); - - /* key: 97 = 'a', alternate: 65 = 'A', base: N/A, mods: 6 = ctrl+shift */ - const char expected_ctrl_shift_a[] = "\033[97:65;6u"; - xassert(count == strlen(expected_ctrl_shift_a)); - xassert(streq(escape, expected_ctrl_shift_a)); - - key_press_release(&seat, &term, 1337, KEY_A + 8, WL_KEYBOARD_KEY_STATE_RELEASED); - } - - { - xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_alt; - keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); - key_press_release(&seat, &term, 1337, KEY_2 + 8, WL_KEYBOARD_KEY_STATE_PRESSED); - - char escape[64] = {0}; - ssize_t count = read(chan[0], escape, sizeof(escape)); - - /* key;. 50 = '2', alternate: 34 = '"', base: N/A, 4 = alt+shift */ - const char expected_alt_shift_2[] = "\033[50:34;4u"; - xassert(count == strlen(expected_alt_shift_2)); - xassert(streq(escape, expected_alt_shift_2)); - - key_press_release(&seat, &term, 1337, KEY_2 + 8, WL_KEYBOARD_KEY_STATE_RELEASED); - } - - { - xkb_mod_index_t alt_gr = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, "Mod5"); - xassert(alt_gr != XKB_MOD_INVALID); - - xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_alt | 1u << alt_gr; - keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); - key_press_release(&seat, &term, 1337, KEY_2 + 8, WL_KEYBOARD_KEY_STATE_PRESSED); - - char escape[64] = {0}; - ssize_t count = read(chan[0], escape, sizeof(escape)); - - /* key; 178 = '²', alternate: N/A, base: 50 = '2', 4 = alt+shift (AltGr not part of the protocol) */ - const char expected_altgr_alt_shift_2[] = "\033[178::50;4u"; - xassert(count == strlen(expected_altgr_alt_shift_2)); - xassert(streq(escape, expected_altgr_alt_shift_2)); - - key_press_release(&seat, &term, 1337, KEY_2 + 8, WL_KEYBOARD_KEY_STATE_RELEASED); - } - - { - xkb_mod_mask_t mods = 1u << seat.kbd.mod_alt; - keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); - key_press_release(&seat, &term, 1337, KEY_BACKSPACE + 8, WL_KEYBOARD_KEY_STATE_PRESSED); - - char escape[64] = {0}; - ssize_t count = read(chan[0], escape, sizeof(escape)); - - /* key; 127 = , alternate: N/A, base: N/A, 3 = alt */ - const char expected_alt_backspace[] = "\033[127;3u"; - xassert(count == strlen(expected_alt_backspace)); - xassert(streq(escape, expected_alt_backspace)); - - key_press_release(&seat, &term, 1337, KEY_BACKSPACE + 8, WL_KEYBOARD_KEY_STATE_RELEASED); - } - - { - xkb_mod_mask_t mods = 1u << seat.kbd.mod_ctrl; - keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); - key_press_release(&seat, &term, 1337, KEY_ENTER + 8, WL_KEYBOARD_KEY_STATE_PRESSED); - - char escape[64] = {0}; - ssize_t count = read(chan[0], escape, sizeof(escape)); - - /* key; 13 = , alternate: N/A, base: N/A, 5 = ctrl */ - const char expected_ctrl_enter[] = "\033[13;5u"; - xassert(count == strlen(expected_ctrl_enter)); - xassert(streq(escape, expected_ctrl_enter)); - - key_press_release(&seat, &term, 1337, KEY_ENTER + 8, WL_KEYBOARD_KEY_STATE_RELEASED); - } - - { - xkb_mod_mask_t mods = 1u << seat.kbd.mod_ctrl; - keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); - key_press_release(&seat, &term, 1337, KEY_TAB + 8, WL_KEYBOARD_KEY_STATE_PRESSED); - - char escape[64] = {0}; - ssize_t count = read(chan[0], escape, sizeof(escape)); - - /* key; 9 = , alternate: N/A, base: N/A, 5 = ctrl */ - const char expected_ctrl_tab[] = "\033[9;5u"; - xassert(count == strlen(expected_ctrl_tab)); - xassert(streq(escape, expected_ctrl_tab)); - - key_press_release(&seat, &term, 1337, KEY_TAB + 8, WL_KEYBOARD_KEY_STATE_RELEASED); - } - - { - xkb_mod_mask_t mods = 1u << seat.kbd.mod_ctrl | 1u << seat.kbd.mod_shift; - keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); - key_press_release(&seat, &term, 1337, KEY_LEFT + 8, WL_KEYBOARD_KEY_STATE_PRESSED); - - char escape[64] = {0}; - ssize_t count = read(chan[0], escape, sizeof(escape)); - - const char expected_ctrl_shift_left[] = "\033[1;6D"; - xassert(count == strlen(expected_ctrl_shift_left)); - xassert(streq(escape, expected_ctrl_shift_left)); - - key_press_release(&seat, &term, 1337, KEY_LEFT + 8, WL_KEYBOARD_KEY_STATE_RELEASED); - } - key_binding_unload_keymap(key_binding_manager, &seat); - key_binding_remove_seat(key_binding_manager, &seat); - - xkb_state_unref(seat.kbd.xkb_state); - xkb_keymap_unref(seat.kbd.xkb_keymap); - - seat.kbd.xkb_state = NULL; - seat.kbd.xkb_keymap = NULL; + /* Swedish keymap */ + { + seat.kbd.xkb_keymap = xkb_keymap_new_from_names( + seat.kbd.xkb, &(struct xkb_rule_names){.layout = "se"}, + XKB_KEYMAP_COMPILE_NO_FLAGS); + if (seat.kbd.xkb_keymap == NULL) { + /* Skip test */ + goto no_keymap; } - /* de(neo) keymap */ + seat.kbd.xkb_state = xkb_state_new(seat.kbd.xkb_keymap); + xassert(seat.kbd.xkb_state != NULL); + + seat.kbd.mod_shift = + xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); + seat.kbd.mod_alt = + xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_ALT); + seat.kbd.mod_ctrl = + xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CTRL); + seat.kbd.mod_super = + xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_LOGO); + seat.kbd.mod_caps = + xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CAPS); + seat.kbd.mod_num = + xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_NUM); + + /* Significant modifiers in the legacy keyboard protocol */ + seat.kbd.legacy_significant = 0; + if (seat.kbd.mod_shift != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_shift; + if (seat.kbd.mod_alt != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_alt; + if (seat.kbd.mod_ctrl != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_ctrl; + if (seat.kbd.mod_super != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_super; + + /* Significant modifiers in the kitty keyboard protocol */ + seat.kbd.kitty_significant = seat.kbd.legacy_significant; + if (seat.kbd.mod_caps != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_caps; + if (seat.kbd.mod_num != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_num; + + key_binding_new_for_seat(key_binding_manager, &seat); + key_binding_load_keymap(key_binding_manager, &seat); + { - seat.kbd.xkb_keymap = xkb_keymap_new_from_names( - seat.kbd.xkb, &(struct xkb_rule_names){.layout = "us,de(neo)"}, - XKB_KEYMAP_COMPILE_NO_FLAGS); + xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_ctrl; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_A + 8, + WL_KEYBOARD_KEY_STATE_PRESSED); - if (seat.kbd.xkb_keymap == NULL) { - /* Skip test */ - goto no_keymap; - } + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); - seat.kbd.xkb_state = xkb_state_new(seat.kbd.xkb_keymap); - xassert(seat.kbd.xkb_state != NULL); + /* key: 97 = 'a', alternate: 65 = 'A', base: N/A, mods: 6 = ctrl+shift */ + const char expected_ctrl_shift_a[] = "\033[97:65;6u"; + xassert(count == strlen(expected_ctrl_shift_a)); + xassert(streq(escape, expected_ctrl_shift_a)); - seat.kbd.mod_shift = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); - seat.kbd.mod_alt = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_ALT) ; - seat.kbd.mod_ctrl = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CTRL); - seat.kbd.mod_super = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_LOGO); - seat.kbd.mod_caps = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CAPS); - seat.kbd.mod_num = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_NUM); - - /* Significant modifiers in the legacy keyboard protocol */ - seat.kbd.legacy_significant = 0; - if (seat.kbd.mod_shift != XKB_MOD_INVALID) - seat.kbd.legacy_significant |= 1 << seat.kbd.mod_shift; - if (seat.kbd.mod_alt != XKB_MOD_INVALID) - seat.kbd.legacy_significant |= 1 << seat.kbd.mod_alt; - if (seat.kbd.mod_ctrl != XKB_MOD_INVALID) - seat.kbd.legacy_significant |= 1 << seat.kbd.mod_ctrl; - if (seat.kbd.mod_super != XKB_MOD_INVALID) - seat.kbd.legacy_significant |= 1 << seat.kbd.mod_super; - - /* Significant modifiers in the kitty keyboard protocol */ - seat.kbd.kitty_significant = seat.kbd.legacy_significant; - if (seat.kbd.mod_caps != XKB_MOD_INVALID) - seat.kbd.kitty_significant |= 1 << seat.kbd.mod_caps; - if (seat.kbd.mod_num != XKB_MOD_INVALID) - seat.kbd.kitty_significant |= 1 << seat.kbd.mod_num; - - key_binding_new_for_seat(key_binding_manager, &seat); - key_binding_load_keymap(key_binding_manager, &seat); - - { - /* - * In the de(neo) layout, the Y key generates 'k'. This - * means we should get a key+alternate that indicates 'k', - * but a base key that is 'y'. - */ - xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_alt; - keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 1); - key_press_release(&seat, &term, 1337, KEY_Y + 8, WL_KEYBOARD_KEY_STATE_PRESSED); - - char escape[64] = {0}; - ssize_t count = read(chan[0], escape, sizeof(escape)); - - /* key: 107 = 'k', alternate: 75 = 'K', base: 121 = 'y', mods: 4 = alt+shift */ - const char expected_alt_shift_y[] = "\033[107:75:121;4u"; - xassert(count == strlen(expected_alt_shift_y)); - xassert(streq(escape, expected_alt_shift_y)); - - key_press_release(&seat, &term, 1337, KEY_Y + 8, WL_KEYBOARD_KEY_STATE_RELEASED); - } - - key_binding_unload_keymap(key_binding_manager, &seat); - key_binding_remove_seat(key_binding_manager, &seat); - - xkb_state_unref(seat.kbd.xkb_state); - xkb_keymap_unref(seat.kbd.xkb_keymap); - - seat.kbd.xkb_state = NULL; - seat.kbd.xkb_keymap = NULL; + key_press_release(&seat, &term, 1337, KEY_A + 8, + WL_KEYBOARD_KEY_STATE_RELEASED); } - /* us(intl) keymap */ { - seat.kbd.xkb_keymap = xkb_keymap_new_from_names( - seat.kbd.xkb, &(struct xkb_rule_names){.layout = "us", .variant = "intl"}, - XKB_KEYMAP_COMPILE_NO_FLAGS); + xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_alt; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_2 + 8, + WL_KEYBOARD_KEY_STATE_PRESSED); - if (seat.kbd.xkb_keymap == NULL) { - /* Skip test */ - goto no_keymap; - } + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); - seat.kbd.xkb_state = xkb_state_new(seat.kbd.xkb_keymap); - xassert(seat.kbd.xkb_state != NULL); + /* key;. 50 = '2', alternate: 34 = '"', base: N/A, 4 = alt+shift */ + const char expected_alt_shift_2[] = "\033[50:34;4u"; + xassert(count == strlen(expected_alt_shift_2)); + xassert(streq(escape, expected_alt_shift_2)); - seat.kbd.xkb_compose_table = xkb_compose_table_new_from_locale( - seat.kbd.xkb, setlocale(LC_CTYPE, NULL), XKB_COMPOSE_COMPILE_NO_FLAGS); - if (seat.kbd.xkb_compose_table == NULL) - goto no_keymap; - - seat.kbd.xkb_compose_state = xkb_compose_state_new( - seat.kbd.xkb_compose_table, XKB_COMPOSE_STATE_NO_FLAGS); - if (seat.kbd.xkb_compose_state == NULL) { - xkb_compose_table_unref(seat.kbd.xkb_compose_table); - goto no_keymap; - } - - seat.kbd.mod_shift = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); - seat.kbd.mod_alt = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_ALT) ; - seat.kbd.mod_ctrl = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CTRL); - seat.kbd.mod_super = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_LOGO); - seat.kbd.mod_caps = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CAPS); - seat.kbd.mod_num = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_NUM); - - /* Significant modifiers in the legacy keyboard protocol */ - seat.kbd.legacy_significant = 0; - if (seat.kbd.mod_shift != XKB_MOD_INVALID) - seat.kbd.legacy_significant |= 1 << seat.kbd.mod_shift; - if (seat.kbd.mod_alt != XKB_MOD_INVALID) - seat.kbd.legacy_significant |= 1 << seat.kbd.mod_alt; - if (seat.kbd.mod_ctrl != XKB_MOD_INVALID) - seat.kbd.legacy_significant |= 1 << seat.kbd.mod_ctrl; - if (seat.kbd.mod_super != XKB_MOD_INVALID) - seat.kbd.legacy_significant |= 1 << seat.kbd.mod_super; - - /* Significant modifiers in the kitty keyboard protocol */ - seat.kbd.kitty_significant = seat.kbd.legacy_significant; - if (seat.kbd.mod_caps != XKB_MOD_INVALID) - seat.kbd.kitty_significant |= 1 << seat.kbd.mod_caps; - if (seat.kbd.mod_num != XKB_MOD_INVALID) - seat.kbd.kitty_significant |= 1 << seat.kbd.mod_num; - - key_binding_new_for_seat(key_binding_manager, &seat); - key_binding_load_keymap(key_binding_manager, &seat); - - { - /* - * Test the compose sequence "shift+', shift+space" - * - * Should result in a double quote, but a regression - * caused it to instead emit a space. See #1987 - * - * Note: "shift+', space" also results in a double quote, - * but never regressed to a space. - */ - grid.kitty_kbd.flags[0] = KITTY_KBD_DISAMBIGUATE; - xkb_compose_state_reset(seat.kbd.xkb_compose_state); - - xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift; - keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 1); - - key_press_release(&seat, &term, 1337, KEY_APOSTROPHE + 8, WL_KEYBOARD_KEY_STATE_PRESSED); - key_press_release(&seat, &term, 1337, KEY_APOSTROPHE + 8, WL_KEYBOARD_KEY_STATE_RELEASED); - - key_press_release(&seat, &term, 1337, KEY_SPACE + 8, WL_KEYBOARD_KEY_STATE_PRESSED); - - char escape[64] = {0}; - ssize_t count = read(chan[0], escape, sizeof(escape)); - - /* key: 34 = '"', alternate: N/A, base: N/A, mods: 2 = shift */ - const char expected_shift_apostrophe[] = "\033[34;2u"; - xassert(count == strlen(expected_shift_apostrophe)); - xassert(streq(escape, expected_shift_apostrophe)); - - key_press_release(&seat, &term, 1337, KEY_SPACE + 8, WL_KEYBOARD_KEY_STATE_RELEASED); - - grid.kitty_kbd.flags[0] = KITTY_KBD_DISAMBIGUATE | KITTY_KBD_REPORT_ALTERNATE; - } - - key_binding_unload_keymap(key_binding_manager, &seat); - key_binding_remove_seat(key_binding_manager, &seat); - - xkb_compose_state_unref(seat.kbd.xkb_compose_state); - xkb_compose_table_unref(seat.kbd.xkb_compose_table); - - xkb_state_unref(seat.kbd.xkb_state); - xkb_keymap_unref(seat.kbd.xkb_keymap); - - seat.kbd.xkb_state = NULL; - seat.kbd.xkb_keymap = NULL; + key_press_release(&seat, &term, 1337, KEY_2 + 8, + WL_KEYBOARD_KEY_STATE_RELEASED); } + { + xkb_mod_index_t alt_gr = + xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, "Mod5"); + xassert(alt_gr != XKB_MOD_INVALID); + + xkb_mod_mask_t mods = + 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_alt | 1u << alt_gr; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_2 + 8, + WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key; 178 = '²', alternate: N/A, base: 50 = '2', 4 = alt+shift (AltGr + * not part of the protocol) */ + const char expected_altgr_alt_shift_2[] = "\033[178::50;4u"; + xassert(count == strlen(expected_altgr_alt_shift_2)); + xassert(streq(escape, expected_altgr_alt_shift_2)); + + key_press_release(&seat, &term, 1337, KEY_2 + 8, + WL_KEYBOARD_KEY_STATE_RELEASED); + } + + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_alt; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_BACKSPACE + 8, + WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key; 127 = , alternate: N/A, base: N/A, 3 = alt */ + const char expected_alt_backspace[] = "\033[127;3u"; + xassert(count == strlen(expected_alt_backspace)); + xassert(streq(escape, expected_alt_backspace)); + + key_press_release(&seat, &term, 1337, KEY_BACKSPACE + 8, + WL_KEYBOARD_KEY_STATE_RELEASED); + } + + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_ctrl; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_ENTER + 8, + WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key; 13 = , alternate: N/A, base: N/A, 5 = ctrl */ + const char expected_ctrl_enter[] = "\033[13;5u"; + xassert(count == strlen(expected_ctrl_enter)); + xassert(streq(escape, expected_ctrl_enter)); + + key_press_release(&seat, &term, 1337, KEY_ENTER + 8, + WL_KEYBOARD_KEY_STATE_RELEASED); + } + + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_ctrl; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_TAB + 8, + WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key; 9 = , alternate: N/A, base: N/A, 5 = ctrl */ + const char expected_ctrl_tab[] = "\033[9;5u"; + xassert(count == strlen(expected_ctrl_tab)); + xassert(streq(escape, expected_ctrl_tab)); + + key_press_release(&seat, &term, 1337, KEY_TAB + 8, + WL_KEYBOARD_KEY_STATE_RELEASED); + } + + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_ctrl | 1u << seat.kbd.mod_shift; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_LEFT + 8, + WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + const char expected_ctrl_shift_left[] = "\033[1;6D"; + xassert(count == strlen(expected_ctrl_shift_left)); + xassert(streq(escape, expected_ctrl_shift_left)); + + key_press_release(&seat, &term, 1337, KEY_LEFT + 8, + WL_KEYBOARD_KEY_STATE_RELEASED); + } + key_binding_unload_keymap(key_binding_manager, &seat); + key_binding_remove_seat(key_binding_manager, &seat); + + xkb_state_unref(seat.kbd.xkb_state); + xkb_keymap_unref(seat.kbd.xkb_keymap); + + seat.kbd.xkb_state = NULL; + seat.kbd.xkb_keymap = NULL; + } + + /* de(neo) keymap */ + { + seat.kbd.xkb_keymap = xkb_keymap_new_from_names( + seat.kbd.xkb, &(struct xkb_rule_names){.layout = "us,de(neo)"}, + XKB_KEYMAP_COMPILE_NO_FLAGS); + + if (seat.kbd.xkb_keymap == NULL) { + /* Skip test */ + goto no_keymap; + } + + seat.kbd.xkb_state = xkb_state_new(seat.kbd.xkb_keymap); + xassert(seat.kbd.xkb_state != NULL); + + seat.kbd.mod_shift = + xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); + seat.kbd.mod_alt = + xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_ALT); + seat.kbd.mod_ctrl = + xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CTRL); + seat.kbd.mod_super = + xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_LOGO); + seat.kbd.mod_caps = + xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CAPS); + seat.kbd.mod_num = + xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_NUM); + + /* Significant modifiers in the legacy keyboard protocol */ + seat.kbd.legacy_significant = 0; + if (seat.kbd.mod_shift != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_shift; + if (seat.kbd.mod_alt != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_alt; + if (seat.kbd.mod_ctrl != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_ctrl; + if (seat.kbd.mod_super != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_super; + + /* Significant modifiers in the kitty keyboard protocol */ + seat.kbd.kitty_significant = seat.kbd.legacy_significant; + if (seat.kbd.mod_caps != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_caps; + if (seat.kbd.mod_num != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_num; + + key_binding_new_for_seat(key_binding_manager, &seat); + key_binding_load_keymap(key_binding_manager, &seat); + + { + /* + * In the de(neo) layout, the Y key generates 'k'. This + * means we should get a key+alternate that indicates 'k', + * but a base key that is 'y'. + */ + xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_alt; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 1); + key_press_release(&seat, &term, 1337, KEY_Y + 8, + WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key: 107 = 'k', alternate: 75 = 'K', base: 121 = 'y', mods: 4 = + * alt+shift */ + const char expected_alt_shift_y[] = "\033[107:75:121;4u"; + xassert(count == strlen(expected_alt_shift_y)); + xassert(streq(escape, expected_alt_shift_y)); + + key_press_release(&seat, &term, 1337, KEY_Y + 8, + WL_KEYBOARD_KEY_STATE_RELEASED); + } + + key_binding_unload_keymap(key_binding_manager, &seat); + key_binding_remove_seat(key_binding_manager, &seat); + + xkb_state_unref(seat.kbd.xkb_state); + xkb_keymap_unref(seat.kbd.xkb_keymap); + + seat.kbd.xkb_state = NULL; + seat.kbd.xkb_keymap = NULL; + } + + /* us(intl) keymap */ + { + seat.kbd.xkb_keymap = xkb_keymap_new_from_names( + seat.kbd.xkb, + &(struct xkb_rule_names){.layout = "us", .variant = "intl"}, + XKB_KEYMAP_COMPILE_NO_FLAGS); + + if (seat.kbd.xkb_keymap == NULL) { + /* Skip test */ + goto no_keymap; + } + + seat.kbd.xkb_state = xkb_state_new(seat.kbd.xkb_keymap); + xassert(seat.kbd.xkb_state != NULL); + + seat.kbd.xkb_compose_table = xkb_compose_table_new_from_locale( + seat.kbd.xkb, setlocale(LC_CTYPE, NULL), XKB_COMPOSE_COMPILE_NO_FLAGS); + if (seat.kbd.xkb_compose_table == NULL) + goto no_keymap; + + seat.kbd.xkb_compose_state = xkb_compose_state_new( + seat.kbd.xkb_compose_table, XKB_COMPOSE_STATE_NO_FLAGS); + if (seat.kbd.xkb_compose_state == NULL) { + xkb_compose_table_unref(seat.kbd.xkb_compose_table); + goto no_keymap; + } + + seat.kbd.mod_shift = + xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); + seat.kbd.mod_alt = + xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_ALT); + seat.kbd.mod_ctrl = + xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CTRL); + seat.kbd.mod_super = + xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_LOGO); + seat.kbd.mod_caps = + xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CAPS); + seat.kbd.mod_num = + xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_NUM); + + /* Significant modifiers in the legacy keyboard protocol */ + seat.kbd.legacy_significant = 0; + if (seat.kbd.mod_shift != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_shift; + if (seat.kbd.mod_alt != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_alt; + if (seat.kbd.mod_ctrl != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_ctrl; + if (seat.kbd.mod_super != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_super; + + /* Significant modifiers in the kitty keyboard protocol */ + seat.kbd.kitty_significant = seat.kbd.legacy_significant; + if (seat.kbd.mod_caps != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_caps; + if (seat.kbd.mod_num != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_num; + + key_binding_new_for_seat(key_binding_manager, &seat); + key_binding_load_keymap(key_binding_manager, &seat); + + { + /* + * Test the compose sequence "shift+', shift+space" + * + * Should result in a double quote, but a regression + * caused it to instead emit a space. See #1987 + * + * Note: "shift+', space" also results in a double quote, + * but never regressed to a space. + */ + grid.kitty_kbd.flags[0] = KITTY_KBD_DISAMBIGUATE; + xkb_compose_state_reset(seat.kbd.xkb_compose_state); + + xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 1); + + key_press_release(&seat, &term, 1337, KEY_APOSTROPHE + 8, + WL_KEYBOARD_KEY_STATE_PRESSED); + key_press_release(&seat, &term, 1337, KEY_APOSTROPHE + 8, + WL_KEYBOARD_KEY_STATE_RELEASED); + + key_press_release(&seat, &term, 1337, KEY_SPACE + 8, + WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key: 34 = '"', alternate: N/A, base: N/A, mods: 2 = shift */ + const char expected_shift_apostrophe[] = "\033[34;2u"; + xassert(count == strlen(expected_shift_apostrophe)); + xassert(streq(escape, expected_shift_apostrophe)); + + key_press_release(&seat, &term, 1337, KEY_SPACE + 8, + WL_KEYBOARD_KEY_STATE_RELEASED); + + grid.kitty_kbd.flags[0] = + KITTY_KBD_DISAMBIGUATE | KITTY_KBD_REPORT_ALTERNATE; + } + + key_binding_unload_keymap(key_binding_manager, &seat); + key_binding_remove_seat(key_binding_manager, &seat); + + xkb_compose_state_unref(seat.kbd.xkb_compose_state); + xkb_compose_table_unref(seat.kbd.xkb_compose_table); + + xkb_state_unref(seat.kbd.xkb_state); + xkb_keymap_unref(seat.kbd.xkb_keymap); + + seat.kbd.xkb_state = NULL; + seat.kbd.xkb_keymap = NULL; + } + no_keymap: - xkb_context_unref(seat.kbd.xkb); - key_binding_manager_destroy(key_binding_manager); + xkb_context_unref(seat.kbd.xkb); + key_binding_manager_destroy(key_binding_manager); - tll_free(wayl.terms); - close(chan[0]); - close(chan[1]); + tll_free(wayl.terms); + close(chan[0]); + close(chan[1]); } -static void -keyboard_repeat_info(void *data, struct wl_keyboard *wl_keyboard, - int32_t rate, int32_t delay) -{ - struct seat *seat = data; - LOG_DBG("keyboard repeat: rate=%d, delay=%d", rate, delay); - seat->kbd.repeat.rate = rate; - seat->kbd.repeat.delay = delay; +static void keyboard_repeat_info(void *data, struct wl_keyboard *wl_keyboard, + int32_t rate, int32_t delay) { + struct seat *seat = data; + LOG_DBG("keyboard repeat: rate=%d, delay=%d", rate, delay); + seat->kbd.repeat.rate = rate; + seat->kbd.repeat.delay = delay; } const struct wl_keyboard_listener keyboard_listener = { @@ -2454,376 +2661,285 @@ const struct wl_keyboard_listener keyboard_listener = { .repeat_info = &keyboard_repeat_info, }; -void -input_repeat(struct seat *seat, uint32_t key) -{ - /* Should be cleared as soon as we loose focus */ - xassert(seat->kbd_focus != NULL); - struct terminal *term = seat->kbd_focus; +void input_repeat(struct seat *seat, uint32_t key) { + /* Should be cleared as soon as we loose focus */ + xassert(seat->kbd_focus != NULL); + struct terminal *term = seat->kbd_focus; - key_press_release(seat, term, seat->kbd.serial, key, XKB_KEY_DOWN); + key_press_release(seat, term, seat->kbd.serial, key, XKB_KEY_DOWN); } -static bool -is_top_left(const struct terminal *term, int x, int y) -{ - int csd_border_size = term->conf->csd.border_width; - return ( - (!term->window->is_constrained_top && !term->window->is_constrained_left) && - ((term->active_surface == TERM_SURF_BORDER_LEFT && y < 10 * term->scale) || - (term->active_surface == TERM_SURF_BORDER_TOP && x < (10 + csd_border_size) * term->scale))); +static bool is_top_left(const struct terminal *term, int x, int y) { + int csd_border_size = term->conf->csd.border_width; + return ((!term->window->is_constrained_top && + !term->window->is_constrained_left) && + ((term->active_surface == TERM_SURF_BORDER_LEFT && + y < 10 * term->scale) || + (term->active_surface == TERM_SURF_BORDER_TOP && + x < (10 + csd_border_size) * term->scale))); } -static bool -is_top_right(const struct terminal *term, int x, int y) -{ - int csd_border_size = term->conf->csd.border_width; - return ( - (!term->window->is_constrained_top && !term->window->is_constrained_right) && - ((term->active_surface == TERM_SURF_BORDER_RIGHT && y < 10 * term->scale) || - (term->active_surface == TERM_SURF_BORDER_TOP && x > term->width + 1 * csd_border_size * term->scale - 10 * term->scale))); +static bool is_top_right(const struct terminal *term, int x, int y) { + int csd_border_size = term->conf->csd.border_width; + return ((!term->window->is_constrained_top && + !term->window->is_constrained_right) && + ((term->active_surface == TERM_SURF_BORDER_RIGHT && + y < 10 * term->scale) || + (term->active_surface == TERM_SURF_BORDER_TOP && + x > term->width + 1 * csd_border_size * term->scale - + 10 * term->scale))); } -static bool -is_bottom_left(const struct terminal *term, int x, int y) -{ - int csd_title_size = term->conf->csd.title_height; - int csd_border_size = term->conf->csd.border_width; - return ( - (!term->window->is_constrained_bottom && !term->window->is_constrained_left) && - ((term->active_surface == TERM_SURF_BORDER_LEFT && y > csd_title_size * term->scale + term->height) || - (term->active_surface == TERM_SURF_BORDER_BOTTOM && x < (10 + csd_border_size) * term->scale))); +static bool is_bottom_left(const struct terminal *term, int x, int y) { + int csd_title_size = term->conf->csd.title_height; + int csd_border_size = term->conf->csd.border_width; + return ((!term->window->is_constrained_bottom && + !term->window->is_constrained_left) && + ((term->active_surface == TERM_SURF_BORDER_LEFT && + y > csd_title_size * term->scale + term->height) || + (term->active_surface == TERM_SURF_BORDER_BOTTOM && + x < (10 + csd_border_size) * term->scale))); } -static bool -is_bottom_right(const struct terminal *term, int x, int y) -{ - int csd_title_size = term->conf->csd.title_height; - int csd_border_size = term->conf->csd.border_width; - return ( - (!term->window->is_constrained_bottom && !term->window->is_constrained_right) && - ((term->active_surface == TERM_SURF_BORDER_RIGHT && y > csd_title_size * term->scale + term->height) || - (term->active_surface == TERM_SURF_BORDER_BOTTOM && x > term->width + 1 * csd_border_size * term->scale - 10 * term->scale))); +static bool is_bottom_right(const struct terminal *term, int x, int y) { + int csd_title_size = term->conf->csd.title_height; + int csd_border_size = term->conf->csd.border_width; + return ((!term->window->is_constrained_bottom && + !term->window->is_constrained_right) && + ((term->active_surface == TERM_SURF_BORDER_RIGHT && + y > csd_title_size * term->scale + term->height) || + (term->active_surface == TERM_SURF_BORDER_BOTTOM && + x > term->width + 1 * csd_border_size * term->scale - + 10 * term->scale))); } -enum cursor_shape -xcursor_for_csd_border(struct terminal *term, int x, int y) -{ - if (is_top_left(term, x, y)) return CURSOR_SHAPE_TOP_LEFT_CORNER; - else if (is_top_right(term, x, y)) return CURSOR_SHAPE_TOP_RIGHT_CORNER; - else if (is_bottom_left(term, x, y)) return CURSOR_SHAPE_BOTTOM_LEFT_CORNER; - else if (is_bottom_right(term, x, y)) return CURSOR_SHAPE_BOTTOM_RIGHT_CORNER; +enum cursor_shape xcursor_for_csd_border(struct terminal *term, int x, int y) { + if (is_top_left(term, x, y)) + return CURSOR_SHAPE_TOP_LEFT_CORNER; + else if (is_top_right(term, x, y)) + return CURSOR_SHAPE_TOP_RIGHT_CORNER; + else if (is_bottom_left(term, x, y)) + return CURSOR_SHAPE_BOTTOM_LEFT_CORNER; + else if (is_bottom_right(term, x, y)) + return CURSOR_SHAPE_BOTTOM_RIGHT_CORNER; - else if (term->active_surface == TERM_SURF_BORDER_LEFT) - return !term->window->is_constrained_left - ? CURSOR_SHAPE_LEFT_SIDE : CURSOR_SHAPE_LEFT_PTR; + else if (term->active_surface == TERM_SURF_BORDER_LEFT) + return !term->window->is_constrained_left ? CURSOR_SHAPE_LEFT_SIDE + : CURSOR_SHAPE_LEFT_PTR; - else if (term->active_surface == TERM_SURF_BORDER_RIGHT) - return !term->window->is_constrained_right - ? CURSOR_SHAPE_RIGHT_SIDE : CURSOR_SHAPE_LEFT_PTR; + else if (term->active_surface == TERM_SURF_BORDER_RIGHT) + return !term->window->is_constrained_right ? CURSOR_SHAPE_RIGHT_SIDE + : CURSOR_SHAPE_LEFT_PTR; - else if (term->active_surface == TERM_SURF_BORDER_TOP) - return !term->window->is_constrained_top - ? CURSOR_SHAPE_TOP_SIDE : CURSOR_SHAPE_LEFT_PTR; + else if (term->active_surface == TERM_SURF_BORDER_TOP) + return !term->window->is_constrained_top ? CURSOR_SHAPE_TOP_SIDE + : CURSOR_SHAPE_LEFT_PTR; - else if (term->active_surface == TERM_SURF_BORDER_BOTTOM) - return !term->window->is_constrained_bottom - ? CURSOR_SHAPE_BOTTOM_SIDE : CURSOR_SHAPE_LEFT_PTR; + else if (term->active_surface == TERM_SURF_BORDER_BOTTOM) + return !term->window->is_constrained_bottom ? CURSOR_SHAPE_BOTTOM_SIDE + : CURSOR_SHAPE_LEFT_PTR; - else { - BUG("Unreachable"); - return CURSOR_SHAPE_NONE; - } + else { + BUG("Unreachable"); + return CURSOR_SHAPE_NONE; + } } -static void -mouse_button_state_reset(struct seat *seat) -{ - tll_free(seat->mouse.buttons); - seat->mouse.count = 0; - seat->mouse.last_released_button = 0; - memset(&seat->mouse.last_time, 0, sizeof(seat->mouse.last_time)); +static void mouse_button_state_reset(struct seat *seat) { + tll_free(seat->mouse.buttons); + seat->mouse.count = 0; + seat->mouse.last_released_button = 0; + memset(&seat->mouse.last_time, 0, sizeof(seat->mouse.last_time)); } -static void -mouse_coord_pixel_to_cell(struct seat *seat, const struct terminal *term, - int x, int y) -{ - /* - * Translate x,y pixel coordinate to a cell coordinate, or -1 - * if the cursor is outside the grid. I.e. if it is inside the - * margins. - */ - if (x < term->margins.left) - seat->mouse.col = 0; - else if (x >= term->width - term->margins.right) - seat->mouse.col = term->cols - 1; - else - seat->mouse.col = (x - term->margins.left) / term->cell_width; +static void mouse_coord_pixel_to_cell(struct seat *seat, + const struct terminal *term, int x, + int y) { + /* + * Translate x,y pixel coordinate to a cell coordinate, or -1 + * if the cursor is outside the grid. I.e. if it is inside the + * margins. + */ + if (x < term->margins.left) + seat->mouse.col = 0; + else if (x >= term->width - term->margins.right) + seat->mouse.col = term->cols - 1; + else + seat->mouse.col = (x - term->margins.left) / term->cell_width; - if (y < term->margins.top) - seat->mouse.row = 0; - else if (y >= term->height - term->margins.bottom) - seat->mouse.row = term->rows - 1; - else - seat->mouse.row = (y - term->margins.top) / term->cell_height; + if (y < term->margins.top) + seat->mouse.row = 0; + else if (y >= term->height - term->margins.bottom) + seat->mouse.row = term->rows - 1; + else + seat->mouse.row = (y - term->margins.top) / term->cell_height; } -static bool -touch_is_active(const struct seat *seat) -{ - if (seat->wl_touch == NULL) { - return false; - } +static bool touch_is_active(const struct seat *seat) { + if (seat->wl_touch == NULL) { + return false; + } + switch (seat->touch.state) { + case TOUCH_STATE_IDLE: + case TOUCH_STATE_INHIBITED: + return false; + + case TOUCH_STATE_HELD: + case TOUCH_STATE_DRAGGING: + case TOUCH_STATE_SCROLLING: + return true; + } + + BUG("Bad touch state: %d", seat->touch.state); + return false; +} + +static void wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, + uint32_t serial, struct wl_surface *surface, + wl_fixed_t surface_x, wl_fixed_t surface_y) { + if (unlikely(surface == NULL)) { + /* Seen on mutter-3.38 */ + LOG_WARN("compositor sent pointer_enter event with a NULL surface"); + return; + } + + struct seat *seat = data; + + struct wl_window *win = wl_surface_get_user_data(surface); + struct terminal *term = win->term; + + seat->mouse_focus = term; + term->active_surface = term_surface_kind(term, surface); + + if (touch_is_active(seat)) + return; + + int x = wl_fixed_to_int(surface_x) * term->scale; + int y = wl_fixed_to_int(surface_y) * term->scale; + + seat->pointer.serial = serial; + seat->pointer.hidden = false; + seat->mouse.x = x; + seat->mouse.y = y; + + LOG_DBG( + "pointer-enter: pointer=%p, serial=%u, surface = %p, new-moused = %p, " + "x=%d, y=%d", + (void *)wl_pointer, serial, (void *)surface, (void *)term, x, y); + + xassert(tll_length(seat->mouse.buttons) == 0); + + wayl_reload_xcursor_theme(seat, term->scale); /* Scale may have changed */ + term_xcursor_update_for_seat(term, seat); + + switch (term->active_surface) { + case TERM_SURF_GRID: { + mouse_coord_pixel_to_cell(seat, term, x, y); + break; + } + + case TERM_SURF_TITLE: + case TERM_SURF_BORDER_LEFT: + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: + case TERM_SURF_TAB_BAR: + case TERM_SURF_TAB_OVERVIEW: + break; + + case TERM_SURF_BUTTON_MINIMIZE: + case TERM_SURF_BUTTON_MAXIMIZE: + case TERM_SURF_BUTTON_CLOSE: + render_refresh_csd(term); + break; + + case TERM_SURF_NONE: + BUG("Invalid surface type"); + break; + } +} + +static void wl_pointer_leave(void *data, struct wl_pointer *wl_pointer, + uint32_t serial, struct wl_surface *surface) { + struct seat *seat = data; + + if (seat->wl_touch != NULL) { switch (seat->touch.state) { case TOUCH_STATE_IDLE: + break; + case TOUCH_STATE_INHIBITED: - return false; + seat->touch.state = TOUCH_STATE_IDLE; + break; case TOUCH_STATE_HELD: case TOUCH_STATE_DRAGGING: case TOUCH_STATE_SCROLLING: - return true; + return; + } + } + + struct terminal *old_moused = seat->mouse_focus; + + LOG_DBG( + "%s: pointer-leave: pointer=%p, serial=%u, surface = %p, old-moused = %p", + seat->name, (void *)wl_pointer, serial, (void *)surface, + (void *)old_moused); + + seat->pointer.hidden = false; + + if (seat->pointer.xcursor_callback != NULL) { + /* A cursor frame callback may never be called if the pointer leaves our + * surface */ + wl_callback_destroy(seat->pointer.xcursor_callback); + seat->pointer.xcursor_callback = NULL; + seat->pointer.xcursor_pending = false; + } + + /* Reset last-set-xcursor, to ensure we update it on a pointer-enter event */ + seat->pointer.shape = CURSOR_SHAPE_NONE; + + /* Reset mouse state */ + seat->mouse.x = seat->mouse.y = 0; + seat->mouse.col = seat->mouse.row = 0; + mouse_button_state_reset(seat); + for (size_t i = 0; i < ALEN(seat->mouse.aggregated); i++) + seat->mouse.aggregated[i] = 0.0; + seat->mouse.have_discrete = false; + + seat->mouse_focus = NULL; + if (old_moused == NULL) { + LOG_WARN("compositor sent pointer_leave event without a pointer_enter " + "event: surface=%p", + (void *)surface); + } else { + if (surface != NULL) { + /* Sway 1.4 sends this event with a NULL surface when we destroy the + * window */ + const struct wl_window UNUSED *win = wl_surface_get_user_data(surface); + xassert(old_moused->window == win); } - BUG("Bad touch state: %d", seat->touch.state); - return false; -} + enum term_surface active_surface = old_moused->active_surface; -static void -wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, - uint32_t serial, struct wl_surface *surface, - wl_fixed_t surface_x, wl_fixed_t surface_y) -{ - if (unlikely(surface == NULL)) { - /* Seen on mutter-3.38 */ - LOG_WARN("compositor sent pointer_enter event with a NULL surface"); - return; - } - - struct seat *seat = data; - - struct wl_window *win = wl_surface_get_user_data(surface); - struct terminal *term = win->term; - - seat->mouse_focus = term; - term->active_surface = term_surface_kind(term, surface); - - if (touch_is_active(seat)) - return; - - int x = wl_fixed_to_int(surface_x) * term->scale; - int y = wl_fixed_to_int(surface_y) * term->scale; - - seat->pointer.serial = serial; - seat->pointer.hidden = false; - seat->mouse.x = x; - seat->mouse.y = y; - - LOG_DBG("pointer-enter: pointer=%p, serial=%u, surface = %p, new-moused = %p, " - "x=%d, y=%d", - (void *)wl_pointer, serial, (void *)surface, (void *)term, - x, y); - - xassert(tll_length(seat->mouse.buttons) == 0); - - wayl_reload_xcursor_theme(seat, term->scale); /* Scale may have changed */ - term_xcursor_update_for_seat(term, seat); - - switch (term->active_surface) { - case TERM_SURF_GRID: { - mouse_coord_pixel_to_cell(seat, term, x, y); - break; - } - - case TERM_SURF_TITLE: - case TERM_SURF_BORDER_LEFT: - case TERM_SURF_BORDER_RIGHT: - case TERM_SURF_BORDER_TOP: - case TERM_SURF_BORDER_BOTTOM: - case TERM_SURF_TAB_BAR: - case TERM_SURF_TAB_OVERVIEW: - break; + old_moused->active_surface = TERM_SURF_NONE; + switch (active_surface) { case TERM_SURF_BUTTON_MINIMIZE: case TERM_SURF_BUTTON_MAXIMIZE: case TERM_SURF_BUTTON_CLOSE: - render_refresh_csd(term); + if (old_moused->shutdown.in_progress) break; - case TERM_SURF_NONE: - BUG("Invalid surface type"); - break; - } -} + render_refresh_csd(old_moused); + break; -static void -wl_pointer_leave(void *data, struct wl_pointer *wl_pointer, - uint32_t serial, struct wl_surface *surface) -{ - struct seat *seat = data; - - if (seat->wl_touch != NULL) { - switch (seat->touch.state) { - case TOUCH_STATE_IDLE: - break; - - case TOUCH_STATE_INHIBITED: - seat->touch.state = TOUCH_STATE_IDLE; - break; - - case TOUCH_STATE_HELD: - case TOUCH_STATE_DRAGGING: - case TOUCH_STATE_SCROLLING: - return; - } - } - - struct terminal *old_moused = seat->mouse_focus; - - LOG_DBG( - "%s: pointer-leave: pointer=%p, serial=%u, surface = %p, old-moused = %p", - seat->name, (void *)wl_pointer, serial, (void *)surface, - (void *)old_moused); - - seat->pointer.hidden = false; - - if (seat->pointer.xcursor_callback != NULL) { - /* A cursor frame callback may never be called if the pointer leaves our surface */ - wl_callback_destroy(seat->pointer.xcursor_callback); - seat->pointer.xcursor_callback = NULL; - seat->pointer.xcursor_pending = false; - } - - /* Reset last-set-xcursor, to ensure we update it on a pointer-enter event */ - seat->pointer.shape = CURSOR_SHAPE_NONE; - - /* Reset mouse state */ - seat->mouse.x = seat->mouse.y = 0; - seat->mouse.col = seat->mouse.row = 0; - mouse_button_state_reset(seat); - for (size_t i = 0; i < ALEN(seat->mouse.aggregated); i++) - seat->mouse.aggregated[i] = 0.0; - seat->mouse.have_discrete = false; - - seat->mouse_focus = NULL; - if (old_moused == NULL) { - LOG_WARN( - "compositor sent pointer_leave event without a pointer_enter " - "event: surface=%p", (void *)surface); - } else { - if (surface != NULL) { - /* Sway 1.4 sends this event with a NULL surface when we destroy the window */ - const struct wl_window UNUSED *win = wl_surface_get_user_data(surface); - xassert(old_moused->window == win); - } - - enum term_surface active_surface = old_moused->active_surface; - - old_moused->active_surface = TERM_SURF_NONE; - - switch (active_surface) { - case TERM_SURF_BUTTON_MINIMIZE: - case TERM_SURF_BUTTON_MAXIMIZE: - case TERM_SURF_BUTTON_CLOSE: - if (old_moused->shutdown.in_progress) - break; - - render_refresh_csd(old_moused); - break; - - case TERM_SURF_GRID: - selection_finalize(seat, old_moused, seat->pointer.serial); - break; - - case TERM_SURF_NONE: - case TERM_SURF_TITLE: - case TERM_SURF_BORDER_LEFT: - case TERM_SURF_BORDER_RIGHT: - case TERM_SURF_BORDER_TOP: - case TERM_SURF_BORDER_BOTTOM: - case TERM_SURF_TAB_BAR: - case TERM_SURF_TAB_OVERVIEW: - break; - } - - } -} - -static bool -pointer_is_on_button(const struct terminal *term, const struct seat *seat, - enum csd_surface csd_surface) -{ - if (seat->mouse.x < 0) - return false; - if (seat->mouse.y < 0) - return false; - - struct csd_data info = get_csd_data(term, csd_surface); - if (seat->mouse.x > info.width) - return false; - - if (seat->mouse.y > info.height) - return false; - - return true; -} - -static void -wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, - uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y) -{ - struct seat *seat = data; - - /* Touch-emulated pointer events have wl_pointer == NULL. */ - if (wl_pointer != NULL && touch_is_active(seat)) - return; - - struct wayland *wayl = seat->wayl; - struct terminal *term = seat->mouse_focus; - - if (unlikely(term == NULL)) { - /* Typically happens when the compositor sent a pointer enter - * event with a NULL surface - see wl_pointer_enter(). - * - * In this case, we never set seat->mouse_focus (since we - * can't map the enter event to a specific window). */ - return; - } - - struct wl_window *win = term->window; - - LOG_DBG("pointer_motion: pointer=%p, x=%d, y=%d", (void *)wl_pointer, - wl_fixed_to_int(surface_x), wl_fixed_to_int(surface_y)); - - xassert(term != NULL); - - int x = wl_fixed_to_int(surface_x) * term->scale; - int y = wl_fixed_to_int(surface_y) * term->scale; - - enum term_surface surf_kind = term->active_surface; - int button = 0; - bool send_to_client = false; - bool is_on_button = false; - - /* If current surface is a button, check if pointer was on it - *before* the motion event */ - switch (surf_kind) { - case TERM_SURF_BUTTON_MINIMIZE: - is_on_button = pointer_is_on_button(term, seat, CSD_SURF_MINIMIZE); - break; - - case TERM_SURF_BUTTON_MAXIMIZE: - is_on_button = pointer_is_on_button(term, seat, CSD_SURF_MAXIMIZE); - break; - - case TERM_SURF_BUTTON_CLOSE: - is_on_button = pointer_is_on_button(term, seat, CSD_SURF_CLOSE); - break; - - case TERM_SURF_NONE: case TERM_SURF_GRID: + selection_finalize(seat, old_moused, seat->pointer.serial); + break; + + case TERM_SURF_NONE: case TERM_SURF_TITLE: case TERM_SURF_BORDER_LEFT: case TERM_SURF_BORDER_RIGHT: @@ -2831,858 +2947,895 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, case TERM_SURF_BORDER_BOTTOM: case TERM_SURF_TAB_BAR: case TERM_SURF_TAB_OVERVIEW: - break; - } - - seat->pointer.hidden = false; - seat->mouse.x = x; - seat->mouse.y = y; - - term_xcursor_update_for_seat(term, seat); - - if (tll_length(seat->mouse.buttons) > 0) { - const struct button_tracker *tracker = &tll_front(seat->mouse.buttons); - surf_kind = tracker->surf_kind; - button = tracker->button; - send_to_client = tracker->send_to_client; - } - - switch (surf_kind) { - case TERM_SURF_NONE: - break; - - case TERM_SURF_BUTTON_MINIMIZE: - if (pointer_is_on_button(term, seat, CSD_SURF_MINIMIZE) != is_on_button) - render_refresh_csd(term); - break; - - case TERM_SURF_BUTTON_MAXIMIZE: - if (pointer_is_on_button(term, seat, CSD_SURF_MAXIMIZE) != is_on_button) - render_refresh_csd(term); - break; - - case TERM_SURF_BUTTON_CLOSE: - if (pointer_is_on_button(term, seat, CSD_SURF_CLOSE) != is_on_button) - render_refresh_csd(term); - break; - - case TERM_SURF_TITLE: - /* We've started a 'move' timer, but user started dragging - * right away - abort the timer and initiate the actual move - * right away */ - if (button == BTN_LEFT && win->csd.move_timeout_fd != -1) { - fdm_del(wayl->fdm, win->csd.move_timeout_fd); - win->csd.move_timeout_fd = -1; - xdg_toplevel_move(win->xdg_toplevel, seat->wl_seat, win->csd.serial); - } - break; - - case TERM_SURF_BORDER_LEFT: - case TERM_SURF_BORDER_RIGHT: - case TERM_SURF_BORDER_TOP: - case TERM_SURF_BORDER_BOTTOM: - case TERM_SURF_TAB_BAR: - break; - - case TERM_SURF_TAB_OVERVIEW: { - const int new_hover = tab_overview_hit_test(win, x, y); - if (new_hover != win->tab_overview_state.hover_idx) { - win->tab_overview_state.hover_idx = new_hover; - render_refresh(term); - } - break; - } - - case TERM_SURF_GRID: { - int old_col = seat->mouse.col; - int old_row = seat->mouse.row; - - mouse_coord_pixel_to_cell(seat, term, seat->mouse.x, seat->mouse.y); - - xassert(seat->mouse.col >= 0 && seat->mouse.col < term->cols); - xassert(seat->mouse.row >= 0 && seat->mouse.row < term->rows); - - /* Cursor has moved to a different cell since last time */ - bool cursor_is_on_new_cell - = old_col != seat->mouse.col || old_row != seat->mouse.row; - - if (cursor_is_on_new_cell) { - /* Prevent multiple/different mouse bindings from - * triggering if the mouse has moved "too much" (to - * another cell) */ - seat->mouse.count = 0; - } - - /* Cursor is inside the grid, i.e. *not* in the margins */ - const bool cursor_is_on_grid = seat->mouse.col >= 0 && seat->mouse.row >= 0; - - enum selection_scroll_direction auto_scroll_direction - = term->selection.coords.end.row < 0 - ? SELECTION_SCROLL_NOT - : y < term->margins.top - ? SELECTION_SCROLL_UP - : y > term->height - term->margins.bottom - ? SELECTION_SCROLL_DOWN - : SELECTION_SCROLL_NOT; - - if (auto_scroll_direction == SELECTION_SCROLL_NOT) - selection_stop_scroll_timer(term); - - /* Update selection */ - if (!term->is_searching) { - if (auto_scroll_direction != SELECTION_SCROLL_NOT) { - /* - * Start 'selection auto-scrolling' - * - * The speed of the scrolling is proportional to the - * distance between the mouse and the grid; the - * further away the mouse is, the faster we scroll. - * - * Note that the speed is measured in 'intervals (in - * ns) between each timed scroll of a single line'. - * - * Thus, the further away the mouse is, the smaller - * interval value we use. - */ - - int distance = auto_scroll_direction == SELECTION_SCROLL_UP - ? term->margins.top - y - : y - (term->height - term->margins.bottom); - - xassert(distance > 0); - int divisor - = distance * term->conf->scrollback.multiplier / term->scale; - - selection_start_scroll_timer( - term, 400000000 / (divisor > 0 ? divisor : 1), - auto_scroll_direction, seat->mouse.col); - } - - if (term->selection.ongoing && - (cursor_is_on_new_cell || - (term->selection.coords.end.row < 0 && - seat->mouse.x >= term->margins.left && - seat->mouse.x < term->width - term->margins.right && - seat->mouse.y >= term->margins.top && - seat->mouse.y < term->height - term->margins.bottom))) - { - selection_update(term, seat->mouse.col, seat->mouse.row); - } - } - - /* Send mouse event to client application */ - if (!term_mouse_grabbed(term, seat) && - (cursor_is_on_new_cell || - term->mouse_reporting == MOUSE_SGR_PIXELS) && - ((button == 0 && cursor_is_on_grid) || - (button != 0 && send_to_client))) - { - xassert(seat->mouse.col < term->cols); - xassert(seat->mouse.row < term->rows); - - term_mouse_motion( - term, button, - seat->mouse.row, seat->mouse.col, - seat->mouse.y - term->margins.top, - seat->mouse.x - term->margins.left, - seat->kbd.shift, seat->kbd.alt, seat->kbd.ctrl); - } - break; - } + break; } + } } -static bool -fdm_csd_move(struct fdm *fdm, int fd, int events, void *data) -{ - struct seat *seat = data; - fdm_del(fdm, fd); +static bool pointer_is_on_button(const struct terminal *term, + const struct seat *seat, + enum csd_surface csd_surface) { + if (seat->mouse.x < 0) + return false; + if (seat->mouse.y < 0) + return false; - if (seat->mouse_focus == NULL) { - LOG_WARN( - "%s: CSD move timeout triggered, but seat's has no mouse focused terminal", - seat->name); - return true; + struct csd_data info = get_csd_data(term, csd_surface); + if (seat->mouse.x > info.width) + return false; + + if (seat->mouse.y > info.height) + return false; + + return true; +} + +static void wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, + uint32_t time, wl_fixed_t surface_x, + wl_fixed_t surface_y) { + struct seat *seat = data; + + /* Touch-emulated pointer events have wl_pointer == NULL. */ + if (wl_pointer != NULL && touch_is_active(seat)) + return; + + struct wayland *wayl = seat->wayl; + struct terminal *term = seat->mouse_focus; + + if (unlikely(term == NULL)) { + /* Typically happens when the compositor sent a pointer enter + * event with a NULL surface - see wl_pointer_enter(). + * + * In this case, we never set seat->mouse_focus (since we + * can't map the enter event to a specific window). */ + return; + } + + struct wl_window *win = term->window; + + LOG_DBG("pointer_motion: pointer=%p, x=%d, y=%d", (void *)wl_pointer, + wl_fixed_to_int(surface_x), wl_fixed_to_int(surface_y)); + + xassert(term != NULL); + + int x = wl_fixed_to_int(surface_x) * term->scale; + int y = wl_fixed_to_int(surface_y) * term->scale; + + enum term_surface surf_kind = term->active_surface; + int button = 0; + bool send_to_client = false; + bool is_on_button = false; + + /* If current surface is a button, check if pointer was on it + *before* the motion event */ + switch (surf_kind) { + case TERM_SURF_BUTTON_MINIMIZE: + is_on_button = pointer_is_on_button(term, seat, CSD_SURF_MINIMIZE); + break; + + case TERM_SURF_BUTTON_MAXIMIZE: + is_on_button = pointer_is_on_button(term, seat, CSD_SURF_MAXIMIZE); + break; + + case TERM_SURF_BUTTON_CLOSE: + is_on_button = pointer_is_on_button(term, seat, CSD_SURF_CLOSE); + break; + + case TERM_SURF_NONE: + case TERM_SURF_GRID: + case TERM_SURF_TITLE: + case TERM_SURF_BORDER_LEFT: + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: + case TERM_SURF_TAB_BAR: + case TERM_SURF_TAB_OVERVIEW: + break; + } + + seat->pointer.hidden = false; + seat->mouse.x = x; + seat->mouse.y = y; + + term_xcursor_update_for_seat(term, seat); + + if (tll_length(seat->mouse.buttons) > 0) { + const struct button_tracker *tracker = &tll_front(seat->mouse.buttons); + surf_kind = tracker->surf_kind; + button = tracker->button; + send_to_client = tracker->send_to_client; + } + + switch (surf_kind) { + case TERM_SURF_NONE: + break; + + case TERM_SURF_BUTTON_MINIMIZE: + if (pointer_is_on_button(term, seat, CSD_SURF_MINIMIZE) != is_on_button) + render_refresh_csd(term); + break; + + case TERM_SURF_BUTTON_MAXIMIZE: + if (pointer_is_on_button(term, seat, CSD_SURF_MAXIMIZE) != is_on_button) + render_refresh_csd(term); + break; + + case TERM_SURF_BUTTON_CLOSE: + if (pointer_is_on_button(term, seat, CSD_SURF_CLOSE) != is_on_button) + render_refresh_csd(term); + break; + + case TERM_SURF_TITLE: + /* We've started a 'move' timer, but user started dragging + * right away - abort the timer and initiate the actual move + * right away */ + if (button == BTN_LEFT && win->csd.move_timeout_fd != -1) { + fdm_del(wayl->fdm, win->csd.move_timeout_fd); + win->csd.move_timeout_fd = -1; + xdg_toplevel_move(win->xdg_toplevel, seat->wl_seat, win->csd.serial); + } + break; + + case TERM_SURF_BORDER_LEFT: + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: + case TERM_SURF_TAB_BAR: + break; + + case TERM_SURF_TAB_OVERVIEW: { + const int new_hover = tab_overview_hit_test(win, x, y); + if (new_hover != win->tab_overview_state.hover_idx) { + win->tab_overview_state.hover_idx = new_hover; + render_refresh(term); + } + break; + } + + case TERM_SURF_GRID: { + int old_col = seat->mouse.col; + int old_row = seat->mouse.row; + + mouse_coord_pixel_to_cell(seat, term, seat->mouse.x, seat->mouse.y); + + xassert(seat->mouse.col >= 0 && seat->mouse.col < term->cols); + xassert(seat->mouse.row >= 0 && seat->mouse.row < term->rows); + + /* Cursor has moved to a different cell since last time */ + bool cursor_is_on_new_cell = + old_col != seat->mouse.col || old_row != seat->mouse.row; + + if (cursor_is_on_new_cell) { + /* Prevent multiple/different mouse bindings from + * triggering if the mouse has moved "too much" (to + * another cell) */ + seat->mouse.count = 0; } - struct wl_window *win = seat->mouse_focus->window; + /* Cursor is inside the grid, i.e. *not* in the margins */ + const bool cursor_is_on_grid = seat->mouse.col >= 0 && seat->mouse.row >= 0; - win->csd.move_timeout_fd = -1; - xdg_toplevel_move(win->xdg_toplevel, seat->wl_seat, win->csd.serial); + enum selection_scroll_direction auto_scroll_direction = + term->selection.coords.end.row < 0 ? SELECTION_SCROLL_NOT + : y < term->margins.top ? SELECTION_SCROLL_UP + : y > term->height - term->margins.bottom ? SELECTION_SCROLL_DOWN + : SELECTION_SCROLL_NOT; + + if (auto_scroll_direction == SELECTION_SCROLL_NOT) + selection_stop_scroll_timer(term); + + /* Update selection */ + if (!term->is_searching) { + if (auto_scroll_direction != SELECTION_SCROLL_NOT) { + /* + * Start 'selection auto-scrolling' + * + * The speed of the scrolling is proportional to the + * distance between the mouse and the grid; the + * further away the mouse is, the faster we scroll. + * + * Note that the speed is measured in 'intervals (in + * ns) between each timed scroll of a single line'. + * + * Thus, the further away the mouse is, the smaller + * interval value we use. + */ + + int distance = auto_scroll_direction == SELECTION_SCROLL_UP + ? term->margins.top - y + : y - (term->height - term->margins.bottom); + + xassert(distance > 0); + int divisor = + distance * term->conf->scrollback.multiplier / term->scale; + + selection_start_scroll_timer(term, + 400000000 / (divisor > 0 ? divisor : 1), + auto_scroll_direction, seat->mouse.col); + } + + if (term->selection.ongoing && + (cursor_is_on_new_cell || + (term->selection.coords.end.row < 0 && + seat->mouse.x >= term->margins.left && + seat->mouse.x < term->width - term->margins.right && + seat->mouse.y >= term->margins.top && + seat->mouse.y < term->height - term->margins.bottom))) { + selection_update(term, seat->mouse.col, seat->mouse.row); + } + } + + /* Send mouse event to client application */ + if (!term_mouse_grabbed(term, seat) && + (cursor_is_on_new_cell || term->mouse_reporting == MOUSE_SGR_PIXELS) && + ((button == 0 && cursor_is_on_grid) || + (button != 0 && send_to_client))) { + xassert(seat->mouse.col < term->cols); + xassert(seat->mouse.row < term->rows); + + term_mouse_motion(term, button, seat->mouse.row, seat->mouse.col, + seat->mouse.y - term->margins.top, + seat->mouse.x - term->margins.left, seat->kbd.shift, + seat->kbd.alt, seat->kbd.ctrl); + } + break; + } + } +} + +static bool fdm_csd_move(struct fdm *fdm, int fd, int events, void *data) { + struct seat *seat = data; + fdm_del(fdm, fd); + + if (seat->mouse_focus == NULL) { + LOG_WARN("%s: CSD move timeout triggered, but seat's has no mouse focused " + "terminal", + seat->name); return true; + } + + struct wl_window *win = seat->mouse_focus->window; + + win->csd.move_timeout_fd = -1; + xdg_toplevel_move(win->xdg_toplevel, seat->wl_seat, win->csd.serial); + return true; } static const struct key_binding * match_mouse_binding(const struct seat *seat, const struct terminal *term, - int button) -{ - if (seat->wl_keyboard != NULL && seat->kbd.xkb_state != NULL) { - /* Seat has keyboard - use mouse bindings *with* modifiers */ + int button) { + if (seat->wl_keyboard != NULL && seat->kbd.xkb_state != NULL) { + /* Seat has keyboard - use mouse bindings *with* modifiers */ - const struct key_binding_set *bindings = - key_binding_for(term->wl->key_binding_manager, term->conf, seat); - xassert(bindings != NULL); + const struct key_binding_set *bindings = + key_binding_for(term->wl->key_binding_manager, term->conf, seat); + xassert(bindings != NULL); - xkb_mod_mask_t mods; - get_current_modifiers(seat, &mods, NULL, 0, true); + xkb_mod_mask_t mods; + get_current_modifiers(seat, &mods, NULL, 0, true); - /* Ignore selection override modifiers when - * matching modifiers */ - mods &= ~bindings->selection_overrides; + /* Ignore selection override modifiers when + * matching modifiers */ + mods &= ~bindings->selection_overrides; - const struct key_binding *match = NULL; + const struct key_binding *match = NULL; - tll_foreach(bindings->mouse, it) { - const struct key_binding *binding = &it->item; + tll_foreach(bindings->mouse, it) { + const struct key_binding *binding = &it->item; - if (binding->m.button != button) { - /* Wrong button */ - continue; - } + if (binding->m.button != button) { + /* Wrong button */ + continue; + } - if (binding->mods != mods) { - /* Modifier mismatch */ - continue; - } + if (binding->mods != mods) { + /* Modifier mismatch */ + continue; + } - if (binding->m.count > seat->mouse.count) { - /* Not correct click count */ - continue; - } + if (binding->m.count > seat->mouse.count) { + /* Not correct click count */ + continue; + } - if (match == NULL || binding->m.count > match->m.count) - match = binding; - } - - return match; + if (match == NULL || binding->m.count > match->m.count) + match = binding; } - else { - /* Seat does NOT have a keyboard - use mouse bindings *without* - * modifiers */ - const struct config_key_binding *match = NULL; - const struct config *conf = term->conf; + return match; + } - for (size_t i = 0; i < conf->bindings.mouse.count; i++) { - const struct config_key_binding *binding = - &conf->bindings.mouse.arr[i]; + else { + /* Seat does NOT have a keyboard - use mouse bindings *without* + * modifiers */ + const struct config_key_binding *match = NULL; + const struct config *conf = term->conf; - if (binding->m.button != button) { - /* Wrong button */ - continue; - } + for (size_t i = 0; i < conf->bindings.mouse.count; i++) { + const struct config_key_binding *binding = &conf->bindings.mouse.arr[i]; - if (binding->m.count > seat->mouse.count) { - /* Incorrect click count */ - continue; - } + if (binding->m.button != button) { + /* Wrong button */ + continue; + } - if (tll_length(binding->modifiers) > 0) { - /* Binding has modifiers */ - continue; - } + if (binding->m.count > seat->mouse.count) { + /* Incorrect click count */ + continue; + } - if (match == NULL || binding->m.count > match->m.count) - match = binding; - } + if (tll_length(binding->modifiers) > 0) { + /* Binding has modifiers */ + continue; + } - if (match != NULL) { - static struct key_binding bind; - bind.action = match->action; - bind.aux = &match->aux; - return &bind; - } - - return NULL; + if (match == NULL || binding->m.count > match->m.count) + match = binding; } - BUG("should not get here"); + if (match != NULL) { + static struct key_binding bind; + bind.action = match->action; + bind.aux = &match->aux; + return &bind; + } + + return NULL; + } + + BUG("should not get here"); } -static void -wl_pointer_button(void *data, struct wl_pointer *wl_pointer, - uint32_t serial, uint32_t time, uint32_t button, uint32_t state) -{ - LOG_DBG("BUTTON: pointer=%p, serial=%u, button=%x, state=%u", - (void *)wl_pointer, serial, button, state); +static void wl_pointer_button(void *data, struct wl_pointer *wl_pointer, + uint32_t serial, uint32_t time, uint32_t button, + uint32_t state) { + LOG_DBG("BUTTON: pointer=%p, serial=%u, button=%x, state=%u", + (void *)wl_pointer, serial, button, state); - xassert(serial != 0); + xassert(serial != 0); - struct seat *seat = data; + struct seat *seat = data; - /* Touch-emulated pointer events have wl_pointer == NULL. */ - if (wl_pointer != NULL && touch_is_active(seat)) - return; + /* Touch-emulated pointer events have wl_pointer == NULL. */ + if (wl_pointer != NULL && touch_is_active(seat)) + return; - struct wayland *wayl = seat->wayl; - struct terminal *term = seat->mouse_focus; + struct wayland *wayl = seat->wayl; + struct terminal *term = seat->mouse_focus; - seat->pointer.serial = serial; - seat->pointer.hidden = false; + seat->pointer.serial = serial; + seat->pointer.hidden = false; - xassert(term != NULL); + xassert(term != NULL); - enum term_surface surf_kind = TERM_SURF_NONE; - bool send_to_client = false; + enum term_surface surf_kind = TERM_SURF_NONE; + bool send_to_client = false; - if (state == WL_POINTER_BUTTON_STATE_PRESSED) { - if (seat->wl_touch != NULL && seat->touch.state == TOUCH_STATE_IDLE) { - seat->touch.state = TOUCH_STATE_INHIBITED; - } + if (state == WL_POINTER_BUTTON_STATE_PRESSED) { + if (seat->wl_touch != NULL && seat->touch.state == TOUCH_STATE_IDLE) { + seat->touch.state = TOUCH_STATE_INHIBITED; + } - /* Time since last click */ - struct timespec now, since_last; - clock_gettime(CLOCK_MONOTONIC, &now); - timespec_sub(&now, &seat->mouse.last_time, &since_last); + /* Time since last click */ + struct timespec now, since_last; + clock_gettime(CLOCK_MONOTONIC, &now); + timespec_sub(&now, &seat->mouse.last_time, &since_last); - if (seat->mouse.last_released_button == button && - since_last.tv_sec == 0 && since_last.tv_nsec <= 300 * 1000 * 1000) - { - seat->mouse.count++; - } else - seat->mouse.count = 1; + if (seat->mouse.last_released_button == button && since_last.tv_sec == 0 && + since_last.tv_nsec <= 300 * 1000 * 1000) { + seat->mouse.count++; + } else + seat->mouse.count = 1; - /* - * Workaround GNOME bug - * - * Dragging the window, then stopping the drag (releasing the - * mouse button), *without* moving the mouse, and then - * clicking twice, waiting for the CSD timer, and finally - * clicking once more, results in the following sequence - * (keyboard and other irrelevant events filtered out, unless - * they're needed to prove a point): - * - * dbg: input.c:1551: cancelling drag timer, moving window - * dbg: input.c:759: keyboard_leave: keyboard=0x607000003580, serial=873, surface=0x6070000036d0 - * dbg: input.c:1432: seat0: pointer-leave: pointer=0x607000003660, serial=874, surface = 0x6070000396e0, old-moused = 0x622000006100 - * - * --> drag stopped here - * - * --> LMB clicked first time after the drag (generates the - * enter event on *release*, but no button events) - * dbg: input.c:1360: pointer-enter: pointer=0x607000003660, serial=876, surface = 0x6070000396e0, new-moused = 0x622000006100 - * - * --> LMB clicked, and held until the timer times out, second - * time after the drag - * dbg: input.c:1712: BUTTON: pointer=0x607000003660, serial=877, button=110, state=1 - * dbg: input.c:1806: starting move timer - * dbg: input.c:1692: move timer timed out - * dbg: input.c:759: keyboard_leave: keyboard=0x607000003580, serial=878, surface=0x6070000036d0 - * - * --> NOTE: ^^ no pointer leave event this time, only the - * keyboard leave - * - * --> LMB clicked one last time - * dbg: input.c:697: seat0: keyboard_enter: keyboard=0x607000003580, serial=879, surface=0x6070000036d0 - * dbg: input.c:1712: BUTTON: pointer=0x607000003660, serial=880, button=110, state=1 - * err: input.c:1741: BUG in wl_pointer_button(): assertion failed: 'it->item.button != button' - * - * What are we seeing? - * - * - GNOME does *not* send a pointer *enter* event after the drag - * has stopped - * - The second drag does *not* generate a pointer *leave* event - * - The missing leave event means we're still tracking LMB as - * being held down in our seat struct. - * - This leads to an assert (debug builds) when LMB is clicked - * again (seat's button list already contains LMB). - * - * Note: I've also observed variants of the above - */ - tll_foreach(seat->mouse.buttons, it) { - if (it->item.button == button) { - LOG_WARN("multiple button press events for button %d " - "(compositor bug?)", button); - tll_remove(seat->mouse.buttons, it); - break; - } - } + /* + * Workaround GNOME bug + * + * Dragging the window, then stopping the drag (releasing the + * mouse button), *without* moving the mouse, and then + * clicking twice, waiting for the CSD timer, and finally + * clicking once more, results in the following sequence + * (keyboard and other irrelevant events filtered out, unless + * they're needed to prove a point): + * + * dbg: input.c:1551: cancelling drag timer, moving window + * dbg: input.c:759: keyboard_leave: keyboard=0x607000003580, serial=873, + * surface=0x6070000036d0 dbg: input.c:1432: seat0: pointer-leave: + * pointer=0x607000003660, serial=874, surface = 0x6070000396e0, old-moused + * = 0x622000006100 + * + * --> drag stopped here + * + * --> LMB clicked first time after the drag (generates the + * enter event on *release*, but no button events) + * dbg: input.c:1360: pointer-enter: pointer=0x607000003660, serial=876, + * surface = 0x6070000396e0, new-moused = 0x622000006100 + * + * --> LMB clicked, and held until the timer times out, second + * time after the drag + * dbg: input.c:1712: BUTTON: pointer=0x607000003660, serial=877, + * button=110, state=1 dbg: input.c:1806: starting move timer dbg: + * input.c:1692: move timer timed out dbg: input.c:759: keyboard_leave: + * keyboard=0x607000003580, serial=878, surface=0x6070000036d0 + * + * --> NOTE: ^^ no pointer leave event this time, only the + * keyboard leave + * + * --> LMB clicked one last time + * dbg: input.c:697: seat0: keyboard_enter: keyboard=0x607000003580, + * serial=879, surface=0x6070000036d0 dbg: input.c:1712: BUTTON: + * pointer=0x607000003660, serial=880, button=110, state=1 err: + * input.c:1741: BUG in wl_pointer_button(): assertion failed: + * 'it->item.button != button' + * + * What are we seeing? + * + * - GNOME does *not* send a pointer *enter* event after the drag + * has stopped + * - The second drag does *not* generate a pointer *leave* event + * - The missing leave event means we're still tracking LMB as + * being held down in our seat struct. + * - This leads to an assert (debug builds) when LMB is clicked + * again (seat's button list already contains LMB). + * + * Note: I've also observed variants of the above + */ + tll_foreach(seat->mouse.buttons, it) { + if (it->item.button == button) { + LOG_WARN("multiple button press events for button %d " + "(compositor bug?)", + button); + tll_remove(seat->mouse.buttons, it); + break; + } + } #if defined(_DEBUG) - tll_foreach(seat->mouse.buttons, it) - xassert(it->item.button != button); + tll_foreach(seat->mouse.buttons, it) xassert(it->item.button != button); #endif - /* - * Remember which surface "owns" this button, so that we can - * send motion and button release events to that surface, even - * if the pointer is no longer over it. - */ - tll_push_back( - seat->mouse.buttons, - ((struct button_tracker){ - .button = button, - .surf_kind = term->active_surface, - .send_to_client = false})); + /* + * Remember which surface "owns" this button, so that we can + * send motion and button release events to that surface, even + * if the pointer is no longer over it. + */ + tll_push_back(seat->mouse.buttons, + ((struct button_tracker){.button = button, + .surf_kind = term->active_surface, + .send_to_client = false})); - seat->mouse.last_time = now; + seat->mouse.last_time = now; - surf_kind = term->active_surface; - send_to_client = false; /* For now, may be set to true if a binding consumes the button */ - } else { - bool UNUSED have_button = false; - tll_foreach(seat->mouse.buttons, it) { - if (it->item.button == button) { - have_button = true; - surf_kind = it->item.surf_kind; - send_to_client = it->item.send_to_client; - tll_remove(seat->mouse.buttons, it); - break; - } - } - - if (seat->wl_touch != NULL && seat->touch.state == TOUCH_STATE_INHIBITED) { - if (tll_length(seat->mouse.buttons) == 0) { - seat->touch.state = TOUCH_STATE_IDLE; - } - } - - if (!have_button) { - /* - * Seen on Sway with slurp - * - * 1. Run slurp - * 2. Press, and hold left mouse button - * 3. Press escape, to cancel slurp - * 4. Release mouse button - * 5. BAM! - */ - LOG_WARN("stray button release event (compositor bug?)"); - return; - } - - seat->mouse.last_released_button = button; + surf_kind = term->active_surface; + send_to_client = false; /* For now, may be set to true if a binding consumes + the button */ + } else { + bool UNUSED have_button = false; + tll_foreach(seat->mouse.buttons, it) { + if (it->item.button == button) { + have_button = true; + surf_kind = it->item.surf_kind; + send_to_client = it->item.send_to_client; + tll_remove(seat->mouse.buttons, it); + break; + } } - switch (surf_kind) { - case TERM_SURF_TITLE: - if (state == WL_POINTER_BUTTON_STATE_PRESSED) { - - struct wl_window *win = term->window; - - /* Toggle maximized state on double-click */ - if (term->conf->csd.double_click_to_maximize && - button == BTN_LEFT && - seat->mouse.count == 2) - { - if (win->is_maximized) - xdg_toplevel_unset_maximized(win->xdg_toplevel); - else - xdg_toplevel_set_maximized(win->xdg_toplevel); - } - - else if (button == BTN_LEFT && win->csd.move_timeout_fd < 0) { - const struct itimerspec timeout = { - .it_value = {.tv_nsec = 200000000}, - }; - - int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); - if (fd >= 0 && - timerfd_settime(fd, 0, &timeout, NULL) == 0 && - fdm_add(wayl->fdm, fd, EPOLLIN, &fdm_csd_move, seat)) - { - win->csd.move_timeout_fd = fd; - win->csd.serial = serial; - } else { - LOG_ERRNO("failed to configure XDG toplevel move timer FD"); - if (fd >= 0) - close(fd); - } - } - - if (button == BTN_RIGHT && tll_length(seat->mouse.buttons) == 1) { - const struct csd_data info = get_csd_data(term, CSD_SURF_TITLE); - xdg_toplevel_show_window_menu( - win->xdg_toplevel, - seat->wl_seat, - seat->pointer.serial, - seat->mouse.x + info.x, seat->mouse.y + info.y); - } - } - - else if (state == WL_POINTER_BUTTON_STATE_RELEASED) { - struct wl_window *win = term->window; - if (win->csd.move_timeout_fd >= 0) { - fdm_del(wayl->fdm, win->csd.move_timeout_fd); - win->csd.move_timeout_fd = -1; - } - } - return; - - case TERM_SURF_BORDER_LEFT: - case TERM_SURF_BORDER_RIGHT: - case TERM_SURF_BORDER_TOP: - case TERM_SURF_BORDER_BOTTOM: { - if (button == BTN_LEFT && state == WL_POINTER_BUTTON_STATE_PRESSED) { - enum xdg_toplevel_resize_edge resize_type = XDG_TOPLEVEL_RESIZE_EDGE_NONE; - - int x = seat->mouse.x; - int y = seat->mouse.y; - - if (is_top_left(term, x, y)) - resize_type = XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT; - else if (is_top_right(term, x, y)) - resize_type = XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT; - else if (is_bottom_left(term, x, y)) - resize_type = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT; - else if (is_bottom_right(term, x, y)) - resize_type = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT; - else { - if (term->active_surface == TERM_SURF_BORDER_LEFT && - !term->window->is_constrained_left) - { - resize_type = XDG_TOPLEVEL_RESIZE_EDGE_LEFT; - } - - else if (term->active_surface == TERM_SURF_BORDER_RIGHT && - !term->window->is_constrained_right) - { - resize_type = XDG_TOPLEVEL_RESIZE_EDGE_RIGHT; - } - - else if (term->active_surface == TERM_SURF_BORDER_TOP && - !term->window->is_constrained_top) - { - resize_type = XDG_TOPLEVEL_RESIZE_EDGE_TOP; - } - - else if (term->active_surface == TERM_SURF_BORDER_BOTTOM && - !term->window->is_constrained_bottom) - { - resize_type = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM; - } - } - - if (resize_type != XDG_TOPLEVEL_RESIZE_EDGE_NONE) { - xdg_toplevel_resize( - term->window->xdg_toplevel, seat->wl_seat, serial, resize_type); - } - } - return; + if (seat->wl_touch != NULL && seat->touch.state == TOUCH_STATE_INHIBITED) { + if (tll_length(seat->mouse.buttons) == 0) { + seat->touch.state = TOUCH_STATE_IDLE; + } } - case TERM_SURF_BUTTON_MINIMIZE: - if (button == BTN_LEFT && - pointer_is_on_button(term, seat, CSD_SURF_MINIMIZE) && - state == WL_POINTER_BUTTON_STATE_RELEASED) - { - xdg_toplevel_set_minimized(term->window->xdg_toplevel); - } - break; - - case TERM_SURF_BUTTON_MAXIMIZE: - if (button == BTN_LEFT && - pointer_is_on_button(term, seat, CSD_SURF_MAXIMIZE) && - state == WL_POINTER_BUTTON_STATE_RELEASED) - { - if (term->window->is_maximized) - xdg_toplevel_unset_maximized(term->window->xdg_toplevel); - else - xdg_toplevel_set_maximized(term->window->xdg_toplevel); - } - break; - - case TERM_SURF_BUTTON_CLOSE: - if (button == BTN_LEFT && - pointer_is_on_button(term, seat, CSD_SURF_CLOSE) && - state == WL_POINTER_BUTTON_STATE_RELEASED) - { - term_shutdown(term); - } - break; - - case TERM_SURF_GRID: { - search_cancel(term); - urls_reset(term); - - bool cursor_is_on_grid = seat->mouse.col >= 0 && seat->mouse.row >= 0; - - switch (state) { - case WL_POINTER_BUTTON_STATE_PRESSED: { - bool consumed = false; - - if (cursor_is_on_grid && term_mouse_grabbed(term, seat)) { - const struct key_binding *match = - match_mouse_binding(seat, term, button); - - if (match != NULL) - consumed = execute_binding(seat, term, match, serial, 1); - } - - send_to_client = !consumed && cursor_is_on_grid; - - if (send_to_client) - tll_back(seat->mouse.buttons).send_to_client = true; - - if (send_to_client && - !term_mouse_grabbed(term, seat) && - cursor_is_on_grid) - { - term_mouse_down( - term, button, seat->mouse.row, seat->mouse.col, - seat->mouse.y - term->margins.top, - seat->mouse.x - term->margins.left, - seat->kbd.shift, seat->kbd.alt, seat->kbd.ctrl); - } - break; - } - - case WL_POINTER_BUTTON_STATE_RELEASED: - selection_finalize(seat, term, serial); - - if (send_to_client && !term_mouse_grabbed(term, seat)) { - term_mouse_up( - term, button, seat->mouse.row, seat->mouse.col, - seat->mouse.y - term->margins.top, - seat->mouse.x - term->margins.left, - seat->kbd.shift, seat->kbd.alt, seat->kbd.ctrl); - } - break; - } - break; + if (!have_button) { + /* + * Seen on Sway with slurp + * + * 1. Run slurp + * 2. Press, and hold left mouse button + * 3. Press escape, to cancel slurp + * 4. Release mouse button + * 5. BAM! + */ + LOG_WARN("stray button release event (compositor bug?)"); + return; } - case TERM_SURF_TAB_BAR: { - if (state != WL_POINTER_BUTTON_STATE_PRESSED) - break; - if (button != BTN_LEFT) - break; + seat->mouse.last_released_button = button; + } - size_t idx = term_tab_bar_hit_test(term, seat->mouse.x, seat->mouse.y); - if (idx == SIZE_MAX || idx >= term->window->tab_count) - break; - if (idx == term->window->active_tab) - break; - term_tab_switch(term->window, idx); - break; - } + switch (surf_kind) { + case TERM_SURF_TITLE: + if (state == WL_POINTER_BUTTON_STATE_PRESSED) { - case TERM_SURF_TAB_OVERVIEW: { - if (state != WL_POINTER_BUTTON_STATE_PRESSED) - break; - struct wl_window *win = term->window; - if (button == BTN_LEFT) { - int idx = tab_overview_hit_test(win, seat->mouse.x, seat->mouse.y); - if (idx >= 0 && (size_t)idx < win->tab_count) { - if ((size_t)idx != win->active_tab) - term_tab_switch(win, (size_t)idx); - /* Selecting a card snaps closed — no fade-out */ - tab_overview_close_instant(win); - } else { - /* Click on backdrop animates closed */ - tab_overview_toggle(win); - } - } else if (button == BTN_RIGHT) { - tab_overview_toggle(win); + struct wl_window *win = term->window; + + /* Toggle maximized state on double-click */ + if (term->conf->csd.double_click_to_maximize && button == BTN_LEFT && + seat->mouse.count == 2) { + if (win->is_maximized) + xdg_toplevel_unset_maximized(win->xdg_toplevel); + else + xdg_toplevel_set_maximized(win->xdg_toplevel); + } + + else if (button == BTN_LEFT && win->csd.move_timeout_fd < 0) { + const struct itimerspec timeout = { + .it_value = {.tv_nsec = 200000000}, + }; + + int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (fd >= 0 && timerfd_settime(fd, 0, &timeout, NULL) == 0 && + fdm_add(wayl->fdm, fd, EPOLLIN, &fdm_csd_move, seat)) { + win->csd.move_timeout_fd = fd; + win->csd.serial = serial; + } else { + LOG_ERRNO("failed to configure XDG toplevel move timer FD"); + if (fd >= 0) + close(fd); } - break; + } + + if (button == BTN_RIGHT && tll_length(seat->mouse.buttons) == 1) { + const struct csd_data info = get_csd_data(term, CSD_SURF_TITLE); + xdg_toplevel_show_window_menu( + win->xdg_toplevel, seat->wl_seat, seat->pointer.serial, + seat->mouse.x + info.x, seat->mouse.y + info.y); + } } - case TERM_SURF_NONE: - BUG("Invalid surface type"); - break; - + else if (state == WL_POINTER_BUTTON_STATE_RELEASED) { + struct wl_window *win = term->window; + if (win->csd.move_timeout_fd >= 0) { + fdm_del(wayl->fdm, win->csd.move_timeout_fd); + win->csd.move_timeout_fd = -1; + } } -} + return; -static void -alternate_scroll(struct seat *seat, int amount, int button) -{ - if (seat->wl_keyboard == NULL) - return; + case TERM_SURF_BORDER_LEFT: + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: { + if (button == BTN_LEFT && state == WL_POINTER_BUTTON_STATE_PRESSED) { + enum xdg_toplevel_resize_edge resize_type = XDG_TOPLEVEL_RESIZE_EDGE_NONE; - /* Should be cleared in leave event */ - xassert(seat->mouse_focus != NULL); - struct terminal *term = seat->mouse_focus; + int x = seat->mouse.x; + int y = seat->mouse.y; - assert(button == BTN_BACK || button == BTN_FORWARD); + if (is_top_left(term, x, y)) + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT; + else if (is_top_right(term, x, y)) + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT; + else if (is_bottom_left(term, x, y)) + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT; + else if (is_bottom_right(term, x, y)) + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT; + else { + if (term->active_surface == TERM_SURF_BORDER_LEFT && + !term->window->is_constrained_left) { + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_LEFT; + } - xkb_keycode_t key = button == BTN_BACK - ? seat->kbd.key_arrow_up : seat->kbd.key_arrow_down; + else if (term->active_surface == TERM_SURF_BORDER_RIGHT && + !term->window->is_constrained_right) { + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_RIGHT; + } - for (int i = 0; i < amount; i++) - key_press_release(seat, term, seat->kbd.serial, key, XKB_KEY_DOWN); - key_press_release(seat, term, seat->kbd.serial, key, XKB_KEY_UP); -} + else if (term->active_surface == TERM_SURF_BORDER_TOP && + !term->window->is_constrained_top) { + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_TOP; + } -static void -mouse_scroll(struct seat *seat, int amount, enum wl_pointer_axis axis) -{ - struct terminal *term = seat->mouse_focus; - xassert(term != NULL); + else if (term->active_surface == TERM_SURF_BORDER_BOTTOM && + !term->window->is_constrained_bottom) { + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM; + } + } - int button = axis == WL_POINTER_AXIS_VERTICAL_SCROLL - ? amount < 0 ? BTN_WHEEL_BACK : BTN_WHEEL_FORWARD - : amount < 0 ? BTN_WHEEL_LEFT : BTN_WHEEL_RIGHT; - amount = abs(amount); + if (resize_type != XDG_TOPLEVEL_RESIZE_EDGE_NONE) { + xdg_toplevel_resize(term->window->xdg_toplevel, seat->wl_seat, serial, + resize_type); + } + } + return; + } - if (term_mouse_grabbed(term, seat)) { - seat->mouse.count = 1; + case TERM_SURF_BUTTON_MINIMIZE: + if (button == BTN_LEFT && + pointer_is_on_button(term, seat, CSD_SURF_MINIMIZE) && + state == WL_POINTER_BUTTON_STATE_RELEASED) { + xdg_toplevel_set_minimized(term->window->xdg_toplevel); + } + break; + case TERM_SURF_BUTTON_MAXIMIZE: + if (button == BTN_LEFT && + pointer_is_on_button(term, seat, CSD_SURF_MAXIMIZE) && + state == WL_POINTER_BUTTON_STATE_RELEASED) { + if (term->window->is_maximized) + xdg_toplevel_unset_maximized(term->window->xdg_toplevel); + else + xdg_toplevel_set_maximized(term->window->xdg_toplevel); + } + break; + + case TERM_SURF_BUTTON_CLOSE: + if (button == BTN_LEFT && + pointer_is_on_button(term, seat, CSD_SURF_CLOSE) && + state == WL_POINTER_BUTTON_STATE_RELEASED) { + term_shutdown(term); + } + break; + + case TERM_SURF_GRID: { + search_cancel(term); + urls_reset(term); + + bool cursor_is_on_grid = seat->mouse.col >= 0 && seat->mouse.row >= 0; + + switch (state) { + case WL_POINTER_BUTTON_STATE_PRESSED: { + bool consumed = false; + + if (cursor_is_on_grid && term_mouse_grabbed(term, seat)) { const struct key_binding *match = match_mouse_binding(seat, term, button); if (match != NULL) - execute_binding(seat, term, match, seat->pointer.serial, amount); + consumed = execute_binding(seat, term, match, serial, 1); + } - seat->mouse.last_released_button = button; + send_to_client = !consumed && cursor_is_on_grid; + + if (send_to_client) + tll_back(seat->mouse.buttons).send_to_client = true; + + if (send_to_client && !term_mouse_grabbed(term, seat) && + cursor_is_on_grid) { + term_mouse_down(term, button, seat->mouse.row, seat->mouse.col, + seat->mouse.y - term->margins.top, + seat->mouse.x - term->margins.left, seat->kbd.shift, + seat->kbd.alt, seat->kbd.ctrl); + } + break; } - else if (seat->mouse.col >= 0 && seat->mouse.row >= 0) { - xassert(seat->mouse.col < term->cols); - xassert(seat->mouse.row < term->rows); + case WL_POINTER_BUTTON_STATE_RELEASED: + selection_finalize(seat, term, serial); - for (int i = 0; i < amount; i++) { - term_mouse_down( - term, button, seat->mouse.row, seat->mouse.col, - seat->mouse.y - term->margins.top, - seat->mouse.x - term->margins.left, - seat->kbd.shift, seat->kbd.alt, seat->kbd.ctrl); - } - - term_mouse_up( - term, button, seat->mouse.row, seat->mouse.col, - seat->mouse.y - term->margins.top, - seat->mouse.x - term->margins.left, - seat->kbd.shift, seat->kbd.alt, seat->kbd.ctrl); + if (send_to_client && !term_mouse_grabbed(term, seat)) { + term_mouse_up(term, button, seat->mouse.row, seat->mouse.col, + seat->mouse.y - term->margins.top, + seat->mouse.x - term->margins.left, seat->kbd.shift, + seat->kbd.alt, seat->kbd.ctrl); + } + break; } + break; + } + + case TERM_SURF_TAB_BAR: { + if (state != WL_POINTER_BUTTON_STATE_PRESSED) + break; + if (button != BTN_LEFT) + break; + + size_t idx = term_tab_bar_hit_test(term, seat->mouse.x, seat->mouse.y); + if (idx == SIZE_MAX || idx >= term->window->tab_count) + break; + if (idx == term->window->active_tab) + break; + term_tab_switch(term->window, idx); + break; + } + + case TERM_SURF_TAB_OVERVIEW: { + if (state != WL_POINTER_BUTTON_STATE_PRESSED) + break; + struct wl_window *win = term->window; + if (button == BTN_LEFT) { + int idx = tab_overview_hit_test(win, seat->mouse.x, seat->mouse.y); + if (idx >= 0 && (size_t)idx < win->tab_count) { + if ((size_t)idx != win->active_tab) + term_tab_switch(win, (size_t)idx); + tab_overview_close_instant(win); + } else { + tab_overview_toggle(win); + } + } else if (button == BTN_RIGHT) { + tab_overview_toggle(win); + } + break; + } + + case TERM_SURF_NONE: + BUG("Invalid surface type"); + break; + } } -static double -mouse_scroll_multiplier(const struct terminal *term, const struct seat *seat) -{ - return (term->grid == &term->normal || - (term_mouse_grabbed(term, seat) && term->alt_scrolling)) - ? term->conf->scrollback.multiplier - : 1.0; +static void alternate_scroll(struct seat *seat, int amount, int button) { + if (seat->wl_keyboard == NULL) + return; + + /* Should be cleared in leave event */ + xassert(seat->mouse_focus != NULL); + struct terminal *term = seat->mouse_focus; + + assert(button == BTN_BACK || button == BTN_FORWARD); + + xkb_keycode_t key = + button == BTN_BACK ? seat->kbd.key_arrow_up : seat->kbd.key_arrow_down; + + for (int i = 0; i < amount; i++) + key_press_release(seat, term, seat->kbd.serial, key, XKB_KEY_DOWN); + key_press_release(seat, term, seat->kbd.serial, key, XKB_KEY_UP); } -static void -wl_pointer_axis(void *data, struct wl_pointer *wl_pointer, - uint32_t time, uint32_t axis, wl_fixed_t value) -{ - struct seat *seat = data; +static void mouse_scroll(struct seat *seat, int amount, + enum wl_pointer_axis axis) { + struct terminal *term = seat->mouse_focus; + xassert(term != NULL); - if (touch_is_active(seat)) - return; + int button = axis == WL_POINTER_AXIS_VERTICAL_SCROLL + ? amount < 0 ? BTN_WHEEL_BACK : BTN_WHEEL_FORWARD + : amount < 0 ? BTN_WHEEL_LEFT + : BTN_WHEEL_RIGHT; + amount = abs(amount); - if (seat->mouse.have_discrete) - return; + if (term_mouse_grabbed(term, seat)) { + seat->mouse.count = 1; - xassert(seat->mouse_focus != NULL); - xassert(axis < ALEN(seat->mouse.aggregated)); + const struct key_binding *match = match_mouse_binding(seat, term, button); - const struct terminal *term = seat->mouse_focus; + if (match != NULL) + execute_binding(seat, term, match, seat->pointer.serial, amount); - /* - * Aggregate scrolled amount until we get at least 1.0 - * - * Without this, very slow scrolling will never actually scroll - * anything. - */ - seat->mouse.aggregated[axis] += - mouse_scroll_multiplier(term, seat) * wl_fixed_to_double(value); - if (fabs(seat->mouse.aggregated[axis]) < seat->mouse_focus->cell_height) - return; + seat->mouse.last_released_button = button; + } - int lines = seat->mouse.aggregated[axis] / seat->mouse_focus->cell_height; - mouse_scroll(seat, lines, axis); - seat->mouse.aggregated[axis] -= (double)lines * seat->mouse_focus->cell_height; + else if (seat->mouse.col >= 0 && seat->mouse.row >= 0) { + xassert(seat->mouse.col < term->cols); + xassert(seat->mouse.row < term->rows); + + for (int i = 0; i < amount; i++) { + term_mouse_down(term, button, seat->mouse.row, seat->mouse.col, + seat->mouse.y - term->margins.top, + seat->mouse.x - term->margins.left, seat->kbd.shift, + seat->kbd.alt, seat->kbd.ctrl); + } + + term_mouse_up(term, button, seat->mouse.row, seat->mouse.col, + seat->mouse.y - term->margins.top, + seat->mouse.x - term->margins.left, seat->kbd.shift, + seat->kbd.alt, seat->kbd.ctrl); + } } -static void -wl_pointer_axis_discrete(void *data, struct wl_pointer *wl_pointer, - enum wl_pointer_axis axis, int32_t discrete) -{ - LOG_DBG("axis_discrete: %d", discrete); - struct seat *seat = data; +static double mouse_scroll_multiplier(const struct terminal *term, + const struct seat *seat) { + return (term->grid == &term->normal || + (term_mouse_grabbed(term, seat) && term->alt_scrolling)) + ? term->conf->scrollback.multiplier + : 1.0; +} - if (touch_is_active(seat)) - return; +static void wl_pointer_axis(void *data, struct wl_pointer *wl_pointer, + uint32_t time, uint32_t axis, wl_fixed_t value) { + struct seat *seat = data; - seat->mouse.have_discrete = true; - int amount = discrete; + if (touch_is_active(seat)) + return; - if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL) { - /* Treat mouse wheel left/right as regular buttons */ - } else - amount *= mouse_scroll_multiplier(seat->mouse_focus, seat); + if (seat->mouse.have_discrete) + return; - mouse_scroll(seat, amount, axis); + xassert(seat->mouse_focus != NULL); + xassert(axis < ALEN(seat->mouse.aggregated)); + + const struct terminal *term = seat->mouse_focus; + + /* + * Aggregate scrolled amount until we get at least 1.0 + * + * Without this, very slow scrolling will never actually scroll + * anything. + */ + seat->mouse.aggregated[axis] += + mouse_scroll_multiplier(term, seat) * wl_fixed_to_double(value); + if (fabs(seat->mouse.aggregated[axis]) < seat->mouse_focus->cell_height) + return; + + int lines = seat->mouse.aggregated[axis] / seat->mouse_focus->cell_height; + mouse_scroll(seat, lines, axis); + seat->mouse.aggregated[axis] -= + (double)lines * seat->mouse_focus->cell_height; +} + +static void wl_pointer_axis_discrete(void *data, struct wl_pointer *wl_pointer, + enum wl_pointer_axis axis, + int32_t discrete) { + LOG_DBG("axis_discrete: %d", discrete); + struct seat *seat = data; + + if (touch_is_active(seat)) + return; + + seat->mouse.have_discrete = true; + int amount = discrete; + + if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL) { + /* Treat mouse wheel left/right as regular buttons */ + } else + amount *= mouse_scroll_multiplier(seat->mouse_focus, seat); + + mouse_scroll(seat, amount, axis); } #if defined(WL_POINTER_AXIS_VALUE120_SINCE_VERSION) -static void -wl_pointer_axis_value120(void *data, struct wl_pointer *wl_pointer, - enum wl_pointer_axis axis, int32_t value120) -{ - LOG_DBG("axis_value120: %d -> %.2f", value120, (float)value120 / 120.); +static void wl_pointer_axis_value120(void *data, struct wl_pointer *wl_pointer, + enum wl_pointer_axis axis, + int32_t value120) { + LOG_DBG("axis_value120: %d -> %.2f", value120, (float)value120 / 120.); - struct seat *seat = data; + struct seat *seat = data; - if (touch_is_active(seat)) - return; + if (touch_is_active(seat)) + return; - seat->mouse.have_discrete = true; + seat->mouse.have_discrete = true; - /* - * 120 corresponds to a single "low-res" scroll step. - * - * When doing high-res scrolling, take the scrollback.multiplier, - * and calculate how many degrees there are per line. - * - * For example, with scrollback.multiplier = 3, we have 120 / 3 == 40. - * - * Then, accumulate high-res scroll events, until we have *at - * least* that much. Translate the accumulated value to number of - * lines, and scroll. - * - * Subtract the "used" degrees from the accumulated value, and - * keep what's left (this value will always be less than the - * per-line value). - */ - const double multiplier = mouse_scroll_multiplier(seat->mouse_focus, seat); - const double per_line = 120. / multiplier; + /* + * 120 corresponds to a single "low-res" scroll step. + * + * When doing high-res scrolling, take the scrollback.multiplier, + * and calculate how many degrees there are per line. + * + * For example, with scrollback.multiplier = 3, we have 120 / 3 == 40. + * + * Then, accumulate high-res scroll events, until we have *at + * least* that much. Translate the accumulated value to number of + * lines, and scroll. + * + * Subtract the "used" degrees from the accumulated value, and + * keep what's left (this value will always be less than the + * per-line value). + */ + const double multiplier = mouse_scroll_multiplier(seat->mouse_focus, seat); + const double per_line = 120. / multiplier; - seat->mouse.aggregated_120[axis] += (double)value120; + seat->mouse.aggregated_120[axis] += (double)value120; - if (fabs(seat->mouse.aggregated_120[axis]) < per_line) - return; + if (fabs(seat->mouse.aggregated_120[axis]) < per_line) + return; - int lines = (int)(seat->mouse.aggregated_120[axis] / per_line); - mouse_scroll(seat, lines, axis); - seat->mouse.aggregated_120[axis] -= (double)lines * per_line; + int lines = (int)(seat->mouse.aggregated_120[axis] / per_line); + mouse_scroll(seat, lines, axis); + seat->mouse.aggregated_120[axis] -= (double)lines * per_line; } #endif -static void -wl_pointer_frame(void *data, struct wl_pointer *wl_pointer) -{ - struct seat *seat = data; +static void wl_pointer_frame(void *data, struct wl_pointer *wl_pointer) { + struct seat *seat = data; - if (touch_is_active(seat)) - return; + if (touch_is_active(seat)) + return; - seat->mouse.have_discrete = false; + seat->mouse.have_discrete = false; } -static void -wl_pointer_axis_source(void *data, struct wl_pointer *wl_pointer, - uint32_t axis_source) -{ -} +static void wl_pointer_axis_source(void *data, struct wl_pointer *wl_pointer, + uint32_t axis_source) {} -static void -wl_pointer_axis_stop(void *data, struct wl_pointer *wl_pointer, - uint32_t time, uint32_t axis) -{ - struct seat *seat = data; +static void wl_pointer_axis_stop(void *data, struct wl_pointer *wl_pointer, + uint32_t time, uint32_t axis) { + struct seat *seat = data; - if (touch_is_active(seat)) - return; + if (touch_is_active(seat)) + return; - xassert(axis < ALEN(seat->mouse.aggregated)); - seat->mouse.aggregated[axis] = 0.; + xassert(axis < ALEN(seat->mouse.aggregated)); + seat->mouse.aggregated[axis] = 0.; } const struct wl_pointer_listener pointer_listener = { @@ -3700,166 +3853,155 @@ const struct wl_pointer_listener pointer_listener = { #endif }; -static bool -touch_to_scroll(struct seat *seat, struct terminal *term, - wl_fixed_t surface_x, wl_fixed_t surface_y) -{ - bool coord_updated = false; +static bool touch_to_scroll(struct seat *seat, struct terminal *term, + wl_fixed_t surface_x, wl_fixed_t surface_y) { + bool coord_updated = false; - int y = wl_fixed_to_int(surface_y) * term->scale; - int rows = (y - seat->mouse.y) / term->cell_height; - if (rows != 0) { - mouse_scroll(seat, -rows, WL_POINTER_AXIS_VERTICAL_SCROLL); - seat->mouse.y += rows * term->cell_height; - coord_updated = true; - } + int y = wl_fixed_to_int(surface_y) * term->scale; + int rows = (y - seat->mouse.y) / term->cell_height; + if (rows != 0) { + mouse_scroll(seat, -rows, WL_POINTER_AXIS_VERTICAL_SCROLL); + seat->mouse.y += rows * term->cell_height; + coord_updated = true; + } - int x = wl_fixed_to_int(surface_x) * term->scale; - int cols = (x - seat->mouse.x) / term->cell_width; - if (cols != 0) { - mouse_scroll(seat, -cols, WL_POINTER_AXIS_HORIZONTAL_SCROLL); - seat->mouse.x += cols * term->cell_width; - coord_updated = true; - } + int x = wl_fixed_to_int(surface_x) * term->scale; + int cols = (x - seat->mouse.x) / term->cell_width; + if (cols != 0) { + mouse_scroll(seat, -cols, WL_POINTER_AXIS_HORIZONTAL_SCROLL); + seat->mouse.x += cols * term->cell_width; + coord_updated = true; + } - return coord_updated; + return coord_updated; } -static void -wl_touch_down(void *data, struct wl_touch *wl_touch, uint32_t serial, - uint32_t time, struct wl_surface *surface, int32_t id, - wl_fixed_t surface_x, wl_fixed_t surface_y) -{ - struct seat *seat = data; +static void wl_touch_down(void *data, struct wl_touch *wl_touch, + uint32_t serial, uint32_t time, + struct wl_surface *surface, int32_t id, + wl_fixed_t surface_x, wl_fixed_t surface_y) { + struct seat *seat = data; - if (seat->touch.state != TOUCH_STATE_IDLE) - return; + if (seat->touch.state != TOUCH_STATE_IDLE) + return; - struct wl_window *win = wl_surface_get_user_data(surface); - struct terminal *term = win->term; + struct wl_window *win = wl_surface_get_user_data(surface); + struct terminal *term = win->term; - LOG_DBG("touch_down: touch=%p, x=%d, y=%d", (void *)wl_touch, - wl_fixed_to_int(surface_x), wl_fixed_to_int(surface_y)); + LOG_DBG("touch_down: touch=%p, x=%d, y=%d", (void *)wl_touch, + wl_fixed_to_int(surface_x), wl_fixed_to_int(surface_y)); - int x = wl_fixed_to_int(surface_x) * term->scale; - int y = wl_fixed_to_int(surface_y) * term->scale; + int x = wl_fixed_to_int(surface_x) * term->scale; + int y = wl_fixed_to_int(surface_y) * term->scale; - seat->mouse.x = x; - seat->mouse.y = y; - mouse_coord_pixel_to_cell(seat, term, x, y); + seat->mouse.x = x; + seat->mouse.y = y; + mouse_coord_pixel_to_cell(seat, term, x, y); - seat->touch.state = TOUCH_STATE_HELD; - seat->touch.serial = serial; - seat->touch.time = time + term->conf->touch.long_press_delay; - seat->touch.surface = surface; - seat->touch.surface_kind = term_surface_kind(term, surface); - seat->touch.id = id; + seat->touch.state = TOUCH_STATE_HELD; + seat->touch.serial = serial; + seat->touch.time = time + term->conf->touch.long_press_delay; + seat->touch.surface = surface; + seat->touch.surface_kind = term_surface_kind(term, surface); + seat->touch.id = id; } -static void -wl_touch_up(void *data, struct wl_touch *wl_touch, uint32_t serial, - uint32_t time, int32_t id) -{ - struct seat *seat = data; +static void wl_touch_up(void *data, struct wl_touch *wl_touch, uint32_t serial, + uint32_t time, int32_t id) { + struct seat *seat = data; - if (seat->touch.state <= TOUCH_STATE_IDLE || id != seat->touch.id) - return; + if (seat->touch.state <= TOUCH_STATE_IDLE || id != seat->touch.id) + return; - LOG_DBG("touch_up: touch=%p", (void *)wl_touch); + LOG_DBG("touch_up: touch=%p", (void *)wl_touch); - struct wl_window *win = wl_surface_get_user_data(seat->touch.surface); - struct terminal *term = win->term; + struct wl_window *win = wl_surface_get_user_data(seat->touch.surface); + struct terminal *term = win->term; - struct terminal *old_term = seat->mouse_focus; - enum term_surface old_active_surface = term->active_surface; - seat->mouse_focus = term; - term->active_surface = seat->touch.surface_kind; - - switch (seat->touch.state) { - case TOUCH_STATE_HELD: - wl_pointer_button(seat, NULL, seat->touch.serial, time, BTN_LEFT, - WL_POINTER_BUTTON_STATE_PRESSED); - /* fallthrough */ - case TOUCH_STATE_DRAGGING: - wl_pointer_button(seat, NULL, serial, time, BTN_LEFT, - WL_POINTER_BUTTON_STATE_RELEASED); - /* fallthrough */ - case TOUCH_STATE_SCROLLING: - term->active_surface = TERM_SURF_NONE; - seat->touch.state = TOUCH_STATE_IDLE; - break; - - case TOUCH_STATE_INHIBITED: - case TOUCH_STATE_IDLE: - BUG("Bad touch state: %d", seat->touch.state); - break; - } - - seat->mouse_focus = old_term; - term->active_surface = old_active_surface; -} - -static void -wl_touch_motion(void *data, struct wl_touch *wl_touch, uint32_t time, - int32_t id, wl_fixed_t surface_x, wl_fixed_t surface_y) -{ - struct seat *seat = data; - if (seat->touch.state <= TOUCH_STATE_IDLE || id != seat->touch.id) - return; - - LOG_DBG("touch_motion: touch=%p, x=%d, y=%d", (void *)wl_touch, - wl_fixed_to_int(surface_x), wl_fixed_to_int(surface_y)); - - struct wl_window *win = wl_surface_get_user_data(seat->touch.surface); - struct terminal *term = win->term; - - struct terminal *old_term = seat->mouse_focus; - enum term_surface old_active_surface = term->active_surface; - seat->mouse_focus = term; - term->active_surface = seat->touch.surface_kind; - - switch (seat->touch.state) { - case TOUCH_STATE_HELD: - if (time <= seat->touch.time && term->active_surface == TERM_SURF_GRID) { - if (touch_to_scroll(seat, term, surface_x, surface_y)) - seat->touch.state = TOUCH_STATE_SCROLLING; - break; - } else { - wl_pointer_button(seat, NULL, seat->touch.serial, time, BTN_LEFT, - WL_POINTER_BUTTON_STATE_PRESSED); - seat->touch.state = TOUCH_STATE_DRAGGING; - /* fallthrough */ - } - case TOUCH_STATE_DRAGGING: - wl_pointer_motion(seat, NULL, time, surface_x, surface_y); - break; - case TOUCH_STATE_SCROLLING: - touch_to_scroll(seat, term, surface_x, surface_y); - break; - - case TOUCH_STATE_INHIBITED: - case TOUCH_STATE_IDLE: - BUG("Bad touch state: %d", seat->touch.state); - break; - } - - seat->mouse_focus = old_term; - term->active_surface = old_active_surface; -} - -static void -wl_touch_frame(void *data, struct wl_touch *wl_touch) -{ -} - -static void -wl_touch_cancel(void *data, struct wl_touch *wl_touch) -{ - struct seat *seat = data; - if (seat->touch.state == TOUCH_STATE_INHIBITED) - return; + struct terminal *old_term = seat->mouse_focus; + enum term_surface old_active_surface = term->active_surface; + seat->mouse_focus = term; + term->active_surface = seat->touch.surface_kind; + switch (seat->touch.state) { + case TOUCH_STATE_HELD: + wl_pointer_button(seat, NULL, seat->touch.serial, time, BTN_LEFT, + WL_POINTER_BUTTON_STATE_PRESSED); + /* fallthrough */ + case TOUCH_STATE_DRAGGING: + wl_pointer_button(seat, NULL, serial, time, BTN_LEFT, + WL_POINTER_BUTTON_STATE_RELEASED); + /* fallthrough */ + case TOUCH_STATE_SCROLLING: + term->active_surface = TERM_SURF_NONE; seat->touch.state = TOUCH_STATE_IDLE; + break; + + case TOUCH_STATE_INHIBITED: + case TOUCH_STATE_IDLE: + BUG("Bad touch state: %d", seat->touch.state); + break; + } + + seat->mouse_focus = old_term; + term->active_surface = old_active_surface; +} + +static void wl_touch_motion(void *data, struct wl_touch *wl_touch, + uint32_t time, int32_t id, wl_fixed_t surface_x, + wl_fixed_t surface_y) { + struct seat *seat = data; + if (seat->touch.state <= TOUCH_STATE_IDLE || id != seat->touch.id) + return; + + LOG_DBG("touch_motion: touch=%p, x=%d, y=%d", (void *)wl_touch, + wl_fixed_to_int(surface_x), wl_fixed_to_int(surface_y)); + + struct wl_window *win = wl_surface_get_user_data(seat->touch.surface); + struct terminal *term = win->term; + + struct terminal *old_term = seat->mouse_focus; + enum term_surface old_active_surface = term->active_surface; + seat->mouse_focus = term; + term->active_surface = seat->touch.surface_kind; + + switch (seat->touch.state) { + case TOUCH_STATE_HELD: + if (time <= seat->touch.time && term->active_surface == TERM_SURF_GRID) { + if (touch_to_scroll(seat, term, surface_x, surface_y)) + seat->touch.state = TOUCH_STATE_SCROLLING; + break; + } else { + wl_pointer_button(seat, NULL, seat->touch.serial, time, BTN_LEFT, + WL_POINTER_BUTTON_STATE_PRESSED); + seat->touch.state = TOUCH_STATE_DRAGGING; + /* fallthrough */ + } + case TOUCH_STATE_DRAGGING: + wl_pointer_motion(seat, NULL, time, surface_x, surface_y); + break; + case TOUCH_STATE_SCROLLING: + touch_to_scroll(seat, term, surface_x, surface_y); + break; + + case TOUCH_STATE_INHIBITED: + case TOUCH_STATE_IDLE: + BUG("Bad touch state: %d", seat->touch.state); + break; + } + + seat->mouse_focus = old_term; + term->active_surface = old_active_surface; +} + +static void wl_touch_frame(void *data, struct wl_touch *wl_touch) {} + +static void wl_touch_cancel(void *data, struct wl_touch *wl_touch) { + struct seat *seat = data; + if (seat->touch.state == TOUCH_STATE_INHIBITED) + return; + + seat->touch.state = TOUCH_STATE_IDLE; } const struct wl_touch_listener touch_listener = { diff --git a/key-binding.h b/key-binding.h index 45702ee..e008ecc 100644 --- a/key-binding.h +++ b/key-binding.h @@ -65,6 +65,11 @@ enum bind_action_normal { BIND_ACTION_TAB_9, BIND_ACTION_TAB_OVERVIEW, + /* Session actions */ + BIND_ACTION_SESSION_SAVE, + BIND_ACTION_SESSION_LOAD, + BIND_ACTION_SESSION_SAVE_SECURE, + /* Mouse specific actions - i.e. they require a mouse coordinate */ BIND_ACTION_SCROLLBACK_UP_MOUSE, BIND_ACTION_SCROLLBACK_DOWN_MOUSE, @@ -77,7 +82,7 @@ enum bind_action_normal { BIND_ACTION_SELECT_QUOTE, BIND_ACTION_SELECT_ROW, - BIND_ACTION_KEY_COUNT = BIND_ACTION_TAB_OVERVIEW + 1, + BIND_ACTION_KEY_COUNT = BIND_ACTION_SESSION_SAVE_SECURE + 1, BIND_ACTION_COUNT = BIND_ACTION_SELECT_ROW + 1, }; diff --git a/meson.build b/meson.build index a0e602b..9a1efa1 100644 --- a/meson.build +++ b/meson.build @@ -162,6 +162,7 @@ endif tllist = dependency('tllist', version: '>=1.1.0', fallback: 'tllist') fcft = dependency('fcft', version: ['>=3.3.1', '<4.0.0'], fallback: 'fcft') +libsodium = dependency('libsodium') wayland_protocols_datadir = wayland_protocols.get_variable('pkgdatadir') @@ -330,6 +331,8 @@ executable( 'reaper.c', 'reaper.h', 'render.c', 'render.h', 'search.c', 'search.h', + 'session.c', 'session.h', 'session-prompt.c', + 'session-crypto.c', 'session-crypto.h', 'server.c', 'server.h', 'client-protocol.h', 'shm.c', 'shm.h', 'slave.c', 'slave.h', @@ -342,7 +345,7 @@ executable( 'xkbcommon-vmod.h', srgb_funcs, wl_proto_src + wl_proto_headers, version, dependencies: [math, threads, libepoll, pixman, wayland_client, wayland_cursor, xkb, fontconfig, utf8proc, - tllist, fcft], + tllist, fcft, libsodium], link_with: pgolib, install: true) diff --git a/render.c b/render.c index 9e8548a..2e2d16b 100644 --- a/render.c +++ b/render.c @@ -5,18 +5,19 @@ #include #include +#include +#include #include #include #include -#include -#include #include "macros.h" #if HAS_INCLUDE() - #include - #define pthread_setname_np(thread, name) (pthread_set_name_np(thread, name), 0) +#include +#define pthread_setname_np(thread, name) (pthread_set_name_np(thread, name), 0) #elif defined(__NetBSD__) - #define pthread_setname_np(thread, name) pthread_setname_np(thread, "%s", (void *)name) +#define pthread_setname_np(thread, name) \ + pthread_setname_np(thread, "%s", (void *)name) #endif #include @@ -28,13 +29,13 @@ #define LOG_MODULE "render" #define LOG_ENABLE_DBG 0 -#include "log.h" #include "box-drawing.h" #include "char32.h" #include "config.h" #include "cursor-shape.h" #include "grid.h" #include "ime.h" +#include "log.h" #include "quirks.h" #include "search.h" #include "selection.h" @@ -48,494 +49,467 @@ #define TIME_SCROLL_DAMAGE 0 struct renderer { - struct fdm *fdm; - struct wayland *wayl; + struct fdm *fdm; + struct wayland *wayl; }; static struct { - size_t total; - size_t zero; /* commits presented in less than one frame interval */ - size_t one; /* commits presented in one frame interval */ - size_t two; /* commits presented in two or more frame intervals */ + size_t total; + size_t zero; /* commits presented in less than one frame interval */ + size_t one; /* commits presented in one frame interval */ + size_t two; /* commits presented in two or more frame intervals */ } presentation_statistics = {0}; static void fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data); static void render_tab_bar(struct terminal *term); static void render_tab_overview(struct terminal *term); -struct renderer * -render_init(struct fdm *fdm, struct wayland *wayl) -{ - struct renderer *renderer = malloc(sizeof(*renderer)); - if (unlikely(renderer == NULL)) { - LOG_ERRNO("malloc() failed"); - return NULL; - } +struct renderer *render_init(struct fdm *fdm, struct wayland *wayl) { + struct renderer *renderer = malloc(sizeof(*renderer)); + if (unlikely(renderer == NULL)) { + LOG_ERRNO("malloc() failed"); + return NULL; + } - *renderer = (struct renderer) { - .fdm = fdm, - .wayl = wayl, - }; - - if (!fdm_hook_add(fdm, &fdm_hook_refresh_pending_terminals, renderer, - FDM_HOOK_PRIORITY_NORMAL)) - { - LOG_ERR("failed to register FDM hook"); - free(renderer); - return NULL; - } - - return renderer; -} - -void -render_destroy(struct renderer *renderer) -{ - if (renderer == NULL) - return; - - fdm_hook_del(renderer->fdm, &fdm_hook_refresh_pending_terminals, - FDM_HOOK_PRIORITY_NORMAL); + *renderer = (struct renderer){ + .fdm = fdm, + .wayl = wayl, + }; + if (!fdm_hook_add(fdm, &fdm_hook_refresh_pending_terminals, renderer, + FDM_HOOK_PRIORITY_NORMAL)) { + LOG_ERR("failed to register FDM hook"); free(renderer); + return NULL; + } + + return renderer; } -static void DESTRUCTOR -log_presentation_statistics(void) -{ - if (presentation_statistics.total == 0) - return; +void render_destroy(struct renderer *renderer) { + if (renderer == NULL) + return; - const size_t total = presentation_statistics.total; - LOG_INFO("presentation statistics: zero=%f%%, one=%f%%, two=%f%%", - 100. * presentation_statistics.zero / total, - 100. * presentation_statistics.one / total, - 100. * presentation_statistics.two / total); + fdm_hook_del(renderer->fdm, &fdm_hook_refresh_pending_terminals, + FDM_HOOK_PRIORITY_NORMAL); + + free(renderer); +} + +static void DESTRUCTOR log_presentation_statistics(void) { + if (presentation_statistics.total == 0) + return; + + const size_t total = presentation_statistics.total; + LOG_INFO("presentation statistics: zero=%f%%, one=%f%%, two=%f%%", + 100. * presentation_statistics.zero / total, + 100. * presentation_statistics.one / total, + 100. * presentation_statistics.two / total); } static void sync_output(void *data, struct wp_presentation_feedback *wp_presentation_feedback, - struct wl_output *output) -{ -} + struct wl_output *output) {} struct presentation_context { - struct terminal *term; - struct timeval input; - struct timeval commit; + struct terminal *term; + struct timeval input; + struct timeval commit; }; -static void -presented(void *data, - struct wp_presentation_feedback *wp_presentation_feedback, - uint32_t tv_sec_hi, uint32_t tv_sec_lo, uint32_t tv_nsec, - uint32_t refresh, uint32_t seq_hi, uint32_t seq_lo, uint32_t flags) -{ - struct presentation_context *ctx = data; - struct terminal *term = ctx->term; - const struct timeval *input = &ctx->input; - const struct timeval *commit = &ctx->commit; +static void presented(void *data, + struct wp_presentation_feedback *wp_presentation_feedback, + uint32_t tv_sec_hi, uint32_t tv_sec_lo, uint32_t tv_nsec, + uint32_t refresh, uint32_t seq_hi, uint32_t seq_lo, + uint32_t flags) { + struct presentation_context *ctx = data; + struct terminal *term = ctx->term; + const struct timeval *input = &ctx->input; + const struct timeval *commit = &ctx->commit; - const struct timeval presented = { - .tv_sec = (uint64_t)tv_sec_hi << 32 | tv_sec_lo, - .tv_usec = tv_nsec / 1000, - }; + const struct timeval presented = { + .tv_sec = (uint64_t)tv_sec_hi << 32 | tv_sec_lo, + .tv_usec = tv_nsec / 1000, + }; - bool use_input = (input->tv_sec > 0 || input->tv_usec > 0) && - timercmp(&presented, input, >); - char msg[1024]; - int chars = 0; + bool use_input = (input->tv_sec > 0 || input->tv_usec > 0) && + timercmp(&presented, input, >); + char msg[1024]; + int chars = 0; - if (use_input && timercmp(&presented, input, <)) - return; - else if (timercmp(&presented, commit, <)) - return; + if (use_input && timercmp(&presented, input, <)) + return; + else if (timercmp(&presented, commit, <)) + return; - LOG_DBG("commit: %lu s %lu µs, presented: %lu s %lu µs", - commit->tv_sec, commit->tv_usec, presented.tv_sec, presented.tv_usec); - - if (use_input) { - struct timeval diff; - timersub(commit, input, &diff); - chars += snprintf( - &msg[chars], sizeof(msg) - chars, - "input - %llu µs -> ", (unsigned long long)diff.tv_usec); - } + LOG_DBG("commit: %lu s %lu µs, presented: %lu s %lu µs", commit->tv_sec, + commit->tv_usec, presented.tv_sec, presented.tv_usec); + if (use_input) { struct timeval diff; + timersub(commit, input, &diff); + chars += snprintf(&msg[chars], sizeof(msg) - chars, "input - %llu µs -> ", + (unsigned long long)diff.tv_usec); + } + + struct timeval diff; + timersub(&presented, commit, &diff); + chars += snprintf(&msg[chars], sizeof(msg) - chars, "commit - %llu µs -> ", + (unsigned long long)diff.tv_usec); + + if (use_input) { + xassert(timercmp(&presented, input, >)); + timersub(&presented, input, &diff); + } else { + xassert(timercmp(&presented, commit, >)); timersub(&presented, commit, &diff); - chars += snprintf( - &msg[chars], sizeof(msg) - chars, - "commit - %llu µs -> ", (unsigned long long)diff.tv_usec); + } - if (use_input) { - xassert(timercmp(&presented, input, >)); - timersub(&presented, input, &diff); - } else { - xassert(timercmp(&presented, commit, >)); - timersub(&presented, commit, &diff); - } + chars += + snprintf(&msg[chars], sizeof(msg) - chars, "presented (total: %llu µs)", + (unsigned long long)diff.tv_usec); - chars += snprintf( - &msg[chars], sizeof(msg) - chars, - "presented (total: %llu µs)", (unsigned long long)diff.tv_usec); + unsigned frame_count = 0; + if (tll_length(term->window->on_outputs) > 0) { + const struct monitor *mon = tll_front(term->window->on_outputs); + frame_count = + (diff.tv_sec * 1000000. + diff.tv_usec) / (1000000. / mon->refresh); + } - unsigned frame_count = 0; - if (tll_length(term->window->on_outputs) > 0) { - const struct monitor *mon = tll_front(term->window->on_outputs); - frame_count = (diff.tv_sec * 1000000. + diff.tv_usec) / (1000000. / mon->refresh); - } - - presentation_statistics.total++; - if (frame_count >= 2) - presentation_statistics.two++; - else if (frame_count >= 1) - presentation_statistics.one++; - else - presentation_statistics.zero++; + presentation_statistics.total++; + if (frame_count >= 2) + presentation_statistics.two++; + else if (frame_count >= 1) + presentation_statistics.one++; + else + presentation_statistics.zero++; #define _log_fmt "%s (more than %u frames)" - if (frame_count >= 2) - LOG_ERR(_log_fmt, msg, frame_count); - else if (frame_count >= 1) - LOG_WARN(_log_fmt, msg, frame_count); - else - LOG_INFO(_log_fmt, msg, frame_count); + if (frame_count >= 2) + LOG_ERR(_log_fmt, msg, frame_count); + else if (frame_count >= 1) + LOG_WARN(_log_fmt, msg, frame_count); + else + LOG_INFO(_log_fmt, msg, frame_count); #undef _log_fmt - wp_presentation_feedback_destroy(wp_presentation_feedback); - free(ctx); + wp_presentation_feedback_destroy(wp_presentation_feedback); + free(ctx); } static void -discarded(void *data, struct wp_presentation_feedback *wp_presentation_feedback) -{ - struct presentation_context *ctx = data; - wp_presentation_feedback_destroy(wp_presentation_feedback); - free(ctx); +discarded(void *data, + struct wp_presentation_feedback *wp_presentation_feedback) { + struct presentation_context *ctx = data; + wp_presentation_feedback_destroy(wp_presentation_feedback); + free(ctx); } -static const struct wp_presentation_feedback_listener presentation_feedback_listener = { - .sync_output = &sync_output, - .presented = &presented, - .discarded = &discarded, +static const struct wp_presentation_feedback_listener + presentation_feedback_listener = { + .sync_output = &sync_output, + .presented = &presented, + .discarded = &discarded, }; -static struct fcft_font * -attrs_to_font(const struct terminal *term, const struct attributes *attrs) -{ - int idx = attrs->italic << 1 | attrs->bold; - return term->fonts[idx]; +static struct fcft_font *attrs_to_font(const struct terminal *term, + const struct attributes *attrs) { + int idx = attrs->italic << 1 | attrs->bold; + return term->fonts[idx]; } -static pixman_color_t -color_hex_to_pixman_srgb(uint32_t color, uint16_t alpha) -{ - return (pixman_color_t){ - .alpha = alpha, /* Consider alpha linear already? */ - .red = srgb_decode_8_to_16((color >> 16) & 0xff), - .green = srgb_decode_8_to_16((color >> 8) & 0xff), - .blue = srgb_decode_8_to_16((color >> 0) & 0xff), +static pixman_color_t color_hex_to_pixman_srgb(uint32_t color, uint16_t alpha) { + return (pixman_color_t){ + .alpha = alpha, /* Consider alpha linear already? */ + .red = srgb_decode_8_to_16((color >> 16) & 0xff), + .green = srgb_decode_8_to_16((color >> 8) & 0xff), + .blue = srgb_decode_8_to_16((color >> 0) & 0xff), + }; +} + +static inline pixman_color_t +color_hex_to_pixman_with_alpha(uint32_t color, uint16_t alpha, bool srgb) { + pixman_color_t ret; + + if (srgb) + ret = color_hex_to_pixman_srgb(color, alpha); + else { + ret = (pixman_color_t){ + .red = ((color >> 16 & 0xff) | (color >> 8 & 0xff00)), + .green = ((color >> 8 & 0xff) | (color >> 0 & 0xff00)), + .blue = ((color >> 0 & 0xff) | (color << 8 & 0xff00)), + .alpha = alpha, }; + } + + ret.red = (uint32_t)ret.red * alpha / 0xffff; + ret.green = (uint32_t)ret.green * alpha / 0xffff; + ret.blue = (uint32_t)ret.blue * alpha / 0xffff; + + return ret; } -static inline pixman_color_t -color_hex_to_pixman_with_alpha(uint32_t color, uint16_t alpha, bool srgb) -{ - pixman_color_t ret; - - if (srgb) - ret = color_hex_to_pixman_srgb(color, alpha); - else { - ret = (pixman_color_t){ - .red = ((color >> 16 & 0xff) | (color >> 8 & 0xff00)), - .green = ((color >> 8 & 0xff) | (color >> 0 & 0xff00)), - .blue = ((color >> 0 & 0xff) | (color << 8 & 0xff00)), - .alpha = alpha, - }; - } - - ret.red = (uint32_t)ret.red * alpha / 0xffff; - ret.green = (uint32_t)ret.green * alpha / 0xffff; - ret.blue = (uint32_t)ret.blue * alpha / 0xffff; - - return ret; -} - -static inline pixman_color_t -color_hex_to_pixman(uint32_t color, bool srgb) -{ - /* Count on the compiler optimizing this */ - return color_hex_to_pixman_with_alpha(color, 0xffff, srgb); +static inline pixman_color_t color_hex_to_pixman(uint32_t color, bool srgb) { + /* Count on the compiler optimizing this */ + return color_hex_to_pixman_with_alpha(color, 0xffff, srgb); } static inline int i_lerp(int from, int to, float t) { - return from + (to - from) * t; + return from + (to - from) * t; } -static inline uint32_t -color_blend_towards(uint32_t from, uint32_t to, float amount) -{ - if (unlikely(amount == 0)) - return from; - float t = 1 - 1/amount; +static inline uint32_t color_blend_towards(uint32_t from, uint32_t to, + float amount) { + if (unlikely(amount == 0)) + return from; + float t = 1 - 1 / amount; - uint32_t alpha = from & 0xff000000; - uint8_t r = i_lerp((from>>16)&0xff, (to>>16)&0xff, t); - uint8_t g = i_lerp((from>>8)&0xff, (to>>8)&0xff, t); - uint8_t b = i_lerp((from>>0)&0xff, (to>>0)&0xff, t); + uint32_t alpha = from & 0xff000000; + uint8_t r = i_lerp((from >> 16) & 0xff, (to >> 16) & 0xff, t); + uint8_t g = i_lerp((from >> 8) & 0xff, (to >> 8) & 0xff, t); + uint8_t b = i_lerp((from >> 0) & 0xff, (to >> 0) & 0xff, t); - return alpha | (r<<16) | (g<<8) | (b<<0); + return alpha | (r << 16) | (g << 8) | (b << 0); } -static inline uint32_t -color_dim(const struct terminal *term, uint32_t color) -{ - const struct config *conf = term->conf; - const uint8_t custom_dim = conf->colors_dark.use_custom.dim; +static inline uint32_t color_dim(const struct terminal *term, uint32_t color) { + const struct config *conf = term->conf; + const uint8_t custom_dim = conf->colors_dark.use_custom.dim; - if (unlikely(custom_dim != 0)) { - for (size_t i = 0; i < 8; i++) { - if (((custom_dim >> i) & 1) == 0) - continue; + if (unlikely(custom_dim != 0)) { + for (size_t i = 0; i < 8; i++) { + if (((custom_dim >> i) & 1) == 0) + continue; - if (term->colors.table[0 + i] == color) { - /* "Regular" color, return the corresponding "dim" */ - return conf->colors_dark.dim[i]; - } + if (term->colors.table[0 + i] == color) { + /* "Regular" color, return the corresponding "dim" */ + return conf->colors_dark.dim[i]; + } - else if (term->colors.table[8 + i] == color) { - /* "Bright" color, return the corresponding "regular" */ - return term->colors.table[i]; - } - } + else if (term->colors.table[8 + i] == color) { + /* "Bright" color, return the corresponding "regular" */ + return term->colors.table[i]; + } } + } - const struct color_theme *theme = term_theme_get(term); + const struct color_theme *theme = term_theme_get(term); - return color_blend_towards( - color, - theme->dim_blend_towards == DIM_BLEND_TOWARDS_BLACK ? 0x00000000 : 0x00ffffff, - conf->dim.amount); + return color_blend_towards(color, + theme->dim_blend_towards == DIM_BLEND_TOWARDS_BLACK + ? 0x00000000 + : 0x00ffffff, + conf->dim.amount); } -static inline uint32_t -color_brighten(const struct terminal *term, uint32_t color) -{ - /* - * First try to match the color against the base 8 colors. If we - * find a match, return the corresponding bright color. - */ - if (term->conf->bold_in_bright.palette_based) { - for (size_t i = 0; i < 8; i++) { - if (term->colors.table[i] == color) - return term->colors.table[i + 8]; - } - return color; +static inline uint32_t color_brighten(const struct terminal *term, + uint32_t color) { + /* + * First try to match the color against the base 8 colors. If we + * find a match, return the corresponding bright color. + */ + if (term->conf->bold_in_bright.palette_based) { + for (size_t i = 0; i < 8; i++) { + if (term->colors.table[i] == color) + return term->colors.table[i + 8]; } + return color; + } - return color_blend_towards(color, 0x00ffffff, term->conf->bold_in_bright.amount); + return color_blend_towards(color, 0x00ffffff, + term->conf->bold_in_bright.amount); } -static void -draw_hollow_block(const struct terminal *term, pixman_image_t *pix, - const pixman_color_t *color, int x, int y, int cell_cols) -{ - const int scale = (int)roundf(term->scale); - const int width = min(min(scale, term->cell_width), term->cell_height); +static void draw_hollow_block(const struct terminal *term, pixman_image_t *pix, + const pixman_color_t *color, int x, int y, + int cell_cols) { + const int scale = (int)roundf(term->scale); + const int width = min(min(scale, term->cell_width), term->cell_height); - pixman_image_fill_rectangles( - PIXMAN_OP_SRC, pix, color, 4, - (pixman_rectangle16_t []){ - {x, y, cell_cols * term->cell_width, width}, /* top */ - {x, y, width, term->cell_height}, /* left */ - {x + cell_cols * term->cell_width - width, y, width, term->cell_height}, /* right */ - {x, y + term->cell_height - width, cell_cols * term->cell_width, width}, /* bottom */ - }); + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, color, 4, + (pixman_rectangle16_t[]){ + {x, y, cell_cols * term->cell_width, width}, /* top */ + {x, y, width, term->cell_height}, /* left */ + {x + cell_cols * term->cell_width - width, y, width, + term->cell_height}, /* right */ + {x, y + term->cell_height - width, cell_cols * term->cell_width, + width}, /* bottom */ + }); } -static void -draw_beam_cursor(const struct terminal *term, pixman_image_t *pix, - const struct fcft_font *font, - const pixman_color_t *color, int x, int y) -{ - int baseline = y + term->font_baseline - term->fonts[0]->ascent; - pixman_image_fill_rectangles( - PIXMAN_OP_SRC, pix, color, - 1, &(pixman_rectangle16_t){ - x, baseline, - term_pt_or_px_as_pixels(term, &term->conf->cursor.beam_thickness), - term->fonts[0]->ascent + term->fonts[0]->descent}); +static void draw_beam_cursor(const struct terminal *term, pixman_image_t *pix, + const struct fcft_font *font, + const pixman_color_t *color, int x, int y) { + int baseline = y + term->font_baseline - term->fonts[0]->ascent; + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){ + x, baseline, + term_pt_or_px_as_pixels(term, &term->conf->cursor.beam_thickness), + term->fonts[0]->ascent + term->fonts[0]->descent}); } -static int -underline_offset(const struct terminal *term, const struct fcft_font *font) -{ - return term->font_baseline - - (term->conf->use_custom_underline_offset - ? -term_pt_or_px_as_pixels(term, &term->conf->underline_offset) - : font->underline.position); +static int underline_offset(const struct terminal *term, + const struct fcft_font *font) { + return term->font_baseline - + (term->conf->use_custom_underline_offset + ? -term_pt_or_px_as_pixels(term, &term->conf->underline_offset) + : font->underline.position); } -static void -draw_underline_cursor(const struct terminal *term, pixman_image_t *pix, - const struct fcft_font *font, - const pixman_color_t *color, int x, int y, int cols) -{ - int thickness = term->conf->cursor.underline_thickness.px >= 0 - ? term_pt_or_px_as_pixels( - term, &term->conf->cursor.underline_thickness) - : font->underline.thickness; +static void draw_underline_cursor(const struct terminal *term, + pixman_image_t *pix, + const struct fcft_font *font, + const pixman_color_t *color, int x, int y, + int cols) { + int thickness = term->conf->cursor.underline_thickness.px >= 0 + ? term_pt_or_px_as_pixels( + term, &term->conf->cursor.underline_thickness) + : font->underline.thickness; - /* Make sure the line isn't positioned below the cell */ - const int y_ofs = min(underline_offset(term, font) + thickness, - term->cell_height - thickness); + /* Make sure the line isn't positioned below the cell */ + const int y_ofs = min(underline_offset(term, font) + thickness, + term->cell_height - thickness); - pixman_image_fill_rectangles( - PIXMAN_OP_SRC, pix, color, - 1, &(pixman_rectangle16_t){ - x, y + y_ofs, cols * term->cell_width, thickness}); + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){x, y + y_ofs, + cols * term->cell_width, + thickness}); } -static void -draw_underline(const struct terminal *term, pixman_image_t *pix, - const struct fcft_font *font, - const pixman_color_t *color, int x, int y, int cols) -{ - const int thickness = term->conf->underline_thickness.px >= 0 - ? term_pt_or_px_as_pixels( - term, &term->conf->underline_thickness) - : font->underline.thickness; +static void draw_underline(const struct terminal *term, pixman_image_t *pix, + const struct fcft_font *font, + const pixman_color_t *color, int x, int y, + int cols) { + const int thickness = + term->conf->underline_thickness.px >= 0 + ? term_pt_or_px_as_pixels(term, &term->conf->underline_thickness) + : font->underline.thickness; - /* Make sure the line isn't positioned below the cell */ - const int y_ofs = min(underline_offset(term, font), - term->cell_height - thickness); + /* Make sure the line isn't positioned below the cell */ + const int y_ofs = + min(underline_offset(term, font), term->cell_height - thickness); - pixman_image_fill_rectangles( - PIXMAN_OP_SRC, pix, color, - 1, &(pixman_rectangle16_t){ - x, y + y_ofs, cols * term->cell_width, thickness}); + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){x, y + y_ofs, + cols * term->cell_width, + thickness}); } static void draw_styled_underline(const struct terminal *term, pixman_image_t *pix, - const struct fcft_font *font, - const pixman_color_t *color, - enum underline_style style, int x, int y, int cols) -{ - xassert(style != UNDERLINE_NONE); + const struct fcft_font *font, const pixman_color_t *color, + enum underline_style style, int x, int y, int cols) { + xassert(style != UNDERLINE_NONE); - if (style == UNDERLINE_SINGLE) { - draw_underline(term, pix, font, color, x, y, cols); - return; - } + if (style == UNDERLINE_SINGLE) { + draw_underline(term, pix, font, color, x, y, cols); + return; + } - const int thickness = term->conf->underline_thickness.px >= 0 - ? term_pt_or_px_as_pixels( - term, &term->conf->underline_thickness) - : font->underline.thickness; + const int thickness = + term->conf->underline_thickness.px >= 0 + ? term_pt_or_px_as_pixels(term, &term->conf->underline_thickness) + : font->underline.thickness; - int y_ofs; + int y_ofs; - /* Make sure the line isn't positioned below the cell */ - switch (style) { - case UNDERLINE_DOUBLE: - case UNDERLINE_CURLY: - y_ofs = min(underline_offset(term, font), - term->cell_height - thickness * 3); - break; + /* Make sure the line isn't positioned below the cell */ + switch (style) { + case UNDERLINE_DOUBLE: + case UNDERLINE_CURLY: + y_ofs = + min(underline_offset(term, font), term->cell_height - thickness * 3); + break; - case UNDERLINE_DASHED: - case UNDERLINE_DOTTED: - y_ofs = min(underline_offset(term, font), - term->cell_height - thickness); - break; + case UNDERLINE_DASHED: + case UNDERLINE_DOTTED: + y_ofs = min(underline_offset(term, font), term->cell_height - thickness); + break; - case UNDERLINE_NONE: - case UNDERLINE_SINGLE: - default: - BUG("unexpected underline style: %d", (int)style); - return; - } + case UNDERLINE_NONE: + case UNDERLINE_SINGLE: + default: + BUG("unexpected underline style: %d", (int)style); + return; + } + const int ceil_w = cols * term->cell_width; + + switch (style) { + case UNDERLINE_DOUBLE: { + const pixman_rectangle16_t rects[] = { + {x, y + y_ofs, ceil_w, thickness}, + {x, y + y_ofs + thickness * 2, ceil_w, thickness}}; + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 2, rects); + break; + } + + case UNDERLINE_DASHED: { const int ceil_w = cols * term->cell_width; + const int dash_w = ceil_w / 3 + (ceil_w % 3 > 0); + const pixman_rectangle16_t rects[] = { + {x, y + y_ofs, dash_w, thickness}, + {x + dash_w * 2, y + y_ofs, dash_w, thickness}, + }; + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 2, rects); + break; + } - switch (style) { - case UNDERLINE_DOUBLE: { - const pixman_rectangle16_t rects[] = { - {x, y + y_ofs, ceil_w, thickness}, - {x, y + y_ofs + thickness * 2, ceil_w, thickness}}; - pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 2, rects); - break; + case UNDERLINE_DOTTED: { + /* Number of dots per cell */ + int per_cell = (term->cell_width / thickness) / 2; + if (per_cell == 0) + per_cell = 1; + + xassert(per_cell >= 1); + + /* Spacing between dots; start with the same width as the dots + themselves, then widen them if necessary, to consume unused + pixels */ + int spacing[per_cell]; + for (int i = 0; i < per_cell; i++) + spacing[i] = thickness; + + /* Pixels remaining at the end of the cell */ + int remaining = term->cell_width - (per_cell * 2) * thickness; + + /* Spread out the left-over pixels across the spacing between + the dots */ + for (int i = 0; remaining > 0; i = (i + 1) % per_cell, remaining--) + spacing[i]++; + + xassert(remaining <= 0); + + pixman_rectangle16_t rects[per_cell]; + int dot_x = x; + for (int i = 0; i < per_cell; i++) { + rects[i] = (pixman_rectangle16_t){dot_x, y + y_ofs, thickness, thickness}; + + dot_x += thickness + spacing[i]; } - case UNDERLINE_DASHED: { - const int ceil_w = cols * term->cell_width; - const int dash_w = ceil_w / 3 + (ceil_w % 3 > 0); - const pixman_rectangle16_t rects[] = { - {x, y + y_ofs, dash_w, thickness}, - {x + dash_w * 2, y + y_ofs, dash_w, thickness}, - }; - pixman_image_fill_rectangles( - PIXMAN_OP_SRC, pix, color, 2, rects); - break; - } + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, per_cell, rects); + break; + } - case UNDERLINE_DOTTED: { - /* Number of dots per cell */ - int per_cell = (term->cell_width / thickness) / 2; - if (per_cell == 0) - per_cell = 1; + case UNDERLINE_CURLY: { + const int top = y + y_ofs; + const int bot = top + thickness * 3; + const int half_x = x + ceil_w / 2.0, full_x = x + ceil_w; - xassert(per_cell >= 1); + const double bt_2 = (bot - top) * (bot - top); + const double th_2 = thickness * thickness; + const double hx_2 = ceil_w * ceil_w / 4.0; + const int th = round(sqrt(th_2 + (th_2 * bt_2 / hx_2)) / 2.); - /* Spacing between dots; start with the same width as the dots - themselves, then widen them if necessary, to consume unused - pixels */ - int spacing[per_cell]; - for (int i = 0; i < per_cell; i++) - spacing[i] = thickness; - - /* Pixels remaining at the end of the cell */ - int remaining = term->cell_width - (per_cell * 2) * thickness; - - /* Spread out the left-over pixels across the spacing between - the dots */ - for (int i = 0; remaining > 0; i = (i + 1) % per_cell, remaining--) - spacing[i]++; - - xassert(remaining <= 0); - - pixman_rectangle16_t rects[per_cell]; - int dot_x = x; - for (int i = 0; i < per_cell; i++) { - rects[i] = (pixman_rectangle16_t){ - dot_x, y + y_ofs, thickness, thickness - }; - - dot_x += thickness + spacing[i]; - } - - pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, per_cell, rects); - break; - } - - case UNDERLINE_CURLY: { - const int top = y + y_ofs; - const int bot = top + thickness * 3; - const int half_x = x + ceil_w / 2.0, full_x = x + ceil_w; - - const double bt_2 = (bot - top) * (bot - top); - const double th_2 = thickness * thickness; - const double hx_2 = ceil_w * ceil_w / 4.0; - const int th = round(sqrt(th_2 + (th_2 * bt_2 / hx_2)) / 2.); - - #define I(x) pixman_int_to_fixed(x) - const pixman_trapezoid_t traps[] = { -#if 0 /* characters sit within the "dips" of the curlies */ +#define I(x) pixman_int_to_fixed(x) + const pixman_trapezoid_t traps[] = { +#if 0 /* characters sit within the "dips" of the curlies */ { I(top), I(bot), {{I(x), I(top + th)}, {I(half_x), I(bot + th)}}, @@ -546,5151 +520,5134 @@ draw_styled_underline(const struct terminal *term, pixman_image_t *pix, {{I(half_x), I(bot - th)}, {I(full_x), I(top - th)}}, {{I(half_x), I(bot + th)}, {I(full_x), I(top + th)}}, } -#else /* characters sit on top of the curlies */ - { - I(top), I(bot), - {{I(x), I(bot - th)}, {I(half_x), I(top - th)}}, - {{I(x), I(bot + th)}, {I(half_x), I(top + th)}}, - }, - { - I(top), I(bot), - {{I(half_x), I(top + th)}, {I(full_x), I(bot + th)}}, - {{I(half_x), I(top - th)}, {I(full_x), I(bot - th)}}, - } +#else /* characters sit on top of the curlies */ + { + I(top), + I(bot), + {{I(x), I(bot - th)}, {I(half_x), I(top - th)}}, + {{I(x), I(bot + th)}, {I(half_x), I(top + th)}}, + }, + { + I(top), + I(bot), + {{I(half_x), I(top + th)}, {I(full_x), I(bot + th)}}, + {{I(half_x), I(top - th)}, {I(full_x), I(bot - th)}}, + } #endif - }; + }; - pixman_image_t *fill = pixman_image_create_solid_fill(color); - pixman_composite_trapezoids( - PIXMAN_OP_OVER, fill, pix, PIXMAN_a8, 0, 0, 0, 0, - sizeof(traps) / sizeof(traps[0]), traps); + pixman_image_t *fill = pixman_image_create_solid_fill(color); + pixman_composite_trapezoids(PIXMAN_OP_OVER, fill, pix, PIXMAN_a8, 0, 0, 0, + 0, sizeof(traps) / sizeof(traps[0]), traps); - pixman_image_unref(fill); - break; - } + pixman_image_unref(fill); + break; + } - case UNDERLINE_NONE: - case UNDERLINE_SINGLE: - BUG("underline styles not supposed to be handled here"); - break; - } + case UNDERLINE_NONE: + case UNDERLINE_SINGLE: + BUG("underline styles not supposed to be handled here"); + break; + } } -static void -draw_strikeout(const struct terminal *term, pixman_image_t *pix, - const struct fcft_font *font, - const pixman_color_t *color, int x, int y, int cols) -{ - const int thickness = term->conf->strikeout_thickness.px >= 0 - ? term_pt_or_px_as_pixels( - term, &term->conf->strikeout_thickness) - : font->strikeout.thickness; +static void draw_strikeout(const struct terminal *term, pixman_image_t *pix, + const struct fcft_font *font, + const pixman_color_t *color, int x, int y, + int cols) { + const int thickness = + term->conf->strikeout_thickness.px >= 0 + ? term_pt_or_px_as_pixels(term, &term->conf->strikeout_thickness) + : font->strikeout.thickness; - /* Try to center custom strikeout */ - const int position = term->conf->strikeout_thickness.px >= 0 - ? font->strikeout.position - round(font->strikeout.thickness / 2.) + round(thickness / 2.) - : font->strikeout.position; + /* Try to center custom strikeout */ + const int position = term->conf->strikeout_thickness.px >= 0 + ? font->strikeout.position - + round(font->strikeout.thickness / 2.) + + round(thickness / 2.) + : font->strikeout.position; - pixman_image_fill_rectangles( - PIXMAN_OP_SRC, pix, color, - 1, &(pixman_rectangle16_t){ - x, y + term->font_baseline - position, - cols * term->cell_width, thickness}); + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){x, y + term->font_baseline - position, + cols * term->cell_width, thickness}); } static void cursor_colors_for_cell(const struct terminal *term, const struct cell *cell, const pixman_color_t *fg, const pixman_color_t *bg, pixman_color_t *cursor_color, pixman_color_t *text_color, - bool gamma_correct) -{ - if (term->colors.cursor_bg >> 31) - *cursor_color = color_hex_to_pixman(term->colors.cursor_bg, gamma_correct); - else - *cursor_color = *fg; + bool gamma_correct) { + if (term->colors.cursor_bg >> 31) + *cursor_color = color_hex_to_pixman(term->colors.cursor_bg, gamma_correct); + else + *cursor_color = *fg; - if (term->colors.cursor_fg >> 31) - *text_color = color_hex_to_pixman(term->colors.cursor_fg, gamma_correct); - else { - xassert(bg->alpha == 0xffff); - *text_color = *bg; - } + if (term->colors.cursor_fg >> 31) + *text_color = color_hex_to_pixman(term->colors.cursor_fg, gamma_correct); + else { + xassert(bg->alpha == 0xffff); + *text_color = *bg; + } - if (text_color->red == cursor_color->red && - text_color->green == cursor_color->green && - text_color->blue == cursor_color->blue) - { - *text_color = color_hex_to_pixman(term->colors.bg, gamma_correct); - *cursor_color = color_hex_to_pixman(term->colors.fg, gamma_correct); - } + if (text_color->red == cursor_color->red && + text_color->green == cursor_color->green && + text_color->blue == cursor_color->blue) { + *text_color = color_hex_to_pixman(term->colors.bg, gamma_correct); + *cursor_color = color_hex_to_pixman(term->colors.fg, gamma_correct); + } } -static void -draw_cursor(const struct terminal *term, const struct cell *cell, - const struct fcft_font *font, pixman_image_t *pix, pixman_color_t *fg, - const pixman_color_t *bg, int x, int y, int cols) -{ - pixman_color_t cursor_color; - pixman_color_t text_color; - cursor_colors_for_cell(term, cell, fg, bg, &cursor_color, &text_color, - wayl_do_linear_blending(term->wl, term->conf)); +static void draw_cursor(const struct terminal *term, const struct cell *cell, + const struct fcft_font *font, pixman_image_t *pix, + pixman_color_t *fg, const pixman_color_t *bg, int x, + int y, int cols) { + pixman_color_t cursor_color; + pixman_color_t text_color; + cursor_colors_for_cell(term, cell, fg, bg, &cursor_color, &text_color, + wayl_do_linear_blending(term->wl, term->conf)); - if (unlikely(!term->kbd_focus)) { - switch (term->conf->cursor.unfocused_style) { - case CURSOR_UNFOCUSED_UNCHANGED: + if (unlikely(!term->kbd_focus)) { + switch (term->conf->cursor.unfocused_style) { + case CURSOR_UNFOCUSED_UNCHANGED: + break; + + case CURSOR_UNFOCUSED_HOLLOW: + draw_hollow_block(term, pix, &cursor_color, x, y, cols); + return; + + case CURSOR_UNFOCUSED_NONE: + return; + } + } + + switch (term->cursor_style) { + case CURSOR_BLOCK: + if (likely(term->cursor_blink.state == CURSOR_BLINK_ON) || + !term->kbd_focus) { + *fg = text_color; + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, &cursor_color, 1, + &(pixman_rectangle16_t){x, y, cols * term->cell_width, + term->cell_height}); + } + break; + + case CURSOR_BEAM: + if (likely(term->cursor_blink.state == CURSOR_BLINK_ON || + !term->kbd_focus)) { + draw_beam_cursor(term, pix, font, &cursor_color, x, y); + } + break; + + case CURSOR_UNDERLINE: + if (likely(term->cursor_blink.state == CURSOR_BLINK_ON || + !term->kbd_focus)) { + draw_underline_cursor(term, pix, font, &cursor_color, x, y, cols); + } + break; + + case CURSOR_HOLLOW: + if (likely(term->cursor_blink.state == CURSOR_BLINK_ON)) + draw_hollow_block(term, pix, &cursor_color, x, y, cols); + break; + } +} + +static int render_cell(struct terminal *term, pixman_image_t *pix, + pixman_region32_t *damage, struct row *row, int row_no, + int col, bool has_cursor) { + struct cell *cell = &row->cells[col]; + if (cell->attrs.clean) + return 0; + + cell->attrs.clean = 1; + cell->attrs.confined = true; + + int width = term->cell_width; + int height = term->cell_height; + const int x = term->margins.left + col * width; + const int y = term->margins.top + row_no * height; + + uint32_t _fg = 0; + uint32_t _bg = 0; + + uint16_t alpha = 0xffff; + const bool is_selected = cell->attrs.selected; + + /* Use cell specific color, if set, otherwise the default colors (possible + * reversed) */ + switch (cell->attrs.fg_src) { + case COLOR_RGB: + _fg = cell->attrs.fg; + break; + + case COLOR_BASE16: + case COLOR_BASE256: + xassert(cell->attrs.fg < ALEN(term->colors.table)); + _fg = term->colors.table[cell->attrs.fg]; + break; + + case COLOR_DEFAULT: + _fg = term->reverse ? term->colors.bg : term->colors.fg; + break; + } + + switch (cell->attrs.bg_src) { + case COLOR_RGB: + _bg = cell->attrs.bg; + break; + + case COLOR_BASE16: + case COLOR_BASE256: + xassert(cell->attrs.bg < ALEN(term->colors.table)); + _bg = term->colors.table[cell->attrs.bg]; + break; + + case COLOR_DEFAULT: + _bg = term->reverse ? term->colors.fg : term->colors.bg; + break; + } + + if (unlikely(is_selected)) { + const uint32_t cell_fg = _fg; + const uint32_t cell_bg = _bg; + + const bool custom_fg = term->colors.selection_fg >> 24 == 0; + const bool custom_bg = term->colors.selection_bg >> 24 == 0; + const bool custom_both = custom_fg && custom_bg; + + if (custom_both) { + _fg = term->colors.selection_fg; + _bg = term->colors.selection_bg; + } else if (custom_bg) { + _bg = term->colors.selection_bg; + _fg = cell->attrs.reverse ? cell_bg : cell_fg; + } else if (custom_fg) { + _fg = term->colors.selection_fg; + _bg = cell->attrs.reverse ? cell_fg : cell_bg; + } else { + _bg = cell_fg; + _fg = cell_bg; + } + + if (unlikely(_fg == _bg)) { + /* Invert bg when selected/highlighted text has same fg/bg */ + _bg = ~_bg; + alpha = 0xffff; + } + + } else { + if (unlikely(cell->attrs.reverse)) { + uint32_t swap = _fg; + _fg = _bg; + _bg = swap; + } + + else if (!term->window->is_fullscreen && term->colors.alpha != 0xffff) { + switch (term->conf->colors_dark.alpha_mode) { + case ALPHA_MODE_DEFAULT: { + if (cell->attrs.bg_src == COLOR_DEFAULT) { + alpha = term->colors.alpha; + } + break; + } + + case ALPHA_MODE_MATCHING: { + if (cell->attrs.bg_src == COLOR_DEFAULT || + ((cell->attrs.bg_src == COLOR_BASE16 || + cell->attrs.bg_src == COLOR_BASE256) && + term->colors.table[cell->attrs.bg] == term->colors.bg) || + (cell->attrs.bg_src == COLOR_RGB && + cell->attrs.bg == term->colors.bg)) { + alpha = term->colors.alpha; + } + break; + } + + case ALPHA_MODE_ALL: { + alpha = term->colors.alpha; + break; + } + } + } else { + /* + * Note: disable transparency when fullscreened. + * + * This is because the wayland protocol mandates no screen + * content is shown behind the fullscreened window. + * + * The _intent_ of the specification is that a black (or + * other static color) should be used as background. + * + * There's a bit of gray area however, and some + * compositors have chosen to interpret the specification + * in a way that allows wallpapers to be seen through a + * fullscreen window. + * + * Given that a) the intent of the specification, and b) + * we don't know what the compositor will do, we simply + * disable transparency while in fullscreen. + * + * To see why, consider what happens if we keep our + * transparency. For example, if the background color is + * white, and alpha is 0.5, then the window will be drawn + * in a shade of gray while fullscreened. + * + * See + * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/116 + * for a discussion on whether transparent, fullscreen + * windows should be allowed in some way or not. + * + * NOTE: if changing this, also update render_margin() + */ + xassert(alpha == 0xffff); + } + } + + if (cell->attrs.dim) + _fg = color_dim(term, _fg); + if (term->conf->bold_in_bright.enabled && cell->attrs.bold) + _fg = color_brighten(term, _fg); + + if (cell->attrs.blink && term->blink.state == BLINK_OFF) + _fg = color_blend_towards(_fg, 0x00000000, term->conf->dim.amount); + + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + pixman_color_t fg = color_hex_to_pixman(_fg, gamma_correct); + pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); + + struct fcft_font *font = attrs_to_font(term, &cell->attrs); + const struct composed *composed = NULL; + const struct fcft_grapheme *grapheme = NULL; + const struct fcft_glyph *single = NULL; + const struct fcft_glyph **glyphs = NULL; + unsigned glyph_count = 0; + + char32_t base = cell->wc; + int cell_cols = 1; + + if (base != 0) { + if (unlikely( + /* Classic box drawings */ + (base >= GLYPH_BOX_DRAWING_FIRST && + base <= GLYPH_BOX_DRAWING_LAST) || + + /* Braille */ + (base >= GLYPH_BRAILLE_FIRST && base <= GLYPH_BRAILLE_LAST) || + + /* + * Unicode 13 "Symbols for Legacy Computing" + * sub-ranges below. + * + * Note, the full range is U+1FB00 - U+1FBF9 + */ + (base >= GLYPH_LEGACY_FIRST && base <= GLYPH_LEGACY_LAST) || + + /* + * Unicode 16 "Symbols for Legacy Computing Supplement" + * + * Note, the full range is U+1CC00 - U+1CEAF + */ + (base >= GLYPH_OCTANTS_FIRST && base <= GLYPH_OCTANTS_LAST)) && + + likely(!term->conf->box_drawings_uses_font_glyphs)) { + struct fcft_glyph ***arr; + size_t count; + size_t idx; + + if (base >= GLYPH_LEGACY_FIRST) { + arr = &term->custom_glyphs.legacy; + count = GLYPH_LEGACY_COUNT; + idx = base - GLYPH_LEGACY_FIRST; + } else if (base >= GLYPH_OCTANTS_FIRST) { + arr = &term->custom_glyphs.octants; + count = GLYPH_OCTANTS_COUNT; + idx = base - GLYPH_OCTANTS_FIRST; + } else if (base >= GLYPH_BRAILLE_FIRST) { + arr = &term->custom_glyphs.braille; + count = GLYPH_BRAILLE_COUNT; + idx = base - GLYPH_BRAILLE_FIRST; + } else { + arr = &term->custom_glyphs.box_drawing; + count = GLYPH_BOX_DRAWING_COUNT; + idx = base - GLYPH_BOX_DRAWING_FIRST; + } + + if (unlikely(*arr == NULL)) + *arr = xcalloc(count, sizeof((*arr)[0])); + + if (likely((*arr)[idx] != NULL)) + single = (*arr)[idx]; + else { + mtx_lock(&term->render.workers.lock); + + /* Other thread may have instantiated it while we + * acquired the lock */ + single = (*arr)[idx]; + if (likely(single == NULL)) + single = (*arr)[idx] = box_drawing(term, base); + mtx_unlock(&term->render.workers.lock); + } + + if (single != NULL) { + glyph_count = 1; + glyphs = &single; + cell_cols = single->cols; + } + } + + else if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) { + composed = composed_lookup(term->composed, base - CELL_COMB_CHARS_LO); + base = composed->chars[0]; + + if (term->conf->can_shape_grapheme && + term->conf->tweak.grapheme_shaping) { + grapheme = fcft_rasterize_grapheme_utf32( + font, composed->count, composed->chars, term->font_subpixel); + } + + if (grapheme != NULL) { + const int forced_width = composed->forced_width; + + cell_cols = forced_width > 0 ? forced_width : composed->width; + + composed = NULL; + glyphs = grapheme->glyphs; + glyph_count = grapheme->count; + + if (forced_width > 0) + glyph_count = min(glyph_count, forced_width); + } + } + + if (single == NULL && grapheme == NULL) { + if (unlikely(base >= CELL_SPACER)) { + glyph_count = 0; + cell_cols = 1; + } else { + xassert(base != 0); + single = fcft_rasterize_char_utf32(font, base, term->font_subpixel); + if (single == NULL) { + glyph_count = 0; + cell_cols = 1; + } else { + glyph_count = 1; + glyphs = &single; + + const size_t forced_width = + composed != NULL ? composed->forced_width : 0; + cell_cols = forced_width > 0 ? forced_width : single->cols; + } + } + } + } + + assert(glyph_count == 0 || glyphs != NULL); + + const int cols_left = term->cols - col; + cell_cols = max(1, min(cell_cols, cols_left)); + + /* + * Determine cells that will bleed into their right neighbor and remember + * them for cleanup in the next frame. + */ + int render_width = cell_cols * width; + if (term->conf->tweak.overflowing_glyphs && glyph_count > 0 && + cols_left > cell_cols) { + int glyph_width = 0, advance = 0; + for (size_t i = 0; i < glyph_count; i++) { + glyph_width = max(glyph_width, advance + glyphs[i]->x + glyphs[i]->width); + advance += glyphs[i]->advance.x; + } + + if (glyph_width > render_width) { + render_width = min(glyph_width, render_width + width); + + for (int i = 0; i < cell_cols; i++) + row->cells[col + i].attrs.confined = false; + } + } + + pixman_region32_t clip; + pixman_region32_init_rect(&clip, x, y, render_width, term->cell_height); + pixman_image_set_clip_region32(pix, &clip); + + if (damage != NULL) { + pixman_region32_union_rect(damage, damage, x, y, render_width, + term->cell_height); + } + + pixman_region32_fini(&clip); + + /* Background */ + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, &bg, 1, + &(pixman_rectangle16_t){x, y, cell_cols * width, height}); + + if (cell->attrs.blink && term->blink.fd < 0) { + /* TODO: use a custom lock for this? */ + mtx_lock(&term->render.workers.lock); + term_arm_blink_timer(term); + mtx_unlock(&term->render.workers.lock); + } + + if (unlikely(has_cursor && term->cursor_style == CURSOR_BLOCK && + term->kbd_focus)) { + const pixman_color_t bg_without_alpha = + color_hex_to_pixman(_bg, gamma_correct); + draw_cursor(term, cell, font, pix, &fg, &bg_without_alpha, x, y, cell_cols); + } + + if (cell->wc == 0 || cell->wc >= CELL_SPACER || cell->wc == U'\t' || + (unlikely(cell->attrs.conceal) && !is_selected)) { + goto draw_cursor; + } + + pixman_image_t *clr_pix = pixman_image_create_solid_fill(&fg); + + int pen_x = x; + for (unsigned i = 0; i < glyph_count; i++) { + const int letter_x_ofs = i == 0 ? term->font_x_ofs : 0; + + const struct fcft_glyph *glyph = glyphs[i]; + if (glyph == NULL) + continue; + + int g_x = glyph->x; + int g_y = glyph->y; + + if (i > 0 && glyph->x >= 0 && cell_cols == 1) + g_x -= term->cell_width; + + if (unlikely(glyph->is_color_glyph)) { + /* Glyph surface is a pre-rendered image (typically a color emoji...) */ + if (!(cell->attrs.blink && term->blink.state == BLINK_OFF)) { + pixman_image_composite32(PIXMAN_OP_OVER, glyph->pix, NULL, pix, 0, 0, 0, + 0, pen_x + letter_x_ofs + g_x, + y + term->font_baseline - g_y, glyph->width, + glyph->height); + } + } else { + pixman_image_composite32(PIXMAN_OP_OVER, clr_pix, glyph->pix, pix, 0, 0, + 0, 0, pen_x + letter_x_ofs + g_x, + y + term->font_baseline - g_y, glyph->width, + glyph->height); + + /* Combining characters */ + if (composed != NULL) { + assert(glyph_count == 1); + + for (size_t j = 1; j < composed->count; j++) { + const struct fcft_glyph *g = fcft_rasterize_char_utf32( + font, composed->chars[j], term->font_subpixel); + + if (g == NULL) + continue; + + /* + * Fonts _should_ assume the pen position is now + * *after* the base glyph, and thus use negative + * offsets for combining glyphs. + * + * Not all fonts behave like this however, and we + * try to accommodate both variants. + * + * Since we haven't moved our pen position yet, we + * add a full cell width to the offset (or two, in + * case of double-width characters). + * + * If the font does *not* use negative offsets, + * we'd normally use an offset of 0. However, to + * somewhat deal with double-width glyphs we use + * an offset of *one* cell. + */ + int x_ofs = cell_cols == 1 ? g->x < 0 + ? cell_cols * term->cell_width + : (cell_cols - 1) * term->cell_width + : 0; + + if (cell_cols > 1) + pen_x += term->cell_width; + + pixman_image_composite32( + PIXMAN_OP_OVER, clr_pix, g->pix, pix, 0, 0, 0, 0, + /* Some fonts use a negative offset, while others use a + * "normal" offset */ + pen_x + letter_x_ofs + x_ofs + g->x, + y + term->font_baseline - g->y, g->width, g->height); + } + } + } + + pen_x += cell_cols > 1 ? term->cell_width : glyph->advance.x; + } + + pixman_image_unref(clr_pix); + + /* Underline */ + if (cell->attrs.underline) { + pixman_color_t underline_color = fg; + enum underline_style underline_style = UNDERLINE_SINGLE; + + /* Check if cell has a styled underline. This lookup is fairly + expensive... */ + if (row->extra != NULL) { + for (int i = 0; i < row->extra->underline_ranges.count; i++) { + const struct row_range *range = &row->extra->underline_ranges.v[i]; + + if (range->start > col) + break; + + if (range->start <= col && col <= range->end) { + switch (range->underline.color_src) { + case COLOR_BASE256: + underline_color = color_hex_to_pixman( + term->colors.table[range->underline.color], gamma_correct); break; - case CURSOR_UNFOCUSED_HOLLOW: - draw_hollow_block(term, pix, &cursor_color, x, y, cols); - return; + case COLOR_RGB: + underline_color = + color_hex_to_pixman(range->underline.color, gamma_correct); + break; - case CURSOR_UNFOCUSED_NONE: - return; + case COLOR_DEFAULT: + break; + + case COLOR_BASE16: + BUG("underline color can't be base-16"); + break; + } + + underline_style = range->underline.style; + break; } + } } - switch (term->cursor_style) { - case CURSOR_BLOCK: - if (likely(term->cursor_blink.state == CURSOR_BLINK_ON) || - !term->kbd_focus) - { - *fg = text_color; - pixman_image_fill_rectangles( - PIXMAN_OP_SRC, pix, &cursor_color, 1, - &(pixman_rectangle16_t){x, y, cols * term->cell_width, term->cell_height}); - } - break; - - case CURSOR_BEAM: - if (likely(term->cursor_blink.state == CURSOR_BLINK_ON || - !term->kbd_focus)) - { - draw_beam_cursor(term, pix, font, &cursor_color, x, y); - } - break; - - case CURSOR_UNDERLINE: - if (likely(term->cursor_blink.state == CURSOR_BLINK_ON || - !term->kbd_focus)) - { - draw_underline_cursor(term, pix, font, &cursor_color, x, y, cols); - } - break; - - case CURSOR_HOLLOW: - if (likely(term->cursor_blink.state == CURSOR_BLINK_ON)) - draw_hollow_block(term, pix, &cursor_color, x, y, cols); - break; - } -} - -static int -render_cell(struct terminal *term, pixman_image_t *pix, - pixman_region32_t *damage, struct row *row, int row_no, int col, - bool has_cursor) -{ - struct cell *cell = &row->cells[col]; - if (cell->attrs.clean) - return 0; - - cell->attrs.clean = 1; - cell->attrs.confined = true; - - int width = term->cell_width; - int height = term->cell_height; - const int x = term->margins.left + col * width; - const int y = term->margins.top + row_no * height; - - uint32_t _fg = 0; - uint32_t _bg = 0; - - uint16_t alpha = 0xffff; - const bool is_selected = cell->attrs.selected; - - /* Use cell specific color, if set, otherwise the default colors (possible reversed) */ - switch (cell->attrs.fg_src) { - case COLOR_RGB: - _fg = cell->attrs.fg; - break; - - case COLOR_BASE16: - case COLOR_BASE256: - xassert(cell->attrs.fg < ALEN(term->colors.table)); - _fg = term->colors.table[cell->attrs.fg]; - break; - - case COLOR_DEFAULT: - _fg = term->reverse ? term->colors.bg : term->colors.fg; - break; - } - - switch (cell->attrs.bg_src) { - case COLOR_RGB: - _bg = cell->attrs.bg; - break; - - case COLOR_BASE16: - case COLOR_BASE256: - xassert(cell->attrs.bg < ALEN(term->colors.table)); - _bg = term->colors.table[cell->attrs.bg]; - break; - - case COLOR_DEFAULT: - _bg = term->reverse ? term->colors.fg : term->colors.bg; - break; - } - - if (unlikely(is_selected)) { - const uint32_t cell_fg = _fg; - const uint32_t cell_bg = _bg; - - const bool custom_fg = term->colors.selection_fg >> 24 == 0; - const bool custom_bg = term->colors.selection_bg >> 24 == 0; - const bool custom_both = custom_fg && custom_bg; - - if (custom_both) { - _fg = term->colors.selection_fg; - _bg = term->colors.selection_bg; - } else if (custom_bg) { - _bg = term->colors.selection_bg; - _fg = cell->attrs.reverse ? cell_bg : cell_fg; - } else if (custom_fg) { - _fg = term->colors.selection_fg; - _bg = cell->attrs.reverse ? cell_fg : cell_bg; - } else { - _bg = cell_fg; - _fg = cell_bg; - } - - if (unlikely(_fg == _bg)) { - /* Invert bg when selected/highlighted text has same fg/bg */ - _bg = ~_bg; - alpha = 0xffff; - } - - } else { - if (unlikely(cell->attrs.reverse)) { - uint32_t swap = _fg; - _fg = _bg; - _bg = swap; - } - - else if (!term->window->is_fullscreen && term->colors.alpha != 0xffff) { - switch (term->conf->colors_dark.alpha_mode) { - case ALPHA_MODE_DEFAULT: { - if (cell->attrs.bg_src == COLOR_DEFAULT) { - alpha = term->colors.alpha; - } - break; - } - - case ALPHA_MODE_MATCHING: { - if (cell->attrs.bg_src == COLOR_DEFAULT || - ((cell->attrs.bg_src == COLOR_BASE16 || - cell->attrs.bg_src == COLOR_BASE256) && - term->colors.table[cell->attrs.bg] == term->colors.bg) || - (cell->attrs.bg_src == COLOR_RGB && - cell->attrs.bg == term->colors.bg)) - { - alpha = term->colors.alpha; - } - break; - } - - case ALPHA_MODE_ALL: { - alpha = term->colors.alpha; - break; - } - } - } else { - /* - * Note: disable transparency when fullscreened. - * - * This is because the wayland protocol mandates no screen - * content is shown behind the fullscreened window. - * - * The _intent_ of the specification is that a black (or - * other static color) should be used as background. - * - * There's a bit of gray area however, and some - * compositors have chosen to interpret the specification - * in a way that allows wallpapers to be seen through a - * fullscreen window. - * - * Given that a) the intent of the specification, and b) - * we don't know what the compositor will do, we simply - * disable transparency while in fullscreen. - * - * To see why, consider what happens if we keep our - * transparency. For example, if the background color is - * white, and alpha is 0.5, then the window will be drawn - * in a shade of gray while fullscreened. - * - * See - * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/116 - * for a discussion on whether transparent, fullscreen - * windows should be allowed in some way or not. - * - * NOTE: if changing this, also update render_margin() - */ - xassert(alpha == 0xffff); - } - } - - if (cell->attrs.dim) - _fg = color_dim(term, _fg); - if (term->conf->bold_in_bright.enabled && cell->attrs.bold) - _fg = color_brighten(term, _fg); - - if (cell->attrs.blink && term->blink.state == BLINK_OFF) - _fg = color_blend_towards(_fg, 0x00000000, term->conf->dim.amount); - - const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); - pixman_color_t fg = color_hex_to_pixman(_fg, gamma_correct); - pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); - - struct fcft_font *font = attrs_to_font(term, &cell->attrs); - const struct composed *composed = NULL; - const struct fcft_grapheme *grapheme = NULL; - const struct fcft_glyph *single = NULL; - const struct fcft_glyph **glyphs = NULL; - unsigned glyph_count = 0; - - char32_t base = cell->wc; - int cell_cols = 1; - - if (base != 0) { - if (unlikely( - /* Classic box drawings */ - (base >= GLYPH_BOX_DRAWING_FIRST && - base <= GLYPH_BOX_DRAWING_LAST) || - - /* Braille */ - (base >= GLYPH_BRAILLE_FIRST && - base <= GLYPH_BRAILLE_LAST) || - - /* - * Unicode 13 "Symbols for Legacy Computing" - * sub-ranges below. - * - * Note, the full range is U+1FB00 - U+1FBF9 - */ - (base >= GLYPH_LEGACY_FIRST && - base <= GLYPH_LEGACY_LAST) || - - /* - * Unicode 16 "Symbols for Legacy Computing Supplement" - * - * Note, the full range is U+1CC00 - U+1CEAF - */ - (base >= GLYPH_OCTANTS_FIRST && - base <= GLYPH_OCTANTS_LAST)) && - - likely(!term->conf->box_drawings_uses_font_glyphs)) - { - struct fcft_glyph ***arr; - size_t count; - size_t idx; - - if (base >= GLYPH_LEGACY_FIRST) { - arr = &term->custom_glyphs.legacy; - count = GLYPH_LEGACY_COUNT; - idx = base - GLYPH_LEGACY_FIRST; - } else if (base >= GLYPH_OCTANTS_FIRST) { - arr = &term->custom_glyphs.octants; - count = GLYPH_OCTANTS_COUNT; - idx = base - GLYPH_OCTANTS_FIRST; - } else if (base >= GLYPH_BRAILLE_FIRST) { - arr = &term->custom_glyphs.braille; - count = GLYPH_BRAILLE_COUNT; - idx = base - GLYPH_BRAILLE_FIRST; - } else { - arr = &term->custom_glyphs.box_drawing; - count = GLYPH_BOX_DRAWING_COUNT; - idx = base - GLYPH_BOX_DRAWING_FIRST; - } - - if (unlikely(*arr == NULL)) - *arr = xcalloc(count, sizeof((*arr)[0])); - - if (likely((*arr)[idx] != NULL)) - single = (*arr)[idx]; - else { - mtx_lock(&term->render.workers.lock); - - /* Other thread may have instantiated it while we - * acquired the lock */ - single = (*arr)[idx]; - if (likely(single == NULL)) - single = (*arr)[idx] = box_drawing(term, base); - mtx_unlock(&term->render.workers.lock); - } - - if (single != NULL) { - glyph_count = 1; - glyphs = &single; - cell_cols = single->cols; - } - } - - else if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) - { - composed = composed_lookup(term->composed, base - CELL_COMB_CHARS_LO); - base = composed->chars[0]; - - if (term->conf->can_shape_grapheme && term->conf->tweak.grapheme_shaping) { - grapheme = fcft_rasterize_grapheme_utf32( - font, composed->count, composed->chars, term->font_subpixel); - } - - if (grapheme != NULL) { - const int forced_width = composed->forced_width; - - cell_cols = forced_width > 0 ? forced_width : composed->width; - - composed = NULL; - glyphs = grapheme->glyphs; - glyph_count = grapheme->count; - - if (forced_width > 0) - glyph_count = min(glyph_count, forced_width); - } - } - - if (single == NULL && grapheme == NULL) { - if (unlikely(base >= CELL_SPACER)) { - glyph_count = 0; - cell_cols = 1; - } else { - xassert(base != 0); - single = fcft_rasterize_char_utf32(font, base, term->font_subpixel); - if (single == NULL) { - glyph_count = 0; - cell_cols = 1; - } else { - glyph_count = 1; - glyphs = &single; - - const size_t forced_width = composed != NULL ? composed->forced_width : 0; - cell_cols = forced_width > 0 ? forced_width : single->cols; - } - } - } - } - - assert(glyph_count == 0 || glyphs != NULL); - - const int cols_left = term->cols - col; - cell_cols = max(1, min(cell_cols, cols_left)); - - /* - * Determine cells that will bleed into their right neighbor and remember - * them for cleanup in the next frame. - */ - int render_width = cell_cols * width; - if (term->conf->tweak.overflowing_glyphs && - glyph_count > 0 && - cols_left > cell_cols) - { - int glyph_width = 0, advance = 0; - for (size_t i = 0; i < glyph_count; i++) { - glyph_width = max(glyph_width, - advance + glyphs[i]->x + glyphs[i]->width); - advance += glyphs[i]->advance.x; - } - - if (glyph_width > render_width) { - render_width = min(glyph_width, render_width + width); - - for (int i = 0; i < cell_cols; i++) - row->cells[col + i].attrs.confined = false; - } - } - - pixman_region32_t clip; - pixman_region32_init_rect( - &clip, x, y, - render_width, term->cell_height); - pixman_image_set_clip_region32(pix, &clip); - - if (damage != NULL) { - pixman_region32_union_rect( - damage, damage, x, y, render_width, term->cell_height); - } - - pixman_region32_fini(&clip); - - /* Background */ - pixman_image_fill_rectangles( - PIXMAN_OP_SRC, pix, &bg, 1, - &(pixman_rectangle16_t){x, y, cell_cols * width, height}); - - if (cell->attrs.blink && term->blink.fd < 0) { - /* TODO: use a custom lock for this? */ - mtx_lock(&term->render.workers.lock); - term_arm_blink_timer(term); - mtx_unlock(&term->render.workers.lock); - } - - if (unlikely(has_cursor && term->cursor_style == CURSOR_BLOCK && term->kbd_focus)) { - const pixman_color_t bg_without_alpha = color_hex_to_pixman(_bg, gamma_correct); - draw_cursor(term, cell, font, pix, &fg, &bg_without_alpha, x, y, cell_cols); - } - - if (cell->wc == 0 || cell->wc >= CELL_SPACER || cell->wc == U'\t' || - (unlikely(cell->attrs.conceal) && !is_selected)) - { - goto draw_cursor; - } - - pixman_image_t *clr_pix = pixman_image_create_solid_fill(&fg); - - int pen_x = x; - for (unsigned i = 0; i < glyph_count; i++) { - const int letter_x_ofs = i == 0 ? term->font_x_ofs : 0; - - const struct fcft_glyph *glyph = glyphs[i]; - if (glyph == NULL) - continue; - - int g_x = glyph->x; - int g_y = glyph->y; - - if (i > 0 && glyph->x >= 0 && cell_cols == 1) - g_x -= term->cell_width; - - if (unlikely(glyph->is_color_glyph)) { - /* Glyph surface is a pre-rendered image (typically a color emoji...) */ - if (!(cell->attrs.blink && term->blink.state == BLINK_OFF)) { - pixman_image_composite32( - PIXMAN_OP_OVER, glyph->pix, NULL, pix, 0, 0, 0, 0, - pen_x + letter_x_ofs + g_x, y + term->font_baseline - g_y, - glyph->width, glyph->height); - } - } else { - pixman_image_composite32( - PIXMAN_OP_OVER, clr_pix, glyph->pix, pix, 0, 0, 0, 0, - pen_x + letter_x_ofs + g_x, y + term->font_baseline - g_y, - glyph->width, glyph->height); - - /* Combining characters */ - if (composed != NULL) { - assert(glyph_count == 1); - - for (size_t j = 1; j < composed->count; j++) { - const struct fcft_glyph *g = fcft_rasterize_char_utf32( - font, composed->chars[j], term->font_subpixel); - - if (g == NULL) - continue; - - /* - * Fonts _should_ assume the pen position is now - * *after* the base glyph, and thus use negative - * offsets for combining glyphs. - * - * Not all fonts behave like this however, and we - * try to accommodate both variants. - * - * Since we haven't moved our pen position yet, we - * add a full cell width to the offset (or two, in - * case of double-width characters). - * - * If the font does *not* use negative offsets, - * we'd normally use an offset of 0. However, to - * somewhat deal with double-width glyphs we use - * an offset of *one* cell. - */ - int x_ofs = cell_cols == 1 - ? g->x < 0 - ? cell_cols * term->cell_width - : (cell_cols - 1) * term->cell_width - : 0; - - if (cell_cols > 1) - pen_x += term->cell_width; - - pixman_image_composite32( - PIXMAN_OP_OVER, clr_pix, g->pix, pix, 0, 0, 0, 0, - /* Some fonts use a negative offset, while others use a - * "normal" offset */ - pen_x + letter_x_ofs + x_ofs + g->x, - y + term->font_baseline - g->y, g->width, g->height); - } - } - } - - pen_x += cell_cols > 1 ? term->cell_width : glyph->advance.x; - } - - pixman_image_unref(clr_pix); - - /* Underline */ - if (cell->attrs.underline) { - pixman_color_t underline_color = fg; - enum underline_style underline_style = UNDERLINE_SINGLE; - - /* Check if cell has a styled underline. This lookup is fairly - expensive... */ - if (row->extra != NULL) { - for (int i = 0; i < row->extra->underline_ranges.count; i++) { - const struct row_range *range = &row->extra->underline_ranges.v[i]; - - if (range->start > col) - break; - - if (range->start <= col && col <= range->end) { - switch (range->underline.color_src) { - case COLOR_BASE256: - underline_color = color_hex_to_pixman( - term->colors.table[range->underline.color], gamma_correct); - break; - - case COLOR_RGB: - underline_color = - color_hex_to_pixman(range->underline.color, gamma_correct); - break; - - case COLOR_DEFAULT: - break; - - case COLOR_BASE16: - BUG("underline color can't be base-16"); - break; - } - - underline_style = range->underline.style; - break; - } - } - } - - draw_styled_underline( - term, pix, font, &underline_color, underline_style, x, y, cell_cols); - - } - - if (cell->attrs.strikethrough) - draw_strikeout(term, pix, font, &fg, x, y, cell_cols); - - if (unlikely(cell->attrs.url && term->conf->url.style != UNDERLINE_NONE)) { - pixman_color_t url_color = color_hex_to_pixman( - term->conf->colors_dark.use_custom.url - ? term->conf->colors_dark.url - : term->colors.table[3], - gamma_correct); - - draw_styled_underline( - term, pix, font, &url_color, term->conf->url.style, - x, y, cell_cols); - } + draw_styled_underline(term, pix, font, &underline_color, underline_style, x, + y, cell_cols); + } + + if (cell->attrs.strikethrough) + draw_strikeout(term, pix, font, &fg, x, y, cell_cols); + + if (unlikely(cell->attrs.url && term->conf->url.style != UNDERLINE_NONE)) { + pixman_color_t url_color = color_hex_to_pixman( + term->conf->colors_dark.use_custom.url ? term->conf->colors_dark.url + : term->colors.table[3], + gamma_correct); + + draw_styled_underline(term, pix, font, &url_color, term->conf->url.style, x, + y, cell_cols); + } draw_cursor: - if (has_cursor && (term->cursor_style != CURSOR_BLOCK || !term->kbd_focus)) { - const pixman_color_t bg_without_alpha = color_hex_to_pixman(_bg, gamma_correct); - draw_cursor(term, cell, font, pix, &fg, &bg_without_alpha, x, y, cell_cols); - } + if (has_cursor && (term->cursor_style != CURSOR_BLOCK || !term->kbd_focus)) { + const pixman_color_t bg_without_alpha = + color_hex_to_pixman(_bg, gamma_correct); + draw_cursor(term, cell, font, pix, &fg, &bg_without_alpha, x, y, cell_cols); + } - pixman_image_set_clip_region32(pix, NULL); - return cell_cols; + pixman_image_set_clip_region32(pix, NULL); + return cell_cols; } -static void -render_row(struct terminal *term, pixman_image_t *pix, - pixman_region32_t *damage, struct row *row, - int row_no, int cursor_col) -{ - for (int col = term->cols - 1; col >= 0; col--) - render_cell(term, pix, damage, row, row_no, col, cursor_col == col); +static void render_row(struct terminal *term, pixman_image_t *pix, + pixman_region32_t *damage, struct row *row, int row_no, + int cursor_col) { + for (int col = term->cols - 1; col >= 0; col--) + render_cell(term, pix, damage, row, row_no, col, cursor_col == col); } -static void -render_urgency(struct terminal *term, struct buffer *buf) -{ - uint32_t red = term->colors.table[1]; - pixman_color_t bg = color_hex_to_pixman( - red, wayl_do_linear_blending(term->wl, term->conf)); +static void render_urgency(struct terminal *term, struct buffer *buf) { + uint32_t red = term->colors.table[1]; + pixman_color_t bg = + color_hex_to_pixman(red, wayl_do_linear_blending(term->wl, term->conf)); - int width = min(min(term->margins.left, term->margins.right), - min(term->margins.top, term->margins.bottom)); + int width = min(min(term->margins.left, term->margins.right), + min(term->margins.top, term->margins.bottom)); - pixman_image_fill_rectangles( - PIXMAN_OP_SRC, buf->pix[0], &bg, 4, - (pixman_rectangle16_t[]){ - /* Top */ - {0, 0, term->width, width}, + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &bg, 4, + (pixman_rectangle16_t[]){ + /* Top */ + {0, 0, term->width, width}, - /* Bottom */ - {0, term->height - width, term->width, width}, + /* Bottom */ + {0, term->height - width, term->width, width}, - /* Left */ - {0, width, width, term->height - 2 * width}, + /* Left */ + {0, width, width, term->height - 2 * width}, - /* Right */ - {term->width - width, width, width, term->height - 2 * width}, - }); + /* Right */ + {term->width - width, width, width, term->height - 2 * width}, + }); } -static void -render_margin(struct terminal *term, struct buffer *buf, - int start_line, int end_line, bool apply_damage) -{ - /* Fill area outside the cell grid with the default background color */ - const int rmargin = term->width - term->margins.right; - const int bmargin = term->height - term->margins.bottom; - const int line_count = end_line - start_line; +static void render_margin(struct terminal *term, struct buffer *buf, + int start_line, int end_line, bool apply_damage) { + /* Fill area outside the cell grid with the default background color */ + const int rmargin = term->width - term->margins.right; + const int bmargin = term->height - term->margins.bottom; + const int line_count = end_line - start_line; - const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); - const uint32_t _bg = !term->reverse ? term->colors.bg : term->colors.fg; - uint16_t alpha = term->colors.alpha; + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + const uint32_t _bg = !term->reverse ? term->colors.bg : term->colors.fg; + uint16_t alpha = term->colors.alpha; - if (term->window->is_fullscreen) { - /* Disable alpha in fullscreen - see render_cell() for details */ - alpha = 0xffff; - } + if (term->window->is_fullscreen) { + /* Disable alpha in fullscreen - see render_cell() for details */ + alpha = 0xffff; + } - pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); + pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); - pixman_image_fill_rectangles( - PIXMAN_OP_SRC, buf->pix[0], &bg, 4, - (pixman_rectangle16_t[]){ - /* Top */ - {0, 0, term->width, term->margins.top}, + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &bg, 4, + (pixman_rectangle16_t[]){ + /* Top */ + {0, 0, term->width, term->margins.top}, - /* Bottom */ - {0, bmargin, term->width, term->margins.bottom}, + /* Bottom */ + {0, bmargin, term->width, term->margins.bottom}, - /* Left */ - {0, term->margins.top + start_line * term->cell_height, - term->margins.left, line_count * term->cell_height}, + /* Left */ + {0, term->margins.top + start_line * term->cell_height, + term->margins.left, line_count * term->cell_height}, - /* Right */ - {rmargin, term->margins.top + start_line * term->cell_height, - term->margins.right, line_count * term->cell_height}, - }); + /* Right */ + {rmargin, term->margins.top + start_line * term->cell_height, + term->margins.right, line_count * term->cell_height}, + }); - if (term->render.urgency) - render_urgency(term, buf); + if (term->render.urgency) + render_urgency(term, buf); - /* Ensure the updated regions are copied to the next frame's - * buffer when we're double buffering */ - pixman_region32_union_rect( - &buf->dirty[0], &buf->dirty[0], 0, 0, term->width, term->margins.top); - pixman_region32_union_rect( - &buf->dirty[0], &buf->dirty[0], 0, bmargin, term->width, term->margins.bottom); - pixman_region32_union_rect( - &buf->dirty[0], &buf->dirty[0], 0, 0, term->margins.left, term->height); - pixman_region32_union_rect( - &buf->dirty[0], &buf->dirty[0], - rmargin, 0, term->margins.right, term->height); + /* Ensure the updated regions are copied to the next frame's + * buffer when we're double buffering */ + pixman_region32_union_rect(&buf->dirty[0], &buf->dirty[0], 0, 0, term->width, + term->margins.top); + pixman_region32_union_rect(&buf->dirty[0], &buf->dirty[0], 0, bmargin, + term->width, term->margins.bottom); + pixman_region32_union_rect(&buf->dirty[0], &buf->dirty[0], 0, 0, + term->margins.left, term->height); + pixman_region32_union_rect(&buf->dirty[0], &buf->dirty[0], rmargin, 0, + term->margins.right, term->height); - if (apply_damage) { - /* Top */ - wl_surface_damage_buffer( - term->window->surface.surf, 0, 0, term->width, term->margins.top); + if (apply_damage) { + /* Top */ + wl_surface_damage_buffer(term->window->surface.surf, 0, 0, term->width, + term->margins.top); - /* Bottom */ - wl_surface_damage_buffer( - term->window->surface.surf, 0, bmargin, term->width, term->margins.bottom); + /* Bottom */ + wl_surface_damage_buffer(term->window->surface.surf, 0, bmargin, + term->width, term->margins.bottom); - /* Left */ - wl_surface_damage_buffer( - term->window->surface.surf, - 0, term->margins.top + start_line * term->cell_height, - term->margins.left, line_count * term->cell_height); + /* Left */ + wl_surface_damage_buffer(term->window->surface.surf, 0, + term->margins.top + start_line * term->cell_height, + term->margins.left, + line_count * term->cell_height); - /* Right */ - wl_surface_damage_buffer( - term->window->surface.surf, - rmargin, term->margins.top + start_line * term->cell_height, - term->margins.right, line_count * term->cell_height); - } + /* Right */ + wl_surface_damage_buffer(term->window->surface.surf, rmargin, + term->margins.top + start_line * term->cell_height, + term->margins.right, + line_count * term->cell_height); + } } -static void -grid_render_scroll(struct terminal *term, struct buffer *buf, - const struct damage *dmg) -{ - LOG_DBG( - "damage: SCROLL: %d-%d by %d lines", - dmg->region.start, dmg->region.end, dmg->lines); +static void grid_render_scroll(struct terminal *term, struct buffer *buf, + const struct damage *dmg) { + LOG_DBG("damage: SCROLL: %d-%d by %d lines", dmg->region.start, + dmg->region.end, dmg->lines); - const int region_size = dmg->region.end - dmg->region.start; + const int region_size = dmg->region.end - dmg->region.start; - if (dmg->lines >= region_size) { - /* The entire scroll region will be scrolled out (i.e. replaced) */ - return; - } + if (dmg->lines >= region_size) { + /* The entire scroll region will be scrolled out (i.e. replaced) */ + return; + } - const int height = (region_size - dmg->lines) * term->cell_height; - xassert(height > 0); + const int height = (region_size - dmg->lines) * term->cell_height; + xassert(height > 0); #if TIME_SCROLL_DAMAGE - struct timespec start_time; - clock_gettime(CLOCK_MONOTONIC, &start_time); + struct timespec start_time; + clock_gettime(CLOCK_MONOTONIC, &start_time); #endif - int dst_y = term->margins.top + (dmg->region.start + 0) * term->cell_height; - int src_y = term->margins.top + (dmg->region.start + dmg->lines) * term->cell_height; + int dst_y = term->margins.top + (dmg->region.start + 0) * term->cell_height; + int src_y = + term->margins.top + (dmg->region.start + dmg->lines) * term->cell_height; - /* - * SHM scrolling can be *much* faster, but it depends on how many - * lines we're scrolling, and how much repairing we need to do. - * - * In short, scrolling a *large* number of rows is faster with a - * memmove, while scrolling a *small* number of lines is faster - * with SHM scrolling. - * - * However, since we need to restore the scrolling regions when - * SHM scrolling, we also need to take this into account. - * - * Finally, we also have to restore the window margins, and this - * is a *huge* performance hit when scrolling a large number of - * lines (in addition to the sloweness of SHM scrolling as - * method). - * - * So, we need to figure out when to SHM scroll, and when to - * memmove. - * - * For now, assume that the both methods perform roughly the same, - * given an equal number of bytes to move/allocate, and use the - * method that results in the least amount of bytes to touch. - * - * Since number of lines directly translates to bytes, we can - * simply count lines. - * - * SHM scrolling needs to first "move" (punch hole + allocate) - * dmg->lines number of lines, and then we need to restore - * the bottom scroll region. - * - * If the total number of lines is less than half the screen - use - * SHM. Otherwise use memmove. - */ - bool try_shm_scroll = - shm_can_scroll(buf) && ( - dmg->lines + - dmg->region.start + - (term->rows - dmg->region.end)) < term->rows / 2; + /* + * SHM scrolling can be *much* faster, but it depends on how many + * lines we're scrolling, and how much repairing we need to do. + * + * In short, scrolling a *large* number of rows is faster with a + * memmove, while scrolling a *small* number of lines is faster + * with SHM scrolling. + * + * However, since we need to restore the scrolling regions when + * SHM scrolling, we also need to take this into account. + * + * Finally, we also have to restore the window margins, and this + * is a *huge* performance hit when scrolling a large number of + * lines (in addition to the sloweness of SHM scrolling as + * method). + * + * So, we need to figure out when to SHM scroll, and when to + * memmove. + * + * For now, assume that the both methods perform roughly the same, + * given an equal number of bytes to move/allocate, and use the + * method that results in the least amount of bytes to touch. + * + * Since number of lines directly translates to bytes, we can + * simply count lines. + * + * SHM scrolling needs to first "move" (punch hole + allocate) + * dmg->lines number of lines, and then we need to restore + * the bottom scroll region. + * + * If the total number of lines is less than half the screen - use + * SHM. Otherwise use memmove. + */ + bool try_shm_scroll = + shm_can_scroll(buf) && (dmg->lines + dmg->region.start + + (term->rows - dmg->region.end)) < term->rows / 2; - bool did_shm_scroll = false; + bool did_shm_scroll = false; - //try_shm_scroll = false; - //try_shm_scroll = true; + // try_shm_scroll = false; + // try_shm_scroll = true; - if (try_shm_scroll) { - did_shm_scroll = shm_scroll( - buf, dmg->lines * term->cell_height, - term->margins.top, dmg->region.start * term->cell_height, - term->margins.bottom, (term->rows - dmg->region.end) * term->cell_height); - } + if (try_shm_scroll) { + did_shm_scroll = + shm_scroll(buf, dmg->lines * term->cell_height, term->margins.top, + dmg->region.start * term->cell_height, term->margins.bottom, + (term->rows - dmg->region.end) * term->cell_height); + } - if (did_shm_scroll) { - /* Restore margins */ - render_margin( - term, buf, dmg->region.end - dmg->lines, term->rows, false); + if (did_shm_scroll) { + /* Restore margins */ + render_margin(term, buf, dmg->region.end - dmg->lines, term->rows, false); + } else { + /* Fallback for when we either cannot do SHM scrolling, or it failed */ + uint8_t *raw = buf->data; + memmove(raw + dst_y * buf->stride, raw + src_y * buf->stride, + height * buf->stride); + } + +#if TIME_SCROLL_DAMAGE + struct timespec end_time; + clock_gettime(CLOCK_MONOTONIC, &end_time); + + struct timespec memmove_time; + timespec_sub(&end_time, &start_time, &memmove_time); + LOG_INFO("scrolled %dKB (%d lines) using %s in %lds %ldns", + height * buf->stride / 1024, dmg->lines, + did_shm_scroll ? "SHM" + : try_shm_scroll ? "memmove (SHM failed)" + : "memmove", + (long)memmove_time.tv_sec, memmove_time.tv_nsec); +#endif + + wl_surface_damage_buffer( + term->window->surface.surf, term->margins.left, dst_y, + term->width - term->margins.left - term->margins.right, height); + + /* + * TODO: remove this if re-enabling scroll damage when re-applying + * last frame's damage (see reapply_old_damage() + */ + pixman_region32_union_rect(&buf->dirty[0], &buf->dirty[0], 0, dst_y, + buf->width, height); +} + +static void grid_render_scroll_reverse(struct terminal *term, + struct buffer *buf, + const struct damage *dmg) { + LOG_DBG("damage: SCROLL REVERSE: %d-%d by %d lines", dmg->region.start, + dmg->region.end, dmg->lines); + + const int region_size = dmg->region.end - dmg->region.start; + + if (dmg->lines >= region_size) { + /* The entire scroll region will be scrolled out (i.e. replaced) */ + return; + } + + const int height = (region_size - dmg->lines) * term->cell_height; + xassert(height > 0); + +#if TIME_SCROLL_DAMAGE + struct timespec start_time; + clock_gettime(CLOCK_MONOTONIC, &start_time); +#endif + + int src_y = term->margins.top + (dmg->region.start + 0) * term->cell_height; + int dst_y = + term->margins.top + (dmg->region.start + dmg->lines) * term->cell_height; + + bool try_shm_scroll = + shm_can_scroll(buf) && (dmg->lines + dmg->region.start + + (term->rows - dmg->region.end)) < term->rows / 2; + + bool did_shm_scroll = false; + + if (try_shm_scroll) { + did_shm_scroll = + shm_scroll(buf, -dmg->lines * term->cell_height, term->margins.top, + dmg->region.start * term->cell_height, term->margins.bottom, + (term->rows - dmg->region.end) * term->cell_height); + } + + if (did_shm_scroll) { + /* Restore margins */ + render_margin(term, buf, dmg->region.start, dmg->region.start + dmg->lines, + false); + } else { + /* Fallback for when we either cannot do SHM scrolling, or it failed */ + uint8_t *raw = buf->data; + memmove(raw + dst_y * buf->stride, raw + src_y * buf->stride, + height * buf->stride); + } + +#if TIME_SCROLL_DAMAGE + struct timespec end_time; + clock_gettime(CLOCK_MONOTONIC, &end_time); + + struct timespec memmove_time; + timespec_sub(&end_time, &start_time, &memmove_time); + LOG_INFO("scrolled REVERSE %dKB (%d lines) using %s in %lds %ldns", + height * buf->stride / 1024, dmg->lines, + did_shm_scroll ? "SHM" + : try_shm_scroll ? "memmove (SHM failed)" + : "memmove", + (long)memmove_time.tv_sec, memmove_time.tv_nsec); +#endif + + wl_surface_damage_buffer( + term->window->surface.surf, term->margins.left, dst_y, + term->width - term->margins.left - term->margins.right, height); + + /* + * TODO: remove this if re-enabling scroll damage when re-applying + * last frame's damage (see reapply_old_damage() + */ + pixman_region32_union_rect(&buf->dirty[0], &buf->dirty[0], 0, dst_y, + buf->width, height); +} + +static void render_sixel_chunk(struct terminal *term, pixman_image_t *pix, + pixman_region32_t *damage, + const struct sixel *sixel, int term_start_row, + int img_start_row, int count) { + /* Translate row/column to x/y pixel values */ + const int x = term->margins.left + sixel->pos.col * term->cell_width; + const int y = term->margins.top + term_start_row * term->cell_height; + + /* Width/height, in pixels - and don't touch the window margins */ + const int width = + max(0, min(sixel->width, term->width - x - term->margins.right)); + const int height = + max(0, min(min(count * term->cell_height, /* 'count' number of rows */ + sixel->height - + img_start_row * + term->cell_height), /* What remains of the sixel */ + term->height - y - term->margins.bottom)); + + /* Verify we're not stepping outside the grid */ + xassert(x >= term->margins.left); + xassert(y >= term->margins.top); + xassert(width == 0 || x + width <= term->width - term->margins.right); + xassert(height == 0 || y + height <= term->height - term->margins.bottom); + + // LOG_DBG("sixel chunk: %dx%d %dx%d", x, y, width, height); + + pixman_image_composite32( + sixel->opaque ? PIXMAN_OP_SRC : PIXMAN_OP_OVER, sixel->pix, NULL, pix, 0, + img_start_row * term->cell_height, 0, 0, x, y, width, height); + + if (damage != NULL) + pixman_region32_union_rect(damage, damage, x, y, width, height); +} + +static void render_sixel(struct terminal *term, pixman_image_t *pix, + pixman_region32_t *damage, const struct coord *cursor, + const struct sixel *sixel) { + xassert(sixel->pix != NULL); + xassert(sixel->width >= 0); + xassert(sixel->height >= 0); + + const int view_end = + (term->grid->view + term->rows - 1) & (term->grid->num_rows - 1); + const bool last_row_needs_erase = sixel->height % term->cell_height != 0; + const bool last_col_needs_erase = sixel->width % term->cell_width != 0; + + int chunk_img_start = -1; /* Image-relative start row of chunk */ + int chunk_term_start = -1; /* Viewport relative start row of chunk */ + int chunk_row_count = 0; /* Number of rows to emit */ + +#define maybe_emit_sixel_chunk_then_reset() \ + if (chunk_row_count != 0) { \ + render_sixel_chunk(term, pix, damage, sixel, chunk_term_start, \ + chunk_img_start, chunk_row_count); \ + chunk_term_start = chunk_img_start = -1; \ + chunk_row_count = 0; \ + } + + /* + * Iterate all sixel rows: + * + * - ignore rows that aren't visible on-screen + * - ignore rows that aren't dirty (they have already been rendered) + * - chunk consecutive dirty rows into a 'chunk' + * - emit (render) chunk as soon as a row isn't visible, or is clean + * - emit final chunk after we've iterated all rows + * + * The purpose of this is to reduce the amount of pixels that + * needs to be composited and marked as damaged for the + * compositor. + * + * Since we do CPU based composition, rendering is a slow and + * heavy task for foot, and thus it is important to not re-render + * things unnecessarily. + */ + + for (int _abs_row_no = sixel->pos.row; + _abs_row_no < sixel->pos.row + sixel->rows; _abs_row_no++) { + const int abs_row_no = _abs_row_no & (term->grid->num_rows - 1); + const int term_row_no = + (abs_row_no - term->grid->view + term->grid->num_rows) & + (term->grid->num_rows - 1); + + /* Check if row is in the visible viewport */ + if (view_end >= term->grid->view) { + /* Not wrapped */ + if (!(abs_row_no >= term->grid->view && abs_row_no <= view_end)) { + /* Not visible */ + maybe_emit_sixel_chunk_then_reset(); + continue; + } } else { - /* Fallback for when we either cannot do SHM scrolling, or it failed */ - uint8_t *raw = buf->data; - memmove(raw + dst_y * buf->stride, - raw + src_y * buf->stride, - height * buf->stride); + /* Wrapped */ + if (!(abs_row_no >= term->grid->view || abs_row_no <= view_end)) { + /* Not visible */ + maybe_emit_sixel_chunk_then_reset(); + continue; + } } -#if TIME_SCROLL_DAMAGE - struct timespec end_time; - clock_gettime(CLOCK_MONOTONIC, &end_time); + /* Is the row dirty? */ + struct row *row = term->grid->rows[abs_row_no]; + xassert(row != NULL); /* Should be visible */ - struct timespec memmove_time; - timespec_sub(&end_time, &start_time, &memmove_time); - LOG_INFO("scrolled %dKB (%d lines) using %s in %lds %ldns", - height * buf->stride / 1024, dmg->lines, - did_shm_scroll ? "SHM" : try_shm_scroll ? "memmove (SHM failed)" : "memmove", - (long)memmove_time.tv_sec, memmove_time.tv_nsec); -#endif + if (!row->dirty) { + maybe_emit_sixel_chunk_then_reset(); + continue; + } - wl_surface_damage_buffer( - term->window->surface.surf, term->margins.left, dst_y, - term->width - term->margins.left - term->margins.right, height); + int cursor_col = cursor->row == term_row_no ? cursor->col : -1; /* - * TODO: remove this if re-enabling scroll damage when re-applying - * last frame's damage (see reapply_old_damage() + * If image contains transparent parts, render all (dirty) + * cells beneath it. + * + * If image is opaque, loop cells and set their 'clean' bit, + * to prevent the grid rendered from overwriting the sixel + * + * If the last sixel row only partially covers the cell row, + * 'erase' the cell by rendering them. + * + * In all cases, do *not* clear the 'dirty' bit on the row, to + * ensure the regular renderer includes them in the damage + * rect. */ - pixman_region32_union_rect( - &buf->dirty[0], &buf->dirty[0], 0, dst_y, buf->width, height); -} - -static void -grid_render_scroll_reverse(struct terminal *term, struct buffer *buf, - const struct damage *dmg) -{ - LOG_DBG( - "damage: SCROLL REVERSE: %d-%d by %d lines", - dmg->region.start, dmg->region.end, dmg->lines); - - const int region_size = dmg->region.end - dmg->region.start; - - if (dmg->lines >= region_size) { - /* The entire scroll region will be scrolled out (i.e. replaced) */ - return; - } - - const int height = (region_size - dmg->lines) * term->cell_height; - xassert(height > 0); - -#if TIME_SCROLL_DAMAGE - struct timespec start_time; - clock_gettime(CLOCK_MONOTONIC, &start_time); -#endif - - int src_y = term->margins.top + (dmg->region.start + 0) * term->cell_height; - int dst_y = term->margins.top + (dmg->region.start + dmg->lines) * term->cell_height; - - bool try_shm_scroll = - shm_can_scroll(buf) && ( - dmg->lines + - dmg->region.start + - (term->rows - dmg->region.end)) < term->rows / 2; - - bool did_shm_scroll = false; - - if (try_shm_scroll) { - did_shm_scroll = shm_scroll( - buf, -dmg->lines * term->cell_height, - term->margins.top, dmg->region.start * term->cell_height, - term->margins.bottom, (term->rows - dmg->region.end) * term->cell_height); - } - - if (did_shm_scroll) { - /* Restore margins */ - render_margin( - term, buf, dmg->region.start, dmg->region.start + dmg->lines, false); + if (!sixel->opaque) { + /* TODO: multithreading */ + render_row(term, pix, damage, row, term_row_no, cursor_col); } else { - /* Fallback for when we either cannot do SHM scrolling, or it failed */ - uint8_t *raw = buf->data; - memmove(raw + dst_y * buf->stride, - raw + src_y * buf->stride, - height * buf->stride); + for (int col = sixel->pos.col; + col < min(sixel->pos.col + sixel->cols, term->cols); col++) { + struct cell *cell = &row->cells[col]; + + if (!cell->attrs.clean) { + bool last_row = abs_row_no == sixel->pos.row + sixel->rows - 1; + bool last_col = col == sixel->pos.col + sixel->cols - 1; + + if ((last_row_needs_erase && last_row) || + (last_col_needs_erase && last_col)) { + render_cell(term, pix, damage, row, term_row_no, col, + cursor_col == col); + } else { + cell->attrs.clean = 1; + cell->attrs.confined = 1; + } + } + } } -#if TIME_SCROLL_DAMAGE - struct timespec end_time; - clock_gettime(CLOCK_MONOTONIC, &end_time); + if (chunk_term_start == -1) { + xassert(chunk_img_start == -1); + chunk_term_start = term_row_no; + chunk_img_start = _abs_row_no - sixel->pos.row; + chunk_row_count = 1; + } else + chunk_row_count++; + } - struct timespec memmove_time; - timespec_sub(&end_time, &start_time, &memmove_time); - LOG_INFO("scrolled REVERSE %dKB (%d lines) using %s in %lds %ldns", - height * buf->stride / 1024, dmg->lines, - did_shm_scroll ? "SHM" : try_shm_scroll ? "memmove (SHM failed)" : "memmove", - (long)memmove_time.tv_sec, memmove_time.tv_nsec); -#endif - - wl_surface_damage_buffer( - term->window->surface.surf, term->margins.left, dst_y, - term->width - term->margins.left - term->margins.right, height); - - /* - * TODO: remove this if re-enabling scroll damage when re-applying - * last frame's damage (see reapply_old_damage() - */ - pixman_region32_union_rect( - &buf->dirty[0], &buf->dirty[0], 0, dst_y, buf->width, height); -} - -static void -render_sixel_chunk(struct terminal *term, pixman_image_t *pix, - pixman_region32_t *damage, const struct sixel *sixel, - int term_start_row, int img_start_row, int count) -{ - /* Translate row/column to x/y pixel values */ - const int x = term->margins.left + sixel->pos.col * term->cell_width; - const int y = term->margins.top + term_start_row * term->cell_height; - - /* Width/height, in pixels - and don't touch the window margins */ - const int width = max( - 0, - min(sixel->width, - term->width - x - term->margins.right)); - const int height = max( - 0, - min( - min(count * term->cell_height, /* 'count' number of rows */ - sixel->height - img_start_row * term->cell_height), /* What remains of the sixel */ - term->height - y - term->margins.bottom)); - - /* Verify we're not stepping outside the grid */ - xassert(x >= term->margins.left); - xassert(y >= term->margins.top); - xassert(width == 0 || x + width <= term->width - term->margins.right); - xassert(height == 0 || y + height <= term->height - term->margins.bottom); - - //LOG_DBG("sixel chunk: %dx%d %dx%d", x, y, width, height); - - pixman_image_composite32( - sixel->opaque ? PIXMAN_OP_SRC : PIXMAN_OP_OVER, - sixel->pix, - NULL, - pix, - 0, img_start_row * term->cell_height, - 0, 0, - x, y, - width, height); - - if (damage != NULL) - pixman_region32_union_rect(damage, damage, x, y, width, height); -} - -static void -render_sixel(struct terminal *term, pixman_image_t *pix, - pixman_region32_t *damage, const struct coord *cursor, - const struct sixel *sixel) -{ - xassert(sixel->pix != NULL); - xassert(sixel->width >= 0); - xassert(sixel->height >= 0); - - const int view_end = (term->grid->view + term->rows - 1) & (term->grid->num_rows - 1); - const bool last_row_needs_erase = sixel->height % term->cell_height != 0; - const bool last_col_needs_erase = sixel->width % term->cell_width != 0; - - int chunk_img_start = -1; /* Image-relative start row of chunk */ - int chunk_term_start = -1; /* Viewport relative start row of chunk */ - int chunk_row_count = 0; /* Number of rows to emit */ - -#define maybe_emit_sixel_chunk_then_reset() \ - if (chunk_row_count != 0) { \ - render_sixel_chunk( \ - term, pix, damage, sixel, \ - chunk_term_start, chunk_img_start, chunk_row_count); \ - chunk_term_start = chunk_img_start = -1; \ - chunk_row_count = 0; \ - } - - /* - * Iterate all sixel rows: - * - * - ignore rows that aren't visible on-screen - * - ignore rows that aren't dirty (they have already been rendered) - * - chunk consecutive dirty rows into a 'chunk' - * - emit (render) chunk as soon as a row isn't visible, or is clean - * - emit final chunk after we've iterated all rows - * - * The purpose of this is to reduce the amount of pixels that - * needs to be composited and marked as damaged for the - * compositor. - * - * Since we do CPU based composition, rendering is a slow and - * heavy task for foot, and thus it is important to not re-render - * things unnecessarily. - */ - - for (int _abs_row_no = sixel->pos.row; - _abs_row_no < sixel->pos.row + sixel->rows; - _abs_row_no++) - { - const int abs_row_no = _abs_row_no & (term->grid->num_rows - 1); - const int term_row_no = - (abs_row_no - term->grid->view + term->grid->num_rows) & - (term->grid->num_rows - 1); - - /* Check if row is in the visible viewport */ - if (view_end >= term->grid->view) { - /* Not wrapped */ - if (!(abs_row_no >= term->grid->view && abs_row_no <= view_end)) { - /* Not visible */ - maybe_emit_sixel_chunk_then_reset(); - continue; - } - } else { - /* Wrapped */ - if (!(abs_row_no >= term->grid->view || abs_row_no <= view_end)) { - /* Not visible */ - maybe_emit_sixel_chunk_then_reset(); - continue; - } - } - - /* Is the row dirty? */ - struct row *row = term->grid->rows[abs_row_no]; - xassert(row != NULL); /* Should be visible */ - - if (!row->dirty) { - maybe_emit_sixel_chunk_then_reset(); - continue; - } - - int cursor_col = cursor->row == term_row_no ? cursor->col : -1; - - /* - * If image contains transparent parts, render all (dirty) - * cells beneath it. - * - * If image is opaque, loop cells and set their 'clean' bit, - * to prevent the grid rendered from overwriting the sixel - * - * If the last sixel row only partially covers the cell row, - * 'erase' the cell by rendering them. - * - * In all cases, do *not* clear the 'dirty' bit on the row, to - * ensure the regular renderer includes them in the damage - * rect. - */ - if (!sixel->opaque) { - /* TODO: multithreading */ - render_row(term, pix, damage, row, term_row_no, cursor_col); - } else { - for (int col = sixel->pos.col; - col < min(sixel->pos.col + sixel->cols, term->cols); - col++) - { - struct cell *cell = &row->cells[col]; - - if (!cell->attrs.clean) { - bool last_row = abs_row_no == sixel->pos.row + sixel->rows - 1; - bool last_col = col == sixel->pos.col + sixel->cols - 1; - - if ((last_row_needs_erase && last_row) || - (last_col_needs_erase && last_col)) - { - render_cell(term, pix, damage, row, term_row_no, col, cursor_col == col); - } else { - cell->attrs.clean = 1; - cell->attrs.confined = 1; - } - } - } - } - - if (chunk_term_start == -1) { - xassert(chunk_img_start == -1); - chunk_term_start = term_row_no; - chunk_img_start = _abs_row_no - sixel->pos.row; - chunk_row_count = 1; - } else - chunk_row_count++; - } - - maybe_emit_sixel_chunk_then_reset(); + maybe_emit_sixel_chunk_then_reset(); #undef maybe_emit_sixel_chunk_then_reset } -static void -render_sixel_images(struct terminal *term, pixman_image_t *pix, - pixman_region32_t *damage, const struct coord *cursor) -{ - if (likely(tll_length(term->grid->sixel_images)) == 0) - return; +static void render_sixel_images(struct terminal *term, pixman_image_t *pix, + pixman_region32_t *damage, + const struct coord *cursor) { + if (likely(tll_length(term->grid->sixel_images)) == 0) + return; - const int scrollback_end - = (term->grid->offset + term->rows) & (term->grid->num_rows - 1); + const int scrollback_end = + (term->grid->offset + term->rows) & (term->grid->num_rows - 1); - const int view_start - = (term->grid->view - - scrollback_end - + term->grid->num_rows) & (term->grid->num_rows - 1); + const int view_start = + (term->grid->view - scrollback_end + term->grid->num_rows) & + (term->grid->num_rows - 1); - const int view_end = view_start + term->rows - 1; + const int view_end = view_start + term->rows - 1; - //LOG_DBG("SIXELS: %zu images, view=%d-%d", - // tll_length(term->grid->sixel_images), view_start, view_end); + // LOG_DBG("SIXELS: %zu images, view=%d-%d", + // tll_length(term->grid->sixel_images), view_start, view_end); - tll_foreach(term->grid->sixel_images, it) { - const struct sixel *six = &it->item; - const int start - = (six->pos.row - - scrollback_end - + term->grid->num_rows) & (term->grid->num_rows - 1); - const int end = start + six->rows - 1; + tll_foreach(term->grid->sixel_images, it) { + const struct sixel *six = &it->item; + const int start = (six->pos.row - scrollback_end + term->grid->num_rows) & + (term->grid->num_rows - 1); + const int end = start + six->rows - 1; - //LOG_DBG(" sixel: %d-%d", start, end); - if (start > view_end) { - /* Sixel starts after view ends, no need to try to render it */ - continue; - } else if (end < view_start) { - /* Image ends before view starts. Since the image list is - * sorted, we can safely stop here */ - break; - } - - sixel_sync_cache(term, &it->item); - render_sixel(term, pix, damage, cursor, &it->item); + // LOG_DBG(" sixel: %d-%d", start, end); + if (start > view_end) { + /* Sixel starts after view ends, no need to try to render it */ + continue; + } else if (end < view_start) { + /* Image ends before view starts. Since the image list is + * sorted, we can safely stop here */ + break; } + + sixel_sync_cache(term, &it->item); + render_sixel(term, pix, damage, cursor, &it->item); + } } #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED -static void -render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, - struct buffer *buf) -{ - if (likely(seat->ime.preedit.cells == NULL)) - return; - - if (unlikely(term->is_searching)) - return; - - const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); - - /* Adjust cursor position to viewport */ - struct coord cursor; - cursor = term->grid->cursor.point; - cursor.row += term->grid->offset; - cursor.row -= term->grid->view; - cursor.row &= term->grid->num_rows - 1; - - if (cursor.row < 0 || cursor.row >= term->rows) - return; - - int cells_needed = seat->ime.preedit.count; - - if (seat->ime.preedit.cursor.start == cells_needed && - seat->ime.preedit.cursor.end == cells_needed) - { - /* Cursor will be drawn *after* the pre-edit string, i.e. in - * the cell *after*. This means we need to copy, and dirty, - * one extra cell from the original grid, or we'll leave - * trailing "cursors" after us if the user deletes text while - * pre-editing */ - cells_needed++; - } - - int row_idx = cursor.row; - int col_idx = cursor.col; - int ime_ofs = 0; /* Offset into pre-edit string to start rendering at */ - - int cells_left = term->cols - cursor.col; - int cells_used = min(cells_needed, term->cols); - - /* Adjust start of pre-edit text to the left if string doesn't fit on row */ - if (cells_left < cells_used) - col_idx -= cells_used - cells_left; - - if (cells_needed > cells_used) { - int start = seat->ime.preedit.cursor.start; - int end = seat->ime.preedit.cursor.end; - - if (start == end) { - /* Ensure *end* of pre-edit string is visible */ - ime_ofs = cells_needed - cells_used; - } else { - /* Ensure the *beginning* of the cursor-area is visible */ - ime_ofs = start; - - /* Display as much as possible of the pre-edit string */ - if (cells_needed - ime_ofs < cells_used) - ime_ofs = cells_needed - cells_used; - } - - /* Make sure we don't start in the middle of a character */ - while (ime_ofs < cells_needed && - seat->ime.preedit.cells[ime_ofs].wc >= CELL_SPACER) - { - ime_ofs++; - } - } - - xassert(col_idx >= 0); - xassert(col_idx < term->cols); - - struct row *row = grid_row_in_view(term->grid, row_idx); - - /* Don't start pre-edit text in the middle of a double-width character */ - while (col_idx > 0 && row->cells[col_idx].wc >= CELL_SPACER) { - cells_used++; - col_idx--; - } - - /* - * Copy original content (render_cell() reads cell data directly - * from grid), and mark all cells as dirty. This ensures they are - * re-rendered when the pre-edit text is modified or removed. - */ - struct cell *real_cells = xmalloc(cells_used * sizeof(real_cells[0])); - for (int i = 0; i < cells_used; i++) { - xassert(col_idx + i < term->cols); - real_cells[i] = row->cells[col_idx + i]; - real_cells[i].attrs.clean = 0; - } - row->dirty = true; - - /* Render pre-edit text */ - xassert(seat->ime.preedit.cells[ime_ofs].wc < CELL_SPACER); - for (int i = 0, idx = ime_ofs; idx < seat->ime.preedit.count; i++, idx++) { - const struct cell *cell = &seat->ime.preedit.cells[idx]; - - if (cell->wc >= CELL_SPACER) - continue; - - int width = max(1, c32width(cell->wc)); - if (col_idx + i + width > term->cols) - break; - - row->cells[col_idx + i] = *cell; - render_cell(term, buf->pix[0], NULL, row, row_idx, col_idx + i, false); - } - - int start = seat->ime.preedit.cursor.start - ime_ofs; - int end = seat->ime.preedit.cursor.end - ime_ofs; - - if (!seat->ime.preedit.cursor.hidden) { - const struct cell *start_cell = &seat->ime.preedit.cells[0]; - - pixman_color_t fg = color_hex_to_pixman(term->colors.fg, gamma_correct); - pixman_color_t bg = color_hex_to_pixman(term->colors.bg, gamma_correct); - - pixman_color_t cursor_color, text_color; - cursor_colors_for_cell( - term, start_cell, &fg, &bg, &cursor_color, &text_color, gamma_correct); - - int x = term->margins.left + (col_idx + start) * term->cell_width; - int y = term->margins.top + row_idx * term->cell_height; - - if (end == start) { - /* Bar */ - if (start >= 0) { - struct fcft_font *font = attrs_to_font(term, &start_cell->attrs); - draw_beam_cursor(term, buf->pix[0], font, &cursor_color, x, y); - } - term_ime_set_cursor_rect(term, x, y, 1, term->cell_height); - } - - else if (end > start) { - /* Hollow cursor */ - if (start >= 0 && end <= term->cols) { - int cols = end - start; - draw_hollow_block(term, buf->pix[0], &cursor_color, x, y, cols); - } - - term_ime_set_cursor_rect( - term, x, y, (end - start) * term->cell_width, term->cell_height); - } - } - - /* Restore original content (but do not render) */ - for (int i = 0; i < cells_used; i++) - row->cells[col_idx + i] = real_cells[i]; - free(real_cells); - - const int damage_x = term->margins.left + col_idx * term->cell_width; - const int damage_y = term->margins.top + row_idx * term->cell_height; - const int damage_w = cells_used * term->cell_width; - const int damage_h = term->cell_height; - - wl_surface_damage_buffer( - term->window->surface.surf, - damage_x, damage_y, damage_w, damage_h); -} -#endif - -static void -render_ime_preedit(struct terminal *term, struct buffer *buf) -{ -#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED - tll_foreach(term->wl->seats, it) { - if (it->item.kbd_focus == term) - render_ime_preedit_for_seat(term, &it->item, buf); - } -#endif -} - -static void -render_overlay_single_pixel(struct terminal *term, enum overlay_style style, - pixman_color_t color) -{ - struct wayland *wayl = term->wl; - struct wayl_sub_surface *overlay = &term->window->overlay; - struct wl_buffer *buf = NULL; - - /* - * In an ideal world, we'd only update the surface (i.e. commit - * any changes) if anything has actually changed. - * - * For technical reasons, we can't do that, since we can't - * determine whether the last committed buffer is still valid - * (i.e. does it correspond to the current overlay style, *and* - * does last frame's size match the current size?) - * - * What we _can_ do is use the fact that single-pixel buffers - * don't have a size; you have to use a viewport to "size" them. - * - * This means we can check if the last frame's overlay style is - * the same as the current size. If so, then we *know* that the - * currently attached buffer is valid, and we *don't* have to - * create a new single-pixel buffer. - * - * What we do *not* know if the *size* is still valid. This means - * we do have to do the viewport calls, and a surface commit. - * - * This is still better than *always* creating a new buffer. - */ - - assert(style == OVERLAY_UNICODE_MODE || style == OVERLAY_FLASH); - assert(wayl->single_pixel_manager != NULL); - assert(overlay->surface.viewport != NULL); - - quirk_weston_subsurface_desync_on(overlay->sub); - - if (style != term->render.last_overlay_style) { - buf = wp_single_pixel_buffer_manager_v1_create_u32_rgba_buffer( - wayl->single_pixel_manager, - (double)color.red / 0xffff * 0xffffffff, - (double)color.green / 0xffff * 0xffffffff, - (double)color.blue / 0xffff * 0xffffffff, - (double)color.alpha / 0xffff * 0xffffffff); - - wl_surface_set_buffer_scale(overlay->surface.surf, 1); - wl_surface_attach(overlay->surface.surf, buf, 0, 0); - } - - wp_viewport_set_destination( - overlay->surface.viewport, - roundf(term->width / term->scale), - roundf(term->height / term->scale)); - - wl_subsurface_set_position(overlay->sub, 0, 0); - - wl_surface_damage_buffer( - overlay->surface.surf, 0, 0, term->width, term->height); - - wl_surface_commit(overlay->surface.surf); - quirk_weston_subsurface_desync_off(overlay->sub); - - term->render.last_overlay_style = style; - - if (buf != NULL) { - wl_buffer_destroy(buf); - } -} - -void -render_overlay(struct terminal *term) -{ - struct wayl_sub_surface *overlay = &term->window->overlay; - const bool unicode_mode_active = term->unicode_mode.active; - - const enum overlay_style style = - term->flash.active ? OVERLAY_FLASH : - unicode_mode_active ? OVERLAY_UNICODE_MODE : - OVERLAY_NONE; - - if (likely(style == OVERLAY_NONE)) { - if (term->render.last_overlay_style != OVERLAY_NONE) { - /* Unmap overlay sub-surface */ - wl_surface_attach(overlay->surface.surf, NULL, 0, 0); - wl_surface_commit(overlay->surface.surf); - term->render.last_overlay_style = OVERLAY_NONE; - term->render.last_overlay_buf = NULL; - } - return; - } - - pixman_color_t color; - - switch (style) { - case OVERLAY_SEARCH: - case OVERLAY_UNICODE_MODE: - color = (pixman_color_t){0, 0, 0, 0x7fff}; - break; - - case OVERLAY_FLASH: - color = color_hex_to_pixman_with_alpha( - term->conf->colors_dark.flash, - term->conf->colors_dark.flash_alpha, - wayl_do_linear_blending(term->wl, term->conf)); - break; - - case OVERLAY_NONE: - xassert(false); - break; - } - - const bool single_pixel = - (style == OVERLAY_UNICODE_MODE || style == OVERLAY_FLASH) && - term->wl->single_pixel_manager != NULL && - overlay->surface.viewport != NULL; - - if (single_pixel) { - render_overlay_single_pixel(term, style, color); - return; - } - - struct buffer *buf = shm_get_buffer( - term->render.chains.overlay, term->width, term->height); - pixman_image_set_clip_region32(buf->pix[0], NULL); - - /* Bounding rectangle of damaged areas - for wl_surface_damage_buffer() */ - pixman_box32_t damage_bounds; - - if (style == OVERLAY_SEARCH) { - /* - * When possible, we only update the areas that have *changed* - * since the last frame. That means: - * - * - clearing/erasing cells that are now selected, but weren't - * in the last frame - * - dimming cells that were selected, but aren't anymore - * - * To do this, we save the last frame's selected cells as a - * pixman region. - * - * Then, we calculate the corresponding region for this - * frame's selected cells. - * - * Last frame's region minus this frame's region gives us the - * region that needs to be *dimmed* in this frame - * - * This frame's region minus last frame's region gives us the - * region that needs to be *cleared* in this frame. - * - * Finally, the union of the two "diff" regions above, gives - * us the total region affected by a change, in either way. We - * use this as the bounding box for the - * wl_surface_damage_buffer() call. - */ - pixman_region32_t *see_through = &term->render.last_overlay_clip; - pixman_region32_t old_see_through; - const bool buffer_reuse = - buf == term->render.last_overlay_buf && - style == term->render.last_overlay_style && - buf->age == 0; - - if (!buffer_reuse) { - /* Can't reuse last frame's damage - set to full window, - * to ensure *everything* is updated */ - pixman_region32_init_rect( - &old_see_through, 0, 0, buf->width, buf->height); - } else { - /* Use last frame's saved region */ - pixman_region32_init(&old_see_through); - pixman_region32_copy(&old_see_through, see_through); - } - - pixman_region32_clear(see_through); - - /* Build region consisting of all current search matches */ - struct search_match_iterator iter = search_matches_new_iter(term); - for (struct range match = search_matches_next(&iter); - match.start.row >= 0; - match = search_matches_next(&iter)) - { - int r = match.start.row; - int start_col = match.start.col; - const int end_row = match.end.row; - - while (true) { - const int end_col = - r == end_row ? match.end.col : term->cols - 1; - - int x = term->margins.left + start_col * term->cell_width; - int y = term->margins.top + r * term->cell_height; - int width = (end_col + 1 - start_col) * term->cell_width; - int height = 1 * term->cell_height; - - pixman_region32_union_rect( - see_through, see_through, x, y, width, height); - - if (++r > end_row) - break; - - start_col = 0; - } - } - - /* Areas that need to be cleared: cells that were dimmed in - * the last frame but is now see-through */ - pixman_region32_t new_see_through; - pixman_region32_init(&new_see_through); - - if (buffer_reuse) - pixman_region32_subtract(&new_see_through, see_through, &old_see_through); - else { - /* Buffer content is unknown - explicitly clear *all* - * current see-through areas */ - pixman_region32_copy(&new_see_through, see_through); - } - pixman_image_set_clip_region32(buf->pix[0], &new_see_through); - - /* Areas that need to be dimmed: cells that were cleared in - * the last frame but is not anymore */ - pixman_region32_t new_dimmed; - pixman_region32_init(&new_dimmed); - pixman_region32_subtract(&new_dimmed, &old_see_through, see_through); - pixman_region32_fini(&old_see_through); - - /* Total affected area */ - pixman_region32_t damage; - pixman_region32_init(&damage); - pixman_region32_union(&damage, &new_see_through, &new_dimmed); - damage_bounds = damage.extents; - - /* Clear cells that became selected in this frame. */ - pixman_image_fill_rectangles( - PIXMAN_OP_SRC, buf->pix[0], &(pixman_color_t){0}, 1, - &(pixman_rectangle16_t){0, 0, term->width, term->height}); - - /* Set clip region for the newly dimmed cells. The actual - * paint call is done below */ - pixman_image_set_clip_region32(buf->pix[0], &new_dimmed); - - pixman_region32_fini(&new_see_through); - pixman_region32_fini(&new_dimmed); - pixman_region32_fini(&damage); - } - - else if (buf == term->render.last_overlay_buf && - style == term->render.last_overlay_style) - { - xassert(style == OVERLAY_FLASH || style == OVERLAY_UNICODE_MODE); - shm_did_not_use_buf(buf); - return; +static void render_ime_preedit_for_seat(struct terminal *term, + struct seat *seat, struct buffer *buf) { + if (likely(seat->ime.preedit.cells == NULL)) + return; + + if (unlikely(term->is_searching)) + return; + + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + + /* Adjust cursor position to viewport */ + struct coord cursor; + cursor = term->grid->cursor.point; + cursor.row += term->grid->offset; + cursor.row -= term->grid->view; + cursor.row &= term->grid->num_rows - 1; + + if (cursor.row < 0 || cursor.row >= term->rows) + return; + + int cells_needed = seat->ime.preedit.count; + + if (seat->ime.preedit.cursor.start == cells_needed && + seat->ime.preedit.cursor.end == cells_needed) { + /* Cursor will be drawn *after* the pre-edit string, i.e. in + * the cell *after*. This means we need to copy, and dirty, + * one extra cell from the original grid, or we'll leave + * trailing "cursors" after us if the user deletes text while + * pre-editing */ + cells_needed++; + } + + int row_idx = cursor.row; + int col_idx = cursor.col; + int ime_ofs = 0; /* Offset into pre-edit string to start rendering at */ + + int cells_left = term->cols - cursor.col; + int cells_used = min(cells_needed, term->cols); + + /* Adjust start of pre-edit text to the left if string doesn't fit on row */ + if (cells_left < cells_used) + col_idx -= cells_used - cells_left; + + if (cells_needed > cells_used) { + int start = seat->ime.preedit.cursor.start; + int end = seat->ime.preedit.cursor.end; + + if (start == end) { + /* Ensure *end* of pre-edit string is visible */ + ime_ofs = cells_needed - cells_used; } else { - pixman_image_set_clip_region32(buf->pix[0], NULL); - damage_bounds = (pixman_box32_t){0, 0, buf->width, buf->height}; + /* Ensure the *beginning* of the cursor-area is visible */ + ime_ofs = start; + + /* Display as much as possible of the pre-edit string */ + if (cells_needed - ime_ofs < cells_used) + ime_ofs = cells_needed - cells_used; } + /* Make sure we don't start in the middle of a character */ + while (ime_ofs < cells_needed && + seat->ime.preedit.cells[ime_ofs].wc >= CELL_SPACER) { + ime_ofs++; + } + } + + xassert(col_idx >= 0); + xassert(col_idx < term->cols); + + struct row *row = grid_row_in_view(term->grid, row_idx); + + /* Don't start pre-edit text in the middle of a double-width character */ + while (col_idx > 0 && row->cells[col_idx].wc >= CELL_SPACER) { + cells_used++; + col_idx--; + } + + /* + * Copy original content (render_cell() reads cell data directly + * from grid), and mark all cells as dirty. This ensures they are + * re-rendered when the pre-edit text is modified or removed. + */ + struct cell *real_cells = xmalloc(cells_used * sizeof(real_cells[0])); + for (int i = 0; i < cells_used; i++) { + xassert(col_idx + i < term->cols); + real_cells[i] = row->cells[col_idx + i]; + real_cells[i].attrs.clean = 0; + } + row->dirty = true; + + /* Render pre-edit text */ + xassert(seat->ime.preedit.cells[ime_ofs].wc < CELL_SPACER); + for (int i = 0, idx = ime_ofs; idx < seat->ime.preedit.count; i++, idx++) { + const struct cell *cell = &seat->ime.preedit.cells[idx]; + + if (cell->wc >= CELL_SPACER) + continue; + + int width = max(1, c32width(cell->wc)); + if (col_idx + i + width > term->cols) + break; + + row->cells[col_idx + i] = *cell; + render_cell(term, buf->pix[0], NULL, row, row_idx, col_idx + i, false); + } + + int start = seat->ime.preedit.cursor.start - ime_ofs; + int end = seat->ime.preedit.cursor.end - ime_ofs; + + if (!seat->ime.preedit.cursor.hidden) { + const struct cell *start_cell = &seat->ime.preedit.cells[0]; + + pixman_color_t fg = color_hex_to_pixman(term->colors.fg, gamma_correct); + pixman_color_t bg = color_hex_to_pixman(term->colors.bg, gamma_correct); + + pixman_color_t cursor_color, text_color; + cursor_colors_for_cell(term, start_cell, &fg, &bg, &cursor_color, + &text_color, gamma_correct); + + int x = term->margins.left + (col_idx + start) * term->cell_width; + int y = term->margins.top + row_idx * term->cell_height; + + if (end == start) { + /* Bar */ + if (start >= 0) { + struct fcft_font *font = attrs_to_font(term, &start_cell->attrs); + draw_beam_cursor(term, buf->pix[0], font, &cursor_color, x, y); + } + term_ime_set_cursor_rect(term, x, y, 1, term->cell_height); + } + + else if (end > start) { + /* Hollow cursor */ + if (start >= 0 && end <= term->cols) { + int cols = end - start; + draw_hollow_block(term, buf->pix[0], &cursor_color, x, y, cols); + } + + term_ime_set_cursor_rect(term, x, y, (end - start) * term->cell_width, + term->cell_height); + } + } + + /* Restore original content (but do not render) */ + for (int i = 0; i < cells_used; i++) + row->cells[col_idx + i] = real_cells[i]; + free(real_cells); + + const int damage_x = term->margins.left + col_idx * term->cell_width; + const int damage_y = term->margins.top + row_idx * term->cell_height; + const int damage_w = cells_used * term->cell_width; + const int damage_h = term->cell_height; + + wl_surface_damage_buffer(term->window->surface.surf, damage_x, damage_y, + damage_w, damage_h); +} +#endif + +static void render_ime_preedit(struct terminal *term, struct buffer *buf) { +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + tll_foreach(term->wl->seats, it) { + if (it->item.kbd_focus == term) + render_ime_preedit_for_seat(term, &it->item, buf); + } +#endif +} + +static void render_overlay_single_pixel(struct terminal *term, + enum overlay_style style, + pixman_color_t color) { + struct wayland *wayl = term->wl; + struct wayl_sub_surface *overlay = &term->window->overlay; + struct wl_buffer *buf = NULL; + + /* + * In an ideal world, we'd only update the surface (i.e. commit + * any changes) if anything has actually changed. + * + * For technical reasons, we can't do that, since we can't + * determine whether the last committed buffer is still valid + * (i.e. does it correspond to the current overlay style, *and* + * does last frame's size match the current size?) + * + * What we _can_ do is use the fact that single-pixel buffers + * don't have a size; you have to use a viewport to "size" them. + * + * This means we can check if the last frame's overlay style is + * the same as the current size. If so, then we *know* that the + * currently attached buffer is valid, and we *don't* have to + * create a new single-pixel buffer. + * + * What we do *not* know if the *size* is still valid. This means + * we do have to do the viewport calls, and a surface commit. + * + * This is still better than *always* creating a new buffer. + */ + + assert(style == OVERLAY_UNICODE_MODE || style == OVERLAY_FLASH); + assert(wayl->single_pixel_manager != NULL); + assert(overlay->surface.viewport != NULL); + + quirk_weston_subsurface_desync_on(overlay->sub); + + if (style != term->render.last_overlay_style) { + buf = wp_single_pixel_buffer_manager_v1_create_u32_rgba_buffer( + wayl->single_pixel_manager, (double)color.red / 0xffff * 0xffffffff, + (double)color.green / 0xffff * 0xffffffff, + (double)color.blue / 0xffff * 0xffffffff, + (double)color.alpha / 0xffff * 0xffffffff); + + wl_surface_set_buffer_scale(overlay->surface.surf, 1); + wl_surface_attach(overlay->surface.surf, buf, 0, 0); + } + + wp_viewport_set_destination(overlay->surface.viewport, + roundf(term->width / term->scale), + roundf(term->height / term->scale)); + + wl_subsurface_set_position(overlay->sub, 0, 0); + + wl_surface_damage_buffer(overlay->surface.surf, 0, 0, term->width, + term->height); + + wl_surface_commit(overlay->surface.surf); + quirk_weston_subsurface_desync_off(overlay->sub); + + term->render.last_overlay_style = style; + + if (buf != NULL) { + wl_buffer_destroy(buf); + } +} + +void render_overlay(struct terminal *term) { + struct wayl_sub_surface *overlay = &term->window->overlay; + const bool unicode_mode_active = term->unicode_mode.active; + + const enum overlay_style style = term->flash.active ? OVERLAY_FLASH + : unicode_mode_active ? OVERLAY_UNICODE_MODE + : OVERLAY_NONE; + + if (likely(style == OVERLAY_NONE)) { + if (term->render.last_overlay_style != OVERLAY_NONE) { + /* Unmap overlay sub-surface */ + wl_surface_attach(overlay->surface.surf, NULL, 0, 0); + wl_surface_commit(overlay->surface.surf); + term->render.last_overlay_style = OVERLAY_NONE; + term->render.last_overlay_buf = NULL; + } + return; + } + + pixman_color_t color; + + switch (style) { + case OVERLAY_SEARCH: + case OVERLAY_UNICODE_MODE: + color = (pixman_color_t){0, 0, 0, 0x7fff}; + break; + + case OVERLAY_FLASH: + color = color_hex_to_pixman_with_alpha( + term->conf->colors_dark.flash, term->conf->colors_dark.flash_alpha, + wayl_do_linear_blending(term->wl, term->conf)); + break; + + case OVERLAY_NONE: + xassert(false); + break; + } + + const bool single_pixel = + (style == OVERLAY_UNICODE_MODE || style == OVERLAY_FLASH) && + term->wl->single_pixel_manager != NULL && + overlay->surface.viewport != NULL; + + if (single_pixel) { + render_overlay_single_pixel(term, style, color); + return; + } + + struct buffer *buf = + shm_get_buffer(term->render.chains.overlay, term->width, term->height); + pixman_image_set_clip_region32(buf->pix[0], NULL); + + /* Bounding rectangle of damaged areas - for wl_surface_damage_buffer() */ + pixman_box32_t damage_bounds; + + if (style == OVERLAY_SEARCH) { + /* + * When possible, we only update the areas that have *changed* + * since the last frame. That means: + * + * - clearing/erasing cells that are now selected, but weren't + * in the last frame + * - dimming cells that were selected, but aren't anymore + * + * To do this, we save the last frame's selected cells as a + * pixman region. + * + * Then, we calculate the corresponding region for this + * frame's selected cells. + * + * Last frame's region minus this frame's region gives us the + * region that needs to be *dimmed* in this frame + * + * This frame's region minus last frame's region gives us the + * region that needs to be *cleared* in this frame. + * + * Finally, the union of the two "diff" regions above, gives + * us the total region affected by a change, in either way. We + * use this as the bounding box for the + * wl_surface_damage_buffer() call. + */ + pixman_region32_t *see_through = &term->render.last_overlay_clip; + pixman_region32_t old_see_through; + const bool buffer_reuse = buf == term->render.last_overlay_buf && + style == term->render.last_overlay_style && + buf->age == 0; + + if (!buffer_reuse) { + /* Can't reuse last frame's damage - set to full window, + * to ensure *everything* is updated */ + pixman_region32_init_rect(&old_see_through, 0, 0, buf->width, + buf->height); + } else { + /* Use last frame's saved region */ + pixman_region32_init(&old_see_through); + pixman_region32_copy(&old_see_through, see_through); + } + + pixman_region32_clear(see_through); + + /* Build region consisting of all current search matches */ + struct search_match_iterator iter = search_matches_new_iter(term); + for (struct range match = search_matches_next(&iter); match.start.row >= 0; + match = search_matches_next(&iter)) { + int r = match.start.row; + int start_col = match.start.col; + const int end_row = match.end.row; + + while (true) { + const int end_col = r == end_row ? match.end.col : term->cols - 1; + + int x = term->margins.left + start_col * term->cell_width; + int y = term->margins.top + r * term->cell_height; + int width = (end_col + 1 - start_col) * term->cell_width; + int height = 1 * term->cell_height; + + pixman_region32_union_rect(see_through, see_through, x, y, width, + height); + + if (++r > end_row) + break; + + start_col = 0; + } + } + + /* Areas that need to be cleared: cells that were dimmed in + * the last frame but is now see-through */ + pixman_region32_t new_see_through; + pixman_region32_init(&new_see_through); + + if (buffer_reuse) + pixman_region32_subtract(&new_see_through, see_through, &old_see_through); + else { + /* Buffer content is unknown - explicitly clear *all* + * current see-through areas */ + pixman_region32_copy(&new_see_through, see_through); + } + pixman_image_set_clip_region32(buf->pix[0], &new_see_through); + + /* Areas that need to be dimmed: cells that were cleared in + * the last frame but is not anymore */ + pixman_region32_t new_dimmed; + pixman_region32_init(&new_dimmed); + pixman_region32_subtract(&new_dimmed, &old_see_through, see_through); + pixman_region32_fini(&old_see_through); + + /* Total affected area */ + pixman_region32_t damage; + pixman_region32_init(&damage); + pixman_region32_union(&damage, &new_see_through, &new_dimmed); + damage_bounds = damage.extents; + + /* Clear cells that became selected in this frame. */ pixman_image_fill_rectangles( - PIXMAN_OP_SRC, buf->pix[0], &color, 1, + PIXMAN_OP_SRC, buf->pix[0], &(pixman_color_t){0}, 1, &(pixman_rectangle16_t){0, 0, term->width, term->height}); - quirk_weston_subsurface_desync_on(overlay->sub); - wayl_surface_scale( - term->window, &overlay->surface, buf, term->scale); - wl_subsurface_set_position(overlay->sub, 0, 0); - wl_surface_attach(overlay->surface.surf, buf->wl_buf, 0, 0); - - wl_surface_damage_buffer( - overlay->surface.surf, - damage_bounds.x1, damage_bounds.y1, - damage_bounds.x2 - damage_bounds.x1, - damage_bounds.y2 - damage_bounds.y1); - - wl_surface_commit(overlay->surface.surf); - quirk_weston_subsurface_desync_off(overlay->sub); - - buf->age = 0; - term->render.last_overlay_buf = buf; - term->render.last_overlay_style = style; -} - -int -render_worker_thread(void *_ctx) -{ - struct render_worker_context *ctx = _ctx; - struct terminal *term = ctx->term; - const int my_id = ctx->my_id; - free(ctx); - - sigset_t mask; - sigfillset(&mask); - pthread_sigmask(SIG_SETMASK, &mask, NULL); - - char proc_title[16]; - snprintf(proc_title, sizeof(proc_title), "foot:render:%d", my_id); - - if (pthread_setname_np(pthread_self(), proc_title) < 0) - LOG_ERRNO("render worker %d: failed to set process title", my_id); - - sem_t *start = &term->render.workers.start; - sem_t *done = &term->render.workers.done; - mtx_t *lock = &term->render.workers.lock; - - while (true) { - sem_wait(start); - - struct buffer *buf = term->render.workers.buf; - - bool frame_done = false; - - /* Translate offset-relative cursor row to view-relative */ - struct coord cursor = {-1, -1}; - if (!term->hide_cursor) { - cursor = term->grid->cursor.point; - cursor.row += term->grid->offset; - cursor.row -= term->grid->view; - cursor.row &= term->grid->num_rows - 1; - } - - while (!frame_done) { - mtx_lock(lock); - xassert(tll_length(term->render.workers.queue) > 0); - - int row_no = tll_pop_front(term->render.workers.queue); - mtx_unlock(lock); - - switch (row_no) { - default: { - struct row *row = grid_row_in_view(term->grid, row_no); - int cursor_col = cursor.row == row_no ? cursor.col : -1; - - render_row(term, buf->pix[my_id], &buf->dirty[my_id], - row, row_no, cursor_col); - break; - } - - case -1: - frame_done = true; - sem_post(done); - break; - - case -2: - return 0; - - case -3: { - if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) - clock_gettime(CLOCK_MONOTONIC, &term->render.workers.preapplied_damage.start); - - mtx_lock(&term->render.workers.preapplied_damage.lock); - buf = term->render.workers.preapplied_damage.buf; - xassert(buf != NULL); - - if (likely(term->render.last_buf != NULL)) { - mtx_unlock(&term->render.workers.preapplied_damage.lock); - - pixman_region32_t dmg; - pixman_region32_init(&dmg); - - if (buf->age == 0) - ; /* No need to do anything */ - else if (buf->age == 1) - pixman_region32_copy(&dmg, - &term->render.last_buf->dirty[0]); - else - pixman_region32_init_rect(&dmg, 0, 0, buf->width, - buf->height); - - pixman_image_set_clip_region32(buf->pix[my_id], &dmg); - pixman_image_composite32(PIXMAN_OP_SRC, - term->render.last_buf->pix[my_id], - NULL, buf->pix[my_id], 0, 0, 0, 0, 0, - 0, buf->width, buf->height); - - pixman_region32_fini(&dmg); - - buf->age = 0; - shm_unref(term->render.last_buf); - shm_addref(buf); - term->render.last_buf = buf; - - mtx_lock(&term->render.workers.preapplied_damage.lock); - } - - term->render.workers.preapplied_damage.buf = NULL; - cnd_signal(&term->render.workers.preapplied_damage.cond); - mtx_unlock(&term->render.workers.preapplied_damage.lock); - - if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) - clock_gettime(CLOCK_MONOTONIC, &term->render.workers.preapplied_damage.stop); - - frame_done = true; - break; - } - } - } - }; - - return -1; -} - -void -render_wait_for_preapply_damage(struct terminal *term) -{ - if (!term->render.preapply_last_frame_damage) - return; - if (term->render.workers.preapplied_damage.buf == NULL) - return; - - mtx_lock(&term->render.workers.preapplied_damage.lock); - while (term->render.workers.preapplied_damage.buf != NULL) { - cnd_wait(&term->render.workers.preapplied_damage.cond, - &term->render.workers.preapplied_damage.lock); - } - mtx_unlock(&term->render.workers.preapplied_damage.lock); -} - -struct csd_data -get_csd_data(const struct terminal *term, enum csd_surface surf_idx) -{ - xassert(term->window->csd_mode == CSD_YES); - - const bool borders_visible = wayl_win_csd_borders_visible(term->window); - const bool title_visible = wayl_win_csd_titlebar_visible(term->window); - - const float scale = term->scale; - - const int border_width = borders_visible - ? roundf(term->conf->csd.border_width * scale) : 0; - - const int title_height = title_visible - ? roundf(term->conf->csd.title_height * scale) : 0; - - const int button_width = title_visible - ? roundf(term->conf->csd.button_width * scale) : 0; - - int remaining_width = term->width; - - const int button_close_width = remaining_width >= button_width ? button_width : 0; - remaining_width -= button_close_width; - const int button_close_start = remaining_width; - - const int button_maximize_width = remaining_width >= button_width && - term->window->wm_capabilities.maximize ? button_width : 0; - remaining_width -= button_maximize_width; - const int button_maximize_start = remaining_width; - - const int button_minimize_width = remaining_width >= button_width && - term->window->wm_capabilities.minimize ? button_width : 0; - remaining_width -= button_minimize_width; - const int button_minimize_start = remaining_width; - - /* - * With fractional scaling, we must ensure the offset, when - * divided by the scale (in set_position()), and the scaled back - * (by the compositor), matches the actual pixel count made up by - * the titlebar and the border. - */ - const int top_offset = roundf( - scale * (roundf(-title_height / scale) - roundf(border_width / scale))); - - const int top_bottom_width = roundf( - scale * (roundf(term->width / scale) + 2 * roundf(border_width / scale))); - - const int left_right_height = roundf( - scale * (roundf(title_height / scale) + roundf(term->height / scale))); - - switch (surf_idx) { - case CSD_SURF_TITLE: return (struct csd_data){ 0, -title_height, term->width, title_height}; - case CSD_SURF_LEFT: return (struct csd_data){-border_width, -title_height, border_width, left_right_height}; - case CSD_SURF_RIGHT: return (struct csd_data){ term->width, -title_height, border_width, left_right_height}; - case CSD_SURF_TOP: return (struct csd_data){-border_width, top_offset, top_bottom_width, border_width}; - case CSD_SURF_BOTTOM: return (struct csd_data){-border_width, term->height, top_bottom_width, border_width}; - - /* Positioned relative to CSD_SURF_TITLE */ - case CSD_SURF_MINIMIZE: return (struct csd_data){button_minimize_start, 0, button_minimize_width, title_height}; - case CSD_SURF_MAXIMIZE: return (struct csd_data){button_maximize_start, 0, button_maximize_width, title_height}; - case CSD_SURF_CLOSE: return (struct csd_data){ button_close_start, 0, button_close_width, title_height}; - - case CSD_SURF_COUNT: - break; - } - - BUG("Invalid csd_surface type"); - return (struct csd_data){0}; -} - -static void -csd_commit(struct terminal *term, struct wayl_surface *surf, struct buffer *buf) -{ - wayl_surface_scale(term->window, surf, buf, term->scale); - wl_surface_attach(surf->surf, buf->wl_buf, 0, 0); - wl_surface_damage_buffer(surf->surf, 0, 0, buf->width, buf->height); - wl_surface_commit(surf->surf); -} - -static void -render_csd_part(struct terminal *term, - struct wl_surface *surf, struct buffer *buf, - int width, int height, pixman_color_t *color) -{ - xassert(term->window->csd_mode == CSD_YES); - - pixman_image_fill_rectangles( - PIXMAN_OP_SRC, buf->pix[0], color, 1, - &(pixman_rectangle16_t){0, 0, buf->width, buf->height}); -} - -static void -render_osd(struct terminal *term, const struct wayl_sub_surface *sub_surf, - struct fcft_font *font, struct buffer *buf, - const char32_t *text, uint32_t _fg, uint32_t _bg, - unsigned x) -{ - pixman_region32_t clip; - pixman_region32_init_rect(&clip, 0, 0, buf->width, buf->height); - pixman_image_set_clip_region32(buf->pix[0], &clip); - pixman_region32_fini(&clip); - - const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); - uint16_t alpha = _bg >> 24 | (_bg >> 24 << 8); - pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); - pixman_image_fill_rectangles( - PIXMAN_OP_SRC, buf->pix[0], &bg, 1, - &(pixman_rectangle16_t){0, 0, buf->width, buf->height}); - - pixman_color_t fg = color_hex_to_pixman(_fg, gamma_correct); - const int x_ofs = term->font_x_ofs; - - const size_t len = c32len(text); - struct fcft_text_run *text_run = NULL; - const struct fcft_glyph **glyphs = NULL; - const struct fcft_glyph *_glyphs[len]; - size_t glyph_count = 0; - - if (fcft_capabilities() & FCFT_CAPABILITY_TEXT_RUN_SHAPING) { - text_run = fcft_rasterize_text_run_utf32( - font, len, (const char32_t *)text, term->font_subpixel); - - if (text_run != NULL) { - glyphs = text_run->glyphs; - glyph_count = text_run->count; - } - } - - if (glyphs == NULL) { - for (size_t i = 0; i < len; i++) { - const struct fcft_glyph *glyph = fcft_rasterize_char_utf32( - font, text[i], term->font_subpixel); - - if (glyph == NULL) - continue; - - _glyphs[glyph_count++] = glyph; - } - - glyphs = _glyphs; - } - - pixman_image_t *src = pixman_image_create_solid_fill(&fg); - - /* Calculate baseline */ - unsigned y; - { - const int line_height = buf->height; - const int font_height = max(font->height, font->ascent + font->descent); - const int glyph_top_y = round((line_height - font_height) / 2.); - y = term->font_y_ofs + glyph_top_y + font->ascent; - } - - for (size_t i = 0; i < glyph_count; i++) { - const struct fcft_glyph *glyph = glyphs[i]; - - if (unlikely(glyph->is_color_glyph)) { - pixman_image_composite32( - PIXMAN_OP_OVER, glyph->pix, NULL, buf->pix[0], 0, 0, 0, 0, - x + x_ofs + glyph->x, y - glyph->y, - glyph->width, glyph->height); - } else { - pixman_image_composite32( - PIXMAN_OP_OVER, src, glyph->pix, buf->pix[0], 0, 0, 0, 0, - x + x_ofs + glyph->x, y - glyph->y, - glyph->width, glyph->height); - } - - x += glyph->advance.x; - } - - fcft_text_run_destroy(text_run); - pixman_image_unref(src); + /* Set clip region for the newly dimmed cells. The actual + * paint call is done below */ + pixman_image_set_clip_region32(buf->pix[0], &new_dimmed); + + pixman_region32_fini(&new_see_through); + pixman_region32_fini(&new_dimmed); + pixman_region32_fini(&damage); + } + + else if (buf == term->render.last_overlay_buf && + style == term->render.last_overlay_style) { + xassert(style == OVERLAY_FLASH || style == OVERLAY_UNICODE_MODE); + shm_did_not_use_buf(buf); + return; + } else { pixman_image_set_clip_region32(buf->pix[0], NULL); + damage_bounds = (pixman_box32_t){0, 0, buf->width, buf->height}; + } - quirk_weston_subsurface_desync_on(sub_surf->sub); - wayl_surface_scale(term->window, &sub_surf->surface, buf, term->scale); - wl_surface_attach(sub_surf->surface.surf, buf->wl_buf, 0, 0); - wl_surface_damage_buffer(sub_surf->surface.surf, 0, 0, buf->width, buf->height); + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &color, 1, + &(pixman_rectangle16_t){0, 0, term->width, term->height}); - if (alpha == 0xffff) { - struct wl_region *region = wl_compositor_create_region(term->wl->compositor); - if (region != NULL) { - wl_region_add(region, 0, 0, buf->width, buf->height); - wl_surface_set_opaque_region(sub_surf->surface.surf, region); - wl_region_destroy(region); - } - } else - wl_surface_set_opaque_region(sub_surf->surface.surf, NULL); + quirk_weston_subsurface_desync_on(overlay->sub); + wayl_surface_scale(term->window, &overlay->surface, buf, term->scale); + wl_subsurface_set_position(overlay->sub, 0, 0); + wl_surface_attach(overlay->surface.surf, buf->wl_buf, 0, 0); - wl_surface_commit(sub_surf->surface.surf); - quirk_weston_subsurface_desync_off(sub_surf->sub); + wl_surface_damage_buffer( + overlay->surface.surf, damage_bounds.x1, damage_bounds.y1, + damage_bounds.x2 - damage_bounds.x1, damage_bounds.y2 - damage_bounds.y1); + + wl_surface_commit(overlay->surface.surf); + quirk_weston_subsurface_desync_off(overlay->sub); + + buf->age = 0; + term->render.last_overlay_buf = buf; + term->render.last_overlay_style = style; } -static void -render_csd_title(struct terminal *term, const struct csd_data *info, - struct buffer *buf) -{ - xassert(term->window->csd_mode == CSD_YES); +int render_worker_thread(void *_ctx) { + struct render_worker_context *ctx = _ctx; + struct terminal *term = ctx->term; + const int my_id = ctx->my_id; + free(ctx); - struct wayl_sub_surface *surf = &term->window->csd.surface[CSD_SURF_TITLE]; - if (info->width == 0 || info->height == 0) - return; + sigset_t mask; + sigfillset(&mask); + pthread_sigmask(SIG_SETMASK, &mask, NULL); - uint32_t bg = term->conf->csd.color.title_set - ? term->conf->csd.color.title - : 0xffu << 24 | term->conf->colors_dark.fg; - uint32_t fg = term->conf->csd.color.buttons_set - ? term->conf->csd.color.buttons - : term->conf->colors_dark.bg; + char proc_title[16]; + snprintf(proc_title, sizeof(proc_title), "foot:render:%d", my_id); - if (!term->visual_focus) { - bg = color_dim(term, bg); - fg = color_dim(term, fg); + if (pthread_setname_np(pthread_self(), proc_title) < 0) + LOG_ERRNO("render worker %d: failed to set process title", my_id); + + sem_t *start = &term->render.workers.start; + sem_t *done = &term->render.workers.done; + mtx_t *lock = &term->render.workers.lock; + + while (true) { + sem_wait(start); + + struct buffer *buf = term->render.workers.buf; + + bool frame_done = false; + + /* Translate offset-relative cursor row to view-relative */ + struct coord cursor = {-1, -1}; + if (!term->hide_cursor) { + cursor = term->grid->cursor.point; + cursor.row += term->grid->offset; + cursor.row -= term->grid->view; + cursor.row &= term->grid->num_rows - 1; } - char32_t *_title_text = ambstoc32(term->window_title); - const char32_t *title_text = _title_text != NULL ? _title_text : U""; + while (!frame_done) { + mtx_lock(lock); + xassert(tll_length(term->render.workers.queue) > 0); - struct wl_window *win = term->window; + int row_no = tll_pop_front(term->render.workers.queue); + mtx_unlock(lock); - const struct fcft_glyph *M = fcft_rasterize_char_utf32( - win->csd.font, U'M', term->font_subpixel); + switch (row_no) { + default: { + struct row *row = grid_row_in_view(term->grid, row_no); + int cursor_col = cursor.row == row_no ? cursor.col : -1; - const int margin = M != NULL ? M->advance.x : win->csd.font->max_advance.x; + render_row(term, buf->pix[my_id], &buf->dirty[my_id], row, row_no, + cursor_col); + break; + } - render_osd(term, surf, win->csd.font, buf, title_text, fg, bg, margin); - csd_commit(term, &surf->surface, buf); - free(_title_text); -} + case -1: + frame_done = true; + sem_post(done); + break; -static void -render_csd_border(struct terminal *term, enum csd_surface surf_idx, - const struct csd_data *info, struct buffer *buf) -{ - xassert(term->window->csd_mode == CSD_YES); - xassert(surf_idx >= CSD_SURF_LEFT && surf_idx <= CSD_SURF_BOTTOM); + case -2: + return 0; - struct wayl_surface *surf = &term->window->csd.surface[surf_idx].surface; + case -3: { + if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) + clock_gettime(CLOCK_MONOTONIC, + &term->render.workers.preapplied_damage.start); - if (info->width == 0 || info->height == 0) - return; + mtx_lock(&term->render.workers.preapplied_damage.lock); + buf = term->render.workers.preapplied_damage.buf; + xassert(buf != NULL); - const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + if (likely(term->render.last_buf != NULL)) { + mtx_unlock(&term->render.workers.preapplied_damage.lock); - { - /* Fully transparent - no need to do a color space transform */ - pixman_color_t color = color_hex_to_pixman_with_alpha(0, 0, gamma_correct); - render_csd_part(term, surf->surf, buf, info->width, info->height, &color); - } + pixman_region32_t dmg; + pixman_region32_init(&dmg); - /* - * The "visible" border. - */ + if (buf->age == 0) + ; /* No need to do anything */ + else if (buf->age == 1) + pixman_region32_copy(&dmg, &term->render.last_buf->dirty[0]); + else + pixman_region32_init_rect(&dmg, 0, 0, buf->width, buf->height); - float scale = term->scale; - int bwidth = (int)roundf(term->conf->csd.border_width * scale); - int vwidth = (int)roundf(term->conf->csd.border_width_visible * scale); /* Visible size */ + pixman_image_set_clip_region32(buf->pix[my_id], &dmg); + pixman_image_composite32( + PIXMAN_OP_SRC, term->render.last_buf->pix[my_id], NULL, + buf->pix[my_id], 0, 0, 0, 0, 0, 0, buf->width, buf->height); - xassert(bwidth >= vwidth); + pixman_region32_fini(&dmg); - if (vwidth > 0) { + buf->age = 0; + shm_unref(term->render.last_buf); + shm_addref(buf); + term->render.last_buf = buf; - const struct config *conf = term->conf; - int x = 0, y = 0, w = 0, h = 0; - - - switch (surf_idx) { - case CSD_SURF_TOP: - case CSD_SURF_BOTTOM: - x = bwidth - vwidth; - y = surf_idx == CSD_SURF_TOP ? info->height - vwidth : 0; - w = info->width - 2 * x; - h = vwidth; - break; - - case CSD_SURF_LEFT: - case CSD_SURF_RIGHT: - x = surf_idx == CSD_SURF_LEFT ? bwidth - vwidth : 0; - y = 0; - w = vwidth; - h = info->height; - break; - - case CSD_SURF_TITLE: - case CSD_SURF_MINIMIZE: - case CSD_SURF_MAXIMIZE: - case CSD_SURF_CLOSE: - case CSD_SURF_COUNT: - BUG("unexpected CSD surface type"); + mtx_lock(&term->render.workers.preapplied_damage.lock); } - xassert(x >= 0); - xassert(y >= 0); - xassert(w >= 0); - xassert(h >= 0); + term->render.workers.preapplied_damage.buf = NULL; + cnd_signal(&term->render.workers.preapplied_damage.cond); + mtx_unlock(&term->render.workers.preapplied_damage.lock); - xassert(x + w <= info->width); - xassert(y + h <= info->height); + if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) + clock_gettime(CLOCK_MONOTONIC, + &term->render.workers.preapplied_damage.stop); - uint32_t _color = - conf->csd.color.border_set ? conf->csd.color.border : - conf->csd.color.title_set ? conf->csd.color.title : - 0xffu << 24 | term->conf->colors_dark.fg; - if (!term->visual_focus) - _color = color_dim(term, _color); + frame_done = true; + break; + } + } + } + }; - uint16_t alpha = _color >> 24 | (_color >> 24 << 8); - pixman_color_t color = - color_hex_to_pixman_with_alpha(_color, alpha, gamma_correct); + return -1; +} - pixman_image_fill_rectangles( - PIXMAN_OP_SRC, buf->pix[0], &color, 1, - &(pixman_rectangle16_t){x, y, w, h}); +void render_wait_for_preapply_damage(struct terminal *term) { + if (!term->render.preapply_last_frame_damage) + return; + if (term->render.workers.preapplied_damage.buf == NULL) + return; + + mtx_lock(&term->render.workers.preapplied_damage.lock); + while (term->render.workers.preapplied_damage.buf != NULL) { + cnd_wait(&term->render.workers.preapplied_damage.cond, + &term->render.workers.preapplied_damage.lock); + } + mtx_unlock(&term->render.workers.preapplied_damage.lock); +} + +struct csd_data get_csd_data(const struct terminal *term, + enum csd_surface surf_idx) { + xassert(term->window->csd_mode == CSD_YES); + + const bool borders_visible = wayl_win_csd_borders_visible(term->window); + const bool title_visible = wayl_win_csd_titlebar_visible(term->window); + + const float scale = term->scale; + + const int border_width = + borders_visible ? roundf(term->conf->csd.border_width * scale) : 0; + + const int title_height = + title_visible ? roundf(term->conf->csd.title_height * scale) : 0; + + const int button_width = + title_visible ? roundf(term->conf->csd.button_width * scale) : 0; + + int remaining_width = term->width; + + const int button_close_width = + remaining_width >= button_width ? button_width : 0; + remaining_width -= button_close_width; + const int button_close_start = remaining_width; + + const int button_maximize_width = + remaining_width >= button_width && term->window->wm_capabilities.maximize + ? button_width + : 0; + remaining_width -= button_maximize_width; + const int button_maximize_start = remaining_width; + + const int button_minimize_width = + remaining_width >= button_width && term->window->wm_capabilities.minimize + ? button_width + : 0; + remaining_width -= button_minimize_width; + const int button_minimize_start = remaining_width; + + /* + * With fractional scaling, we must ensure the offset, when + * divided by the scale (in set_position()), and the scaled back + * (by the compositor), matches the actual pixel count made up by + * the titlebar and the border. + */ + const int top_offset = roundf( + scale * (roundf(-title_height / scale) - roundf(border_width / scale))); + + const int top_bottom_width = roundf( + scale * (roundf(term->width / scale) + 2 * roundf(border_width / scale))); + + const int left_right_height = roundf( + scale * (roundf(title_height / scale) + roundf(term->height / scale))); + + switch (surf_idx) { + case CSD_SURF_TITLE: + return (struct csd_data){0, -title_height, term->width, title_height}; + case CSD_SURF_LEFT: + return (struct csd_data){-border_width, -title_height, border_width, + left_right_height}; + case CSD_SURF_RIGHT: + return (struct csd_data){term->width, -title_height, border_width, + left_right_height}; + case CSD_SURF_TOP: + return (struct csd_data){-border_width, top_offset, top_bottom_width, + border_width}; + case CSD_SURF_BOTTOM: + return (struct csd_data){-border_width, term->height, top_bottom_width, + border_width}; + + /* Positioned relative to CSD_SURF_TITLE */ + case CSD_SURF_MINIMIZE: + return (struct csd_data){button_minimize_start, 0, button_minimize_width, + title_height}; + case CSD_SURF_MAXIMIZE: + return (struct csd_data){button_maximize_start, 0, button_maximize_width, + title_height}; + case CSD_SURF_CLOSE: + return (struct csd_data){button_close_start, 0, button_close_width, + title_height}; + + case CSD_SURF_COUNT: + break; + } + + BUG("Invalid csd_surface type"); + return (struct csd_data){0}; +} + +static void csd_commit(struct terminal *term, struct wayl_surface *surf, + struct buffer *buf) { + wayl_surface_scale(term->window, surf, buf, term->scale); + wl_surface_attach(surf->surf, buf->wl_buf, 0, 0); + wl_surface_damage_buffer(surf->surf, 0, 0, buf->width, buf->height); + wl_surface_commit(surf->surf); +} + +static void render_csd_part(struct terminal *term, struct wl_surface *surf, + struct buffer *buf, int width, int height, + pixman_color_t *color) { + xassert(term->window->csd_mode == CSD_YES); + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], color, 1, + &(pixman_rectangle16_t){0, 0, buf->width, buf->height}); +} + +static void render_osd(struct terminal *term, + const struct wayl_sub_surface *sub_surf, + struct fcft_font *font, struct buffer *buf, + const char32_t *text, uint32_t _fg, uint32_t _bg, + unsigned x) { + pixman_region32_t clip; + pixman_region32_init_rect(&clip, 0, 0, buf->width, buf->height); + pixman_image_set_clip_region32(buf->pix[0], &clip); + pixman_region32_fini(&clip); + + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + uint16_t alpha = _bg >> 24 | (_bg >> 24 << 8); + pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &bg, 1, + &(pixman_rectangle16_t){0, 0, buf->width, buf->height}); + + pixman_color_t fg = color_hex_to_pixman(_fg, gamma_correct); + const int x_ofs = term->font_x_ofs; + + const size_t len = c32len(text); + struct fcft_text_run *text_run = NULL; + const struct fcft_glyph **glyphs = NULL; + const struct fcft_glyph *_glyphs[len]; + size_t glyph_count = 0; + + if (fcft_capabilities() & FCFT_CAPABILITY_TEXT_RUN_SHAPING) { + text_run = fcft_rasterize_text_run_utf32(font, len, (const char32_t *)text, + term->font_subpixel); + + if (text_run != NULL) { + glyphs = text_run->glyphs; + glyph_count = text_run->count; + } + } + + if (glyphs == NULL) { + for (size_t i = 0; i < len; i++) { + const struct fcft_glyph *glyph = + fcft_rasterize_char_utf32(font, text[i], term->font_subpixel); + + if (glyph == NULL) + continue; + + _glyphs[glyph_count++] = glyph; } - csd_commit(term, surf, buf); -} + glyphs = _glyphs; + } -static pixman_color_t -get_csd_button_fg_color(const struct terminal *term) -{ - const struct config *conf = term->conf; - uint32_t _color = conf->colors_dark.bg; - uint16_t alpha = 0xffff; + pixman_image_t *src = pixman_image_create_solid_fill(&fg); - if (conf->csd.color.buttons_set) { - _color = conf->csd.color.buttons; - alpha = _color >> 24 | (_color >> 24 << 8); - } + /* Calculate baseline */ + unsigned y; + { + const int line_height = buf->height; + const int font_height = max(font->height, font->ascent + font->descent); + const int glyph_top_y = round((line_height - font_height) / 2.); + y = term->font_y_ofs + glyph_top_y + font->ascent; + } - return color_hex_to_pixman_with_alpha( - _color, alpha, wayl_do_linear_blending(term->wl, term->conf)); -} + for (size_t i = 0; i < glyph_count; i++) { + const struct fcft_glyph *glyph = glyphs[i]; -static void -render_csd_button_minimize(struct terminal *term, struct buffer *buf) -{ - pixman_color_t color = get_csd_button_fg_color(term); - pixman_image_t *src = pixman_image_create_solid_fill(&color); - - const int max_height = buf->height / 3; - const int max_width = buf->width / 3; - - int width = min(max_height, max_width); - int thick = min(width / 2, 1 * term->scale); - - const int x_margin = (buf->width - width) / 2; - const int y_margin = (buf->height - width) / 2; - - xassert(x_margin + width - thick >= 0); - xassert(width - 2 * thick >= 0); - xassert(y_margin + width - thick >= 0); - pixman_image_fill_rectangles( - PIXMAN_OP_SRC, buf->pix[0], &color, 1, - (pixman_rectangle16_t[]) { - {x_margin, y_margin + width - thick, width, thick} - }); - - pixman_image_unref(src); -} - -static void -render_csd_button_maximize_maximized( - struct terminal *term, struct buffer *buf) -{ - pixman_color_t color = get_csd_button_fg_color(term); - pixman_image_t *src = pixman_image_create_solid_fill(&color); - - const int max_height = buf->height / 3; - const int max_width = buf->width / 3; - - int width = min(max_height, max_width); - int thick = min(width / 2, 1 * term->scale); - - const int x_margin = (buf->width - width) / 2; - const int y_margin = (buf->height - width) / 2; - const int shrink = 1; - xassert(x_margin + width - thick >= 0); - xassert(width - 2 * thick >= 0); - xassert(y_margin + width - thick >= 0); - - pixman_image_fill_rectangles( - PIXMAN_OP_SRC, buf->pix[0], &color, 4, - (pixman_rectangle16_t[]){ - {x_margin + shrink, y_margin + shrink, width - 2 * shrink, thick}, - { x_margin + shrink, y_margin + thick, thick, width - 2 * thick - shrink }, - { x_margin + width - thick - shrink, y_margin + thick, thick, width - 2 * thick - shrink }, - { x_margin + shrink, y_margin + width - thick - shrink, width - 2 * shrink, thick }}); - - pixman_image_unref(src); - -} - -static void -render_csd_button_maximize_window( - struct terminal *term, struct buffer *buf) -{ - pixman_color_t color = get_csd_button_fg_color(term); - pixman_image_t *src = pixman_image_create_solid_fill(&color); - - const int max_height = buf->height / 3; - const int max_width = buf->width / 3; - - int width = min(max_height, max_width); - int thick = min(width / 2, 1 * term->scale); - - const int x_margin = (buf->width - width) / 2; - const int y_margin = (buf->height - width) / 2; - - xassert(x_margin + width - thick >= 0); - xassert(width - 2 * thick >= 0); - xassert(y_margin + width - thick >= 0); - - pixman_image_fill_rectangles( - PIXMAN_OP_SRC, buf->pix[0], &color, 4, - (pixman_rectangle16_t[]) { - {x_margin, y_margin, width, thick}, - { x_margin, y_margin + thick, thick, width - 2 * thick }, - { x_margin + width - thick, y_margin + thick, thick, width - 2 * thick }, - { x_margin, y_margin + width - thick, width, thick } - }); - - pixman_image_unref(src); -} - -static void -render_csd_button_maximize(struct terminal *term, struct buffer *buf) -{ - if (term->window->is_maximized) - render_csd_button_maximize_maximized(term, buf); - else - render_csd_button_maximize_window(term, buf); -} - -static void -render_csd_button_close(struct terminal *term, struct buffer *buf) -{ - pixman_color_t color = get_csd_button_fg_color(term); - pixman_image_t *src = pixman_image_create_solid_fill(&color); - - const int max_height = buf->height / 3; - const int max_width = buf->width / 3; - - int width = min(max_height, max_width); - int thick = min(width / 2, 1 * term->scale); - const int x_margin = (buf->width - width) / 2; - const int y_margin = (buf->height - width) / 2; - - xassert(x_margin + width - thick >= 0); - xassert(width - 2 * thick >= 0); - xassert(y_margin + width - thick >= 0); - - pixman_triangle_t tri[4] = { - { - .p1 = { - .x = pixman_int_to_fixed(x_margin), - .y = pixman_int_to_fixed(y_margin + thick), - }, - .p2 = { - .x = pixman_int_to_fixed(x_margin + width - thick), - .y = pixman_int_to_fixed(y_margin + width), - }, - .p3 = { - .x = pixman_int_to_fixed(x_margin + thick), - .y = pixman_int_to_fixed(y_margin), - }, - }, - - { - .p1 = { - .x = pixman_int_to_fixed(x_margin + width), - .y = pixman_int_to_fixed(y_margin + width - thick), - }, - .p2 = { - .x = pixman_int_to_fixed(x_margin + thick), - .y = pixman_int_to_fixed(y_margin), - }, - .p3 = { - .x = pixman_int_to_fixed(x_margin + width - thick), - .y = pixman_int_to_fixed(y_margin + width), - }, - }, - - { - .p1 = { - .x = pixman_int_to_fixed(x_margin), - .y = pixman_int_to_fixed(y_margin + width - thick), - }, - .p2 = { - .x = pixman_int_to_fixed(x_margin + width), - .y = pixman_int_to_fixed(y_margin + thick), - }, - .p3 = { - .x = pixman_int_to_fixed(x_margin + thick), - .y = pixman_int_to_fixed(y_margin + width), - }, - }, - - { - .p1 = { - .x = pixman_int_to_fixed(x_margin + width), - .y = pixman_int_to_fixed(y_margin + thick), - }, - .p2 = { - .x = pixman_int_to_fixed(x_margin), - .y = pixman_int_to_fixed(y_margin + width - thick), - }, - .p3 = { - .x = pixman_int_to_fixed(x_margin + width - thick), - .y = pixman_int_to_fixed(y_margin), - }, - }, - }; - - pixman_composite_triangles( - PIXMAN_OP_OVER, src, buf->pix[0], PIXMAN_a1, - 0, 0, 0, 0, 4, tri); - - pixman_image_unref(src); -} - -static bool -any_pointer_is_on_button(const struct terminal *term, enum csd_surface csd_surface) -{ - if (unlikely(tll_length(term->wl->seats) == 0)) - return false; - - tll_foreach(term->wl->seats, it) { - const struct seat *seat = &it->item; - - if (seat->mouse.x < 0) - continue; - if (seat->mouse.y < 0) - continue; - - struct csd_data info = get_csd_data(term, csd_surface); - if (seat->mouse.x > info.width) - continue; - - if (seat->mouse.y > info.height) - continue; - return true; - } - - return false; -} - -static void -render_csd_button(struct terminal *term, enum csd_surface surf_idx, - const struct csd_data *info, struct buffer *buf) -{ - xassert(term->window->csd_mode == CSD_YES); - xassert(surf_idx >= CSD_SURF_MINIMIZE && surf_idx <= CSD_SURF_CLOSE); - - struct wayl_surface *surf = &term->window->csd.surface[surf_idx].surface; - - if (info->width == 0 || info->height == 0) - return; - - uint32_t _color; - uint16_t alpha = 0xffff; - bool is_active = false; - bool is_set = false; - const uint32_t *conf_color = NULL; - - switch (surf_idx) { - case CSD_SURF_MINIMIZE: - _color = term->conf->colors_dark.table[4]; /* blue */ - is_set = term->conf->csd.color.minimize_set; - conf_color = &term->conf->csd.color.minimize; - is_active = term->active_surface == TERM_SURF_BUTTON_MINIMIZE && - any_pointer_is_on_button(term, CSD_SURF_MINIMIZE); - break; - - case CSD_SURF_MAXIMIZE: - _color = term->conf->colors_dark.table[2]; /* green */ - is_set = term->conf->csd.color.maximize_set; - conf_color = &term->conf->csd.color.maximize; - is_active = term->active_surface == TERM_SURF_BUTTON_MAXIMIZE && - any_pointer_is_on_button(term, CSD_SURF_MAXIMIZE); - break; - - case CSD_SURF_CLOSE: - _color = term->conf->colors_dark.table[1]; /* red */ - is_set = term->conf->csd.color.close_set; - conf_color = &term->conf->csd.color.quit; - is_active = term->active_surface == TERM_SURF_BUTTON_CLOSE && - any_pointer_is_on_button(term, CSD_SURF_CLOSE); - break; - - default: - BUG("unhandled surface type: %u", (unsigned)surf_idx); - break; - } - - if (is_active) { - if (is_set) { - _color = *conf_color; - alpha = _color >> 24 | (_color >> 24 << 8); - } + if (unlikely(glyph->is_color_glyph)) { + pixman_image_composite32(PIXMAN_OP_OVER, glyph->pix, NULL, buf->pix[0], 0, + 0, 0, 0, x + x_ofs + glyph->x, y - glyph->y, + glyph->width, glyph->height); } else { - _color = 0; - alpha = 0; + pixman_image_composite32(PIXMAN_OP_OVER, src, glyph->pix, buf->pix[0], 0, + 0, 0, 0, x + x_ofs + glyph->x, y - glyph->y, + glyph->width, glyph->height); } - if (!term->visual_focus) - _color = color_dim(term, _color); + x += glyph->advance.x; + } - const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); - pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha, gamma_correct); + fcft_text_run_destroy(text_run); + pixman_image_unref(src); + pixman_image_set_clip_region32(buf->pix[0], NULL); + + quirk_weston_subsurface_desync_on(sub_surf->sub); + wayl_surface_scale(term->window, &sub_surf->surface, buf, term->scale); + wl_surface_attach(sub_surf->surface.surf, buf->wl_buf, 0, 0); + wl_surface_damage_buffer(sub_surf->surface.surf, 0, 0, buf->width, + buf->height); + + if (alpha == 0xffff) { + struct wl_region *region = + wl_compositor_create_region(term->wl->compositor); + if (region != NULL) { + wl_region_add(region, 0, 0, buf->width, buf->height); + wl_surface_set_opaque_region(sub_surf->surface.surf, region); + wl_region_destroy(region); + } + } else + wl_surface_set_opaque_region(sub_surf->surface.surf, NULL); + + wl_surface_commit(sub_surf->surface.surf); + quirk_weston_subsurface_desync_off(sub_surf->sub); +} + +static void render_csd_title(struct terminal *term, const struct csd_data *info, + struct buffer *buf) { + xassert(term->window->csd_mode == CSD_YES); + + struct wayl_sub_surface *surf = &term->window->csd.surface[CSD_SURF_TITLE]; + if (info->width == 0 || info->height == 0) + return; + + uint32_t bg = term->conf->csd.color.title_set + ? term->conf->csd.color.title + : 0xffu << 24 | term->conf->colors_dark.fg; + uint32_t fg = term->conf->csd.color.buttons_set + ? term->conf->csd.color.buttons + : term->conf->colors_dark.bg; + + if (!term->visual_focus) { + bg = color_dim(term, bg); + fg = color_dim(term, fg); + } + + char32_t *_title_text = ambstoc32(term->window_title); + const char32_t *title_text = _title_text != NULL ? _title_text : U""; + + struct wl_window *win = term->window; + + const struct fcft_glyph *M = + fcft_rasterize_char_utf32(win->csd.font, U'M', term->font_subpixel); + + const int margin = M != NULL ? M->advance.x : win->csd.font->max_advance.x; + + render_osd(term, surf, win->csd.font, buf, title_text, fg, bg, margin); + csd_commit(term, &surf->surface, buf); + free(_title_text); +} + +static void render_csd_border(struct terminal *term, enum csd_surface surf_idx, + const struct csd_data *info, struct buffer *buf) { + xassert(term->window->csd_mode == CSD_YES); + xassert(surf_idx >= CSD_SURF_LEFT && surf_idx <= CSD_SURF_BOTTOM); + + struct wayl_surface *surf = &term->window->csd.surface[surf_idx].surface; + + if (info->width == 0 || info->height == 0) + return; + + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + + { + /* Fully transparent - no need to do a color space transform */ + pixman_color_t color = color_hex_to_pixman_with_alpha(0, 0, gamma_correct); render_csd_part(term, surf->surf, buf, info->width, info->height, &color); + } + + /* + * The "visible" border. + */ + + float scale = term->scale; + int bwidth = (int)roundf(term->conf->csd.border_width * scale); + int vwidth = (int)roundf(term->conf->csd.border_width_visible * + scale); /* Visible size */ + + xassert(bwidth >= vwidth); + + if (vwidth > 0) { + + const struct config *conf = term->conf; + int x = 0, y = 0, w = 0, h = 0; switch (surf_idx) { - case CSD_SURF_MINIMIZE: render_csd_button_minimize(term, buf); break; - case CSD_SURF_MAXIMIZE: render_csd_button_maximize(term, buf); break; - case CSD_SURF_CLOSE: render_csd_button_close(term, buf); break; + case CSD_SURF_TOP: + case CSD_SURF_BOTTOM: + x = bwidth - vwidth; + y = surf_idx == CSD_SURF_TOP ? info->height - vwidth : 0; + w = info->width - 2 * x; + h = vwidth; + break; - default: - BUG("unhandled surface type: %u", (unsigned)surf_idx); - break; + case CSD_SURF_LEFT: + case CSD_SURF_RIGHT: + x = surf_idx == CSD_SURF_LEFT ? bwidth - vwidth : 0; + y = 0; + w = vwidth; + h = info->height; + break; + + case CSD_SURF_TITLE: + case CSD_SURF_MINIMIZE: + case CSD_SURF_MAXIMIZE: + case CSD_SURF_CLOSE: + case CSD_SURF_COUNT: + BUG("unexpected CSD surface type"); } - csd_commit(term, surf, buf); + xassert(x >= 0); + xassert(y >= 0); + xassert(w >= 0); + xassert(h >= 0); + + xassert(x + w <= info->width); + xassert(y + h <= info->height); + + uint32_t _color = conf->csd.color.border_set ? conf->csd.color.border + : conf->csd.color.title_set + ? conf->csd.color.title + : 0xffu << 24 | term->conf->colors_dark.fg; + if (!term->visual_focus) + _color = color_dim(term, _color); + + uint16_t alpha = _color >> 24 | (_color >> 24 << 8); + pixman_color_t color = + color_hex_to_pixman_with_alpha(_color, alpha, gamma_correct); + + pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], &color, 1, + &(pixman_rectangle16_t){x, y, w, h}); + } + + csd_commit(term, surf, buf); } -static void -render_csd(struct terminal *term) -{ - xassert(term->window->csd_mode == CSD_YES); +static pixman_color_t get_csd_button_fg_color(const struct terminal *term) { + const struct config *conf = term->conf; + uint32_t _color = conf->colors_dark.bg; + uint16_t alpha = 0xffff; - if (term->window->is_fullscreen) - return; + if (conf->csd.color.buttons_set) { + _color = conf->csd.color.buttons; + alpha = _color >> 24 | (_color >> 24 << 8); + } - const float scale = term->scale; - struct csd_data infos[CSD_SURF_COUNT]; - int widths[CSD_SURF_COUNT]; - int heights[CSD_SURF_COUNT]; - - for (size_t i = 0; i < CSD_SURF_COUNT; i++) { - infos[i] = get_csd_data(term, i); - const int x = infos[i].x; - const int y = infos[i].y; - const int width = infos[i].width; - const int height = infos[i].height; - - struct wl_surface *surf = term->window->csd.surface[i].surface.surf; - struct wl_subsurface *sub = term->window->csd.surface[i].sub; - - xassert(surf != NULL); - xassert(sub != NULL); - - if (width == 0 || height == 0) { - widths[i] = heights[i] = 0; - wl_subsurface_set_position(sub, 0, 0); - wl_surface_attach(surf, NULL, 0, 0); - wl_surface_commit(surf); - continue; - } - - widths[i] = width; - heights[i] = height; - wl_subsurface_set_position(sub, roundf(x / scale), roundf(y / scale)); - } - - struct buffer *bufs[CSD_SURF_COUNT]; - shm_get_many(term->render.chains.csd, CSD_SURF_COUNT, widths, heights, bufs); - - for (size_t i = CSD_SURF_LEFT; i <= CSD_SURF_BOTTOM; i++) - render_csd_border(term, i, &infos[i], bufs[i]); - for (size_t i = CSD_SURF_MINIMIZE; i <= CSD_SURF_CLOSE; i++) - render_csd_button(term, i, &infos[i], bufs[i]); - render_csd_title(term, &infos[CSD_SURF_TITLE], bufs[CSD_SURF_TITLE]); + return color_hex_to_pixman_with_alpha( + _color, alpha, wayl_do_linear_blending(term->wl, term->conf)); } -static void -render_scrollback_position(struct terminal *term) -{ - if (term->conf->scrollback.indicator.position == SCROLLBACK_INDICATOR_POSITION_NONE) - return; +static void render_csd_button_minimize(struct terminal *term, + struct buffer *buf) { + pixman_color_t color = get_csd_button_fg_color(term); + pixman_image_t *src = pixman_image_create_solid_fill(&color); - struct wl_window *win = term->window; + const int max_height = buf->height / 3; + const int max_width = buf->width / 3; - if (term->grid->view == term->grid->offset) { - if (win->scrollback_indicator.surface.surf != NULL) - wayl_win_subsurface_destroy(&win->scrollback_indicator); - return; - } + int width = min(max_height, max_width); + int thick = min(width / 2, 1 * term->scale); - if (win->scrollback_indicator.surface.surf == NULL) { - if (!wayl_win_subsurface_new( - win, &win->scrollback_indicator, false)) - { - LOG_ERR("failed to create scrollback indicator surface"); - return; - } - } + const int x_margin = (buf->width - width) / 2; + const int y_margin = (buf->height - width) / 2; - xassert(win->scrollback_indicator.surface.surf != NULL); - xassert(win->scrollback_indicator.sub != NULL); + xassert(x_margin + width - thick >= 0); + xassert(width - 2 * thick >= 0); + xassert(y_margin + width - thick >= 0); + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &color, 1, + (pixman_rectangle16_t[]){ + {x_margin, y_margin + width - thick, width, thick}}); - /* Find absolute row number of the scrollback start */ - int scrollback_start = term->grid->offset + term->rows; - int empty_rows = 0; - while (term->grid->rows[scrollback_start & (term->grid->num_rows - 1)] == NULL) { - scrollback_start++; - empty_rows++; - } - - /* Rebase viewport against scrollback start (so that 0 is at - * the beginning of the scrollback) */ - int rebased_view = term->grid->view - scrollback_start + term->grid->num_rows; - rebased_view &= term->grid->num_rows - 1; - - /* How much of the scrollback is actually used? */ - int populated_rows = term->grid->num_rows - empty_rows; - xassert(populated_rows > 0); - xassert(populated_rows <= term->grid->num_rows); - - /* - * How far down in the scrollback we are. - * - * 0% -> at the beginning of the scrollback - * 100% -> at the bottom, i.e. where new lines are inserted - */ - double percent = - rebased_view + term->rows == populated_rows - ? 1.0 - : (double)rebased_view / (populated_rows - term->rows); - - char32_t _text[64]; - const char32_t *text = _text; - int cell_count = 0; - - /* *What* to render */ - switch (term->conf->scrollback.indicator.format) { - case SCROLLBACK_INDICATOR_FORMAT_PERCENTAGE: { - char percent_str[8]; - snprintf(percent_str, sizeof(percent_str), "%u%%", (int)(100 * percent)); - mbstoc32(_text, percent_str, ALEN(_text)); - cell_count = 3; - break; - } - - case SCROLLBACK_INDICATOR_FORMAT_LINENO: { - char lineno_str[64]; - snprintf(lineno_str, sizeof(lineno_str), "%d", rebased_view + 1); - mbstoc32(_text, lineno_str, ALEN(_text)); - cell_count = (int)ceilf(log10f(term->grid->num_rows)); - break; - } - - case SCROLLBACK_INDICATOR_FORMAT_TEXT: - text = term->conf->scrollback.indicator.text; - cell_count = c32len(text); - break; - } - - const float scale = term->scale; - const int margin = (int)roundf(3. * scale); - - int width = margin + cell_count * term->cell_width + margin; - int height = margin + term->cell_height + margin; - - width = roundf(scale * ceilf(width / scale)); - height = roundf(scale * ceilf(height / scale)); - - /* *Where* to render - parent relative coordinates */ - int surf_top = 0; - switch (term->conf->scrollback.indicator.position) { - case SCROLLBACK_INDICATOR_POSITION_NONE: - BUG("Invalid scrollback indicator position type"); - return; - - case SCROLLBACK_INDICATOR_POSITION_FIXED: - surf_top = term->cell_height - margin; - break; - - case SCROLLBACK_INDICATOR_POSITION_RELATIVE: { - int lines = term->rows - 2; /* Avoid using first and last rows */ - if (term->is_searching) { - /* Make sure we don't collide with the scrollback search box */ - lines--; - } - - lines = max(lines, 0); - - int pixels = max(lines * term->cell_height - height + 2 * margin, 0); - surf_top = term->cell_height - margin + (int)(percent * pixels); - break; - } - } - - int x = term->width - margin - width; - int y = term->margins.top + surf_top; - - x = roundf(scale * ceilf(x / scale)); - y = roundf(scale * ceilf(y / scale)); - - if (y + height > term->height) { - wl_surface_attach(win->scrollback_indicator.surface.surf, NULL, 0, 0); - wl_surface_commit(win->scrollback_indicator.surface.surf); - return; - } - - struct buffer_chain *chain = term->render.chains.scrollback_indicator; - struct buffer *buf = shm_get_buffer(chain, width, height); - - wl_subsurface_set_position( - win->scrollback_indicator.sub, roundf(x / scale), roundf(y / scale)); - - uint32_t fg = term->colors.table[0]; - uint32_t bg = term->colors.table[8 + 4]; - if (term->conf->colors_dark.use_custom.scrollback_indicator) { - fg = term->conf->colors_dark.scrollback_indicator.fg; - bg = term->conf->colors_dark.scrollback_indicator.bg; - } - - render_osd( - term, - &win->scrollback_indicator, - term->fonts[0], buf, text, - fg, 0xffu << 24 | bg, - width - margin - c32len(text) * term->cell_width); + pixman_image_unref(src); } -static void -render_render_timer(struct terminal *term, struct timespec render_time) -{ - struct wl_window *win = term->window; +static void render_csd_button_maximize_maximized(struct terminal *term, + struct buffer *buf) { + pixman_color_t color = get_csd_button_fg_color(term); + pixman_image_t *src = pixman_image_create_solid_fill(&color); - char usecs_str[256]; - double usecs = render_time.tv_sec * 1000000 + render_time.tv_nsec / 1000.0; - snprintf(usecs_str, sizeof(usecs_str), "%.2f µs", usecs); + const int max_height = buf->height / 3; + const int max_width = buf->width / 3; - char32_t text[256]; - mbstoc32(text, usecs_str, ALEN(text)); + int width = min(max_height, max_width); + int thick = min(width / 2, 1 * term->scale); - const float scale = term->scale; - const int cell_count = c32len(text); - const int margin = (int)roundf(3. * scale); + const int x_margin = (buf->width - width) / 2; + const int y_margin = (buf->height - width) / 2; + const int shrink = 1; + xassert(x_margin + width - thick >= 0); + xassert(width - 2 * thick >= 0); + xassert(y_margin + width - thick >= 0); - int width = margin + cell_count * term->cell_width + margin; - int height = margin + term->cell_height + margin; + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &color, 4, + (pixman_rectangle16_t[]){ + {x_margin + shrink, y_margin + shrink, width - 2 * shrink, thick}, + {x_margin + shrink, y_margin + thick, thick, + width - 2 * thick - shrink}, + {x_margin + width - thick - shrink, y_margin + thick, thick, + width - 2 * thick - shrink}, + {x_margin + shrink, y_margin + width - thick - shrink, + width - 2 * shrink, thick}}); - width = roundf(scale * ceilf(width / scale)); - height = roundf(scale * ceilf(height / scale)); - - struct buffer_chain *chain = term->render.chains.render_timer; - struct buffer *buf = shm_get_buffer(chain, width, height); - - wl_subsurface_set_position( - win->render_timer.sub, - roundf(margin / scale), - roundf((term->margins.top + term->cell_height - margin) / scale)); - - render_osd( - term, - &win->render_timer, - term->fonts[0], buf, text, - term->colors.table[0], 0xffu << 24 | term->colors.table[8 + 1], - margin); + pixman_image_unref(src); } -static void frame_callback( - void *data, struct wl_callback *wl_callback, uint32_t callback_data); +static void render_csd_button_maximize_window(struct terminal *term, + struct buffer *buf) { + pixman_color_t color = get_csd_button_fg_color(term); + pixman_image_t *src = pixman_image_create_solid_fill(&color); + + const int max_height = buf->height / 3; + const int max_width = buf->width / 3; + + int width = min(max_height, max_width); + int thick = min(width / 2, 1 * term->scale); + + const int x_margin = (buf->width - width) / 2; + const int y_margin = (buf->height - width) / 2; + + xassert(x_margin + width - thick >= 0); + xassert(width - 2 * thick >= 0); + xassert(y_margin + width - thick >= 0); + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &color, 4, + (pixman_rectangle16_t[]){ + {x_margin, y_margin, width, thick}, + {x_margin, y_margin + thick, thick, width - 2 * thick}, + {x_margin + width - thick, y_margin + thick, thick, + width - 2 * thick}, + {x_margin, y_margin + width - thick, width, thick}}); + + pixman_image_unref(src); +} + +static void render_csd_button_maximize(struct terminal *term, + struct buffer *buf) { + if (term->window->is_maximized) + render_csd_button_maximize_maximized(term, buf); + else + render_csd_button_maximize_window(term, buf); +} + +static void render_csd_button_close(struct terminal *term, struct buffer *buf) { + pixman_color_t color = get_csd_button_fg_color(term); + pixman_image_t *src = pixman_image_create_solid_fill(&color); + + const int max_height = buf->height / 3; + const int max_width = buf->width / 3; + + int width = min(max_height, max_width); + int thick = min(width / 2, 1 * term->scale); + const int x_margin = (buf->width - width) / 2; + const int y_margin = (buf->height - width) / 2; + + xassert(x_margin + width - thick >= 0); + xassert(width - 2 * thick >= 0); + xassert(y_margin + width - thick >= 0); + + pixman_triangle_t tri[4] = { + { + .p1 = + { + .x = pixman_int_to_fixed(x_margin), + .y = pixman_int_to_fixed(y_margin + thick), + }, + .p2 = + { + .x = pixman_int_to_fixed(x_margin + width - thick), + .y = pixman_int_to_fixed(y_margin + width), + }, + .p3 = + { + .x = pixman_int_to_fixed(x_margin + thick), + .y = pixman_int_to_fixed(y_margin), + }, + }, + + { + .p1 = + { + .x = pixman_int_to_fixed(x_margin + width), + .y = pixman_int_to_fixed(y_margin + width - thick), + }, + .p2 = + { + .x = pixman_int_to_fixed(x_margin + thick), + .y = pixman_int_to_fixed(y_margin), + }, + .p3 = + { + .x = pixman_int_to_fixed(x_margin + width - thick), + .y = pixman_int_to_fixed(y_margin + width), + }, + }, + + { + .p1 = + { + .x = pixman_int_to_fixed(x_margin), + .y = pixman_int_to_fixed(y_margin + width - thick), + }, + .p2 = + { + .x = pixman_int_to_fixed(x_margin + width), + .y = pixman_int_to_fixed(y_margin + thick), + }, + .p3 = + { + .x = pixman_int_to_fixed(x_margin + thick), + .y = pixman_int_to_fixed(y_margin + width), + }, + }, + + { + .p1 = + { + .x = pixman_int_to_fixed(x_margin + width), + .y = pixman_int_to_fixed(y_margin + thick), + }, + .p2 = + { + .x = pixman_int_to_fixed(x_margin), + .y = pixman_int_to_fixed(y_margin + width - thick), + }, + .p3 = + { + .x = pixman_int_to_fixed(x_margin + width - thick), + .y = pixman_int_to_fixed(y_margin), + }, + }, + }; + + pixman_composite_triangles(PIXMAN_OP_OVER, src, buf->pix[0], PIXMAN_a1, 0, 0, + 0, 0, 4, tri); + + pixman_image_unref(src); +} + +static bool any_pointer_is_on_button(const struct terminal *term, + enum csd_surface csd_surface) { + if (unlikely(tll_length(term->wl->seats) == 0)) + return false; + + tll_foreach(term->wl->seats, it) { + const struct seat *seat = &it->item; + + if (seat->mouse.x < 0) + continue; + if (seat->mouse.y < 0) + continue; + + struct csd_data info = get_csd_data(term, csd_surface); + if (seat->mouse.x > info.width) + continue; + + if (seat->mouse.y > info.height) + continue; + return true; + } + + return false; +} + +static void render_csd_button(struct terminal *term, enum csd_surface surf_idx, + const struct csd_data *info, struct buffer *buf) { + xassert(term->window->csd_mode == CSD_YES); + xassert(surf_idx >= CSD_SURF_MINIMIZE && surf_idx <= CSD_SURF_CLOSE); + + struct wayl_surface *surf = &term->window->csd.surface[surf_idx].surface; + + if (info->width == 0 || info->height == 0) + return; + + uint32_t _color; + uint16_t alpha = 0xffff; + bool is_active = false; + bool is_set = false; + const uint32_t *conf_color = NULL; + + switch (surf_idx) { + case CSD_SURF_MINIMIZE: + _color = term->conf->colors_dark.table[4]; /* blue */ + is_set = term->conf->csd.color.minimize_set; + conf_color = &term->conf->csd.color.minimize; + is_active = term->active_surface == TERM_SURF_BUTTON_MINIMIZE && + any_pointer_is_on_button(term, CSD_SURF_MINIMIZE); + break; + + case CSD_SURF_MAXIMIZE: + _color = term->conf->colors_dark.table[2]; /* green */ + is_set = term->conf->csd.color.maximize_set; + conf_color = &term->conf->csd.color.maximize; + is_active = term->active_surface == TERM_SURF_BUTTON_MAXIMIZE && + any_pointer_is_on_button(term, CSD_SURF_MAXIMIZE); + break; + + case CSD_SURF_CLOSE: + _color = term->conf->colors_dark.table[1]; /* red */ + is_set = term->conf->csd.color.close_set; + conf_color = &term->conf->csd.color.quit; + is_active = term->active_surface == TERM_SURF_BUTTON_CLOSE && + any_pointer_is_on_button(term, CSD_SURF_CLOSE); + break; + + default: + BUG("unhandled surface type: %u", (unsigned)surf_idx); + break; + } + + if (is_active) { + if (is_set) { + _color = *conf_color; + alpha = _color >> 24 | (_color >> 24 << 8); + } + } else { + _color = 0; + alpha = 0; + } + + if (!term->visual_focus) + _color = color_dim(term, _color); + + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + pixman_color_t color = + color_hex_to_pixman_with_alpha(_color, alpha, gamma_correct); + render_csd_part(term, surf->surf, buf, info->width, info->height, &color); + + switch (surf_idx) { + case CSD_SURF_MINIMIZE: + render_csd_button_minimize(term, buf); + break; + case CSD_SURF_MAXIMIZE: + render_csd_button_maximize(term, buf); + break; + case CSD_SURF_CLOSE: + render_csd_button_close(term, buf); + break; + + default: + BUG("unhandled surface type: %u", (unsigned)surf_idx); + break; + } + + csd_commit(term, surf, buf); +} + +static void render_csd(struct terminal *term) { + xassert(term->window->csd_mode == CSD_YES); + + if (term->window->is_fullscreen) + return; + + const float scale = term->scale; + struct csd_data infos[CSD_SURF_COUNT]; + int widths[CSD_SURF_COUNT]; + int heights[CSD_SURF_COUNT]; + + for (size_t i = 0; i < CSD_SURF_COUNT; i++) { + infos[i] = get_csd_data(term, i); + const int x = infos[i].x; + const int y = infos[i].y; + const int width = infos[i].width; + const int height = infos[i].height; + + struct wl_surface *surf = term->window->csd.surface[i].surface.surf; + struct wl_subsurface *sub = term->window->csd.surface[i].sub; + + xassert(surf != NULL); + xassert(sub != NULL); + + if (width == 0 || height == 0) { + widths[i] = heights[i] = 0; + wl_subsurface_set_position(sub, 0, 0); + wl_surface_attach(surf, NULL, 0, 0); + wl_surface_commit(surf); + continue; + } + + widths[i] = width; + heights[i] = height; + wl_subsurface_set_position(sub, roundf(x / scale), roundf(y / scale)); + } + + struct buffer *bufs[CSD_SURF_COUNT]; + shm_get_many(term->render.chains.csd, CSD_SURF_COUNT, widths, heights, bufs); + + for (size_t i = CSD_SURF_LEFT; i <= CSD_SURF_BOTTOM; i++) + render_csd_border(term, i, &infos[i], bufs[i]); + for (size_t i = CSD_SURF_MINIMIZE; i <= CSD_SURF_CLOSE; i++) + render_csd_button(term, i, &infos[i], bufs[i]); + render_csd_title(term, &infos[CSD_SURF_TITLE], bufs[CSD_SURF_TITLE]); +} + +static void render_scrollback_position(struct terminal *term) { + if (term->conf->scrollback.indicator.position == + SCROLLBACK_INDICATOR_POSITION_NONE) + return; + + struct wl_window *win = term->window; + + if (term->grid->view == term->grid->offset) { + if (win->scrollback_indicator.surface.surf != NULL) + wayl_win_subsurface_destroy(&win->scrollback_indicator); + return; + } + + if (win->scrollback_indicator.surface.surf == NULL) { + if (!wayl_win_subsurface_new(win, &win->scrollback_indicator, false)) { + LOG_ERR("failed to create scrollback indicator surface"); + return; + } + } + + xassert(win->scrollback_indicator.surface.surf != NULL); + xassert(win->scrollback_indicator.sub != NULL); + + /* Find absolute row number of the scrollback start */ + int scrollback_start = term->grid->offset + term->rows; + int empty_rows = 0; + while (term->grid->rows[scrollback_start & (term->grid->num_rows - 1)] == + NULL) { + scrollback_start++; + empty_rows++; + } + + /* Rebase viewport against scrollback start (so that 0 is at + * the beginning of the scrollback) */ + int rebased_view = term->grid->view - scrollback_start + term->grid->num_rows; + rebased_view &= term->grid->num_rows - 1; + + /* How much of the scrollback is actually used? */ + int populated_rows = term->grid->num_rows - empty_rows; + xassert(populated_rows > 0); + xassert(populated_rows <= term->grid->num_rows); + + /* + * How far down in the scrollback we are. + * + * 0% -> at the beginning of the scrollback + * 100% -> at the bottom, i.e. where new lines are inserted + */ + double percent = rebased_view + term->rows == populated_rows + ? 1.0 + : (double)rebased_view / (populated_rows - term->rows); + + char32_t _text[64]; + const char32_t *text = _text; + int cell_count = 0; + + /* *What* to render */ + switch (term->conf->scrollback.indicator.format) { + case SCROLLBACK_INDICATOR_FORMAT_PERCENTAGE: { + char percent_str[8]; + snprintf(percent_str, sizeof(percent_str), "%u%%", (int)(100 * percent)); + mbstoc32(_text, percent_str, ALEN(_text)); + cell_count = 3; + break; + } + + case SCROLLBACK_INDICATOR_FORMAT_LINENO: { + char lineno_str[64]; + snprintf(lineno_str, sizeof(lineno_str), "%d", rebased_view + 1); + mbstoc32(_text, lineno_str, ALEN(_text)); + cell_count = (int)ceilf(log10f(term->grid->num_rows)); + break; + } + + case SCROLLBACK_INDICATOR_FORMAT_TEXT: + text = term->conf->scrollback.indicator.text; + cell_count = c32len(text); + break; + } + + const float scale = term->scale; + const int margin = (int)roundf(3. * scale); + + int width = margin + cell_count * term->cell_width + margin; + int height = margin + term->cell_height + margin; + + width = roundf(scale * ceilf(width / scale)); + height = roundf(scale * ceilf(height / scale)); + + /* *Where* to render - parent relative coordinates */ + int surf_top = 0; + switch (term->conf->scrollback.indicator.position) { + case SCROLLBACK_INDICATOR_POSITION_NONE: + BUG("Invalid scrollback indicator position type"); + return; + + case SCROLLBACK_INDICATOR_POSITION_FIXED: + surf_top = term->cell_height - margin; + break; + + case SCROLLBACK_INDICATOR_POSITION_RELATIVE: { + int lines = term->rows - 2; /* Avoid using first and last rows */ + if (term->is_searching) { + /* Make sure we don't collide with the scrollback search box */ + lines--; + } + + lines = max(lines, 0); + + int pixels = max(lines * term->cell_height - height + 2 * margin, 0); + surf_top = term->cell_height - margin + (int)(percent * pixels); + break; + } + } + + int x = term->width - margin - width; + int y = term->margins.top + surf_top; + + x = roundf(scale * ceilf(x / scale)); + y = roundf(scale * ceilf(y / scale)); + + if (y + height > term->height) { + wl_surface_attach(win->scrollback_indicator.surface.surf, NULL, 0, 0); + wl_surface_commit(win->scrollback_indicator.surface.surf); + return; + } + + struct buffer_chain *chain = term->render.chains.scrollback_indicator; + struct buffer *buf = shm_get_buffer(chain, width, height); + + wl_subsurface_set_position(win->scrollback_indicator.sub, roundf(x / scale), + roundf(y / scale)); + + uint32_t fg = term->colors.table[0]; + uint32_t bg = term->colors.table[8 + 4]; + if (term->conf->colors_dark.use_custom.scrollback_indicator) { + fg = term->conf->colors_dark.scrollback_indicator.fg; + bg = term->conf->colors_dark.scrollback_indicator.bg; + } + + render_osd(term, &win->scrollback_indicator, term->fonts[0], buf, text, fg, + 0xffu << 24 | bg, + width - margin - c32len(text) * term->cell_width); +} + +static void render_render_timer(struct terminal *term, + struct timespec render_time) { + struct wl_window *win = term->window; + + char usecs_str[256]; + double usecs = render_time.tv_sec * 1000000 + render_time.tv_nsec / 1000.0; + snprintf(usecs_str, sizeof(usecs_str), "%.2f µs", usecs); + + char32_t text[256]; + mbstoc32(text, usecs_str, ALEN(text)); + + const float scale = term->scale; + const int cell_count = c32len(text); + const int margin = (int)roundf(3. * scale); + + int width = margin + cell_count * term->cell_width + margin; + int height = margin + term->cell_height + margin; + + width = roundf(scale * ceilf(width / scale)); + height = roundf(scale * ceilf(height / scale)); + + struct buffer_chain *chain = term->render.chains.render_timer; + struct buffer *buf = shm_get_buffer(chain, width, height); + + wl_subsurface_set_position( + win->render_timer.sub, roundf(margin / scale), + roundf((term->margins.top + term->cell_height - margin) / scale)); + + render_osd(term, &win->render_timer, term->fonts[0], buf, text, + term->colors.table[0], 0xffu << 24 | term->colors.table[8 + 1], + margin); +} + +static void frame_callback(void *data, struct wl_callback *wl_callback, + uint32_t callback_data); static const struct wl_callback_listener frame_listener = { .done = &frame_callback, }; -static void -force_full_repaint(struct terminal *term, struct buffer *buf) -{ - tll_free(term->grid->scroll_damage); - render_margin(term, buf, 0, term->rows, true); - term_damage_view(term); +static void force_full_repaint(struct terminal *term, struct buffer *buf) { + tll_free(term->grid->scroll_damage); + render_margin(term, buf, 0, term->rows, true); + term_damage_view(term); } -static void -reapply_old_damage(struct terminal *term, struct buffer *new, struct buffer *old) -{ - if (new->age > 1) { - memcpy(new->data, old->data, new->height * new->stride); - return; +static void reapply_old_damage(struct terminal *term, struct buffer *new, + struct buffer *old) { + if (new->age > 1) { + memcpy(new->data, old->data, new->height * new->stride); + return; + } + + pixman_region32_t dirty; + pixman_region32_init(&dirty); + + /* + * Figure out current frame's damage region + * + * If current frame doesn't have any scroll damage, we can simply + * subtract this frame's damage from the last frame's damage. That + * way, we don't have to copy areas from the old frame that'll + * just get overwritten by current frame. + * + * Note that this is row based. A "half damaged" row is not + * excluded. I.e. the entire row will be copied from the old frame + * to the new, and then when actually rendering the new frame, the + * updated cells will overwrite parts of the copied row. + * + * Since we're scanning the entire viewport anyway, we also track + * whether *all* cells are to be updated. In this case, just force + * a full re-rendering, and don't copy anything from the old + * frame. + */ + bool full_repaint_needed = true; + + for (int r = 0; r < term->rows; r++) { + const struct row *row = grid_row_in_view(term->grid, r); + + if (!row->dirty) { + full_repaint_needed = false; + continue; } - pixman_region32_t dirty; - pixman_region32_init(&dirty); + bool row_all_dirty = true; + for (int c = 0; c < term->cols; c++) { + if (row->cells[c].attrs.clean) { + row_all_dirty = false; + full_repaint_needed = false; + break; + } + } + if (row_all_dirty) { + pixman_region32_union_rect(&dirty, &dirty, term->margins.left, + term->margins.top + r * term->cell_height, + term->width - term->margins.left - + term->margins.right, + term->cell_height); + } + } + + if (full_repaint_needed) { + force_full_repaint(term, new); + return; + } + + /* + * TODO: re-apply last frame's scroll damage + * + * We used to do this, but it turned out to be buggy. If we decide + * to re-add it, this is where to do it. Note that we'd also have + * to remove the updates to buf->dirty from grid_render_scroll() + * and grid_render_scroll_reverse(). + */ + + if (tll_length(term->grid->scroll_damage) == 0) { /* - * Figure out current frame's damage region + * We can only subtract current frame's damage from the old + * frame's if we don't have any scroll damage. * - * If current frame doesn't have any scroll damage, we can simply - * subtract this frame's damage from the last frame's damage. That - * way, we don't have to copy areas from the old frame that'll - * just get overwritten by current frame. - * - * Note that this is row based. A "half damaged" row is not - * excluded. I.e. the entire row will be copied from the old frame - * to the new, and then when actually rendering the new frame, the - * updated cells will overwrite parts of the copied row. - * - * Since we're scanning the entire viewport anyway, we also track - * whether *all* cells are to be updated. In this case, just force - * a full re-rendering, and don't copy anything from the old - * frame. + * If we do have scroll damage, the damage region we + * calculated above is not (yet) valid - we need to apply the + * current frame's scroll damage *first*. This is done later, + * when rendering the frame. */ - bool full_repaint_needed = true; + pixman_region32_subtract(&dirty, &old->dirty[0], &dirty); + pixman_image_set_clip_region32(new->pix[0], &dirty); + } else { + /* Copy *all* of last frame's damaged areas */ + pixman_image_set_clip_region32(new->pix[0], &old->dirty[0]); + } - for (int r = 0; r < term->rows; r++) { - const struct row *row = grid_row_in_view(term->grid, r); + pixman_image_composite32(PIXMAN_OP_SRC, old->pix[0], NULL, new->pix[0], 0, 0, + 0, 0, 0, 0, term->width, term->height); - if (!row->dirty) { - full_repaint_needed = false; - continue; - } - - bool row_all_dirty = true; - for (int c = 0; c < term->cols; c++) { - if (row->cells[c].attrs.clean) { - row_all_dirty = false; - full_repaint_needed = false; - break; - } - } - - if (row_all_dirty) { - pixman_region32_union_rect( - &dirty, &dirty, - term->margins.left, - term->margins.top + r * term->cell_height, - term->width - term->margins.left - term->margins.right, - term->cell_height); - } - } - - if (full_repaint_needed) { - force_full_repaint(term, new); - return; - } - - /* - * TODO: re-apply last frame's scroll damage - * - * We used to do this, but it turned out to be buggy. If we decide - * to re-add it, this is where to do it. Note that we'd also have - * to remove the updates to buf->dirty from grid_render_scroll() - * and grid_render_scroll_reverse(). - */ - - if (tll_length(term->grid->scroll_damage) == 0) { - /* - * We can only subtract current frame's damage from the old - * frame's if we don't have any scroll damage. - * - * If we do have scroll damage, the damage region we - * calculated above is not (yet) valid - we need to apply the - * current frame's scroll damage *first*. This is done later, - * when rendering the frame. - */ - pixman_region32_subtract(&dirty, &old->dirty[0], &dirty); - pixman_image_set_clip_region32(new->pix[0], &dirty); - } else { - /* Copy *all* of last frame's damaged areas */ - pixman_image_set_clip_region32(new->pix[0], &old->dirty[0]); - } - - pixman_image_composite32( - PIXMAN_OP_SRC, old->pix[0], NULL, new->pix[0], - 0, 0, 0, 0, 0, 0, term->width, term->height); - - pixman_image_set_clip_region32(new->pix[0], NULL); - pixman_region32_fini(&dirty); + pixman_image_set_clip_region32(new->pix[0], NULL); + pixman_region32_fini(&dirty); } -static void -dirty_old_cursor(struct terminal *term) -{ - if (term->render.last_cursor.row != NULL && !term->render.last_cursor.hidden) { - struct row *row = term->render.last_cursor.row; - struct cell *cell = &row->cells[term->render.last_cursor.col]; - cell->attrs.clean = 0; - row->dirty = true; - } - - /* Remember current cursor position, for the next frame */ - term->render.last_cursor.row = grid_row(term->grid, term->grid->cursor.point.row); - term->render.last_cursor.col = term->grid->cursor.point.col; - term->render.last_cursor.hidden = term->hide_cursor; -} - -static void -dirty_cursor(struct terminal *term) -{ - if (term->hide_cursor) - return; - - const struct coord *cursor = &term->grid->cursor.point; - - struct row *row = grid_row(term->grid, cursor->row); - struct cell *cell = &row->cells[cursor->col]; +static void dirty_old_cursor(struct terminal *term) { + if (term->render.last_cursor.row != NULL && + !term->render.last_cursor.hidden) { + struct row *row = term->render.last_cursor.row; + struct cell *cell = &row->cells[term->render.last_cursor.col]; cell->attrs.clean = 0; row->dirty = true; + } + + /* Remember current cursor position, for the next frame */ + term->render.last_cursor.row = + grid_row(term->grid, term->grid->cursor.point.row); + term->render.last_cursor.col = term->grid->cursor.point.col; + term->render.last_cursor.hidden = term->hide_cursor; } -static void -grid_render(struct terminal *term) -{ - if (term->shutdown.in_progress) - return; +static void dirty_cursor(struct terminal *term) { + if (term->hide_cursor) + return; - struct timespec start_time; - struct timespec start_wait_preapply = {0}, stop_wait_preapply = {0}; - struct timespec start_double_buffering = {0}, stop_double_buffering = {0}; + const struct coord *cursor = &term->grid->cursor.point; - /* Might be a thread doing pre-applied damage */ - if (unlikely(term->render.preapply_last_frame_damage && - term->render.workers.preapplied_damage.buf != NULL)) - { - clock_gettime(CLOCK_MONOTONIC, &start_wait_preapply); - render_wait_for_preapply_damage(term); - clock_gettime(CLOCK_MONOTONIC, &stop_wait_preapply); - } + struct row *row = grid_row(term->grid, cursor->row); + struct cell *cell = &row->cells[cursor->col]; + cell->attrs.clean = 0; + row->dirty = true; +} - if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) - clock_gettime(CLOCK_MONOTONIC, &start_time); +static void grid_render(struct terminal *term) { + if (term->shutdown.in_progress) + return; - xassert(term->width > 0); - xassert(term->height > 0); + struct timespec start_time; + struct timespec start_wait_preapply = {0}, stop_wait_preapply = {0}; + struct timespec start_double_buffering = {0}, stop_double_buffering = {0}; - struct buffer_chain *chain = term->render.chains.grid; - struct buffer *buf = shm_get_buffer(chain, term->width, term->height); + /* Might be a thread doing pre-applied damage */ + if (unlikely(term->render.preapply_last_frame_damage && + term->render.workers.preapplied_damage.buf != NULL)) { + clock_gettime(CLOCK_MONOTONIC, &start_wait_preapply); + render_wait_for_preapply_damage(term); + clock_gettime(CLOCK_MONOTONIC, &stop_wait_preapply); + } - /* Dirty old and current cursor cell, to ensure they're repainted */ - dirty_old_cursor(term); - dirty_cursor(term); + if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) + clock_gettime(CLOCK_MONOTONIC, &start_time); + xassert(term->width > 0); + xassert(term->height > 0); + + struct buffer_chain *chain = term->render.chains.grid; + struct buffer *buf = shm_get_buffer(chain, term->width, term->height); + + /* Dirty old and current cursor cell, to ensure they're repainted */ + dirty_old_cursor(term); + dirty_cursor(term); + + LOG_DBG("buffer age: %u (%p)", buf->age, (void *)buf); + + if (term->render.last_buf == NULL || + term->render.last_buf->width != buf->width || + term->render.last_buf->height != buf->height || term->render.margins) { + force_full_repaint(term, buf); + } + + else if (buf->age > 0) { LOG_DBG("buffer age: %u (%p)", buf->age, (void *)buf); - if (term->render.last_buf == NULL || - term->render.last_buf->width != buf->width || - term->render.last_buf->height != buf->height || - term->render.margins) - { - force_full_repaint(term, buf); + xassert(term->render.last_buf != NULL); + xassert(term->render.last_buf != buf); + xassert(term->render.last_buf->width == buf->width); + xassert(term->render.last_buf->height == buf->height); + + if (++term->render.frames_since_last_immediate_release > 10) { + static bool have_warned = false; + + if (!term->render.preapply_last_frame_damage && + term->conf->tweak.preapply_damage && term->render.workers.count > 0) { + LOG_INFO("enabling pre-applied frame damage"); + term->render.preapply_last_frame_damage = true; + } else if (!have_warned && !term->render.preapply_last_frame_damage) { + LOG_WARN("compositor is not releasing buffers immediately; " + "expect lower rendering performance"); + have_warned = true; + } } - else if (buf->age > 0) { - LOG_DBG("buffer age: %u (%p)", buf->age, (void *)buf); + clock_gettime(CLOCK_MONOTONIC, &start_double_buffering); + reapply_old_damage(term, buf, term->render.last_buf); + clock_gettime(CLOCK_MONOTONIC, &stop_double_buffering); + } else if (!term->render.preapply_last_frame_damage) { + term->render.frames_since_last_immediate_release = 0; + } - xassert(term->render.last_buf != NULL); - xassert(term->render.last_buf != buf); - xassert(term->render.last_buf->width == buf->width); - xassert(term->render.last_buf->height == buf->height); + if (term->render.last_buf != NULL) { + shm_unref(term->render.last_buf); + term->render.last_buf = NULL; + } - if (++term->render.frames_since_last_immediate_release > 10) { - static bool have_warned = false; + term->render.last_buf = buf; + shm_addref(buf); + buf->age = 0; - if (!term->render.preapply_last_frame_damage && - term->conf->tweak.preapply_damage && - term->render.workers.count > 0) - { - LOG_INFO("enabling pre-applied frame damage"); - term->render.preapply_last_frame_damage = true; - } else if (!have_warned && !term->render.preapply_last_frame_damage) { - LOG_WARN("compositor is not releasing buffers immediately; " - "expect lower rendering performance"); - have_warned = true; - } - } + tll_foreach(term->grid->scroll_damage, it) { + switch (it->item.type) { + case DAMAGE_SCROLL: + if (term->grid->view == term->grid->offset) + grid_render_scroll(term, buf, &it->item); + break; - clock_gettime(CLOCK_MONOTONIC, &start_double_buffering); - reapply_old_damage(term, buf, term->render.last_buf); - clock_gettime(CLOCK_MONOTONIC, &stop_double_buffering); - } else if (!term->render.preapply_last_frame_damage) { - term->render.frames_since_last_immediate_release = 0; + case DAMAGE_SCROLL_REVERSE: + if (term->grid->view == term->grid->offset) + grid_render_scroll_reverse(term, buf, &it->item); + break; + + case DAMAGE_SCROLL_IN_VIEW: + grid_render_scroll(term, buf, &it->item); + break; + + case DAMAGE_SCROLL_REVERSE_IN_VIEW: + grid_render_scroll_reverse(term, buf, &it->item); + break; } - if (term->render.last_buf != NULL) { - shm_unref(term->render.last_buf); - term->render.last_buf = NULL; - } + tll_remove(term->grid->scroll_damage, it); + } - term->render.last_buf = buf; - shm_addref(buf); - buf->age = 0; + /* + * Ensure selected cells have their 'selected' bit set. This is + * normally "automatically" true - the bit is set when the + * selection is made. + * + * However, if the cell is updated (printed to) while the + * selection is active, the 'selected' bit is cleared. Checking + * for this and re-setting the bit in term_print() is too + * expensive performance wise. + * + * Instead, we synchronize the selection bits here and now. This + * makes the performance impact linear to the number of selected + * cells rather than to the number of updated cells. + * + * (note that selection_dirty_cells() will not set the dirty flag + * on cells where the 'selected' bit is already set) + */ + selection_dirty_cells(term); + /* Translate offset-relative row to view-relative, unless cursor + * is hidden, then we just set it to -1 */ + struct coord cursor = {-1, -1}; + if (!term->hide_cursor) { + cursor = term->grid->cursor.point; + cursor.row += term->grid->offset; + cursor.row -= term->grid->view; + cursor.row &= term->grid->num_rows - 1; + } - tll_foreach(term->grid->scroll_damage, it) { - switch (it->item.type) { - case DAMAGE_SCROLL: - if (term->grid->view == term->grid->offset) - grid_render_scroll(term, buf, &it->item); - break; - - case DAMAGE_SCROLL_REVERSE: - if (term->grid->view == term->grid->offset) - grid_render_scroll_reverse(term, buf, &it->item); - break; - - case DAMAGE_SCROLL_IN_VIEW: - grid_render_scroll(term, buf, &it->item); - break; - - case DAMAGE_SCROLL_REVERSE_IN_VIEW: - grid_render_scroll_reverse(term, buf, &it->item); - break; - } - - tll_remove(term->grid->scroll_damage, it); - } - + if (term->conf->tweak.overflowing_glyphs) { /* - * Ensure selected cells have their 'selected' bit set. This is - * normally "automatically" true - the bit is set when the - * selection is made. + * Pre-pass to dirty cells affected by overflowing glyphs. * - * However, if the cell is updated (printed to) while the - * selection is active, the 'selected' bit is cleared. Checking - * for this and re-setting the bit in term_print() is too - * expensive performance wise. + * Given any two pair of cells where the first cell is + * overflowing into the second, *both* cells must be + * re-rendered if any one of them is dirty. * - * Instead, we synchronize the selection bits here and now. This - * makes the performance impact linear to the number of selected - * cells rather than to the number of updated cells. - * - * (note that selection_dirty_cells() will not set the dirty flag - * on cells where the 'selected' bit is already set) + * Thus, given a string of overflowing glyphs, with a single + * dirty cell in the middle, we need to re-render the entire + * string. */ - selection_dirty_cells(term); + for (int r = 0; r < term->rows; r++) { + struct row *row = grid_row_in_view(term->grid, r); - /* Translate offset-relative row to view-relative, unless cursor - * is hidden, then we just set it to -1 */ - struct coord cursor = {-1, -1}; - if (!term->hide_cursor) { - cursor = term->grid->cursor.point; - cursor.row += term->grid->offset; - cursor.row -= term->grid->view; - cursor.row &= term->grid->num_rows - 1; - } + if (!row->dirty) + continue; + + /* Loop row from left to right, looking for dirty cells */ + for (struct cell *cell = &row->cells[0]; cell < &row->cells[term->cols]; + cell++) { + if (cell->attrs.clean) + continue; - if (term->conf->tweak.overflowing_glyphs) { /* - * Pre-pass to dirty cells affected by overflowing glyphs. + * Cell is dirty, go back and dirty previous cells, if + * they are overflowing. * - * Given any two pair of cells where the first cell is - * overflowing into the second, *both* cells must be - * re-rendered if any one of them is dirty. + * As soon as we see a non-overflowing cell we can + * stop, since it isn't affecting the string of + * overflowing glyphs that follows it. * - * Thus, given a string of overflowing glyphs, with a single - * dirty cell in the middle, we need to re-render the entire - * string. + * As soon as we see a dirty cell, we can stop, since + * that means we've already handled it (remember the + * outer loop goes from left to right). */ - for (int r = 0; r < term->rows; r++) { - struct row *row = grid_row_in_view(term->grid, r); - - if (!row->dirty) - continue; - - /* Loop row from left to right, looking for dirty cells */ - for (struct cell *cell = &row->cells[0]; - cell < &row->cells[term->cols]; - cell++) - { - if (cell->attrs.clean) - continue; - - /* - * Cell is dirty, go back and dirty previous cells, if - * they are overflowing. - * - * As soon as we see a non-overflowing cell we can - * stop, since it isn't affecting the string of - * overflowing glyphs that follows it. - * - * As soon as we see a dirty cell, we can stop, since - * that means we've already handled it (remember the - * outer loop goes from left to right). - */ - for (struct cell *c = cell - 1; c >= &row->cells[0]; c--) { - if (c->attrs.confined) - break; - if (!c->attrs.clean) - break; - c->attrs.clean = false; - } - - /* - * Now move forward, dirtying all cells until we hit a - * non-overflowing cell. - * - * Note that the first non-overflowing cell must be - * re-rendered as well, but any cell *after* that is - * unaffected by the string of overflowing glyphs - * we're dealing with right now. - * - * For performance, this iterates the *outer* loop's - * cell pointer - no point in re-checking all these - * glyphs again, in the outer loop. - */ - for (; cell < &row->cells[term->cols]; cell++) { - cell->attrs.clean = false; - if (cell->attrs.confined) - break; - } - } + for (struct cell *c = cell - 1; c >= &row->cells[0]; c--) { + if (c->attrs.confined) + break; + if (!c->attrs.clean) + break; + c->attrs.clean = false; } + + /* + * Now move forward, dirtying all cells until we hit a + * non-overflowing cell. + * + * Note that the first non-overflowing cell must be + * re-rendered as well, but any cell *after* that is + * unaffected by the string of overflowing glyphs + * we're dealing with right now. + * + * For performance, this iterates the *outer* loop's + * cell pointer - no point in re-checking all these + * glyphs again, in the outer loop. + */ + for (; cell < &row->cells[term->cols]; cell++) { + cell->attrs.clean = false; + if (cell->attrs.confined) + break; + } + } } + } #if defined(_DEBUG) - for (int r = 0; r < term->rows; r++) { - const struct row *row = grid_row_in_view(term->grid, r); + for (int r = 0; r < term->rows; r++) { + const struct row *row = grid_row_in_view(term->grid, r); - if (row->dirty) { - bool all_clean = true; - for (int c = 0; c < term->cols; c++) { - if (!row->cells[c].attrs.clean) { - all_clean = false; - break; - } - } - if (all_clean) - BUG("row #%d is dirty, but all cells are marked as clean", r); - } else { - for (int c = 0; c < term->cols; c++) { - if (!row->cells[c].attrs.clean) - BUG("row #%d is clean, but cell #%d is dirty", r, c); - } + if (row->dirty) { + bool all_clean = true; + for (int c = 0; c < term->cols; c++) { + if (!row->cells[c].attrs.clean) { + all_clean = false; + break; } + } + if (all_clean) + BUG("row #%d is dirty, but all cells are marked as clean", r); + } else { + for (int c = 0; c < term->cols; c++) { + if (!row->cells[c].attrs.clean) + BUG("row #%d is clean, but cell #%d is dirty", r, c); + } } + } #endif - pixman_region32_t damage; - pixman_region32_init(&damage); + pixman_region32_t damage; + pixman_region32_init(&damage); - render_sixel_images(term, buf->pix[0], &damage, &cursor); + render_sixel_images(term, buf->pix[0], &damage, &cursor); + if (term->render.workers.count > 0) { + mtx_lock(&term->render.workers.lock); + term->render.workers.buf = buf; + for (size_t i = 0; i < term->render.workers.count; i++) + sem_post(&term->render.workers.start); - if (term->render.workers.count > 0) { - mtx_lock(&term->render.workers.lock); - term->render.workers.buf = buf; - for (size_t i = 0; i < term->render.workers.count; i++) - sem_post(&term->render.workers.start); + xassert(tll_length(term->render.workers.queue) == 0); + } - xassert(tll_length(term->render.workers.queue) == 0); + for (int r = 0; r < term->rows; r++) { + struct row *row = grid_row_in_view(term->grid, r); + + if (!row->dirty) + continue; + + row->dirty = false; + + if (term->render.workers.count > 0) + tll_push_back(term->render.workers.queue, r); + + else { + /* TODO: damage region */ + int cursor_col = cursor.row == r ? cursor.col : -1; + render_row(term, buf->pix[0], &damage, row, r, cursor_col); } + } - for (int r = 0; r < term->rows; r++) { - struct row *row = grid_row_in_view(term->grid, r); - - if (!row->dirty) - continue; - - row->dirty = false; - - if (term->render.workers.count > 0) - tll_push_back(term->render.workers.queue, r); - - else { - /* TODO: damage region */ - int cursor_col = cursor.row == r ? cursor.col : -1; - render_row(term, buf->pix[0], &damage, row, r, cursor_col); - } - } - - /* Signal workers the frame is done */ - if (term->render.workers.count > 0) { - for (size_t i = 0; i < term->render.workers.count; i++) - tll_push_back(term->render.workers.queue, -1); - mtx_unlock(&term->render.workers.lock); - - for (size_t i = 0; i < term->render.workers.count; i++) - sem_wait(&term->render.workers.done); - term->render.workers.buf = NULL; - } + /* Signal workers the frame is done */ + if (term->render.workers.count > 0) { + for (size_t i = 0; i < term->render.workers.count; i++) + tll_push_back(term->render.workers.queue, -1); + mtx_unlock(&term->render.workers.lock); for (size_t i = 0; i < term->render.workers.count; i++) - pixman_region32_union(&damage, &damage, &buf->dirty[i + 1]); + sem_wait(&term->render.workers.done); + term->render.workers.buf = NULL; + } - pixman_region32_union(&buf->dirty[0], &buf->dirty[0], &damage); + for (size_t i = 0; i < term->render.workers.count; i++) + pixman_region32_union(&damage, &damage, &buf->dirty[i + 1]); - { - int box_count = 0; - pixman_box32_t *boxes = pixman_region32_rectangles(&damage, &box_count); + pixman_region32_union(&buf->dirty[0], &buf->dirty[0], &damage); - for (size_t i = 0; i < box_count; i++) { - wl_surface_damage_buffer( - term->window->surface.surf, - boxes[i].x1, boxes[i].y1, - boxes[i].x2 - boxes[i].x1, boxes[i].y2 - boxes[i].y1); - } + { + int box_count = 0; + pixman_box32_t *boxes = pixman_region32_rectangles(&damage, &box_count); + + for (size_t i = 0; i < box_count; i++) { + wl_surface_damage_buffer(term->window->surface.surf, boxes[i].x1, + boxes[i].y1, boxes[i].x2 - boxes[i].x1, + boxes[i].y2 - boxes[i].y1); + } + } + + pixman_region32_fini(&damage); + + render_overlay(term); + render_ime_preedit(term, buf); + render_scrollback_position(term); + + if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) { + struct timespec end_time; + clock_gettime(CLOCK_MONOTONIC, &end_time); + + struct timespec wait_time; + timespec_sub(&stop_wait_preapply, &start_wait_preapply, &wait_time); + + struct timespec render_time; + timespec_sub(&end_time, &start_time, &render_time); + + struct timespec double_buffering_time; + timespec_sub(&stop_double_buffering, &start_double_buffering, + &double_buffering_time); + + struct timespec preapply_damage; + timespec_sub(&term->render.workers.preapplied_damage.stop, + &term->render.workers.preapplied_damage.start, + &preapply_damage); + + struct timespec total_render_time; + timespec_add(&render_time, &double_buffering_time, &total_render_time); + timespec_add(&wait_time, &total_render_time, &total_render_time); + + switch (term->conf->tweak.render_timer) { + case RENDER_TIMER_LOG: + case RENDER_TIMER_BOTH: + LOG_INFO("frame rendered in %lds %9ldns " + "(%lds %9ldns wait, %lds %9ldns rendering, %lds %9ldns double " + "buffering) not included: %lds %ldns pre-apply damage", + (long)total_render_time.tv_sec, total_render_time.tv_nsec, + (long)wait_time.tv_sec, wait_time.tv_nsec, + (long)render_time.tv_sec, render_time.tv_nsec, + (long)double_buffering_time.tv_sec, + double_buffering_time.tv_nsec, (long)preapply_damage.tv_sec, + preapply_damage.tv_nsec); + break; + + case RENDER_TIMER_OSD: + case RENDER_TIMER_NONE: + break; } - pixman_region32_fini(&damage); + switch (term->conf->tweak.render_timer) { + case RENDER_TIMER_OSD: + case RENDER_TIMER_BOTH: + render_render_timer(term, total_render_time); + break; - render_overlay(term); - render_ime_preedit(term, buf); - render_scrollback_position(term); - - if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) { - struct timespec end_time; - clock_gettime(CLOCK_MONOTONIC, &end_time); - - struct timespec wait_time; - timespec_sub(&stop_wait_preapply, &start_wait_preapply, &wait_time); - - struct timespec render_time; - timespec_sub(&end_time, &start_time, &render_time); - - struct timespec double_buffering_time; - timespec_sub(&stop_double_buffering, &start_double_buffering, &double_buffering_time); - - struct timespec preapply_damage; - timespec_sub(&term->render.workers.preapplied_damage.stop, - &term->render.workers.preapplied_damage.start, - &preapply_damage); - - struct timespec total_render_time; - timespec_add(&render_time, &double_buffering_time, &total_render_time); - timespec_add(&wait_time, &total_render_time, &total_render_time); - - switch (term->conf->tweak.render_timer) { - case RENDER_TIMER_LOG: - case RENDER_TIMER_BOTH: - LOG_INFO( - "frame rendered in %lds %9ldns " - "(%lds %9ldns wait, %lds %9ldns rendering, %lds %9ldns double buffering) not included: %lds %ldns pre-apply damage", - (long)total_render_time.tv_sec, - total_render_time.tv_nsec, - (long)wait_time.tv_sec, - wait_time.tv_nsec, - (long)render_time.tv_sec, - render_time.tv_nsec, - (long)double_buffering_time.tv_sec, - double_buffering_time.tv_nsec, - (long)preapply_damage.tv_sec, - preapply_damage.tv_nsec); - break; - - case RENDER_TIMER_OSD: - case RENDER_TIMER_NONE: - break; - } - - switch (term->conf->tweak.render_timer) { - case RENDER_TIMER_OSD: - case RENDER_TIMER_BOTH: - render_render_timer(term, total_render_time); - break; - - case RENDER_TIMER_LOG: - case RENDER_TIMER_NONE: - break; - } + case RENDER_TIMER_LOG: + case RENDER_TIMER_NONE: + break; } + } - xassert(term->grid->offset >= 0 && term->grid->offset < term->grid->num_rows); - xassert(term->grid->view >= 0 && term->grid->view < term->grid->num_rows); + xassert(term->grid->offset >= 0 && term->grid->offset < term->grid->num_rows); + xassert(term->grid->view >= 0 && term->grid->view < term->grid->num_rows); - xassert(term->window->frame_callback == NULL); - term->window->frame_callback = wl_surface_frame(term->window->surface.surf); - wl_callback_add_listener(term->window->frame_callback, &frame_listener, term); + xassert(term->window->frame_callback == NULL); + term->window->frame_callback = wl_surface_frame(term->window->surface.surf); + wl_callback_add_listener(term->window->frame_callback, &frame_listener, term); - wayl_win_scale(term->window, buf); + wayl_win_scale(term->window, buf); - if (term->wl->presentation != NULL && term->conf->presentation_timings) { - struct timespec commit_time; - clock_gettime(term->wl->presentation_clock_id, &commit_time); + if (term->wl->presentation != NULL && term->conf->presentation_timings) { + struct timespec commit_time; + clock_gettime(term->wl->presentation_clock_id, &commit_time); - struct wp_presentation_feedback *feedback = wp_presentation_feedback( - term->wl->presentation, term->window->surface.surf); + struct wp_presentation_feedback *feedback = wp_presentation_feedback( + term->wl->presentation, term->window->surface.surf); - if (feedback == NULL) { - LOG_WARN("failed to create presentation feedback"); - } else { - struct presentation_context *ctx = xmalloc(sizeof(*ctx)); - *ctx = (struct presentation_context){ - .term = term, - .input.tv_sec = term->render.input_time.tv_sec, - .input.tv_usec = term->render.input_time.tv_nsec / 1000, - .commit.tv_sec = commit_time.tv_sec, - .commit.tv_usec = commit_time.tv_nsec / 1000, - }; + if (feedback == NULL) { + LOG_WARN("failed to create presentation feedback"); + } else { + struct presentation_context *ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct presentation_context){ + .term = term, + .input.tv_sec = term->render.input_time.tv_sec, + .input.tv_usec = term->render.input_time.tv_nsec / 1000, + .commit.tv_sec = commit_time.tv_sec, + .commit.tv_usec = commit_time.tv_nsec / 1000, + }; - wp_presentation_feedback_add_listener( - feedback, &presentation_feedback_listener, ctx); + wp_presentation_feedback_add_listener( + feedback, &presentation_feedback_listener, ctx); - term->render.input_time.tv_sec = 0; - term->render.input_time.tv_nsec = 0; - } + term->render.input_time.tv_sec = 0; + term->render.input_time.tv_nsec = 0; } + } - if (term->conf->tweak.damage_whole_window) { - wl_surface_damage_buffer( - term->window->surface.surf, 0, 0, INT32_MAX, INT32_MAX); - } + if (term->conf->tweak.damage_whole_window) { + wl_surface_damage_buffer(term->window->surface.surf, 0, 0, INT32_MAX, + INT32_MAX); + } - wl_surface_attach(term->window->surface.surf, buf->wl_buf, 0, 0); - wl_surface_commit(term->window->surface.surf); + wl_surface_attach(term->window->surface.surf, buf->wl_buf, 0, 0); + wl_surface_commit(term->window->surface.surf); } -static void -render_search_box(struct terminal *term) -{ - xassert(term->window->search.sub != NULL); +static void render_search_box(struct terminal *term) { + xassert(term->window->search.sub != NULL); - /* - * We treat the search box pretty much like a row of cells. That - * is, a glyph is either 1 or 2 (or more) "cells" wide. - * - * The search 'length', and 'cursor' (position) is in - * *characters*, not cells. This means we need to translate from - * character count to cell count when calculating the length of - * the search box, where in the search string we should start - * rendering etc. - */ + /* + * We treat the search box pretty much like a row of cells. That + * is, a glyph is either 1 or 2 (or more) "cells" wide. + * + * The search 'length', and 'cursor' (position) is in + * *characters*, not cells. This means we need to translate from + * character count to cell count when calculating the length of + * the search box, where in the search string we should start + * rendering etc. + */ #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED - /* TODO: do we want to/need to handle multi-seat? */ - struct seat *ime_seat = NULL; - tll_foreach(term->wl->seats, it) { - if (it->item.kbd_focus == term) { - ime_seat = &it->item; - break; - } + /* TODO: do we want to/need to handle multi-seat? */ + struct seat *ime_seat = NULL; + tll_foreach(term->wl->seats, it) { + if (it->item.kbd_focus == term) { + ime_seat = &it->item; + break; } + } - size_t text_len = term->search.len; - if (ime_seat != NULL && ime_seat->ime.preedit.text != NULL) - text_len += c32len(ime_seat->ime.preedit.text); + size_t text_len = term->search.len; + if (ime_seat != NULL && ime_seat->ime.preedit.text != NULL) + text_len += c32len(ime_seat->ime.preedit.text); - char32_t *text = xmalloc((text_len + 1) * sizeof(char32_t)); - text[0] = U'\0'; + char32_t *text = xmalloc((text_len + 1) * sizeof(char32_t)); + text[0] = U'\0'; - /* Copy everything up to the cursor */ - c32ncpy(text, term->search.buf, term->search.cursor); - text[term->search.cursor] = U'\0'; + /* Copy everything up to the cursor */ + c32ncpy(text, term->search.buf, term->search.cursor); + text[term->search.cursor] = U'\0'; - /* Insert pre-edit text at cursor */ - if (ime_seat != NULL && ime_seat->ime.preedit.text != NULL) - c32cat(text, ime_seat->ime.preedit.text); + /* Insert pre-edit text at cursor */ + if (ime_seat != NULL && ime_seat->ime.preedit.text != NULL) + c32cat(text, ime_seat->ime.preedit.text); - /* And finally everything after the cursor */ - c32ncat(text, &term->search.buf[term->search.cursor], - term->search.len - term->search.cursor); + /* And finally everything after the cursor */ + c32ncat(text, &term->search.buf[term->search.cursor], + term->search.len - term->search.cursor); #else - const char32_t *text = term->search.buf; - const size_t text_len = term->search.len; + const char32_t *text = term->search.buf; + const size_t text_len = term->search.len; #endif - /* Calculate the width of each character */ - int widths[text_len + 1]; + /* Password modes: replace every codepoint with a mask glyph for display. */ + char32_t *masked_text = NULL; + char32_t *ime_alloc = NULL; +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + ime_alloc = text; /* remember the IME-allocated buffer for cleanup */ +#endif + if (term->search.mode == SEARCH_MODE_SESSION_SAVE_SECURE_PASSWORD || + term->search.mode == SEARCH_MODE_SESSION_LOAD_PASSWORD) { + masked_text = xmalloc((text_len + 1) * sizeof(char32_t)); for (size_t i = 0; i < text_len; i++) - widths[i] = max(0, c32width(text[i])); - widths[text_len] = 0; + masked_text[i] = U'•'; + masked_text[text_len] = U'\0'; + text = masked_text; + } - const size_t total_cells = c32swidth(text, text_len); - const size_t wanted_visible_cells = max(20, total_cells); + /* Calculate the width of each character */ + int widths[text_len + 1]; + for (size_t i = 0; i < text_len; i++) + widths[i] = max(0, c32width(text[i])); + widths[text_len] = 0; - /* - * Build status string: " Aa Wb Re 3/17 ↻" - * Aa - case mode (blank=smart, A=sensitive, a=insensitive) - * Wb - whole-word toggle - * Re - regex toggle - * N/M - match counter - * ↻ - wrap indicator - */ - char32_t status[64]; - size_t st_len = 0; + const size_t total_cells = c32swidth(text, text_len); + size_t wanted_visible_cells = max(20, total_cells); + if (term->search.mode == SEARCH_MODE_SESSION_LOAD) { + /* Make room for the longest session name */ + for (size_t i = 0; i < term->session_picker.all_count; i++) { + size_t n = strlen(term->session_picker.all[i]); + if (n > wanted_visible_cells) + wanted_visible_cells = n; + } + wanted_visible_cells = max(wanted_visible_cells, 30); + } + /* + * Build status string: " Aa Wb Re 3/17 ↻" + * Aa - case mode (blank=smart, A=sensitive, a=insensitive) + * Wb - whole-word toggle + * Re - regex toggle + * N/M - match counter + * ↻ - wrap indicator + */ + char32_t status[64]; + size_t st_len = 0; + + if (term->search.mode != SEARCH_MODE_NORMAL) { + char label_buf[128]; + const char *label; + switch (term->search.mode) { + case SEARCH_MODE_SESSION_OVERWRITE_CONFIRM: + snprintf(label_buf, sizeof(label_buf), "Overwrite '%s'? (y/n) ", + term->session_pending_name != NULL + ? term->session_pending_name + : ""); + label = label_buf; + break; + case SEARCH_MODE_SESSION_SAVE: + label = "Save State As: "; + break; + case SEARCH_MODE_SESSION_SAVE_SECURE_NAME: + label = "Secure Save As: "; + break; + case SEARCH_MODE_SESSION_SAVE_SECURE_PASSWORD: + label = "Password (new): "; + break; + case SEARCH_MODE_SESSION_LOAD: + label = "Load State: "; + break; + case SEARCH_MODE_SESSION_LOAD_PASSWORD: + label = "Decrypt Password: "; + break; + default: + label = ""; + break; + } + for (const char *p = label; *p != '\0' && st_len < ALEN(status) - 1; p++) + status[st_len++] = (char32_t)(unsigned char)*p; + } else { if (term->search.case_mode == SEARCH_CASE_SENSITIVE) { - status[st_len++] = U'A'; status[st_len++] = U'a'; - status[st_len++] = U' '; + status[st_len++] = U'A'; + status[st_len++] = U'a'; + status[st_len++] = U' '; } else if (term->search.case_mode == SEARCH_CASE_INSENSITIVE) { - status[st_len++] = U'a'; status[st_len++] = U'a'; - status[st_len++] = U' '; + status[st_len++] = U'a'; + status[st_len++] = U'a'; + status[st_len++] = U' '; } if (term->search.whole_word) { - status[st_len++] = U'W'; status[st_len++] = U'b'; - status[st_len++] = U' '; + status[st_len++] = U'W'; + status[st_len++] = U'b'; + status[st_len++] = U' '; } if (term->search.regex) { - status[st_len++] = U'.'; status[st_len++] = U'*'; - status[st_len++] = U' '; + status[st_len++] = U'.'; + status[st_len++] = U'*'; + status[st_len++] = U' '; } if (term->search.wrapped) { - status[st_len++] = U'~'; status[st_len++] = U' '; + status[st_len++] = U'~'; + status[st_len++] = U' '; } if (term->search.len > 0 && term->search.total_count > 0) { - char tmp[32]; - snprintf(tmp, sizeof(tmp), "%zu/%zu", - term->search.current_idx, term->search.total_count); - for (size_t i = 0; tmp[i] != '\0' && st_len < ALEN(status) - 1; i++) - status[st_len++] = (char32_t)tmp[i]; - status[st_len++] = U' '; + char tmp[32]; + snprintf(tmp, sizeof(tmp), "%zu/%zu", term->search.current_idx, + term->search.total_count); + for (size_t i = 0; tmp[i] != '\0' && st_len < ALEN(status) - 1; i++) + status[st_len++] = (char32_t)tmp[i]; + status[st_len++] = U' '; } - status[st_len] = U'\0'; - const size_t status_cells = c32swidth(status, st_len); + } + status[st_len] = U'\0'; + const size_t status_cells = c32swidth(status, st_len); - const float scale = term->scale; - xassert(scale >= 1.); - const size_t margin = (size_t)roundf(3 * scale); - const size_t outer_margin = (size_t)roundf(6 * scale); - const size_t close_gap = (size_t)roundf(4 * scale); - const size_t close_w = term->cell_width; - const size_t status_w = status_cells * term->cell_width; - const size_t chrome_w = margin + status_w + close_gap + close_w + margin; + const float scale = term->scale; + xassert(scale >= 1.); + const size_t margin = (size_t)roundf(3 * scale); + const size_t outer_margin = (size_t)roundf(6 * scale); + const size_t close_gap = (size_t)roundf(4 * scale); + const size_t close_w = term->cell_width; + const size_t status_w = status_cells * term->cell_width; + const size_t chrome_w = margin + status_w + close_gap + close_w + margin; - const size_t max_box_w = term->width > (int)(2 * outer_margin) - ? (size_t)term->width - 2 * outer_margin : (size_t)term->width; - const size_t want_box_w = chrome_w + wanted_visible_cells * term->cell_width; + const size_t max_box_w = term->width > (int)(2 * outer_margin) + ? (size_t)term->width - 2 * outer_margin + : (size_t)term->width; + const size_t want_box_w = chrome_w + wanted_visible_cells * term->cell_width; - size_t width = min(want_box_w, max_box_w); - size_t height = min( - term->height - 2 * outer_margin, - margin + term->cell_height + margin); + size_t width = min(want_box_w, max_box_w); - width = roundf(scale * ceilf(width / scale)); - height = roundf(scale * ceilf(height / scale)); + /* Session-load picker: grow box to fit a few rows of session names */ + size_t list_rows = 0; + if (term->search.mode == SEARCH_MODE_SESSION_LOAD) { + const size_t max_visible = 10; + list_rows = min(max_visible, term->session_picker.filtered_count); + /* Reserve at least one row so empty state is visible */ + if (list_rows == 0) + list_rows = 1; + } - const size_t text_area_w = width > chrome_w ? width - chrome_w : 0; - const size_t visible_cells = text_area_w / term->cell_width; - size_t glyph_offset = term->render.search_glyph_offset; + size_t height = min( + term->height - 2 * outer_margin, + margin + (1 + list_rows) * term->cell_height + margin); - struct buffer_chain *chain = term->render.chains.search; - struct buffer *buf = shm_get_buffer(chain, width, height); + width = roundf(scale * ceilf(width / scale)); + height = roundf(scale * ceilf(height / scale)); - pixman_region32_t clip; - pixman_region32_init_rect(&clip, 0, 0, width, height); - pixman_image_set_clip_region32(buf->pix[0], &clip); - pixman_region32_fini(&clip); + const size_t text_area_w = width > chrome_w ? width - chrome_w : 0; + const size_t visible_cells = text_area_w / term->cell_width; + size_t glyph_offset = term->render.search_glyph_offset; + + struct buffer_chain *chain = term->render.chains.search; + struct buffer *buf = shm_get_buffer(chain, width, height); + + pixman_region32_t clip; + pixman_region32_init_rect(&clip, 0, 0, width, height); + pixman_image_set_clip_region32(buf->pix[0], &clip); + pixman_region32_fini(&clip); #define WINDOW_X(x) ((int)term->width - (int)width - (int)outer_margin + (x)) #define WINDOW_Y(y) ((int)outer_margin + (y)) - /* Use the terminal's own bg/fg for the popup, tinted to indicate - * the search state. */ - const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + /* Use the terminal's own bg/fg for the popup, tinted to indicate + * the search state. */ + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); - uint32_t bg_hex = term->colors.bg; - uint32_t fg_hex = term->colors.fg; + uint32_t bg_hex = term->colors.bg; + uint32_t fg_hex = term->colors.fg; + if (term->search.mode == SEARCH_MODE_NORMAL) { if (term->search.len > 0 && term->search.match_len == 0) { - /* No match — red */ - bg_hex = term->colors.table[1]; - fg_hex = term->colors.table[0]; + bg_hex = term->colors.table[1]; + fg_hex = term->colors.table[0]; } else if (term->search.wrapped) { - /* Wrapped — yellow */ - bg_hex = term->colors.table[3]; - fg_hex = term->colors.table[0]; + bg_hex = term->colors.table[3]; + fg_hex = term->colors.table[0]; } + } - const pixman_color_t color = color_hex_to_pixman(bg_hex, gamma_correct); + const pixman_color_t color = color_hex_to_pixman(bg_hex, gamma_correct); - pixman_image_fill_rectangles( - PIXMAN_OP_SRC, buf->pix[0], &color, - 1, &(pixman_rectangle16_t){0, 0, width, height}); + pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], &color, 1, + &(pixman_rectangle16_t){0, 0, width, height}); - struct fcft_font *font = term->fonts[0]; - const int x_left = margin; - const int x_ofs = term->font_x_ofs; - int x = x_left; - int y = margin; + struct fcft_font *font = term->fonts[0]; + const int x_left = margin; + const int x_ofs = term->font_x_ofs; + int x = x_left; + int y = margin; - pixman_color_t fg = color_hex_to_pixman(fg_hex, gamma_correct); + pixman_color_t fg = color_hex_to_pixman(fg_hex, gamma_correct); - /* Move offset we start rendering at, to ensure the cursor is visible */ - for (size_t i = 0, cell_idx = 0; i <= term->search.cursor; cell_idx += widths[i], i++) { - if (i != term->search.cursor) - continue; + /* Move offset we start rendering at, to ensure the cursor is visible */ + for (size_t i = 0, cell_idx = 0; i <= term->search.cursor; + cell_idx += widths[i], i++) { + if (i != term->search.cursor) + continue; #if (FOOT_IME_ENABLED) && FOOT_IME_ENABLED - if (ime_seat != NULL && ime_seat->ime.preedit.cells != NULL) { - if (ime_seat->ime.preedit.cursor.start == ime_seat->ime.preedit.cursor.end) { - /* All IME's I've seen so far keeps the cursor at - * index 0, so ensure the *end* of the pre-edit string - * is visible */ - cell_idx += ime_seat->ime.preedit.count; - } else { - /* Try to predict in which direction we'll shift the text */ - if (cell_idx + ime_seat->ime.preedit.cursor.start > glyph_offset) - cell_idx += ime_seat->ime.preedit.cursor.end; - else - cell_idx += ime_seat->ime.preedit.cursor.start; - } - } -#endif - - if (cell_idx < glyph_offset) { - /* Shift to the *left*, making *this* character the - * *first* visible one */ - term->render.search_glyph_offset = glyph_offset = cell_idx; - } - - else if (cell_idx > glyph_offset + visible_cells) { - /* Shift to the *right*, making *this* character the - * *last* visible one */ - term->render.search_glyph_offset = glyph_offset = - cell_idx - min(cell_idx, visible_cells); - } - - /* Adjust offset if there is free space available */ - if (total_cells - glyph_offset < visible_cells) { - term->render.search_glyph_offset = glyph_offset = - total_cells - min(total_cells, visible_cells); - } - - break; - } - - /* Ensure offset is at a character boundary */ - for (size_t i = 0, cell_idx = 0; i <= text_len; cell_idx += widths[i], i++) { - if (cell_idx >= glyph_offset) { - term->render.search_glyph_offset = glyph_offset = cell_idx; - break; - } - } - - /* - * Render the search string, starting at 'glyph_offset'. Note that - * glyph_offset is in cells, not characters - */ - for (size_t i = 0, - cell_idx = 0, - width = widths[i], - next_cell_idx = width; - i < text_len; - i++, - cell_idx = next_cell_idx, - width = widths[i], - next_cell_idx += width) - { - /* Convert subsurface coordinates to window coordinates*/ - /* Render cursor */ - if (i == term->search.cursor) { -#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED - bool have_preedit = - ime_seat != NULL && ime_seat->ime.preedit.cells != NULL; - bool hidden = - ime_seat != NULL && ime_seat->ime.preedit.cursor.hidden; - - if (have_preedit && !hidden) { - /* Cursor may be outside the visible area: - * cell_idx-glyph_offset can be negative */ - int cells_left = visible_cells - max( - (ssize_t)(cell_idx - glyph_offset), 0); - - /* If cursor is outside the visible area, we need to - * adjust our rectangle's position */ - int start = ime_seat->ime.preedit.cursor.start - + min((ssize_t)(cell_idx - glyph_offset), 0); - int end = ime_seat->ime.preedit.cursor.end - + min((ssize_t)(cell_idx - glyph_offset), 0); - - if (start == end) { - int count = min(ime_seat->ime.preedit.count, cells_left); - - /* Underline the entire (visible part of) pre-edit text */ - draw_underline(term, buf->pix[0], font, &fg, x, y, count); - - /* Bar-styled cursor, if in the visible area */ - if (start >= 0 && start <= visible_cells) { - draw_beam_cursor( - term, buf->pix[0], font, &fg, - x + start * term->cell_width, y); - } - - term_ime_set_cursor_rect(term, - WINDOW_X(x + start * term->cell_width), WINDOW_Y(y), - 1, term->cell_height); - } else { - /* Underline everything before and after the cursor */ - int count1 = min(start, cells_left); - int count2 = max( - min(ime_seat->ime.preedit.count - ime_seat->ime.preedit.cursor.end, - cells_left - end), - 0); - draw_underline(term, buf->pix[0], font, &fg, x, y, count1); - draw_underline(term, buf->pix[0], font, &fg, x + end * term->cell_width, y, count2); - - /* TODO: how do we handle a partially hidden rectangle? */ - if (start >= 0 && end <= visible_cells) { - draw_hollow_block( - term, buf->pix[0], &fg, x + start * term->cell_width, y, end - start); - } - term_ime_set_cursor_rect(term, - WINDOW_X(x + start * term->cell_width), WINDOW_Y(y), - term->cell_width * (end - start), term->cell_height); - } - } else if (!have_preedit) -#endif - { - /* Cursor *should* be in the visible area */ - xassert(cell_idx >= glyph_offset); - xassert(cell_idx <= glyph_offset + visible_cells); - draw_beam_cursor(term, buf->pix[0], font, &fg, x, y); - term_ime_set_cursor_rect( - term, WINDOW_X(x), WINDOW_Y(y), 1, term->cell_height); - } - } - - if (next_cell_idx >= glyph_offset && next_cell_idx - glyph_offset > visible_cells) { - /* We're now beyond the visible area - nothing more to render */ - break; - } - - if (cell_idx < glyph_offset) { - /* We haven't yet reached the visible part of the string */ - cell_idx = next_cell_idx; - continue; - } - - const struct fcft_glyph *glyph = fcft_rasterize_char_utf32( - font, text[i], term->font_subpixel); - - if (glyph == NULL) { - cell_idx = next_cell_idx; - continue; - } - - if (unlikely(glyph->is_color_glyph)) { - pixman_image_composite32( - PIXMAN_OP_OVER, glyph->pix, NULL, buf->pix[0], 0, 0, 0, 0, - x + x_ofs + glyph->x, y + term->font_baseline - glyph->y, - glyph->width, glyph->height); - } else { - int combining_ofs = width == 0 - ? (glyph->x < 0 - ? width * term->cell_width - : (width - 1) * term->cell_width) - : 0; /* Not a zero-width character - no additional offset */ - pixman_image_t *src = pixman_image_create_solid_fill(&fg); - pixman_image_composite32( - PIXMAN_OP_OVER, src, glyph->pix, buf->pix[0], 0, 0, 0, 0, - x + x_ofs + combining_ofs + glyph->x, - y + term->font_baseline - glyph->y, - glyph->width, glyph->height); - pixman_image_unref(src); - } - - x += width * term->cell_width; - cell_idx = next_cell_idx; - } - -#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED - if (ime_seat != NULL && ime_seat->ime.preedit.cells != NULL) - /* Already rendered */; + if (ime_seat != NULL && ime_seat->ime.preedit.cells != NULL) { + if (ime_seat->ime.preedit.cursor.start == + ime_seat->ime.preedit.cursor.end) { + /* All IME's I've seen so far keeps the cursor at + * index 0, so ensure the *end* of the pre-edit string + * is visible */ + cell_idx += ime_seat->ime.preedit.count; + } else { + /* Try to predict in which direction we'll shift the text */ + if (cell_idx + ime_seat->ime.preedit.cursor.start > glyph_offset) + cell_idx += ime_seat->ime.preedit.cursor.end; else + cell_idx += ime_seat->ime.preedit.cursor.start; + } + } #endif - if (term->search.cursor >= term->search.len) { - draw_beam_cursor(term, buf->pix[0], font, &fg, x, y); - term_ime_set_cursor_rect( - term, WINDOW_X(x), WINDOW_Y(y), 1, term->cell_height); - } - /* Status string (toggles + counter) immediately to the left of - * the close glyph */ - { - int sx = (int)width - (int)margin - (int)close_w - - (int)close_gap - (int)status_w; - const int sy = (int)margin; - for (size_t i = 0; i < st_len; i++) { - const struct fcft_glyph *g = fcft_rasterize_char_utf32( - font, status[i], term->font_subpixel); - int w = max(1, c32width(status[i])); - if (g != NULL && !g->is_color_glyph) { - pixman_image_t *src = pixman_image_create_solid_fill(&fg); - pixman_image_composite32( - PIXMAN_OP_OVER, src, g->pix, buf->pix[0], 0, 0, 0, 0, - sx + x_ofs + g->x, - sy + term->font_baseline - g->y, - g->width, g->height); - pixman_image_unref(src); - } - sx += w * (int)term->cell_width; - } + if (cell_idx < glyph_offset) { + /* Shift to the *left*, making *this* character the + * *first* visible one */ + term->render.search_glyph_offset = glyph_offset = cell_idx; } - /* Close glyph (×) at the top-right of the popup */ - { - const struct fcft_glyph *cg = fcft_rasterize_char_utf32( - font, U'×', term->font_subpixel); - if (cg != NULL) { - const int cx = (int)width - (int)margin - (int)close_w; - const int cy = (int)margin; - pixman_image_t *src = pixman_image_create_solid_fill(&fg); - pixman_image_composite32( - PIXMAN_OP_OVER, src, cg->pix, buf->pix[0], 0, 0, 0, 0, - cx + x_ofs + cg->x, - cy + term->font_baseline - cg->y, - cg->width, cg->height); - pixman_image_unref(src); + else if (cell_idx > glyph_offset + visible_cells) { + /* Shift to the *right*, making *this* character the + * *last* visible one */ + term->render.search_glyph_offset = glyph_offset = + cell_idx - min(cell_idx, visible_cells); + } + + /* Adjust offset if there is free space available */ + if (total_cells - glyph_offset < visible_cells) { + term->render.search_glyph_offset = glyph_offset = + total_cells - min(total_cells, visible_cells); + } + + break; + } + + /* Ensure offset is at a character boundary */ + for (size_t i = 0, cell_idx = 0; i <= text_len; cell_idx += widths[i], i++) { + if (cell_idx >= glyph_offset) { + term->render.search_glyph_offset = glyph_offset = cell_idx; + break; + } + } + + /* + * Render the search string, starting at 'glyph_offset'. Note that + * glyph_offset is in cells, not characters + */ + for (size_t i = 0, cell_idx = 0, width = widths[i], next_cell_idx = width; + i < text_len; i++, cell_idx = next_cell_idx, width = widths[i], + next_cell_idx += width) { + /* Convert subsurface coordinates to window coordinates*/ + /* Render cursor */ + if (i == term->search.cursor) { +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + bool have_preedit = + ime_seat != NULL && ime_seat->ime.preedit.cells != NULL; + bool hidden = ime_seat != NULL && ime_seat->ime.preedit.cursor.hidden; + + if (have_preedit && !hidden) { + /* Cursor may be outside the visible area: + * cell_idx-glyph_offset can be negative */ + int cells_left = + visible_cells - max((ssize_t)(cell_idx - glyph_offset), 0); + + /* If cursor is outside the visible area, we need to + * adjust our rectangle's position */ + int start = ime_seat->ime.preedit.cursor.start + + min((ssize_t)(cell_idx - glyph_offset), 0); + int end = ime_seat->ime.preedit.cursor.end + + min((ssize_t)(cell_idx - glyph_offset), 0); + + if (start == end) { + int count = min(ime_seat->ime.preedit.count, cells_left); + + /* Underline the entire (visible part of) pre-edit text */ + draw_underline(term, buf->pix[0], font, &fg, x, y, count); + + /* Bar-styled cursor, if in the visible area */ + if (start >= 0 && start <= visible_cells) { + draw_beam_cursor(term, buf->pix[0], font, &fg, + x + start * term->cell_width, y); + } + + term_ime_set_cursor_rect(term, WINDOW_X(x + start * term->cell_width), + WINDOW_Y(y), 1, term->cell_height); + } else { + /* Underline everything before and after the cursor */ + int count1 = min(start, cells_left); + int count2 = max(min(ime_seat->ime.preedit.count - + ime_seat->ime.preedit.cursor.end, + cells_left - end), + 0); + draw_underline(term, buf->pix[0], font, &fg, x, y, count1); + draw_underline(term, buf->pix[0], font, &fg, + x + end * term->cell_width, y, count2); + + /* TODO: how do we handle a partially hidden rectangle? */ + if (start >= 0 && end <= visible_cells) { + draw_hollow_block(term, buf->pix[0], &fg, + x + start * term->cell_width, y, end - start); + } + term_ime_set_cursor_rect( + term, WINDOW_X(x + start * term->cell_width), WINDOW_Y(y), + term->cell_width * (end - start), term->cell_height); } + } else if (!have_preedit) +#endif + { + /* Cursor *should* be in the visible area */ + xassert(cell_idx >= glyph_offset); + xassert(cell_idx <= glyph_offset + visible_cells); + draw_beam_cursor(term, buf->pix[0], font, &fg, x, y); + term_ime_set_cursor_rect(term, WINDOW_X(x), WINDOW_Y(y), 1, + term->cell_height); + } } - quirk_weston_subsurface_desync_on(term->window->search.sub); - - /* TODO: this is only necessary on a window resize */ - wl_subsurface_set_position( - term->window->search.sub, - (int32_t)roundf(max(0, (int32_t)term->width - (int32_t)width - (int32_t)outer_margin) / scale), - (int32_t)roundf(outer_margin / scale)); - - wayl_surface_scale(term->window, &term->window->search.surface, buf, scale); - wl_surface_attach(term->window->search.surface.surf, buf->wl_buf, 0, 0); - wl_surface_damage_buffer(term->window->search.surface.surf, 0, 0, width, height); - - struct wl_region *region = wl_compositor_create_region(term->wl->compositor); - if (region != NULL) { - wl_region_add(region, 0, 0, width, height); - wl_surface_set_opaque_region(term->window->search.surface.surf, region); - wl_region_destroy(region); + if (next_cell_idx >= glyph_offset && + next_cell_idx - glyph_offset > visible_cells) { + /* We're now beyond the visible area - nothing more to render */ + break; } - wl_surface_commit(term->window->search.surface.surf); - quirk_weston_subsurface_desync_off(term->window->search.sub); + if (cell_idx < glyph_offset) { + /* We haven't yet reached the visible part of the string */ + cell_idx = next_cell_idx; + continue; + } + + const struct fcft_glyph *glyph = + fcft_rasterize_char_utf32(font, text[i], term->font_subpixel); + + if (glyph == NULL) { + cell_idx = next_cell_idx; + continue; + } + + if (unlikely(glyph->is_color_glyph)) { + pixman_image_composite32(PIXMAN_OP_OVER, glyph->pix, NULL, buf->pix[0], 0, + 0, 0, 0, x + x_ofs + glyph->x, + y + term->font_baseline - glyph->y, glyph->width, + glyph->height); + } else { + int combining_ofs = + width == 0 + ? (glyph->x < 0 ? width * term->cell_width + : (width - 1) * term->cell_width) + : 0; /* Not a zero-width character - no additional offset */ + pixman_image_t *src = pixman_image_create_solid_fill(&fg); + pixman_image_composite32(PIXMAN_OP_OVER, src, glyph->pix, buf->pix[0], 0, + 0, 0, 0, x + x_ofs + combining_ofs + glyph->x, + y + term->font_baseline - glyph->y, glyph->width, + glyph->height); + pixman_image_unref(src); + } + + x += width * term->cell_width; + cell_idx = next_cell_idx; + } #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED - free(text); + if (ime_seat != NULL && ime_seat->ime.preedit.cells != NULL) + /* Already rendered */; + else #endif + if (term->search.cursor >= term->search.len) { + draw_beam_cursor(term, buf->pix[0], font, &fg, x, y); + term_ime_set_cursor_rect(term, WINDOW_X(x), WINDOW_Y(y), 1, + term->cell_height); + } + + /* Status string (toggles + counter) immediately to the left of + * the close glyph */ + { + int sx = (int)width - (int)margin - (int)close_w - (int)close_gap - + (int)status_w; + const int sy = (int)margin; + for (size_t i = 0; i < st_len; i++) { + const struct fcft_glyph *g = + fcft_rasterize_char_utf32(font, status[i], term->font_subpixel); + int w = max(1, c32width(status[i])); + if (g != NULL && !g->is_color_glyph) { + pixman_image_t *src = pixman_image_create_solid_fill(&fg); + pixman_image_composite32(PIXMAN_OP_OVER, src, g->pix, buf->pix[0], 0, 0, + 0, 0, sx + x_ofs + g->x, + sy + term->font_baseline - g->y, g->width, + g->height); + pixman_image_unref(src); + } + sx += w * (int)term->cell_width; + } + } + + /* Close glyph (×) at the top-right of the popup */ + { + const struct fcft_glyph *cg = + fcft_rasterize_char_utf32(font, U'×', term->font_subpixel); + if (cg != NULL) { + const int cx = (int)width - (int)margin - (int)close_w; + const int cy = (int)margin; + pixman_image_t *src = pixman_image_create_solid_fill(&fg); + pixman_image_composite32(PIXMAN_OP_OVER, src, cg->pix, buf->pix[0], 0, 0, + 0, 0, cx + x_ofs + cg->x, + cy + term->font_baseline - cg->y, cg->width, + cg->height); + pixman_image_unref(src); + } + } + + /* Session-load list */ + if (term->search.mode == SEARCH_MODE_SESSION_LOAD) { + const pixman_color_t sel_bg = color_hex_to_pixman( + term->colors.table[4], gamma_correct); + const size_t name_x = margin; + const size_t name_w = width > 2 * margin ? width - 2 * margin : 0; + + if (term->session_picker.filtered_count == 0) { + const char32_t *msg = U"(no sessions)"; + int rx = (int)name_x; + const int ry = (int)margin + (int)term->cell_height; + for (size_t i = 0; msg[i] != U'\0'; i++) { + const struct fcft_glyph *g = + fcft_rasterize_char_utf32(font, msg[i], term->font_subpixel); + int gw = max(1, c32width(msg[i])); + if (g != NULL && !g->is_color_glyph) { + pixman_image_t *src = pixman_image_create_solid_fill(&fg); + pixman_image_composite32(PIXMAN_OP_OVER, src, g->pix, buf->pix[0], 0, + 0, 0, 0, rx + x_ofs + g->x, + ry + term->font_baseline - g->y, g->width, + g->height); + pixman_image_unref(src); + } + rx += gw * (int)term->cell_width; + } + } else { + const size_t shown = min(list_rows, term->session_picker.filtered_count); + /* Scroll the list so the selection is visible */ + size_t first = 0; + if (term->session_picker.sel >= shown) + first = term->session_picker.sel - shown + 1; + for (size_t r = 0; r < shown; r++) { + size_t fi = first + r; + if (fi >= term->session_picker.filtered_count) + break; + const char *name = + term->session_picker.all[term->session_picker.filtered[fi]]; + + const int ry = (int)margin + (int)(1 + r) * (int)term->cell_height; + const bool selected = fi == term->session_picker.sel; + + if (selected) { + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &sel_bg, 1, + &(pixman_rectangle16_t){(int16_t)name_x, (int16_t)ry, + (uint16_t)name_w, + (uint16_t)term->cell_height}); + } + + const pixman_color_t row_fg = selected + ? color_hex_to_pixman(term->colors.bg, gamma_correct) + : fg; + + int rx = (int)name_x; + for (const char *p = name; *p != '\0'; p++) { + char32_t ch = (char32_t)(unsigned char)*p; + const struct fcft_glyph *g = + fcft_rasterize_char_utf32(font, ch, term->font_subpixel); + if (g != NULL && !g->is_color_glyph) { + pixman_image_t *src = pixman_image_create_solid_fill(&row_fg); + pixman_image_composite32(PIXMAN_OP_OVER, src, g->pix, buf->pix[0], + 0, 0, 0, 0, rx + x_ofs + g->x, + ry + term->font_baseline - g->y, + g->width, g->height); + pixman_image_unref(src); + } + rx += (int)term->cell_width; + if (rx > (int)(name_x + name_w)) + break; + } + } + } + } + + quirk_weston_subsurface_desync_on(term->window->search.sub); + + /* TODO: this is only necessary on a window resize */ + wl_subsurface_set_position( + term->window->search.sub, + (int32_t)roundf(max(0, (int32_t)term->width - (int32_t)width - + (int32_t)outer_margin) / + scale), + (int32_t)roundf(outer_margin / scale)); + + wayl_surface_scale(term->window, &term->window->search.surface, buf, scale); + wl_surface_attach(term->window->search.surface.surf, buf->wl_buf, 0, 0); + wl_surface_damage_buffer(term->window->search.surface.surf, 0, 0, width, + height); + + struct wl_region *region = wl_compositor_create_region(term->wl->compositor); + if (region != NULL) { + wl_region_add(region, 0, 0, width, height); + wl_surface_set_opaque_region(term->window->search.surface.surf, region); + wl_region_destroy(region); + } + + wl_surface_commit(term->window->search.surface.surf); + quirk_weston_subsurface_desync_off(term->window->search.sub); + + free(ime_alloc); + free(masked_text); #undef WINDOW_X #undef WINDOW_Y } -static void -render_urls(struct terminal *term) -{ - struct wl_window *win = term->window; - xassert(tll_length(win->urls) > 0); +static void render_urls(struct terminal *term) { + struct wl_window *win = term->window; + xassert(tll_length(win->urls) > 0); - const float scale = term->scale; - const int x_margin = (int)roundf(2 * scale); - const int y_margin = (int)roundf(1 * scale); + const float scale = term->scale; + const int x_margin = (int)roundf(2 * scale); + const int y_margin = (int)roundf(1 * scale); - /* Calculate view start, counted from the *current* scrollback start */ - const int scrollback_end - = (term->grid->offset + term->rows) & (term->grid->num_rows - 1); - const int view_start - = (term->grid->view - - scrollback_end - + term->grid->num_rows) & (term->grid->num_rows - 1); - const int view_end = view_start + term->rows - 1; + /* Calculate view start, counted from the *current* scrollback start */ + const int scrollback_end = + (term->grid->offset + term->rows) & (term->grid->num_rows - 1); + const int view_start = + (term->grid->view - scrollback_end + term->grid->num_rows) & + (term->grid->num_rows - 1); + const int view_end = view_start + term->rows - 1; - const bool show_url = term->urls_show_uri_on_jump_label; + const bool show_url = term->urls_show_uri_on_jump_label; + + /* + * There can potentially be a lot of URLs. + * + * Since each URL is a separate sub-surface, and requires its own + * SHM buffer, we may be allocating a lot of buffers. + * + * SHM buffers normally have their own, private SHM buffer + * pool. Each pool is mmapped, and thus allocates *at least* + * 4K. Since URL labels are typically small, we end up using an + * excessive amount of both virtual and physical memory. + * + * For this reason, we instead use shm_get_many(), which uses a + * single, shared pool for all buffers. + * + * To be able to use it, we need to have all the *all* the buffer + * dimensions up front. + * + * Thus, the first iteration through the URLs do the heavy + * lifting: builds the label contents and calculates both its + * position and size. But instead of rendering the label + * immediately, we store the calculated data, and then do a second + * pass, where we first get all our buffers, and then render to + * them. + */ + + /* Positioning data + label contents */ + struct { + const struct wl_url *url; + char32_t *text; + int x; + int y; + } info[tll_length(win->urls)]; + + /* For shm_get_many() */ + int widths[tll_length(win->urls)]; + int heights[tll_length(win->urls)]; + + size_t render_count = 0; + + tll_foreach(win->urls, it) { + const struct url *url = it->item.url; + const char32_t *key = url->key; + const size_t entered_key_len = c32len(term->url_keys); + + if (key == NULL) { + /* TODO: if we decide to use the .text field, we cannot + * just skip the entire jump label like this */ + continue; + } + + struct wl_surface *surf = it->item.surf.surface.surf; + struct wl_subsurface *sub_surf = it->item.surf.sub; + + if (surf == NULL || sub_surf == NULL) + continue; + + bool hide = false; + const struct coord *pos = &url->range.start; + const int _row = (pos->row - scrollback_end + term->grid->num_rows) & + (term->grid->num_rows - 1); + + if (_row < view_start || _row > view_end) + hide = true; + if (c32len(key) <= entered_key_len) + hide = true; + if (c32ncasecmp(term->url_keys, key, entered_key_len) != 0) + hide = true; + + if (hide) { + wl_surface_attach(surf, NULL, 0, 0); + wl_surface_commit(surf); + continue; + } + + int col = pos->col; + int row = pos->row - term->grid->view; + while (row < 0) + row += term->grid->num_rows; + row &= (term->grid->num_rows - 1); + + /* Position label slightly above and to the left */ + int x = col * term->cell_width - 15 * term->cell_width / 10; + int y = row * term->cell_height - 5 * term->cell_height / 10; + + /* Don't position it outside our window */ + if (x < -term->margins.left) + x = -term->margins.left; + if (y < -term->margins.top) + y = -term->margins.top; + + /* Maximum width of label, in pixels */ + const int max_width = + term->width - term->margins.left - term->margins.right - x; + const int max_cols = max_width / term->cell_width; + + const size_t key_len = c32len(key); + + size_t url_len = mbstoc32(NULL, url->url, 0); + if (url_len == (size_t)-1) + url_len = 0; + + char32_t url_wchars[url_len + 1]; + mbstoc32(url_wchars, url->url, url_len + 1); + + /* Format label, not yet subject to any size limitations */ + size_t chars = key_len + (show_url ? (2 + url_len) : 0); + char32_t label[chars + 1]; + label[chars] = U'\0'; + + if (show_url) { + c32cpy(label, key); + c32cat(label, U": "); + c32cat(label, url_wchars); + } else + c32ncpy(label, key, chars); + + /* Upper case the key characters */ + for (size_t i = 0; i < c32len(key); i++) + label[i] = toc32upper(label[i]); + + /* Blank already entered key characters */ + for (size_t i = 0; i < entered_key_len; i++) + label[i] = U' '; /* - * There can potentially be a lot of URLs. + * Don't extend outside our window * - * Since each URL is a separate sub-surface, and requires its own - * SHM buffer, we may be allocating a lot of buffers. + * Truncate label so that it doesn't extend outside our + * window. * - * SHM buffers normally have their own, private SHM buffer - * pool. Each pool is mmapped, and thus allocates *at least* - * 4K. Since URL labels are typically small, we end up using an - * excessive amount of both virtual and physical memory. - * - * For this reason, we instead use shm_get_many(), which uses a - * single, shared pool for all buffers. - * - * To be able to use it, we need to have all the *all* the buffer - * dimensions up front. - * - * Thus, the first iteration through the URLs do the heavy - * lifting: builds the label contents and calculates both its - * position and size. But instead of rendering the label - * immediately, we store the calculated data, and then do a second - * pass, where we first get all our buffers, and then render to - * them. + * Do it in a way such that we don't cut the label in the + * middle of a double-width character. */ - /* Positioning data + label contents */ - struct { - const struct wl_url *url; - char32_t *text; - int x; - int y; - } info[tll_length(win->urls)]; + int cols = 0; - /* For shm_get_many() */ - int widths[tll_length(win->urls)]; - int heights[tll_length(win->urls)]; + for (size_t i = 0; i <= c32len(label); i++) { + int _cols = c32swidth(label, i); - size_t render_count = 0; + if (_cols == (size_t)-1) + continue; - tll_foreach(win->urls, it) { - const struct url *url = it->item.url; - const char32_t *key = url->key; - const size_t entered_key_len = c32len(term->url_keys); - - if (key == NULL) { - /* TODO: if we decide to use the .text field, we cannot - * just skip the entire jump label like this */ - continue; - } - - struct wl_surface *surf = it->item.surf.surface.surf; - struct wl_subsurface *sub_surf = it->item.surf.sub; - - if (surf == NULL || sub_surf == NULL) - continue; - - bool hide = false; - const struct coord *pos = &url->range.start; - const int _row - = (pos->row - - scrollback_end - + term->grid->num_rows) & (term->grid->num_rows - 1); - - if (_row < view_start || _row > view_end) - hide = true; - if (c32len(key) <= entered_key_len) - hide = true; - if (c32ncasecmp(term->url_keys, key, entered_key_len) != 0) - hide = true; - - if (hide) { - wl_surface_attach(surf, NULL, 0, 0); - wl_surface_commit(surf); - continue; - } - - int col = pos->col; - int row = pos->row - term->grid->view; - while (row < 0) - row += term->grid->num_rows; - row &= (term->grid->num_rows - 1); - - /* Position label slightly above and to the left */ - int x = col * term->cell_width - 15 * term->cell_width / 10; - int y = row * term->cell_height - 5 * term->cell_height / 10; - - /* Don't position it outside our window */ - if (x < -term->margins.left) - x = -term->margins.left; - if (y < -term->margins.top) - y = -term->margins.top; - - /* Maximum width of label, in pixels */ - const int max_width = - term->width - term->margins.left - term->margins.right - x; - const int max_cols = max_width / term->cell_width; - - const size_t key_len = c32len(key); - - size_t url_len = mbstoc32(NULL, url->url, 0); - if (url_len == (size_t)-1) - url_len = 0; - - char32_t url_wchars[url_len + 1]; - mbstoc32(url_wchars, url->url, url_len + 1); - - /* Format label, not yet subject to any size limitations */ - size_t chars = key_len + (show_url ? (2 + url_len) : 0); - char32_t label[chars + 1]; - label[chars] = U'\0'; - - if (show_url) { - c32cpy(label, key); - c32cat(label, U": "); - c32cat(label, url_wchars); - } else - c32ncpy(label, key, chars); - - /* Upper case the key characters */ - for (size_t i = 0; i < c32len(key); i++) - label[i] = toc32upper(label[i]); - - /* Blank already entered key characters */ - for (size_t i = 0; i < entered_key_len; i++) - label[i] = U' '; - - /* - * Don't extend outside our window - * - * Truncate label so that it doesn't extend outside our - * window. - * - * Do it in a way such that we don't cut the label in the - * middle of a double-width character. - */ - - int cols = 0; - - for (size_t i = 0; i <= c32len(label); i++) { - int _cols = c32swidth(label, i); - - if (_cols == (size_t)-1) - continue; - - if (_cols >= max_cols) { - if (i > 0) - label[i - 1] = U'…'; - label[i] = U'\0'; - cols = max_cols; - break; - } - cols = _cols; - } - - if (cols == 0) - continue; - - int width = x_margin + cols * term->cell_width + x_margin; - int height = y_margin + term->cell_height + y_margin; - - width = roundf(scale * ceilf(width / scale)); - height = roundf(scale * ceilf(height / scale)); - - info[render_count].url = &it->item; - info[render_count].text = xc32dup(label); - info[render_count].x = x; - info[render_count].y = y; - - widths[render_count] = width; - heights[render_count] = height; - - render_count++; + if (_cols >= max_cols) { + if (i > 0) + label[i - 1] = U'…'; + label[i] = U'\0'; + cols = max_cols; + break; + } + cols = _cols; } - struct buffer_chain *chain = term->render.chains.url; - struct buffer *bufs[render_count]; - shm_get_many(chain, render_count, widths, heights, bufs); + if (cols == 0) + continue; - uint32_t fg = term->conf->colors_dark.use_custom.jump_label - ? term->conf->colors_dark.jump_label.fg - : term->colors.table[0]; - uint32_t bg = term->conf->colors_dark.use_custom.jump_label - ? term->conf->colors_dark.jump_label.bg - : term->colors.table[3]; + int width = x_margin + cols * term->cell_width + x_margin; + int height = y_margin + term->cell_height + y_margin; - for (size_t i = 0; i < render_count; i++) { - const struct wayl_sub_surface *sub_surf = &info[i].url->surf; + width = roundf(scale * ceilf(width / scale)); + height = roundf(scale * ceilf(height / scale)); - const char32_t *label = info[i].text; - const int x = info[i].x; - const int y = info[i].y; + info[render_count].url = &it->item; + info[render_count].text = xc32dup(label); + info[render_count].x = x; + info[render_count].y = y; - xassert(sub_surf->surface.surf != NULL); - xassert(sub_surf->sub != NULL); + widths[render_count] = width; + heights[render_count] = height; - wl_subsurface_set_position( - sub_surf->sub, - roundf((term->margins.left + x) / scale), - roundf((term->margins.top + y) / scale)); + render_count++; + } - render_osd( - term, sub_surf, term->fonts[0], bufs[i], label, - fg, 0xffu << 24 | bg, x_margin); + struct buffer_chain *chain = term->render.chains.url; + struct buffer *bufs[render_count]; + shm_get_many(chain, render_count, widths, heights, bufs); - free(info[i].text); - } + uint32_t fg = term->conf->colors_dark.use_custom.jump_label + ? term->conf->colors_dark.jump_label.fg + : term->colors.table[0]; + uint32_t bg = term->conf->colors_dark.use_custom.jump_label + ? term->conf->colors_dark.jump_label.bg + : term->colors.table[3]; + + for (size_t i = 0; i < render_count; i++) { + const struct wayl_sub_surface *sub_surf = &info[i].url->surf; + + const char32_t *label = info[i].text; + const int x = info[i].x; + const int y = info[i].y; + + xassert(sub_surf->surface.surf != NULL); + xassert(sub_surf->sub != NULL); + + wl_subsurface_set_position(sub_surf->sub, + roundf((term->margins.left + x) / scale), + roundf((term->margins.top + y) / scale)); + + render_osd(term, sub_surf, term->fonts[0], bufs[i], label, fg, + 0xffu << 24 | bg, x_margin); + + free(info[i].text); + } } -static void -render_update_title(struct terminal *term) -{ - static const size_t max_len = 2048; +static void render_update_title(struct terminal *term) { + static const size_t max_len = 2048; - const char *title = term->window_title != NULL ? term->window_title : "foot"; - char *copy = NULL; + const char *title = term->window_title != NULL ? term->window_title : "foot"; + char *copy = NULL; - if (strlen(title) > max_len) { - copy = xstrndup(title, max_len); - title = copy; - } + if (strlen(title) > max_len) { + copy = xstrndup(title, max_len); + title = copy; + } - xdg_toplevel_set_title(term->window->xdg_toplevel, title); - free(copy); + xdg_toplevel_set_title(term->window->xdg_toplevel, title); + free(copy); } -static void -frame_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_data) -{ - struct terminal *term = data; +static void frame_callback(void *data, struct wl_callback *wl_callback, + uint32_t callback_data) { + struct terminal *term = data; - xassert(term->window->frame_callback == wl_callback); - wl_callback_destroy(wl_callback); - term->window->frame_callback = NULL; + xassert(term->window->frame_callback == wl_callback); + wl_callback_destroy(wl_callback); + term->window->frame_callback = NULL; - bool grid = term->render.pending.grid; - bool csd = term->render.pending.csd; - bool search = term->is_searching && term->render.pending.search; - bool urls = urls_mode_is_active(term) > 0 && term->render.pending.urls; + bool grid = term->render.pending.grid; + bool csd = term->render.pending.csd; + bool search = term->is_searching && term->render.pending.search; + bool urls = urls_mode_is_active(term) > 0 && term->render.pending.urls; - term->render.pending.grid = false; - term->render.pending.csd = false; - term->render.pending.search = false; - term->render.pending.urls = false; + term->render.pending.grid = false; + term->render.pending.csd = false; + term->render.pending.search = false; + term->render.pending.urls = false; - struct grid *original_grid = term->grid; - if (urls_mode_is_active(term)) { - xassert(term->url_grid_snapshot != NULL); - term->grid = term->url_grid_snapshot; + struct grid *original_grid = term->grid; + if (urls_mode_is_active(term)) { + xassert(term->url_grid_snapshot != NULL); + term->grid = term->url_grid_snapshot; + } + + if (csd && term->window->csd_mode == CSD_YES) { + quirk_weston_csd_on(term); + render_csd(term); + quirk_weston_csd_off(term); + } + + if (search) + render_search_box(term); + + if (urls) + render_urls(term); + + if (term->conf->tabs.enabled && tab_overview_is_active(term->window)) + render_tab_overview(term); + + if ((grid && !term->delayed_render_timer.is_armed) || (csd | search | urls)) + grid_render(term); + + tll_foreach(term->wl->seats, it) { + if (it->item.ime_focus == term) + ime_update_cursor_rect(&it->item); + } + + term->grid = original_grid; +} + +static void tiocswinsz(struct terminal *term) { + if (term->ptmx >= 0) { + if (ioctl(term->ptmx, (unsigned int)TIOCSWINSZ, + &(struct winsize){.ws_row = term->rows, + .ws_col = term->cols, + .ws_xpixel = term->cols * term->cell_width, + .ws_ypixel = term->rows * term->cell_height}) < + 0) { + LOG_ERRNO("TIOCSWINSZ"); } - if (csd && term->window->csd_mode == CSD_YES) { - quirk_weston_csd_on(term); - render_csd(term); - quirk_weston_csd_off(term); + term_send_size_notification(term); + } +} + +static void delayed_reflow_of_normal_grid(struct terminal *term) { + if (term->interactive_resizing.grid == NULL) + return; + + xassert(term->interactive_resizing.new_rows > 0); + + struct coord *const tracking_points[] = { + &term->selection.coords.start, + &term->selection.coords.end, + }; + + /* Reflow the original (since before the resize was started) grid, + * to the *current* dimensions */ + grid_resize_and_reflow( + term->interactive_resizing.grid, term, + term->interactive_resizing.new_rows, term->normal.num_cols, + term->interactive_resizing.old_screen_rows, term->rows, + term->selection.coords.end.row >= 0 ? ALEN(tracking_points) : 0, + tracking_points); + + /* Replace the current, truncated, "normal" grid with the + * correctly reflowed one */ + grid_free(&term->normal); + term->normal = *term->interactive_resizing.grid; + free(term->interactive_resizing.grid); + + term->hide_cursor = term->interactive_resizing.old_hide_cursor; + + /* Reset */ + term->interactive_resizing.grid = NULL; + term->interactive_resizing.old_screen_rows = 0; + term->interactive_resizing.new_rows = 0; + term->interactive_resizing.old_hide_cursor = false; + + /* Invalidate render pointers */ + render_wait_for_preapply_damage(term); + shm_unref(term->render.last_buf); + term->render.last_buf = NULL; + term->render.last_cursor.row = NULL; + + tll_free(term->normal.scroll_damage); + sixel_reflow_grid(term, &term->normal); + + if (term->grid == &term->normal) { + term_damage_view(term); + render_refresh(term); + } + + term_ptmx_resume(term); +} + +static bool fdm_tiocswinsz(struct fdm *fdm, int fd, int events, void *data) { + struct terminal *term = data; + + if (events & EPOLLIN) { + tiocswinsz(term); + delayed_reflow_of_normal_grid(term); + } + + if (term->window->resize_timeout_fd >= 0) { + fdm_del(fdm, term->window->resize_timeout_fd); + term->window->resize_timeout_fd = -1; + } + return true; +} + +static void send_dimensions_to_client(struct terminal *term) { + struct wl_window *win = term->window; + + if (!win->is_resizing || term->conf->resize_delay_ms == 0) { + /* Send new dimensions to client immediately */ + tiocswinsz(term); + delayed_reflow_of_normal_grid(term); + + /* And make sure to reset and deallocate a lingering timer */ + if (win->resize_timeout_fd >= 0) { + fdm_del(term->fdm, win->resize_timeout_fd); + win->resize_timeout_fd = -1; + } + } else { + /* Send new dimensions to client "in a while" */ + assert(win->is_resizing && term->conf->resize_delay_ms > 0); + + int fd = win->resize_timeout_fd; + uint16_t delay_ms = term->conf->resize_delay_ms; + bool successfully_scheduled = false; + + if (fd < 0) { + /* Lazy create timer fd */ + fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (fd < 0) + LOG_ERRNO("failed to create TIOCSWINSZ timer"); + else if (!fdm_add(term->fdm, fd, EPOLLIN, &fdm_tiocswinsz, term)) { + close(fd); + fd = -1; + } + + win->resize_timeout_fd = fd; } - if (search) - render_search_box(term); + if (fd >= 0) { + /* Reset timeout */ + const struct itimerspec timeout = { + .it_value = + { + .tv_sec = delay_ms / 1000, + .tv_nsec = (delay_ms % 1000) * 1000000, + }, + }; - if (urls) - render_urls(term); + if (timerfd_settime(fd, 0, &timeout, NULL) < 0) { + LOG_ERRNO("failed to arm TIOCSWINSZ timer"); + fdm_del(term->fdm, fd); + win->resize_timeout_fd = -1; + } else + successfully_scheduled = true; + } - if (term->conf->tabs.enabled && tab_overview_is_active(term->window)) - render_tab_overview(term); + if (!successfully_scheduled) { + tiocswinsz(term); + delayed_reflow_of_normal_grid(term); + } + } +} - if ((grid && !term->delayed_render_timer.is_armed) || (csd | search | urls)) - grid_render(term); +static void set_size_from_grid(struct terminal *term, int *width, int *height, + int cols, int rows) { + int new_width, new_height; + /* Nominal grid dimensions */ + new_width = cols * term->cell_width; + new_height = rows * term->cell_height; + + /* Include any configured padding */ + new_width += (term->conf->pad_left + term->conf->pad_right) * term->scale; + new_height += (term->conf->pad_top + term->conf->pad_bottom) * term->scale; + + /* Round to multiples of scale */ + new_width = round(term->scale * round(new_width / term->scale)); + new_height = round(term->scale * round(new_height / term->scale)); + + if (width != NULL) + *width = new_width; + if (height != NULL) + *height = new_height; +} + +/* Move to terminal.c? */ +bool render_resize(struct terminal *term, int width, int height, uint8_t opts) { + if (term->shutdown.in_progress) + return false; + + if (!term->window->is_configured) + return false; + + if (term->cell_width == 0 && term->cell_height == 0) + return false; + + const bool is_floating = !term->window->is_maximized && + !term->window->is_fullscreen && + !term->window->is_tiled; + + /* Convert logical size to physical size */ + const float scale = term->scale; + width = round(width * scale); + height = round(height * scale); + + /* If the grid should be kept, the size should be overridden */ + if (is_floating && (opts & RESIZE_KEEP_GRID)) { + set_size_from_grid(term, &width, &height, term->cols, term->rows); + } + + if (width == 0) { + /* The compositor is letting us choose the width */ + if (term->stashed_width != 0) { + /* If a default size is requested, prefer the "last used" size */ + width = term->stashed_width; + } else { + /* Otherwise, use a user-configured size */ + switch (term->conf->size.type) { + case CONF_SIZE_PX: + width = term->conf->size.width; + + if (wayl_win_csd_borders_visible(term->window)) + width -= 2 * term->conf->csd.border_width_visible; + + width *= scale; + break; + + case CONF_SIZE_CELLS: + set_size_from_grid(term, &width, NULL, term->conf->size.width, + term->conf->size.height); + break; + } + } + } + + if (height == 0) { + /* The compositor is letting us choose the height */ + if (term->stashed_height != 0) { + /* If a default size is requested, prefer the "last used" size */ + height = term->stashed_height; + } else { + /* Otherwise, use a user-configured size */ + switch (term->conf->size.type) { + case CONF_SIZE_PX: + height = term->conf->size.height; + + /* Take CSDs into account */ + if (wayl_win_csd_titlebar_visible(term->window)) + height -= term->conf->csd.title_height; + + if (wayl_win_csd_borders_visible(term->window)) + height -= 2 * term->conf->csd.border_width_visible; + + height *= scale; + break; + + case CONF_SIZE_CELLS: + set_size_from_grid(term, NULL, &height, term->conf->size.width, + term->conf->size.height); + break; + } + } + } + + /* Don't shrink grid too much */ + const int min_cols = 1; + const int min_rows = 1; + + /* Minimum window size (must be divisible by the scaling factor)*/ + const int min_width = + roundf(scale * ceilf((min_cols * term->cell_width) / scale)); + const int min_height = + roundf(scale * ceilf((min_rows * term->cell_height) / scale)); + + width = max(width, min_width); + height = max(height, min_height); + + /* Padding */ + const int max_pad_x = (width - min_width) / 2; + const int max_pad_y = (height - min_height) / 2; + /* Total bar height = pill height + margin (margin only used in floating + * layout) */ + const uint16_t tabs_bar_logical = + term->conf->tabs.enabled + ? (term->conf->tabs.height + + (term->conf->tabs.layout == CONF_TABS_LAYOUT_FLOATING + ? term->conf->tabs.margin + : 0)) + : 0; + const int conf_pad_top = + term->conf->pad_top + + (term->conf->tabs.enabled && + term->conf->tabs.position == CONF_TABS_POSITION_TOP + ? tabs_bar_logical + : 0); + const int conf_pad_bottom = + term->conf->pad_bottom + + (term->conf->tabs.enabled && + term->conf->tabs.position == CONF_TABS_POSITION_BOTTOM + ? tabs_bar_logical + : 0); + const int pad_left = min(max_pad_x, scale * term->conf->pad_left); + const int pad_right = min(max_pad_x, scale * term->conf->pad_right); + const int pad_top = min(max_pad_y, scale * conf_pad_top); + const int pad_bottom = min(max_pad_y, scale * conf_pad_bottom); + + if (is_floating && (opts & RESIZE_BY_CELLS) && term->conf->resize_by_cells) { + /* If resizing in cell increments, restrict the width and height */ + width = ((width - (pad_left + pad_right)) / term->cell_width) * + term->cell_width + + (pad_left + pad_right); + width = max(min_width, roundf(scale * roundf(width / scale))); + + height = ((height - (pad_top + pad_bottom)) / term->cell_height) * + term->cell_height + + (pad_top + pad_bottom); + height = max(min_height, roundf(scale * roundf(height / scale))); + } + + if (!(opts & RESIZE_FORCE) && width == term->width && + height == term->height && scale == term->scale) { + return false; + } + + /* Cancel an application initiated "Synchronized Update" */ + term_disable_app_sync_updates(term); + + /* Drop out of URL mode */ + urls_reset(term); + + LOG_DBG("resized: size=%dx%d (scale=%.2f)", width, height, term->scale); + term->width = width; + term->height = height; + + /* Screen rows/cols before resize */ + int old_cols = term->cols; + int old_rows = term->rows; + + /* Screen rows/cols after resize */ + const int new_cols = + (term->width - (pad_left + pad_right)) / term->cell_width; + const int new_rows = + (term->height - (pad_top + pad_bottom)) / term->cell_height; + + /* + * Requirements for scrollback: + * + * a) total number of rows (visible + scrollback history) must be + * a power of two + * b) must be representable in a plain int (signed) + * + * This means that on a "normal" system, where ints are 32-bit, + * the largest possible scrollback size is 1073741824 (0x40000000, + * 1 << 30). + * + * The largest *signed* int is 2147483647 (0x7fffffff), which is + * *not* a power of two. + * + * Note that these are theoretical limits. Most of the time, + * you'll get a memory allocation failure when trying to allocate + * the grid array. + */ + const unsigned max_scrollback = (INT_MAX >> 1) + 1; + const unsigned scrollback_lines_not_yet_power_of_two = min( + (uint64_t)term->render.scrollback_lines + new_rows - 1, max_scrollback); + + /* Grid rows/cols after resize */ + const int new_normal_grid_rows = + min(1u << (32 - __builtin_clz(scrollback_lines_not_yet_power_of_two)), + max_scrollback); + const int new_alt_grid_rows = + min(1u << (32 - __builtin_clz(new_rows)), max_scrollback); + + LOG_DBG("grid rows: %d", new_normal_grid_rows); + + xassert(new_cols >= 1); + xassert(new_rows >= 1); + + /* Margins */ + const int grid_width = new_cols * term->cell_width; + const int grid_height = new_rows * term->cell_height; + const int total_x_pad = term->width - grid_width; + const int total_y_pad = term->height - grid_height; + + const enum center_when center = term->conf->center_when; + const bool centered_padding = + center == CENTER_ALWAYS || + (center == CENTER_MAXIMIZED_AND_FULLSCREEN && + (term->window->is_fullscreen || term->window->is_maximized)) || + (center == CENTER_FULLSCREEN && term->window->is_fullscreen); + + if (centered_padding && !term->window->is_resizing) { + term->margins.left = + max(pad_left, min(total_x_pad - pad_right, total_x_pad / 2)); + term->margins.top = + max(pad_top, min(total_y_pad - pad_bottom, total_y_pad / 2)); + } else { + term->margins.left = pad_left; + term->margins.top = pad_top; + } + term->margins.right = total_x_pad - term->margins.left; + term->margins.bottom = total_y_pad - term->margins.top; + + xassert(term->margins.left >= pad_left); + xassert(term->margins.right >= pad_right); + xassert(term->margins.top >= pad_top); + xassert(term->margins.bottom >= pad_bottom); + + if (new_cols == old_cols && new_rows == old_rows) { + LOG_DBG("grid layout unaffected; skipping reflow"); + term->interactive_resizing.new_rows = new_normal_grid_rows; + goto damage_view; + } + + /* + * Since text reflow is slow, don't do it *while* resizing. Only + * do it when done, or after "pausing" the resize for sufficiently + * long. We reuse the TIOCSWINSZ timer to handle this. See + * send_dimensions_to_client() and fdm_tiocswinsz(). + * + * To be able to do the final reflow correctly, we need a copy of + * the original grid, before the resize started. + */ + if (term->window->is_resizing && term->conf->resize_delay_ms > 0) { + if (term->interactive_resizing.grid == NULL) { + term_ptmx_pause(term); + + /* Stash the current 'normal' grid, as-is, to be used when + * doing the final reflow */ + term->interactive_resizing.old_screen_rows = term->rows; + term->interactive_resizing.old_cols = term->cols; + term->interactive_resizing.old_hide_cursor = term->hide_cursor; + term->interactive_resizing.grid = + xmalloc(sizeof(*term->interactive_resizing.grid)); + *term->interactive_resizing.grid = term->normal; + + if (term->grid == &term->normal) + term->interactive_resizing.selection_coords = term->selection.coords; + } else { + /* We'll replace the current temporary grid, with a new + * one (again based on the original grid) */ + grid_free(&term->normal); + } + + struct grid *orig = term->interactive_resizing.grid; + + /* + * Copy the current viewport (of the original grid) to a new + * grid that will be used during the resize. For now, throw + * away sixels and OSC-8 URLs. They'll be "restored" when we + * do the final reflow. + * + * Note that OSC-8 URLs are perfectly ok to throw away; they + * cannot be interacted with during the resize. And, even if + * url.osc8-underline=always, the "underline" attribute is + * part of the cell, not the URI struct (and thus our faked + * grid will still render OSC-8 links underlined). + * + * TODO: + * - sixels? + */ + struct grid g = { + .num_rows = 1 << (32 - __builtin_clz( + term->interactive_resizing.old_screen_rows)), + .num_cols = term->interactive_resizing.old_cols, + .offset = 0, + .view = 0, + .cursor = orig->cursor, + .saved_cursor = orig->saved_cursor, + .rows = xcalloc(g.num_rows, sizeof(g.rows[0])), + .cur_row = NULL, + .scroll_damage = tll_init(), + .sixel_images = tll_init(), + .kitty_kbd = orig->kitty_kbd, + }; + + term->selection.coords.start.row -= orig->view; + term->selection.coords.end.row -= orig->view; + + for (size_t i = 0, j = orig->view; + i < term->interactive_resizing.old_screen_rows; + i++, j = (j + 1) & (orig->num_rows - 1)) { + g.rows[i] = grid_row_alloc(g.num_cols, false); + memcpy(g.rows[i]->cells, orig->rows[j]->cells, + g.num_cols * sizeof(g.rows[i]->cells[0])); + + if (orig->rows[j]->extra == NULL || + orig->rows[j]->extra->underline_ranges.count == 0) { + continue; + } + + /* + * Copy underline ranges + */ + + const struct row_ranges *underline_src = + &orig->rows[j]->extra->underline_ranges; + + const int count = underline_src->count; + g.rows[i]->extra = xcalloc(1, sizeof(*g.rows[i]->extra)); + g.rows[i]->extra->underline_ranges.v = + xmalloc(count * sizeof(g.rows[i]->extra->underline_ranges.v[0])); + + struct row_ranges *underline_dst = &g.rows[i]->extra->underline_ranges; + underline_dst->count = underline_dst->size = count; + + for (int k = 0; k < count; k++) + underline_dst->v[k] = underline_src->v[k]; + } + + term->normal = g; + term->hide_cursor = true; + } + + if (term->grid == &term->alt) + selection_cancel(term); + else { + /* + * Don't cancel, but make sure there aren't any ongoing + * selections after the resize. + */ tll_foreach(term->wl->seats, it) { - if (it->item.ime_focus == term) - ime_update_cursor_rect(&it->item); + if (it->item.kbd_focus == term) + selection_finalize(&it->item, term, it->item.pointer.serial); } + } - term->grid = original_grid; -} + /* + * TODO: if we remove the selection_finalize() call above (i.e. if + * we start allowing selections to be ongoing across resizes), the + * selection's pivot point coordinates *must* be added to the + * tracking points list. + */ + /* Resize grids */ + if (term->window->is_resizing && term->conf->resize_delay_ms > 0) { + /* Simple truncating resize, *while* an interactive resize is + * ongoing. */ + xassert(term->interactive_resizing.grid != NULL); + xassert(new_normal_grid_rows > 0); + term->interactive_resizing.new_rows = new_normal_grid_rows; -static void -tiocswinsz(struct terminal *term) -{ - if (term->ptmx >= 0) { - if (ioctl(term->ptmx, (unsigned int)TIOCSWINSZ, - &(struct winsize){ - .ws_row = term->rows, - .ws_col = term->cols, - .ws_xpixel = term->cols * term->cell_width, - .ws_ypixel = term->rows * term->cell_height}) < 0) - { - LOG_ERRNO("TIOCSWINSZ"); - } + grid_resize_without_reflow(&term->normal, new_alt_grid_rows, new_cols, + term->interactive_resizing.old_screen_rows, + new_rows); + } else { + /* Full text reflow */ - term_send_size_notification(term); + int old_normal_rows = old_rows; + + if (term->interactive_resizing.grid != NULL) { + /* Throw away the current, truncated, "normal" grid, and + * use the original grid instead (from before the resize + * started) */ + grid_free(&term->normal); + term->normal = *term->interactive_resizing.grid; + free(term->interactive_resizing.grid); + + term->hide_cursor = term->interactive_resizing.old_hide_cursor; + term->selection.coords = term->interactive_resizing.selection_coords; + + old_normal_rows = term->interactive_resizing.old_screen_rows; + + term->interactive_resizing.grid = NULL; + term->interactive_resizing.old_screen_rows = 0; + term->interactive_resizing.new_rows = 0; + term->interactive_resizing.old_hide_cursor = false; + term->interactive_resizing.selection_coords = + (struct range){{-1, -1}, {-1, -1}}; + term_ptmx_resume(term); } -} - -static void -delayed_reflow_of_normal_grid(struct terminal *term) -{ - if (term->interactive_resizing.grid == NULL) - return; - - xassert(term->interactive_resizing.new_rows > 0); struct coord *const tracking_points[] = { &term->selection.coords.start, &term->selection.coords.end, }; - /* Reflow the original (since before the resize was started) grid, - * to the *current* dimensions */ grid_resize_and_reflow( - term->interactive_resizing.grid, term, - term->interactive_resizing.new_rows, term->normal.num_cols, - term->interactive_resizing.old_screen_rows, term->rows, + &term->normal, term, new_normal_grid_rows, new_cols, old_normal_rows, + new_rows, term->selection.coords.end.row >= 0 ? ALEN(tracking_points) : 0, tracking_points); + } - /* Replace the current, truncated, "normal" grid with the - * correctly reflowed one */ - grid_free(&term->normal); - term->normal = *term->interactive_resizing.grid; - free(term->interactive_resizing.grid); + grid_resize_without_reflow(&term->alt, new_alt_grid_rows, new_cols, old_rows, + new_rows); - term->hide_cursor = term->interactive_resizing.old_hide_cursor; + /* Reset tab stops */ + tll_free(term->tab_stops); + for (int c = 0; c < new_cols; c += 8) + tll_push_back(term->tab_stops, c); - /* Reset */ - term->interactive_resizing.grid = NULL; - term->interactive_resizing.old_screen_rows = 0; - term->interactive_resizing.new_rows = 0; - term->interactive_resizing.old_hide_cursor = false; + term->cols = new_cols; + term->rows = new_rows; - /* Invalidate render pointers */ - render_wait_for_preapply_damage(term); - shm_unref(term->render.last_buf); - term->render.last_buf = NULL; - term->render.last_cursor.row = NULL; + sixel_reflow(term); - tll_free(term->normal.scroll_damage); - sixel_reflow_grid(term, &term->normal); + LOG_DBG("resized: grid: cols=%d, rows=%d " + "(left-margin=%d, right-margin=%d, top-margin=%d, bottom-margin=%d)", + term->cols, term->rows, term->margins.left, term->margins.right, + term->margins.top, term->margins.bottom); - if (term->grid == &term->normal) { - term_damage_view(term); - render_refresh(term); - } + if (term->scroll_region.start >= term->rows) + term->scroll_region.start = 0; + if (term->scroll_region.end > term->rows || + term->scroll_region.end >= old_rows) { + term->scroll_region.end = term->rows; + } - term_ptmx_resume(term); -} - -static bool -fdm_tiocswinsz(struct fdm *fdm, int fd, int events, void *data) -{ - struct terminal *term = data; - - if (events & EPOLLIN) { - tiocswinsz(term); - delayed_reflow_of_normal_grid(term); - } - - if (term->window->resize_timeout_fd >= 0) { - fdm_del(fdm, term->window->resize_timeout_fd); - term->window->resize_timeout_fd = -1; - } - return true; -} - -static void -send_dimensions_to_client(struct terminal *term) -{ - struct wl_window *win = term->window; - - if (!win->is_resizing || term->conf->resize_delay_ms == 0) { - /* Send new dimensions to client immediately */ - tiocswinsz(term); - delayed_reflow_of_normal_grid(term); - - /* And make sure to reset and deallocate a lingering timer */ - if (win->resize_timeout_fd >= 0) { - fdm_del(term->fdm, win->resize_timeout_fd); - win->resize_timeout_fd = -1; - } - } else { - /* Send new dimensions to client "in a while" */ - assert(win->is_resizing && term->conf->resize_delay_ms > 0); - - int fd = win->resize_timeout_fd; - uint16_t delay_ms = term->conf->resize_delay_ms; - bool successfully_scheduled = false; - - if (fd < 0) { - /* Lazy create timer fd */ - fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); - if (fd < 0) - LOG_ERRNO("failed to create TIOCSWINSZ timer"); - else if (!fdm_add(term->fdm, fd, EPOLLIN, &fdm_tiocswinsz, term)) { - close(fd); - fd = -1; - } - - win->resize_timeout_fd = fd; - } - - if (fd >= 0) { - /* Reset timeout */ - const struct itimerspec timeout = { - .it_value = { - .tv_sec = delay_ms / 1000, - .tv_nsec = (delay_ms % 1000) * 1000000, - }, - }; - - if (timerfd_settime(fd, 0, &timeout, NULL) < 0) { - LOG_ERRNO("failed to arm TIOCSWINSZ timer"); - fdm_del(term->fdm, fd); - win->resize_timeout_fd = -1; - } else - successfully_scheduled = true; - } - - if (!successfully_scheduled) { - tiocswinsz(term); - delayed_reflow_of_normal_grid(term); - } - } -} - -static void -set_size_from_grid(struct terminal *term, int *width, int *height, int cols, int rows) -{ - int new_width, new_height; - - /* Nominal grid dimensions */ - new_width = cols * term->cell_width; - new_height = rows * term->cell_height; - - /* Include any configured padding */ - new_width += (term->conf->pad_left + term->conf->pad_right) * term->scale; - new_height += (term->conf->pad_top + term->conf->pad_bottom) * term->scale; - - /* Round to multiples of scale */ - new_width = round(term->scale * round(new_width / term->scale)); - new_height = round(term->scale * round(new_height / term->scale)); - - if (width != NULL) - *width = new_width; - if (height != NULL) - *height = new_height; -} - -/* Move to terminal.c? */ -bool -render_resize(struct terminal *term, int width, int height, uint8_t opts) -{ - if (term->shutdown.in_progress) - return false; - - if (!term->window->is_configured) - return false; - - if (term->cell_width == 0 && term->cell_height == 0) - return false; - - const bool is_floating = - !term->window->is_maximized && - !term->window->is_fullscreen && - !term->window->is_tiled; - - /* Convert logical size to physical size */ - const float scale = term->scale; - width = round(width * scale); - height = round(height * scale); - - /* If the grid should be kept, the size should be overridden */ - if (is_floating && (opts & RESIZE_KEEP_GRID)) { - set_size_from_grid(term, &width, &height, term->cols, term->rows); - } - - if (width == 0) { - /* The compositor is letting us choose the width */ - if (term->stashed_width != 0) { - /* If a default size is requested, prefer the "last used" size */ - width = term->stashed_width; - } else { - /* Otherwise, use a user-configured size */ - switch (term->conf->size.type) { - case CONF_SIZE_PX: - width = term->conf->size.width; - - if (wayl_win_csd_borders_visible(term->window)) - width -= 2 * term->conf->csd.border_width_visible; - - width *= scale; - break; - - case CONF_SIZE_CELLS: - set_size_from_grid(term, &width, NULL, - term->conf->size.width, term->conf->size.height); - break; - } - } - } - - if (height == 0) { - /* The compositor is letting us choose the height */ - if (term->stashed_height != 0) { - /* If a default size is requested, prefer the "last used" size */ - height = term->stashed_height; - } else { - /* Otherwise, use a user-configured size */ - switch (term->conf->size.type) { - case CONF_SIZE_PX: - height = term->conf->size.height; - - /* Take CSDs into account */ - if (wayl_win_csd_titlebar_visible(term->window)) - height -= term->conf->csd.title_height; - - if (wayl_win_csd_borders_visible(term->window)) - height -= 2 * term->conf->csd.border_width_visible; - - height *= scale; - break; - - case CONF_SIZE_CELLS: - set_size_from_grid(term, NULL, &height, - term->conf->size.width, term->conf->size.height); - break; - } - } - } - - /* Don't shrink grid too much */ - const int min_cols = 1; - const int min_rows = 1; - - /* Minimum window size (must be divisible by the scaling factor)*/ - const int min_width = roundf(scale * ceilf((min_cols * term->cell_width) / scale)); - const int min_height = roundf(scale * ceilf((min_rows * term->cell_height) / scale)); - - width = max(width, min_width); - height = max(height, min_height); - - /* Padding */ - const int max_pad_x = (width - min_width) / 2; - const int max_pad_y = (height - min_height) / 2; - /* Total bar height = pill height + margin (margin only used in floating layout) */ - const uint16_t tabs_bar_logical = - term->conf->tabs.enabled - ? (term->conf->tabs.height + - (term->conf->tabs.layout == CONF_TABS_LAYOUT_FLOATING - ? term->conf->tabs.margin : 0)) - : 0; - const int conf_pad_top = term->conf->pad_top + - (term->conf->tabs.enabled && - term->conf->tabs.position == CONF_TABS_POSITION_TOP - ? tabs_bar_logical : 0); - const int conf_pad_bottom = term->conf->pad_bottom + - (term->conf->tabs.enabled && - term->conf->tabs.position == CONF_TABS_POSITION_BOTTOM - ? tabs_bar_logical : 0); - const int pad_left = min(max_pad_x, scale * term->conf->pad_left); - const int pad_right = min(max_pad_x, scale * term->conf->pad_right); - const int pad_top = min(max_pad_y, scale * conf_pad_top); - const int pad_bottom= min(max_pad_y, scale * conf_pad_bottom); - - if (is_floating && - (opts & RESIZE_BY_CELLS) && - term->conf->resize_by_cells) - { - /* If resizing in cell increments, restrict the width and height */ - width = ((width - (pad_left + pad_right)) / term->cell_width) - * term->cell_width + (pad_left + pad_right); - width = max(min_width, roundf(scale * roundf(width / scale))); - - height = ((height - (pad_top + pad_bottom)) / term->cell_height) - * term->cell_height + (pad_top + pad_bottom); - height = max(min_height, roundf(scale * roundf(height / scale))); - } - - if (!(opts & RESIZE_FORCE) && - width == term->width && - height == term->height && - scale == term->scale) - { - return false; - } - - /* Cancel an application initiated "Synchronized Update" */ - term_disable_app_sync_updates(term); - - /* Drop out of URL mode */ - urls_reset(term); - - LOG_DBG("resized: size=%dx%d (scale=%.2f)", width, height, term->scale); - term->width = width; - term->height = height; - - /* Screen rows/cols before resize */ - int old_cols = term->cols; - int old_rows = term->rows; - - /* Screen rows/cols after resize */ - const int new_cols = - (term->width - (pad_left + pad_right)) / term->cell_width; - const int new_rows = - (term->height - (pad_top + pad_bottom)) / term->cell_height; - - /* - * Requirements for scrollback: - * - * a) total number of rows (visible + scrollback history) must be - * a power of two - * b) must be representable in a plain int (signed) - * - * This means that on a "normal" system, where ints are 32-bit, - * the largest possible scrollback size is 1073741824 (0x40000000, - * 1 << 30). - * - * The largest *signed* int is 2147483647 (0x7fffffff), which is - * *not* a power of two. - * - * Note that these are theoretical limits. Most of the time, - * you'll get a memory allocation failure when trying to allocate - * the grid array. - */ - const unsigned max_scrollback = (INT_MAX >> 1) + 1; - const unsigned scrollback_lines_not_yet_power_of_two = - min((uint64_t)term->render.scrollback_lines + new_rows - 1, max_scrollback); - - /* Grid rows/cols after resize */ - const int new_normal_grid_rows = - min(1u << (32 - __builtin_clz(scrollback_lines_not_yet_power_of_two)), - max_scrollback); - const int new_alt_grid_rows = - min(1u << (32 - __builtin_clz(new_rows)), max_scrollback); - - LOG_DBG("grid rows: %d", new_normal_grid_rows); - - xassert(new_cols >= 1); - xassert(new_rows >= 1); - - /* Margins */ - const int grid_width = new_cols * term->cell_width; - const int grid_height = new_rows * term->cell_height; - const int total_x_pad = term->width - grid_width; - const int total_y_pad = term->height - grid_height; - - const enum center_when center = term->conf->center_when; - const bool centered_padding = - center == CENTER_ALWAYS || - (center == CENTER_MAXIMIZED_AND_FULLSCREEN && - (term->window->is_fullscreen || term->window->is_maximized)) || - (center == CENTER_FULLSCREEN && term->window->is_fullscreen); - - if (centered_padding && !term->window->is_resizing) { - term->margins.left = max(pad_left, min(total_x_pad - pad_right, total_x_pad / 2)); - term->margins.top = max(pad_top, min(total_y_pad - pad_bottom, total_y_pad / 2)); - } else { - term->margins.left = pad_left; - term->margins.top = pad_top; - } - term->margins.right = total_x_pad - term->margins.left; - term->margins.bottom = total_y_pad - term->margins.top; - - xassert(term->margins.left >= pad_left); - xassert(term->margins.right >= pad_right); - xassert(term->margins.top >= pad_top); - xassert(term->margins.bottom >= pad_bottom); - - if (new_cols == old_cols && new_rows == old_rows) { - LOG_DBG("grid layout unaffected; skipping reflow"); - term->interactive_resizing.new_rows = new_normal_grid_rows; - goto damage_view; - } - - - /* - * Since text reflow is slow, don't do it *while* resizing. Only - * do it when done, or after "pausing" the resize for sufficiently - * long. We reuse the TIOCSWINSZ timer to handle this. See - * send_dimensions_to_client() and fdm_tiocswinsz(). - * - * To be able to do the final reflow correctly, we need a copy of - * the original grid, before the resize started. - */ - if (term->window->is_resizing && term->conf->resize_delay_ms > 0) { - if (term->interactive_resizing.grid == NULL) { - term_ptmx_pause(term); - - /* Stash the current 'normal' grid, as-is, to be used when - * doing the final reflow */ - term->interactive_resizing.old_screen_rows = term->rows; - term->interactive_resizing.old_cols = term->cols; - term->interactive_resizing.old_hide_cursor = term->hide_cursor; - term->interactive_resizing.grid = xmalloc(sizeof(*term->interactive_resizing.grid)); - *term->interactive_resizing.grid = term->normal; - - if (term->grid == &term->normal) - term->interactive_resizing.selection_coords = term->selection.coords; - } else { - /* We'll replace the current temporary grid, with a new - * one (again based on the original grid) */ - grid_free(&term->normal); - } - - struct grid *orig = term->interactive_resizing.grid; - - /* - * Copy the current viewport (of the original grid) to a new - * grid that will be used during the resize. For now, throw - * away sixels and OSC-8 URLs. They'll be "restored" when we - * do the final reflow. - * - * Note that OSC-8 URLs are perfectly ok to throw away; they - * cannot be interacted with during the resize. And, even if - * url.osc8-underline=always, the "underline" attribute is - * part of the cell, not the URI struct (and thus our faked - * grid will still render OSC-8 links underlined). - * - * TODO: - * - sixels? - */ - struct grid g = { - .num_rows = 1 << (32 - __builtin_clz(term->interactive_resizing.old_screen_rows)), - .num_cols = term->interactive_resizing.old_cols, - .offset = 0, - .view = 0, - .cursor = orig->cursor, - .saved_cursor = orig->saved_cursor, - .rows = xcalloc(g.num_rows, sizeof(g.rows[0])), - .cur_row = NULL, - .scroll_damage = tll_init(), - .sixel_images = tll_init(), - .kitty_kbd = orig->kitty_kbd, - }; - - term->selection.coords.start.row -= orig->view; - term->selection.coords.end.row -= orig->view; - - for (size_t i = 0, j = orig->view; - i < term->interactive_resizing.old_screen_rows; - i++, j = (j + 1) & (orig->num_rows - 1)) - { - g.rows[i] = grid_row_alloc(g.num_cols, false); - memcpy(g.rows[i]->cells, - orig->rows[j]->cells, - g.num_cols * sizeof(g.rows[i]->cells[0])); - - if (orig->rows[j]->extra == NULL || - orig->rows[j]->extra->underline_ranges.count == 0) - { - continue; - } - - /* - * Copy underline ranges - */ - - const struct row_ranges *underline_src = &orig->rows[j]->extra->underline_ranges; - - const int count = underline_src->count; - g.rows[i]->extra = xcalloc(1, sizeof(*g.rows[i]->extra)); - g.rows[i]->extra->underline_ranges.v = xmalloc( - count * sizeof(g.rows[i]->extra->underline_ranges.v[0])); - - struct row_ranges *underline_dst = &g.rows[i]->extra->underline_ranges; - underline_dst->count = underline_dst->size = count; - - for (int k = 0; k < count; k++) - underline_dst->v[k] = underline_src->v[k]; - } - - term->normal = g; - term->hide_cursor = true; - } - - if (term->grid == &term->alt) - selection_cancel(term); - else { - /* - * Don't cancel, but make sure there aren't any ongoing - * selections after the resize. - */ - tll_foreach(term->wl->seats, it) { - if (it->item.kbd_focus == term) - selection_finalize(&it->item, term, it->item.pointer.serial); - } - } - - /* - * TODO: if we remove the selection_finalize() call above (i.e. if - * we start allowing selections to be ongoing across resizes), the - * selection's pivot point coordinates *must* be added to the - * tracking points list. - */ - /* Resize grids */ - if (term->window->is_resizing && term->conf->resize_delay_ms > 0) { - /* Simple truncating resize, *while* an interactive resize is - * ongoing. */ - xassert(term->interactive_resizing.grid != NULL); - xassert(new_normal_grid_rows > 0); - term->interactive_resizing.new_rows = new_normal_grid_rows; - - grid_resize_without_reflow( - &term->normal, new_alt_grid_rows, new_cols, - term->interactive_resizing.old_screen_rows, new_rows); - } else { - /* Full text reflow */ - - int old_normal_rows = old_rows; - - if (term->interactive_resizing.grid != NULL) { - /* Throw away the current, truncated, "normal" grid, and - * use the original grid instead (from before the resize - * started) */ - grid_free(&term->normal); - term->normal = *term->interactive_resizing.grid; - free(term->interactive_resizing.grid); - - term->hide_cursor = term->interactive_resizing.old_hide_cursor; - term->selection.coords = term->interactive_resizing.selection_coords; - - old_normal_rows = term->interactive_resizing.old_screen_rows; - - term->interactive_resizing.grid = NULL; - term->interactive_resizing.old_screen_rows = 0; - term->interactive_resizing.new_rows = 0; - term->interactive_resizing.old_hide_cursor = false; - term->interactive_resizing.selection_coords = (struct range){{-1, -1}, {-1, -1}}; - term_ptmx_resume(term); - } - - struct coord *const tracking_points[] = { - &term->selection.coords.start, - &term->selection.coords.end, - }; - - grid_resize_and_reflow( - &term->normal, term, new_normal_grid_rows, new_cols, old_normal_rows, new_rows, - term->selection.coords.end.row >= 0 ? ALEN(tracking_points) : 0, - tracking_points); - } - - grid_resize_without_reflow( - &term->alt, new_alt_grid_rows, new_cols, old_rows, new_rows); - - /* Reset tab stops */ - tll_free(term->tab_stops); - for (int c = 0; c < new_cols; c += 8) - tll_push_back(term->tab_stops, c); - - term->cols = new_cols; - term->rows = new_rows; - - sixel_reflow(term); - - LOG_DBG("resized: grid: cols=%d, rows=%d " - "(left-margin=%d, right-margin=%d, top-margin=%d, bottom-margin=%d)", - term->cols, term->rows, - term->margins.left, term->margins.right, - term->margins.top, term->margins.bottom); - - if (term->scroll_region.start >= term->rows) - term->scroll_region.start = 0; - if (term->scroll_region.end > term->rows || - term->scroll_region.end >= old_rows) - { - term->scroll_region.end = term->rows; - } - - term->render.last_cursor.row = NULL; + term->render.last_cursor.row = NULL; damage_view: - /* Signal TIOCSWINSZ */ - send_dimensions_to_client(term); + /* Signal TIOCSWINSZ */ + send_dimensions_to_client(term); - if (is_floating) { - /* Stash current size, to enable us to restore it when we're - * being un-maximized/fullscreened/tiled */ - term->stashed_width = term->width; - term->stashed_height = term->height; - } + if (is_floating) { + /* Stash current size, to enable us to restore it when we're + * being un-maximized/fullscreened/tiled */ + term->stashed_width = term->width; + term->stashed_height = term->height; + } - { - const bool title_shown = wayl_win_csd_titlebar_visible(term->window); - const bool border_shown = wayl_win_csd_borders_visible(term->window); + { + const bool title_shown = wayl_win_csd_titlebar_visible(term->window); + const bool border_shown = wayl_win_csd_borders_visible(term->window); - const int title = title_shown - ? roundf(term->conf->csd.title_height * scale) - : 0; - const int border = border_shown - ? roundf(term->conf->csd.border_width_visible * scale) - : 0; + const int title = + title_shown ? roundf(term->conf->csd.title_height * scale) : 0; + const int border = + border_shown ? roundf(term->conf->csd.border_width_visible * scale) : 0; - /* Must use surface logical coordinates (same calculations as - in get_csd_data(), but with different inputs) */ - const int toplevel_min_width = roundf(border / scale) + - roundf(min_width / scale) + - roundf(border / scale); - - const int toplevel_min_height = roundf(border / scale) + - roundf(title / scale) + - roundf(min_height / scale) + - roundf(border / scale); - - const int toplevel_width = roundf(border / scale) + - roundf(term->width / scale) + + /* Must use surface logical coordinates (same calculations as + in get_csd_data(), but with different inputs) */ + const int toplevel_min_width = roundf(border / scale) + + roundf(min_width / scale) + roundf(border / scale); - const int toplevel_height = roundf(border / scale) + - roundf(title / scale) + - roundf(term->height / scale) + - roundf(border / scale); + const int toplevel_min_height = + roundf(border / scale) + roundf(title / scale) + + roundf(min_height / scale) + roundf(border / scale); - const int x = roundf(-border / scale); - const int y = roundf(-title / scale) - roundf(border / scale); + const int toplevel_width = roundf(border / scale) + + roundf(term->width / scale) + + roundf(border / scale); - xdg_toplevel_set_min_size( - term->window->xdg_toplevel, - toplevel_min_width, toplevel_min_height); + const int toplevel_height = roundf(border / scale) + roundf(title / scale) + + roundf(term->height / scale) + + roundf(border / scale); - xdg_surface_set_window_geometry( - term->window->xdg_surface, - x, y, toplevel_width, toplevel_height); - } + const int x = roundf(-border / scale); + const int y = roundf(-title / scale) - roundf(border / scale); - tll_free(term->normal.scroll_damage); - tll_free(term->alt.scroll_damage); + xdg_toplevel_set_min_size(term->window->xdg_toplevel, toplevel_min_width, + toplevel_min_height); - render_wait_for_preapply_damage(term); - shm_unref(term->render.last_buf); - term->render.last_buf = NULL; - term_damage_view(term); - render_refresh_csd(term); - render_refresh_search(term); - render_refresh(term); + xdg_surface_set_window_geometry(term->window->xdg_surface, x, y, + toplevel_width, toplevel_height); + } - return true; + tll_free(term->normal.scroll_damage); + tll_free(term->alt.scroll_damage); + + render_wait_for_preapply_damage(term); + shm_unref(term->render.last_buf); + term->render.last_buf = NULL; + term_damage_view(term); + render_refresh_csd(term); + render_refresh_search(term); + render_refresh(term); + + return true; } -static void xcursor_callback( - void *data, struct wl_callback *wl_callback, uint32_t callback_data); +static void xcursor_callback(void *data, struct wl_callback *wl_callback, + uint32_t callback_data); static const struct wl_callback_listener xcursor_listener = { .done = &xcursor_callback, }; -bool -render_xcursor_is_valid(const struct seat *seat, const char *cursor) -{ - if (cursor == NULL) - return false; - if (seat->pointer.theme == NULL) - return false; - return wl_cursor_theme_get_cursor(seat->pointer.theme, cursor) != NULL; +bool render_xcursor_is_valid(const struct seat *seat, const char *cursor) { + if (cursor == NULL) + return false; + if (seat->pointer.theme == NULL) + return false; + return wl_cursor_theme_get_cursor(seat->pointer.theme, cursor) != NULL; } -static void -render_xcursor_update(struct seat *seat) -{ - /* If called from a frame callback, we may no longer have mouse focus */ - if (!seat->mouse_focus) - return; +static void render_xcursor_update(struct seat *seat) { + /* If called from a frame callback, we may no longer have mouse focus */ + if (!seat->mouse_focus) + return; - xassert(seat->pointer.shape != CURSOR_SHAPE_NONE); - - if (seat->pointer.shape == CURSOR_SHAPE_HIDDEN) { - /* Hide cursor */ - LOG_DBG("hiding cursor using client-side NULL-surface"); - wl_surface_attach(seat->pointer.surface.surf, NULL, 0, 0); - wl_pointer_set_cursor( - seat->wl_pointer, seat->pointer.serial, seat->pointer.surface.surf, - 0, 0); - wl_surface_commit(seat->pointer.surface.surf); - return; - } - - const enum cursor_shape shape = seat->pointer.shape; - const char *const xcursor = seat->pointer.last_custom_xcursor; - - if (seat->pointer.shape_device != NULL) { - xassert(shape != CURSOR_SHAPE_CUSTOM || xcursor != NULL); - - const enum wp_cursor_shape_device_v1_shape custom_shape = - (shape == CURSOR_SHAPE_CUSTOM && xcursor != NULL - ? cursor_string_to_server_shape( - xcursor, seat->wayl->shape_manager_version) - : 0); - - if (shape != CURSOR_SHAPE_CUSTOM || custom_shape != 0) { - xassert(custom_shape == 0 || shape == CURSOR_SHAPE_CUSTOM); - - const enum wp_cursor_shape_device_v1_shape wp_shape = custom_shape != 0 - ? custom_shape - : cursor_shape_to_server_shape(shape); - - LOG_DBG("setting %scursor shape using cursor-shape-v1", - custom_shape != 0 ? "custom " : ""); - - wp_cursor_shape_device_v1_set_shape( - seat->pointer.shape_device, - seat->pointer.serial, - wp_shape); - - return; - } - } - - LOG_DBG("setting %scursor shape using a client-side cursor surface", - seat->pointer.shape == CURSOR_SHAPE_CUSTOM ? "custom " : ""); - - if (seat->pointer.cursor == NULL) { - /* - * Normally, we never get here with a NULL-cursor, because we - * only schedule a cursor update when we succeed to load the - * cursor image. - * - * However, it is possible that we did succeed to load an - * image, and scheduled an update. But, *before* the scheduled - * update triggers, the user mvoes the pointer, and we try to - * load a new cursor image. This time failing. - * - * In this case, we have a NULL cursor, but the scheduled - * update is still scheduled. - */ - return; - } - - const float scale = seat->pointer.scale; - struct wl_cursor_image *image = seat->pointer.cursor->images[0]; - struct wl_buffer *buf = wl_cursor_image_get_buffer(image); - - wayl_surface_scale_explicit_width_height( - seat->mouse_focus->window, - &seat->pointer.surface, image->width, image->height, scale); - - wl_surface_attach(seat->pointer.surface.surf, buf, 0, 0); - - wl_pointer_set_cursor( - seat->wl_pointer, seat->pointer.serial, - seat->pointer.surface.surf, - image->hotspot_x / scale, image->hotspot_y / scale); - - wl_surface_damage_buffer( - seat->pointer.surface.surf, 0, 0, INT32_MAX, INT32_MAX); - - xassert(seat->pointer.xcursor_callback == NULL); - seat->pointer.xcursor_callback = wl_surface_frame(seat->pointer.surface.surf); - wl_callback_add_listener(seat->pointer.xcursor_callback, &xcursor_listener, seat); + xassert(seat->pointer.shape != CURSOR_SHAPE_NONE); + if (seat->pointer.shape == CURSOR_SHAPE_HIDDEN) { + /* Hide cursor */ + LOG_DBG("hiding cursor using client-side NULL-surface"); + wl_surface_attach(seat->pointer.surface.surf, NULL, 0, 0); + wl_pointer_set_cursor(seat->wl_pointer, seat->pointer.serial, + seat->pointer.surface.surf, 0, 0); wl_surface_commit(seat->pointer.surface.surf); + return; + } + + const enum cursor_shape shape = seat->pointer.shape; + const char *const xcursor = seat->pointer.last_custom_xcursor; + + if (seat->pointer.shape_device != NULL) { + xassert(shape != CURSOR_SHAPE_CUSTOM || xcursor != NULL); + + const enum wp_cursor_shape_device_v1_shape custom_shape = + (shape == CURSOR_SHAPE_CUSTOM && xcursor != NULL + ? cursor_string_to_server_shape(xcursor, + seat->wayl->shape_manager_version) + : 0); + + if (shape != CURSOR_SHAPE_CUSTOM || custom_shape != 0) { + xassert(custom_shape == 0 || shape == CURSOR_SHAPE_CUSTOM); + + const enum wp_cursor_shape_device_v1_shape wp_shape = + custom_shape != 0 ? custom_shape + : cursor_shape_to_server_shape(shape); + + LOG_DBG("setting %scursor shape using cursor-shape-v1", + custom_shape != 0 ? "custom " : ""); + + wp_cursor_shape_device_v1_set_shape(seat->pointer.shape_device, + seat->pointer.serial, wp_shape); + + return; + } + } + + LOG_DBG("setting %scursor shape using a client-side cursor surface", + seat->pointer.shape == CURSOR_SHAPE_CUSTOM ? "custom " : ""); + + if (seat->pointer.cursor == NULL) { + /* + * Normally, we never get here with a NULL-cursor, because we + * only schedule a cursor update when we succeed to load the + * cursor image. + * + * However, it is possible that we did succeed to load an + * image, and scheduled an update. But, *before* the scheduled + * update triggers, the user mvoes the pointer, and we try to + * load a new cursor image. This time failing. + * + * In this case, we have a NULL cursor, but the scheduled + * update is still scheduled. + */ + return; + } + + const float scale = seat->pointer.scale; + struct wl_cursor_image *image = seat->pointer.cursor->images[0]; + struct wl_buffer *buf = wl_cursor_image_get_buffer(image); + + wayl_surface_scale_explicit_width_height(seat->mouse_focus->window, + &seat->pointer.surface, image->width, + image->height, scale); + + wl_surface_attach(seat->pointer.surface.surf, buf, 0, 0); + + wl_pointer_set_cursor(seat->wl_pointer, seat->pointer.serial, + seat->pointer.surface.surf, image->hotspot_x / scale, + image->hotspot_y / scale); + + wl_surface_damage_buffer(seat->pointer.surface.surf, 0, 0, INT32_MAX, + INT32_MAX); + + xassert(seat->pointer.xcursor_callback == NULL); + seat->pointer.xcursor_callback = wl_surface_frame(seat->pointer.surface.surf); + wl_callback_add_listener(seat->pointer.xcursor_callback, &xcursor_listener, + seat); + + wl_surface_commit(seat->pointer.surface.surf); } -static void -xcursor_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_data) -{ - struct seat *seat = data; +static void xcursor_callback(void *data, struct wl_callback *wl_callback, + uint32_t callback_data) { + struct seat *seat = data; - xassert(seat->pointer.xcursor_callback == wl_callback); - wl_callback_destroy(wl_callback); - seat->pointer.xcursor_callback = NULL; + xassert(seat->pointer.xcursor_callback == wl_callback); + wl_callback_destroy(wl_callback); + seat->pointer.xcursor_callback = NULL; - if (seat->pointer.xcursor_pending) { - render_xcursor_update(seat); - seat->pointer.xcursor_pending = false; - } + if (seat->pointer.xcursor_pending) { + render_xcursor_update(seat); + seat->pointer.xcursor_pending = false; + } } -static void -fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data) -{ - struct renderer *renderer = data; - struct wayland *wayl = renderer->wayl; +static void fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data) { + struct renderer *renderer = data; + struct wayland *wayl = renderer->wayl; - tll_foreach(renderer->wayl->terms, it) { - struct terminal *term = it->item; + tll_foreach(renderer->wayl->terms, it) { + struct terminal *term = it->item; - if (unlikely(term->shutdown.in_progress || !term->window->is_configured)) - continue; + if (unlikely(term->shutdown.in_progress || !term->window->is_configured)) + continue; - /* Skip inactive tabs - they process PTY data but don't render */ - if (term->window->term != term) - continue; + /* Skip inactive tabs - they process PTY data but don't render */ + if (term->window->term != term) + continue; - bool grid = term->render.refresh.grid; - bool csd = term->render.refresh.csd; - bool search = term->is_searching && term->render.refresh.search; - bool urls = urls_mode_is_active(term) && term->render.refresh.urls; + bool grid = term->render.refresh.grid; + bool csd = term->render.refresh.csd; + bool search = term->is_searching && term->render.refresh.search; + bool urls = urls_mode_is_active(term) && term->render.refresh.urls; - if (!(grid | csd | search | urls)) - continue; + if (!(grid | csd | search | urls)) + continue; - if (term->render.app_sync_updates.enabled && !(csd | search | urls)) - continue; + if (term->render.app_sync_updates.enabled && !(csd | search | urls)) + continue; - term->render.refresh.grid = false; - term->render.refresh.csd = false; - term->render.refresh.search = false; - term->render.refresh.urls = false; + term->render.refresh.grid = false; + term->render.refresh.csd = false; + term->render.refresh.search = false; + term->render.refresh.urls = false; - if (term->window->frame_callback == NULL) { - struct grid *original_grid = term->grid; - if (urls_mode_is_active(term)) { - xassert(term->url_grid_snapshot != NULL); - term->grid = term->url_grid_snapshot; - } + if (term->window->frame_callback == NULL) { + struct grid *original_grid = term->grid; + if (urls_mode_is_active(term)) { + xassert(term->url_grid_snapshot != NULL); + term->grid = term->url_grid_snapshot; + } - if (csd && term->window->csd_mode == CSD_YES) { - quirk_weston_csd_on(term); - render_csd(term); - quirk_weston_csd_off(term); - } - if (search) - render_search_box(term); - if (urls) - render_urls(term); - if (term->conf->tabs.enabled) - render_tab_bar(term); - if (term->conf->tabs.enabled && tab_overview_is_active(term->window)) - render_tab_overview(term); - if (grid | csd | search | urls) - grid_render(term); + if (csd && term->window->csd_mode == CSD_YES) { + quirk_weston_csd_on(term); + render_csd(term); + quirk_weston_csd_off(term); + } + if (search) + render_search_box(term); + if (urls) + render_urls(term); + if (term->conf->tabs.enabled) + render_tab_bar(term); + if (term->conf->tabs.enabled && tab_overview_is_active(term->window)) + render_tab_overview(term); + if (grid | csd | search | urls) + grid_render(term); - tll_foreach(term->wl->seats, it) { - if (it->item.ime_focus == term) - ime_update_cursor_rect(&it->item); - } + tll_foreach(term->wl->seats, it) { + if (it->item.ime_focus == term) + ime_update_cursor_rect(&it->item); + } - term->grid = original_grid; - } else { - /* Tells the frame callback to render again */ - term->render.pending.grid |= grid; - term->render.pending.csd |= csd; - term->render.pending.search |= search; - term->render.pending.urls |= urls; - - /* The tab bar is a desync subsurface — its commits apply - * independently of the parent surface, so we can repaint it - * now instead of deferring to the frame callback (which never - * calls render_tab_bar anyway). Without this, title changes - * while a frame is in flight leave the tab label stale until - * something else dirties the grid. */ - if (grid && term->conf->tabs.enabled) - render_tab_bar(term); - if (grid && term->conf->tabs.enabled && - tab_overview_is_active(term->window)) - render_tab_overview(term); - } - } - - tll_foreach(wayl->seats, it) { - if (it->item.pointer.xcursor_pending) { - if (it->item.pointer.xcursor_callback == NULL) { - render_xcursor_update(&it->item); - it->item.pointer.xcursor_pending = false; - } else { - /* Frame callback will call render_xcursor_update() */ - } - } - } -} - -void -render_refresh_title(struct terminal *term) -{ - struct timespec now; - if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) - return; - - struct timespec diff; - timespec_sub(&now, &term->render.title.last_update, &diff); - - if (diff.tv_sec == 0 && diff.tv_nsec < 8333 * 1000) { - const struct itimerspec timeout = { - .it_value = {.tv_nsec = 8333 * 1000 - diff.tv_nsec}, - }; - - timerfd_settime(term->render.title.timer_fd, 0, &timeout, NULL); + term->grid = original_grid; } else { - term->render.title.last_update = now; - render_update_title(term); + /* Tells the frame callback to render again */ + term->render.pending.grid |= grid; + term->render.pending.csd |= csd; + term->render.pending.search |= search; + term->render.pending.urls |= urls; + + if (grid && term->conf->tabs.enabled) + render_tab_bar(term); + if (grid && term->conf->tabs.enabled && + tab_overview_is_active(term->window)) + render_tab_overview(term); } + } - render_refresh_csd(term); -} - -void -render_refresh_app_id(struct terminal *term) -{ - struct timespec now; - if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) - return; - - struct timespec diff; - timespec_sub(&now, &term->render.app_id.last_update, &diff); - - if (diff.tv_sec == 0 && diff.tv_nsec < 8333 * 1000) { - const struct itimerspec timeout = { - .it_value = {.tv_nsec = 8333 * 1000 - diff.tv_nsec}, - }; - - timerfd_settime(term->render.app_id.timer_fd, 0, &timeout, NULL); - return; + tll_foreach(wayl->seats, it) { + if (it->item.pointer.xcursor_pending) { + if (it->item.pointer.xcursor_callback == NULL) { + render_xcursor_update(&it->item); + it->item.pointer.xcursor_pending = false; + } else { + /* Frame callback will call render_xcursor_update() */ + } } - - const char *app_id = - term->app_id != NULL ? term->app_id : term->conf->app_id; - - xdg_toplevel_set_app_id(term->window->xdg_toplevel, app_id); - term->render.app_id.last_update = now; + } } -void -render_refresh_icon(struct terminal *term) -{ - if (term->wl->toplevel_icon_manager == NULL) { - LOG_DBG("compositor does not implement xdg-toplevel-icon: " - "ignoring request to refresh window icon"); - return; +void render_refresh_title(struct terminal *term) { + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) + return; + + struct timespec diff; + timespec_sub(&now, &term->render.title.last_update, &diff); + + if (diff.tv_sec == 0 && diff.tv_nsec < 8333 * 1000) { + const struct itimerspec timeout = { + .it_value = {.tv_nsec = 8333 * 1000 - diff.tv_nsec}, + }; + + timerfd_settime(term->render.title.timer_fd, 0, &timeout, NULL); + } else { + term->render.title.last_update = now; + render_update_title(term); + } + + render_refresh_csd(term); +} + +void render_refresh_app_id(struct terminal *term) { + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) + return; + + struct timespec diff; + timespec_sub(&now, &term->render.app_id.last_update, &diff); + + if (diff.tv_sec == 0 && diff.tv_nsec < 8333 * 1000) { + const struct itimerspec timeout = { + .it_value = {.tv_nsec = 8333 * 1000 - diff.tv_nsec}, + }; + + timerfd_settime(term->render.app_id.timer_fd, 0, &timeout, NULL); + return; + } + + const char *app_id = term->app_id != NULL ? term->app_id : term->conf->app_id; + + xdg_toplevel_set_app_id(term->window->xdg_toplevel, app_id); + term->render.app_id.last_update = now; +} + +void render_refresh_icon(struct terminal *term) { + if (term->wl->toplevel_icon_manager == NULL) { + LOG_DBG("compositor does not implement xdg-toplevel-icon: " + "ignoring request to refresh window icon"); + return; + } + + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) + return; + + struct timespec diff; + timespec_sub(&now, &term->render.icon.last_update, &diff); + + if (diff.tv_sec == 0 && diff.tv_nsec < 8333 * 1000) { + const struct itimerspec timeout = { + .it_value = {.tv_nsec = 8333 * 1000 - diff.tv_nsec}, + }; + + timerfd_settime(term->render.icon.timer_fd, 0, &timeout, NULL); + return; + } + + const char *icon_name = term_icon(term); + LOG_DBG("setting toplevel icon: %s", icon_name); + + struct xdg_toplevel_icon_v1 *icon = + xdg_toplevel_icon_manager_v1_create_icon(term->wl->toplevel_icon_manager); + xdg_toplevel_icon_v1_set_name(icon, icon_name); + xdg_toplevel_icon_manager_v1_set_icon(term->wl->toplevel_icon_manager, + term->window->xdg_toplevel, icon); + xdg_toplevel_icon_v1_destroy(icon); + + term->render.icon.last_update = now; +} + +void render_refresh(struct terminal *term) { term->render.refresh.grid = true; } + +void render_refresh_csd(struct terminal *term) { + if (term->window->csd_mode == CSD_YES) + term->render.refresh.csd = true; +} + +void render_refresh_search(struct terminal *term) { + if (term->is_searching) + term->render.refresh.search = true; +} + +void render_refresh_urls(struct terminal *term) { + if (urls_mode_is_active(term)) + term->render.refresh.urls = true; +} + +void render_refresh_tab_bar(struct terminal *term) { + if (term->conf->tabs.enabled) + term->render.refresh.grid = + true; /* triggers full re-render which includes tab bar */ +} + +static void draw_rounded_corner_aa(pixman_image_t *pix, + const pixman_color_t *color, int dx, int dy, + int r, int cx_inside, int cy_inside) { + if (r <= 0) + return; + + const int stride = (r + 3) & ~3; /* A8 stride aligned to 4 bytes */ + uint8_t *data = xcalloc(stride * r, sizeof(uint8_t)); + + /* Circle center in local (0..r, 0..r) coords of the corner tile. */ + const float cx = cx_inside > 0 ? (float)r : 0.f; + const float cy = cy_inside > 0 ? (float)r : 0.f; + const float r_f = (float)r; + + for (int py = 0; py < r; py++) { + for (int px = 0; px < r; px++) { + const float ex = (float)px + 0.5f - cx; + const float ey = (float)py + 0.5f - cy; + const float dist = sqrtf(ex * ex + ey * ey); + float cov = r_f - dist + 0.5f; + if (cov <= 0.f) + cov = 0.f; + else if (cov >= 1.f) + cov = 1.f; + data[py * stride + px] = (uint8_t)(cov * 255.f + 0.5f); } + } - struct timespec now; - if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) - return; - - struct timespec diff; - timespec_sub(&now, &term->render.icon.last_update, &diff); - - if (diff.tv_sec == 0 && diff.tv_nsec < 8333 * 1000) { - const struct itimerspec timeout = { - .it_value = {.tv_nsec = 8333 * 1000 - diff.tv_nsec}, - }; - - timerfd_settime(term->render.icon.timer_fd, 0, &timeout, NULL); - return; - } - - const char *icon_name = term_icon(term); - LOG_DBG("setting toplevel icon: %s", icon_name); - - struct xdg_toplevel_icon_v1 *icon = - xdg_toplevel_icon_manager_v1_create_icon(term->wl->toplevel_icon_manager); - xdg_toplevel_icon_v1_set_name(icon, icon_name); - xdg_toplevel_icon_manager_v1_set_icon( - term->wl->toplevel_icon_manager, term->window->xdg_toplevel, icon); - xdg_toplevel_icon_v1_destroy(icon); - - term->render.icon.last_update = now; + pixman_image_t *mask = + pixman_image_create_bits(PIXMAN_a8, r, r, (uint32_t *)data, stride); + pixman_image_t *src = pixman_image_create_solid_fill(color); + pixman_image_composite32(PIXMAN_OP_OVER, src, mask, pix, 0, 0, 0, 0, dx, dy, + r, r); + pixman_image_unref(mask); + pixman_image_unref(src); + free(data); } -void -render_refresh(struct terminal *term) -{ - term->render.refresh.grid = true; -} - -void -render_refresh_csd(struct terminal *term) -{ - if (term->window->csd_mode == CSD_YES) - term->render.refresh.csd = true; -} - -void -render_refresh_search(struct terminal *term) -{ - if (term->is_searching) - term->render.refresh.search = true; -} - -void -render_refresh_urls(struct terminal *term) -{ - if (urls_mode_is_active(term)) - term->render.refresh.urls = true; -} - -void -render_refresh_tab_bar(struct terminal *term) -{ - if (term->conf->tabs.enabled) - term->render.refresh.grid = true; /* triggers full re-render which includes tab bar */ -} - -static void -draw_rounded_corner_aa(pixman_image_t *pix, const pixman_color_t *color, - int dx, int dy, int r, - int cx_inside, int cy_inside) -{ - if (r <= 0) - return; - - const int stride = (r + 3) & ~3; /* A8 stride aligned to 4 bytes */ - uint8_t *data = xcalloc(stride * r, sizeof(uint8_t)); - - /* Circle center in local (0..r, 0..r) coords of the corner tile. */ - const float cx = cx_inside > 0 ? (float)r : 0.f; - const float cy = cy_inside > 0 ? (float)r : 0.f; - const float r_f = (float)r; - - for (int py = 0; py < r; py++) { - for (int px = 0; px < r; px++) { - const float ex = (float)px + 0.5f - cx; - const float ey = (float)py + 0.5f - cy; - const float dist = sqrtf(ex * ex + ey * ey); - float cov = r_f - dist + 0.5f; - if (cov <= 0.f) - cov = 0.f; - else if (cov >= 1.f) - cov = 1.f; - data[py * stride + px] = (uint8_t)(cov * 255.f + 0.5f); - } - } - - pixman_image_t *mask = pixman_image_create_bits( - PIXMAN_a8, r, r, (uint32_t *)data, stride); - pixman_image_t *src = pixman_image_create_solid_fill(color); - pixman_image_composite32(PIXMAN_OP_OVER, - src, mask, pix, 0, 0, 0, 0, dx, dy, r, r); - pixman_image_unref(mask); - pixman_image_unref(src); - free(data); -} - -static void -draw_rounded_rect(pixman_image_t *pix, const pixman_color_t *color, - int x, int y, int w, int h, int r, unsigned corners) -{ - if (r <= 0 || w <= 0 || h <= 0) { - pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, - &(pixman_rectangle16_t){x, y, w, h}); - return; - } - r = min(r, min(w / 2, h / 2)); - - const bool tl = corners & 1; - const bool tr = corners & 2; - const bool bl = corners & 4; - const bool br = corners & 8; - - /* Fill body regions. Corners handled separately if rounded. */ - /* Top band */ +static void draw_rounded_rect(pixman_image_t *pix, const pixman_color_t *color, + int x, int y, int w, int h, int r, + unsigned corners) { + if (r <= 0 || w <= 0 || h <= 0) { pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, - &(pixman_rectangle16_t){x + (tl ? r : 0), y, - w - (tl ? r : 0) - (tr ? r : 0), r}); - /* Middle band (full width) */ - pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, - &(pixman_rectangle16_t){x, y + r, w, h - 2 * r}); - /* Bottom band */ - pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, - &(pixman_rectangle16_t){x + (bl ? r : 0), y + h - r, - w - (bl ? r : 0) - (br ? r : 0), r}); + &(pixman_rectangle16_t){x, y, w, h}); + return; + } + r = min(r, min(w / 2, h / 2)); - /* Square corners: fill the r×r square. */ - if (!tl) - pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, - &(pixman_rectangle16_t){x, y, r, r}); - if (!tr) - pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, - &(pixman_rectangle16_t){x + w - r, y, r, r}); - if (!bl) - pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, - &(pixman_rectangle16_t){x, y + h - r, r, r}); - if (!br) - pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, - &(pixman_rectangle16_t){x + w - r, y + h - r, r, r}); + const bool tl = corners & 1; + const bool tr = corners & 2; + const bool bl = corners & 4; + const bool br = corners & 8; - /* Rounded corners: anti-aliased quarter-circles. */ - if (tl) draw_rounded_corner_aa(pix, color, x, y, r, 1, 1); - if (tr) draw_rounded_corner_aa(pix, color, x + w - r, y, r, -1, 1); - if (bl) draw_rounded_corner_aa(pix, color, x, y + h - r, r, 1, -1); - if (br) draw_rounded_corner_aa(pix, color, x + w - r, y + h - r, r, -1, -1); + /* Fill body regions. Corners handled separately if rounded. */ + /* Top band */ + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){x + (tl ? r : 0), y, + w - (tl ? r : 0) - (tr ? r : 0), r}); + /* Middle band (full width) */ + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){x, y + r, w, h - 2 * r}); + /* Bottom band */ + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){x + (bl ? r : 0), y + h - r, + w - (bl ? r : 0) - (br ? r : 0), r}); + + /* Square corners: fill the r×r square. */ + if (!tl) + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){x, y, r, r}); + if (!tr) + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){x + w - r, y, r, r}); + if (!bl) + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){x, y + h - r, r, r}); + if (!br) + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){x + w - r, y + h - r, r, r}); + + /* Rounded corners: anti-aliased quarter-circles. */ + if (tl) + draw_rounded_corner_aa(pix, color, x, y, r, 1, 1); + if (tr) + draw_rounded_corner_aa(pix, color, x + w - r, y, r, -1, 1); + if (bl) + draw_rounded_corner_aa(pix, color, x, y + h - r, r, 1, -1); + if (br) + draw_rounded_corner_aa(pix, color, x + w - r, y + h - r, r, -1, -1); } -static int -tab_label_build(char *buf, size_t bufsz, - const struct terminal *tab, size_t idx) -{ - const char *title = NULL; - if (tab->window_title_has_been_set && tab->window_title != NULL) - title = tab->window_title; - else if (tab->cwd != NULL) { - const char *slash = strrchr(tab->cwd, '/'); - title = (slash != NULL && slash[1] != '\0') ? slash + 1 : tab->cwd; - } - int len = snprintf(buf, bufsz, "%zu: %s", idx + 1, - title != NULL ? title : ""); - if (len < 0) - return 0; - if ((size_t)len >= bufsz) - len = (int)bufsz - 1; - return len; +static int tab_label_build(char *buf, size_t bufsz, const struct terminal *tab, + size_t idx) { + const char *title = NULL; + if (tab->window_title_has_been_set && tab->window_title != NULL) + title = tab->window_title; + else if (tab->cwd != NULL) { + const char *slash = strrchr(tab->cwd, '/'); + title = (slash != NULL && slash[1] != '\0') ? slash + 1 : tab->cwd; + } + int len = + snprintf(buf, bufsz, "%zu: %s", idx + 1, title != NULL ? title : ""); + if (len < 0) + return 0; + if ((size_t)len >= bufsz) + len = (int)bufsz - 1; + return len; } -static int -tab_label_width(struct fcft_font *font, const char *buf, int len) -{ - if (font == NULL || len <= 0) - return 0; +static int tab_label_width(struct fcft_font *font, const char *buf, int len) { + if (font == NULL || len <= 0) + return 0; - int total = 0; - const unsigned char *p = (const unsigned char *)buf; - const unsigned char *end = p + len; - while (p < end) { - utf8proc_int32_t cp; - utf8proc_ssize_t consumed = utf8proc_iterate(p, end - p, &cp); - if (consumed <= 0 || cp < 0) { - p++; - continue; - } - p += consumed; - - const struct fcft_glyph *g = fcft_rasterize_char_utf32( - font, (uint32_t)cp, FCFT_SUBPIXEL_NONE); - if (g != NULL) - total += g->advance.x; + int total = 0; + const unsigned char *p = (const unsigned char *)buf; + const unsigned char *end = p + len; + while (p < end) { + utf8proc_int32_t cp; + utf8proc_ssize_t consumed = utf8proc_iterate(p, end - p, &cp); + if (consumed <= 0 || cp < 0) { + p++; + continue; } - return total; + p += consumed; + + const struct fcft_glyph *g = + fcft_rasterize_char_utf32(font, (uint32_t)cp, FCFT_SUBPIXEL_NONE); + if (g != NULL) + total += g->advance.x; + } + return total; } /* Width (px) of the unread indicator + trailing gap, or 0 if disabled */ -static int -tab_unread_indicator_width(struct fcft_font *font, const struct config *conf, - bool has_unread, float scale) -{ - if (!has_unread || font == NULL || - conf->tabs.unread_indicator == NULL || - conf->tabs.unread_indicator[0] == '\0') - return 0; +static int tab_unread_indicator_width(struct fcft_font *font, + const struct config *conf, + bool has_unread, float scale) { + if (!has_unread || font == NULL || conf->tabs.unread_indicator == NULL || + conf->tabs.unread_indicator[0] == '\0') + return 0; - int w = tab_label_width(font, conf->tabs.unread_indicator, - (int)strlen(conf->tabs.unread_indicator)); - if (w <= 0) - return 0; - return w + (int)roundf(4 * scale); + int w = tab_label_width(font, conf->tabs.unread_indicator, + (int)strlen(conf->tabs.unread_indicator)); + if (w <= 0) + return 0; + return w + (int)roundf(4 * scale); } /* Draw the unread indicator at (x, baseline_y); returns the x offset to * advance past the indicator (including trailing gap). */ -static int -render_tab_unread_indicator(pixman_image_t *pix, struct fcft_font *font, - const struct config *conf, float scale, - int x, int baseline_y, bool gamma_correct) -{ - if (font == NULL || conf->tabs.unread_indicator == NULL || - conf->tabs.unread_indicator[0] == '\0') - return 0; +static int render_tab_unread_indicator(pixman_image_t *pix, + struct fcft_font *font, + const struct config *conf, float scale, + int x, int baseline_y, + bool gamma_correct) { + if (font == NULL || conf->tabs.unread_indicator == NULL || + conf->tabs.unread_indicator[0] == '\0') + return 0; - const pixman_color_t fg = color_hex_to_pixman( - conf->tabs.colors.unread_fg, gamma_correct); + const pixman_color_t fg = + color_hex_to_pixman(conf->tabs.colors.unread_fg, gamma_correct); - int gx = x; - const unsigned char *p = (const unsigned char *)conf->tabs.unread_indicator; - const unsigned char *end = p + strlen(conf->tabs.unread_indicator); + int gx = x; + const unsigned char *p = (const unsigned char *)conf->tabs.unread_indicator; + const unsigned char *end = p + strlen(conf->tabs.unread_indicator); - while (p < end) { - utf8proc_int32_t cp; - utf8proc_ssize_t consumed = utf8proc_iterate(p, end - p, &cp); - if (consumed <= 0 || cp < 0) { - p++; - continue; - } - p += consumed; - - const struct fcft_glyph *g = fcft_rasterize_char_utf32( - font, (uint32_t)cp, FCFT_SUBPIXEL_NONE); - if (g == NULL) - continue; - - if (pixman_image_get_format(g->pix) == PIXMAN_a8r8g8b8) { - pixman_image_composite32(PIXMAN_OP_OVER, - g->pix, NULL, pix, 0, 0, 0, 0, - gx + g->x, baseline_y - g->y, g->width, g->height); - } else { - pixman_image_t *src = pixman_image_create_solid_fill(&fg); - pixman_image_composite32(PIXMAN_OP_OVER, - src, g->pix, pix, 0, 0, 0, 0, - gx + g->x, baseline_y - g->y, g->width, g->height); - pixman_image_unref(src); - } - gx += g->advance.x; + while (p < end) { + utf8proc_int32_t cp; + utf8proc_ssize_t consumed = utf8proc_iterate(p, end - p, &cp); + if (consumed <= 0 || cp < 0) { + p++; + continue; } - return (gx - x) + (int)roundf(4 * scale); + p += consumed; + + const struct fcft_glyph *g = + fcft_rasterize_char_utf32(font, (uint32_t)cp, FCFT_SUBPIXEL_NONE); + if (g == NULL) + continue; + + if (pixman_image_get_format(g->pix) == PIXMAN_a8r8g8b8) { + pixman_image_composite32(PIXMAN_OP_OVER, g->pix, NULL, pix, 0, 0, 0, 0, + gx + g->x, baseline_y - g->y, g->width, + g->height); + } else { + pixman_image_t *src = pixman_image_create_solid_fill(&fg); + pixman_image_composite32(PIXMAN_OP_OVER, src, g->pix, pix, 0, 0, 0, 0, + gx + g->x, baseline_y - g->y, g->width, + g->height); + pixman_image_unref(src); + } + gx += g->advance.x; + } + return (gx - x) + (int)roundf(4 * scale); } -static void -render_tab_label(pixman_image_t *pix, struct fcft_font *font, - const pixman_color_t *fg, const struct terminal *tab, - size_t idx, int x, int y, int max_x, float scale) -{ - char label_buf[256]; - int label_len = tab_label_build(label_buf, sizeof(label_buf), tab, idx); - if (label_len <= 0 || font == NULL) - return; +static void render_tab_label(pixman_image_t *pix, struct fcft_font *font, + const pixman_color_t *fg, + const struct terminal *tab, size_t idx, int x, + int y, int max_x, float scale) { + char label_buf[256]; + int label_len = tab_label_build(label_buf, sizeof(label_buf), tab, idx); + if (label_len <= 0 || font == NULL) + return; - const int pad = (int)roundf(tab->conf->tabs.label_padding * scale); - int gx = x + pad; - const int clip_x = max_x - pad; + const int pad = (int)roundf(tab->conf->tabs.label_padding * scale); + int gx = x + pad; + const int clip_x = max_x - pad; - const unsigned char *p = (const unsigned char *)label_buf; - const unsigned char *end = p + label_len; - while (p < end) { - utf8proc_int32_t cp; - utf8proc_ssize_t consumed = utf8proc_iterate(p, end - p, &cp); - if (consumed <= 0 || cp < 0) { - p++; - continue; - } - p += consumed; - - const struct fcft_glyph *glyph = fcft_rasterize_char_utf32( - font, (uint32_t)cp, FCFT_SUBPIXEL_NONE); - if (glyph == NULL) - continue; - - if (gx + glyph->advance.x > clip_x) - break; - - if (pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8) { - pixman_image_composite32(PIXMAN_OP_OVER, - glyph->pix, NULL, pix, 0, 0, 0, 0, - gx + glyph->x, y - glyph->y, - glyph->width, glyph->height); - } else { - pixman_image_t *src = pixman_image_create_solid_fill(fg); - pixman_image_composite32(PIXMAN_OP_OVER, - src, glyph->pix, pix, 0, 0, 0, 0, - gx + glyph->x, y - glyph->y, - glyph->width, glyph->height); - pixman_image_unref(src); - } - gx += glyph->advance.x; + const unsigned char *p = (const unsigned char *)label_buf; + const unsigned char *end = p + label_len; + while (p < end) { + utf8proc_int32_t cp; + utf8proc_ssize_t consumed = utf8proc_iterate(p, end - p, &cp); + if (consumed <= 0 || cp < 0) { + p++; + continue; } + p += consumed; + + const struct fcft_glyph *glyph = + fcft_rasterize_char_utf32(font, (uint32_t)cp, FCFT_SUBPIXEL_NONE); + if (glyph == NULL) + continue; + + if (gx + glyph->advance.x > clip_x) + break; + + if (pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8) { + pixman_image_composite32(PIXMAN_OP_OVER, glyph->pix, NULL, pix, 0, 0, 0, + 0, gx + glyph->x, y - glyph->y, glyph->width, + glyph->height); + } else { + pixman_image_t *src = pixman_image_create_solid_fill(fg); + pixman_image_composite32(PIXMAN_OP_OVER, src, glyph->pix, pix, 0, 0, 0, 0, + gx + glyph->x, y - glyph->y, glyph->width, + glyph->height); + pixman_image_unref(src); + } + gx += glyph->advance.x; + } } /* Greyscale ramp indices for the gradient tab style. * The 256-color palette greyscale ramp lives at indices 232..255. */ -#define GRADIENT_BAR_BG_IDX 234 -#define GRADIENT_PILL_ACTIVE_IDX 250 +#define GRADIENT_BAR_BG_IDX 234 +#define GRADIENT_PILL_ACTIVE_IDX 250 #define GRADIENT_PILL_INACTIVE_IDX 240 -#define GRADIENT_FG_ACTIVE_IDX 232 -#define GRADIENT_FG_INACTIVE_IDX 250 +#define GRADIENT_FG_ACTIVE_IDX 232 +#define GRADIENT_FG_INACTIVE_IDX 250 /* Hardcoded fade level masks (braille bitmasks); index = fade level (1..6). * Mirrors the visual progression: ⠐ ⠡ ⡐ ⢔ ⣑ ⣪ */ @@ -5699,302 +5656,310 @@ static const uint8_t gradient_fade_masks[7] = { }; /* Braille bit (0..7) → (col, row) within the 2-col × 4-row dot grid */ -static const struct { uint8_t col, row; } gradient_dot_pos[8] = { +static const struct { + uint8_t col, row; +} gradient_dot_pos[8] = { {0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, {1, 2}, {0, 3}, {1, 3}, }; -static void -draw_gradient_pill(pixman_image_t *pix, const struct terminal *term, - uint8_t pill_idx, uint8_t bar_bg_idx, bool gamma_correct, - int x, int y, int w, int h, float scale) -{ - const int n_levels = (int)ALEN(gradient_fade_masks) - 1; /* 6 */ - const int cell_w = max(2, (int)roundf(scale * 4)); - const int fade_w = n_levels * cell_w; +static void draw_gradient_pill(pixman_image_t *pix, const struct terminal *term, + uint8_t pill_idx, uint8_t bar_bg_idx, + bool gamma_correct, int x, int y, int w, int h, + float scale) { + const int n_levels = (int)ALEN(gradient_fade_masks) - 1; /* 6 */ + const int cell_w = max(2, (int)roundf(scale * 4)); + const int fade_w = n_levels * cell_w; - const pixman_color_t pill = color_hex_to_pixman( - term->colors.table[pill_idx], gamma_correct); + const pixman_color_t pill = + color_hex_to_pixman(term->colors.table[pill_idx], gamma_correct); - if (w <= 2 * fade_w) { - /* Tab too narrow for full fades — draw it solid */ - pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, &pill, - 1, &(pixman_rectangle16_t){x, y, w, h}); - return; - } - - /* Solid pill interior */ - pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, &pill, - 1, &(pixman_rectangle16_t){x + fade_w, y, w - 2 * fade_w, h}); - - const int dot_size = max(1, (int)roundf(scale)); - const float sub_w = (float)cell_w / 2.0f; - const float sub_h = (float)h / 4.0f; - - const int inner = (int)pill_idx - 1; - const int outer = (int)bar_bg_idx; - const int range = inner - outer; - const int t_den = max(1, n_levels - 1); - - for (int side = 0; side < 2; side++) { - const bool left = (side == 0); - for (int i = 1; i <= n_levels; i++) { - /* i == 1 is the outermost cell, i == n_levels the innermost */ - const int cell_x = left - ? x + (i - 1) * cell_w - : x + w - i * cell_w; - - const uint8_t band_idx = (uint8_t)( - outer + (range * (i - 1) + t_den / 2) / t_den); - const uint8_t dot_idx = band_idx > 233 ? band_idx - 2 : 232; - - const pixman_color_t band = color_hex_to_pixman( - term->colors.table[band_idx], gamma_correct); - pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, &band, - 1, &(pixman_rectangle16_t){cell_x, y, cell_w, h}); - - const uint8_t mask = gradient_fade_masks[i]; - const pixman_color_t dot = color_hex_to_pixman( - term->colors.table[dot_idx], gamma_correct); - - pixman_rectangle16_t dot_rects[8]; - int n_dots = 0; - for (int b = 0; b < 8; b++) { - if (!(mask & (1u << b))) - continue; - const int dx = (int)roundf((gradient_dot_pos[b].col + 0.5f) * sub_w) - - dot_size / 2; - const int dy = (int)roundf((gradient_dot_pos[b].row + 0.5f) * sub_h) - - dot_size / 2; - dot_rects[n_dots++] = (pixman_rectangle16_t){ - cell_x + dx, y + dy, dot_size, dot_size, - }; - } - if (n_dots > 0) - pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, &dot, - n_dots, dot_rects); - } + if (w <= 2 * fade_w) { + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, &pill, 1, + &(pixman_rectangle16_t){x, y, w, h}); + return; + } + + /* Solid pill interior */ + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, &pill, 1, + &(pixman_rectangle16_t){x + fade_w, y, w - 2 * fade_w, h}); + + const int dot_size = max(1, (int)roundf(scale)); + const float sub_w = (float)cell_w / 2.0f; + const float sub_h = (float)h / 4.0f; + + const int inner = (int)pill_idx - 1; + const int outer = (int)bar_bg_idx; + const int range = inner - outer; + const int t_den = max(1, n_levels - 1); + + for (int side = 0; side < 2; side++) { + const bool left = (side == 0); + for (int i = 1; i <= n_levels; i++) { + /* i == 1 is the outermost cell, i == n_levels the innermost */ + const int cell_x = left ? x + (i - 1) * cell_w : x + w - i * cell_w; + + const uint8_t band_idx = + (uint8_t)(outer + (range * (i - 1) + t_den / 2) / t_den); + const uint8_t dot_idx = band_idx > 233 ? band_idx - 2 : 232; + + const pixman_color_t band = + color_hex_to_pixman(term->colors.table[band_idx], gamma_correct); + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, &band, 1, + &(pixman_rectangle16_t){cell_x, y, cell_w, h}); + + const uint8_t mask = gradient_fade_masks[i]; + const pixman_color_t dot = + color_hex_to_pixman(term->colors.table[dot_idx], gamma_correct); + + pixman_rectangle16_t dot_rects[8]; + int n_dots = 0; + for (int b = 0; b < 8; b++) { + if (!(mask & (1u << b))) + continue; + const int dx = (int)roundf((gradient_dot_pos[b].col + 0.5f) * sub_w) - + dot_size / 2; + const int dy = (int)roundf((gradient_dot_pos[b].row + 0.5f) * sub_h) - + dot_size / 2; + dot_rects[n_dots++] = (pixman_rectangle16_t){ + cell_x + dx, + y + dy, + dot_size, + dot_size, + }; + } + if (n_dots > 0) + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, &dot, n_dots, + dot_rects); } + } } -static void -render_tab_bar(struct terminal *term) -{ - struct wl_window *win = term->window; - if (win->tab_bar.sub == NULL) - return; +static void render_tab_bar(struct terminal *term) { + struct wl_window *win = term->window; + if (win->tab_bar.sub == NULL) + return; - const struct config *conf = term->conf; - const float scale = term->scale; - const bool floating = (conf->tabs.layout == CONF_TABS_LAYOUT_FLOATING); - const bool pos_top = (conf->tabs.position == CONF_TABS_POSITION_TOP); + const struct config *conf = term->conf; + const float scale = term->scale; + const bool floating = (conf->tabs.layout == CONF_TABS_LAYOUT_FLOATING); + const bool pos_top = (conf->tabs.position == CONF_TABS_POSITION_TOP); - /* pill height is always conf->tabs.height; total bar = pill + margin in floating */ - const int margin_px = floating ? (int)roundf(scale * conf->tabs.margin) : 0; - const int tab_h_px = (int)roundf(scale * conf->tabs.height); - const int total_h = tab_h_px + margin_px; - const int width = term->width; + /* pill height is always conf->tabs.height; total bar = pill + margin in + * floating */ + const int margin_px = floating ? (int)roundf(scale * conf->tabs.margin) : 0; + const int tab_h_px = (int)roundf(scale * conf->tabs.height); + const int total_h = tab_h_px + margin_px; + const int width = term->width; - if (width <= 0 || total_h <= 0) - return; + if (width <= 0 || total_h <= 0) + return; - struct buffer_chain *chain = term->render.chains.tab_bar; - struct buffer *buf = shm_get_buffer(chain, width, total_h); - if (buf == NULL) - return; + struct buffer_chain *chain = term->render.chains.tab_bar; + struct buffer *buf = shm_get_buffer(chain, width, total_h); + if (buf == NULL) + return; - const bool gamma_correct = wayl_do_linear_blending(win->term->wl, conf); - const bool rounded = (conf->tabs.style == CONF_TABS_STYLE_ROUNDED); - const bool gradient = (conf->tabs.style == CONF_TABS_STYLE_GRADIENT); - const int r = rounded ? (int)roundf(scale * conf->tabs.corner_radius) : 0; + const bool gamma_correct = wayl_do_linear_blending(win->term->wl, conf); + const bool rounded = (conf->tabs.style == CONF_TABS_STYLE_ROUNDED); + const bool gradient = (conf->tabs.style == CONF_TABS_STYLE_GRADIENT); + const int r = rounded ? (int)roundf(scale * conf->tabs.corner_radius) : 0; - /* Clear buffer: transparent for floating (shows terminal behind gaps), bg for span. - * Gradient style overrides the configured bar bg with a fixed greyscale ramp index. */ - const pixman_color_t transparent = {0, 0, 0, 0}; - const pixman_color_t bg_color = gradient - ? color_hex_to_pixman(term->colors.table[GRADIENT_BAR_BG_IDX], gamma_correct) - : color_hex_to_pixman(conf->tabs.colors.bg, gamma_correct); - pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], - floating ? &transparent : &bg_color, - 1, &(pixman_rectangle16_t){0, 0, width, total_h}); + /* Clear buffer: transparent for floating (shows terminal behind gaps), bg for + * span. Gradient style overrides the configured bar bg with a fixed greyscale + * ramp index. */ + const pixman_color_t transparent = {0, 0, 0, 0}; + const pixman_color_t bg_color = + gradient ? color_hex_to_pixman(term->colors.table[GRADIENT_BAR_BG_IDX], + gamma_correct) + : color_hex_to_pixman(conf->tabs.colors.bg, gamma_correct); + pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], + floating ? &transparent : &bg_color, 1, + &(pixman_rectangle16_t){0, 0, width, total_h}); - const size_t tab_count = win->tab_count; - if (tab_count == 0) - goto commit; + const size_t tab_count = win->tab_count; + if (tab_count == 0) + goto commit; - struct fcft_font *font = term->fonts[0]; + struct fcft_font *font = term->fonts[0]; - /* Per-tab layout arrays */ - int tab_xs[TAB_MAX]; - int tab_ws[TAB_MAX]; - int tab_y, tab_h; + /* Per-tab layout arrays */ + int tab_xs[TAB_MAX]; + int tab_ws[TAB_MAX]; + int tab_y, tab_h; - const size_t n = min(tab_count, (size_t)TAB_MAX); + const size_t n = min(tab_count, (size_t)TAB_MAX); - if (floating) { - const int pad_px = (int)roundf(scale * conf->tabs.tab_padding); - const int max_tw = (int)roundf(scale * conf->tabs.tab_width); - const int label_pad = (int)roundf(scale * conf->tabs.label_padding); - const int min_tw = max(2 * label_pad, 1); - - /* Measure each tab's natural width (label + padding), capped at max_tw */ - int natural_w[TAB_MAX]; - int total_w = 0; - for (size_t i = 0; i < n; i++) { - char buf_s[256]; - int len = tab_label_build(buf_s, sizeof(buf_s), win->tabs[i], i); - int lw = tab_label_width(font, buf_s, len); - int iw = tab_unread_indicator_width( - font, conf, win->tabs[i]->has_unread, scale); - int w = lw + iw + 2 * label_pad; - if (w > max_tw) w = max_tw; - if (w < min_tw) w = min_tw; - natural_w[i] = w; - total_w += w; - } - const int total_pads = pad_px * ((int)n - 1); - int bar_w = total_w + total_pads; - - /* Shrink uniformly if we overflow the bar width */ - if (bar_w > width && total_w > 0) { - const int avail = max(width - total_pads, (int)n); - int used = 0; - for (size_t i = 0; i < n; i++) { - int w = (int)((int64_t)natural_w[i] * avail / total_w); - if (w < 1) w = 1; - tab_ws[i] = w; - used += w; - } - /* Distribute rounding residue to the first few tabs */ - int residue = avail - used; - for (size_t i = 0; i < n && residue > 0; i++, residue--) - tab_ws[i]++; - bar_w = avail + total_pads; - } else { - for (size_t i = 0; i < n; i++) - tab_ws[i] = natural_w[i]; - } - - int x_cur = (width - bar_w) / 2; - for (size_t i = 0; i < n; i++) { - tab_xs[i] = x_cur; - x_cur += tab_ws[i] + pad_px; - } - - tab_y = pos_top ? margin_px : 0; - tab_h = tab_h_px; - } else { - const int base = width / (int)n; - int x_cur = 0; - for (size_t i = 0; i < n; i++) { - tab_xs[i] = x_cur; - tab_ws[i] = (i + 1 == n) ? (width - x_cur) : base; - x_cur += base; - } - tab_y = 0; - tab_h = tab_h_px; - } - - /* Text baseline centered within the pill */ - const int text_baseline = tab_y + (font - ? (tab_h - (int)(font->ascent + font->descent)) / 2 + (int)font->ascent - : tab_h / 2); + if (floating) { + const int pad_px = (int)roundf(scale * conf->tabs.tab_padding); + const int max_tw = (int)roundf(scale * conf->tabs.tab_width); + const int label_pad = (int)roundf(scale * conf->tabs.label_padding); + const int min_tw = max(2 * label_pad, 1); + /* Measure each tab's natural width (label + padding), capped at max_tw */ + int natural_w[TAB_MAX]; + int total_w = 0; for (size_t i = 0; i < n; i++) { - struct terminal *tab = win->tabs[i]; - const bool is_active = (i == win->active_tab); - const int x = tab_xs[i]; - const int w = tab_ws[i]; - - const pixman_color_t fg_color = gradient - ? color_hex_to_pixman( - term->colors.table[is_active - ? GRADIENT_FG_ACTIVE_IDX : GRADIENT_FG_INACTIVE_IDX], - gamma_correct) - : color_hex_to_pixman( - is_active ? conf->tabs.colors.active_fg : conf->tabs.colors.fg, - gamma_correct); - - if (gradient) { - const uint8_t pill_idx = is_active - ? GRADIENT_PILL_ACTIVE_IDX : GRADIENT_PILL_INACTIVE_IDX; - draw_gradient_pill(buf->pix[0], term, pill_idx, GRADIENT_BAR_BG_IDX, - gamma_correct, x, tab_y, w, tab_h, scale); - } else if (rounded) { - /* Floating: all 4 corners rounded. Span: only the open edge rounded. */ - const pixman_color_t tab_bg = color_hex_to_pixman( - is_active ? conf->tabs.colors.active_bg : conf->tabs.colors.bg, - gamma_correct); - unsigned corners; - if (floating) { - corners = 0xf; /* all corners */ - } else if (pos_top) { - corners = 0x3; /* top-left + top-right */ - } else { - corners = 0xc; /* bottom-left + bottom-right */ - } - draw_rounded_rect(buf->pix[0], &tab_bg, x, tab_y, w, tab_h, r, corners); - } else { - const pixman_color_t tab_bg = color_hex_to_pixman( - is_active ? conf->tabs.colors.active_bg : conf->tabs.colors.bg, - gamma_correct); - pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], &tab_bg, - 1, &(pixman_rectangle16_t){x, tab_y, w, tab_h}); - } - - /* Span: separator between inactive tabs (skipped for gradient — fades - * already provide visual separation) */ - if (!floating && !is_active && i + 1 < n && !gradient) { - const pixman_color_t sep = color_hex_to_pixman( - conf->tabs.colors.bg, gamma_correct); - pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], &sep, - 1, &(pixman_rectangle16_t){x + w - 1, tab_y + 2, 1, tab_h - 4}); - } - - if (font != NULL) { - int label_x = x; - if (tab->has_unread) { - const int pad = (int)roundf(conf->tabs.label_padding * scale); - int adv = render_tab_unread_indicator( - buf->pix[0], font, conf, scale, - x + pad, text_baseline, gamma_correct); - label_x = x + adv; - } - render_tab_label(buf->pix[0], font, &fg_color, tab, i, - label_x, text_baseline, x + w, scale); - } + char buf_s[256]; + int len = tab_label_build(buf_s, sizeof(buf_s), win->tabs[i], i); + int lw = tab_label_width(font, buf_s, len); + int iw = tab_unread_indicator_width(font, conf, win->tabs[i]->has_unread, + scale); + int w = lw + iw + 2 * label_pad; + if (w > max_tw) + w = max_tw; + if (w < min_tw) + w = min_tw; + natural_w[i] = w; + total_w += w; } + const int total_pads = pad_px * ((int)n - 1); + int bar_w = total_w + total_pads; - /* Publish layout for hit-testing */ - for (size_t i = 0; i < n; i++) { - win->tab_layout.xs[i] = tab_xs[i]; - win->tab_layout.ws[i] = tab_ws[i]; - } - win->tab_layout.y = tab_y; - win->tab_layout.h = tab_h; - win->tab_layout.count = n; - -commit: ; - if (tab_count == 0) - win->tab_layout.count = 0; - const int y_pos = pos_top ? 0 : term->height - total_h; - wl_subsurface_set_position(win->tab_bar.sub, 0, (int)roundf(y_pos / scale)); - - wayl_surface_scale(win, &win->tab_bar.surface, buf, scale); - wl_surface_attach(win->tab_bar.surface.surf, buf->wl_buf, 0, 0); - wl_surface_damage_buffer(win->tab_bar.surface.surf, 0, 0, width, total_h); - - if (!floating) { - struct wl_region *region = wl_compositor_create_region(term->wl->compositor); - if (region != NULL) { - wl_region_add(region, 0, 0, width, total_h); - wl_surface_set_opaque_region(win->tab_bar.surface.surf, region); - wl_region_destroy(region); - } + /* Shrink uniformly if we overflow the bar width */ + if (bar_w > width && total_w > 0) { + const int avail = max(width - total_pads, (int)n); + int used = 0; + for (size_t i = 0; i < n; i++) { + int w = (int)((int64_t)natural_w[i] * avail / total_w); + if (w < 1) + w = 1; + tab_ws[i] = w; + used += w; + } + /* Distribute rounding residue to the first few tabs */ + int residue = avail - used; + for (size_t i = 0; i < n && residue > 0; i++, residue--) + tab_ws[i]++; + bar_w = avail + total_pads; } else { - wl_surface_set_opaque_region(win->tab_bar.surface.surf, NULL); + for (size_t i = 0; i < n; i++) + tab_ws[i] = natural_w[i]; } - wl_surface_commit(win->tab_bar.surface.surf); + int x_cur = (width - bar_w) / 2; + for (size_t i = 0; i < n; i++) { + tab_xs[i] = x_cur; + x_cur += tab_ws[i] + pad_px; + } + + tab_y = pos_top ? margin_px : 0; + tab_h = tab_h_px; + } else { + const int base = width / (int)n; + int x_cur = 0; + for (size_t i = 0; i < n; i++) { + tab_xs[i] = x_cur; + tab_ws[i] = (i + 1 == n) ? (width - x_cur) : base; + x_cur += base; + } + tab_y = 0; + tab_h = tab_h_px; + } + + /* Text baseline centered within the pill */ + const int text_baseline = + tab_y + (font ? (tab_h - (int)(font->ascent + font->descent)) / 2 + + (int)font->ascent + : tab_h / 2); + + for (size_t i = 0; i < n; i++) { + struct terminal *tab = win->tabs[i]; + const bool is_active = (i == win->active_tab); + const int x = tab_xs[i]; + const int w = tab_ws[i]; + + const pixman_color_t fg_color = + gradient ? color_hex_to_pixman( + term->colors.table[is_active ? GRADIENT_FG_ACTIVE_IDX + : GRADIENT_FG_INACTIVE_IDX], + gamma_correct) + : color_hex_to_pixman(is_active ? conf->tabs.colors.active_fg + : conf->tabs.colors.fg, + gamma_correct); + + if (gradient) { + const uint8_t pill_idx = + is_active ? GRADIENT_PILL_ACTIVE_IDX : GRADIENT_PILL_INACTIVE_IDX; + draw_gradient_pill(buf->pix[0], term, pill_idx, GRADIENT_BAR_BG_IDX, + gamma_correct, x, tab_y, w, tab_h, scale); + } else if (rounded) { + /* Floating: all 4 corners rounded. Span: only the open edge rounded. */ + const pixman_color_t tab_bg = color_hex_to_pixman( + is_active ? conf->tabs.colors.active_bg : conf->tabs.colors.bg, + gamma_correct); + unsigned corners; + if (floating) { + corners = 0xf; /* all corners */ + } else if (pos_top) { + corners = 0x3; /* top-left + top-right */ + } else { + corners = 0xc; /* bottom-left + bottom-right */ + } + draw_rounded_rect(buf->pix[0], &tab_bg, x, tab_y, w, tab_h, r, corners); + } else { + const pixman_color_t tab_bg = color_hex_to_pixman( + is_active ? conf->tabs.colors.active_bg : conf->tabs.colors.bg, + gamma_correct); + pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], &tab_bg, 1, + &(pixman_rectangle16_t){x, tab_y, w, tab_h}); + } + + if (!floating && !is_active && i + 1 < n && !gradient) { + const pixman_color_t sep = + color_hex_to_pixman(conf->tabs.colors.bg, gamma_correct); + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &sep, 1, + &(pixman_rectangle16_t){x + w - 1, tab_y + 2, 1, tab_h - 4}); + } + + if (font != NULL) { + int label_x = x; + if (tab->has_unread) { + const int pad = (int)roundf(conf->tabs.label_padding * scale); + int adv = + render_tab_unread_indicator(buf->pix[0], font, conf, scale, x + pad, + text_baseline, gamma_correct); + label_x = x + adv; + } + render_tab_label(buf->pix[0], font, &fg_color, tab, i, label_x, + text_baseline, x + w, scale); + } + } + + /* Publish layout for hit-testing */ + for (size_t i = 0; i < n; i++) { + win->tab_layout.xs[i] = tab_xs[i]; + win->tab_layout.ws[i] = tab_ws[i]; + } + win->tab_layout.y = tab_y; + win->tab_layout.h = tab_h; + win->tab_layout.count = n; + +commit:; + if (tab_count == 0) + win->tab_layout.count = 0; + const int y_pos = pos_top ? 0 : term->height - total_h; + wl_subsurface_set_position(win->tab_bar.sub, 0, (int)roundf(y_pos / scale)); + + wayl_surface_scale(win, &win->tab_bar.surface, buf, scale); + wl_surface_attach(win->tab_bar.surface.surf, buf->wl_buf, 0, 0); + wl_surface_damage_buffer(win->tab_bar.surface.surf, 0, 0, width, total_h); + + if (!floating) { + struct wl_region *region = + wl_compositor_create_region(term->wl->compositor); + if (region != NULL) { + wl_region_add(region, 0, 0, width, total_h); + wl_surface_set_opaque_region(win->tab_bar.surface.surf, region); + wl_region_destroy(region); + } + } else { + wl_surface_set_opaque_region(win->tab_bar.surface.surf, NULL); + } + + wl_surface_commit(win->tab_bar.surface.surf); } /* === Tab overview === @@ -6004,528 +5969,519 @@ commit: ; * opacity. The animation duration matches libadwaita's Adw.TabOverview. */ -#define TAB_OVERVIEW_DURATION_NS 400000000ull +#define TAB_OVERVIEW_DURATION_NS 400000000ull -static uint64_t -now_monotonic_ns(void) -{ - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec; +static uint64_t now_monotonic_ns(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec; } -static float -ease_out_cubic(float t) -{ - if (t <= 0.f) return 0.f; - if (t >= 1.f) return 1.f; - float u = 1.f - t; - return 1.f - u * u * u; +static float ease_out_cubic(float t) { + if (t <= 0.f) + return 0.f; + if (t >= 1.f) + return 1.f; + float u = 1.f - t; + return 1.f - u * u * u; } -bool -tab_overview_is_active(const struct wl_window *win) -{ - if (win == NULL) - return false; - const __typeof__(win->tab_overview_state) *s = &win->tab_overview_state; - return s->target_open || s->progress > 0.f || s->visible; +bool tab_overview_is_active(const struct wl_window *win) { + if (win == NULL) + return false; + const __typeof__(win->tab_overview_state) *s = &win->tab_overview_state; + return s->target_open || s->progress > 0.f || s->visible; } -void -tab_overview_toggle(struct wl_window *win) -{ - if (win == NULL || win->tab_overview.sub == NULL) - return; +void tab_overview_toggle(struct wl_window *win) { + if (win == NULL || win->tab_overview.sub == NULL) + return; - /* Snapshot current progress so the animation continues smoothly even - * if the user toggles mid-fade. */ - struct terminal *active = win->tabs[win->active_tab]; - if (active == NULL) - return; + /* Snapshot current progress so the animation continues smoothly even + * if the user toggles mid-fade. */ + struct terminal *active = win->tabs[win->active_tab]; + if (active == NULL) + return; - win->tab_overview_state.target_open = !win->tab_overview_state.target_open; - win->tab_overview_state.anim_from = win->tab_overview_state.progress; - win->tab_overview_state.anim_start_ns = now_monotonic_ns(); - win->tab_overview_state.sel_idx = win->active_tab; - win->tab_overview_state.hover_idx = -1; + win->tab_overview_state.target_open = !win->tab_overview_state.target_open; + win->tab_overview_state.anim_from = win->tab_overview_state.progress; + win->tab_overview_state.anim_start_ns = now_monotonic_ns(); + win->tab_overview_state.sel_idx = win->active_tab; + win->tab_overview_state.hover_idx = -1; + render_refresh(active); +} + +void tab_overview_close_instant(struct wl_window *win) { + if (win == NULL || win->tab_overview.sub == NULL) + return; + __typeof__(win->tab_overview_state) *st = &win->tab_overview_state; + st->target_open = false; + st->progress = 0.f; + st->anim_from = 0.f; + st->anim_start_ns = 0; + st->hover_idx = -1; + + /* Hide the sub-surface immediately so the new active tab is visible + * without going through the fade-out frames. */ + if (st->visible) { + wl_surface_attach(win->tab_overview.surface.surf, NULL, 0, 0); + wl_surface_commit(win->tab_overview.surface.surf); + st->visible = false; + } + st->card_count = 0; + + struct terminal *active = win->tabs[win->active_tab]; + if (active != NULL) render_refresh(active); } -void -tab_overview_close_instant(struct wl_window *win) -{ - if (win == NULL || win->tab_overview.sub == NULL) - return; - __typeof__(win->tab_overview_state) *st = &win->tab_overview_state; - st->target_open = false; - st->progress = 0.f; - st->anim_from = 0.f; - st->anim_start_ns = 0; - st->hover_idx = -1; - - /* Hide the sub-surface immediately so the new active tab is visible - * without going through the fade-out frames. */ - if (st->visible) { - wl_surface_attach(win->tab_overview.surface.surf, NULL, 0, 0); - wl_surface_commit(win->tab_overview.surface.surf); - st->visible = false; - } - st->card_count = 0; - - struct terminal *active = win->tabs[win->active_tab]; - if (active != NULL) - render_refresh(active); -} - -void -render_refresh_tab_overview(struct terminal *term) -{ - if (term->conf->tabs.enabled) - term->render.refresh.grid = true; +void render_refresh_tab_overview(struct terminal *term) { + if (term->conf->tabs.enabled) + term->render.refresh.grid = true; } /* Compute the {cols, rows, card_w, card_h, gap, top_left} layout for n * tabs in a window of (win_w, win_h) buffer pixels. The card aspect ratio * mirrors the window's. */ -static void -overview_layout(int win_w, int win_h, size_t n, - int *out_cols, int *out_rows, - int *out_card_w, int *out_card_h, - int *out_gap, int *out_origin_x, int *out_origin_y, - int *out_title_h) -{ - if (n == 0 || win_w <= 0 || win_h <= 0) { - *out_cols = *out_rows = 0; - *out_card_w = *out_card_h = 0; - *out_gap = *out_origin_x = *out_origin_y = *out_title_h = 0; - return; - } +static void overview_layout(int win_w, int win_h, size_t n, int *out_cols, + int *out_rows, int *out_card_w, int *out_card_h, + int *out_gap, int *out_origin_x, int *out_origin_y, + int *out_title_h) { + if (n == 0 || win_w <= 0 || win_h <= 0) { + *out_cols = *out_rows = 0; + *out_card_w = *out_card_h = 0; + *out_gap = *out_origin_x = *out_origin_y = *out_title_h = 0; + return; + } - const int margin = max(24, win_w / 32); - const int gap = max(12, win_w / 64); - const int title_h = max(18, win_h / 36); + const int margin = max(24, win_w / 32); + const int gap = max(12, win_w / 64); + const int title_h = max(18, win_h / 36); - /* Pick column count that yields cards closest to window aspect ratio. - * cols = round(sqrt(n * win_w/win_h)) is the standard heuristic. */ - const float aspect = (float)win_w / (float)win_h; - int cols = (int)roundf(sqrtf((float)n * aspect)); - if (cols < 1) cols = 1; - if ((size_t)cols > n) cols = (int)n; - int rows = (int)((n + cols - 1) / cols); + /* Pick column count that yields cards closest to window aspect ratio. + * cols = round(sqrt(n * win_w/win_h)) is the standard heuristic. */ + const float aspect = (float)win_w / (float)win_h; + int cols = (int)roundf(sqrtf((float)n * aspect)); + if (cols < 1) + cols = 1; + if ((size_t)cols > n) + cols = (int)n; + int rows = (int)((n + cols - 1) / cols); - /* Compute card size that fits both axes */ - int avail_w = win_w - 2 * margin - (cols - 1) * gap; - int avail_h = win_h - 2 * margin - (rows - 1) * gap; - if (avail_w < 1) avail_w = 1; - if (avail_h < 1) avail_h = 1; + /* Compute card size that fits both axes */ + int avail_w = win_w - 2 * margin - (cols - 1) * gap; + int avail_h = win_h - 2 * margin - (rows - 1) * gap; + if (avail_w < 1) + avail_w = 1; + if (avail_h < 1) + avail_h = 1; - int card_w = avail_w / cols; - int card_h_from_w = (int)((int64_t)card_w * win_h / win_w); - int card_h_avail = (avail_h - rows * title_h) / rows; - if (card_h_avail < 1) card_h_avail = 1; + int card_w = avail_w / cols; + int card_h_from_w = (int)((int64_t)card_w * win_h / win_w); + int card_h_avail = (avail_h - rows * title_h) / rows; + if (card_h_avail < 1) + card_h_avail = 1; - int card_h; - if (card_h_from_w <= card_h_avail) { - card_h = card_h_from_w; - } else { - card_h = card_h_avail; - card_w = (int)((int64_t)card_h * win_w / win_h); - } - if (card_w < 1) card_w = 1; - if (card_h < 1) card_h = 1; + int card_h; + if (card_h_from_w <= card_h_avail) { + card_h = card_h_from_w; + } else { + card_h = card_h_avail; + card_w = (int)((int64_t)card_h * win_w / win_h); + } + if (card_w < 1) + card_w = 1; + if (card_h < 1) + card_h = 1; - const int total_w = cols * card_w + (cols - 1) * gap; - const int total_h = rows * (card_h + title_h) + (rows - 1) * gap; - const int origin_x = (win_w - total_w) / 2; - const int origin_y = (win_h - total_h) / 2; + const int total_w = cols * card_w + (cols - 1) * gap; + const int total_h = rows * (card_h + title_h) + (rows - 1) * gap; + const int origin_x = (win_w - total_w) / 2; + const int origin_y = (win_h - total_h) / 2; - *out_cols = cols; - *out_rows = rows; - *out_card_w = card_w; - *out_card_h = card_h; - *out_gap = gap; - *out_origin_x = origin_x; - *out_origin_y = origin_y; - *out_title_h = title_h; + *out_cols = cols; + *out_rows = rows; + *out_card_w = card_w; + *out_card_h = card_h; + *out_gap = gap; + *out_origin_x = origin_x; + *out_origin_y = origin_y; + *out_title_h = title_h; } -static void -draw_card_thumbnail(pixman_image_t *dst, struct buffer *src_buf, - int dst_x, int dst_y, int dst_w, int dst_h) -{ - if (src_buf == NULL || src_buf->pix == NULL || src_buf->pix[0] == NULL || - src_buf->width <= 0 || src_buf->height <= 0 || - dst_w <= 0 || dst_h <= 0) - return; +static void draw_card_thumbnail(pixman_image_t *dst, struct buffer *src_buf, + int dst_x, int dst_y, int dst_w, int dst_h) { + if (src_buf == NULL || src_buf->pix == NULL || src_buf->pix[0] == NULL || + src_buf->width <= 0 || src_buf->height <= 0 || dst_w <= 0 || dst_h <= 0) + return; - pixman_image_t *src = src_buf->pix[0]; - pixman_image_set_filter(src, PIXMAN_FILTER_BILINEAR, NULL, 0); + pixman_image_t *src = src_buf->pix[0]; + pixman_image_set_filter(src, PIXMAN_FILTER_BILINEAR, NULL, 0); - pixman_transform_t xform; - pixman_transform_init_scale(&xform, - pixman_double_to_fixed((double)src_buf->width / dst_w), - pixman_double_to_fixed((double)src_buf->height / dst_h)); - pixman_image_set_transform(src, &xform); + pixman_transform_t xform; + pixman_transform_init_scale( + &xform, pixman_double_to_fixed((double)src_buf->width / dst_w), + pixman_double_to_fixed((double)src_buf->height / dst_h)); + pixman_image_set_transform(src, &xform); - pixman_image_composite32(PIXMAN_OP_SRC, - src, NULL, dst, - 0, 0, 0, 0, - dst_x, dst_y, dst_w, dst_h); + pixman_image_composite32(PIXMAN_OP_SRC, src, NULL, dst, 0, 0, 0, 0, dst_x, + dst_y, dst_w, dst_h); - /* Reset for subsequent normal use */ - pixman_image_set_transform(src, NULL); - pixman_image_set_filter(src, PIXMAN_FILTER_NEAREST, NULL, 0); + /* Reset for subsequent normal use */ + pixman_image_set_transform(src, NULL); + pixman_image_set_filter(src, PIXMAN_FILTER_NEAREST, NULL, 0); } -static void -render_tab_overview(struct terminal *term) -{ - struct wl_window *win = term->window; - if (win->tab_overview.sub == NULL) - return; +static void render_tab_overview(struct terminal *term) { + struct wl_window *win = term->window; + if (win->tab_overview.sub == NULL) + return; - __typeof__(win->tab_overview_state) *st = &win->tab_overview_state; + __typeof__(win->tab_overview_state) *st = &win->tab_overview_state; - /* Advance animation */ - if (st->progress != (st->target_open ? 1.f : 0.f)) { - const uint64_t now = now_monotonic_ns(); - const uint64_t elapsed = now - st->anim_start_ns; - float t = (float)elapsed / (float)TAB_OVERVIEW_DURATION_NS; - if (t < 0.f) t = 0.f; - if (t > 1.f) t = 1.f; - const float eased = ease_out_cubic(t); - const float target = st->target_open ? 1.f : 0.f; - st->progress = st->anim_from + (target - st->anim_from) * eased; - if (t >= 1.f) - st->progress = target; + /* Advance animation */ + if (st->progress != (st->target_open ? 1.f : 0.f)) { + const uint64_t now = now_monotonic_ns(); + const uint64_t elapsed = now - st->anim_start_ns; + float t = (float)elapsed / (float)TAB_OVERVIEW_DURATION_NS; + if (t < 0.f) + t = 0.f; + if (t > 1.f) + t = 1.f; + const float eased = ease_out_cubic(t); + const float target = st->target_open ? 1.f : 0.f; + st->progress = st->anim_from + (target - st->anim_from) * eased; + if (t >= 1.f) + st->progress = target; + } + + /* Hide sub-surface entirely when fully closed */ + if (st->progress <= 0.f && !st->target_open) { + if (st->visible) { + wl_surface_attach(win->tab_overview.surface.surf, NULL, 0, 0); + wl_surface_commit(win->tab_overview.surface.surf); + st->visible = false; + } + st->card_count = 0; + return; + } + + const float scale = term->scale; + const int width = term->width; + const int height = term->height; + if (width <= 0 || height <= 0) + return; + + struct buffer_chain *chain = term->render.chains.tab_overview; + struct buffer *buf = shm_get_buffer(chain, width, height); + if (buf == NULL) + return; + + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + + /* Backdrop dim: black at progress*0.55 alpha */ + { + const uint16_t dim = (uint16_t)(0xffff * (0.55f * st->progress)); + const pixman_color_t dim_color = {0, 0, 0, dim}; + pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], &dim_color, 1, + &(pixman_rectangle16_t){0, 0, width, height}); + } + + const size_t n = min(win->tab_count, (size_t)TAB_MAX); + if (n == 0) + goto commit; + + int cols, rows, card_w, card_h, gap, origin_x, origin_y, title_h; + overview_layout(width, height, n, &cols, &rows, &card_w, &card_h, &gap, + &origin_x, &origin_y, &title_h); + + /* Scale factor: 0.92 -> 1.0 over the animation */ + const float card_scale = 0.92f + 0.08f * st->progress; + const int sc_w = (int)roundf(card_w * card_scale); + const int sc_h = (int)roundf(card_h * card_scale); + const int sc_title_h = (int)roundf(title_h * card_scale); + + const int corner_r = max(6, (int)roundf(8.f * scale)); + + struct fcft_font *font = term->fonts[0]; + + /* Card opacity: ramps in faster than the backdrop */ + const float card_alpha = st->progress; + const uint16_t alpha16 = (uint16_t)(0xffff * card_alpha); + + /* Use the window/terminal palette to make cards feel native */ + pixman_color_t card_bg = color_hex_to_pixman(term->colors.bg, gamma_correct); + pixman_color_t card_fg = color_hex_to_pixman(term->colors.fg, gamma_correct); + /* Distinct ring colors so the active tab is visually distinguishable + * from the keyboard/hover-selected tab as the user moves through the grid. */ + pixman_color_t ring_active = color_hex_to_pixman( + term->conf->tabs.colors.overview_active_border, gamma_correct); + pixman_color_t ring_select = color_hex_to_pixman( + term->conf->tabs.colors.overview_select_border, gamma_correct); + card_bg.alpha = alpha16; + ring_active.alpha = alpha16; + ring_select.alpha = alpha16; + + pixman_color_t title_bg = {0, 0, 0, (uint16_t)(0x8000 * card_alpha)}; + + st->card_count = n; + + for (size_t i = 0; i < n; i++) { + const int col = (int)(i % (size_t)cols); + const int row = (int)(i / (size_t)cols); + + /* Per-card layout (full size) */ + const int cell_x = origin_x + col * (card_w + gap); + const int cell_y = origin_y + row * (card_h + title_h + gap); + + /* Cache full-size geometry for hit-testing */ + st->cards[i].x = cell_x; + st->cards[i].y = cell_y; + st->cards[i].w = card_w; + st->cards[i].h = card_h + title_h; + + /* Scaled-around-center geometry for drawing */ + const int draw_x = cell_x + (card_w - sc_w) / 2; + const int draw_y = cell_y + (card_h + title_h - sc_h - sc_title_h) / 2; + + const bool is_active = (i == win->active_tab); + const bool is_sel = (i == st->sel_idx); + const bool is_hover = ((int)i == st->hover_idx); + + /* Card background */ + draw_rounded_rect(buf->pix[0], &card_bg, draw_x, draw_y, sc_w, + sc_h + sc_title_h, corner_r, 0xf); + + /* Thumbnail of the tab's last rendered grid */ + struct terminal *tab = win->tabs[i]; + if (tab != NULL && sc_w > 4 && sc_h > 4) { + draw_card_thumbnail(buf->pix[0], tab->render.last_buf, draw_x + 2, + draw_y + 2, sc_w - 4, sc_h - 4); } - /* Hide sub-surface entirely when fully closed */ - if (st->progress <= 0.f && !st->target_open) { - if (st->visible) { - wl_surface_attach(win->tab_overview.surface.surf, NULL, 0, 0); - wl_surface_commit(win->tab_overview.surface.surf); - st->visible = false; - } - st->card_count = 0; - return; + /* Title bar strip across the bottom of the card */ + pixman_image_fill_rectangles( + PIXMAN_OP_OVER, buf->pix[0], &title_bg, 1, + &(pixman_rectangle16_t){draw_x, draw_y + sc_h, sc_w, sc_title_h}); + + if (font != NULL && tab != NULL) { + const int baseline = + draw_y + sc_h + + (sc_title_h - (int)(font->ascent + font->descent)) / 2 + + (int)font->ascent; + const int label_pad = max(4, (int)roundf(4.f * scale)); + render_tab_label(buf->pix[0], font, &card_fg, tab, i, draw_x + label_pad, + baseline, draw_x + sc_w - label_pad, scale); } - const float scale = term->scale; - const int width = term->width; - const int height = term->height; - if (width <= 0 || height <= 0) - return; - - struct buffer_chain *chain = term->render.chains.tab_overview; - struct buffer *buf = shm_get_buffer(chain, width, height); - if (buf == NULL) - return; - - const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); - - /* Backdrop dim: black at progress*0.55 alpha */ - { - const uint16_t dim = (uint16_t)(0xffff * (0.55f * st->progress)); - const pixman_color_t dim_color = {0, 0, 0, dim}; - pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], - &dim_color, 1, - &(pixman_rectangle16_t){0, 0, width, height}); - } - - const size_t n = min(win->tab_count, (size_t)TAB_MAX); - if (n == 0) - goto commit; - - int cols, rows, card_w, card_h, gap, origin_x, origin_y, title_h; - overview_layout(width, height, n, - &cols, &rows, &card_w, &card_h, &gap, - &origin_x, &origin_y, &title_h); - - /* Scale factor: 0.92 -> 1.0 over the animation */ - const float card_scale = 0.92f + 0.08f * st->progress; - const int sc_w = (int)roundf(card_w * card_scale); - const int sc_h = (int)roundf(card_h * card_scale); - const int sc_title_h = (int)roundf(title_h * card_scale); - - const int corner_r = max(6, (int)roundf(8.f * scale)); - - struct fcft_font *font = term->fonts[0]; - - /* Card opacity: ramps in faster than the backdrop */ - const float card_alpha = st->progress; - const uint16_t alpha16 = (uint16_t)(0xffff * card_alpha); - - /* Use the window/terminal palette to make cards feel native */ - pixman_color_t card_bg = color_hex_to_pixman(term->colors.bg, gamma_correct); - pixman_color_t card_fg = color_hex_to_pixman(term->colors.fg, gamma_correct); - /* Accent for active card: borrow active_bg from tab config if rounded - * tabs are configured, otherwise tint by mixing fg/bg */ - pixman_color_t accent = color_hex_to_pixman( - term->conf->tabs.colors.active_bg ? term->conf->tabs.colors.active_bg - : term->colors.fg, - gamma_correct); - card_bg.alpha = alpha16; - accent.alpha = alpha16; - - pixman_color_t title_bg = {0, 0, 0, (uint16_t)(0x8000 * card_alpha)}; - - st->card_count = n; - - for (size_t i = 0; i < n; i++) { - const int col = (int)(i % (size_t)cols); - const int row = (int)(i / (size_t)cols); - - /* Per-card layout (full size) */ - const int cell_x = origin_x + col * (card_w + gap); - const int cell_y = origin_y + row * (card_h + title_h + gap); - - /* Cache full-size geometry for hit-testing */ - st->cards[i].x = cell_x; - st->cards[i].y = cell_y; - st->cards[i].w = card_w; - st->cards[i].h = card_h + title_h; - - /* Scaled-around-center geometry for drawing */ - const int draw_x = cell_x + (card_w - sc_w) / 2; - const int draw_y = cell_y + (card_h + title_h - sc_h - sc_title_h) / 2; - - const bool is_active = (i == win->active_tab); - const bool is_sel = (i == st->sel_idx); - const bool is_hover = ((int)i == st->hover_idx); - - /* Card background */ - draw_rounded_rect(buf->pix[0], &card_bg, - draw_x, draw_y, sc_w, sc_h + sc_title_h, - corner_r, 0xf); - - /* Thumbnail of the tab's last rendered grid */ - struct terminal *tab = win->tabs[i]; - if (tab != NULL && sc_w > 4 && sc_h > 4) { - draw_card_thumbnail(buf->pix[0], tab->render.last_buf, - draw_x + 2, draw_y + 2, - sc_w - 4, sc_h - 4); - } - - /* Title bar strip across the bottom of the card */ - pixman_image_fill_rectangles(PIXMAN_OP_OVER, buf->pix[0], &title_bg, - 1, &(pixman_rectangle16_t){ - draw_x, draw_y + sc_h, sc_w, sc_title_h}); - - if (font != NULL && tab != NULL) { - const int baseline = draw_y + sc_h - + (sc_title_h - (int)(font->ascent + font->descent)) / 2 - + (int)font->ascent; - const int label_pad = max(4, (int)roundf(4.f * scale)); - render_tab_label(buf->pix[0], font, &card_fg, tab, i, - draw_x + label_pad, baseline, - draw_x + sc_w - label_pad, scale); - } - - /* Highlights (drawn on top): a 2-3 px ring */ - if (is_active || is_sel || is_hover) { - const int ring = max(2, (int)roundf(2.f * scale)) - + (is_active ? 1 : 0); - pixman_color_t ring_color = accent; - if (!is_active && is_hover) { - ring_color.alpha = (uint16_t)(0xc000 * card_alpha); - } else if (!is_active && is_sel) { - ring_color.alpha = (uint16_t)(0xa000 * card_alpha); - } - /* Top */ - pixman_image_fill_rectangles(PIXMAN_OP_OVER, buf->pix[0], &ring_color, - 1, &(pixman_rectangle16_t){draw_x, draw_y, sc_w, ring}); - /* Bottom */ - pixman_image_fill_rectangles(PIXMAN_OP_OVER, buf->pix[0], &ring_color, - 1, &(pixman_rectangle16_t){ - draw_x, draw_y + sc_h + sc_title_h - ring, sc_w, ring}); - /* Left */ - pixman_image_fill_rectangles(PIXMAN_OP_OVER, buf->pix[0], &ring_color, - 1, &(pixman_rectangle16_t){ - draw_x, draw_y, ring, sc_h + sc_title_h}); - /* Right */ - pixman_image_fill_rectangles(PIXMAN_OP_OVER, buf->pix[0], &ring_color, - 1, &(pixman_rectangle16_t){ - draw_x + sc_w - ring, draw_y, ring, sc_h + sc_title_h}); - } + /* Highlights (drawn on top): a 2-3 px ring. + * + * Priority when a card is both active and selected/hovered: + * draw the active ring (it's the persistent "you are here" + * marker); the selection ring is layered on the inside so the + * two colors are both visible. */ + if (is_active || is_sel || is_hover) { + const int ring = max(2, (int)roundf(2.f * scale)) + (is_active ? 1 : 0); + pixman_color_t ring_color; + if (is_active) { + ring_color = ring_active; + } else { + ring_color = ring_select; + if (is_hover && !is_sel) + ring_color.alpha = (uint16_t)(0xc000 * card_alpha); + else + ring_color.alpha = alpha16; + } + /* Top */ + pixman_image_fill_rectangles( + PIXMAN_OP_OVER, buf->pix[0], &ring_color, 1, + &(pixman_rectangle16_t){draw_x, draw_y, sc_w, ring}); + /* Bottom */ + pixman_image_fill_rectangles( + PIXMAN_OP_OVER, buf->pix[0], &ring_color, 1, + &(pixman_rectangle16_t){draw_x, draw_y + sc_h + sc_title_h - ring, + sc_w, ring}); + /* Left */ + pixman_image_fill_rectangles( + PIXMAN_OP_OVER, buf->pix[0], &ring_color, 1, + &(pixman_rectangle16_t){draw_x, draw_y, ring, sc_h + sc_title_h}); + /* Right */ + pixman_image_fill_rectangles(PIXMAN_OP_OVER, buf->pix[0], &ring_color, 1, + &(pixman_rectangle16_t){draw_x + sc_w - ring, + draw_y, ring, + sc_h + sc_title_h}); } + } commit: - wl_subsurface_set_position(win->tab_overview.sub, 0, 0); - wayl_surface_scale(win, &win->tab_overview.surface, buf, scale); - wl_surface_attach(win->tab_overview.surface.surf, buf->wl_buf, 0, 0); - wl_surface_damage_buffer(win->tab_overview.surface.surf, 0, 0, width, height); - wl_surface_set_opaque_region(win->tab_overview.surface.surf, NULL); - wl_surface_commit(win->tab_overview.surface.surf); - st->visible = true; + wl_subsurface_set_position(win->tab_overview.sub, 0, 0); + wayl_surface_scale(win, &win->tab_overview.surface, buf, scale); + wl_surface_attach(win->tab_overview.surface.surf, buf->wl_buf, 0, 0); + wl_surface_damage_buffer(win->tab_overview.surface.surf, 0, 0, width, height); + wl_surface_set_opaque_region(win->tab_overview.surface.surf, NULL); + wl_surface_commit(win->tab_overview.surface.surf); + st->visible = true; - /* Keep animating until we reach the target */ - if (st->progress != (st->target_open ? 1.f : 0.f)) - term->render.refresh.grid = true; + /* Keep animating until we reach the target */ + if (st->progress != (st->target_open ? 1.f : 0.f)) + term->render.refresh.grid = true; } -int -tab_overview_hit_test(struct wl_window *win, int x_buf, int y_buf) -{ - if (win == NULL || !tab_overview_is_active(win)) - return -1; - const __typeof__(win->tab_overview_state) *s = &win->tab_overview_state; - for (size_t i = 0; i < s->card_count; i++) { - const int x = s->cards[i].x; - const int y = s->cards[i].y; - const int w = s->cards[i].w; - const int h = s->cards[i].h; - if (x_buf >= x && x_buf < x + w && y_buf >= y && y_buf < y + h) - return (int)i; - } +int tab_overview_hit_test(struct wl_window *win, int x_buf, int y_buf) { + if (win == NULL || !tab_overview_is_active(win)) return -1; + const __typeof__(win->tab_overview_state) *s = &win->tab_overview_state; + for (size_t i = 0; i < s->card_count; i++) { + const int x = s->cards[i].x; + const int y = s->cards[i].y; + const int w = s->cards[i].w; + const int h = s->cards[i].h; + if (x_buf >= x && x_buf < x + w && y_buf >= y && y_buf < y + h) + return (int)i; + } + return -1; } -bool -render_xcursor_set(struct seat *seat, struct terminal *term, - enum cursor_shape shape) -{ - if (seat->pointer.theme == NULL && seat->pointer.shape_device == NULL) - return false; +bool render_xcursor_set(struct seat *seat, struct terminal *term, + enum cursor_shape shape) { + if (seat->pointer.theme == NULL && seat->pointer.shape_device == NULL) + return false; - if (seat->mouse_focus == NULL) { - seat->pointer.shape = CURSOR_SHAPE_NONE; - return true; - } - - if (seat->mouse_focus != term) { - /* This terminal doesn't have mouse focus */ - return true; - } - - if (seat->pointer.shape == shape && - !(shape == CURSOR_SHAPE_CUSTOM && - !streq(seat->pointer.last_custom_xcursor, - term->mouse_user_cursor))) - { - return true; - } - - if (shape == CURSOR_SHAPE_HIDDEN) { - seat->pointer.cursor = NULL; - free(seat->pointer.last_custom_xcursor); - seat->pointer.last_custom_xcursor = NULL; - } - - else if (seat->pointer.shape_device == NULL) { - const char *const custom_xcursors[] = {term->mouse_user_cursor, NULL}; - const char *const *xcursors = shape == CURSOR_SHAPE_CUSTOM - ? custom_xcursors - : cursor_shape_to_string(shape); - - xassert(xcursors[0] != NULL); - - seat->pointer.cursor = NULL; - - for (size_t i = 0; xcursors[i] != NULL; i++) { - seat->pointer.cursor = - wl_cursor_theme_get_cursor(seat->pointer.theme, xcursors[i]); - - if (seat->pointer.cursor != NULL) { - LOG_DBG("loaded xcursor %s", xcursors[i]); - break; - } - } - - if (seat->pointer.cursor == NULL) { - LOG_ERR( - "failed to load xcursor pointer '%s', and all of its fallbacks", - xcursors[0]); - return false; - } - } else { - /* Server-side cursors - no need to load anything */ - } - - if (shape == CURSOR_SHAPE_CUSTOM) { - free(seat->pointer.last_custom_xcursor); - seat->pointer.last_custom_xcursor = - xstrdup(term->mouse_user_cursor); - } - - /* FDM hook takes care of actual rendering */ - seat->pointer.shape = shape; - seat->pointer.xcursor_pending = true; + if (seat->mouse_focus == NULL) { + seat->pointer.shape = CURSOR_SHAPE_NONE; return true; -} + } -void -render_buffer_release_callback(struct buffer *buf, void *data) -{ - /* - * Called from shm.c when a buffer is released - * - * We use it to pre-apply last-frame's damage to it, when we're - * forced to double buffer (compositor doesn't release buffers - * immediately). - * - * The timeline is thus: - * 1. We render and push a new frame - * 2. Some (hopefully short) time after that, the compositor releases the previous buffer - * 3. We're called, and kick off the thread that copies the changes from (1) to the just freed buffer - * 4. Time passes.... - * 5. The compositor calls our frame callback, signalling to us that it's time to start rendering the next frame - * 6. Hopefully, our thread is already done with copying the changes, otherwise we stall, waiting for it - * 7. We render the frame as if the compositor does immediate releases. - * - * What's the gain? Reduced latency, by applying the previous - * frame's damage as soon as possible, we shorten the time it - * takes to render the frame after the frame callback. - * - * This means the compositor can, in theory, push the frame - * callback closer to the vblank deadline, and thus reduce input - * latency. Not all compositors (most, in fact?) don't adapt like - * this, unfortunately. But some allows the user to manually - * configure the deadline. - */ - struct terminal *term = data; + if (seat->mouse_focus != term) { + /* This terminal doesn't have mouse focus */ + return true; + } - if (likely(buf->age != 1)) - return; + if (seat->pointer.shape == shape && + !(shape == CURSOR_SHAPE_CUSTOM && + !streq(seat->pointer.last_custom_xcursor, term->mouse_user_cursor))) { + return true; + } - if (likely(!term->render.preapply_last_frame_damage)) - return; + if (shape == CURSOR_SHAPE_HIDDEN) { + seat->pointer.cursor = NULL; + free(seat->pointer.last_custom_xcursor); + seat->pointer.last_custom_xcursor = NULL; + } - if (term->render.last_buf == NULL) - return; + else if (seat->pointer.shape_device == NULL) { + const char *const custom_xcursors[] = {term->mouse_user_cursor, NULL}; + const char *const *xcursors = shape == CURSOR_SHAPE_CUSTOM + ? custom_xcursors + : cursor_shape_to_string(shape); - if (term->render.last_buf->age != 0) - return; + xassert(xcursors[0] != NULL); - if (buf->width != term->render.last_buf->width) - return; + seat->pointer.cursor = NULL; - if (buf->height != term->render.last_buf->height) - return; + for (size_t i = 0; xcursors[i] != NULL; i++) { + seat->pointer.cursor = + wl_cursor_theme_get_cursor(seat->pointer.theme, xcursors[i]); - xassert(term->render.workers.count > 0); - xassert(term->render.last_buf != NULL); - - xassert(term->render.last_buf->age == 0); - xassert(term->render.last_buf != buf); - - mtx_lock(&term->render.workers.preapplied_damage.lock); - if (term->render.workers.preapplied_damage.buf != NULL) { - mtx_unlock(&term->render.workers.preapplied_damage.lock); - return; + if (seat->pointer.cursor != NULL) { + LOG_DBG("loaded xcursor %s", xcursors[i]); + break; + } } - xassert(term->render.workers.preapplied_damage.buf == NULL); - term->render.workers.preapplied_damage.buf = buf; - term->render.workers.preapplied_damage.start = (struct timespec){0}; - term->render.workers.preapplied_damage.stop = (struct timespec){0}; - mtx_unlock(&term->render.workers.preapplied_damage.lock); + if (seat->pointer.cursor == NULL) { + LOG_ERR("failed to load xcursor pointer '%s', and all of its fallbacks", + xcursors[0]); + return false; + } + } else { + /* Server-side cursors - no need to load anything */ + } - mtx_lock(&term->render.workers.lock); - sem_post(&term->render.workers.start); - xassert(tll_length(term->render.workers.queue) == 0); - tll_push_back(term->render.workers.queue, -3); - mtx_unlock(&term->render.workers.lock); + if (shape == CURSOR_SHAPE_CUSTOM) { + free(seat->pointer.last_custom_xcursor); + seat->pointer.last_custom_xcursor = xstrdup(term->mouse_user_cursor); + } + + /* FDM hook takes care of actual rendering */ + seat->pointer.shape = shape; + seat->pointer.xcursor_pending = true; + return true; +} + +void render_buffer_release_callback(struct buffer *buf, void *data) { + /* + * Called from shm.c when a buffer is released + * + * We use it to pre-apply last-frame's damage to it, when we're + * forced to double buffer (compositor doesn't release buffers + * immediately). + * + * The timeline is thus: + * 1. We render and push a new frame + * 2. Some (hopefully short) time after that, the compositor releases the + * previous buffer + * 3. We're called, and kick off the thread that copies the changes from (1) + * to the just freed buffer + * 4. Time passes.... + * 5. The compositor calls our frame callback, signalling to us that it's + * time to start rendering the next frame + * 6. Hopefully, our thread is already done with copying the changes, + * otherwise we stall, waiting for it + * 7. We render the frame as if the compositor does immediate releases. + * + * What's the gain? Reduced latency, by applying the previous + * frame's damage as soon as possible, we shorten the time it + * takes to render the frame after the frame callback. + * + * This means the compositor can, in theory, push the frame + * callback closer to the vblank deadline, and thus reduce input + * latency. Not all compositors (most, in fact?) don't adapt like + * this, unfortunately. But some allows the user to manually + * configure the deadline. + */ + struct terminal *term = data; + + if (likely(buf->age != 1)) + return; + + if (likely(!term->render.preapply_last_frame_damage)) + return; + + if (term->render.last_buf == NULL) + return; + + if (term->render.last_buf->age != 0) + return; + + if (buf->width != term->render.last_buf->width) + return; + + if (buf->height != term->render.last_buf->height) + return; + + xassert(term->render.workers.count > 0); + xassert(term->render.last_buf != NULL); + + xassert(term->render.last_buf->age == 0); + xassert(term->render.last_buf != buf); + + mtx_lock(&term->render.workers.preapplied_damage.lock); + if (term->render.workers.preapplied_damage.buf != NULL) { + mtx_unlock(&term->render.workers.preapplied_damage.lock); + return; + } + + xassert(term->render.workers.preapplied_damage.buf == NULL); + term->render.workers.preapplied_damage.buf = buf; + term->render.workers.preapplied_damage.start = (struct timespec){0}; + term->render.workers.preapplied_damage.stop = (struct timespec){0}; + mtx_unlock(&term->render.workers.preapplied_damage.lock); + + mtx_lock(&term->render.workers.lock); + sem_post(&term->render.workers.start); + xassert(tll_length(term->render.workers.queue) == 0); + tll_push_back(term->render.workers.queue, -3); + mtx_unlock(&term->render.workers.lock); } diff --git a/search.c b/search.c index 0fa2756..3d6d632 100644 --- a/search.c +++ b/search.c @@ -8,7 +8,6 @@ #define LOG_MODULE "search" #define LOG_ENABLE_DBG 0 -#include "log.h" #include "char32.h" #include "commands.h" #include "config.h" @@ -16,10 +15,12 @@ #include "grid.h" #include "input.h" #include "key-binding.h" +#include "log.h" #include "misc.h" #include "quirks.h" #include "render.h" #include "selection.h" +#include "session.h" #include "shm.h" #include "unicode-mode.h" #include "util.h" @@ -40,335 +41,329 @@ * row, and if it is NULL, we move the viewport *backward* until the * last row is non-NULL. */ -static int -ensure_view_is_allocated(struct terminal *term, int new_view) -{ - struct grid *grid = term->grid; - int view_end = (new_view + term->rows - 1) & (grid->num_rows - 1); +static int ensure_view_is_allocated(struct terminal *term, int new_view) { + struct grid *grid = term->grid; + int view_end = (new_view + term->rows - 1) & (grid->num_rows - 1); - if (grid->rows[new_view] == NULL) { - while (grid->rows[new_view] == NULL) - new_view = (new_view + 1) & (grid->num_rows - 1); - } + if (grid->rows[new_view] == NULL) { + while (grid->rows[new_view] == NULL) + new_view = (new_view + 1) & (grid->num_rows - 1); + } - else if (grid->rows[view_end] == NULL) { - while (grid->rows[view_end] == NULL) { - new_view--; - if (new_view < 0) - new_view += grid->num_rows; - view_end = (new_view + term->rows - 1) & (grid->num_rows - 1); - } + else if (grid->rows[view_end] == NULL) { + while (grid->rows[view_end] == NULL) { + new_view--; + if (new_view < 0) + new_view += grid->num_rows; + view_end = (new_view + term->rows - 1) & (grid->num_rows - 1); } + } #if defined(_DEBUG) - for (size_t r = 0; r < term->rows; r++) - xassert(grid->rows[(new_view + r) & (grid->num_rows - 1)] != NULL); + for (size_t r = 0; r < term->rows; r++) + xassert(grid->rows[(new_view + r) & (grid->num_rows - 1)] != NULL); #endif - return new_view; + return new_view; } -static bool -search_ensure_size(struct terminal *term, size_t wanted_size) -{ - while (wanted_size >= term->search.sz) { - size_t new_sz = term->search.sz == 0 ? 64 : term->search.sz * 2; - char32_t *new_buf = realloc(term->search.buf, new_sz * sizeof(term->search.buf[0])); +static bool search_ensure_size(struct terminal *term, size_t wanted_size) { + while (wanted_size >= term->search.sz) { + size_t new_sz = term->search.sz == 0 ? 64 : term->search.sz * 2; + char32_t *new_buf = + realloc(term->search.buf, new_sz * sizeof(term->search.buf[0])); - if (new_buf == NULL) { - LOG_ERRNO("failed to resize search buffer"); - return false; - } - - term->search.buf = new_buf; - term->search.sz = new_sz; + if (new_buf == NULL) { + LOG_ERRNO("failed to resize search buffer"); + return false; } - return true; + term->search.buf = new_buf; + term->search.sz = new_sz; + } + + return true; } -static bool -has_wrapped_around_left(const struct terminal *term, int abs_row_no) -{ - int rebased_row = grid_row_abs_to_sb(term->grid, term->rows, abs_row_no); - return rebased_row == term->grid->num_rows - 1 || term->grid->rows[abs_row_no] == NULL; +static bool has_wrapped_around_left(const struct terminal *term, + int abs_row_no) { + int rebased_row = grid_row_abs_to_sb(term->grid, term->rows, abs_row_no); + return rebased_row == term->grid->num_rows - 1 || + term->grid->rows[abs_row_no] == NULL; } -static bool -has_wrapped_around_right(const struct terminal *term, int abs_row_no) -{ - int rebased_row = grid_row_abs_to_sb(term->grid, term->rows, abs_row_no); - return rebased_row == 0; +static bool has_wrapped_around_right(const struct terminal *term, + int abs_row_no) { + int rebased_row = grid_row_abs_to_sb(term->grid, term->rows, abs_row_no); + return rebased_row == 0; } -static void -search_history_push(struct terminal *term, const char32_t *buf, size_t len) -{ - if (len == 0) - return; +static void search_history_push(struct terminal *term, const char32_t *buf, + size_t len) { + if (len == 0) + return; - /* Avoid pushing a duplicate of the most recent entry */ - if (term->search.history_tail != NULL && - term->search.history_tail->len == len && - c32ncmp(term->search.history_tail->buf, buf, len) == 0) - return; + /* Avoid pushing a duplicate of the most recent entry */ + if (term->search.history_tail != NULL && + term->search.history_tail->len == len && + c32ncmp(term->search.history_tail->buf, buf, len) == 0) + return; - struct search_history_entry *e = xcalloc(1, sizeof(*e)); - e->buf = xmalloc((len + 1) * sizeof(char32_t)); - memcpy(e->buf, buf, len * sizeof(char32_t)); - e->buf[len] = U'\0'; - e->len = len; + struct search_history_entry *e = xcalloc(1, sizeof(*e)); + e->buf = xmalloc((len + 1) * sizeof(char32_t)); + memcpy(e->buf, buf, len * sizeof(char32_t)); + e->buf[len] = U'\0'; + e->len = len; - e->prev = term->search.history_tail; - if (term->search.history_tail != NULL) - term->search.history_tail->next = e; - else - term->search.history_head = e; - term->search.history_tail = e; + e->prev = term->search.history_tail; + if (term->search.history_tail != NULL) + term->search.history_tail->next = e; + else + term->search.history_head = e; + term->search.history_tail = e; } -static void -search_cancel_keep_selection(struct terminal *term) -{ - struct wl_window *win = term->window; - wayl_win_subsurface_destroy(&win->search); +static void search_cancel_keep_selection(struct terminal *term) { + struct wl_window *win = term->window; + wayl_win_subsurface_destroy(&win->search); - if (term->search.len > 0) { - /* Save into history */ - search_history_push(term, term->search.buf, term->search.len); + if (term->search.len > 0) { + /* Save into history */ + search_history_push(term, term->search.buf, term->search.len); - free(term->search.last.buf); - term->search.last.buf = term->search.buf; - term->search.last.len = term->search.len; - } else - free(term->search.buf); + free(term->search.last.buf); + term->search.last.buf = term->search.buf; + term->search.last.len = term->search.len; + } else + free(term->search.buf); - /* Free compiled regex */ - if (term->search.regex_compiled != NULL) { - regfree(term->search.regex_compiled); - free(term->search.regex_compiled); - term->search.regex_compiled = NULL; - } - term->search.regex_valid = false; + /* Free compiled regex */ + if (term->search.regex_compiled != NULL) { + regfree(term->search.regex_compiled); + free(term->search.regex_compiled); + term->search.regex_compiled = NULL; + } + term->search.regex_valid = false; - term->search.buf = NULL; - term->search.len = term->search.sz = 0; + term->search.buf = NULL; + term->search.len = term->search.sz = 0; - term->search.cursor = 0; - term->search.match = (struct coord){-1, -1}; - term->search.match_len = 0; - term->search.total_count = 0; - term->search.current_idx = 0; - term->search.wrapped = false; - term->search.history_pos = NULL; - term->is_searching = false; - term->render.search_glyph_offset = 0; + term->search.cursor = 0; + term->search.match = (struct coord){-1, -1}; + term->search.match_len = 0; + term->search.total_count = 0; + term->search.current_idx = 0; + term->search.wrapped = false; + term->search.history_pos = NULL; + term->is_searching = false; + if (term->search.mode == SEARCH_MODE_SESSION_LOAD) + session_picker_free(term); + term->search.mode = SEARCH_MODE_NORMAL; + term->render.search_glyph_offset = 0; - /* Reset IME state */ - if (term_ime_is_enabled(term)) { - term_ime_disable(term); - term_ime_enable(term); - } + /* Reset IME state */ + if (term_ime_is_enabled(term)) { + term_ime_disable(term); + term_ime_enable(term); + } - term_xcursor_update(term); - render_refresh(term); + term_xcursor_update(term); + render_refresh(term); } -void -search_begin(struct terminal *term) -{ - LOG_DBG("search: begin"); +void search_begin(struct terminal *term) { + LOG_DBG("search: begin"); - search_cancel_keep_selection(term); - selection_cancel(term); + search_cancel_keep_selection(term); + selection_cancel(term); - /* Reset IME state */ - if (term_ime_is_enabled(term)) { - term_ime_disable(term); - term_ime_enable(term); - } + /* Reset IME state */ + if (term_ime_is_enabled(term)) { + term_ime_disable(term); + term_ime_enable(term); + } - /* On-demand instantiate wayland surface */ - bool ret = wayl_win_subsurface_new( - term->window, &term->window->search, false); - xassert(ret); + /* On-demand instantiate wayland surface */ + bool ret = + wayl_win_subsurface_new(term->window, &term->window->search, false); + xassert(ret); - const struct grid *grid = term->grid; - term->search.original_view = grid->view; - term->search.view_followed_offset = grid->view == grid->offset; - term->is_searching = true; + const struct grid *grid = term->grid; + term->search.original_view = grid->view; + term->search.view_followed_offset = grid->view == grid->offset; + term->is_searching = true; - term->search.len = 0; - term->search.sz = 64; - term->search.buf = xmalloc(term->search.sz * sizeof(term->search.buf[0])); - term->search.buf[0] = U'\0'; + term->search.len = 0; + term->search.sz = 64; + term->search.buf = xmalloc(term->search.sz * sizeof(term->search.buf[0])); + term->search.buf[0] = U'\0'; + term->search.mode = SEARCH_MODE_NORMAL; - term_xcursor_update(term); - render_refresh_search(term); + term_xcursor_update(term); + render_refresh_search(term); } -void -search_cancel(struct terminal *term) -{ - if (!term->is_searching) - return; - - search_cancel_keep_selection(term); - selection_cancel(term); +void search_begin_session(struct terminal *term, enum search_mode mode) { + search_begin(term); + term->search.mode = mode; + if (mode == SEARCH_MODE_SESSION_LOAD) + session_picker_init(term); + render_refresh_search(term); } -void -search_selection_cancelled(struct terminal *term) -{ - term->search.match = (struct coord){-1, -1}; - term->search.match_len = 0; - render_refresh_search(term); +void search_cancel(struct terminal *term) { + if (!term->is_searching) + return; + + search_cancel_keep_selection(term); + selection_cancel(term); } -static void -search_update_selection(struct terminal *term, const struct range *match) -{ - struct grid *grid = term->grid; - int start_row = match->start.row; - int start_col = match->start.col; - int end_row = match->end.row; - int end_col = match->end.col; +void search_selection_cancelled(struct terminal *term) { + term->search.match = (struct coord){-1, -1}; + term->search.match_len = 0; + render_refresh_search(term); +} - xassert(start_row >= 0); - xassert(start_row < grid->num_rows); +static void search_update_selection(struct terminal *term, + const struct range *match) { + struct grid *grid = term->grid; + int start_row = match->start.row; + int start_col = match->start.col; + int end_row = match->end.row; + int end_col = match->end.col; - bool move_viewport = true; + xassert(start_row >= 0); + xassert(start_row < grid->num_rows); - int view_end = (grid->view + term->rows - 1) & (grid->num_rows - 1); - if (view_end >= grid->view) { - /* Viewport does *not* wrap around */ - if (start_row >= grid->view && end_row <= view_end) - move_viewport = false; - } else { - /* Viewport wraps */ - if (start_row >= grid->view || end_row <= view_end) - move_viewport = false; - } + bool move_viewport = true; - if (move_viewport) { - int rebased_new_view = grid_row_abs_to_sb(grid, term->rows, start_row); + int view_end = (grid->view + term->rows - 1) & (grid->num_rows - 1); + if (view_end >= grid->view) { + /* Viewport does *not* wrap around */ + if (start_row >= grid->view && end_row <= view_end) + move_viewport = false; + } else { + /* Viewport wraps */ + if (start_row >= grid->view || end_row <= view_end) + move_viewport = false; + } - rebased_new_view -= term->rows / 2; - rebased_new_view = - min(max(rebased_new_view, 0), grid->num_rows - term->rows); + if (move_viewport) { + int rebased_new_view = grid_row_abs_to_sb(grid, term->rows, start_row); - const int old_view = grid->view; - int new_view = grid_row_sb_to_abs(grid, term->rows, rebased_new_view); + rebased_new_view -= term->rows / 2; + rebased_new_view = + min(max(rebased_new_view, 0), grid->num_rows - term->rows); - /* Scrollback may not be completely filled yet */ - { - const int mask = grid->num_rows - 1; - while (grid->rows[new_view] == NULL) - new_view = (new_view + 1) & mask; - } + const int old_view = grid->view; + int new_view = grid_row_sb_to_abs(grid, term->rows, rebased_new_view); -#if defined(_DEBUG) - /* Verify all to-be-visible rows have been allocated */ - for (int r = 0; r < term->rows; r++) - xassert(grid->rows[(new_view + r) & (grid->num_rows - 1)] != NULL); -#endif - -#if defined(_DEBUG) - { - int rel_start_row = grid_row_abs_to_sb(grid, term->rows, start_row); - int rel_view = grid_row_abs_to_sb(grid, term->rows, new_view); - xassert(rel_view <= rel_start_row); - xassert(rel_start_row < rel_view + term->rows); - } -#endif - - /* Update view */ - grid->view = new_view; - if (new_view != old_view) - term_damage_view(term); - } - - if (start_row != term->search.match.row || - start_col != term->search.match.col || - - /* Pointer leave events trigger selection_finalize() :/ */ - !term->selection.ongoing) + /* Scrollback may not be completely filled yet */ { - int selection_row = start_row - grid->view + grid->num_rows; - selection_row &= grid->num_rows - 1; - - selection_start( - term, start_col, selection_row, SELECTION_CHAR_WISE, false); - - term->search.match.row = start_row; - term->search.match.col = start_col; + const int mask = grid->num_rows - 1; + while (grid->rows[new_view] == NULL) + new_view = (new_view + 1) & mask; } - /* Update selection endpoint */ +#if defined(_DEBUG) + /* Verify all to-be-visible rows have been allocated */ + for (int r = 0; r < term->rows; r++) + xassert(grid->rows[(new_view + r) & (grid->num_rows - 1)] != NULL); +#endif + +#if defined(_DEBUG) { - int selection_row = end_row - grid->view + grid->num_rows; - selection_row &= grid->num_rows - 1; - selection_update(term, end_col, selection_row); + int rel_start_row = grid_row_abs_to_sb(grid, term->rows, start_row); + int rel_view = grid_row_abs_to_sb(grid, term->rows, new_view); + xassert(rel_view <= rel_start_row); + xassert(rel_start_row < rel_view + term->rows); } +#endif + + /* Update view */ + grid->view = new_view; + if (new_view != old_view) + term_damage_view(term); + } + + if (start_row != term->search.match.row || + start_col != term->search.match.col || + + /* Pointer leave events trigger selection_finalize() :/ */ + !term->selection.ongoing) { + int selection_row = start_row - grid->view + grid->num_rows; + selection_row &= grid->num_rows - 1; + + selection_start(term, start_col, selection_row, SELECTION_CHAR_WISE, false); + + term->search.match.row = start_row; + term->search.match.col = start_col; + } + + /* Update selection endpoint */ + { + int selection_row = end_row - grid->view + grid->num_rows; + selection_row &= grid->num_rows - 1; + selection_update(term, end_col, selection_row); + } } -static bool -search_is_case_sensitive(const struct terminal *term) -{ - switch (term->search.case_mode) { - case SEARCH_CASE_SENSITIVE: return true; - case SEARCH_CASE_INSENSITIVE: return false; - case SEARCH_CASE_SMART: return hasc32upper(term->search.buf); - } +static bool search_is_case_sensitive(const struct terminal *term) { + switch (term->search.case_mode) { + case SEARCH_CASE_SENSITIVE: return true; + case SEARCH_CASE_INSENSITIVE: + return false; + case SEARCH_CASE_SMART: + return hasc32upper(term->search.buf); + } + return true; } -static bool -search_cell_is_word(const struct terminal *term, const struct cell *cell) -{ - char32_t base = cell->wc; - if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) { - const struct composed *c = - composed_lookup(term->composed, base - CELL_COMB_CHARS_LO); - base = c->chars[0]; - } - if (base == 0 || base >= CELL_SPACER) - return false; - return isword(base, false, term->conf->word_delimiters); +static bool search_cell_is_word(const struct terminal *term, + const struct cell *cell) { + char32_t base = cell->wc; + if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) { + const struct composed *c = + composed_lookup(term->composed, base - CELL_COMB_CHARS_LO); + base = c->chars[0]; + } + if (base == 0 || base >= CELL_SPACER) + return false; + return isword(base, false, term->conf->word_delimiters); } /* True if the cell *immediately before* (col-1, row), wrapping back a * row if needed, is a word character. Returns false at scrollback * boundaries (treat boundary as non-word). */ -static bool -search_neighbor_is_word(const struct terminal *term, int row, int col, - bool look_right) -{ - const struct grid *grid = term->grid; - int r = row, c = col; +static bool search_neighbor_is_word(const struct terminal *term, int row, + int col, bool look_right) { + const struct grid *grid = term->grid; + int r = row, c = col; - if (look_right) { - c++; - if (c >= term->cols) { - r = (r + 1) & (grid->num_rows - 1); - c = 0; - if (has_wrapped_around_right(term, r)) - return false; - } - } else { - c--; - if (c < 0) { - r = (r - 1 + grid->num_rows) & (grid->num_rows - 1); - c = term->cols - 1; - if (has_wrapped_around_left(term, r)) - return false; - } - } - - const struct row *gr = grid->rows[r]; - if (gr == NULL) + if (look_right) { + c++; + if (c >= term->cols) { + r = (r + 1) & (grid->num_rows - 1); + c = 0; + if (has_wrapped_around_right(term, r)) return false; - return search_cell_is_word(term, &gr->cells[c]); + } + } else { + c--; + if (c < 0) { + r = (r - 1 + grid->num_rows) & (grid->num_rows - 1); + c = term->cols - 1; + if (has_wrapped_around_left(term, r)) + return false; + } + } + + const struct row *gr = grid->rows[r]; + if (gr == NULL) + return false; + return search_cell_is_word(term, &gr->cells[c]); } /* Extract a row's printable cells into a UTF-8 buffer plus a @@ -376,1702 +371,1667 @@ search_neighbor_is_word(const struct terminal *term, int row, int col, * code unit (so byte_to_col[i] is the column the byte at offset i * came from). The trailing NUL has the column == term->cols. */ struct row_text { - char *utf8; - size_t len; - int *byte_to_col; + char *utf8; + size_t len; + int *byte_to_col; }; -static bool -extract_row_text(const struct terminal *term, const struct row *row, - struct row_text *out) -{ - out->utf8 = NULL; - out->len = 0; - out->byte_to_col = NULL; +static bool extract_row_text(const struct terminal *term, const struct row *row, + struct row_text *out) { + out->utf8 = NULL; + out->len = 0; + out->byte_to_col = NULL; - if (row == NULL) - return false; + if (row == NULL) + return false; - /* Worst case: every cell becomes 4 UTF-8 bytes for the base char, - * plus combining chars (cap to a few). */ - size_t cap = (size_t)term->cols * 8 + 1; - char *buf = xmalloc(cap); - int *map = xmalloc(cap * sizeof(int)); - size_t pos = 0; + /* Worst case: every cell becomes 4 UTF-8 bytes for the base char, + * plus combining chars (cap to a few). */ + size_t cap = (size_t)term->cols * 8 + 1; + char *buf = xmalloc(cap); + int *map = xmalloc(cap * sizeof(int)); + size_t pos = 0; - mbstate_t ps = {0}; + mbstate_t ps = {0}; - for (int col = 0; col < term->cols; col++) { - const struct cell *cell = &row->cells[col]; - char32_t base = cell->wc; + for (int col = 0; col < term->cols; col++) { + const struct cell *cell = &row->cells[col]; + char32_t base = cell->wc; - if (base >= CELL_SPACER) - continue; /* right-half of wide char */ + if (base >= CELL_SPACER) + continue; /* right-half of wide char */ - if (base == 0) - base = U' '; + if (base == 0) + base = U' '; - const struct composed *composed = NULL; - if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) { - composed = composed_lookup(term->composed, base - CELL_COMB_CHARS_LO); - base = composed->chars[0]; - } - - size_t encoded = c32rtomb(&buf[pos], base, &ps); - if (encoded == (size_t)-1) { - buf[pos] = '?'; - encoded = 1; - memset(&ps, 0, sizeof(ps)); - } - for (size_t i = 0; i < encoded; i++) - map[pos + i] = col; - pos += encoded; - - if (composed != NULL) { - for (size_t j = 1; j < composed->count; j++) { - size_t e = c32rtomb(&buf[pos], composed->chars[j], &ps); - if (e == (size_t)-1) { - buf[pos] = '?'; - e = 1; - memset(&ps, 0, sizeof(ps)); - } - for (size_t i = 0; i < e; i++) - map[pos + i] = col; - pos += e; - } - } + const struct composed *composed = NULL; + if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) { + composed = composed_lookup(term->composed, base - CELL_COMB_CHARS_LO); + base = composed->chars[0]; } - buf[pos] = '\0'; - map[pos] = term->cols; - - out->utf8 = buf; - out->len = pos; - out->byte_to_col = map; - return true; -} - -static void -free_row_text(struct row_text *rt) -{ - free(rt->utf8); - free(rt->byte_to_col); - rt->utf8 = NULL; - rt->byte_to_col = NULL; - rt->len = 0; -} - -static bool -search_compile_regex(struct terminal *term) -{ - /* Free any previously compiled regex */ - if (term->search.regex_compiled != NULL) { - regfree(term->search.regex_compiled); - free(term->search.regex_compiled); - term->search.regex_compiled = NULL; + size_t encoded = c32rtomb(&buf[pos], base, &ps); + if (encoded == (size_t)-1) { + buf[pos] = '?'; + encoded = 1; + memset(&ps, 0, sizeof(ps)); } - term->search.regex_valid = false; + for (size_t i = 0; i < encoded; i++) + map[pos + i] = col; + pos += encoded; - if (!term->search.regex || term->search.len == 0) - return false; - - /* Convert search buffer to UTF-8 */ - char pattern[term->search.len * 4 + 1]; - mbstate_t ps = {0}; - size_t out = 0; - for (size_t i = 0; i < term->search.len; i++) { - size_t e = c32rtomb(&pattern[out], term->search.buf[i], &ps); + if (composed != NULL) { + for (size_t j = 1; j < composed->count; j++) { + size_t e = c32rtomb(&buf[pos], composed->chars[j], &ps); if (e == (size_t)-1) { - pattern[out++] = '?'; - memset(&ps, 0, sizeof(ps)); - } else { - out += e; + buf[pos] = '?'; + e = 1; + memset(&ps, 0, sizeof(ps)); } + for (size_t i = 0; i < e; i++) + map[pos + i] = col; + pos += e; + } } - pattern[out] = '\0'; + } - regex_t *re = xmalloc(sizeof(*re)); - int flags = REG_EXTENDED; - if (!search_is_case_sensitive(term)) - flags |= REG_ICASE; + buf[pos] = '\0'; + map[pos] = term->cols; - if (regcomp(re, pattern, flags) != 0) { - free(re); - return false; + out->utf8 = buf; + out->len = pos; + out->byte_to_col = map; + return true; +} + +static void free_row_text(struct row_text *rt) { + free(rt->utf8); + free(rt->byte_to_col); + rt->utf8 = NULL; + rt->byte_to_col = NULL; + rt->len = 0; +} + +static bool search_compile_regex(struct terminal *term) { + /* Free any previously compiled regex */ + if (term->search.regex_compiled != NULL) { + regfree(term->search.regex_compiled); + free(term->search.regex_compiled); + term->search.regex_compiled = NULL; + } + term->search.regex_valid = false; + + if (!term->search.regex || term->search.len == 0) + return false; + + /* Convert search buffer to UTF-8 */ + char pattern[term->search.len * 4 + 1]; + mbstate_t ps = {0}; + size_t out = 0; + for (size_t i = 0; i < term->search.len; i++) { + size_t e = c32rtomb(&pattern[out], term->search.buf[i], &ps); + if (e == (size_t)-1) { + pattern[out++] = '?'; + memset(&ps, 0, sizeof(ps)); + } else { + out += e; } + } + pattern[out] = '\0'; - term->search.regex_compiled = re; - term->search.regex_valid = true; - return true; + regex_t *re = xmalloc(sizeof(*re)); + int flags = REG_EXTENDED; + if (!search_is_case_sensitive(term)) + flags |= REG_ICASE; + + if (regcomp(re, pattern, flags) != 0) { + free(re); + return false; + } + + term->search.regex_compiled = re; + term->search.regex_valid = true; + return true; } /* Find one regex match on a single row. Returns true if found and * fills [start_col, end_col]. */ -static bool -regex_find_in_row(const struct terminal *term, const struct row *row, - int min_col, int max_col, int *out_start, int *out_end) -{ - if (!term->search.regex_valid || row == NULL) - return false; +static bool regex_find_in_row(const struct terminal *term, + const struct row *row, int min_col, int max_col, + int *out_start, int *out_end) { + if (!term->search.regex_valid || row == NULL) + return false; - struct row_text rt; - if (!extract_row_text(term, row, &rt)) - return false; + struct row_text rt; + if (!extract_row_text(term, row, &rt)) + return false; - bool found = false; - regmatch_t m; - size_t scan_from = 0; + bool found = false; + regmatch_t m; + size_t scan_from = 0; - /* Find matches; honor min_col by skipping ones that end before it, - * and max_col by stopping after them. Pick the *first* match whose - * starting column is in [min_col, max_col]. */ - while (scan_from <= rt.len) { - if (regexec(term->search.regex_compiled, rt.utf8 + scan_from, 1, &m, - scan_from > 0 ? REG_NOTBOL : 0) != 0) - break; - if (m.rm_so == m.rm_eo) { - /* Zero-width — advance one byte to avoid infinite loop */ - scan_from++; - continue; - } - - int s_col = rt.byte_to_col[scan_from + m.rm_so]; - int e_col = rt.byte_to_col[scan_from + m.rm_eo - 1]; - - if (s_col >= min_col && s_col <= max_col) { - if (term->search.whole_word) { - if ((s_col > 0 && search_cell_is_word(term, &row->cells[s_col - 1])) || - (e_col + 1 < term->cols && search_cell_is_word(term, &row->cells[e_col + 1]))) - { - scan_from += m.rm_eo; - continue; - } - } - *out_start = s_col; - *out_end = e_col; - found = true; - break; - } - - scan_from += m.rm_eo; + /* Find matches; honor min_col by skipping ones that end before it, + * and max_col by stopping after them. Pick the *first* match whose + * starting column is in [min_col, max_col]. */ + while (scan_from <= rt.len) { + if (regexec(term->search.regex_compiled, rt.utf8 + scan_from, 1, &m, + scan_from > 0 ? REG_NOTBOL : 0) != 0) + break; + if (m.rm_so == m.rm_eo) { + scan_from++; + continue; } - free_row_text(&rt); - return found; + int s_col = rt.byte_to_col[scan_from + m.rm_so]; + int e_col = rt.byte_to_col[scan_from + m.rm_eo - 1]; + + if (s_col >= min_col && s_col <= max_col) { + if (term->search.whole_word) { + if ((s_col > 0 && search_cell_is_word(term, &row->cells[s_col - 1])) || + (e_col + 1 < term->cols && + search_cell_is_word(term, &row->cells[e_col + 1]))) { + scan_from += m.rm_eo; + continue; + } + } + *out_start = s_col; + *out_end = e_col; + found = true; + break; + } + + scan_from += m.rm_eo; + } + + free_row_text(&rt); + return found; } /* Walk rows in the requested direction, returning the first regex * match found within the [start, end] range. */ -static bool -regex_find_next(const struct terminal *term, enum search_direction direction, - struct coord abs_start, struct coord abs_end, - struct range *match) -{ - const struct grid *grid = term->grid; - const bool backward = direction != SEARCH_FORWARD; +static bool regex_find_next(const struct terminal *term, + enum search_direction direction, + struct coord abs_start, struct coord abs_end, + struct range *match) { + const struct grid *grid = term->grid; + const bool backward = direction != SEARCH_FORWARD; - int row_no = abs_start.row; - int from_col = abs_start.col; - int to_col = (row_no == abs_end.row) ? abs_end.col : - (backward ? 0 : term->cols - 1); + int row_no = abs_start.row; + int from_col = abs_start.col; + int to_col = + (row_no == abs_end.row) ? abs_end.col : (backward ? 0 : term->cols - 1); - while (true) { - const struct row *row = grid->rows[row_no]; - if (row != NULL) { - int min_col = backward ? min(from_col, to_col) : from_col; - int max_col = backward ? max(from_col, to_col) : to_col; + while (true) { + const struct row *row = grid->rows[row_no]; + if (row != NULL) { + int min_col = backward ? min(from_col, to_col) : from_col; + int max_col = backward ? max(from_col, to_col) : to_col; - int s, e; - if (regex_find_in_row(term, row, min_col, max_col, &s, &e)) { - match->start = (struct coord){s, row_no}; - match->end = (struct coord){e, row_no}; - return true; - } - } - - if (row_no == abs_end.row) - break; - - if (backward) { - row_no = (row_no - 1 + grid->num_rows) & (grid->num_rows - 1); - from_col = term->cols - 1; - to_col = (row_no == abs_end.row) ? abs_end.col : 0; - } else { - row_no = (row_no + 1) & (grid->num_rows - 1); - from_col = 0; - to_col = (row_no == abs_end.row) ? abs_end.col : term->cols - 1; - } + int s, e; + if (regex_find_in_row(term, row, min_col, max_col, &s, &e)) { + match->start = (struct coord){s, row_no}; + match->end = (struct coord){e, row_no}; + return true; + } } - return false; -} + if (row_no == abs_end.row) + break; -static ssize_t -matches_cell(const struct terminal *term, const struct cell *cell, size_t search_ofs) -{ - assert(search_ofs < term->search.len); - - char32_t base = cell->wc; - const struct composed *composed = NULL; - - if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) - { - composed = composed_lookup(term->composed, base - CELL_COMB_CHARS_LO); - base = composed->chars[0]; - } - - if (composed == NULL && base == 0 && term->search.buf[search_ofs] == U' ') - return 1; - - if (search_is_case_sensitive(term)) { - if (c32ncmp(&base, &term->search.buf[search_ofs], 1) != 0) - return -1; + if (backward) { + row_no = (row_no - 1 + grid->num_rows) & (grid->num_rows - 1); + from_col = term->cols - 1; + to_col = (row_no == abs_end.row) ? abs_end.col : 0; } else { - if (c32ncasecmp(&base, &term->search.buf[search_ofs], 1) != 0) - return -1; + row_no = (row_no + 1) & (grid->num_rows - 1); + from_col = 0; + to_col = (row_no == abs_end.row) ? abs_end.col : term->cols - 1; } + } - if (composed != NULL) { - if (search_ofs + composed->count > term->search.len) - return -1; - - for (size_t j = 1; j < composed->count; j++) { - if (composed->chars[j] != term->search.buf[search_ofs + j]) - return -1; - } - } - - return composed != NULL ? composed->count : 1; + return false; } -static bool -find_next(struct terminal *term, enum search_direction direction, - struct coord abs_start, struct coord abs_end, struct range *match) -{ +static ssize_t matches_cell(const struct terminal *term, + const struct cell *cell, size_t search_ofs) { + assert(search_ofs < term->search.len); + + char32_t base = cell->wc; + const struct composed *composed = NULL; + + if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) { + composed = composed_lookup(term->composed, base - CELL_COMB_CHARS_LO); + base = composed->chars[0]; + } + + if (composed == NULL && base == 0 && term->search.buf[search_ofs] == U' ') + return 1; + + if (search_is_case_sensitive(term)) { + if (c32ncmp(&base, &term->search.buf[search_ofs], 1) != 0) + return -1; + } else { + if (c32ncasecmp(&base, &term->search.buf[search_ofs], 1) != 0) + return -1; + } + + if (composed != NULL) { + if (search_ofs + composed->count > term->search.len) + return -1; + + for (size_t j = 1; j < composed->count; j++) { + if (composed->chars[j] != term->search.buf[search_ofs + j]) + return -1; + } + } + + return composed != NULL ? composed->count : 1; +} + +static bool find_next(struct terminal *term, enum search_direction direction, + struct coord abs_start, struct coord abs_end, + struct range *match) { #define ROW_DEC(_r) ((_r) = ((_r) - 1 + grid->num_rows) & (grid->num_rows - 1)) #define ROW_INC(_r) ((_r) = ((_r) + 1) & (grid->num_rows - 1)) - if (term->search.regex && term->search.regex_valid) - return regex_find_next(term, direction, abs_start, abs_end, match); + if (term->search.regex && term->search.regex_valid) + return regex_find_next(term, direction, abs_start, abs_end, match); - struct grid *grid = term->grid; - const bool backward = direction != SEARCH_FORWARD; + struct grid *grid = term->grid; + const bool backward = direction != SEARCH_FORWARD; - LOG_DBG("%s: start: %dx%d, end: %dx%d", backward ? "backward" : "forward", - abs_start.row, abs_start.col, abs_end.row, abs_end.col); + LOG_DBG("%s: start: %dx%d, end: %dx%d", backward ? "backward" : "forward", + abs_start.row, abs_start.col, abs_end.row, abs_end.col); - xassert(abs_start.row >= 0); - xassert(abs_start.row < grid->num_rows); - xassert(abs_start.col >= 0); - xassert(abs_start.col < term->cols); + xassert(abs_start.row >= 0); + xassert(abs_start.row < grid->num_rows); + xassert(abs_start.col >= 0); + xassert(abs_start.col < term->cols); - xassert(abs_end.row >= 0); - xassert(abs_end.row < grid->num_rows); - xassert(abs_end.col >= 0); - xassert(abs_end.col < term->cols); + xassert(abs_end.row >= 0); + xassert(abs_end.row < grid->num_rows); + xassert(abs_end.col >= 0); + xassert(abs_end.col < term->cols); - for (int match_start_row = abs_start.row, match_start_col = abs_start.col; - ; - backward ? ROW_DEC(match_start_row) : ROW_INC(match_start_row)) { + for (int match_start_row = abs_start.row, match_start_col = abs_start.col;; + backward ? ROW_DEC(match_start_row) : ROW_INC(match_start_row)) { - const struct row *row = grid->rows[match_start_row]; - if (row == NULL) { - if (match_start_row == abs_end.row) - break; - continue; - } - - for (; - backward ? match_start_col >= 0 : match_start_col < term->cols; - backward ? match_start_col-- : match_start_col++) - { - if (matches_cell(term, &row->cells[match_start_col], 0) < 0) { - if (match_start_row == abs_end.row && - match_start_col == abs_end.col) - { - break; - } - continue; - } - - /* - * Got a match on the first letter. Now we'll see if the - * rest of the search buffer matches. - */ - - LOG_DBG("search: initial match at row=%d, col=%d", - match_start_row, match_start_col); - - int match_end_row = match_start_row; - int match_end_col = match_start_col; - const struct row *match_row = row; - size_t match_len = 0; - - for (size_t i = 0; i < term->search.len;) { - if (match_end_col >= term->cols) { - ROW_INC(match_end_row); - match_end_col = 0; - - match_row = grid->rows[match_end_row]; - if (match_row == NULL) - break; - } - - if (match_row->cells[match_end_col].wc >= CELL_SPACER) { - match_end_col++; - continue; - } - - ssize_t additional_chars = matches_cell( - term, &match_row->cells[match_end_col], i); - if (additional_chars < 0) - break; - - i += additional_chars; - match_len += additional_chars; - match_end_col++; - - while (match_end_col < term->cols && - match_row->cells[match_end_col].wc > CELL_SPACER) - { - match_end_col++; - } - } - - if (match_len != term->search.len) { - /* Didn't match (completely) */ - - if (match_start_row == abs_end.row && - match_start_col == abs_end.col) - { - break; - } - - continue; - } - - if (term->search.whole_word) { - /* Reject if neighbour cells are word-chars */ - if (search_neighbor_is_word( - term, match_start_row, match_start_col, false) || - search_neighbor_is_word( - term, match_end_row, match_end_col - 1, true)) - { - if (match_start_row == abs_end.row && - match_start_col == abs_end.col) - { - break; - } - continue; - } - } - - *match = (struct range){ - .start = {match_start_col, match_start_row}, - .end = {match_end_col - 1, match_end_row}, - }; - - return true; - } - - if (match_start_row == abs_end.row && match_start_col == abs_end.col) - break; - - match_start_col = backward ? term->cols - 1 : 0; + const struct row *row = grid->rows[match_start_row]; + if (row == NULL) { + if (match_start_row == abs_end.row) + break; + continue; } - return false; + for (; backward ? match_start_col >= 0 : match_start_col < term->cols; + backward ? match_start_col-- : match_start_col++) { + if (matches_cell(term, &row->cells[match_start_col], 0) < 0) { + if (match_start_row == abs_end.row && match_start_col == abs_end.col) { + break; + } + continue; + } + + /* + * Got a match on the first letter. Now we'll see if the + * rest of the search buffer matches. + */ + + LOG_DBG("search: initial match at row=%d, col=%d", match_start_row, + match_start_col); + + int match_end_row = match_start_row; + int match_end_col = match_start_col; + const struct row *match_row = row; + size_t match_len = 0; + + for (size_t i = 0; i < term->search.len;) { + if (match_end_col >= term->cols) { + ROW_INC(match_end_row); + match_end_col = 0; + + match_row = grid->rows[match_end_row]; + if (match_row == NULL) + break; + } + + if (match_row->cells[match_end_col].wc >= CELL_SPACER) { + match_end_col++; + continue; + } + + ssize_t additional_chars = + matches_cell(term, &match_row->cells[match_end_col], i); + if (additional_chars < 0) + break; + + i += additional_chars; + match_len += additional_chars; + match_end_col++; + + while (match_end_col < term->cols && + match_row->cells[match_end_col].wc > CELL_SPACER) { + match_end_col++; + } + } + + if (match_len != term->search.len) { + /* Didn't match (completely) */ + + if (match_start_row == abs_end.row && match_start_col == abs_end.col) { + break; + } + + continue; + } + + if (term->search.whole_word) { + /* Reject if neighbour cells are word-chars */ + if (search_neighbor_is_word(term, match_start_row, match_start_col, + false) || + search_neighbor_is_word(term, match_end_row, match_end_col - 1, + true)) { + if (match_start_row == abs_end.row && + match_start_col == abs_end.col) { + break; + } + continue; + } + } + + *match = (struct range){ + .start = {match_start_col, match_start_row}, + .end = {match_end_col - 1, match_end_row}, + }; + + return true; + } + + if (match_start_row == abs_end.row && match_start_col == abs_end.col) + break; + + match_start_col = backward ? term->cols - 1 : 0; + } + + return false; } /* Count total matches across the whole grid, capped at * SEARCH_COUNT_CAP. Returns the cap if we hit it. */ -static size_t -search_count_all(struct terminal *term) -{ - if (term->search.len == 0) - return 0; - if (term->search.regex && !term->search.regex_valid) - return 0; +static size_t search_count_all(struct terminal *term) { + if (term->search.len == 0) + return 0; + if (term->search.regex && !term->search.regex_valid) + return 0; - struct grid *grid = term->grid; + struct grid *grid = term->grid; - /* Start at the very top of the scrollback (oldest row) */ - int oldest = (grid->offset + term->rows) & (grid->num_rows - 1); + /* Start at the very top of the scrollback (oldest row) */ + int oldest = (grid->offset + term->rows) & (grid->num_rows - 1); - struct coord pos = { 0, oldest }; - struct coord end = { term->cols - 1, oldest }; - /* end.row should be the row *before* oldest, i.e. the newest */ - end.row = (oldest - 1 + grid->num_rows) & (grid->num_rows - 1); + struct coord pos = {0, oldest}; + struct coord end = {term->cols - 1, oldest}; + /* end.row should be the row *before* oldest, i.e. the newest */ + end.row = (oldest - 1 + grid->num_rows) & (grid->num_rows - 1); - size_t count = 0; - while (count < SEARCH_COUNT_CAP) { - struct range m; - if (!find_next(term, SEARCH_FORWARD, pos, end, &m)) - break; - count++; + size_t count = 0; + while (count < SEARCH_COUNT_CAP) { + struct range m; + if (!find_next(term, SEARCH_FORWARD, pos, end, &m)) + break; + count++; - /* Advance one cell past the match start */ - pos.col = m.start.col + 1; - pos.row = m.start.row; - if (pos.col >= term->cols) { - pos.col = 0; - pos.row = (pos.row + 1) & (grid->num_rows - 1); - if (pos.row == oldest) - break; /* wrapped */ - } - - if (pos.row == end.row && pos.col > end.col) - break; + /* Advance one cell past the match start */ + pos.col = m.start.col + 1; + pos.row = m.start.row; + if (pos.col >= term->cols) { + pos.col = 0; + pos.row = (pos.row + 1) & (grid->num_rows - 1); + if (pos.row == oldest) + break; /* wrapped */ } - return count; + if (pos.row == end.row && pos.col > end.col) + break; + } + + return count; } /* Recompute current_idx by counting matches from the top of the * scrollback up to (and including) the current match position. */ -static size_t -search_compute_current_idx(struct terminal *term) -{ - if (term->search.match_len == 0 || term->search.len == 0) - return 0; - if (term->search.regex && !term->search.regex_valid) - return 0; - - struct grid *grid = term->grid; - int oldest = (grid->offset + term->rows) & (grid->num_rows - 1); - - /* Sentinel "newest cell" - we'll abort when we pass our own match */ - struct coord newest = { term->cols - 1, - (oldest - 1 + grid->num_rows) & (grid->num_rows - 1) }; - - struct coord pos = { 0, oldest }; - - size_t idx = 0; - while (idx < SEARCH_COUNT_CAP) { - struct range m; - if (!find_next(term, SEARCH_FORWARD, pos, newest, &m)) - break; - idx++; - - if (m.start.row == term->search.match.row && - m.start.col == term->search.match.col) - return idx; - - pos.col = m.start.col + 1; - pos.row = m.start.row; - if (pos.col >= term->cols) { - pos.col = 0; - pos.row = (pos.row + 1) & (grid->num_rows - 1); - if (pos.row == oldest) - break; - } - } - +static size_t search_compute_current_idx(struct terminal *term) { + if (term->search.match_len == 0 || term->search.len == 0) return 0; + if (term->search.regex && !term->search.regex_valid) + return 0; + + struct grid *grid = term->grid; + int oldest = (grid->offset + term->rows) & (grid->num_rows - 1); + + /* Sentinel "newest cell" - we'll abort when we pass our own match */ + struct coord newest = {term->cols - 1, + (oldest - 1 + grid->num_rows) & (grid->num_rows - 1)}; + + struct coord pos = {0, oldest}; + + size_t idx = 0; + while (idx < SEARCH_COUNT_CAP) { + struct range m; + if (!find_next(term, SEARCH_FORWARD, pos, newest, &m)) + break; + idx++; + + if (m.start.row == term->search.match.row && + m.start.col == term->search.match.col) + return idx; + + pos.col = m.start.col + 1; + pos.row = m.start.row; + if (pos.col >= term->cols) { + pos.col = 0; + pos.row = (pos.row + 1) & (grid->num_rows - 1); + if (pos.row == oldest) + break; + } + } + + return 0; } -static void -search_find_next(struct terminal *term, enum search_direction direction) -{ - struct grid *grid = term->grid; +static void search_find_next(struct terminal *term, + enum search_direction direction) { + struct grid *grid = term->grid; - /* Recompile regex if active (cheap when len==0; no-op otherwise) */ - if (term->search.regex) - search_compile_regex(term); + /* Recompile regex if active (cheap when len==0; no-op otherwise) */ + if (term->search.regex) + search_compile_regex(term); - if (term->search.len == 0) { - term->search.match = (struct coord){-1, -1}; - term->search.match_len = 0; - term->search.total_count = 0; - term->search.current_idx = 0; - term->search.wrapped = false; - selection_cancel(term); - return; - } + if (term->search.len == 0) { + term->search.match = (struct coord){-1, -1}; + term->search.match_len = 0; + term->search.total_count = 0; + term->search.current_idx = 0; + term->search.wrapped = false; + selection_cancel(term); + return; + } - struct coord start = term->search.match; - size_t len = term->search.match_len; + struct coord start = term->search.match; + size_t len = term->search.match_len; - xassert((len == 0 && start.row == -1 && start.col == -1) || - (len > 0 && start.row >= 0 && start.col >= 0)); + xassert((len == 0 && start.row == -1 && start.col == -1) || + (len > 0 && start.row >= 0 && start.col >= 0)); - if (len == 0) { - /* No previous match, start from the top, or bottom, of the scrollback */ - switch (direction) { - case SEARCH_FORWARD: - start.row = grid_row_absolute_in_view(grid, 0); - start.col = 0; - break; - - case SEARCH_BACKWARD: - case SEARCH_BACKWARD_SAME_POSITION: - start.row = grid_row_absolute_in_view(grid, term->rows - 1); - start.col = term->cols - 1; - break; - } - } else { - /* Continue from last match */ - xassert(start.row >= 0); - xassert(start.col >= 0); - - switch (direction) { - case SEARCH_BACKWARD_SAME_POSITION: - break; - - case SEARCH_BACKWARD: - if (--start.col < 0) { - start.col = term->cols - 1; - start.row += grid->num_rows - 1; - start.row &= grid->num_rows - 1; - } - break; - - case SEARCH_FORWARD: - if (++start.col >= term->cols) { - start.col = 0; - start.row++; - start.row &= grid->num_rows - 1; - } - break; - } - - xassert(start.row >= 0); - xassert(start.row < grid->num_rows); - xassert(start.col >= 0); - xassert(start.col < term->cols); - } - - LOG_DBG( - "update: %s: starting at row=%d col=%d " - "(offset = %d, view = %d)", - direction != SEARCH_FORWARD ? "backward" : "forward", - start.row, start.col, - grid->offset, grid->view); - - struct coord end = start; + if (len == 0) { + /* No previous match, start from the top, or bottom, of the scrollback */ switch (direction) { case SEARCH_FORWARD: - /* Search forward, until we reach the cell *before* current start */ - if (--end.col < 0) { - end.col = term->cols - 1; - end.row += grid->num_rows - 1; - end.row &= grid->num_rows - 1; - } - break; + start.row = grid_row_absolute_in_view(grid, 0); + start.col = 0; + break; case SEARCH_BACKWARD: case SEARCH_BACKWARD_SAME_POSITION: - /* Search backwards, until we reach the cell *after* current start */ - if (++end.col >= term->cols) { - end.col = 0; - end.row++; - end.row &= grid->num_rows - 1; - } - break; + start.row = grid_row_absolute_in_view(grid, term->rows - 1); + start.col = term->cols - 1; + break; + } + } else { + /* Continue from last match */ + xassert(start.row >= 0); + xassert(start.col >= 0); + + switch (direction) { + case SEARCH_BACKWARD_SAME_POSITION: + break; + + case SEARCH_BACKWARD: + if (--start.col < 0) { + start.col = term->cols - 1; + start.row += grid->num_rows - 1; + start.row &= grid->num_rows - 1; + } + break; + + case SEARCH_FORWARD: + if (++start.col >= term->cols) { + start.col = 0; + start.row++; + start.row &= grid->num_rows - 1; + } + break; } - /* Remember previous match position for wrap detection */ - const struct coord prev_match = term->search.match; - const size_t prev_match_len = term->search.match_len; + xassert(start.row >= 0); + xassert(start.row < grid->num_rows); + xassert(start.col >= 0); + xassert(start.col < term->cols); + } - struct range match; - bool found = find_next(term, direction, start, end, &match); + LOG_DBG("update: %s: starting at row=%d col=%d " + "(offset = %d, view = %d)", + direction != SEARCH_FORWARD ? "backward" : "forward", start.row, + start.col, grid->offset, grid->view); - term->search.wrapped = false; + struct coord end = start; + switch (direction) { + case SEARCH_FORWARD: + /* Search forward, until we reach the cell *before* current start */ + if (--end.col < 0) { + end.col = term->cols - 1; + end.row += grid->num_rows - 1; + end.row &= grid->num_rows - 1; + } + break; - if (found) { - LOG_DBG("primary match found at %dx%d", - match.start.row, match.start.col); + case SEARCH_BACKWARD: + case SEARCH_BACKWARD_SAME_POSITION: + /* Search backwards, until we reach the cell *after* current start */ + if (++end.col >= term->cols) { + end.col = 0; + end.row++; + end.row &= grid->num_rows - 1; + } + break; + } - /* Detect wrap: if we had a prior match and the new match is in - * the "wrong" direction relative to it, we wrapped. */ - if (prev_match_len > 0 && - direction != SEARCH_BACKWARD_SAME_POSITION) - { - int oldest = (grid->offset + term->rows) & (grid->num_rows - 1); - int prev_rebased = (prev_match.row - oldest + grid->num_rows) - & (grid->num_rows - 1); - int new_rebased = (match.start.row - oldest + grid->num_rows) - & (grid->num_rows - 1); + /* Remember previous match position for wrap detection */ + const struct coord prev_match = term->search.match; + const size_t prev_match_len = term->search.match_len; - if (direction == SEARCH_FORWARD) { - if (new_rebased < prev_rebased || - (new_rebased == prev_rebased && - match.start.col <= prev_match.col)) - term->search.wrapped = true; - } else { - if (new_rebased > prev_rebased || - (new_rebased == prev_rebased && - match.start.col >= prev_match.col)) - term->search.wrapped = true; - } - } + struct range match; + bool found = find_next(term, direction, start, end, &match); - search_update_selection(term, &match); - term->search.match = match.start; - term->search.match_len = term->search.len; - } else { - LOG_DBG("no match"); - term->search.match = (struct coord){-1, -1}; - term->search.match_len = 0; - selection_cancel(term); + term->search.wrapped = false; + + if (found) { + LOG_DBG("primary match found at %dx%d", match.start.row, match.start.col); + + /* Detect wrap: if we had a prior match and the new match is in + * the "wrong" direction relative to it, we wrapped. */ + if (prev_match_len > 0 && direction != SEARCH_BACKWARD_SAME_POSITION) { + int oldest = (grid->offset + term->rows) & (grid->num_rows - 1); + int prev_rebased = + (prev_match.row - oldest + grid->num_rows) & (grid->num_rows - 1); + int new_rebased = + (match.start.row - oldest + grid->num_rows) & (grid->num_rows - 1); + + if (direction == SEARCH_FORWARD) { + if (new_rebased < prev_rebased || + (new_rebased == prev_rebased && match.start.col <= prev_match.col)) + term->search.wrapped = true; + } else { + if (new_rebased > prev_rebased || + (new_rebased == prev_rebased && match.start.col >= prev_match.col)) + term->search.wrapped = true; + } } - /* Refresh counter */ - term->search.total_count = search_count_all(term); - term->search.current_idx = search_compute_current_idx(term); + search_update_selection(term, &match); + term->search.match = match.start; + term->search.match_len = term->search.len; + } else { + LOG_DBG("no match"); + term->search.match = (struct coord){-1, -1}; + term->search.match_len = 0; + selection_cancel(term); + } + + /* Refresh counter */ + term->search.total_count = search_count_all(term); + term->search.current_idx = search_compute_current_idx(term); #undef ROW_DEC } -struct search_match_iterator -search_matches_new_iter(struct terminal *term) -{ - return (struct search_match_iterator){ - .term = term, - .start = {0, 0}, - }; +struct search_match_iterator search_matches_new_iter(struct terminal *term) { + return (struct search_match_iterator){ + .term = term, + .start = {0, 0}, + }; } -struct range -search_matches_next(struct search_match_iterator *iter) -{ - struct terminal *term = iter->term; - struct grid *grid = term->grid; +struct range search_matches_next(struct search_match_iterator *iter) { + struct terminal *term = iter->term; + struct grid *grid = term->grid; - if (term->search.match_len == 0) - goto no_match; + if (term->search.match_len == 0) + goto no_match; - if (iter->start.row >= term->rows) - goto no_match; + if (iter->start.row >= term->rows) + goto no_match; - xassert(iter->start.row >= 0); - xassert(iter->start.row < term->rows); - xassert(iter->start.col >= 0); - xassert(iter->start.col < term->cols); + xassert(iter->start.row >= 0); + xassert(iter->start.row < term->rows); + xassert(iter->start.col >= 0); + xassert(iter->start.col < term->cols); - struct coord abs_start = iter->start; - abs_start.row = grid_row_absolute_in_view(grid, abs_start.row); + struct coord abs_start = iter->start; + abs_start.row = grid_row_absolute_in_view(grid, abs_start.row); - struct coord abs_end = { - term->cols - 1, - grid_row_absolute_in_view(grid, term->rows - 1)}; + struct coord abs_end = {term->cols - 1, + grid_row_absolute_in_view(grid, term->rows - 1)}; - /* BUG: matches *starting* outside the view, but ending *inside*, aren't matched */ - struct range match; - bool found = find_next(term, SEARCH_FORWARD, abs_start, abs_end, &match); - if (!found) - goto no_match; + /* BUG: matches *starting* outside the view, but ending *inside*, aren't + * matched */ + struct range match; + bool found = find_next(term, SEARCH_FORWARD, abs_start, abs_end, &match); + if (!found) + goto no_match; - LOG_DBG("match at (absolute coordinates) %dx%d-%dx%d", - match.start.row, match.start.col, - match.end.row, match.end.col); + LOG_DBG("match at (absolute coordinates) %dx%d-%dx%d", match.start.row, + match.start.col, match.end.row, match.end.col); - /* Convert absolute row numbers back to view relative */ - match.start.row = match.start.row - grid->view + grid->num_rows; - match.start.row &= grid->num_rows - 1; - match.end.row = match.end.row - grid->view + grid->num_rows; - match.end.row &= grid->num_rows - 1; + /* Convert absolute row numbers back to view relative */ + match.start.row = match.start.row - grid->view + grid->num_rows; + match.start.row &= grid->num_rows - 1; + match.end.row = match.end.row - grid->view + grid->num_rows; + match.end.row &= grid->num_rows - 1; - LOG_DBG("match at (view-local coordinates) %dx%d-%dx%d, view=%d", - match.start.row, match.start.col, - match.end.row, match.end.col, grid->view); + LOG_DBG("match at (view-local coordinates) %dx%d-%dx%d, view=%d", + match.start.row, match.start.col, match.end.row, match.end.col, + grid->view); - /* Assert match end comes *after* the match start */ - xassert(match.end.row > match.start.row || - (match.end.row == match.start.row && - match.end.col >= match.start.col)); + /* Assert match end comes *after* the match start */ + xassert( + match.end.row > match.start.row || + (match.end.row == match.start.row && match.end.col >= match.start.col)); - /* Assert the match starts at, or after, the iterator position */ - xassert(match.start.row > iter->start.row || - (match.start.row == iter->start.row && - match.start.col >= iter->start.col)); + /* Assert the match starts at, or after, the iterator position */ + xassert(match.start.row > iter->start.row || + (match.start.row == iter->start.row && + match.start.col >= iter->start.col)); - /* Continue at next column, next time */ - iter->start.row = match.start.row; - iter->start.col = match.start.col + 1; + /* Continue at next column, next time */ + iter->start.row = match.start.row; + iter->start.col = match.start.col + 1; - if (iter->start.col >= term->cols) { - iter->start.col = 0; - iter->start.row++; /* Overflow is caught in next iteration */ - } + if (iter->start.col >= term->cols) { + iter->start.col = 0; + iter->start.row++; /* Overflow is caught in next iteration */ + } - xassert(iter->start.row >= 0); - xassert(iter->start.row <= term->rows); - xassert(iter->start.col >= 0); - xassert(iter->start.col < term->cols); - return match; + xassert(iter->start.row >= 0); + xassert(iter->start.row <= term->rows); + xassert(iter->start.col >= 0); + xassert(iter->start.col < term->cols); + return match; no_match: - iter->start.row = -1; - iter->start.col = -1; - return (struct range){{-1, -1}, {-1, -1}}; + iter->start.row = -1; + iter->start.col = -1; + return (struct range){{-1, -1}, {-1, -1}}; } -static void -add_wchars(struct terminal *term, char32_t *src, size_t count) -{ - /* Strip non-printable characters */ - for (size_t i = 0, j = 0, orig_count = count; i < orig_count; i++) { - if (isc32print(src[i])) - src[j++] = src[i]; - else - count--; - } +static void add_wchars(struct terminal *term, char32_t *src, size_t count) { + /* Strip non-printable characters */ + for (size_t i = 0, j = 0, orig_count = count; i < orig_count; i++) { + if (isc32print(src[i])) + src[j++] = src[i]; + else + count--; + } - if (!search_ensure_size(term, term->search.len + count)) - return; + if (!search_ensure_size(term, term->search.len + count)) + return; - xassert(term->search.len + count < term->search.sz); + xassert(term->search.len + count < term->search.sz); - memmove(&term->search.buf[term->search.cursor + count], - &term->search.buf[term->search.cursor], - (term->search.len - term->search.cursor) * sizeof(char32_t)); + memmove(&term->search.buf[term->search.cursor + count], + &term->search.buf[term->search.cursor], + (term->search.len - term->search.cursor) * sizeof(char32_t)); - memcpy(&term->search.buf[term->search.cursor], src, count * sizeof(char32_t)); + memcpy(&term->search.buf[term->search.cursor], src, count * sizeof(char32_t)); - term->search.len += count; - term->search.cursor += count; - term->search.buf[term->search.len] = U'\0'; + term->search.len += count; + term->search.cursor += count; + term->search.buf[term->search.len] = U'\0'; } -void -search_add_chars(struct terminal *term, const char *src, size_t count) -{ - size_t chars = mbsntoc32(NULL, src, count, 0); - if (chars == (size_t)-1) { - LOG_ERRNO("failed to convert %.*s to Unicode", (int)count, src); - return; - } +void search_add_chars(struct terminal *term, const char *src, size_t count) { + size_t chars = mbsntoc32(NULL, src, count, 0); + if (chars == (size_t)-1) { + LOG_ERRNO("failed to convert %.*s to Unicode", (int)count, src); + return; + } - char32_t c32s[chars + 1]; - mbsntoc32(c32s, src, count, chars); - add_wchars(term, c32s, chars); + char32_t c32s[chars + 1]; + mbsntoc32(c32s, src, count, chars); + add_wchars(term, c32s, chars); } -enum extend_direction {SEARCH_EXTEND_LEFT, SEARCH_EXTEND_RIGHT}; +enum extend_direction { SEARCH_EXTEND_LEFT, SEARCH_EXTEND_RIGHT }; -static bool -coord_advance_left(const struct terminal *term, struct coord *pos, - const struct row **row) -{ - const struct grid *grid = term->grid; - struct coord new_pos = *pos; +static bool coord_advance_left(const struct terminal *term, struct coord *pos, + const struct row **row) { + const struct grid *grid = term->grid; + struct coord new_pos = *pos; - if (--new_pos.col < 0) { - new_pos.row = (new_pos.row - 1 + grid->num_rows) & (grid->num_rows - 1); - new_pos.col = term->cols - 1; + if (--new_pos.col < 0) { + new_pos.row = (new_pos.row - 1 + grid->num_rows) & (grid->num_rows - 1); + new_pos.col = term->cols - 1; - if (has_wrapped_around_left(term, new_pos.row)) - return false; + if (has_wrapped_around_left(term, new_pos.row)) + return false; - if (row != NULL) - *row = grid->rows[new_pos.row]; - } + if (row != NULL) + *row = grid->rows[new_pos.row]; + } - *pos = new_pos; - return true; + *pos = new_pos; + return true; } -static bool -coord_advance_right(const struct terminal *term, struct coord *pos, - const struct row **row) -{ - const struct grid *grid = term->grid; - struct coord new_pos = *pos; +static bool coord_advance_right(const struct terminal *term, struct coord *pos, + const struct row **row) { + const struct grid *grid = term->grid; + struct coord new_pos = *pos; - if (++new_pos.col >= term->cols) { - new_pos.row = (new_pos.row + 1) & (grid->num_rows - 1); - new_pos.col = 0; + if (++new_pos.col >= term->cols) { + new_pos.row = (new_pos.row + 1) & (grid->num_rows - 1); + new_pos.col = 0; - if (has_wrapped_around_right(term, new_pos.row)) - return false; + if (has_wrapped_around_right(term, new_pos.row)) + return false; - if (row != NULL) - *row = grid->rows[new_pos.row]; - } + if (row != NULL) + *row = grid->rows[new_pos.row]; + } - *pos = new_pos; - return true; + *pos = new_pos; + return true; } -static bool -search_extend_find_char(const struct terminal *term, struct coord *target, - enum extend_direction direction) -{ - if (term->search.match_len == 0) - return false; +static bool search_extend_find_char(const struct terminal *term, + struct coord *target, + enum extend_direction direction) { + if (term->search.match_len == 0) + return false; - struct coord pos = direction == SEARCH_EXTEND_LEFT - ? selection_get_start(term) : selection_get_end(term); - xassert(pos.row >= 0); - xassert(pos.row < term->grid->num_rows); + struct coord pos = direction == SEARCH_EXTEND_LEFT ? selection_get_start(term) + : selection_get_end(term); + xassert(pos.row >= 0); + xassert(pos.row < term->grid->num_rows); - *target = pos; + *target = pos; - const struct row *row = term->grid->rows[pos.row]; + const struct row *row = term->grid->rows[pos.row]; - while (true) { - switch (direction) { - case SEARCH_EXTEND_LEFT: - if (!coord_advance_left(term, &pos, &row)) - return false; - break; - - case SEARCH_EXTEND_RIGHT: - if (!coord_advance_right(term, &pos, &row)) - return false; - break; - } - - const char32_t wc = row->cells[pos.col].wc; - - if (wc >= CELL_SPACER || wc == U'\0') - continue; - - *target = pos; - return true; - } -} - -static bool -search_extend_find_char_left(const struct terminal *term, struct coord *target) -{ - return search_extend_find_char(term, target, SEARCH_EXTEND_LEFT); -} - -static bool -search_extend_find_char_right(const struct terminal *term, struct coord *target) -{ - return search_extend_find_char(term, target, SEARCH_EXTEND_RIGHT); -} - -static bool -search_extend_find_word(const struct terminal *term, bool spaces_only, - struct coord *target, enum extend_direction direction) -{ - if (term->search.match_len == 0) - return false; - - struct grid *grid = term->grid; - struct coord pos = direction == SEARCH_EXTEND_LEFT - ? selection_get_start(term) - : selection_get_end(term); - - xassert(pos.row >= 0); - xassert(pos.row < grid->num_rows); - - *target = pos; - - /* First character to consider is the *next* character */ + while (true) { switch (direction) { case SEARCH_EXTEND_LEFT: - if (!coord_advance_left(term, &pos, NULL)) - return false; - break; - - case SEARCH_EXTEND_RIGHT: - if (!coord_advance_right(term, &pos, NULL)) - return false; - break; - } - - xassert(pos.row >= 0); - xassert(pos.row < grid->num_rows); - xassert(grid->rows[pos.row] != NULL); - - /* Find next word boundary */ - switch (direction) { - case SEARCH_EXTEND_LEFT: - selection_find_word_boundary_left(term, &pos, spaces_only); - break; - - case SEARCH_EXTEND_RIGHT: - selection_find_word_boundary_right(term, &pos, spaces_only, false); - break; - } - - *target = pos; - return true; -} - -static bool -search_extend_find_word_left(const struct terminal *term, bool spaces_only, - struct coord *target) -{ - return search_extend_find_word(term, spaces_only, target, SEARCH_EXTEND_LEFT); -} - -static bool -search_extend_find_word_right(const struct terminal *term, bool spaces_only, - struct coord *target) -{ - return search_extend_find_word(term, spaces_only, target, SEARCH_EXTEND_RIGHT); -} - -static bool -search_extend_find_line(const struct terminal *term, struct coord *target, - enum extend_direction direction) -{ - if (term->search.match_len == 0) + if (!coord_advance_left(term, &pos, &row)) return false; - - struct coord pos = direction == SEARCH_EXTEND_LEFT - ? selection_get_start(term) : selection_get_end(term); - - xassert(pos.row >= 0); - xassert(pos.row < term->grid->num_rows); - - *target = pos; - - const struct grid *grid = term->grid; - - switch (direction) { - case SEARCH_EXTEND_LEFT: - pos.row = (pos.row - 1 + grid->num_rows) & (grid->num_rows - 1); - if (has_wrapped_around_left(term, pos.row)) - return false; - break; + break; case SEARCH_EXTEND_RIGHT: - pos.row = (pos.row + 1) & (grid->num_rows - 1); - if (has_wrapped_around_right(term, pos.row)) - return false; - break; + if (!coord_advance_right(term, &pos, &row)) + return false; + break; } + const char32_t wc = row->cells[pos.col].wc; + + if (wc >= CELL_SPACER || wc == U'\0') + continue; + *target = pos; return true; + } } -static bool -search_extend_find_line_up(const struct terminal *term, struct coord *target) -{ - return search_extend_find_line(term, target, SEARCH_EXTEND_LEFT); +static bool search_extend_find_char_left(const struct terminal *term, + struct coord *target) { + return search_extend_find_char(term, target, SEARCH_EXTEND_LEFT); } -static bool -search_extend_find_line_down(const struct terminal *term, struct coord *target) -{ - return search_extend_find_line(term, target, SEARCH_EXTEND_RIGHT); +static bool search_extend_find_char_right(const struct terminal *term, + struct coord *target) { + return search_extend_find_char(term, target, SEARCH_EXTEND_RIGHT); } -static void -search_extend_left(struct terminal *term, const struct coord *target) -{ - if (term->search.match_len == 0) - return; +static bool search_extend_find_word(const struct terminal *term, + bool spaces_only, struct coord *target, + enum extend_direction direction) { + if (term->search.match_len == 0) + return false; - const struct coord last_coord = selection_get_start(term); - struct coord pos = *target; - const struct row *row = term->grid->rows[pos.row]; + struct grid *grid = term->grid; + struct coord pos = direction == SEARCH_EXTEND_LEFT ? selection_get_start(term) + : selection_get_end(term); - const bool move_cursor = term->search.cursor != 0; + xassert(pos.row >= 0); + xassert(pos.row < grid->num_rows); - struct extraction_context *ctx = extract_begin(SELECTION_NONE, false); - if (ctx == NULL) - return; + *target = pos; - while (pos.col != last_coord.col || pos.row != last_coord.row) { - if (!extract_one(term, row, &row->cells[pos.col], pos.col, ctx)) - break; - if (!coord_advance_right(term, &pos, &row)) - break; + /* First character to consider is the *next* character */ + switch (direction) { + case SEARCH_EXTEND_LEFT: + if (!coord_advance_left(term, &pos, NULL)) + return false; + break; + + case SEARCH_EXTEND_RIGHT: + if (!coord_advance_right(term, &pos, NULL)) + return false; + break; + } + + xassert(pos.row >= 0); + xassert(pos.row < grid->num_rows); + xassert(grid->rows[pos.row] != NULL); + + /* Find next word boundary */ + switch (direction) { + case SEARCH_EXTEND_LEFT: + selection_find_word_boundary_left(term, &pos, spaces_only); + break; + + case SEARCH_EXTEND_RIGHT: + selection_find_word_boundary_right(term, &pos, spaces_only, false); + break; + } + + *target = pos; + return true; +} + +static bool search_extend_find_word_left(const struct terminal *term, + bool spaces_only, + struct coord *target) { + return search_extend_find_word(term, spaces_only, target, SEARCH_EXTEND_LEFT); +} + +static bool search_extend_find_word_right(const struct terminal *term, + bool spaces_only, + struct coord *target) { + return search_extend_find_word(term, spaces_only, target, + SEARCH_EXTEND_RIGHT); +} + +static bool search_extend_find_line(const struct terminal *term, + struct coord *target, + enum extend_direction direction) { + if (term->search.match_len == 0) + return false; + + struct coord pos = direction == SEARCH_EXTEND_LEFT ? selection_get_start(term) + : selection_get_end(term); + + xassert(pos.row >= 0); + xassert(pos.row < term->grid->num_rows); + + *target = pos; + + const struct grid *grid = term->grid; + + switch (direction) { + case SEARCH_EXTEND_LEFT: + pos.row = (pos.row - 1 + grid->num_rows) & (grid->num_rows - 1); + if (has_wrapped_around_left(term, pos.row)) + return false; + break; + + case SEARCH_EXTEND_RIGHT: + pos.row = (pos.row + 1) & (grid->num_rows - 1); + if (has_wrapped_around_right(term, pos.row)) + return false; + break; + } + + *target = pos; + return true; +} + +static bool search_extend_find_line_up(const struct terminal *term, + struct coord *target) { + return search_extend_find_line(term, target, SEARCH_EXTEND_LEFT); +} + +static bool search_extend_find_line_down(const struct terminal *term, + struct coord *target) { + return search_extend_find_line(term, target, SEARCH_EXTEND_RIGHT); +} + +static void search_extend_left(struct terminal *term, + const struct coord *target) { + if (term->search.match_len == 0) + return; + + const struct coord last_coord = selection_get_start(term); + struct coord pos = *target; + const struct row *row = term->grid->rows[pos.row]; + + const bool move_cursor = term->search.cursor != 0; + + struct extraction_context *ctx = extract_begin(SELECTION_NONE, false); + if (ctx == NULL) + return; + + while (pos.col != last_coord.col || pos.row != last_coord.row) { + if (!extract_one(term, row, &row->cells[pos.col], pos.col, ctx)) + break; + if (!coord_advance_right(term, &pos, &row)) + break; + } + + char32_t *new_text; + size_t new_len; + + if (!extract_finish_wide(ctx, &new_text, &new_len)) + return; + + if (!search_ensure_size(term, term->search.len + new_len)) + return; + + memmove(&term->search.buf[new_len], &term->search.buf[0], + term->search.len * sizeof(term->search.buf[0])); + + size_t actually_copied = 0; + for (size_t i = 0; i < new_len; i++) { + if (new_text[i] == U'\n') { + /* extract() adds newlines, which we never match against */ + continue; } - char32_t *new_text; - size_t new_len; + term->search.buf[actually_copied++] = new_text[i]; + term->search.len++; + } - if (!extract_finish_wide(ctx, &new_text, &new_len)) - return; - - if (!search_ensure_size(term, term->search.len + new_len)) - return; - - memmove(&term->search.buf[new_len], &term->search.buf[0], - term->search.len * sizeof(term->search.buf[0])); - - size_t actually_copied = 0; - for (size_t i = 0; i < new_len; i++) { - if (new_text[i] == U'\n') { - /* extract() adds newlines, which we never match against */ - continue; - } - - term->search.buf[actually_copied++] = new_text[i]; - term->search.len++; - } - - xassert(actually_copied <= new_len); - if (actually_copied < new_len) { - memmove( - &term->search.buf[actually_copied], &term->search.buf[new_len], + xassert(actually_copied <= new_len); + if (actually_copied < new_len) { + memmove(&term->search.buf[actually_copied], &term->search.buf[new_len], (term->search.len - actually_copied) * sizeof(term->search.buf[0])); - } + } - term->search.buf[term->search.len] = U'\0'; - free(new_text); + term->search.buf[term->search.len] = U'\0'; + free(new_text); - if (move_cursor) - term->search.cursor += actually_copied; + if (move_cursor) + term->search.cursor += actually_copied; - struct range match = {.start = *target, .end = selection_get_end(term)}; - search_update_selection(term, &match); + struct range match = {.start = *target, .end = selection_get_end(term)}; + search_update_selection(term, &match); - term->search.match_len = term->search.len; + term->search.match_len = term->search.len; } -static void -search_extend_right(struct terminal *term, const struct coord *target) -{ - if (term->search.match_len == 0) - return; +static void search_extend_right(struct terminal *term, + const struct coord *target) { + if (term->search.match_len == 0) + return; - struct coord pos = selection_get_end(term); - const struct row *row = term->grid->rows[pos.row]; + struct coord pos = selection_get_end(term); + const struct row *row = term->grid->rows[pos.row]; - const bool move_cursor = term->search.cursor == term->search.len; + const bool move_cursor = term->search.cursor == term->search.len; - struct extraction_context *ctx = extract_begin(SELECTION_NONE, false); - if (ctx == NULL) - return; + struct extraction_context *ctx = extract_begin(SELECTION_NONE, false); + if (ctx == NULL) + return; - do { - if (!coord_advance_right(term, &pos, &row)) - break; - if (!extract_one(term, row, &row->cells[pos.col], pos.col, ctx)) - break; - } while (pos.col != target->col || pos.row != target->row); + do { + if (!coord_advance_right(term, &pos, &row)) + break; + if (!extract_one(term, row, &row->cells[pos.col], pos.col, ctx)) + break; + } while (pos.col != target->col || pos.row != target->row); - char32_t *new_text; - size_t new_len; + char32_t *new_text; + size_t new_len; - if (!extract_finish_wide(ctx, &new_text, &new_len)) - return; + if (!extract_finish_wide(ctx, &new_text, &new_len)) + return; - if (!search_ensure_size(term, term->search.len + new_len)) - return; + if (!search_ensure_size(term, term->search.len + new_len)) + return; - for (size_t i = 0; i < new_len; i++) { - if (new_text[i] == U'\n') { - /* extract() adds newlines, which we never match against */ - continue; - } - - term->search.buf[term->search.len++] = new_text[i]; + for (size_t i = 0; i < new_len; i++) { + if (new_text[i] == U'\n') { + /* extract() adds newlines, which we never match against */ + continue; } - term->search.buf[term->search.len] = U'\0'; - free(new_text); + term->search.buf[term->search.len++] = new_text[i]; + } - if (move_cursor) - term->search.cursor = term->search.len; + term->search.buf[term->search.len] = U'\0'; + free(new_text); - struct range match = {.start = term->search.match, .end = *target}; - search_update_selection(term, &match); - term->search.match_len = term->search.len; + if (move_cursor) + term->search.cursor = term->search.len; + + struct range match = {.start = term->search.match, .end = *target}; + search_update_selection(term, &match); + term->search.match_len = term->search.len; } -static size_t -distance_next_word(const struct terminal *term) -{ +static size_t distance_next_word(const struct terminal *term) { + size_t cursor = term->search.cursor; + + /* First eat non-whitespace. This is the word we're skipping past */ + while (cursor < term->search.len) { + if (isc32space(term->search.buf[cursor++])) + break; + } + + xassert(cursor == term->search.len || + isc32space(term->search.buf[cursor - 1])); + + /* Now skip past whitespace, so that we end up at the beginning of + * the next word */ + while (cursor < term->search.len) { + if (!isc32space(term->search.buf[cursor++])) + break; + } + + xassert(cursor == term->search.len || + !isc32space(term->search.buf[cursor - 1])); + + if (cursor < term->search.len && !isc32space(term->search.buf[cursor])) + cursor--; + + return cursor - term->search.cursor; +} + +static size_t distance_prev_word(const struct terminal *term) { + int cursor = term->search.cursor; + + /* First, eat whitespace prefix */ + while (cursor > 0) { + if (!isc32space(term->search.buf[--cursor])) + break; + } + + xassert(cursor == 0 || !isc32space(term->search.buf[cursor])); + + /* Now eat non-whitespace. This is the word we're skipping past */ + while (cursor > 0) { + if (isc32space(term->search.buf[--cursor])) + break; + } + + xassert(cursor == 0 || isc32space(term->search.buf[cursor])); + if (cursor > 0 && isc32space(term->search.buf[cursor])) + cursor++; + + return term->search.cursor - cursor; +} + +static void from_clipboard_cb(char *text, size_t size, void *user) { + struct terminal *term = user; + search_add_chars(term, text, size); +} + +static void from_clipboard_done(void *user) { + struct terminal *term = user; + + LOG_DBG("search: buffer: %ls", (const wchar_t *)term->search.buf); + search_find_next(term, SEARCH_BACKWARD_SAME_POSITION); + render_refresh_search(term); +} + +static bool execute_binding(struct seat *seat, struct terminal *term, + const struct key_binding *binding, uint32_t serial, + bool *update_search_result, + enum search_direction *direction, bool *redraw) { + *update_search_result = *redraw = false; + const enum bind_action_search action = binding->action; + + struct grid *grid = term->grid; + + switch (action) { + case BIND_ACTION_SEARCH_NONE: + return false; + + case BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, term->rows); + return true; + } + return false; + + case BIND_ACTION_SEARCH_SCROLLBACK_UP_HALF_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, max(term->rows / 2, 1)); + return true; + } + break; + + case BIND_ACTION_SEARCH_SCROLLBACK_UP_LINE: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, 1); + return true; + } + break; + + case BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, term->rows); + return true; + } + return false; + + case BIND_ACTION_SEARCH_SCROLLBACK_DOWN_HALF_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, max(term->rows / 2, 1)); + return true; + } + break; + + case BIND_ACTION_SEARCH_SCROLLBACK_DOWN_LINE: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, 1); + return true; + } + break; + + case BIND_ACTION_SEARCH_SCROLLBACK_HOME: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, term->grid->num_rows); + return true; + } + break; + + case BIND_ACTION_SEARCH_SCROLLBACK_END: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, term->grid->num_rows); + return true; + } + break; + + case BIND_ACTION_SEARCH_CANCEL: + if (term->search.view_followed_offset) + grid->view = grid->offset; + else { + grid->view = ensure_view_is_allocated(term, term->search.original_view); + } + term_damage_view(term); + search_cancel(term); + return true; + + case BIND_ACTION_SEARCH_COMMIT: + if (term->search.mode != SEARCH_MODE_NORMAL) { + session_prompt_commit(term); + return true; + } + selection_finalize(seat, term, serial); + search_cancel_keep_selection(term); + return true; + + case BIND_ACTION_SEARCH_FIND_PREV: + if (term->search.last.buf != NULL && term->search.len == 0) { + add_wchars(term, term->search.last.buf, term->search.last.len); + + free(term->search.last.buf); + term->search.last.buf = NULL; + term->search.last.len = 0; + } + + *direction = SEARCH_BACKWARD; + *update_search_result = *redraw = true; + return true; + + case BIND_ACTION_SEARCH_FIND_NEXT: + if (term->search.last.buf != NULL && term->search.len == 0) { + add_wchars(term, term->search.last.buf, term->search.last.len); + + free(term->search.last.buf); + term->search.last.buf = NULL; + term->search.last.len = 0; + } + + *direction = SEARCH_FORWARD; + *update_search_result = *redraw = true; + return true; + + case BIND_ACTION_SEARCH_EDIT_LEFT: + if (term->search.cursor > 0) { + term->search.cursor--; + *redraw = true; + } + return true; + + case BIND_ACTION_SEARCH_EDIT_LEFT_WORD: { + size_t diff = distance_prev_word(term); + term->search.cursor -= diff; + xassert(term->search.cursor <= term->search.len); + + if (diff > 0) + *redraw = true; + return true; + } + + case BIND_ACTION_SEARCH_EDIT_RIGHT: + if (term->search.cursor < term->search.len) { + term->search.cursor++; + *redraw = true; + } + return true; + + case BIND_ACTION_SEARCH_EDIT_RIGHT_WORD: { + size_t diff = distance_next_word(term); + term->search.cursor += diff; + xassert(term->search.cursor <= term->search.len); + + if (diff > 0) + *redraw = true; + return true; + } + + case BIND_ACTION_SEARCH_EDIT_HOME: + if (term->search.cursor != 0) { + term->search.cursor = 0; + *redraw = true; + } + return true; + + case BIND_ACTION_SEARCH_EDIT_END: + if (term->search.cursor != term->search.len) { + term->search.cursor = term->search.len; + *redraw = true; + } + return true; + + case BIND_ACTION_SEARCH_DELETE_PREV: + if (term->search.cursor > 0) { + memmove(&term->search.buf[term->search.cursor - 1], + &term->search.buf[term->search.cursor], + (term->search.len - term->search.cursor) * sizeof(char32_t)); + term->search.cursor--; + term->search.buf[--term->search.len] = U'\0'; + *update_search_result = *redraw = true; + } + return true; + + case BIND_ACTION_SEARCH_DELETE_PREV_WORD: { + size_t diff = distance_prev_word(term); + size_t old_cursor = term->search.cursor; + size_t new_cursor = old_cursor - diff; + + if (diff > 0) { + memmove(&term->search.buf[new_cursor], &term->search.buf[old_cursor], + (term->search.len - old_cursor) * sizeof(char32_t)); + + term->search.len -= diff; + term->search.cursor = new_cursor; + *update_search_result = *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_DELETE_NEXT: + if (term->search.cursor < term->search.len) { + memmove(&term->search.buf[term->search.cursor], + &term->search.buf[term->search.cursor + 1], + (term->search.len - term->search.cursor - 1) * sizeof(char32_t)); + term->search.buf[--term->search.len] = U'\0'; + *update_search_result = *redraw = true; + } + return true; + + case BIND_ACTION_SEARCH_DELETE_NEXT_WORD: { + size_t diff = distance_next_word(term); size_t cursor = term->search.cursor; - /* First eat non-whitespace. This is the word we're skipping past */ - while (cursor < term->search.len) { - if (isc32space(term->search.buf[cursor++])) - break; + if (diff > 0) { + memmove(&term->search.buf[cursor], &term->search.buf[cursor + diff], + (term->search.len - (cursor + diff)) * sizeof(char32_t)); + + term->search.len -= diff; + *update_search_result = *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_DELETE_TO_START: { + if (term->search.cursor > 0) { + memmove(&term->search.buf[0], &term->search.buf[term->search.cursor], + (term->search.len - term->search.cursor) * sizeof(char32_t)); + + term->search.len -= term->search.cursor; + term->search.cursor = 0; + *update_search_result = *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_DELETE_TO_END: { + if (term->search.cursor < term->search.len) { + term->search.buf[term->search.cursor] = '\0'; + term->search.len = term->search.cursor; + *update_search_result = *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_CHAR: { + struct coord target; + if (search_extend_find_char_right(term, &target)) { + search_extend_right(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_WORD: { + struct coord target; + if (search_extend_find_word_right(term, false, &target)) { + search_extend_right(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_WORD_WS: { + struct coord target; + if (search_extend_find_word_right(term, true, &target)) { + search_extend_right(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_LINE_DOWN: { + struct coord target; + if (search_extend_find_line_down(term, &target)) { + search_extend_right(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR: { + struct coord target; + if (search_extend_find_char_left(term, &target)) { + search_extend_left(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD: { + struct coord target; + if (search_extend_find_word_left(term, false, &target)) { + search_extend_left(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD_WS: { + struct coord target; + if (search_extend_find_word_left(term, true, &target)) { + search_extend_left(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_LINE_UP: { + struct coord target; + if (search_extend_find_line_up(term, &target)) { + search_extend_left(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_CLIPBOARD_PASTE: + text_from_clipboard(seat, term, false, &from_clipboard_cb, + &from_clipboard_done, term); + *update_search_result = *redraw = true; + return true; + + case BIND_ACTION_SEARCH_PRIMARY_PASTE: + text_from_primary(seat, term, false, &from_clipboard_cb, + &from_clipboard_done, term); + *update_search_result = *redraw = true; + return true; + + case BIND_ACTION_SEARCH_UNICODE_INPUT: + unicode_mode_activate(term); + return true; + + case BIND_ACTION_SEARCH_TOGGLE_CASE: + term->search.case_mode = (term->search.case_mode + 1) % 3; + *update_search_result = *redraw = true; + return true; + + case BIND_ACTION_SEARCH_TOGGLE_WHOLE_WORD: + term->search.whole_word = !term->search.whole_word; + *update_search_result = *redraw = true; + return true; + + case BIND_ACTION_SEARCH_TOGGLE_REGEX: + term->search.regex = !term->search.regex; + *update_search_result = *redraw = true; + return true; + + case BIND_ACTION_SEARCH_HISTORY_PREV: { + struct search_history_entry *target; + if (term->search.history_pos == NULL) + target = term->search.history_tail; + else + target = term->search.history_pos->prev; + + if (target != NULL) { + term->search.history_pos = target; + term->search.len = 0; + term->search.cursor = 0; + if (term->search.buf != NULL) + term->search.buf[0] = U'\0'; + add_wchars(term, target->buf, target->len); + *update_search_result = *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_HISTORY_NEXT: { + if (term->search.history_pos == NULL) + return true; + + struct search_history_entry *target = term->search.history_pos->next; + term->search.history_pos = target; + + term->search.len = 0; + term->search.cursor = 0; + if (term->search.buf != NULL) + term->search.buf[0] = U'\0'; + + if (target != NULL) + add_wchars(term, target->buf, target->len); + + *update_search_result = *redraw = true; + return true; + } + + case BIND_ACTION_SEARCH_COMMIT_LINE: { + if (term->search.match_len == 0) { + selection_finalize(seat, term, serial); + search_cancel_keep_selection(term); + return true; } - xassert(cursor == term->search.len || isc32space(term->search.buf[cursor - 1])); + /* Extend selection to span entire line(s) of the match */ + const struct coord match_end = selection_get_end(term); + const int start_row = term->search.match.row; + const int end_row = match_end.row; - /* Now skip past whitespace, so that we end up at the beginning of - * the next word */ - while (cursor < term->search.len) { - if (!isc32space(term->search.buf[cursor++])) - break; - } + int sel_start_row = start_row - grid->view + grid->num_rows; + sel_start_row &= grid->num_rows - 1; - xassert(cursor == term->search.len || !isc32space(term->search.buf[cursor - 1])); + int sel_end_row = end_row - grid->view + grid->num_rows; + sel_end_row &= grid->num_rows - 1; - if (cursor < term->search.len && !isc32space(term->search.buf[cursor])) - cursor--; + selection_cancel(term); + selection_start(term, 0, sel_start_row, SELECTION_CHAR_WISE, false); + selection_update(term, term->cols - 1, sel_end_row); + selection_finalize(seat, term, serial); + search_cancel_keep_selection(term); + return true; + } - return cursor - term->search.cursor; + case BIND_ACTION_SEARCH_COUNT: + BUG("Invalid action type"); + return true; + } + + BUG("Unhandled action type"); + return false; } -static size_t -distance_prev_word(const struct terminal *term) -{ - int cursor = term->search.cursor; - - /* First, eat whitespace prefix */ - while (cursor > 0) { - if (!isc32space(term->search.buf[--cursor])) - break; - } - - xassert(cursor == 0 || !isc32space(term->search.buf[cursor])); - - /* Now eat non-whitespace. This is the word we're skipping past */ - while (cursor > 0) { - if (isc32space(term->search.buf[--cursor])) - break; - } - - xassert(cursor == 0 || isc32space(term->search.buf[cursor])); - if (cursor > 0 && isc32space(term->search.buf[cursor])) - cursor++; - - return term->search.cursor - cursor; -} - -static void -from_clipboard_cb(char *text, size_t size, void *user) -{ - struct terminal *term = user; - search_add_chars(term, text, size); -} - -static void -from_clipboard_done(void *user) -{ - struct terminal *term = user; - - LOG_DBG("search: buffer: %ls", (const wchar_t *)term->search.buf); - search_find_next(term, SEARCH_BACKWARD_SAME_POSITION); - render_refresh_search(term); -} - -static bool -execute_binding(struct seat *seat, struct terminal *term, - const struct key_binding *binding, uint32_t serial, - bool *update_search_result, enum search_direction *direction, - bool *redraw) -{ - *update_search_result = *redraw = false; - const enum bind_action_search action = binding->action; - - struct grid *grid = term->grid; - - switch (action) { - case BIND_ACTION_SEARCH_NONE: - return false; - - case BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE: - if (term->grid == &term->normal) { - cmd_scrollback_up(term, term->rows); - return true; - } - return false; - - case BIND_ACTION_SEARCH_SCROLLBACK_UP_HALF_PAGE: - if (term->grid == &term->normal) { - cmd_scrollback_up(term, max(term->rows / 2, 1)); - return true; - } - break; - - case BIND_ACTION_SEARCH_SCROLLBACK_UP_LINE: - if (term->grid == &term->normal) { - cmd_scrollback_up(term, 1); - return true; - } - break; - - case BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE: - if (term->grid == &term->normal) { - cmd_scrollback_down(term, term->rows); - return true; - } - return false; - - case BIND_ACTION_SEARCH_SCROLLBACK_DOWN_HALF_PAGE: - if (term->grid == &term->normal) { - cmd_scrollback_down(term, max(term->rows / 2, 1)); - return true; - } - break; - - case BIND_ACTION_SEARCH_SCROLLBACK_DOWN_LINE: - if (term->grid == &term->normal) { - cmd_scrollback_down(term, 1); - return true; - } - break; - - case BIND_ACTION_SEARCH_SCROLLBACK_HOME: - if (term->grid == &term->normal) { - cmd_scrollback_up(term, term->grid->num_rows); - return true; - } - break; - - case BIND_ACTION_SEARCH_SCROLLBACK_END: - if (term->grid == &term->normal) { - cmd_scrollback_down(term, term->grid->num_rows); - return true; - } - break; - - case BIND_ACTION_SEARCH_CANCEL: - if (term->search.view_followed_offset) - grid->view = grid->offset; - else { - grid->view = ensure_view_is_allocated( - term, term->search.original_view); - } - term_damage_view(term); - search_cancel(term); - return true; - - case BIND_ACTION_SEARCH_COMMIT: - selection_finalize(seat, term, serial); - search_cancel_keep_selection(term); - return true; - - case BIND_ACTION_SEARCH_FIND_PREV: - if (term->search.last.buf != NULL && term->search.len == 0) { - add_wchars(term, term->search.last.buf, term->search.last.len); - - free(term->search.last.buf); - term->search.last.buf = NULL; - term->search.last.len = 0; - } - - *direction = SEARCH_BACKWARD; - *update_search_result = *redraw = true; - return true; - - case BIND_ACTION_SEARCH_FIND_NEXT: - if (term->search.last.buf != NULL && term->search.len == 0) { - add_wchars(term, term->search.last.buf, term->search.last.len); - - free(term->search.last.buf); - term->search.last.buf = NULL; - term->search.last.len = 0; - } - - *direction = SEARCH_FORWARD; - *update_search_result = *redraw = true; - return true; - - case BIND_ACTION_SEARCH_EDIT_LEFT: - if (term->search.cursor > 0) { - term->search.cursor--; - *redraw = true; - } - return true; - - case BIND_ACTION_SEARCH_EDIT_LEFT_WORD: { - size_t diff = distance_prev_word(term); - term->search.cursor -= diff; - xassert(term->search.cursor <= term->search.len); - - if (diff > 0) - *redraw = true; - return true; - } - - case BIND_ACTION_SEARCH_EDIT_RIGHT: - if (term->search.cursor < term->search.len) { - term->search.cursor++; - *redraw = true; - } - return true; - - case BIND_ACTION_SEARCH_EDIT_RIGHT_WORD: { - size_t diff = distance_next_word(term); - term->search.cursor += diff; - xassert(term->search.cursor <= term->search.len); - - if (diff > 0) - *redraw = true; - return true; - } - - case BIND_ACTION_SEARCH_EDIT_HOME: - if (term->search.cursor != 0) { - term->search.cursor = 0; - *redraw = true; - } - return true; - - case BIND_ACTION_SEARCH_EDIT_END: - if (term->search.cursor != term->search.len) { - term->search.cursor = term->search.len; - *redraw = true; - } - return true; - - case BIND_ACTION_SEARCH_DELETE_PREV: - if (term->search.cursor > 0) { - memmove( - &term->search.buf[term->search.cursor - 1], - &term->search.buf[term->search.cursor], - (term->search.len - term->search.cursor) * sizeof(char32_t)); - term->search.cursor--; - term->search.buf[--term->search.len] = U'\0'; - *update_search_result = *redraw = true; - } - return true; - - case BIND_ACTION_SEARCH_DELETE_PREV_WORD: { - size_t diff = distance_prev_word(term); - size_t old_cursor = term->search.cursor; - size_t new_cursor = old_cursor - diff; - - if (diff > 0) { - memmove(&term->search.buf[new_cursor], - &term->search.buf[old_cursor], - (term->search.len - old_cursor) * sizeof(char32_t)); - - term->search.len -= diff; - term->search.cursor = new_cursor; - *update_search_result = *redraw = true; - } - return true; - } - - case BIND_ACTION_SEARCH_DELETE_NEXT: - if (term->search.cursor < term->search.len) { - memmove( - &term->search.buf[term->search.cursor], - &term->search.buf[term->search.cursor + 1], - (term->search.len - term->search.cursor - 1) * sizeof(char32_t)); - term->search.buf[--term->search.len] = U'\0'; - *update_search_result = *redraw = true; - } - return true; - - case BIND_ACTION_SEARCH_DELETE_NEXT_WORD: { - size_t diff = distance_next_word(term); - size_t cursor = term->search.cursor; - - if (diff > 0) { - memmove(&term->search.buf[cursor], - &term->search.buf[cursor + diff], - (term->search.len - (cursor + diff)) * sizeof(char32_t)); - - term->search.len -= diff; - *update_search_result = *redraw = true; - } - return true; - } - - case BIND_ACTION_SEARCH_DELETE_TO_START: { - if (term->search.cursor > 0) { - memmove(&term->search.buf[0], - &term->search.buf[term->search.cursor], - (term->search.len - term->search.cursor) - * sizeof(char32_t)); - - term->search.len -= term->search.cursor; - term->search.cursor = 0; - *update_search_result = *redraw = true; - } - return true; - } - - case BIND_ACTION_SEARCH_DELETE_TO_END: { - if (term->search.cursor < term->search.len) { - term->search.buf[term->search.cursor] = '\0'; - term->search.len = term->search.cursor; - *update_search_result = *redraw = true; - } - return true; - } - - case BIND_ACTION_SEARCH_EXTEND_CHAR: { - struct coord target; - if (search_extend_find_char_right(term, &target)) { - search_extend_right(term, &target); - *update_search_result = false; - *redraw = true; - } - return true; - } - - case BIND_ACTION_SEARCH_EXTEND_WORD: { - struct coord target; - if (search_extend_find_word_right(term, false, &target)) { - search_extend_right(term, &target); - *update_search_result = false; - *redraw = true; - } - return true; - } - - case BIND_ACTION_SEARCH_EXTEND_WORD_WS: { - struct coord target; - if (search_extend_find_word_right(term, true, &target)) { - search_extend_right(term, &target); - *update_search_result = false; - *redraw = true; - } - return true; - } - - case BIND_ACTION_SEARCH_EXTEND_LINE_DOWN: { - struct coord target; - if (search_extend_find_line_down(term, &target)) { - search_extend_right(term, &target); - *update_search_result = false; - *redraw = true; - } - return true; - } - - case BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR: { - struct coord target; - if (search_extend_find_char_left(term, &target)) { - search_extend_left(term, &target); - *update_search_result = false; - *redraw = true; - } - return true; - } - - case BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD: { - struct coord target; - if (search_extend_find_word_left(term, false, &target)) { - search_extend_left(term, &target); - *update_search_result = false; - *redraw = true; - } - return true; - } - - case BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD_WS: { - struct coord target; - if (search_extend_find_word_left(term, true, &target)) { - search_extend_left(term, &target); - *update_search_result = false; - *redraw = true; - } - return true; - } - - case BIND_ACTION_SEARCH_EXTEND_LINE_UP: { - struct coord target; - if (search_extend_find_line_up(term, &target)) { - search_extend_left(term, &target); - *update_search_result = false; - *redraw = true; - } - return true; - } - - case BIND_ACTION_SEARCH_CLIPBOARD_PASTE: - text_from_clipboard( - seat, term, false, &from_clipboard_cb, &from_clipboard_done, term); - *update_search_result = *redraw = true; - return true; - - case BIND_ACTION_SEARCH_PRIMARY_PASTE: - text_from_primary( - seat, term, false, &from_clipboard_cb, &from_clipboard_done, term); - *update_search_result = *redraw = true; - return true; - - case BIND_ACTION_SEARCH_UNICODE_INPUT: - unicode_mode_activate(term); - return true; - - case BIND_ACTION_SEARCH_TOGGLE_CASE: - term->search.case_mode = (term->search.case_mode + 1) % 3; - *update_search_result = *redraw = true; - return true; - - case BIND_ACTION_SEARCH_TOGGLE_WHOLE_WORD: - term->search.whole_word = !term->search.whole_word; - *update_search_result = *redraw = true; - return true; - - case BIND_ACTION_SEARCH_TOGGLE_REGEX: - term->search.regex = !term->search.regex; - *update_search_result = *redraw = true; - return true; - - case BIND_ACTION_SEARCH_HISTORY_PREV: { - struct search_history_entry *target; - if (term->search.history_pos == NULL) - target = term->search.history_tail; - else - target = term->search.history_pos->prev; - - if (target != NULL) { - term->search.history_pos = target; - term->search.len = 0; - term->search.cursor = 0; - if (term->search.buf != NULL) - term->search.buf[0] = U'\0'; - add_wchars(term, target->buf, target->len); - *update_search_result = *redraw = true; - } - return true; - } - - case BIND_ACTION_SEARCH_HISTORY_NEXT: { - if (term->search.history_pos == NULL) - return true; - - struct search_history_entry *target = term->search.history_pos->next; - term->search.history_pos = target; - - term->search.len = 0; - term->search.cursor = 0; - if (term->search.buf != NULL) - term->search.buf[0] = U'\0'; - - if (target != NULL) - add_wchars(term, target->buf, target->len); - - *update_search_result = *redraw = true; - return true; - } - - case BIND_ACTION_SEARCH_COMMIT_LINE: { - if (term->search.match_len == 0) { - /* No match — fall back to plain commit */ - selection_finalize(seat, term, serial); - search_cancel_keep_selection(term); - return true; - } - - /* Extend selection to span entire line(s) of the match */ - const struct coord match_end = selection_get_end(term); - const int start_row = term->search.match.row; - const int end_row = match_end.row; - - int sel_start_row = start_row - grid->view + grid->num_rows; - sel_start_row &= grid->num_rows - 1; - - int sel_end_row = end_row - grid->view + grid->num_rows; - sel_end_row &= grid->num_rows - 1; - - selection_cancel(term); - selection_start(term, 0, sel_start_row, SELECTION_CHAR_WISE, false); - selection_update(term, term->cols - 1, sel_end_row); - selection_finalize(seat, term, serial); - search_cancel_keep_selection(term); - return true; - } - - case BIND_ACTION_SEARCH_COUNT: - BUG("Invalid action type"); - return true; - } - - BUG("Unhandled action type"); - return false; -} - -void -search_input(struct seat *seat, struct terminal *term, - const struct key_binding_set *bindings, uint32_t key, - xkb_keysym_t sym, xkb_mod_mask_t mods, xkb_mod_mask_t consumed, - const xkb_keysym_t *raw_syms, size_t raw_count, - uint32_t serial) -{ - LOG_DBG("search: input: sym=%d/0x%x, mods=0x%08x, consumed=0x%08x", - sym, sym, mods, consumed); - - enum xkb_compose_status compose_status = seat->kbd.xkb_compose_state != NULL - ? xkb_compose_state_get_status(seat->kbd.xkb_compose_state) - : XKB_COMPOSE_NOTHING; - - enum search_direction search_direction = SEARCH_BACKWARD_SAME_POSITION; - bool update_search_result = false; - bool redraw = false; - - /* - * Key bindings - */ - - /* Match untranslated symbols */ - tll_foreach(bindings->search, it) { - const struct key_binding *bind = &it->item; - - if (bind->mods != mods || bind->mods == 0) - continue; - - for (size_t i = 0; i < raw_count; i++) { - if (bind->k.sym == raw_syms[i]) { - if (execute_binding(seat, term, bind, serial, - &update_search_result, &search_direction, - &redraw)) - { - goto update_search; - } - return; - } - } - } - - /* Match translated symbol */ - tll_foreach(bindings->search, it) { - const struct key_binding *bind = &it->item; - - if (bind->k.sym == sym && - bind->mods == (mods & ~consumed)) { - - if (execute_binding(seat, term, bind, serial, - &update_search_result, &search_direction, - &redraw)) - { - goto update_search; - } - return; - } - } - - /* Match raw key code */ - tll_foreach(bindings->search, it) { - const struct key_binding *bind = &it->item; - - if (bind->mods != mods || bind->mods == 0) - continue; - - tll_foreach(bind->k.key_codes, code) { - if (code->item == key) { - if (execute_binding(seat, term, bind, serial, - &update_search_result, &search_direction, - &redraw)) - { - goto update_search; - } - return; - } - } - } - - uint8_t buf[64] = {0}; - int count = 0; - - if (compose_status == XKB_COMPOSE_COMPOSED) { - count = xkb_compose_state_get_utf8( - seat->kbd.xkb_compose_state, (char *)buf, sizeof(buf)); - xkb_compose_state_reset(seat->kbd.xkb_compose_state); - } else if (compose_status == XKB_COMPOSE_CANCELLED || - compose_status == XKB_COMPOSE_COMPOSING) { - count = 0; +void search_input(struct seat *seat, struct terminal *term, + const struct key_binding_set *bindings, uint32_t key, + xkb_keysym_t sym, xkb_mod_mask_t mods, + xkb_mod_mask_t consumed, const xkb_keysym_t *raw_syms, + size_t raw_count, uint32_t serial) { + LOG_DBG("search: input: sym=%d/0x%x, mods=0x%08x, consumed=0x%08x", sym, sym, + mods, consumed); + + /* Overwrite-confirm: only y/Y proceeds; anything else cancels. */ + if (term->search.mode == SEARCH_MODE_SESSION_OVERWRITE_CONFIRM) { + if (sym == XKB_KEY_y || sym == XKB_KEY_Y || sym == XKB_KEY_Return || + sym == XKB_KEY_KP_Enter) { + session_prompt_confirm_overwrite(term); } else { - count = xkb_state_key_get_utf8( - seat->kbd.xkb_state, key, (char *)buf, sizeof(buf)); + session_prompt_cancel(term); } + return; + } - update_search_result = redraw = count > 0; - search_direction = SEARCH_BACKWARD_SAME_POSITION; + /* In session-load mode, hijack a few keys for picker navigation. We do this + * before binding dispatch so configured search-bindings (e.g. history-prev + * on Up) don't fire. */ + if (term->search.mode == SEARCH_MODE_SESSION_LOAD) { + bool handled = true; + switch (sym) { + case XKB_KEY_Up: + session_picker_move(term, -1); + break; + case XKB_KEY_Down: + session_picker_move(term, +1); + break; + case XKB_KEY_Page_Up: + session_picker_move(term, -5); + break; + case XKB_KEY_Page_Down: + session_picker_move(term, +5); + break; + case XKB_KEY_Delete: + session_picker_delete_selected(term); + break; + default: + handled = false; + break; + } + if (handled) { + render_refresh_search(term); + return; + } + } - if (count == 0) + enum xkb_compose_status compose_status = + seat->kbd.xkb_compose_state != NULL + ? xkb_compose_state_get_status(seat->kbd.xkb_compose_state) + : XKB_COMPOSE_NOTHING; + + enum search_direction search_direction = SEARCH_BACKWARD_SAME_POSITION; + bool update_search_result = false; + bool redraw = false; + + /* + * Key bindings + */ + + /* Match untranslated symbols */ + tll_foreach(bindings->search, it) { + const struct key_binding *bind = &it->item; + + if (bind->mods != mods || bind->mods == 0) + continue; + + for (size_t i = 0; i < raw_count; i++) { + if (bind->k.sym == raw_syms[i]) { + if (execute_binding(seat, term, bind, serial, &update_search_result, + &search_direction, &redraw)) { + goto update_search; + } return; + } + } + } - search_add_chars(term, (const char *)buf, count); + /* Match translated symbol */ + tll_foreach(bindings->search, it) { + const struct key_binding *bind = &it->item; + + if (bind->k.sym == sym && bind->mods == (mods & ~consumed)) { + + if (execute_binding(seat, term, bind, serial, &update_search_result, + &search_direction, &redraw)) { + goto update_search; + } + return; + } + } + + /* Match raw key code */ + tll_foreach(bindings->search, it) { + const struct key_binding *bind = &it->item; + + if (bind->mods != mods || bind->mods == 0) + continue; + + tll_foreach(bind->k.key_codes, code) { + if (code->item == key) { + if (execute_binding(seat, term, bind, serial, &update_search_result, + &search_direction, &redraw)) { + goto update_search; + } + return; + } + } + } + + uint8_t buf[64] = {0}; + int count = 0; + + if (compose_status == XKB_COMPOSE_COMPOSED) { + count = xkb_compose_state_get_utf8(seat->kbd.xkb_compose_state, (char *)buf, + sizeof(buf)); + xkb_compose_state_reset(seat->kbd.xkb_compose_state); + } else if (compose_status == XKB_COMPOSE_CANCELLED || + compose_status == XKB_COMPOSE_COMPOSING) { + count = 0; + } else { + count = xkb_state_key_get_utf8(seat->kbd.xkb_state, key, (char *)buf, + sizeof(buf)); + } + + update_search_result = redraw = count > 0; + search_direction = SEARCH_BACKWARD_SAME_POSITION; + + if (count == 0) + return; + + search_add_chars(term, (const char *)buf, count); update_search: - LOG_DBG("search: buffer: %ls", (const wchar_t *)term->search.buf); - if (update_search_result) - search_find_next(term, search_direction); - if (redraw) - render_refresh_search(term); + LOG_DBG("search: buffer: %ls", (const wchar_t *)term->search.buf); + if (update_search_result && term->search.mode == SEARCH_MODE_NORMAL) + search_find_next(term, search_direction); + if (term->search.mode == SEARCH_MODE_SESSION_LOAD) + session_picker_refilter(term); + if (redraw) + render_refresh_search(term); } diff --git a/search.h b/search.h index ee8ecd7..c8fba13 100644 --- a/search.h +++ b/search.h @@ -6,6 +6,7 @@ #include "terminal.h" void search_begin(struct terminal *term); +void search_begin_session(struct terminal *term, enum search_mode mode); void search_cancel(struct terminal *term); void search_input( struct seat *seat, struct terminal *term, diff --git a/session-crypto.c b/session-crypto.c new file mode 100644 index 0000000..f2af313 --- /dev/null +++ b/session-crypto.c @@ -0,0 +1,160 @@ +#include "session-crypto.h" + +#include +#include + +#include + +#define LOG_MODULE "session-crypto" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "xmalloc.h" + +#define MAGIC "FOOT-ENC1\0" +#define MAGIC_LEN 10 + +static bool sodium_ready = false; + +bool +session_crypto_init(void) +{ + if (sodium_ready) + return true; + if (sodium_init() < 0) { + LOG_ERR("libsodium init failed"); + return false; + } + sodium_ready = true; + return true; +} + +static bool +derive_key(const char *password, const unsigned char *salt, + unsigned char key[crypto_aead_xchacha20poly1305_ietf_KEYBYTES]) +{ + return crypto_pwhash( + key, crypto_aead_xchacha20poly1305_ietf_KEYBYTES, + password, strlen(password), salt, + crypto_pwhash_OPSLIMIT_INTERACTIVE, + crypto_pwhash_MEMLIMIT_INTERACTIVE, + crypto_pwhash_ALG_ARGON2ID13) == 0; +} + +bool +session_crypto_encrypt(const char *password, + const unsigned char *plaintext, size_t plaintext_len, + unsigned char **out, size_t *out_len) +{ + if (!session_crypto_init()) + return false; + *out = NULL; + *out_len = 0; + + const size_t salt_len = crypto_pwhash_SALTBYTES; + const size_t nonce_len = crypto_aead_xchacha20poly1305_ietf_NPUBBYTES; + const size_t tag_len = crypto_aead_xchacha20poly1305_ietf_ABYTES; + + const size_t total = MAGIC_LEN + salt_len + nonce_len + plaintext_len + tag_len; + unsigned char *buf = xmalloc(total); + memcpy(buf, MAGIC, MAGIC_LEN); + + unsigned char *salt = buf + MAGIC_LEN; + unsigned char *nonce = salt + salt_len; + unsigned char *cipher = nonce + nonce_len; + + randombytes_buf(salt, salt_len); + randombytes_buf(nonce, nonce_len); + + unsigned char key[crypto_aead_xchacha20poly1305_ietf_KEYBYTES]; + if (!derive_key(password, salt, key)) { + LOG_ERR("crypto_pwhash failed (out of memory?)"); + sodium_memzero(key, sizeof(key)); + free(buf); + return false; + } + + /* Bind the file header into the AEAD so any tampering breaks decryption. */ + unsigned long long clen = 0; + int rc = crypto_aead_xchacha20poly1305_ietf_encrypt( + cipher, &clen, + plaintext, plaintext_len, + buf, MAGIC_LEN + salt_len + nonce_len, /* AAD = full header */ + NULL, + nonce, + key); + sodium_memzero(key, sizeof(key)); + + if (rc != 0) { + LOG_ERR("xchacha20poly1305 encrypt failed"); + free(buf); + return false; + } + /* Sanity */ + if (clen != plaintext_len + tag_len) { + free(buf); + return false; + } + + *out = buf; + *out_len = total; + return true; +} + +bool +session_crypto_decrypt(const char *password, + const unsigned char *blob, size_t blob_len, + unsigned char **out, size_t *out_len) +{ + if (!session_crypto_init()) + return false; + *out = NULL; + *out_len = 0; + + const size_t salt_len = crypto_pwhash_SALTBYTES; + const size_t nonce_len = crypto_aead_xchacha20poly1305_ietf_NPUBBYTES; + const size_t tag_len = crypto_aead_xchacha20poly1305_ietf_ABYTES; + const size_t header_len = MAGIC_LEN + salt_len + nonce_len; + + if (blob_len < header_len + tag_len) { + LOG_ERR("encrypted blob too small (%zu bytes)", blob_len); + return false; + } + if (memcmp(blob, MAGIC, MAGIC_LEN) != 0) { + LOG_ERR("bad magic in encrypted scrollback"); + return false; + } + + const unsigned char *salt = blob + MAGIC_LEN; + const unsigned char *nonce = salt + salt_len; + const unsigned char *cipher = nonce + nonce_len; + const size_t cipher_len = blob_len - header_len; + + unsigned char key[crypto_aead_xchacha20poly1305_ietf_KEYBYTES]; + if (!derive_key(password, salt, key)) { + LOG_ERR("crypto_pwhash failed during decrypt"); + sodium_memzero(key, sizeof(key)); + return false; + } + + unsigned char *plain = xmalloc(cipher_len); /* upper bound */ + unsigned long long plen = 0; + int rc = crypto_aead_xchacha20poly1305_ietf_decrypt( + plain, &plen, + NULL, + cipher, cipher_len, + blob, header_len, /* AAD must match what was used in encrypt */ + nonce, + key); + sodium_memzero(key, sizeof(key)); + + if (rc != 0) { + LOG_ERR("decryption failed (wrong password or corrupted file)"); + sodium_memzero(plain, cipher_len); + free(plain); + return false; + } + + *out = plain; + *out_len = (size_t)plen; + return true; +} diff --git a/session-crypto.h b/session-crypto.h new file mode 100644 index 0000000..16aae8f --- /dev/null +++ b/session-crypto.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +/* + * Encrypt and decrypt session scrollback with a password. + * + * File format (binary): + * [magic : 10 bytes "FOOT-ENC1\0"] + * [salt : crypto_pwhash_SALTBYTES (16)] + * [nonce : crypto_aead_xchacha20poly1305_ietf_NPUBBYTES (24)] + * [ciphertext : plaintext_len + crypto_aead_xchacha20poly1305_ietf_ABYTES (16)] + * + * KDF: argon2id at INTERACTIVE ops/mem limits (≈100ms on modern hardware). + * AEAD: XChaCha20-Poly1305 (authenticates the magic+salt+nonce as AAD). + */ + +bool session_crypto_init(void); + +/* + * Encrypts plaintext with the given password. Allocates and returns a buffer + * in *out (caller frees); *out_len receives its size. Returns true on success. + */ +bool session_crypto_encrypt( + const char *password, + const unsigned char *plaintext, size_t plaintext_len, + unsigned char **out, size_t *out_len); + +/* + * Decrypts an encrypted blob (as produced by session_crypto_encrypt) with + * the given password. Allocates *out (caller frees) on success. Returns + * false on bad magic, truncated file, or authentication failure (wrong + * password / corrupted file). + */ +bool session_crypto_decrypt( + const char *password, + const unsigned char *blob, size_t blob_len, + unsigned char **out, size_t *out_len); diff --git a/session-prompt.c b/session-prompt.c new file mode 100644 index 0000000..640bd3b --- /dev/null +++ b/session-prompt.c @@ -0,0 +1,460 @@ +#include "session.h" + +#include +#include + +#include + +#define LOG_MODULE "session-prompt" +#define LOG_ENABLE_DBG 0 +#include "char32.h" +#include "extract.h" +#include "grid.h" +#include "log.h" +#include "render.h" +#include "search.h" +#include "session-crypto.h" +#include "terminal.h" +#include "vt.h" +#include "xmalloc.h" + +#define SCROLLBACK_MAX_BYTES (1u << 20) /* 1 MB */ + +static char * +buf_to_utf8(const struct terminal *term) +{ + if (term->search.len == 0) + return NULL; + /* Make a NUL-terminated char32_t copy for ac32tombs */ + char32_t *tmp = calloc(term->search.len + 1, sizeof(char32_t)); + if (tmp == NULL) + return NULL; + memcpy(tmp, term->search.buf, term->search.len * sizeof(char32_t)); + tmp[term->search.len] = U'\0'; + char *utf8 = ac32tombs(tmp); + free(tmp); + return utf8; +} + +void +session_picker_init(struct terminal *term) +{ + session_picker_free(term); + term->session_picker.all = session_list(&term->session_picker.all_count); + term->session_picker.filtered_cap = term->session_picker.all_count; + term->session_picker.filtered = term->session_picker.filtered_cap > 0 + ? xmalloc(sizeof(size_t) * term->session_picker.filtered_cap) + : NULL; + session_picker_refilter(term); +} + +void +session_picker_free(struct terminal *term) +{ + session_free_names(term->session_picker.all, + term->session_picker.all_count); + free(term->session_picker.filtered); + term->session_picker.all = NULL; + term->session_picker.all_count = 0; + term->session_picker.filtered = NULL; + term->session_picker.filtered_count = 0; + term->session_picker.filtered_cap = 0; + term->session_picker.sel = 0; +} + +void +session_picker_refilter(struct terminal *term) +{ + char *filter = buf_to_utf8(term); + term->session_picker.filtered_count = 0; + for (size_t i = 0; i < term->session_picker.all_count; i++) { + const char *name = term->session_picker.all[i]; + if (filter == NULL || strstr(name, filter) != NULL) + term->session_picker.filtered[term->session_picker.filtered_count++] = i; + } + free(filter); + if (term->session_picker.sel >= term->session_picker.filtered_count) + term->session_picker.sel = term->session_picker.filtered_count > 0 + ? term->session_picker.filtered_count - 1 + : 0; +} + +void +session_picker_move(struct terminal *term, int delta) +{ + if (term->session_picker.filtered_count == 0) + return; + long n = (long)term->session_picker.filtered_count; + long cur = (long)term->session_picker.sel + delta; + /* Wrap */ + cur = ((cur % n) + n) % n; + term->session_picker.sel = (size_t)cur; +} + +bool +session_picker_delete_selected(struct terminal *term) +{ + if (term->session_picker.filtered_count == 0) + return false; + size_t idx = term->session_picker.filtered[term->session_picker.sel]; + const char *name = term->session_picker.all[idx]; + if (!session_delete(name)) + return false; + /* Rebuild the list */ + session_picker_init(term); + return true; +} + +static void +dismiss(struct terminal *term) +{ + session_picker_free(term); + if (term->session_pending_name != NULL) + sodium_memzero(term->session_pending_name, + strlen(term->session_pending_name)); + free(term->session_pending_name); + term->session_pending_name = NULL; + term->session_pending_secure = false; + if (term->session_pending_load_state != NULL) { + struct session_state *st = term->session_pending_load_state; + session_state_free(st); + free(st); + term->session_pending_load_state = NULL; + } + /* Zero any residual password bytes in the input buffer */ + if (term->search.buf != NULL && term->search.len > 0) + sodium_memzero(term->search.buf, + term->search.len * sizeof(term->search.buf[0])); + term->search.mode = SEARCH_MODE_NORMAL; + /* search_cancel clears the search bar; reuse it. We deliberately do not + * leave selection state behind (no commit semantics for sessions). */ + search_cancel(term); +} + +static char * +extract_scrollback_text(struct terminal *term, size_t *out_len) +{ + *out_len = 0; + struct grid *grid = &term->normal; + if (grid->rows == NULL) + return NULL; + + const int total = grid->num_rows; + const int visible = term->rows; + const int scrollback = total > visible ? total - visible : 0; + + struct extraction_context *ctx = extract_begin(SELECTION_NONE, true); + if (ctx == NULL) + return NULL; + for (int row_no = -scrollback; row_no < visible; row_no++) { + int idx = ((grid->offset + row_no) % total + total) % total; + const struct row *row = grid->rows[idx]; + if (row == NULL) + continue; + for (int col = 0; col < grid->num_cols; col++) { + if (!extract_one(term, row, &row->cells[col], col, ctx)) { + char *junk = NULL; + size_t jl = 0; + extract_finish(ctx, &junk, &jl); + free(junk); + return NULL; + } + } + } + char *text = NULL; + size_t len = 0; + if (!extract_finish(ctx, &text, &len)) + return NULL; + + /* Trim leading newlines (uninitialized scrollback) */ + size_t start = 0; + while (start < len && (text[start] == '\n' || text[start] == ' ')) + start++; + if (start > 0) { + memmove(text, text + start, len - start + 1); + len -= start; + } + + /* Cap at SCROLLBACK_MAX_BYTES, keeping the newest content. */ + if (len > SCROLLBACK_MAX_BYTES) { + size_t drop = len - SCROLLBACK_MAX_BYTES; + const char *nl = memchr(text + drop, '\n', len - drop); + if (nl != NULL) + drop = (size_t)(nl + 1 - text); + memmove(text, text + drop, len - drop + 1); + len -= drop; + } + + *out_len = len; + return text; +} + + +static void +do_save(struct terminal *term, const char *name) +{ + struct session_state st = {0}; + if (!session_capture(term->ptmx, term->cwd, &st)) { + LOG_ERR("session_save: could not capture state"); + session_state_free(&st); + return; + } + if (!session_save(name, &st)) + LOG_ERR("session_save: write failed for '%s'", name); + session_state_free(&st); + + /* Plain save deliberately drops any stale encrypted scrollback so the + * session reflects the current save. */ + session_delete_enc_scrollback(name); + session_delete_scrollback(name); +} + +static void +do_save_secure(struct terminal *term, const char *name, const char *password) +{ + struct session_state st = {0}; + if (!session_capture(term->ptmx, term->cwd, &st)) { + LOG_ERR("session_save_secure: could not capture state"); + session_state_free(&st); + return; + } + bool ok = session_save(name, &st); + session_state_free(&st); + if (!ok) { + LOG_ERR("session_save_secure: JSON write failed for '%s'", name); + return; + } + /* Capture scrollback as plain text, encrypt, write sidecar. */ + size_t plain_len = 0; + char *plain = extract_scrollback_text(term, &plain_len); + if (plain == NULL || plain_len == 0) { + /* No scrollback to encrypt — clear any stale ciphertext. */ + free(plain); + session_delete_enc_scrollback(name); + session_delete_scrollback(name); + return; + } + unsigned char *blob = NULL; + size_t blob_len = 0; + bool enc_ok = session_crypto_encrypt( + password, (const unsigned char *)plain, plain_len, &blob, &blob_len); + sodium_memzero(plain, plain_len); + free(plain); + if (!enc_ok) { + LOG_ERR("session_save_secure: encryption failed"); + return; + } + if (!session_write_enc_blob(name, blob, blob_len)) + LOG_ERR("session_save_secure: sidecar write failed"); + /* Remove any legacy plain sidecar so we never load it accidentally. */ + session_delete_scrollback(name); + sodium_memzero(blob, blob_len); + free(blob); +} + +/* + * Replay a saved scrollback into a freshly created terminal. Walks the bytes + * once to convert bare LF into CRLF (so each saved line lands at column 0), + * then runs them through vt_from_slave. Called from the LOAD path right after + * the new tab is spawned, before any output from the new slave has arrived. + */ +static void +replay_scrollback(struct terminal *new_term, const char *text, size_t len) +{ + if (text == NULL || len == 0 || new_term == NULL) + return; + + char *buf = xmalloc(len * 2); + size_t out = 0; + for (size_t i = 0; i < len; i++) { + char c = text[i]; + if (c == '\n') + buf[out++] = '\r'; + buf[out++] = c; + } + if (out > 0) + buf[out++] = '\n'; + + /* Visually separate replayed content from the upcoming shell prompt */ + /* (single trailing newline is enough; the shell will emit its own CRLF) */ + + vt_from_slave(new_term, (const uint8_t *)buf, out); + free(buf); +} + +void +session_prompt_confirm_overwrite(struct terminal *term) +{ + if (term->session_pending_name == NULL) { + dismiss(term); + return; + } + if (term->session_pending_secure) { + /* Don't save yet — transition to password prompt. */ + term->search.mode = SEARCH_MODE_SESSION_SAVE_SECURE_PASSWORD; + term->search.len = 0; + term->search.cursor = 0; + if (term->search.buf != NULL) + term->search.buf[0] = U'\0'; + render_refresh_search(term); + return; + } + do_save(term, term->session_pending_name); + dismiss(term); +} + +void +session_prompt_cancel(struct terminal *term) +{ + dismiss(term); +} + +static void +spawn_and_replay(struct terminal *term, struct session_state *st, + const char *name, const char *password) +{ + struct terminal *nt = term_tab_new_with_cwd( + term, st->cwd, st->argc, st->argv, NULL, + term->shutdown.cb, term->shutdown.cb_data); + if (nt == NULL) { + LOG_ERR("session_load: could not spawn new tab"); + return; + } + /* Encrypted sidecar takes priority if present. */ + if (password != NULL && session_has_enc_scrollback(name)) { + unsigned char *blob = NULL; + size_t blob_len = 0; + if (session_read_enc_blob(name, &blob, &blob_len)) { + unsigned char *plain = NULL; + size_t plain_len = 0; + if (session_crypto_decrypt(password, blob, blob_len, + &plain, &plain_len)) { + replay_scrollback(nt, (const char *)plain, plain_len); + sodium_memzero(plain, plain_len); + free(plain); + } else { + LOG_ERR("session_load: decryption failed for '%s'", name); + } + free(blob); + } + } else { + /* Plain sidecar (legacy or never-encrypted). */ + char *sb_text = NULL; + size_t sb_len = 0; + if (session_load_scrollback(name, &sb_text, &sb_len)) + replay_scrollback(nt, sb_text, sb_len); + free(sb_text); + } +} + +void +session_prompt_commit(struct terminal *term) +{ + enum search_mode mode = term->search.mode; + + if (mode == SEARCH_MODE_SESSION_SAVE || + mode == SEARCH_MODE_SESSION_SAVE_SECURE_NAME) { + const bool secure = (mode == SEARCH_MODE_SESSION_SAVE_SECURE_NAME); + char *name = buf_to_utf8(term); + if (name == NULL || !session_name_is_valid(name)) { + LOG_ERR("session name must be non-empty [A-Za-z0-9._-]"); + free(name); + dismiss(term); + return; + } + if (session_exists(name)) { + free(term->session_pending_name); + term->session_pending_name = name; + term->session_pending_secure = secure; + term->search.mode = SEARCH_MODE_SESSION_OVERWRITE_CONFIRM; + render_refresh_search(term); + return; + } + if (secure) { + /* No overwrite needed, go straight to password prompt. */ + free(term->session_pending_name); + term->session_pending_name = name; + term->session_pending_secure = true; + term->search.mode = SEARCH_MODE_SESSION_SAVE_SECURE_PASSWORD; + term->search.len = 0; + term->search.cursor = 0; + if (term->search.buf != NULL) + term->search.buf[0] = U'\0'; + render_refresh_search(term); + return; + } + do_save(term, name); + free(name); + } else if (mode == SEARCH_MODE_SESSION_SAVE_SECURE_PASSWORD) { + char *password = buf_to_utf8(term); + if (password == NULL || password[0] == '\0') { + LOG_ERR("session_save_secure: empty password"); + free(password); + dismiss(term); + return; + } + const char *name = term->session_pending_name; + if (name == NULL) { + sodium_memzero(password, strlen(password)); + free(password); + dismiss(term); + return; + } + do_save_secure(term, name, password); + sodium_memzero(password, strlen(password)); + free(password); + } else if (mode == SEARCH_MODE_SESSION_LOAD) { + if (term->session_picker.filtered_count == 0) { + LOG_ERR("session_load: no matching session"); + dismiss(term); + return; + } + size_t idx = term->session_picker.filtered[term->session_picker.sel]; + const char *picked = term->session_picker.all[idx]; + + struct session_state *st = xmalloc(sizeof(*st)); + memset(st, 0, sizeof(*st)); + if (!session_load(picked, st)) { + LOG_ERR("session_load: could not read '%s'", picked); + free(st); + dismiss(term); + return; + } + if (session_has_enc_scrollback(picked)) { + /* Stash state, transition to password mode. */ + free(term->session_pending_name); + term->session_pending_name = xstrdup(picked); + term->session_pending_load_state = st; + term->search.mode = SEARCH_MODE_SESSION_LOAD_PASSWORD; + term->search.len = 0; + term->search.cursor = 0; + if (term->search.buf != NULL) + term->search.buf[0] = U'\0'; + session_picker_free(term); + render_refresh_search(term); + return; + } + /* No encrypted scrollback — spawn now. */ + spawn_and_replay(term, st, picked, NULL); + session_state_free(st); + free(st); + } else if (mode == SEARCH_MODE_SESSION_LOAD_PASSWORD) { + char *password = buf_to_utf8(term); + struct session_state *st = term->session_pending_load_state; + const char *name = term->session_pending_name; + if (st != NULL && name != NULL && password != NULL) { + spawn_and_replay(term, st, name, password); + } + if (st != NULL) { + session_state_free(st); + free(st); + term->session_pending_load_state = NULL; + } + if (password != NULL) { + sodium_memzero(password, strlen(password)); + free(password); + } + } + + dismiss(term); +} diff --git a/session.c b/session.c new file mode 100644 index 0000000..3bbcac7 --- /dev/null +++ b/session.c @@ -0,0 +1,854 @@ +#include "session.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#define LOG_MODULE "session" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "xmalloc.h" + +#define JSON_SUFFIX ".json" +#define JSON_SUFFIX_LEN 5 +#define SCROLLBACK_SUFFIX ".scrollback.txt" +#define SCROLLBACK_ENC_SUFFIX ".scrollback.enc" +#define SCROLLBACK_MAX_BYTES (1u << 20) /* 1 MB cap */ + +static int +state_dir(char *buf, size_t buf_sz) +{ + const char *xdg = getenv("XDG_DATA_HOME"); + int n; + if (xdg != NULL && xdg[0] == '/') + n = snprintf(buf, buf_sz, "%s/foot/state", xdg); + else { + const char *home = getenv("HOME"); + if (home == NULL) + return -1; + n = snprintf(buf, buf_sz, "%s/.local/share/foot/state", home); + } + if (n < 0 || (size_t)n >= buf_sz) + return -1; + return n; +} + +static bool +mkdir_p(const char *path) +{ + char tmp[4096]; + size_t len = strlen(path); + if (len >= sizeof(tmp)) + return false; + memcpy(tmp, path, len + 1); + + for (size_t i = 1; i < len; i++) { + if (tmp[i] == '/') { + tmp[i] = '\0'; + if (mkdir(tmp, 0700) < 0 && errno != EEXIST) { + LOG_ERRNO("mkdir(%s)", tmp); + return false; + } + tmp[i] = '/'; + } + } + if (mkdir(tmp, 0700) < 0 && errno != EEXIST) { + LOG_ERRNO("mkdir(%s)", tmp); + return false; + } + return true; +} + +static int +session_path(const char *name, char *buf, size_t buf_sz) +{ + char dir[1024]; + if (state_dir(dir, sizeof(dir)) < 0) + return -1; + int n = snprintf(buf, buf_sz, "%s/%s.json", dir, name); + if (n < 0 || (size_t)n >= buf_sz) + return -1; + return n; +} + +bool +session_name_is_valid(const char *name) +{ + if (name == NULL || name[0] == '\0') + return false; + for (const char *p = name; *p != '\0'; p++) { + char c = *p; + bool ok = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-'; + if (!ok) + return false; + } + return true; +} + +void +session_state_free(struct session_state *st) +{ + if (st == NULL) + return; + free(st->cwd); + if (st->argv != NULL) { + for (int i = 0; i < st->argc; i++) + free(st->argv[i]); + free(st->argv); + } + st->cwd = NULL; + st->argv = NULL; + st->argc = 0; +} + +#if defined(__linux__) +static char * +read_link(const char *path) +{ + char *buf = NULL; + size_t cap = 256; + for (;;) { + buf = xrealloc(buf, cap); + ssize_t n = readlink(path, buf, cap - 1); + if (n < 0) { + free(buf); + return NULL; + } + if ((size_t)n < cap - 1) { + buf[n] = '\0'; + return buf; + } + cap *= 2; + if (cap > 1 << 16) { + free(buf); + return NULL; + } + } +} + +static bool +read_cmdline(pid_t pid, int *out_argc, char ***out_argv) +{ + char path[64]; + snprintf(path, sizeof(path), "/proc/%d/cmdline", (int)pid); + + int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) + return false; + + char *buf = NULL; + size_t len = 0; + size_t cap = 0; + char chunk[1024]; + for (;;) { + ssize_t n = read(fd, chunk, sizeof(chunk)); + if (n < 0) { + if (errno == EINTR) + continue; + close(fd); + free(buf); + return false; + } + if (n == 0) + break; + if (len + (size_t)n + 1 > cap) { + cap = (cap == 0 ? 1024 : cap * 2); + while (len + (size_t)n + 1 > cap) + cap *= 2; + buf = xrealloc(buf, cap); + } + memcpy(buf + len, chunk, n); + len += n; + } + close(fd); + + if (buf == NULL || len == 0) { + free(buf); + return false; + } + /* /proc//cmdline is NUL-separated; ensure trailing NUL */ + if (buf[len - 1] != '\0') { + buf = xrealloc(buf, len + 1); + buf[len] = '\0'; + len++; + } + + /* Count args */ + int argc = 0; + for (size_t i = 0; i < len; i++) + if (buf[i] == '\0' && (i == 0 || buf[i - 1] != '\0')) + argc++; + + if (argc <= 0) { + free(buf); + return false; + } + + char **argv = xmalloc(sizeof(char *) * (argc + 1)); + int idx = 0; + size_t start = 0; + for (size_t i = 0; i < len && idx < argc; i++) { + if (buf[i] == '\0') { + argv[idx++] = xstrdup(buf + start); + start = i + 1; + } + } + argv[argc] = NULL; + free(buf); + + *out_argc = argc; + *out_argv = argv; + return true; +} +#endif + +bool +session_capture(int ptmx, const char *fallback_cwd, struct session_state *out) +{ + memset(out, 0, sizeof(*out)); + +#if defined(__linux__) + pid_t fg = tcgetpgrp(ptmx); + if (fg > 0) { + char path[64]; + snprintf(path, sizeof(path), "/proc/%d/cwd", (int)fg); + char *cwd = read_link(path); + if (cwd != NULL) + out->cwd = cwd; + + int argc = 0; + char **argv = NULL; + if (read_cmdline(fg, &argc, &argv)) { + /* If the foreground process is a shell (login or otherwise), don't + * try to resurrect it via argv — let the user's normal shell + * launch take over. We only restore non-shell processes. */ + const char *base = argv[0]; + const char *slash = strrchr(base, '/'); + if (slash != NULL) + base = slash + 1; + const char *shells[] = { + "bash", "zsh", "fish", "sh", "dash", "ksh", "tcsh", "csh", + "-bash", "-zsh", "-fish", "-sh", "-dash" + }; + bool is_shell = false; + for (size_t i = 0; i < sizeof(shells) / sizeof(shells[0]); i++) { + if (strcmp(base, shells[i]) == 0) { + is_shell = true; + break; + } + } + if (is_shell) { + for (int i = 0; i < argc; i++) + free(argv[i]); + free(argv); + } else { + out->argc = argc; + out->argv = argv; + } + } + } +#endif + + if (out->cwd == NULL && fallback_cwd != NULL) + out->cwd = xstrdup(fallback_cwd); + + return out->cwd != NULL; +} + +/* ------------- JSON encoding/decoding (minimal, hand-rolled) ------------- */ + +static void +json_escape(FILE *fp, const char *s) +{ + fputc('"', fp); + for (const char *p = s; *p != '\0'; p++) { + unsigned char c = (unsigned char)*p; + switch (c) { + case '"': fputs("\\\"", fp); break; + case '\\': fputs("\\\\", fp); break; + case '\b': fputs("\\b", fp); break; + case '\f': fputs("\\f", fp); break; + case '\n': fputs("\\n", fp); break; + case '\r': fputs("\\r", fp); break; + case '\t': fputs("\\t", fp); break; + default: + if (c < 0x20) + fprintf(fp, "\\u%04x", c); + else + fputc(c, fp); + } + } + fputc('"', fp); +} + +bool +session_save(const char *name, const struct session_state *st) +{ + if (!session_name_is_valid(name)) { + LOG_ERR("invalid session name: %s", name == NULL ? "(null)" : name); + return false; + } + if (st == NULL || st->cwd == NULL) { + LOG_ERR("session_save: missing cwd"); + return false; + } + + char dir[1024]; + if (state_dir(dir, sizeof(dir)) < 0) { + LOG_ERR("could not resolve state directory"); + return false; + } + if (!mkdir_p(dir)) + return false; + + char path[1280]; + if (session_path(name, path, sizeof(path)) < 0) + return false; + + char tmp_path[1408]; + snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", path); + + FILE *fp = fopen(tmp_path, "we"); + if (fp == NULL) { + LOG_ERRNO("fopen(%s)", tmp_path); + return false; + } + + fputs("{\n \"cwd\": ", fp); + json_escape(fp, st->cwd); + if (st->argc > 0 && st->argv != NULL) { + fputs(",\n \"argv\": [", fp); + for (int i = 0; i < st->argc; i++) { + if (i > 0) + fputs(", ", fp); + json_escape(fp, st->argv[i]); + } + fputc(']', fp); + } + fputs("\n}\n", fp); + + if (fflush(fp) != 0 || fsync(fileno(fp)) != 0) { + LOG_ERRNO("fsync(%s)", tmp_path); + fclose(fp); + unlink(tmp_path); + return false; + } + fclose(fp); + + if (rename(tmp_path, path) < 0) { + LOG_ERRNO("rename(%s -> %s)", tmp_path, path); + unlink(tmp_path); + return false; + } + + LOG_INFO("session saved: %s", path); + return true; +} + +/* ------------- JSON parsing (just enough for our schema) ------------- */ + +struct parser { + const char *p; + const char *end; +}; + +static void +skip_ws(struct parser *pa) +{ + while (pa->p < pa->end) { + char c = *pa->p; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') + pa->p++; + else + break; + } +} + +static bool +parse_string(struct parser *pa, char **out) +{ + skip_ws(pa); + if (pa->p >= pa->end || *pa->p != '"') + return false; + pa->p++; + + size_t cap = 64; + size_t len = 0; + char *buf = xmalloc(cap); + + while (pa->p < pa->end && *pa->p != '"') { + char c = *pa->p++; + char decoded; + if (c == '\\') { + if (pa->p >= pa->end) { free(buf); return false; } + char esc = *pa->p++; + switch (esc) { + case '"': decoded = '"'; break; + case '\\': decoded = '\\'; break; + case '/': decoded = '/'; break; + case 'b': decoded = '\b'; break; + case 'f': decoded = '\f'; break; + case 'n': decoded = '\n'; break; + case 'r': decoded = '\r'; break; + case 't': decoded = '\t'; break; + case 'u': { + if (pa->end - pa->p < 4) { free(buf); return false; } + unsigned val = 0; + for (int i = 0; i < 4; i++) { + char h = pa->p[i]; + val <<= 4; + if (h >= '0' && h <= '9') val |= h - '0'; + else if (h >= 'a' && h <= 'f') val |= h - 'a' + 10; + else if (h >= 'A' && h <= 'F') val |= h - 'A' + 10; + else { free(buf); return false; } + } + pa->p += 4; + if (val < 0x80) { + decoded = (char)val; + } else { + /* UTF-8 encode codepoint (no surrogate-pair handling) */ + char enc[4]; + int n; + if (val < 0x800) { + enc[0] = (char)(0xc0 | (val >> 6)); + enc[1] = (char)(0x80 | (val & 0x3f)); + n = 2; + } else { + enc[0] = (char)(0xe0 | (val >> 12)); + enc[1] = (char)(0x80 | ((val >> 6) & 0x3f)); + enc[2] = (char)(0x80 | (val & 0x3f)); + n = 3; + } + if (len + (size_t)n + 1 > cap) { + while (len + (size_t)n + 1 > cap) cap *= 2; + buf = xrealloc(buf, cap); + } + memcpy(buf + len, enc, n); + len += n; + continue; + } + break; + } + default: free(buf); return false; + } + } else { + decoded = c; + } + if (len + 2 > cap) { + cap *= 2; + buf = xrealloc(buf, cap); + } + buf[len++] = decoded; + } + if (pa->p >= pa->end) { free(buf); return false; } + pa->p++; /* consume closing quote */ + buf[len] = '\0'; + *out = buf; + return true; +} + +static bool +expect_char(struct parser *pa, char c) +{ + skip_ws(pa); + if (pa->p >= pa->end || *pa->p != c) + return false; + pa->p++; + return true; +} + +bool +session_load(const char *name, struct session_state *out) +{ + memset(out, 0, sizeof(*out)); + + if (!session_name_is_valid(name)) { + LOG_ERR("invalid session name: %s", name == NULL ? "(null)" : name); + return false; + } + + char path[1280]; + if (session_path(name, path, sizeof(path)) < 0) + return false; + + int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + LOG_ERRNO("open(%s)", path); + return false; + } + struct stat sb; + if (fstat(fd, &sb) < 0 || sb.st_size <= 0 || sb.st_size > (off_t)(1 << 20)) { + LOG_ERR("session file %s has unexpected size", path); + close(fd); + return false; + } + char *data = xmalloc(sb.st_size + 1); + ssize_t got = 0; + while (got < sb.st_size) { + ssize_t n = read(fd, data + got, sb.st_size - got); + if (n < 0) { + if (errno == EINTR) continue; + free(data); + close(fd); + return false; + } + if (n == 0) break; + got += n; + } + close(fd); + data[got] = '\0'; + + struct parser pa = { .p = data, .end = data + got }; + bool ok = false; + + if (!expect_char(&pa, '{')) goto done; + + for (;;) { + skip_ws(&pa); + if (pa.p < pa.end && *pa.p == '}') { pa.p++; break; } + + char *key = NULL; + if (!parse_string(&pa, &key)) goto done; + if (!expect_char(&pa, ':')) { free(key); goto done; } + + if (strcmp(key, "cwd") == 0) { + free(key); + if (!parse_string(&pa, &out->cwd)) goto done; + } else if (strcmp(key, "argv") == 0) { + free(key); + if (!expect_char(&pa, '[')) goto done; + int cap = 4; + out->argv = xmalloc(sizeof(char *) * cap); + out->argc = 0; + for (;;) { + skip_ws(&pa); + if (pa.p < pa.end && *pa.p == ']') { pa.p++; break; } + if (out->argc + 1 >= cap) { + cap *= 2; + out->argv = xrealloc(out->argv, sizeof(char *) * cap); + } + if (!parse_string(&pa, &out->argv[out->argc])) goto done; + out->argc++; + skip_ws(&pa); + if (pa.p < pa.end && *pa.p == ',') { pa.p++; continue; } + } + out->argv[out->argc] = NULL; + } else { + free(key); + /* unknown key: skip a string value */ + char *junk = NULL; + if (!parse_string(&pa, &junk)) goto done; + free(junk); + } + + skip_ws(&pa); + if (pa.p < pa.end && *pa.p == ',') { pa.p++; continue; } + } + + ok = out->cwd != NULL; + +done: + free(data); + if (!ok) + session_state_free(out); + return ok; +} + +/* ------------- listing ------------- */ + +static int +cmp_names(const void *a, const void *b) +{ + return strcmp(*(const char *const *)a, *(const char *const *)b); +} + +char ** +session_list(size_t *out_count) +{ + *out_count = 0; + + char dir[1024]; + if (state_dir(dir, sizeof(dir)) < 0) + return NULL; + + DIR *d = opendir(dir); + if (d == NULL) + return NULL; + + size_t cap = 8; + size_t count = 0; + char **names = xmalloc(sizeof(char *) * cap); + + struct dirent *de; + while ((de = readdir(d)) != NULL) { + const char *n = de->d_name; + size_t nlen = strlen(n); + if (nlen <= JSON_SUFFIX_LEN) + continue; + if (strcmp(n + nlen - JSON_SUFFIX_LEN, JSON_SUFFIX) != 0) + continue; + char *stripped = xmalloc(nlen - JSON_SUFFIX_LEN + 1); + memcpy(stripped, n, nlen - JSON_SUFFIX_LEN); + stripped[nlen - JSON_SUFFIX_LEN] = '\0'; + if (!session_name_is_valid(stripped)) { + free(stripped); + continue; + } + if (count >= cap) { + cap *= 2; + names = xrealloc(names, sizeof(char *) * cap); + } + names[count++] = stripped; + } + closedir(d); + + qsort(names, count, sizeof(char *), cmp_names); + + *out_count = count; + return names; +} + +bool +session_exists(const char *name) +{ + if (!session_name_is_valid(name)) + return false; + char path[1280]; + if (session_path(name, path, sizeof(path)) < 0) + return false; + return access(path, F_OK) == 0; +} + +bool +session_delete(const char *name) +{ + if (!session_name_is_valid(name)) + return false; + char path[1280]; + if (session_path(name, path, sizeof(path)) < 0) + return false; + if (unlink(path) < 0) { + LOG_ERRNO("unlink(%s)", path); + return false; + } + session_delete_scrollback(name); + session_delete_enc_scrollback(name); + LOG_INFO("session deleted: %s", path); + return true; +} + +static int +scrollback_path(const char *name, char *buf, size_t buf_sz) +{ + char dir[1024]; + if (state_dir(dir, sizeof(dir)) < 0) + return -1; + int n = snprintf(buf, buf_sz, "%s/%s%s", dir, name, SCROLLBACK_SUFFIX); + if (n < 0 || (size_t)n >= buf_sz) + return -1; + return n; +} + +bool +session_load_scrollback(const char *name, char **out_text, size_t *out_len) +{ + *out_text = NULL; + *out_len = 0; + if (!session_name_is_valid(name)) + return false; + + char path[1280]; + if (scrollback_path(name, path, sizeof(path)) < 0) + return false; + + int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) + return false; + + struct stat sb; + if (fstat(fd, &sb) < 0 || sb.st_size < 0 || + (size_t)sb.st_size > SCROLLBACK_MAX_BYTES * 4 /* generous on load */) { + close(fd); + return false; + } + if (sb.st_size == 0) { + close(fd); + return false; + } + + char *data = xmalloc(sb.st_size + 1); + ssize_t got = 0; + while (got < sb.st_size) { + ssize_t n = read(fd, data + got, sb.st_size - got); + if (n < 0) { + if (errno == EINTR) continue; + free(data); + close(fd); + return false; + } + if (n == 0) break; + got += n; + } + close(fd); + data[got] = '\0'; + + *out_text = data; + *out_len = (size_t)got; + return true; +} + +void +session_delete_scrollback(const char *name) +{ + if (!session_name_is_valid(name)) + return; + char path[1280]; + if (scrollback_path(name, path, sizeof(path)) < 0) + return; + unlink(path); +} + +static int +enc_path(const char *name, char *buf, size_t buf_sz) +{ + char dir[1024]; + if (state_dir(dir, sizeof(dir)) < 0) + return -1; + int n = snprintf(buf, buf_sz, "%s/%s%s", dir, name, SCROLLBACK_ENC_SUFFIX); + if (n < 0 || (size_t)n >= buf_sz) + return -1; + return n; +} + +bool +session_has_enc_scrollback(const char *name) +{ + if (!session_name_is_valid(name)) + return false; + char path[1280]; + if (enc_path(name, path, sizeof(path)) < 0) + return false; + return access(path, F_OK) == 0; +} + +void +session_delete_enc_scrollback(const char *name) +{ + if (!session_name_is_valid(name)) + return; + char path[1280]; + if (enc_path(name, path, sizeof(path)) < 0) + return; + unlink(path); +} + +bool +session_write_enc_blob(const char *name, const unsigned char *blob, size_t len) +{ + if (!session_name_is_valid(name)) + return false; + char dir[1024]; + if (state_dir(dir, sizeof(dir)) < 0) + return false; + if (!mkdir_p(dir)) + return false; + char path[1280]; + if (enc_path(name, path, sizeof(path)) < 0) + return false; + char tmp_path[1408]; + snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", path); + + /* 0600 — encrypted but still reduces accidental exposure. */ + int fd = open(tmp_path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600); + if (fd < 0) { + LOG_ERRNO("open(%s)", tmp_path); + return false; + } + size_t written = 0; + while (written < len) { + ssize_t n = write(fd, blob + written, len - written); + if (n < 0) { + if (errno == EINTR) continue; + LOG_ERRNO("write(%s)", tmp_path); + close(fd); + unlink(tmp_path); + return false; + } + written += n; + } + if (fsync(fd) < 0) { + LOG_ERRNO("fsync(%s)", tmp_path); + close(fd); + unlink(tmp_path); + return false; + } + close(fd); + if (rename(tmp_path, path) < 0) { + LOG_ERRNO("rename(%s -> %s)", tmp_path, path); + unlink(tmp_path); + return false; + } + return true; +} + +bool +session_read_enc_blob(const char *name, unsigned char **out, size_t *out_len) +{ + *out = NULL; + *out_len = 0; + if (!session_name_is_valid(name)) + return false; + char path[1280]; + if (enc_path(name, path, sizeof(path)) < 0) + return false; + int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) + return false; + struct stat sb; + if (fstat(fd, &sb) < 0 || sb.st_size <= 0 || + (size_t)sb.st_size > SCROLLBACK_MAX_BYTES * 4) { + close(fd); + return false; + } + unsigned char *buf = xmalloc(sb.st_size); + ssize_t got = 0; + while (got < sb.st_size) { + ssize_t n = read(fd, buf + got, sb.st_size - got); + if (n < 0) { + if (errno == EINTR) continue; + free(buf); + close(fd); + return false; + } + if (n == 0) break; + got += n; + } + close(fd); + if (got != sb.st_size) { + free(buf); + return false; + } + *out = buf; + *out_len = (size_t)got; + return true; +} + +void +session_free_names(char **names, size_t count) +{ + if (names == NULL) + return; + for (size_t i = 0; i < count; i++) + free(names[i]); + free(names); +} diff --git a/session.h b/session.h new file mode 100644 index 0000000..2b1921a --- /dev/null +++ b/session.h @@ -0,0 +1,92 @@ +#pragma once + +#include +#include + +/* + * Persistent terminal session state. + * + * Sessions are stored as JSON files under $XDG_DATA_HOME/foot/state/.json + * (defaulting to ~/.local/share/foot/state). Each file records the cwd of the + * foreground process on the pty plus its argv, so a session can be resurrected + * in a new tab. + */ + +struct session_state { + char *cwd; + int argc; + char **argv; /* NULL-terminated for convenience; argc does not count the terminator */ +}; + +void session_state_free(struct session_state *st); + +/* + * Inspect the pty for its foreground process. On Linux, walks /proc to grab + * cmdline + cwd. Falls back to fallback_cwd and a NULL argv on other platforms + * or when introspection fails (the caller will then save cwd only and let the + * shell start fresh on resume). Returns true if at least cwd is populated. + */ +bool session_capture(int ptmx, const char *fallback_cwd, + struct session_state *out); + +/* Persist to /.json. Creates the directory if missing. */ +bool session_save(const char *name, const struct session_state *st); + +/* Read /.json. */ +bool session_load(const char *name, struct session_state *out); + +/* + * List existing sessions. Returns a malloc'd array of malloc'd names + * (no .json suffix), sorted. Caller frees with session_free_names(). + */ +char **session_list(size_t *out_count); +void session_free_names(char **names, size_t count); + +/* Returns true if name is non-empty and contains only [A-Za-z0-9._-]. */ +bool session_name_is_valid(const char *name); + +/* Remove /.json. Returns true on success. */ +bool session_delete(const char *name); + +/* True if /.json exists. */ +bool session_exists(const char *name); + +struct terminal; + +/* + * Read /.scrollback.txt into out_text and out_len. Returns + * false (with out_text set to NULL) if the file is missing or fails sanity + * checks. Caller frees out_text. + */ +bool session_load_scrollback(const char *name, char **out_text, size_t *out_len); + +/* Best-effort delete of the scrollback sidecar (used when removing a session). */ +void session_delete_scrollback(const char *name); + +/* Encrypted-sidecar I/O. /.scrollback.enc */ +bool session_write_enc_blob(const char *name, + const unsigned char *blob, size_t len); +bool session_read_enc_blob(const char *name, + unsigned char **out, size_t *out_len); +bool session_has_enc_scrollback(const char *name); +void session_delete_enc_scrollback(const char *name); + +struct terminal; + +/* + * Called from search.c when the user presses Enter while the search bar is + * in a session prompt mode. Reads the typed name from term->search.buf and + * performs the requested save or load, then dismisses the prompt. + */ +void session_prompt_commit(struct terminal *term); +void session_prompt_cancel(struct terminal *term); + +/* Finalize a save after the user has confirmed overwrite (y). */ +void session_prompt_confirm_overwrite(struct terminal *term); + +/* Picker (session-load) helpers. */ +void session_picker_init(struct terminal *term); +void session_picker_free(struct terminal *term); +void session_picker_refilter(struct terminal *term); +void session_picker_move(struct terminal *term, int delta); /* +1/-1 etc */ +bool session_picker_delete_selected(struct terminal *term); diff --git a/terminal.c b/terminal.c index 0b9f56d..2ed756f 100644 --- a/terminal.c +++ b/terminal.c @@ -3,21 +3,21 @@ #if defined(__GLIBC__) #include #endif +#include +#include #include #include #include #include -#include -#include -#include -#include -#include -#include -#include -#include #include #include +#include +#include +#include +#include +#include +#include #include #define LOG_MODULE "terminal" @@ -50,186 +50,175 @@ #define PTMX_TIMING 0 -static void -enqueue_data_for_slave(const void *data, size_t len, size_t offset, - ptmx_buffer_list_t *buffer_list) -{ - struct ptmx_buffer queued = { - .data = xmemdup(data, len), - .len = len, - .idx = offset, - }; - tll_push_back(*buffer_list, queued); +static void enqueue_data_for_slave(const void *data, size_t len, size_t offset, + ptmx_buffer_list_t *buffer_list) { + struct ptmx_buffer queued = { + .data = xmemdup(data, len), + .len = len, + .idx = offset, + }; + tll_push_back(*buffer_list, queued); } -static bool -data_to_slave(struct terminal *term, const void *data, size_t len, - ptmx_buffer_list_t *buffer_list) -{ - /* - * Try a synchronous write first. If we fail to write everything, - * switch to asynchronous. - */ +static bool data_to_slave(struct terminal *term, const void *data, size_t len, + ptmx_buffer_list_t *buffer_list) { + /* + * Try a synchronous write first. If we fail to write everything, + * switch to asynchronous. + */ - size_t async_idx = 0; - switch (async_write(term->ptmx, data, len, &async_idx)) { - case ASYNC_WRITE_REMAIN: - /* Switch to asynchronous mode; let FDM write the remaining data */ - if (!fdm_event_add(term->fdm, term->ptmx, EPOLLOUT)) - return false; - enqueue_data_for_slave(data, len, async_idx, buffer_list); - return true; + size_t async_idx = 0; + switch (async_write(term->ptmx, data, len, &async_idx)) { + case ASYNC_WRITE_REMAIN: + /* Switch to asynchronous mode; let FDM write the remaining data */ + if (!fdm_event_add(term->fdm, term->ptmx, EPOLLOUT)) + return false; + enqueue_data_for_slave(data, len, async_idx, buffer_list); + return true; - case ASYNC_WRITE_DONE: - return true; + case ASYNC_WRITE_DONE: + return true; - case ASYNC_WRITE_ERR: - LOG_ERRNO("failed to synchronously write %zu bytes to slave", len); - return false; - } - - BUG("Unexpected async_write() return value"); + case ASYNC_WRITE_ERR: + LOG_ERRNO("failed to synchronously write %zu bytes to slave", len); return false; + } + + BUG("Unexpected async_write() return value"); + return false; } -bool -term_paste_data_to_slave(struct terminal *term, const void *data, size_t len) -{ - xassert(term->is_sending_paste_data); +bool term_paste_data_to_slave(struct terminal *term, const void *data, + size_t len) { + xassert(term->is_sending_paste_data); - if (term->ptmx < 0) { - /* We're probably in "hold" */ - return false; - } + if (term->ptmx < 0) { + /* We're probably in "hold" */ + return false; + } - if (tll_length(term->ptmx_paste_buffers) > 0) { - /* Don't even try to send data *now* if there's queued up - * data, since that would result in events arriving out of - * order. */ - enqueue_data_for_slave(data, len, 0, &term->ptmx_paste_buffers); - return true; - } + if (tll_length(term->ptmx_paste_buffers) > 0) { + /* Don't even try to send data *now* if there's queued up + * data, since that would result in events arriving out of + * order. */ + enqueue_data_for_slave(data, len, 0, &term->ptmx_paste_buffers); + return true; + } - return data_to_slave(term, data, len, &term->ptmx_paste_buffers); + return data_to_slave(term, data, len, &term->ptmx_paste_buffers); } -bool -term_to_slave(struct terminal *term, const void *data, size_t len) -{ - if (term->ptmx < 0) { - /* We're probably in "hold" */ - return false; - } - - if (unlikely(tll_length(term->ptmx_buffers) > 0 || - term->is_sending_paste_data || - tll_length(term->ptmx_paste_buffers) > 0)) - { - /* - * Don't even try to send data *now* if there's queued up - * data, since that would result in events arriving out of - * order. - * - * Furthermore, if we're currently sending paste data to the - * client, do *not* mix that stream with other events - * (https://codeberg.org/dnkl/foot/issues/101). - */ - enqueue_data_for_slave(data, len, 0, &term->ptmx_buffers); - return true; - } - - return data_to_slave(term, data, len, &term->ptmx_buffers); -} - -static bool -fdm_ptmx_out(struct fdm *fdm, int fd, int events, void *data) -{ - struct terminal *term = data; - - /* If there is no queued data, then we shouldn't be in asynchronous mode */ - xassert(tll_length(term->ptmx_buffers) > 0 || - tll_length(term->ptmx_paste_buffers) > 0); - - /* Writes a single buffer, returns if not all of it could be written */ -#define write_one_buffer(buffer_list) \ - { \ - switch (async_write(term->ptmx, it->item.data, it->item.len, &it->item.idx)) { \ - case ASYNC_WRITE_DONE: \ - free(it->item.data); \ - tll_remove(buffer_list, it); \ - break; \ - case ASYNC_WRITE_REMAIN: \ - /* to_slave() updated it->item.idx */ \ - return true; \ - case ASYNC_WRITE_ERR: \ - LOG_ERRNO("failed to asynchronously write %zu bytes to slave", \ - it->item.len - it->item.idx); \ - return false; \ - } \ - } - - tll_foreach(term->ptmx_paste_buffers, it) - write_one_buffer(term->ptmx_paste_buffers); - - /* If we get here, *all* paste data buffers were successfully - * flushed */ - - if (!term->is_sending_paste_data) { - tll_foreach(term->ptmx_buffers, it) - write_one_buffer(term->ptmx_buffers); - } +bool term_to_slave(struct terminal *term, const void *data, size_t len) { + if (term->ptmx < 0) { + /* We're probably in "hold" */ + return false; + } + if (unlikely(tll_length(term->ptmx_buffers) > 0 || + term->is_sending_paste_data || + tll_length(term->ptmx_paste_buffers) > 0)) { /* - * If we get here, *all* buffers were successfully flushed. + * Don't even try to send data *now* if there's queued up + * data, since that would result in events arriving out of + * order. * - * Or, we're still sending paste data, in which case we do *not* - * want to send the "normal" queued up data - * - * In both cases, we want to *disable* the FDM callback since - * otherwise we'd just be called right away again, with nothing to - * write. + * Furthermore, if we're currently sending paste data to the + * client, do *not* mix that stream with other events + * (https://codeberg.org/dnkl/foot/issues/101). */ - fdm_event_del(term->fdm, term->ptmx, EPOLLOUT); + enqueue_data_for_slave(data, len, 0, &term->ptmx_buffers); return true; + } + + return data_to_slave(term, data, len, &term->ptmx_buffers); } -static bool -add_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx) -{ +static bool fdm_ptmx_out(struct fdm *fdm, int fd, int events, void *data) { + struct terminal *term = data; + + /* If there is no queued data, then we shouldn't be in asynchronous mode */ + xassert(tll_length(term->ptmx_buffers) > 0 || + tll_length(term->ptmx_paste_buffers) > 0); + + /* Writes a single buffer, returns if not all of it could be written */ +#define write_one_buffer(buffer_list) \ + { \ + switch ( \ + async_write(term->ptmx, it->item.data, it->item.len, &it->item.idx)) { \ + case ASYNC_WRITE_DONE: \ + free(it->item.data); \ + tll_remove(buffer_list, it); \ + break; \ + case ASYNC_WRITE_REMAIN: \ + /* to_slave() updated it->item.idx */ \ + return true; \ + case ASYNC_WRITE_ERR: \ + LOG_ERRNO("failed to asynchronously write %zu bytes to slave", \ + it->item.len - it->item.idx); \ + return false; \ + } \ + } + + tll_foreach(term->ptmx_paste_buffers, it) + write_one_buffer(term->ptmx_paste_buffers); + + /* If we get here, *all* paste data buffers were successfully + * flushed */ + + if (!term->is_sending_paste_data) { + tll_foreach(term->ptmx_buffers, it) write_one_buffer(term->ptmx_buffers); + } + + /* + * If we get here, *all* buffers were successfully flushed. + * + * Or, we're still sending paste data, in which case we do *not* + * want to send the "normal" queued up data + * + * In both cases, we want to *disable* the FDM callback since + * otherwise we'd just be called right away again, with nothing to + * write. + */ + fdm_event_del(term->fdm, term->ptmx, EPOLLOUT); + return true; +} + +static bool add_utmp_record(const struct config *conf, struct reaper *reaper, + int ptmx) { #if defined(UTMP_ADD) - if (ptmx < 0) - return true; - if (conf->utmp_helper_path == NULL) - return true; - - char *const argv[] = {conf->utmp_helper_path, UTMP_ADD, getenv("WAYLAND_DISPLAY"), NULL}; - return spawn(reaper, NULL, argv, ptmx, -1, -1, NULL, NULL, NULL) >= 0; -#else + if (ptmx < 0) return true; + if (conf->utmp_helper_path == NULL) + return true; + + char *const argv[] = {conf->utmp_helper_path, UTMP_ADD, + getenv("WAYLAND_DISPLAY"), NULL}; + return spawn(reaper, NULL, argv, ptmx, -1, -1, NULL, NULL, NULL) >= 0; +#else + return true; #endif } -static bool -del_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx) -{ +static bool del_utmp_record(const struct config *conf, struct reaper *reaper, + int ptmx) { #if defined(UTMP_DEL) - if (ptmx < 0) - return true; - if (conf->utmp_helper_path == NULL) - return true; - - char *del_argument = -#if defined(UTMP_DEL_HAVE_ARGUMENT) - getenv("WAYLAND_DISPLAY") -#else - NULL -#endif - ; - - char *const argv[] = {conf->utmp_helper_path, UTMP_DEL, del_argument, NULL}; - return spawn(reaper, NULL, argv, ptmx, -1, -1, NULL, NULL, NULL) >= 0; -#else + if (ptmx < 0) return true; + if (conf->utmp_helper_path == NULL) + return true; + + char *del_argument = +#if defined(UTMP_DEL_HAVE_ARGUMENT) + getenv("WAYLAND_DISPLAY") +#else + NULL +#endif + ; + + char *const argv[] = {conf->utmp_helper_path, UTMP_DEL, del_argument, NULL}; + return spawn(reaper, NULL, argv, ptmx, -1, -1, NULL, NULL, NULL) >= 0; +#else + return true; #endif } @@ -241,1687 +230,1715 @@ static bool cursor_blink_rearm_timer(struct terminal *term); /* Externally visible, but not declared in terminal.h, to enable pgo * to call this function directly */ -bool -fdm_ptmx(struct fdm *fdm, int fd, int events, void *data) -{ - struct terminal *term = data; +bool fdm_ptmx(struct fdm *fdm, int fd, int events, void *data) { + struct terminal *term = data; - const bool pollin = events & EPOLLIN; - const bool pollout = events & EPOLLOUT; - const bool hup = events & EPOLLHUP; + const bool pollin = events & EPOLLIN; + const bool pollout = events & EPOLLOUT; + const bool hup = events & EPOLLHUP; - if (pollout) { - if (!fdm_ptmx_out(fdm, fd, events, data)) - return false; - } + if (pollout) { + if (!fdm_ptmx_out(fdm, fd, events, data)) + return false; + } - /* Prevent blinking while typing */ - if (term->cursor_blink.fd >= 0) { - term->cursor_blink.state = CURSOR_BLINK_ON; - cursor_blink_rearm_timer(term); - } + /* Prevent blinking while typing */ + if (term->cursor_blink.fd >= 0) { + term->cursor_blink.state = CURSOR_BLINK_ON; + cursor_blink_rearm_timer(term); + } - if (unlikely(term->interactive_resizing.grid != NULL)) { + if (unlikely(term->interactive_resizing.grid != NULL)) { + /* + * Don't consume PTMX while we're doing an interactive resize, + * since the 'normal' grid we're currently using is a + * temporary one - all changes done to it will be lost when + * the interactive resize ends. + */ + return true; + } + + uint8_t buf[24 * 1024]; + const size_t max_iterations = !hup ? 10 : SIZE_MAX; + + for (size_t i = 0; i < max_iterations && pollin; i++) { + xassert(pollin); + ssize_t count = read(term->ptmx, buf, sizeof(buf)); + + if (count < 0) { + if (errno == EAGAIN || errno == EIO) { /* - * Don't consume PTMX while we're doing an interactive resize, - * since the 'normal' grid we're currently using is a - * temporary one - all changes done to it will be lost when - * the interactive resize ends. + * EAGAIN: no more to read - FDM will trigger us again + * EIO: assume PTY was closed - we already have, or will get, a EPOLLHUP */ - return true; + break; + } + + LOG_ERRNO("failed to read from pseudo terminal"); + return false; + } else if (count == 0) { + /* Reached end-of-file */ + break; } - uint8_t buf[24 * 1024]; - const size_t max_iterations = !hup ? 10 : SIZE_MAX; + xassert(term->interactive_resizing.grid == NULL); + vt_from_slave(term, buf, count); - for (size_t i = 0; i < max_iterations && pollin; i++) { - xassert(pollin); - ssize_t count = read(term->ptmx, buf, sizeof(buf)); - - if (count < 0) { - if (errno == EAGAIN || errno == EIO) { - /* - * EAGAIN: no more to read - FDM will trigger us again - * EIO: assume PTY was closed - we already have, or will get, a EPOLLHUP - */ - break; - } - - LOG_ERRNO("failed to read from pseudo terminal"); - return false; - } else if (count == 0) { - /* Reached end-of-file */ - break; - } - - xassert(term->interactive_resizing.grid == NULL); - vt_from_slave(term, buf, count); - - /* Mark inactive tabs as having unread output, so the tab bar - * can show an indicator. Force a tab-bar refresh on the active - * terminal, since the active terminal owns the rendered bar. */ - if (term->window != NULL && term->window->tab_count > 1 && - term != term->window->term) - { - if (!term->has_unread) { - term->has_unread = true; - if (term->window->term != NULL && term->conf->tabs.enabled) - render_refresh_tab_bar(term->window->term); - } - } + /* Mark inactive tabs as having unread output, so the tab bar + * can show an indicator. Force a tab-bar refresh on the active + * terminal, since the active terminal owns the rendered bar. */ + if (term->window != NULL && term->window->tab_count > 1 && + term != term->window->term) { + if (!term->has_unread) { + term->has_unread = true; + if (term->window->term != NULL && term->conf->tabs.enabled) + render_refresh_tab_bar(term->window->term); + } } + } - if (!term->render.app_sync_updates.enabled) { - /* - * We likely need to re-render. But, we don't want to do it - * immediately. Often, a single client update is done through - * multiple writes. This could lead to us rendering one frame with - * "intermediate" state. - * - * For example, we might end up rendering a frame - * where the client just erased a line, while in the - * next frame, the client wrote to the same line. This - * causes screen "flickering". - * - * Mitigate by always incuring a small delay before - * rendering the next frame. This gives the client - * some time to finish the operation (and thus gives - * us time to receive the last writes before doing any - * actual rendering). - * - * We incur this delay *every* time we receive - * input. To ensure we don't delay rendering - * indefinitely, we start a second timer that is only - * reset when we render. - * - * Note that when the client is producing data at a - * very high pace, we're rate limited by the wayland - * compositor anyway. The delay we introduce here only - * has any effect when the renderer is idle. - */ - uint64_t lower_ns = term->conf->tweak.delayed_render_lower_ns; - uint64_t upper_ns = term->conf->tweak.delayed_render_upper_ns; + if (!term->render.app_sync_updates.enabled) { + /* + * We likely need to re-render. But, we don't want to do it + * immediately. Often, a single client update is done through + * multiple writes. This could lead to us rendering one frame with + * "intermediate" state. + * + * For example, we might end up rendering a frame + * where the client just erased a line, while in the + * next frame, the client wrote to the same line. This + * causes screen "flickering". + * + * Mitigate by always incuring a small delay before + * rendering the next frame. This gives the client + * some time to finish the operation (and thus gives + * us time to receive the last writes before doing any + * actual rendering). + * + * We incur this delay *every* time we receive + * input. To ensure we don't delay rendering + * indefinitely, we start a second timer that is only + * reset when we render. + * + * Note that when the client is producing data at a + * very high pace, we're rate limited by the wayland + * compositor anyway. The delay we introduce here only + * has any effect when the renderer is idle. + */ + uint64_t lower_ns = term->conf->tweak.delayed_render_lower_ns; + uint64_t upper_ns = term->conf->tweak.delayed_render_upper_ns; - if (lower_ns > 0 && upper_ns > 0) { + if (lower_ns > 0 && upper_ns > 0) { #if PTMX_TIMING - struct timespec now; + struct timespec now; - clock_gettime(CLOCK_MONOTONIC, &now); - if (last.tv_sec > 0 || last.tv_nsec > 0) { - struct timespec diff; + clock_gettime(CLOCK_MONOTONIC, &now); + if (last.tv_sec > 0 || last.tv_nsec > 0) { + struct timespec diff; - timespec_sub(&now, &last, &diff); - LOG_INFO("waited %lds %ldns for more input", - (long)diff.tv_sec, diff.tv_nsec); - } - last = now; + timespec_sub(&now, &last, &diff); + LOG_INFO("waited %lds %ldns for more input", (long)diff.tv_sec, + diff.tv_nsec); + } + last = now; #endif - xassert(lower_ns < 1000000000); - xassert(upper_ns < 1000000000); - xassert(upper_ns > lower_ns); + xassert(lower_ns < 1000000000); + xassert(upper_ns < 1000000000); + xassert(upper_ns > lower_ns); - timerfd_settime( - term->delayed_render_timer.lower_fd, 0, - &(struct itimerspec){.it_value = {.tv_nsec = lower_ns}}, - NULL); + timerfd_settime(term->delayed_render_timer.lower_fd, 0, + &(struct itimerspec){.it_value = {.tv_nsec = lower_ns}}, + NULL); - /* Second timeout - only reset when we render. Set to one - * frame (assuming 60Hz) */ - if (!term->delayed_render_timer.is_armed) { - timerfd_settime( - term->delayed_render_timer.upper_fd, 0, - &(struct itimerspec){.it_value = {.tv_nsec = upper_ns}}, - NULL); - term->delayed_render_timer.is_armed = true; - } - } else - render_refresh(term); - } - - if (hup) { - del_utmp_record(term->conf, term->reaper, term->ptmx); - fdm_del(fdm, fd); - term->ptmx = -1; - - /* - * Normally, we do *not* want to shutdown when the PTY is - * closed. Instead, we want to wait for the client application - * to exit. - * - * However, when we're using a pre-existing PTY (the --pty - * option), there _is_ no client application. That is, foot - * does *not* fork+exec anything, and thus the only way to - * shutdown is to wait for the PTY to be closed. - */ - if (term->slave < 0 && !term->conf->hold_at_exit) { - term_shutdown(term); - } - } - - return true; -} - -bool -term_ptmx_pause(struct terminal *term) -{ - if (term->ptmx < 0) - return false; - return fdm_event_del(term->fdm, term->ptmx, EPOLLIN); -} - -bool -term_ptmx_resume(struct terminal *term) -{ - if (term->ptmx < 0) - return false; - return fdm_event_add(term->fdm, term->ptmx, EPOLLIN); -} - -static bool -fdm_flash(struct fdm *fdm, int fd, int events, void *data) -{ - if (events & EPOLLHUP) - return false; - - struct terminal *term = data; - uint64_t expiration_count; - ssize_t ret = read( - term->flash.fd, &expiration_count, sizeof(expiration_count)); - - if (ret < 0) { - if (errno == EAGAIN) - return true; - - LOG_ERRNO("failed to read flash timer"); - return false; - } - - LOG_DBG("flash timer expired %llu times", - (unsigned long long)expiration_count); - - term->flash.active = false; - render_overlay(term); - - // since the overlay surface is synced with the main window surface, we have - // to commit the main surface for the compositor to acknowledge the new - // overlay state. - wl_surface_commit(term->window->surface.surf); - return true; -} - -static bool -fdm_blink(struct fdm *fdm, int fd, int events, void *data) -{ - if (events & EPOLLHUP) - return false; - - struct terminal *term = data; - uint64_t expiration_count; - ssize_t ret = read( - term->blink.fd, &expiration_count, sizeof(expiration_count)); - - if (ret < 0) { - if (errno == EAGAIN) - return true; - - LOG_ERRNO("failed to read blink timer"); - return false; - } - - LOG_DBG("blink timer expired %llu times", - (unsigned long long)expiration_count); - - /* Invert blink state */ - term->blink.state = term->blink.state == BLINK_ON - ? BLINK_OFF : BLINK_ON; - - /* Scan all visible cells and mark rows with blinking cells dirty */ - bool no_blinking_cells = true; - for (int r = 0; r < term->rows; r++) { - struct row *row = grid_row_in_view(term->grid, r); - for (int col = 0; col < term->cols; col++) { - struct cell *cell = &row->cells[col]; - - if (cell->attrs.blink) { - cell->attrs.clean = 0; - row->dirty = true; - no_blinking_cells = false; - } - } - } - - if (no_blinking_cells) { - LOG_DBG("disarming blink timer"); - - term->blink.state = BLINK_ON; - fdm_del(term->fdm, term->blink.fd); - term->blink.fd = -1; + /* Second timeout - only reset when we render. Set to one + * frame (assuming 60Hz) */ + if (!term->delayed_render_timer.is_armed) { + timerfd_settime(term->delayed_render_timer.upper_fd, 0, + &(struct itimerspec){.it_value = {.tv_nsec = upper_ns}}, + NULL); + term->delayed_render_timer.is_armed = true; + } } else - render_refresh(term); - return true; + render_refresh(term); + } + + if (hup) { + del_utmp_record(term->conf, term->reaper, term->ptmx); + fdm_del(fdm, fd); + term->ptmx = -1; + + /* + * Normally, we do *not* want to shutdown when the PTY is + * closed. Instead, we want to wait for the client application + * to exit. + * + * However, when we're using a pre-existing PTY (the --pty + * option), there _is_ no client application. That is, foot + * does *not* fork+exec anything, and thus the only way to + * shutdown is to wait for the PTY to be closed. + */ + if (term->slave < 0 && !term->conf->hold_at_exit) { + term_shutdown(term); + } + } + + return true; } -void -term_arm_blink_timer(struct terminal *term) -{ - if (term->blink.fd >= 0) - return; +bool term_ptmx_pause(struct terminal *term) { + if (term->ptmx < 0) + return false; + return fdm_event_del(term->fdm, term->ptmx, EPOLLIN); +} - LOG_DBG("arming blink timer"); +bool term_ptmx_resume(struct terminal *term) { + if (term->ptmx < 0) + return false; + return fdm_event_add(term->fdm, term->ptmx, EPOLLIN); +} - int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); - if (fd < 0) { - LOG_ERRNO("failed to create blink timer FD"); - return; +static bool fdm_flash(struct fdm *fdm, int fd, int events, void *data) { + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + uint64_t expiration_count; + ssize_t ret = + read(term->flash.fd, &expiration_count, sizeof(expiration_count)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + + LOG_ERRNO("failed to read flash timer"); + return false; + } + + LOG_DBG("flash timer expired %llu times", + (unsigned long long)expiration_count); + + term->flash.active = false; + render_overlay(term); + + // since the overlay surface is synced with the main window surface, we have + // to commit the main surface for the compositor to acknowledge the new + // overlay state. + wl_surface_commit(term->window->surface.surf); + return true; +} + +static bool fdm_blink(struct fdm *fdm, int fd, int events, void *data) { + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + uint64_t expiration_count; + ssize_t ret = + read(term->blink.fd, &expiration_count, sizeof(expiration_count)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + + LOG_ERRNO("failed to read blink timer"); + return false; + } + + LOG_DBG("blink timer expired %llu times", + (unsigned long long)expiration_count); + + /* Invert blink state */ + term->blink.state = term->blink.state == BLINK_ON ? BLINK_OFF : BLINK_ON; + + /* Scan all visible cells and mark rows with blinking cells dirty */ + bool no_blinking_cells = true; + for (int r = 0; r < term->rows; r++) { + struct row *row = grid_row_in_view(term->grid, r); + for (int col = 0; col < term->cols; col++) { + struct cell *cell = &row->cells[col]; + + if (cell->attrs.blink) { + cell->attrs.clean = 0; + row->dirty = true; + no_blinking_cells = false; + } } + } - if (!fdm_add(term->fdm, fd, EPOLLIN, &fdm_blink, term)) { - close(fd); - return; - } + if (no_blinking_cells) { + LOG_DBG("disarming blink timer"); - struct itimerspec alarm = { - .it_value = {.tv_sec = 0, .tv_nsec = 500 * 1000000}, - .it_interval = {.tv_sec = 0, .tv_nsec = 500 * 1000000}, + term->blink.state = BLINK_ON; + fdm_del(term->fdm, term->blink.fd); + term->blink.fd = -1; + } else + render_refresh(term); + return true; +} + +void term_arm_blink_timer(struct terminal *term) { + if (term->blink.fd >= 0) + return; + + LOG_DBG("arming blink timer"); + + int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (fd < 0) { + LOG_ERRNO("failed to create blink timer FD"); + return; + } + + if (!fdm_add(term->fdm, fd, EPOLLIN, &fdm_blink, term)) { + close(fd); + return; + } + + struct itimerspec alarm = { + .it_value = {.tv_sec = 0, .tv_nsec = 500 * 1000000}, + .it_interval = {.tv_sec = 0, .tv_nsec = 500 * 1000000}, + }; + + if (timerfd_settime(fd, 0, &alarm, NULL) < 0) { + LOG_ERRNO("failed to arm blink timer"); + fdm_del(term->fdm, fd); + } + + term->blink.fd = fd; +} + +static void cursor_refresh(struct terminal *term) { + if (!term->window->is_configured) + return; + + term->grid->cur_row->cells[term->grid->cursor.point.col].attrs.clean = 0; + term->grid->cur_row->dirty = true; + render_refresh(term); +} + +static bool fdm_cursor_blink(struct fdm *fdm, int fd, int events, void *data) { + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + uint64_t expiration_count; + ssize_t ret = + read(term->cursor_blink.fd, &expiration_count, sizeof(expiration_count)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + + LOG_ERRNO("failed to read cursor blink timer"); + return false; + } + + LOG_DBG("cursor blink timer expired %llu times", + (unsigned long long)expiration_count); + + /* Invert blink state */ + term->cursor_blink.state = term->cursor_blink.state == CURSOR_BLINK_ON + ? CURSOR_BLINK_OFF + : CURSOR_BLINK_ON; + + cursor_refresh(term); + return true; +} + +static bool fdm_delayed_render(struct fdm *fdm, int fd, int events, + void *data) { + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + + uint64_t unused; + ssize_t ret1 = 0; + ssize_t ret2 = 0; + + if (fd == term->delayed_render_timer.lower_fd) + ret1 = read(term->delayed_render_timer.lower_fd, &unused, sizeof(unused)); + if (fd == term->delayed_render_timer.upper_fd) + ret2 = read(term->delayed_render_timer.upper_fd, &unused, sizeof(unused)); + + if ((ret1 < 0 || ret2 < 0)) { + if (errno == EAGAIN) + return true; + + LOG_ERRNO("failed to read timeout timer"); + return false; + } + + if (ret1 > 0) + LOG_DBG("lower delay timer expired"); + else if (ret2 > 0) + LOG_DBG("upper delay timer expired"); + + if (ret1 == 0 && ret2 == 0) + return true; + +#if PTMX_TIMING + last = (struct timespec){0}; +#endif + + /* Reset timers */ + struct itimerspec reset = {{0}}; + timerfd_settime(term->delayed_render_timer.lower_fd, 0, &reset, NULL); + timerfd_settime(term->delayed_render_timer.upper_fd, 0, &reset, NULL); + term->delayed_render_timer.is_armed = false; + + render_refresh(term); + return true; +} + +static bool fdm_app_sync_updates_timeout(struct fdm *fdm, int fd, int events, + void *data) { + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + uint64_t unused; + ssize_t ret = + read(term->render.app_sync_updates.timer_fd, &unused, sizeof(unused)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + LOG_ERRNO("failed to read application synchronized updates timeout timer"); + return false; + } + + term_disable_app_sync_updates(term); + return true; +} + +static bool fdm_title_update_timeout(struct fdm *fdm, int fd, int events, + void *data) { + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + uint64_t unused; + ssize_t ret = read(term->render.title.timer_fd, &unused, sizeof(unused)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + LOG_ERRNO("failed to read title update throttle timer"); + return false; + } + + struct itimerspec reset = {{0}}; + timerfd_settime(term->render.title.timer_fd, 0, &reset, NULL); + + render_refresh_title(term); + return true; +} + +static bool fdm_icon_update_timeout(struct fdm *fdm, int fd, int events, + void *data) { + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + uint64_t unused; + ssize_t ret = read(term->render.icon.timer_fd, &unused, sizeof(unused)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + LOG_ERRNO("failed to read icon update throttle timer"); + return false; + } + + struct itimerspec reset = {{0}}; + timerfd_settime(term->render.icon.timer_fd, 0, &reset, NULL); + + render_refresh_icon(term); + return true; +} + +static bool fdm_app_id_update_timeout(struct fdm *fdm, int fd, int events, + void *data) { + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + uint64_t unused; + ssize_t ret = read(term->render.app_id.timer_fd, &unused, sizeof(unused)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + LOG_ERRNO("failed to read app ID update throttle timer"); + return false; + } + + struct itimerspec reset = {{0}}; + timerfd_settime(term->render.app_id.timer_fd, 0, &reset, NULL); + + render_refresh_app_id(term); + return true; +} + +static bool initialize_render_workers(struct terminal *term) { + LOG_INFO("using %hu rendering threads", term->render.workers.count); + + if (sem_init(&term->render.workers.start, 0, 0) < 0 || + sem_init(&term->render.workers.done, 0, 0) < 0) { + LOG_ERRNO("failed to instantiate render worker semaphores"); + return false; + } + + int err; + if ((err = mtx_init(&term->render.workers.lock, mtx_plain)) != thrd_success) { + LOG_ERR("failed to instantiate render worker mutex: %s (%d)", + thrd_err_as_string(err), err); + goto err_sem_destroy; + } + + mtx_init(&term->render.workers.preapplied_damage.lock, mtx_plain); + cnd_init(&term->render.workers.preapplied_damage.cond); + + term->render.workers.threads = xcalloc( + term->render.workers.count, sizeof(term->render.workers.threads[0])); + + for (size_t i = 0; i < term->render.workers.count; i++) { + struct render_worker_context *ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct render_worker_context){ + .term = term, + .my_id = 1 + i, }; - if (timerfd_settime(fd, 0, &alarm, NULL) < 0) { - LOG_ERRNO("failed to arm blink timer"); - fdm_del(term->fdm, fd); + int ret = thrd_create(&term->render.workers.threads[i], + &render_worker_thread, ctx); + if (ret != thrd_success) { + + LOG_ERR("failed to create render worker thread: %s (%d)", + thrd_err_as_string(ret), ret); + term->render.workers.threads[i] = 0; + return false; } + } - term->blink.fd = fd; -} - -static void -cursor_refresh(struct terminal *term) -{ - if (!term->window->is_configured) - return; - - term->grid->cur_row->cells[term->grid->cursor.point.col].attrs.clean = 0; - term->grid->cur_row->dirty = true; - render_refresh(term); -} - -static bool -fdm_cursor_blink(struct fdm *fdm, int fd, int events, void *data) -{ - if (events & EPOLLHUP) - return false; - - struct terminal *term = data; - uint64_t expiration_count; - ssize_t ret = read( - term->cursor_blink.fd, &expiration_count, sizeof(expiration_count)); - - if (ret < 0) { - if (errno == EAGAIN) - return true; - - LOG_ERRNO("failed to read cursor blink timer"); - return false; - } - - LOG_DBG("cursor blink timer expired %llu times", - (unsigned long long)expiration_count); - - /* Invert blink state */ - term->cursor_blink.state = term->cursor_blink.state == CURSOR_BLINK_ON - ? CURSOR_BLINK_OFF : CURSOR_BLINK_ON; - - cursor_refresh(term); - return true; -} - -static bool -fdm_delayed_render(struct fdm *fdm, int fd, int events, void *data) -{ - if (events & EPOLLHUP) - return false; - - struct terminal *term = data; - - uint64_t unused; - ssize_t ret1 = 0; - ssize_t ret2 = 0; - - if (fd == term->delayed_render_timer.lower_fd) - ret1 = read(term->delayed_render_timer.lower_fd, &unused, sizeof(unused)); - if (fd == term->delayed_render_timer.upper_fd) - ret2 = read(term->delayed_render_timer.upper_fd, &unused, sizeof(unused)); - - if ((ret1 < 0 || ret2 < 0)) { - if (errno == EAGAIN) - return true; - - LOG_ERRNO("failed to read timeout timer"); - return false; - } - - if (ret1 > 0) - LOG_DBG("lower delay timer expired"); - else if (ret2 > 0) - LOG_DBG("upper delay timer expired"); - - if (ret1 == 0 && ret2 == 0) - return true; - -#if PTMX_TIMING - last = (struct timespec){0}; -#endif - - /* Reset timers */ - struct itimerspec reset = {{0}}; - timerfd_settime(term->delayed_render_timer.lower_fd, 0, &reset, NULL); - timerfd_settime(term->delayed_render_timer.upper_fd, 0, &reset, NULL); - term->delayed_render_timer.is_armed = false; - - render_refresh(term); - return true; -} - -static bool -fdm_app_sync_updates_timeout( - struct fdm *fdm, int fd, int events, void *data) -{ - if (events & EPOLLHUP) - return false; - - struct terminal *term = data; - uint64_t unused; - ssize_t ret = read(term->render.app_sync_updates.timer_fd, - &unused, sizeof(unused)); - - if (ret < 0) { - if (errno == EAGAIN) - return true; - LOG_ERRNO("failed to read application synchronized updates timeout timer"); - return false; - } - - term_disable_app_sync_updates(term); - return true; -} - -static bool -fdm_title_update_timeout(struct fdm *fdm, int fd, int events, void *data) -{ - if (events & EPOLLHUP) - return false; - - struct terminal *term = data; - uint64_t unused; - ssize_t ret = read(term->render.title.timer_fd, &unused, sizeof(unused)); - - if (ret < 0) { - if (errno == EAGAIN) - return true; - LOG_ERRNO("failed to read title update throttle timer"); - return false; - } - - struct itimerspec reset = {{0}}; - timerfd_settime(term->render.title.timer_fd, 0, &reset, NULL); - - render_refresh_title(term); - return true; -} - -static bool -fdm_icon_update_timeout(struct fdm *fdm, int fd, int events, void *data) -{ - if (events & EPOLLHUP) - return false; - - struct terminal *term = data; - uint64_t unused; - ssize_t ret = read(term->render.icon.timer_fd, &unused, sizeof(unused)); - - if (ret < 0) { - if (errno == EAGAIN) - return true; - LOG_ERRNO("failed to read icon update throttle timer"); - return false; - } - - struct itimerspec reset = {{0}}; - timerfd_settime(term->render.icon.timer_fd, 0, &reset, NULL); - - render_refresh_icon(term); - return true; -} - -static bool -fdm_app_id_update_timeout(struct fdm *fdm, int fd, int events, void *data) -{ - if (events & EPOLLHUP) - return false; - - struct terminal *term = data; - uint64_t unused; - ssize_t ret = read(term->render.app_id.timer_fd, &unused, sizeof(unused)); - - if (ret < 0) { - if (errno == EAGAIN) - return true; - LOG_ERRNO("failed to read app ID update throttle timer"); - return false; - } - - struct itimerspec reset = {{0}}; - timerfd_settime(term->render.app_id.timer_fd, 0, &reset, NULL); - - render_refresh_app_id(term); - return true; -} - -static bool -initialize_render_workers(struct terminal *term) -{ - LOG_INFO("using %hu rendering threads", term->render.workers.count); - - if (sem_init(&term->render.workers.start, 0, 0) < 0 || - sem_init(&term->render.workers.done, 0, 0) < 0) - { - LOG_ERRNO("failed to instantiate render worker semaphores"); - return false; - } - - int err; - if ((err = mtx_init(&term->render.workers.lock, mtx_plain)) != thrd_success) { - LOG_ERR("failed to instantiate render worker mutex: %s (%d)", - thrd_err_as_string(err), err); - goto err_sem_destroy; - } - - mtx_init(&term->render.workers.preapplied_damage.lock, mtx_plain); - cnd_init(&term->render.workers.preapplied_damage.cond); - - term->render.workers.threads = xcalloc( - term->render.workers.count, sizeof(term->render.workers.threads[0])); - - for (size_t i = 0; i < term->render.workers.count; i++) { - struct render_worker_context *ctx = xmalloc(sizeof(*ctx)); - *ctx = (struct render_worker_context) { - .term = term, - .my_id = 1 + i, - }; - - int ret = thrd_create( - &term->render.workers.threads[i], &render_worker_thread, ctx); - if (ret != thrd_success) { - - LOG_ERR("failed to create render worker thread: %s (%d)", - thrd_err_as_string(ret), ret); - term->render.workers.threads[i] = 0; - return false; - } - } - - return true; + return true; err_sem_destroy: - sem_destroy(&term->render.workers.start); - sem_destroy(&term->render.workers.done); - return false; + sem_destroy(&term->render.workers.start); + sem_destroy(&term->render.workers.done); + return false; } -static void -free_custom_glyph(struct fcft_glyph **glyph) -{ - if (*glyph == NULL) - return; +static void free_custom_glyph(struct fcft_glyph **glyph) { + if (*glyph == NULL) + return; - free(pixman_image_get_data((*glyph)->pix)); - pixman_image_unref((*glyph)->pix); - free(*glyph); - *glyph = NULL; + free(pixman_image_get_data((*glyph)->pix)); + pixman_image_unref((*glyph)->pix); + free(*glyph); + *glyph = NULL; } -static void -free_custom_glyphs(struct fcft_glyph ***glyphs, size_t count) -{ - if (*glyphs == NULL) - return; +static void free_custom_glyphs(struct fcft_glyph ***glyphs, size_t count) { + if (*glyphs == NULL) + return; - for (size_t i = 0; i < count; i++) - free_custom_glyph(&(*glyphs)[i]); + for (size_t i = 0; i < count; i++) + free_custom_glyph(&(*glyphs)[i]); - free(*glyphs); - *glyphs = NULL; + free(*glyphs); + *glyphs = NULL; } -static void -term_line_height_update(struct terminal *term) -{ - const struct config *conf = term->conf; +static void term_line_height_update(struct terminal *term) { + const struct config *conf = term->conf; - if (term->conf->line_height.px < 0) { - term->font_line_height.pt = 0; - term->font_line_height.px = -1; - return; - } + if (term->conf->line_height.px < 0) { + term->font_line_height.pt = 0; + term->font_line_height.px = -1; + return; + } - const float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; + const float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; - const float font_original_pt_size = - conf->fonts[0].arr[0].px_size > 0 - ? conf->fonts[0].arr[0].px_size * 72. / dpi - : conf->fonts[0].arr[0].pt_size; - const float font_current_pt_size = - term->font_sizes[0][0].px_size > 0 - ? term->font_sizes[0][0].px_size * 72. / dpi - : term->font_sizes[0][0].pt_size; + const float font_original_pt_size = + conf->fonts[0].arr[0].px_size > 0 + ? conf->fonts[0].arr[0].px_size * 72. / dpi + : conf->fonts[0].arr[0].pt_size; + const float font_current_pt_size = + term->font_sizes[0][0].px_size > 0 + ? term->font_sizes[0][0].px_size * 72. / dpi + : term->font_sizes[0][0].pt_size; - const float change = font_current_pt_size / font_original_pt_size; - const float line_original_pt_size = conf->line_height.px > 0 - ? conf->line_height.px * 72. / dpi - : conf->line_height.pt; + const float change = font_current_pt_size / font_original_pt_size; + const float line_original_pt_size = conf->line_height.px > 0 + ? conf->line_height.px * 72. / dpi + : conf->line_height.pt; - term->font_line_height.px = 0; - term->font_line_height.pt = fmaxf(line_original_pt_size * change, 0.); + term->font_line_height.px = 0; + term->font_line_height.pt = fmaxf(line_original_pt_size * change, 0.); } -static bool -term_set_fonts(struct terminal *term, struct fcft_font *fonts[static 4], - bool resize_grid) -{ - for (size_t i = 0; i < 4; i++) { - xassert(fonts[i] != NULL); +static bool term_set_fonts(struct terminal *term, + struct fcft_font *fonts[static 4], + bool resize_grid) { + for (size_t i = 0; i < 4; i++) { + xassert(fonts[i] != NULL); - fcft_destroy(term->fonts[i]); - term->fonts[i] = fonts[i]; - } + fcft_destroy(term->fonts[i]); + term->fonts[i] = fonts[i]; + } - free_custom_glyphs( - &term->custom_glyphs.box_drawing, GLYPH_BOX_DRAWING_COUNT); - free_custom_glyphs( - &term->custom_glyphs.braille, GLYPH_BRAILLE_COUNT); - free_custom_glyphs( - &term->custom_glyphs.legacy, GLYPH_LEGACY_COUNT); - free_custom_glyphs( - &term->custom_glyphs.octants, GLYPH_OCTANTS_COUNT); + free_custom_glyphs(&term->custom_glyphs.box_drawing, GLYPH_BOX_DRAWING_COUNT); + free_custom_glyphs(&term->custom_glyphs.braille, GLYPH_BRAILLE_COUNT); + free_custom_glyphs(&term->custom_glyphs.legacy, GLYPH_LEGACY_COUNT); + free_custom_glyphs(&term->custom_glyphs.octants, GLYPH_OCTANTS_COUNT); - const struct config *conf = term->conf; + const struct config *conf = term->conf; - const struct fcft_glyph *M = fcft_rasterize_char_utf32( - fonts[0], U'M', term->font_subpixel); - int advance = M != NULL ? M->advance.x : term->fonts[0]->max_advance.x; + const struct fcft_glyph *M = + fcft_rasterize_char_utf32(fonts[0], U'M', term->font_subpixel); + int advance = M != NULL ? M->advance.x : term->fonts[0]->max_advance.x; - term_line_height_update(term); + term_line_height_update(term); - term->cell_width = advance + - term_pt_or_px_as_pixels(term, &conf->letter_spacing); + term->cell_width = + advance + term_pt_or_px_as_pixels(term, &conf->letter_spacing); - term->cell_height = term->font_line_height.px >= 0 - ? term_pt_or_px_as_pixels(term, &term->font_line_height) - : max(term->fonts[0]->height, - term->fonts[0]->ascent + term->fonts[0]->descent); + term->cell_height = + term->font_line_height.px >= 0 + ? term_pt_or_px_as_pixels(term, &term->font_line_height) + : max(term->fonts[0]->height, + term->fonts[0]->ascent + term->fonts[0]->descent); - if (term->cell_width <= 0) - term->cell_width = 1; - if (term->cell_height <= 0) - term->cell_height = 1; + if (term->cell_width <= 0) + term->cell_width = 1; + if (term->cell_height <= 0) + term->cell_height = 1; - term->font_x_ofs = term_pt_or_px_as_pixels(term, &conf->horizontal_letter_offset); - term->font_y_ofs = term_pt_or_px_as_pixels(term, &conf->vertical_letter_offset); + term->font_x_ofs = + term_pt_or_px_as_pixels(term, &conf->horizontal_letter_offset); + term->font_y_ofs = + term_pt_or_px_as_pixels(term, &conf->vertical_letter_offset); - term->font_baseline = term_font_baseline(term); + term->font_baseline = term_font_baseline(term); - LOG_INFO("cell width=%d, height=%d", term->cell_width, term->cell_height); + LOG_INFO("cell width=%d, height=%d", term->cell_width, term->cell_height); - sixel_cell_size_changed(term); + sixel_cell_size_changed(term); - /* Optimization - some code paths (are forced to) call - * render_resize() after this function */ - if (resize_grid) { - /* Use force, since cell-width/height may have changed */ - enum resize_options resize_opts = RESIZE_FORCE; - if (conf->resize_keep_grid) - resize_opts |= RESIZE_KEEP_GRID; + /* Optimization - some code paths (are forced to) call + * render_resize() after this function */ + if (resize_grid) { + /* Use force, since cell-width/height may have changed */ + enum resize_options resize_opts = RESIZE_FORCE; + if (conf->resize_keep_grid) + resize_opts |= RESIZE_KEEP_GRID; - render_resize( - term, - (int)roundf(term->width / term->scale), - (int)roundf(term->height / term->scale), - resize_opts); - } - return true; + render_resize(term, (int)roundf(term->width / term->scale), + (int)roundf(term->height / term->scale), resize_opts); + } + return true; } -static float -get_font_dpi(const struct terminal *term) -{ - /* - * Use output's DPI to scale font. This is to ensure the font has - * the same physical height (if measured by a ruler) regardless of - * monitor. - * - * Conceptually, we use the physical monitor specs to calculate - * the DPI, and we ignore the output's scaling factor. - * - * However, to deal with legacy fractional scaling, where we're - * told to render at e.g. 2x, but are then downscaled by the - * compositor to e.g. 1.25, we use the scaled DPI value multiplied - * by the scale factor instead. - * - * For integral scaling factors the resulting DPI is the same as - * if we had used the physical DPI. - * - * For legacy fractional scaling factors we'll get a DPI *larger* - * than the physical DPI, that ends up being right when later - * downscaled by the compositor. - * - * With the newer fractional-scale-v1 protocol, we use the - * monitor's real DPI, since we scale everything to the correct - * scaling factor (no downscaling done by the compositor). - */ +static float get_font_dpi(const struct terminal *term) { + /* + * Use output's DPI to scale font. This is to ensure the font has + * the same physical height (if measured by a ruler) regardless of + * monitor. + * + * Conceptually, we use the physical monitor specs to calculate + * the DPI, and we ignore the output's scaling factor. + * + * However, to deal with legacy fractional scaling, where we're + * told to render at e.g. 2x, but are then downscaled by the + * compositor to e.g. 1.25, we use the scaled DPI value multiplied + * by the scale factor instead. + * + * For integral scaling factors the resulting DPI is the same as + * if we had used the physical DPI. + * + * For legacy fractional scaling factors we'll get a DPI *larger* + * than the physical DPI, that ends up being right when later + * downscaled by the compositor. + * + * With the newer fractional-scale-v1 protocol, we use the + * monitor's real DPI, since we scale everything to the correct + * scaling factor (no downscaling done by the compositor). + */ - const struct wl_window *win = term->window; - const struct monitor *mon = NULL; + const struct wl_window *win = term->window; + const struct monitor *mon = NULL; - if (tll_length(win->on_outputs) > 0) - mon = tll_back(win->on_outputs); - else { - if (term->font_dpi_before_unmap > 0.) { - /* - * Use last known "good" DPI - * - * This avoids flickering when window is unmapped/mapped - * (some compositors do this when a window is minimized), - * on a multi-monitor setup with different monitor DPIs. - */ - return term->font_dpi_before_unmap; - } - - if (tll_length(term->wl->monitors) > 0) - mon = &tll_front(term->wl->monitors); + if (tll_length(win->on_outputs) > 0) + mon = tll_back(win->on_outputs); + else { + if (term->font_dpi_before_unmap > 0.) { + /* + * Use last known "good" DPI + * + * This avoids flickering when window is unmapped/mapped + * (some compositors do this when a window is minimized), + * on a multi-monitor setup with different monitor DPIs. + */ + return term->font_dpi_before_unmap; } - const float monitor_dpi = mon != NULL - ? term_fractional_scaling(term) - ? mon->dpi.physical - : mon->dpi.scaled - : 96.; + if (tll_length(term->wl->monitors) > 0) + mon = &tll_front(term->wl->monitors); + } - return monitor_dpi > 0. ? monitor_dpi : 96.; + const float monitor_dpi = + mon != NULL + ? term_fractional_scaling(term) ? mon->dpi.physical : mon->dpi.scaled + : 96.; + + return monitor_dpi > 0. ? monitor_dpi : 96.; } -static enum fcft_subpixel -get_font_subpixel(const struct terminal *term) -{ - if (term->colors.alpha != 0xffff) { - /* Can't do subpixel rendering on transparent background */ - return FCFT_SUBPIXEL_NONE; - } +static enum fcft_subpixel get_font_subpixel(const struct terminal *term) { + if (term->colors.alpha != 0xffff) { + /* Can't do subpixel rendering on transparent background */ + return FCFT_SUBPIXEL_NONE; + } - enum wl_output_subpixel wl_subpixel; + enum wl_output_subpixel wl_subpixel; - /* - * Wayland doesn't tell us *which* part of the surface that goes - * on a specific output, only whether the surface is mapped to an - * output or not. - * - * Thus, when determining which subpixel mode to use, we can't do - * much but select *an* output. So, we pick the one we were most - * recently mapped on. - * - * If we're not mapped at all, we pick the first available - * monitor, and hope that's where we'll eventually get mapped. - * - * If there aren't any monitors we use the "default" subpixel - * mode. - */ + /* + * Wayland doesn't tell us *which* part of the surface that goes + * on a specific output, only whether the surface is mapped to an + * output or not. + * + * Thus, when determining which subpixel mode to use, we can't do + * much but select *an* output. So, we pick the one we were most + * recently mapped on. + * + * If we're not mapped at all, we pick the first available + * monitor, and hope that's where we'll eventually get mapped. + * + * If there aren't any monitors we use the "default" subpixel + * mode. + */ - if (tll_length(term->window->on_outputs) > 0) - wl_subpixel = tll_back(term->window->on_outputs)->subpixel; - else if (tll_length(term->wl->monitors) > 0) - wl_subpixel = tll_front(term->wl->monitors).subpixel; - else - wl_subpixel = WL_OUTPUT_SUBPIXEL_UNKNOWN; - - switch (wl_subpixel) { - case WL_OUTPUT_SUBPIXEL_UNKNOWN: return FCFT_SUBPIXEL_DEFAULT; - case WL_OUTPUT_SUBPIXEL_NONE: return FCFT_SUBPIXEL_NONE; - case WL_OUTPUT_SUBPIXEL_HORIZONTAL_RGB: return FCFT_SUBPIXEL_HORIZONTAL_RGB; - case WL_OUTPUT_SUBPIXEL_HORIZONTAL_BGR: return FCFT_SUBPIXEL_HORIZONTAL_BGR; - case WL_OUTPUT_SUBPIXEL_VERTICAL_RGB: return FCFT_SUBPIXEL_VERTICAL_RGB; - case WL_OUTPUT_SUBPIXEL_VERTICAL_BGR: return FCFT_SUBPIXEL_VERTICAL_BGR; - } + if (tll_length(term->window->on_outputs) > 0) + wl_subpixel = tll_back(term->window->on_outputs)->subpixel; + else if (tll_length(term->wl->monitors) > 0) + wl_subpixel = tll_front(term->wl->monitors).subpixel; + else + wl_subpixel = WL_OUTPUT_SUBPIXEL_UNKNOWN; + switch (wl_subpixel) { + case WL_OUTPUT_SUBPIXEL_UNKNOWN: return FCFT_SUBPIXEL_DEFAULT; + case WL_OUTPUT_SUBPIXEL_NONE: + return FCFT_SUBPIXEL_NONE; + case WL_OUTPUT_SUBPIXEL_HORIZONTAL_RGB: + return FCFT_SUBPIXEL_HORIZONTAL_RGB; + case WL_OUTPUT_SUBPIXEL_HORIZONTAL_BGR: + return FCFT_SUBPIXEL_HORIZONTAL_BGR; + case WL_OUTPUT_SUBPIXEL_VERTICAL_RGB: + return FCFT_SUBPIXEL_VERTICAL_RGB; + case WL_OUTPUT_SUBPIXEL_VERTICAL_BGR: + return FCFT_SUBPIXEL_VERTICAL_BGR; + } + + return FCFT_SUBPIXEL_DEFAULT; } -int -term_pt_or_px_as_pixels(const struct terminal *term, - const struct pt_or_px *pt_or_px) -{ - float scale = !term->font_is_sized_by_dpi ? term->scale : 1.; - float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; +int term_pt_or_px_as_pixels(const struct terminal *term, + const struct pt_or_px *pt_or_px) { + float scale = !term->font_is_sized_by_dpi ? term->scale : 1.; + float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; - return pt_or_px->px == 0 - ? (int)roundf(pt_or_px->pt * scale * dpi / 72) - : (int)roundf(pt_or_px->px * scale); + return pt_or_px->px == 0 ? (int)roundf(pt_or_px->pt * scale * dpi / 72) + : (int)roundf(pt_or_px->px * scale); } struct font_load_data { - size_t count; - const char **names; - const char *attrs; + size_t count; + const char **names; + const char *attrs; - const struct fcft_font_options *options; - struct fcft_font **font; + const struct fcft_font_options *options; + struct fcft_font **font; }; -static int -font_loader_thread(void *_data) -{ - struct font_load_data *data = _data; - *data->font = fcft_from_name2( - data->count, data->names, data->attrs, data->options); - return *data->font != NULL; +static int font_loader_thread(void *_data) { + struct font_load_data *data = _data; + *data->font = + fcft_from_name2(data->count, data->names, data->attrs, data->options); + return *data->font != NULL; } -static bool -reload_fonts(struct terminal *term, bool resize_grid) -{ - const struct config *conf = term->conf; +static bool reload_fonts(struct terminal *term, bool resize_grid) { + const struct config *conf = term->conf; - const size_t counts[4] = { - conf->fonts[0].count, - conf->fonts[1].count, - conf->fonts[2].count, - conf->fonts[3].count, - }; + const size_t counts[4] = { + conf->fonts[0].count, + conf->fonts[1].count, + conf->fonts[2].count, + conf->fonts[3].count, + }; - /* Configure size (which may have been changed run-time) */ - char **names[4]; - for (size_t i = 0; i < 4; i++) { - names[i] = xmalloc(counts[i] * sizeof(names[i][0])); + /* Configure size (which may have been changed run-time) */ + char **names[4]; + for (size_t i = 0; i < 4; i++) { + names[i] = xmalloc(counts[i] * sizeof(names[i][0])); - const struct config_font_list *font_list = &conf->fonts[i]; + const struct config_font_list *font_list = &conf->fonts[i]; - for (size_t j = 0; j < font_list->count; j++) { - const struct config_font *font = &font_list->arr[j]; - bool use_px_size = term->font_sizes[i][j].px_size > 0; - char size[64]; + for (size_t j = 0; j < font_list->count; j++) { + const struct config_font *font = &font_list->arr[j]; + bool use_px_size = term->font_sizes[i][j].px_size > 0; + char size[64]; - const float scale = term->font_is_sized_by_dpi ? 1. : term->scale; + const float scale = term->font_is_sized_by_dpi ? 1. : term->scale; - if (use_px_size) - snprintf(size, sizeof(size), ":pixelsize=%d", - (int)roundf(term->font_sizes[i][j].px_size * scale)); - else - snprintf(size, sizeof(size), ":size=%.2f", - term->font_sizes[i][j].pt_size * scale); + if (use_px_size) + snprintf(size, sizeof(size), ":pixelsize=%d", + (int)roundf(term->font_sizes[i][j].px_size * scale)); + else + snprintf(size, sizeof(size), ":size=%.2f", + term->font_sizes[i][j].pt_size * scale); - names[i][j] = xstrjoin(font->pattern, size); - } + names[i][j] = xstrjoin(font->pattern, size); } + } - /* Did user configure custom bold/italic fonts? - * Or should we use the regular font, with weight/slant attributes? */ - const bool custom_bold = counts[1] > 0; - const bool custom_italic = counts[2] > 0; - const bool custom_bold_italic = counts[3] > 0; + /* Did user configure custom bold/italic fonts? + * Or should we use the regular font, with weight/slant attributes? */ + const bool custom_bold = counts[1] > 0; + const bool custom_italic = counts[2] > 0; + const bool custom_bold_italic = counts[3] > 0; - const size_t count_regular = counts[0]; - const char **names_regular = (const char **)names[0]; + const size_t count_regular = counts[0]; + const char **names_regular = (const char **)names[0]; - const size_t count_bold = custom_bold ? counts[1] : counts[0]; - const char **names_bold = (const char **)(custom_bold ? names[1] : names[0]); + const size_t count_bold = custom_bold ? counts[1] : counts[0]; + const char **names_bold = (const char **)(custom_bold ? names[1] : names[0]); - const size_t count_italic = custom_italic ? counts[2] : counts[0]; - const char **names_italic = (const char **)(custom_italic ? names[2] : names[0]); + const size_t count_italic = custom_italic ? counts[2] : counts[0]; + const char **names_italic = + (const char **)(custom_italic ? names[2] : names[0]); - const size_t count_bold_italic = custom_bold_italic ? counts[3] : counts[0]; - const char **names_bold_italic = (const char **)(custom_bold_italic ? names[3] : names[0]); + const size_t count_bold_italic = custom_bold_italic ? counts[3] : counts[0]; + const char **names_bold_italic = + (const char **)(custom_bold_italic ? names[3] : names[0]); - const bool use_dpi = term->font_is_sized_by_dpi; - char *dpi = xasprintf("dpi=%.2f", use_dpi ? term->font_dpi : 96.); + const bool use_dpi = term->font_is_sized_by_dpi; + char *dpi = xasprintf("dpi=%.2f", use_dpi ? term->font_dpi : 96.); - char *attrs[4] = { - [0] = dpi, /* Takes ownership */ - [1] = xstrjoin(dpi, !custom_bold ? ":weight=bold" : ""), - [2] = xstrjoin(dpi, !custom_italic ? ":slant=italic" : ""), - [3] = xstrjoin(dpi, !custom_bold_italic ? ":weight=bold:slant=italic" : ""), - }; + char *attrs[4] = { + [0] = dpi, /* Takes ownership */ + [1] = xstrjoin(dpi, !custom_bold ? ":weight=bold" : ""), + [2] = xstrjoin(dpi, !custom_italic ? ":slant=italic" : ""), + [3] = + xstrjoin(dpi, !custom_bold_italic ? ":weight=bold:slant=italic" : ""), + }; - struct fcft_font_options *options = fcft_font_options_create(); + struct fcft_font_options *options = fcft_font_options_create(); - options->scaling_filter = conf->tweak.fcft_filter; - options->color_glyphs.format = PIXMAN_a8r8g8b8; - options->color_glyphs.srgb_decode = - wayl_do_linear_blending(term->wl, term->conf); + options->scaling_filter = conf->tweak.fcft_filter; + options->color_glyphs.format = PIXMAN_a8r8g8b8; + options->color_glyphs.srgb_decode = + wayl_do_linear_blending(term->wl, term->conf); - if (shm_chain_bit_depth(term->render.chains.grid) >= SHM_BITS_10) { - /* - * Use a high-res buffer type for emojis. We don't want to use - * an a2r10g0b10 type of surface, since we need more than 2 - * bits for alpha. - */ + if (shm_chain_bit_depth(term->render.chains.grid) >= SHM_BITS_10) { + /* + * Use a high-res buffer type for emojis. We don't want to use + * an a2r10g0b10 type of surface, since we need more than 2 + * bits for alpha. + */ #if defined(HAVE_PIXMAN_RGBA_16) - options->color_glyphs.format = PIXMAN_a16b16g16r16; + options->color_glyphs.format = PIXMAN_a16b16g16r16; #else - options->color_glyphs.format = PIXMAN_rgba_float; + options->color_glyphs.format = PIXMAN_rgba_float; #endif + } + + struct fcft_font *fonts[4]; + struct font_load_data data[4] = { + {count_regular, names_regular, attrs[0], options, &fonts[0]}, + {count_bold, names_bold, attrs[1], options, &fonts[1]}, + {count_italic, names_italic, attrs[2], options, &fonts[2]}, + {count_bold_italic, names_bold_italic, attrs[3], options, &fonts[3]}, + }; + + thrd_t tids[4] = {0}; + for (size_t i = 0; i < 4; i++) { + int ret = thrd_create(&tids[i], &font_loader_thread, &data[i]); + if (ret != thrd_success) { + LOG_ERR("failed to create font loader thread: %s (%d)", + thrd_err_as_string(ret), ret); + break; } + } - struct fcft_font *fonts[4]; - struct font_load_data data[4] = { - {count_regular, names_regular, attrs[0], options, &fonts[0]}, - {count_bold, names_bold, attrs[1], options, &fonts[1]}, - {count_italic, names_italic, attrs[2], options, &fonts[2]}, - {count_bold_italic, names_bold_italic, attrs[3], options, &fonts[3]}, - }; + bool success = true; + for (size_t i = 0; i < 4; i++) { + if (tids[i] != 0) { + int ret; + if (thrd_join(tids[i], &ret) != thrd_success) + success = false; + else + success = success && ret; + } else + success = false; + } - thrd_t tids[4] = {0}; + fcft_font_options_destroy(options); + + for (size_t i = 0; i < 4; i++) { + for (size_t j = 0; j < counts[i]; j++) + free(names[i][j]); + free(names[i]); + free(attrs[i]); + } + + if (!success) { + LOG_ERR("failed to load primary fonts"); for (size_t i = 0; i < 4; i++) { - int ret = thrd_create(&tids[i], &font_loader_thread, &data[i]); - if (ret != thrd_success) { - LOG_ERR("failed to create font loader thread: %s (%d)", - thrd_err_as_string(ret), ret); - break; - } + fcft_destroy(fonts[i]); + fonts[i] = NULL; } + } - bool success = true; - for (size_t i = 0; i < 4; i++) { - if (tids[i] != 0) { - int ret; - if (thrd_join(tids[i], &ret) != thrd_success) - success = false; - else - success = success && ret; - } else - success = false; - } - - fcft_font_options_destroy(options); - - for (size_t i = 0; i < 4; i++) { - for (size_t j = 0; j < counts[i]; j++) - free(names[i][j]); - free(names[i]); - free(attrs[i]); - } - - if (!success) { - LOG_ERR("failed to load primary fonts"); - for (size_t i = 0; i < 4; i++) { - fcft_destroy(fonts[i]); - fonts[i] = NULL; - } - } - - return success ? term_set_fonts(term, fonts, resize_grid) : success; + return success ? term_set_fonts(term, fonts, resize_grid) : success; } -static bool -load_fonts_from_conf(struct terminal *term) -{ - const struct config *conf = term->conf; +static bool load_fonts_from_conf(struct terminal *term) { + const struct config *conf = term->conf; - for (size_t i = 0; i < 4; i++) { - const struct config_font_list *font_list = &conf->fonts[i]; + for (size_t i = 0; i < 4; i++) { + const struct config_font_list *font_list = &conf->fonts[i]; - for (size_t j = 0; j < font_list->count; j++) { - const struct config_font *font = &font_list->arr[j]; - term->font_sizes[i][j] = (struct config_font){ - .pt_size = font->pt_size, .px_size = font->px_size}; - } + for (size_t j = 0; j < font_list->count; j++) { + const struct config_font *font = &font_list->arr[j]; + term->font_sizes[i][j] = (struct config_font){.pt_size = font->pt_size, + .px_size = font->px_size}; } + } - return reload_fonts(term, true); + return reload_fonts(term, true); } -static void fdm_client_terminated( - struct reaper *reaper, pid_t pid, int status, void *data); +static void fdm_client_terminated(struct reaper *reaper, pid_t pid, int status, + void *data); static const int PTY_OPEN_FLAGS = O_RDWR | O_NOCTTY; -struct terminal * -term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, - struct wayland *wayl, const char *foot_exe, const char *cwd, - const char *token, const char *pty_path, - int argc, char *const *argv, const char *const *envp, - void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data) -{ - int ptmx = -1; - int flash_fd = -1; - int delay_lower_fd = -1; - int delay_upper_fd = -1; - int app_sync_updates_fd = -1; - int title_update_fd = -1; - int icon_update_fd = -1; - int app_id_update_fd = -1; +struct terminal *term_init(const struct config *conf, struct fdm *fdm, + struct reaper *reaper, struct wayland *wayl, + const char *foot_exe, const char *cwd, + const char *token, const char *pty_path, int argc, + char *const *argv, const char *const *envp, + void (*shutdown_cb)(void *data, int exit_code), + void *shutdown_data) { + int ptmx = -1; + int flash_fd = -1; + int delay_lower_fd = -1; + int delay_upper_fd = -1; + int app_sync_updates_fd = -1; + int title_update_fd = -1; + int icon_update_fd = -1; + int app_id_update_fd = -1; - struct terminal *term = malloc(sizeof(*term)); - if (unlikely(term == NULL)) { - LOG_ERRNO("malloc() failed"); - return NULL; - } - - ptmx = pty_path ? open(pty_path, PTY_OPEN_FLAGS) : posix_openpt(PTY_OPEN_FLAGS); - if (ptmx < 0) { - LOG_ERRNO("failed to open PTY"); - goto close_fds; - } - if ((flash_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) { - LOG_ERRNO("failed to create flash timer FD"); - goto close_fds; - } - if ((delay_lower_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || - (delay_upper_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) - { - LOG_ERRNO("failed to create delayed rendering timer FDs"); - goto close_fds; - } - - if ((app_sync_updates_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) - { - LOG_ERRNO("failed to create application synchronized updates timer FD"); - goto close_fds; - } - - if ((title_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) - { - LOG_ERRNO("failed to create title update throttle timer FD"); - goto close_fds; - } - - if ((icon_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) - { - LOG_ERRNO("failed to create icon update throttle timer FD"); - goto close_fds; - } - - if ((app_id_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) - { - LOG_ERRNO("failed to create app ID update throttle timer FD"); - goto close_fds; - } - - if (ioctl(ptmx, (unsigned int)TIOCSWINSZ, - &(struct winsize){.ws_row = 24, .ws_col = 80}) < 0) - { - LOG_ERRNO("failed to set initial TIOCSWINSZ"); - goto close_fds; - } - - /* Need to register *very* early (before the first "goto err"), to - * ensure term_destroy() doesn't unref a key-binding we haven't - * yet ref:d */ - key_binding_new_for_conf(wayl->key_binding_manager, wayl, conf); - - int ptmx_flags; - if ((ptmx_flags = fcntl(ptmx, F_GETFL)) < 0 || - fcntl(ptmx, F_SETFL, ptmx_flags | O_NONBLOCK) < 0) - { - LOG_ERRNO("failed to configure ptmx as non-blocking"); - goto err; - } - - /* - * Enable all FDM callbackes *except* ptmx - we can't do that - * until the window has been 'configured' since we don't have a - * size (and thus no grid) before then. - */ - - if (!fdm_add(fdm, flash_fd, EPOLLIN, &fdm_flash, term) || - !fdm_add(fdm, delay_lower_fd, EPOLLIN, &fdm_delayed_render, term) || - !fdm_add(fdm, delay_upper_fd, EPOLLIN, &fdm_delayed_render, term) || - !fdm_add(fdm, app_sync_updates_fd, EPOLLIN, &fdm_app_sync_updates_timeout, term) || - !fdm_add(fdm, title_update_fd, EPOLLIN, &fdm_title_update_timeout, term) || - !fdm_add(fdm, icon_update_fd, EPOLLIN, &fdm_icon_update_timeout, term) || - !fdm_add(fdm, app_id_update_fd, EPOLLIN, &fdm_app_id_update_timeout, term)) - { - goto err; - } - - const enum shm_bit_depth desired_bit_depth = - conf->tweak.surface_bit_depth == SHM_BITS_AUTO - ? wayl_do_linear_blending(wayl, conf) ? SHM_BITS_16 : SHM_BITS_8 - : conf->tweak.surface_bit_depth; - - const struct color_theme *theme = NULL; - switch (conf->initial_color_theme) { - case COLOR_THEME_DARK: theme = &conf->colors_dark; break; - case COLOR_THEME_LIGHT: theme = &conf->colors_light; break; - case COLOR_THEME_1: BUG("COLOR_THEME_1 should not be used"); break; - case COLOR_THEME_2: BUG("COLOR_THEME_2 should not be used"); break; - } - - /* Initialize configure-based terminal attributes */ - *term = (struct terminal) { - .fdm = fdm, - .reaper = reaper, - .conf = conf, - .slave = -1, - .ptmx = ptmx, - .ptmx_buffers = tll_init(), - .ptmx_paste_buffers = tll_init(), - .font_sizes = { - xmalloc(sizeof(term->font_sizes[0][0]) * conf->fonts[0].count), - xmalloc(sizeof(term->font_sizes[1][0]) * conf->fonts[1].count), - xmalloc(sizeof(term->font_sizes[2][0]) * conf->fonts[2].count), - xmalloc(sizeof(term->font_sizes[3][0]) * conf->fonts[3].count), - }, - .font_dpi = 0., - .font_dpi_before_unmap = -1., - .font_subpixel = (theme->alpha == 0xffff /* Can't do subpixel rendering on transparent background */ - ? FCFT_SUBPIXEL_DEFAULT - : FCFT_SUBPIXEL_NONE), - .cursor_keys_mode = CURSOR_KEYS_NORMAL, - .keypad_keys_mode = KEYPAD_NUMERICAL, - .reverse_wrap = true, - .auto_margin = true, - .window_title_stack = tll_init(), - .scale = 1., - .scale_before_unmap = -1, - .flash = {.fd = flash_fd}, - .blink = {.fd = -1}, - .vt = { - .state = 0, /* STATE_GROUND */ - }, - .colors = { - .fg = theme->fg, - .bg = theme->bg, - .alpha = theme->alpha, - .cursor_fg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.text, - .cursor_bg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.cursor, - .selection_fg = theme->selection_fg, - .selection_bg = theme->selection_bg, - .active_theme = conf->initial_color_theme, - }, - .color_stack = { - .stack = NULL, - .size = 0, - .idx = 0, - }, - .origin = ORIGIN_ABSOLUTE, - .cursor_style = conf->cursor.style, - .cursor_blink = { - .decset = false, - .deccsusr = conf->cursor.blink.enabled, - .state = CURSOR_BLINK_ON, - .fd = -1, - }, - .selection = { - .coords = { - .start = {-1, -1}, - .end = {-1, -1}, - }, - .pivot = { - .start = {-1, -1}, - .end = {-1, -1}, - }, - .auto_scroll = { - .fd = -1, - }, - }, - .normal = {.scroll_damage = tll_init(), .sixel_images = tll_init()}, - .alt = {.scroll_damage = tll_init(), .sixel_images = tll_init()}, - .grid = &term->normal, - .composed = NULL, - .alt_scrolling = conf->mouse.alternate_scroll_mode, - .meta = { - .esc_prefix = true, - .eight_bit = true, - }, - .num_lock_modifier = true, - .bell_action_enabled = true, - .tab_stops = tll_init(), - .wl = wayl, - .render = { - .chains = { - .grid = shm_chain_new(wayl, true, 1 + conf->render_worker_count, - desired_bit_depth, &render_buffer_release_callback, term), - .search = shm_chain_new(wayl, false, 1 ,desired_bit_depth, NULL, NULL), - .scrollback_indicator = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), - .render_timer = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), - .url = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), - .csd = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), - .overlay = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), - .tab_bar = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), - .tab_overview = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), - }, - .scrollback_lines = conf->scrollback.lines, - .app_sync_updates.timer_fd = app_sync_updates_fd, - .title = { - .timer_fd = title_update_fd, - }, - .icon = { - .timer_fd = icon_update_fd, - }, - .app_id = { - .timer_fd = app_id_update_fd, - }, - .workers = { - .count = conf->render_worker_count, - .queue = tll_init(), - }, - }, - .delayed_render_timer = { - .is_armed = false, - .lower_fd = delay_lower_fd, - .upper_fd = delay_upper_fd, - }, - .sixel = { - .scrolling = true, - .use_private_palette = true, - .palette_size = SIXEL_MAX_COLORS, - .max_width = SIXEL_MAX_WIDTH, - .max_height = SIXEL_MAX_HEIGHT, - }, - .shutdown = { - .terminate_timeout_fd = -1, - .cb = shutdown_cb, - .cb_data = shutdown_data, - }, - .foot_exe = xstrdup(foot_exe), - .cwd = xstrdup(cwd), - .grapheme_shaping = conf->tweak.grapheme_shaping, -#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED - .ime_enabled = true, -#endif - .active_notifications = tll_init(), - }; - - pixman_region32_init(&term->render.last_overlay_clip); - - term_update_ascii_printer(term); - memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); - - for (size_t i = 0; i < 4; i++) { - const struct config_font_list *font_list = &conf->fonts[i]; - for (size_t j = 0; j < font_list->count; j++) { - const struct config_font *font = &font_list->arr[j]; - term->font_sizes[i][j] = (struct config_font){ - .pt_size = font->pt_size, .px_size = font->px_size}; - } - } - - for (size_t i = 0; i < ALEN(term->notification_icons); i++) { - term->notification_icons[i].tmp_file_fd = -1; - } - - add_utmp_record(conf, reaper, ptmx); - - if (!pty_path) { - /* Start the slave/client */ - if ((term->slave = slave_spawn( - term->ptmx, argc, term->cwd, argv, envp, &conf->env_vars, - conf->term, conf->shell, conf->login_shell, - &conf->notifications)) == -1) - { - goto err; - } - - reaper_add(term->reaper, term->slave, &fdm_client_terminated, term); - } - - /* Guess scale; we're not mapped yet, so we don't know on which - * output we'll be. Use scaling factor from first monitor */ - xassert(tll_length(term->wl->monitors) > 0); - term->scale = tll_front(term->wl->monitors).scale; - - /* Initialize the Wayland window backend */ - if ((term->window = wayl_win_init(term, token)) == NULL) - goto err; - - /* Load fonts */ - if (!term_font_dpi_changed(term, 0.)) - goto err; - - term->font_subpixel = get_font_subpixel(term); - - term_set_window_title(term, conf->title); - - /* Let the Wayland backend know we exist */ - tll_push_back(wayl->terms, term); - - switch (conf->startup_mode) { - case STARTUP_WINDOWED: - break; - - case STARTUP_MAXIMIZED: - xdg_toplevel_set_maximized(term->window->xdg_toplevel); - break; - - case STARTUP_FULLSCREEN: - xdg_toplevel_set_fullscreen(term->window->xdg_toplevel, NULL); - break; - } - - if (!initialize_render_workers(term)) - goto err; - - return term; - -err: - term->shutdown.in_progress = true; - term_destroy(term); + struct terminal *term = malloc(sizeof(*term)); + if (unlikely(term == NULL)) { + LOG_ERRNO("malloc() failed"); return NULL; + } -close_fds: - close(ptmx); - fdm_del(fdm, flash_fd); - fdm_del(fdm, delay_lower_fd); - fdm_del(fdm, delay_upper_fd); - fdm_del(fdm, app_sync_updates_fd); - fdm_del(fdm, title_update_fd); - fdm_del(fdm, icon_update_fd); - fdm_del(fdm, app_id_update_fd); + ptmx = + pty_path ? open(pty_path, PTY_OPEN_FLAGS) : posix_openpt(PTY_OPEN_FLAGS); + if (ptmx < 0) { + LOG_ERRNO("failed to open PTY"); + goto close_fds; + } + if ((flash_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < + 0) { + LOG_ERRNO("failed to create flash timer FD"); + goto close_fds; + } + if ((delay_lower_fd = + timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || + (delay_upper_fd = + timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) { + LOG_ERRNO("failed to create delayed rendering timer FDs"); + goto close_fds; + } - free(term); - return NULL; -} + if ((app_sync_updates_fd = + timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) { + LOG_ERRNO("failed to create application synchronized updates timer FD"); + goto close_fds; + } -struct terminal * -term_tab_new(struct terminal *primary, - int argc, char *const *argv, const char *const *envp, - void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data) -{ - struct wl_window *win = primary->window; + if ((title_update_fd = + timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) { + LOG_ERRNO("failed to create title update throttle timer FD"); + goto close_fds; + } - if (win->tab_count >= TAB_MAX) { - LOG_ERR("maximum number of tabs (%d) reached", TAB_MAX); - return NULL; - } + if ((icon_update_fd = + timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) { + LOG_ERRNO("failed to create icon update throttle timer FD"); + goto close_fds; + } - const struct config *conf = primary->conf; - struct fdm *fdm = primary->fdm; - struct reaper *reaper = primary->reaper; - struct wayland *wayl = primary->wl; + if ((app_id_update_fd = + timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) { + LOG_ERRNO("failed to create app ID update throttle timer FD"); + goto close_fds; + } - int ptmx = -1; - int flash_fd = -1; - int delay_lower_fd = -1; - int delay_upper_fd = -1; - int app_sync_updates_fd = -1; - int title_update_fd = -1; - int icon_update_fd = -1; - int app_id_update_fd = -1; + if (ioctl(ptmx, (unsigned int)TIOCSWINSZ, + &(struct winsize){.ws_row = 24, .ws_col = 80}) < 0) { + LOG_ERRNO("failed to set initial TIOCSWINSZ"); + goto close_fds; + } - struct terminal *term = malloc(sizeof(*term)); - if (unlikely(term == NULL)) { - LOG_ERRNO("malloc() failed"); - return NULL; - } + /* Need to register *very* early (before the first "goto err"), to + * ensure term_destroy() doesn't unref a key-binding we haven't + * yet ref:d */ + key_binding_new_for_conf(wayl->key_binding_manager, wayl, conf); - ptmx = posix_openpt(PTY_OPEN_FLAGS); - if (ptmx < 0) { - LOG_ERRNO("failed to open PTY for new tab"); - goto close_fds; - } - if ((flash_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || - (delay_lower_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || - (delay_upper_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || - (app_sync_updates_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || - (title_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || - (icon_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || - (app_id_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) - { - LOG_ERRNO("failed to create timer FDs for new tab"); - goto close_fds; - } + int ptmx_flags; + if ((ptmx_flags = fcntl(ptmx, F_GETFL)) < 0 || + fcntl(ptmx, F_SETFL, ptmx_flags | O_NONBLOCK) < 0) { + LOG_ERRNO("failed to configure ptmx as non-blocking"); + goto err; + } - if (ioctl(ptmx, (unsigned int)TIOCSWINSZ, - &(struct winsize){ - .ws_row = (unsigned short)primary->rows, - .ws_col = (unsigned short)primary->cols}) < 0) - { - LOG_ERRNO("failed to set TIOCSWINSZ for new tab"); - goto close_fds; - } + /* + * Enable all FDM callbackes *except* ptmx - we can't do that + * until the window has been 'configured' since we don't have a + * size (and thus no grid) before then. + */ - key_binding_new_for_conf(wayl->key_binding_manager, wayl, conf); + if (!fdm_add(fdm, flash_fd, EPOLLIN, &fdm_flash, term) || + !fdm_add(fdm, delay_lower_fd, EPOLLIN, &fdm_delayed_render, term) || + !fdm_add(fdm, delay_upper_fd, EPOLLIN, &fdm_delayed_render, term) || + !fdm_add(fdm, app_sync_updates_fd, EPOLLIN, &fdm_app_sync_updates_timeout, + term) || + !fdm_add(fdm, title_update_fd, EPOLLIN, &fdm_title_update_timeout, + term) || + !fdm_add(fdm, icon_update_fd, EPOLLIN, &fdm_icon_update_timeout, term) || + !fdm_add(fdm, app_id_update_fd, EPOLLIN, &fdm_app_id_update_timeout, + term)) { + goto err; + } - int ptmx_flags; - if ((ptmx_flags = fcntl(ptmx, F_GETFL)) < 0 || - fcntl(ptmx, F_SETFL, ptmx_flags | O_NONBLOCK) < 0) - { - LOG_ERRNO("failed to configure ptmx as non-blocking"); - goto err; - } + const enum shm_bit_depth desired_bit_depth = + conf->tweak.surface_bit_depth == SHM_BITS_AUTO + ? wayl_do_linear_blending(wayl, conf) ? SHM_BITS_16 : SHM_BITS_8 + : conf->tweak.surface_bit_depth; - const enum shm_bit_depth desired_bit_depth = - conf->tweak.surface_bit_depth == SHM_BITS_AUTO - ? wayl_do_linear_blending(wayl, conf) ? SHM_BITS_16 : SHM_BITS_8 - : conf->tweak.surface_bit_depth; + const struct color_theme *theme = NULL; + switch (conf->initial_color_theme) { + case COLOR_THEME_DARK: + theme = &conf->colors_dark; + break; + case COLOR_THEME_LIGHT: + theme = &conf->colors_light; + break; + case COLOR_THEME_1: + BUG("COLOR_THEME_1 should not be used"); + break; + case COLOR_THEME_2: + BUG("COLOR_THEME_2 should not be used"); + break; + } - const struct color_theme *theme = NULL; - switch (conf->initial_color_theme) { - case COLOR_THEME_DARK: theme = &conf->colors_dark; break; - case COLOR_THEME_LIGHT: theme = &conf->colors_light; break; - case COLOR_THEME_1: BUG("COLOR_THEME_1 should not be used"); break; - case COLOR_THEME_2: BUG("COLOR_THEME_2 should not be used"); break; - } - - *term = (struct terminal){ - .fdm = fdm, - .reaper = reaper, - .conf = conf, - .is_tab = true, - .slave = -1, - .ptmx = ptmx, - .ptmx_buffers = tll_init(), - .ptmx_paste_buffers = tll_init(), - .font_sizes = { - xmalloc(sizeof(term->font_sizes[0][0]) * conf->fonts[0].count), - xmalloc(sizeof(term->font_sizes[1][0]) * conf->fonts[1].count), - xmalloc(sizeof(term->font_sizes[2][0]) * conf->fonts[2].count), - xmalloc(sizeof(term->font_sizes[3][0]) * conf->fonts[3].count), - }, - .font_dpi = 0., - .font_dpi_before_unmap = -1., - .font_subpixel = (theme->alpha == 0xffff - ? FCFT_SUBPIXEL_DEFAULT - : FCFT_SUBPIXEL_NONE), - .cursor_keys_mode = CURSOR_KEYS_NORMAL, - .keypad_keys_mode = KEYPAD_NUMERICAL, - .reverse_wrap = true, - .auto_margin = true, - .window_title_stack = tll_init(), - .scale = primary->scale, - .scale_before_unmap = -1, - .flash = {.fd = flash_fd}, - .blink = {.fd = -1}, - .vt = {.state = 0}, - .colors = { - .fg = theme->fg, - .bg = theme->bg, - .alpha = theme->alpha, - .cursor_fg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.text, - .cursor_bg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.cursor, - .selection_fg = theme->selection_fg, - .selection_bg = theme->selection_bg, - .active_theme = conf->initial_color_theme, - }, - .color_stack = {.stack = NULL, .size = 0, .idx = 0}, - .origin = ORIGIN_ABSOLUTE, - .cursor_style = conf->cursor.style, - .cursor_blink = { - .decset = false, - .deccsusr = conf->cursor.blink.enabled, - .state = CURSOR_BLINK_ON, - .fd = -1, - }, - .selection = { - .coords = {.start = {-1, -1}, .end = {-1, -1}}, - .pivot = {.start = {-1, -1}, .end = {-1, -1}}, - .auto_scroll = {.fd = -1}, - }, - .normal = {.scroll_damage = tll_init(), .sixel_images = tll_init()}, - .alt = {.scroll_damage = tll_init(), .sixel_images = tll_init()}, - .grid = &term->normal, - .composed = NULL, - .alt_scrolling = conf->mouse.alternate_scroll_mode, - .meta = {.esc_prefix = true, .eight_bit = true}, - .num_lock_modifier = true, - .bell_action_enabled = true, - .tab_stops = tll_init(), - .wl = wayl, - .window = win, - .render = { - .chains = { - .grid = shm_chain_new(wayl, true, 1 + conf->render_worker_count, - desired_bit_depth, &render_buffer_release_callback, term), - .search = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), - .scrollback_indicator = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), - .render_timer = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), - .url = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), - .csd = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), - .overlay = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), - .tab_bar = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), - .tab_overview = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), - }, - .scrollback_lines = conf->scrollback.lines, - .app_sync_updates.timer_fd = app_sync_updates_fd, - .title = {.timer_fd = title_update_fd}, - .icon = {.timer_fd = icon_update_fd}, - .app_id = {.timer_fd = app_id_update_fd}, - .workers = { - .count = conf->render_worker_count, - .queue = tll_init(), - }, - }, - .delayed_render_timer = { - .is_armed = false, - .lower_fd = delay_lower_fd, - .upper_fd = delay_upper_fd, - }, - .sixel = { - .scrolling = true, - .use_private_palette = true, - .palette_size = SIXEL_MAX_COLORS, - .max_width = SIXEL_MAX_WIDTH, - .max_height = SIXEL_MAX_HEIGHT, - }, - .shutdown = { - .terminate_timeout_fd = -1, - .cb = shutdown_cb, - .cb_data = shutdown_data, - }, - .foot_exe = xstrdup(primary->foot_exe), - .cwd = xstrdup( - conf->tabs.inherit_cwd - ? win->term->cwd - : (getenv("HOME") != NULL ? getenv("HOME") : "/")), - .grapheme_shaping = conf->tweak.grapheme_shaping, + /* Initialize configure-based terminal attributes */ + *term = (struct terminal){ + .fdm = fdm, + .reaper = reaper, + .conf = conf, + .slave = -1, + .ptmx = ptmx, + .ptmx_buffers = tll_init(), + .ptmx_paste_buffers = tll_init(), + .font_sizes = + { + xmalloc(sizeof(term->font_sizes[0][0]) * conf->fonts[0].count), + xmalloc(sizeof(term->font_sizes[1][0]) * conf->fonts[1].count), + xmalloc(sizeof(term->font_sizes[2][0]) * conf->fonts[2].count), + xmalloc(sizeof(term->font_sizes[3][0]) * conf->fonts[3].count), + }, + .font_dpi = 0., + .font_dpi_before_unmap = -1., + .font_subpixel = (theme->alpha == 0xffff /* Can't do subpixel rendering on + transparent background */ + ? FCFT_SUBPIXEL_DEFAULT + : FCFT_SUBPIXEL_NONE), + .cursor_keys_mode = CURSOR_KEYS_NORMAL, + .keypad_keys_mode = KEYPAD_NUMERICAL, + .reverse_wrap = true, + .auto_margin = true, + .window_title_stack = tll_init(), + .scale = 1., + .scale_before_unmap = -1, + .flash = {.fd = flash_fd}, + .blink = {.fd = -1}, + .vt = + { + .state = 0, /* STATE_GROUND */ + }, + .colors = + { + .fg = theme->fg, + .bg = theme->bg, + .alpha = theme->alpha, + .cursor_fg = (theme->use_custom.cursor ? 1u << 31 : 0) | + theme->cursor.text, + .cursor_bg = (theme->use_custom.cursor ? 1u << 31 : 0) | + theme->cursor.cursor, + .selection_fg = theme->selection_fg, + .selection_bg = theme->selection_bg, + .active_theme = conf->initial_color_theme, + }, + .color_stack = + { + .stack = NULL, + .size = 0, + .idx = 0, + }, + .origin = ORIGIN_ABSOLUTE, + .cursor_style = conf->cursor.style, + .cursor_blink = + { + .decset = false, + .deccsusr = conf->cursor.blink.enabled, + .state = CURSOR_BLINK_ON, + .fd = -1, + }, + .selection = + { + .coords = + { + .start = {-1, -1}, + .end = {-1, -1}, + }, + .pivot = + { + .start = {-1, -1}, + .end = {-1, -1}, + }, + .auto_scroll = + { + .fd = -1, + }, + }, + .normal = {.scroll_damage = tll_init(), .sixel_images = tll_init()}, + .alt = {.scroll_damage = tll_init(), .sixel_images = tll_init()}, + .grid = &term->normal, + .composed = NULL, + .alt_scrolling = conf->mouse.alternate_scroll_mode, + .meta = + { + .esc_prefix = true, + .eight_bit = true, + }, + .num_lock_modifier = true, + .bell_action_enabled = true, + .tab_stops = tll_init(), + .wl = wayl, + .render = + { + .chains = + { + .grid = shm_chain_new( + wayl, true, 1 + conf->render_worker_count, + desired_bit_depth, &render_buffer_release_callback, + term), + .search = shm_chain_new(wayl, false, 1, desired_bit_depth, + NULL, NULL), + .scrollback_indicator = shm_chain_new( + wayl, false, 1, desired_bit_depth, NULL, NULL), + .render_timer = shm_chain_new( + wayl, false, 1, desired_bit_depth, NULL, NULL), + .url = shm_chain_new(wayl, false, 1, desired_bit_depth, + NULL, NULL), + .csd = shm_chain_new(wayl, false, 1, desired_bit_depth, + NULL, NULL), + .overlay = shm_chain_new(wayl, false, 1, + desired_bit_depth, NULL, NULL), + .tab_bar = shm_chain_new(wayl, false, 1, + desired_bit_depth, NULL, NULL), + .tab_overview = shm_chain_new( + wayl, false, 1, desired_bit_depth, NULL, NULL), + }, + .scrollback_lines = conf->scrollback.lines, + .app_sync_updates.timer_fd = app_sync_updates_fd, + .title = + { + .timer_fd = title_update_fd, + }, + .icon = + { + .timer_fd = icon_update_fd, + }, + .app_id = + { + .timer_fd = app_id_update_fd, + }, + .workers = + { + .count = conf->render_worker_count, + .queue = tll_init(), + }, + }, + .delayed_render_timer = + { + .is_armed = false, + .lower_fd = delay_lower_fd, + .upper_fd = delay_upper_fd, + }, + .sixel = + { + .scrolling = true, + .use_private_palette = true, + .palette_size = SIXEL_MAX_COLORS, + .max_width = SIXEL_MAX_WIDTH, + .max_height = SIXEL_MAX_HEIGHT, + }, + .shutdown = + { + .terminate_timeout_fd = -1, + .cb = shutdown_cb, + .cb_data = shutdown_data, + }, + .foot_exe = xstrdup(foot_exe), + .cwd = xstrdup(cwd), + .grapheme_shaping = conf->tweak.grapheme_shaping, #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED - .ime_enabled = true, + .ime_enabled = true, #endif - .active_notifications = tll_init(), - }; + .active_notifications = tll_init(), + }; - pixman_region32_init(&term->render.last_overlay_clip); - term_update_ascii_printer(term); - memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); + pixman_region32_init(&term->render.last_overlay_clip); - /* Inherit font sizes from primary so the new tab matches its zoom level */ - for (size_t i = 0; i < 4; i++) { - const struct config_font_list *font_list = &conf->fonts[i]; - for (size_t j = 0; j < font_list->count; j++) - term->font_sizes[i][j] = primary->font_sizes[i][j]; + term_update_ascii_printer(term); + memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); + + for (size_t i = 0; i < 4; i++) { + const struct config_font_list *font_list = &conf->fonts[i]; + for (size_t j = 0; j < font_list->count; j++) { + const struct config_font *font = &font_list->arr[j]; + term->font_sizes[i][j] = (struct config_font){.pt_size = font->pt_size, + .px_size = font->px_size}; } + } - for (size_t i = 0; i < ALEN(term->notification_icons); i++) - term->notification_icons[i].tmp_file_fd = -1; + for (size_t i = 0; i < ALEN(term->notification_icons); i++) { + term->notification_icons[i].tmp_file_fd = -1; + } - if (!fdm_add(fdm, flash_fd, EPOLLIN, &fdm_flash, term) || - !fdm_add(fdm, delay_lower_fd, EPOLLIN, &fdm_delayed_render, term) || - !fdm_add(fdm, delay_upper_fd, EPOLLIN, &fdm_delayed_render, term) || - !fdm_add(fdm, app_sync_updates_fd, EPOLLIN, &fdm_app_sync_updates_timeout, term) || - !fdm_add(fdm, title_update_fd, EPOLLIN, &fdm_title_update_timeout, term) || - !fdm_add(fdm, icon_update_fd, EPOLLIN, &fdm_icon_update_timeout, term) || - !fdm_add(fdm, app_id_update_fd, EPOLLIN, &fdm_app_id_update_timeout, term)) - { - goto err; - } + add_utmp_record(conf, reaper, ptmx); - add_utmp_record(conf, reaper, ptmx); - - if ((term->slave = slave_spawn( - term->ptmx, argc, term->cwd, argv, envp, &conf->env_vars, - conf->term, conf->shell, conf->login_shell, - &conf->notifications)) == -1) - { - goto err; + if (!pty_path) { + /* Start the slave/client */ + if ((term->slave = slave_spawn(term->ptmx, argc, term->cwd, argv, envp, + &conf->env_vars, conf->term, conf->shell, + conf->login_shell, &conf->notifications)) == + -1) { + goto err; } reaper_add(term->reaper, term->slave, &fdm_client_terminated, term); + } - /* Load fonts (uses primary's scale) */ - if (!term_font_dpi_changed(term, 0.)) - goto err; + /* Guess scale; we're not mapped yet, so we don't know on which + * output we'll be. Use scaling factor from first monitor */ + xassert(tll_length(term->wl->monitors) > 0); + term->scale = tll_front(term->wl->monitors).scale; - term->font_subpixel = get_font_subpixel(term); + /* Initialize the Wayland window backend */ + if ((term->window = wayl_win_init(term, token)) == NULL) + goto err; - /* Resize grid to match current window dimensions. cell_width and - * cell_height were already computed by term_set_fonts above, using the - * inherited font_sizes — inheriting primary's cell geometry would mask - * that, since primary may be mid-zoom or mid-reload. */ - term->width = primary->width; - term->height = primary->height; + /* Load fonts */ + if (!term_font_dpi_changed(term, 0.)) + goto err; - tll_push_back(wayl->terms, term); + term->font_subpixel = get_font_subpixel(term); - /* Register in window's tab list */ - win->tabs[win->tab_count++] = term; + term_set_window_title(term, conf->title); - /* Enable ptmx I/O now (window is already configured) */ - fdm_add(term->fdm, term->ptmx, EPOLLIN, &fdm_ptmx, term); + /* Let the Wayland backend know we exist */ + tll_push_back(wayl->terms, term); - if (!initialize_render_workers(term)) - goto err; + switch (conf->startup_mode) { + case STARTUP_WINDOWED: + break; - /* Switch to the new tab */ - term_tab_switch(win, win->tab_count - 1); + case STARTUP_MAXIMIZED: + xdg_toplevel_set_maximized(term->window->xdg_toplevel); + break; - return term; + case STARTUP_FULLSCREEN: + xdg_toplevel_set_fullscreen(term->window->xdg_toplevel, NULL); + break; + } + + if (!initialize_render_workers(term)) + goto err; + + return term; err: - term->shutdown.in_progress = true; - term_destroy(term); - return NULL; + term->shutdown.in_progress = true; + term_destroy(term); + return NULL; close_fds: - if (ptmx >= 0) close(ptmx); - fdm_del(fdm, flash_fd); - fdm_del(fdm, delay_lower_fd); - fdm_del(fdm, delay_upper_fd); - fdm_del(fdm, app_sync_updates_fd); - fdm_del(fdm, title_update_fd); - fdm_del(fdm, icon_update_fd); - fdm_del(fdm, app_id_update_fd); - free(term); + close(ptmx); + fdm_del(fdm, flash_fd); + fdm_del(fdm, delay_lower_fd); + fdm_del(fdm, delay_upper_fd); + fdm_del(fdm, app_sync_updates_fd); + fdm_del(fdm, title_update_fd); + fdm_del(fdm, icon_update_fd); + fdm_del(fdm, app_id_update_fd); + + free(term); + return NULL; +} + +struct terminal *term_tab_new(struct terminal *primary, int argc, + char *const *argv, const char *const *envp, + void (*shutdown_cb)(void *data, int exit_code), + void *shutdown_data) { + return term_tab_new_with_cwd(primary, NULL, argc, argv, envp, shutdown_cb, + shutdown_data); +} + +struct terminal *term_tab_new_with_cwd(struct terminal *primary, + const char *override_cwd, int argc, + char *const *argv, + const char *const *envp, + void (*shutdown_cb)(void *data, + int exit_code), + void *shutdown_data) { + struct wl_window *win = primary->window; + + if (win->tab_count >= TAB_MAX) { + LOG_ERR("maximum number of tabs (%d) reached", TAB_MAX); return NULL; + } + + const struct config *conf = primary->conf; + struct fdm *fdm = primary->fdm; + struct reaper *reaper = primary->reaper; + struct wayland *wayl = primary->wl; + + int ptmx = -1; + int flash_fd = -1; + int delay_lower_fd = -1; + int delay_upper_fd = -1; + int app_sync_updates_fd = -1; + int title_update_fd = -1; + int icon_update_fd = -1; + int app_id_update_fd = -1; + + struct terminal *term = malloc(sizeof(*term)); + if (unlikely(term == NULL)) { + LOG_ERRNO("malloc() failed"); + return NULL; + } + + ptmx = posix_openpt(PTY_OPEN_FLAGS); + if (ptmx < 0) { + LOG_ERRNO("failed to open PTY for new tab"); + goto close_fds; + } + if ((flash_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < + 0 || + (delay_lower_fd = + timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || + (delay_upper_fd = + timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || + (app_sync_updates_fd = + timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || + (title_update_fd = + timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || + (icon_update_fd = + timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || + (app_id_update_fd = + timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) { + LOG_ERRNO("failed to create timer FDs for new tab"); + goto close_fds; + } + + if (ioctl(ptmx, (unsigned int)TIOCSWINSZ, + &(struct winsize){.ws_row = (unsigned short)primary->rows, + .ws_col = (unsigned short)primary->cols}) < 0) { + LOG_ERRNO("failed to set TIOCSWINSZ for new tab"); + goto close_fds; + } + + key_binding_new_for_conf(wayl->key_binding_manager, wayl, conf); + + int ptmx_flags; + if ((ptmx_flags = fcntl(ptmx, F_GETFL)) < 0 || + fcntl(ptmx, F_SETFL, ptmx_flags | O_NONBLOCK) < 0) { + LOG_ERRNO("failed to configure ptmx as non-blocking"); + goto err; + } + + const enum shm_bit_depth desired_bit_depth = + conf->tweak.surface_bit_depth == SHM_BITS_AUTO + ? wayl_do_linear_blending(wayl, conf) ? SHM_BITS_16 : SHM_BITS_8 + : conf->tweak.surface_bit_depth; + + const struct color_theme *theme = NULL; + switch (conf->initial_color_theme) { + case COLOR_THEME_DARK: + theme = &conf->colors_dark; + break; + case COLOR_THEME_LIGHT: + theme = &conf->colors_light; + break; + case COLOR_THEME_1: + BUG("COLOR_THEME_1 should not be used"); + break; + case COLOR_THEME_2: + BUG("COLOR_THEME_2 should not be used"); + break; + } + + *term = (struct terminal){ + .fdm = fdm, + .reaper = reaper, + .conf = conf, + .is_tab = true, + .slave = -1, + .ptmx = ptmx, + .ptmx_buffers = tll_init(), + .ptmx_paste_buffers = tll_init(), + .font_sizes = + { + xmalloc(sizeof(term->font_sizes[0][0]) * conf->fonts[0].count), + xmalloc(sizeof(term->font_sizes[1][0]) * conf->fonts[1].count), + xmalloc(sizeof(term->font_sizes[2][0]) * conf->fonts[2].count), + xmalloc(sizeof(term->font_sizes[3][0]) * conf->fonts[3].count), + }, + .font_dpi = 0., + .font_dpi_before_unmap = -1., + .font_subpixel = + (theme->alpha == 0xffff ? FCFT_SUBPIXEL_DEFAULT : FCFT_SUBPIXEL_NONE), + .cursor_keys_mode = CURSOR_KEYS_NORMAL, + .keypad_keys_mode = KEYPAD_NUMERICAL, + .reverse_wrap = true, + .auto_margin = true, + .window_title_stack = tll_init(), + .scale = primary->scale, + .scale_before_unmap = -1, + .flash = {.fd = flash_fd}, + .blink = {.fd = -1}, + .vt = {.state = 0}, + .colors = + { + .fg = theme->fg, + .bg = theme->bg, + .alpha = theme->alpha, + .cursor_fg = (theme->use_custom.cursor ? 1u << 31 : 0) | + theme->cursor.text, + .cursor_bg = (theme->use_custom.cursor ? 1u << 31 : 0) | + theme->cursor.cursor, + .selection_fg = theme->selection_fg, + .selection_bg = theme->selection_bg, + .active_theme = conf->initial_color_theme, + }, + .color_stack = {.stack = NULL, .size = 0, .idx = 0}, + .origin = ORIGIN_ABSOLUTE, + .cursor_style = conf->cursor.style, + .cursor_blink = + { + .decset = false, + .deccsusr = conf->cursor.blink.enabled, + .state = CURSOR_BLINK_ON, + .fd = -1, + }, + .selection = + { + .coords = {.start = {-1, -1}, .end = {-1, -1}}, + .pivot = {.start = {-1, -1}, .end = {-1, -1}}, + .auto_scroll = {.fd = -1}, + }, + .normal = {.scroll_damage = tll_init(), .sixel_images = tll_init()}, + .alt = {.scroll_damage = tll_init(), .sixel_images = tll_init()}, + .grid = &term->normal, + .composed = NULL, + .alt_scrolling = conf->mouse.alternate_scroll_mode, + .meta = {.esc_prefix = true, .eight_bit = true}, + .num_lock_modifier = true, + .bell_action_enabled = true, + .tab_stops = tll_init(), + .wl = wayl, + .window = win, + .render = + { + .chains = + { + .grid = shm_chain_new( + wayl, true, 1 + conf->render_worker_count, + desired_bit_depth, &render_buffer_release_callback, + term), + .search = shm_chain_new(wayl, false, 1, desired_bit_depth, + NULL, NULL), + .scrollback_indicator = shm_chain_new( + wayl, false, 1, desired_bit_depth, NULL, NULL), + .render_timer = shm_chain_new( + wayl, false, 1, desired_bit_depth, NULL, NULL), + .url = shm_chain_new(wayl, false, 1, desired_bit_depth, + NULL, NULL), + .csd = shm_chain_new(wayl, false, 1, desired_bit_depth, + NULL, NULL), + .overlay = shm_chain_new(wayl, false, 1, + desired_bit_depth, NULL, NULL), + .tab_bar = shm_chain_new(wayl, false, 1, + desired_bit_depth, NULL, NULL), + .tab_overview = shm_chain_new( + wayl, false, 1, desired_bit_depth, NULL, NULL), + }, + .scrollback_lines = conf->scrollback.lines, + .app_sync_updates.timer_fd = app_sync_updates_fd, + .title = {.timer_fd = title_update_fd}, + .icon = {.timer_fd = icon_update_fd}, + .app_id = {.timer_fd = app_id_update_fd}, + .workers = + { + .count = conf->render_worker_count, + .queue = tll_init(), + }, + }, + .delayed_render_timer = + { + .is_armed = false, + .lower_fd = delay_lower_fd, + .upper_fd = delay_upper_fd, + }, + .sixel = + { + .scrolling = true, + .use_private_palette = true, + .palette_size = SIXEL_MAX_COLORS, + .max_width = SIXEL_MAX_WIDTH, + .max_height = SIXEL_MAX_HEIGHT, + }, + .shutdown = + { + .terminate_timeout_fd = -1, + .cb = shutdown_cb, + .cb_data = shutdown_data, + }, + .foot_exe = xstrdup(primary->foot_exe), + .cwd = xstrdup(override_cwd != NULL + ? override_cwd + : (conf->tabs.inherit_cwd + ? win->term->cwd + : (getenv("HOME") != NULL ? getenv("HOME") + : "/"))), + .grapheme_shaping = conf->tweak.grapheme_shaping, +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + .ime_enabled = true, +#endif + .active_notifications = tll_init(), + }; + + pixman_region32_init(&term->render.last_overlay_clip); + term_update_ascii_printer(term); + memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); + + /* Inherit font sizes from primary so the new tab matches its zoom level */ + for (size_t i = 0; i < 4; i++) { + const struct config_font_list *font_list = &conf->fonts[i]; + for (size_t j = 0; j < font_list->count; j++) + term->font_sizes[i][j] = primary->font_sizes[i][j]; + } + + for (size_t i = 0; i < ALEN(term->notification_icons); i++) + term->notification_icons[i].tmp_file_fd = -1; + + if (!fdm_add(fdm, flash_fd, EPOLLIN, &fdm_flash, term) || + !fdm_add(fdm, delay_lower_fd, EPOLLIN, &fdm_delayed_render, term) || + !fdm_add(fdm, delay_upper_fd, EPOLLIN, &fdm_delayed_render, term) || + !fdm_add(fdm, app_sync_updates_fd, EPOLLIN, &fdm_app_sync_updates_timeout, + term) || + !fdm_add(fdm, title_update_fd, EPOLLIN, &fdm_title_update_timeout, + term) || + !fdm_add(fdm, icon_update_fd, EPOLLIN, &fdm_icon_update_timeout, term) || + !fdm_add(fdm, app_id_update_fd, EPOLLIN, &fdm_app_id_update_timeout, + term)) { + goto err; + } + + add_utmp_record(conf, reaper, ptmx); + + /* Initialize window title to the configured default; otherwise a freshly + * spawned child sending CSI 22 t (push title) sees a NULL title. */ + term_set_window_title(term, conf->title); + + if ((term->slave = slave_spawn( + term->ptmx, argc, term->cwd, argv, envp, &conf->env_vars, conf->term, + conf->shell, conf->login_shell, &conf->notifications)) == -1) { + goto err; + } + + reaper_add(term->reaper, term->slave, &fdm_client_terminated, term); + + /* Load fonts (uses primary's scale) */ + if (!term_font_dpi_changed(term, 0.)) + goto err; + + term->font_subpixel = get_font_subpixel(term); + + term->width = primary->width; + term->height = primary->height; + + tll_push_back(wayl->terms, term); + + /* Register in window's tab list */ + win->tabs[win->tab_count++] = term; + + /* Enable ptmx I/O now (window is already configured) */ + fdm_add(term->fdm, term->ptmx, EPOLLIN, &fdm_ptmx, term); + + if (!initialize_render_workers(term)) + goto err; + + /* Switch to the new tab */ + term_tab_switch(win, win->tab_count - 1); + + return term; + +err: + term->shutdown.in_progress = true; + term_destroy(term); + return NULL; + +close_fds: + if (ptmx >= 0) + close(ptmx); + fdm_del(fdm, flash_fd); + fdm_del(fdm, delay_lower_fd); + fdm_del(fdm, delay_upper_fd); + fdm_del(fdm, app_sync_updates_fd); + fdm_del(fdm, title_update_fd); + fdm_del(fdm, icon_update_fd); + fdm_del(fdm, app_id_update_fd); + free(term); + return NULL; } -void -term_tab_switch(struct wl_window *win, size_t idx) -{ - if (idx >= win->tab_count) - return; +void term_tab_switch(struct wl_window *win, size_t idx) { + if (idx >= win->tab_count) + return; - struct terminal *prev = win->term; - /* render_resize expects logical (unscaled) sizes; prev->width/height - * are physical pixels. Convert back to logical so render_resize's - * internal scale multiplication doesn't double-apply it. */ - int cur_width = (int)roundf(prev->width / prev->scale); - int cur_height = (int)roundf(prev->height / prev->scale); + struct terminal *prev = win->term; + /* render_resize expects logical (unscaled) sizes; prev->width/height + * are physical pixels. Convert back to logical so render_resize's + * internal scale multiplication doesn't double-apply it. */ + int cur_width = (int)roundf(prev->width / prev->scale); + int cur_height = (int)roundf(prev->height / prev->scale); - win->active_tab = idx; - win->term = win->tabs[idx]; + win->active_tab = idx; + win->term = win->tabs[idx]; - struct terminal *term = win->term; + struct terminal *term = win->term; - /* The newly active tab is being looked at — clear its unread flag */ - term->has_unread = false; + term->has_unread = false; - /* Inherit the surface kind so cursor updates work without a pointer re-enter */ - term->active_surface = prev->active_surface; + /* Inherit the surface kind so cursor updates work without a pointer re-enter + */ + term->active_surface = prev->active_surface; - /* Update keyboard and mouse focus for all seats looking at this window. - * Do this before term_kbd_focus_out(prev) so its guard (checking whether - * any seat still points to it) sees that no seat does. */ - tll_foreach(term->wl->seats, it) { - struct seat *seat = &it->item; - if (seat->kbd_focus != NULL && seat->kbd_focus->window == win) - seat->kbd_focus = term; - if (seat->mouse_focus != NULL && seat->mouse_focus->window == win) - seat->mouse_focus = term; + /* Update keyboard and mouse focus for all seats looking at this window. + * Do this before term_kbd_focus_out(prev) so its guard (checking whether + * any seat still points to it) sees that no seat does. */ + tll_foreach(term->wl->seats, it) { + struct seat *seat = &it->item; + if (seat->kbd_focus != NULL && seat->kbd_focus->window == win) + seat->kbd_focus = term; + if (seat->mouse_focus != NULL && seat->mouse_focus->window == win) + seat->mouse_focus = term; + } + + /* prev loses keyboard focus now that no seat points to it */ + term_kbd_focus_out(prev); + + bool has_kbd_focus = false; + tll_foreach(term->wl->seats, it) { + if (it->item.kbd_focus == term) { + has_kbd_focus = true; + break; } + } + if (has_kbd_focus) + term_visual_focus_in(term); + else + term_visual_focus_out(term); - /* prev loses keyboard focus now that no seat points to it */ - term_kbd_focus_out(prev); + const float old_scale = term->scale; + term_update_scale(term); + term_font_dpi_changed(term, old_scale); + term_font_subpixel_changed(term); - bool has_kbd_focus = false; - tll_foreach(term->wl->seats, it) { - if (it->item.kbd_focus == term) { - has_kbd_focus = true; - break; - } - } - if (has_kbd_focus) - term_visual_focus_in(term); - else - term_visual_focus_out(term); + /* Cancel the outgoing tab's in-flight frame_callback. Its listener data + * points at prev, so when it fires it services prev's empty pending bits + * and nothing gets drawn. Dropping it lets fdm_hook_refresh_pending_terminals + * take the immediate-render path for the new active tab. */ + if (win->frame_callback != NULL) { + wl_callback_destroy(win->frame_callback); + win->frame_callback = NULL; + } - /* Scale changes (fractional-scale-v1, preferred_buffer_scale, output - * enter/leave) only notify win->term — inactive tabs miss them. Catch - * the incoming tab up on scale, DPI, and font size before we resize. */ - const float old_scale = term->scale; - term_update_scale(term); - term_font_dpi_changed(term, old_scale); - term_font_subpixel_changed(term); + render_resize(term, cur_width, cur_height, RESIZE_FORCE); - /* Cancel the outgoing tab's in-flight frame_callback. Its listener data - * points at prev, so when it fires it services prev's empty pending bits - * and nothing gets drawn. Dropping it lets fdm_hook_refresh_pending_terminals - * take the immediate-render path for the new active tab. */ - if (win->frame_callback != NULL) { - wl_callback_destroy(win->frame_callback); - win->frame_callback = NULL; - } + if (has_kbd_focus) + term_kbd_focus_in(term); - /* Use current window dimensions, not the inactive tab's stale ones. - * render_resize allocates the grid rows — term_kbd_focus_in must come - * after this because cursor_refresh dereferences cur_row. */ - render_resize(term, cur_width, cur_height, RESIZE_FORCE); - - if (has_kbd_focus) - term_kbd_focus_in(term); - - render_refresh_csd(term); - render_refresh_tab_bar(term); - term_xcursor_update(term); + render_refresh_csd(term); + render_refresh_tab_bar(term); + term_xcursor_update(term); } -void -term_tab_close(struct terminal *term) -{ - term_shutdown(term); -} +void term_tab_close(struct terminal *term) { term_shutdown(term); } -size_t -term_tab_bar_hit_test(const struct terminal *term, int x, int y) -{ - const struct wl_window *win = term->window; - if (win->tab_bar.sub == NULL) - return SIZE_MAX; - - const size_t n = win->tab_layout.count; - if (n == 0) - return SIZE_MAX; - - if (y < win->tab_layout.y || y >= win->tab_layout.y + win->tab_layout.h) - return SIZE_MAX; - - for (size_t i = 0; i < n; i++) { - const int tx = win->tab_layout.xs[i]; - const int tw = win->tab_layout.ws[i]; - if (x >= tx && x < tx + tw) - return i; - } +size_t term_tab_bar_hit_test(const struct terminal *term, int x, int y) { + const struct wl_window *win = term->window; + if (win->tab_bar.sub == NULL) return SIZE_MAX; + + const size_t n = win->tab_layout.count; + if (n == 0) + return SIZE_MAX; + + if (y < win->tab_layout.y || y >= win->tab_layout.y + win->tab_layout.h) + return SIZE_MAX; + + for (size_t i = 0; i < n; i++) { + const int tx = win->tab_layout.xs[i]; + const int tw = win->tab_layout.ws[i]; + if (x >= tx && x < tx + tw) + return i; + } + return SIZE_MAX; } -void -term_window_configured(struct terminal *term) -{ - /* Enable ptmx FDM callback */ - if (!term->shutdown.in_progress) { - xassert(term->window->is_configured); - fdm_add(term->fdm, term->ptmx, EPOLLIN, &fdm_ptmx, term); +void term_window_configured(struct terminal *term) { + /* Enable ptmx FDM callback */ + if (!term->shutdown.in_progress) { + xassert(term->window->is_configured); + fdm_add(term->fdm, term->ptmx, EPOLLIN, &fdm_ptmx, term); - const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); - LOG_INFO("gamma-correct blending: %s", gamma_correct ? "enabled" : "disabled"); - } + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + LOG_INFO("gamma-correct blending: %s", + gamma_correct ? "enabled" : "disabled"); + } } /* @@ -1929,8 +1946,8 @@ term_window_configured(struct terminal *term) * * A foot instance can be terminated in two ways: * - * - the client application terminates (user types 'exit', or pressed C-d in the - * shell, etc) + * - the client application terminates (user types 'exit', or pressed C-d in + * the shell, etc) * - the foot window is closed * * Both variants need to trigger to "other" action. I.e. if the client @@ -1988,2470 +2005,2311 @@ term_window_configured(struct terminal *term) * waitpid(). */ -static void -shutdown_maybe_done(struct terminal *term) -{ - bool shutdown_done = - term->shutdown.fdm_done && term->shutdown.client_has_terminated; +static void shutdown_maybe_done(struct terminal *term) { + bool shutdown_done = + term->shutdown.fdm_done && term->shutdown.client_has_terminated; - LOG_DBG("fdm_done=%d, slave-has-been-reaped=%d --> %s", - term->shutdown.fdm_done, term->shutdown.client_has_terminated, - (shutdown_done - ? "shutdown done, calling term_destroy()" - : "no action")); + LOG_DBG( + "fdm_done=%d, slave-has-been-reaped=%d --> %s", term->shutdown.fdm_done, + term->shutdown.client_has_terminated, + (shutdown_done ? "shutdown done, calling term_destroy()" : "no action")); - if (!shutdown_done) - return; + if (!shutdown_done) + return; - void (*cb)(void *, int) = term->shutdown.cb; - void *cb_data = term->shutdown.cb_data; + void (*cb)(void *, int) = term->shutdown.cb; + void *cb_data = term->shutdown.cb_data; - int exit_code = term_destroy(term); - if (cb != NULL) - cb(cb_data, exit_code); + int exit_code = term_destroy(term); + if (cb != NULL) + cb(cb_data, exit_code); } -static void -fdm_client_terminated(struct reaper *reaper, pid_t pid, int status, void *data) -{ - struct terminal *term = data; - LOG_DBG("slave (PID=%u) died", pid); +static void fdm_client_terminated(struct reaper *reaper, pid_t pid, int status, + void *data) { + struct terminal *term = data; + LOG_DBG("slave (PID=%u) died", pid); - term->shutdown.client_has_terminated = true; - term->shutdown.exit_status = status; + term->shutdown.client_has_terminated = true; + term->shutdown.exit_status = status; - if (term->shutdown.terminate_timeout_fd >= 0) { - fdm_del(term->fdm, term->shutdown.terminate_timeout_fd); - term->shutdown.terminate_timeout_fd = -1; - } + if (term->shutdown.terminate_timeout_fd >= 0) { + fdm_del(term->fdm, term->shutdown.terminate_timeout_fd); + term->shutdown.terminate_timeout_fd = -1; + } - if (term->shutdown.in_progress) - shutdown_maybe_done(term); - else if (!term->conf->hold_at_exit) - term_shutdown(term); -} - -static bool -fdm_shutdown(struct fdm *fdm, int fd, int events, void *data) -{ - struct terminal *term = data; - - /* Kill the event FD */ - fdm_del(term->fdm, fd); - - struct wl_window *win = term->window; - - if (win != NULL && win->tab_count <= 1) { - /* - * Last (or only) tab — destroy the whole window. - * - * Normally we'd get unmapped when we destroy the Wayland - * surface above. However, it appears that under certain - * conditions, those events are deferred (for example, when - * a screen locker is active), and thus we can get here - * without having been unmapped. - */ - wayl_win_destroy(win); - term->window = NULL; - - struct wayland *wayl = term->wl; - tll_foreach(wayl->seats, it) { - if (it->item.kbd_focus == term) - it->item.kbd_focus = NULL; - if (it->item.mouse_focus == term) - it->item.mouse_focus = NULL; - } - } - /* - * Multi-tab case: leave term->window intact so term_destroy() can - * use its existing tab-removal and tab-switch logic. - */ - - term->shutdown.fdm_done = true; + if (term->shutdown.in_progress) shutdown_maybe_done(term); - return true; + else if (!term->conf->hold_at_exit) + term_shutdown(term); } -static bool -fdm_terminate_timeout(struct fdm *fdm, int fd, int events, void *data) -{ - uint64_t unused; - ssize_t bytes = read(fd, &unused, sizeof(unused)); - if (bytes < 0) { - LOG_ERRNO("failed to read from slave terminate timeout FD"); - return false; +static bool fdm_shutdown(struct fdm *fdm, int fd, int events, void *data) { + struct terminal *term = data; + + /* Kill the event FD */ + fdm_del(term->fdm, fd); + + struct wl_window *win = term->window; + + if (win != NULL && win->tab_count <= 1) { + wayl_win_destroy(win); + term->window = NULL; + + struct wayland *wayl = term->wl; + tll_foreach(wayl->seats, it) { + if (it->item.kbd_focus == term) + it->item.kbd_focus = NULL; + if (it->item.mouse_focus == term) + it->item.mouse_focus = NULL; } + } + /* + * Multi-tab case: leave term->window intact so term_destroy() can + * use its existing tab-removal and tab-switch logic. + */ - struct terminal *term = data; - xassert(!term->shutdown.client_has_terminated); - - LOG_DBG("slave (PID=%u) has not terminated, sending %s (%d)", - term->slave, - term->shutdown.next_signal == SIGTERM ? "SIGTERM" - : term->shutdown.next_signal == SIGKILL ? "SIGKILL" - : "", - term->shutdown.next_signal); - - kill(-term->slave, term->shutdown.next_signal); - - switch (term->shutdown.next_signal) { - case SIGTERM: - term->shutdown.next_signal = SIGKILL; - break; - - case SIGKILL: - /* Disarm. Shouldn't be necessary, as we should be able to - shutdown completely after sending SIGKILL, before the next - timeout occurs). But lets play it safe... */ - if (term->shutdown.terminate_timeout_fd >= 0) { - timerfd_settime( - term->shutdown.terminate_timeout_fd, 0, - &(const struct itimerspec){0}, NULL); - } - break; - - default: - BUG("can only handle SIGTERM and SIGKILL"); - return false; - } - - return true; + term->shutdown.fdm_done = true; + shutdown_maybe_done(term); + return true; } -bool -term_shutdown(struct terminal *term) -{ - if (term->shutdown.in_progress) - return true; +static bool fdm_terminate_timeout(struct fdm *fdm, int fd, int events, + void *data) { + uint64_t unused; + ssize_t bytes = read(fd, &unused, sizeof(unused)); + if (bytes < 0) { + LOG_ERRNO("failed to read from slave terminate timeout FD"); + return false; + } - term->shutdown.in_progress = true; + struct terminal *term = data; + xassert(!term->shutdown.client_has_terminated); - /* - * Close FDs then postpone self-destruction to the next poll - * iteration, by creating an event FD that we trigger immediately. - */ + LOG_DBG("slave (PID=%u) has not terminated, sending %s (%d)", term->slave, + term->shutdown.next_signal == SIGTERM ? "SIGTERM" + : term->shutdown.next_signal == SIGKILL ? "SIGKILL" + : "", + term->shutdown.next_signal); - term_cursor_blink_update(term); - xassert(term->cursor_blink.fd < 0); + kill(-term->slave, term->shutdown.next_signal); - fdm_del(term->fdm, term->selection.auto_scroll.fd); - fdm_del(term->fdm, term->render.app_sync_updates.timer_fd); - fdm_del(term->fdm, term->render.app_id.timer_fd); - fdm_del(term->fdm, term->render.icon.timer_fd); - fdm_del(term->fdm, term->render.title.timer_fd); - fdm_del(term->fdm, term->delayed_render_timer.lower_fd); - fdm_del(term->fdm, term->delayed_render_timer.upper_fd); - fdm_del(term->fdm, term->blink.fd); - fdm_del(term->fdm, term->flash.fd); + switch (term->shutdown.next_signal) { + case SIGTERM: + term->shutdown.next_signal = SIGKILL; + break; - del_utmp_record(term->conf, term->reaper, term->ptmx); - - if (term->window != NULL && term->window->is_configured) - fdm_del(term->fdm, term->ptmx); - else - close(term->ptmx); - - if (!term->shutdown.client_has_terminated) { - if (term->slave <= 0) { - term->shutdown.client_has_terminated = true; - } else { - LOG_DBG("initiating asynchronous terminate of slave; " - "sending SIGHUP to PID=%u", term->slave); - - kill(-term->slave, SIGHUP); - - /* - * Set up a timer, with an interval - on the first timeout - * we'll send SIGTERM. If the the client application still - * isn't terminating, we'll wait an additional interval, - * and then send SIGKILL. - */ - const struct itimerspec timeout = {.it_value = {.tv_sec = 30}, - .it_interval = {.tv_sec = 30}}; - - int timeout_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); - if (timeout_fd < 0 || - timerfd_settime(timeout_fd, 0, &timeout, NULL) < 0 || - !fdm_add(term->fdm, timeout_fd, EPOLLIN, &fdm_terminate_timeout, term)) - { - if (timeout_fd >= 0) - close(timeout_fd); - LOG_ERRNO("failed to create slave terminate timeout FD"); - return false; - } - - xassert(term->shutdown.terminate_timeout_fd < 0); - term->shutdown.terminate_timeout_fd = timeout_fd; - term->shutdown.next_signal = SIGTERM; - } + case SIGKILL: + /* Disarm. Shouldn't be necessary, as we should be able to + shutdown completely after sending SIGKILL, before the next + timeout occurs). But lets play it safe... */ + if (term->shutdown.terminate_timeout_fd >= 0) { + timerfd_settime(term->shutdown.terminate_timeout_fd, 0, + &(const struct itimerspec){0}, NULL); } + break; - term->selection.auto_scroll.fd = -1; - term->render.app_sync_updates.timer_fd = -1; - term->render.app_id.timer_fd = -1; - term->render.icon.timer_fd = -1; - term->render.title.timer_fd = -1; - term->delayed_render_timer.lower_fd = -1; - term->delayed_render_timer.upper_fd = -1; - term->blink.fd = -1; - term->flash.fd = -1; - term->ptmx = -1; + default: + BUG("can only handle SIGTERM and SIGKILL"); + return false; + } - int event_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); - if (event_fd == -1) { - LOG_ERRNO("failed to create terminal shutdown event FD"); - return false; - } - - if (!fdm_add(term->fdm, event_fd, EPOLLIN, &fdm_shutdown, term)) { - close(event_fd); - return false; - } - - if (write(event_fd, &(uint64_t){1}, sizeof(uint64_t)) != sizeof(uint64_t)) { - LOG_ERRNO("failed to send terminal shutdown event"); - fdm_del(term->fdm, event_fd); - return false; - } + return true; +} +bool term_shutdown(struct terminal *term) { + if (term->shutdown.in_progress) return true; + + term->shutdown.in_progress = true; + + /* + * Close FDs then postpone self-destruction to the next poll + * iteration, by creating an event FD that we trigger immediately. + */ + + term_cursor_blink_update(term); + xassert(term->cursor_blink.fd < 0); + + fdm_del(term->fdm, term->selection.auto_scroll.fd); + fdm_del(term->fdm, term->render.app_sync_updates.timer_fd); + fdm_del(term->fdm, term->render.app_id.timer_fd); + fdm_del(term->fdm, term->render.icon.timer_fd); + fdm_del(term->fdm, term->render.title.timer_fd); + fdm_del(term->fdm, term->delayed_render_timer.lower_fd); + fdm_del(term->fdm, term->delayed_render_timer.upper_fd); + fdm_del(term->fdm, term->blink.fd); + fdm_del(term->fdm, term->flash.fd); + + del_utmp_record(term->conf, term->reaper, term->ptmx); + + if (term->window != NULL && term->window->is_configured) + fdm_del(term->fdm, term->ptmx); + else + close(term->ptmx); + + if (!term->shutdown.client_has_terminated) { + if (term->slave <= 0) { + term->shutdown.client_has_terminated = true; + } else { + LOG_DBG("initiating asynchronous terminate of slave; " + "sending SIGHUP to PID=%u", + term->slave); + + kill(-term->slave, SIGHUP); + + /* + * Set up a timer, with an interval - on the first timeout + * we'll send SIGTERM. If the the client application still + * isn't terminating, we'll wait an additional interval, + * and then send SIGKILL. + */ + const struct itimerspec timeout = {.it_value = {.tv_sec = 30}, + .it_interval = {.tv_sec = 30}}; + + int timeout_fd = + timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (timeout_fd < 0 || + timerfd_settime(timeout_fd, 0, &timeout, NULL) < 0 || + !fdm_add(term->fdm, timeout_fd, EPOLLIN, &fdm_terminate_timeout, + term)) { + if (timeout_fd >= 0) + close(timeout_fd); + LOG_ERRNO("failed to create slave terminate timeout FD"); + return false; + } + + xassert(term->shutdown.terminate_timeout_fd < 0); + term->shutdown.terminate_timeout_fd = timeout_fd; + term->shutdown.next_signal = SIGTERM; + } + } + + term->selection.auto_scroll.fd = -1; + term->render.app_sync_updates.timer_fd = -1; + term->render.app_id.timer_fd = -1; + term->render.icon.timer_fd = -1; + term->render.title.timer_fd = -1; + term->delayed_render_timer.lower_fd = -1; + term->delayed_render_timer.upper_fd = -1; + term->blink.fd = -1; + term->flash.fd = -1; + term->ptmx = -1; + + int event_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); + if (event_fd == -1) { + LOG_ERRNO("failed to create terminal shutdown event FD"); + return false; + } + + if (!fdm_add(term->fdm, event_fd, EPOLLIN, &fdm_shutdown, term)) { + close(event_fd); + return false; + } + + if (write(event_fd, &(uint64_t){1}, sizeof(uint64_t)) != sizeof(uint64_t)) { + LOG_ERRNO("failed to send terminal shutdown event"); + fdm_del(term->fdm, event_fd); + return false; + } + + return true; } static volatile sig_atomic_t alarm_raised; -static void -sig_alarm(int signo) -{ - LOG_DBG("SIGALRM"); - alarm_raised = 1; +static void sig_alarm(int signo) { + LOG_DBG("SIGALRM"); + alarm_raised = 1; } -int -term_destroy(struct terminal *term) -{ - if (term == NULL) - return 0; +int term_destroy(struct terminal *term) { + if (term == NULL) + return 0; - tll_foreach(term->wl->terms, it) { - if (it->item == term) { - tll_remove(term->wl->terms, it); - break; - } + tll_foreach(term->wl->terms, it) { + if (it->item == term) { + tll_remove(term->wl->terms, it); + break; + } + } + + del_utmp_record(term->conf, term->reaper, term->ptmx); + + fdm_del(term->fdm, term->selection.auto_scroll.fd); + fdm_del(term->fdm, term->render.app_sync_updates.timer_fd); + fdm_del(term->fdm, term->render.app_id.timer_fd); + fdm_del(term->fdm, term->render.icon.timer_fd); + fdm_del(term->fdm, term->render.title.timer_fd); + fdm_del(term->fdm, term->delayed_render_timer.lower_fd); + fdm_del(term->fdm, term->delayed_render_timer.upper_fd); + fdm_del(term->fdm, term->cursor_blink.fd); + fdm_del(term->fdm, term->blink.fd); + fdm_del(term->fdm, term->flash.fd); + fdm_del(term->fdm, term->ptmx); + if (term->shutdown.terminate_timeout_fd >= 0) + fdm_del(term->fdm, term->shutdown.terminate_timeout_fd); + + if (term->window != NULL) { + struct wl_window *win = term->window; + + /* Remove ourselves from the window's tab list */ + bool was_active = (win->term == term); + size_t our_idx = win->tab_count; /* invalid sentinel */ + for (size_t i = 0; i < win->tab_count; i++) { + if (win->tabs[i] == term) { + our_idx = i; + memmove(&win->tabs[i], &win->tabs[i + 1], + (win->tab_count - i - 1) * sizeof(win->tabs[0])); + win->tab_count--; + if (win->active_tab > 0 && win->active_tab >= win->tab_count) + win->active_tab = win->tab_count - 1; + else if (our_idx < win->active_tab) + win->active_tab--; + break; + } } - del_utmp_record(term->conf, term->reaper, term->ptmx); + if (win->tab_count == 0) { + /* Last tab - destroy the window normally */ + wayl_win_destroy(win); + } else { + /* Other tabs remain - just purge our render chains */ - fdm_del(term->fdm, term->selection.auto_scroll.fd); - fdm_del(term->fdm, term->render.app_sync_updates.timer_fd); - fdm_del(term->fdm, term->render.app_id.timer_fd); - fdm_del(term->fdm, term->render.icon.timer_fd); - fdm_del(term->fdm, term->render.title.timer_fd); - fdm_del(term->fdm, term->delayed_render_timer.lower_fd); - fdm_del(term->fdm, term->delayed_render_timer.upper_fd); - fdm_del(term->fdm, term->cursor_blink.fd); - fdm_del(term->fdm, term->blink.fd); - fdm_del(term->fdm, term->flash.fd); - fdm_del(term->fdm, term->ptmx); - if (term->shutdown.terminate_timeout_fd >= 0) - fdm_del(term->fdm, term->shutdown.terminate_timeout_fd); + /* Cancel any pending frame callback that still holds a pointer + * to this terminal, preventing a use-after-free when the + * compositor fires it after term is freed. */ + if (win->frame_callback != NULL) { + wl_callback_destroy(win->frame_callback); + win->frame_callback = NULL; + } - if (term->window != NULL) { - struct wl_window *win = term->window; + render_wait_for_preapply_damage(term); + shm_purge(term->render.chains.search); + shm_purge(term->render.chains.scrollback_indicator); + shm_purge(term->render.chains.render_timer); + shm_purge(term->render.chains.grid); + shm_purge(term->render.chains.url); + shm_purge(term->render.chains.csd); + shm_purge(term->render.chains.tab_bar); + shm_purge(term->render.chains.tab_overview); + shm_purge(term->render.chains.overlay); - /* Remove ourselves from the window's tab list */ - bool was_active = (win->term == term); - size_t our_idx = win->tab_count; /* invalid sentinel */ - for (size_t i = 0; i < win->tab_count; i++) { - if (win->tabs[i] == term) { - our_idx = i; - memmove(&win->tabs[i], &win->tabs[i + 1], - (win->tab_count - i - 1) * sizeof(win->tabs[0])); - win->tab_count--; - if (win->active_tab > 0 && win->active_tab >= win->tab_count) - win->active_tab = win->tab_count - 1; - else if (our_idx < win->active_tab) - win->active_tab--; - break; - } + /* Switch to the new active tab if needed */ + if (was_active) { + struct terminal *next = win->tabs[win->active_tab]; + win->term = next; + next->active_surface = term->active_surface; + + /* Update keyboard and mouse focus */ + bool has_kbd_focus = false; + tll_foreach(next->wl->seats, it) { + struct seat *seat = &it->item; + if (seat->kbd_focus == term) + seat->kbd_focus = next; + if (seat->mouse_focus == term) + seat->mouse_focus = next; + if (seat->kbd_focus == next) + has_kbd_focus = true; } - if (win->tab_count == 0) { - /* Last tab - destroy the window normally */ - wayl_win_destroy(win); + if (has_kbd_focus) { + term_kbd_focus_in(next); + term_visual_focus_in(next); } else { - /* Other tabs remain - just purge our render chains */ - - /* Cancel any pending frame callback that still holds a pointer - * to this terminal, preventing a use-after-free when the - * compositor fires it after term is freed. */ - if (win->frame_callback != NULL) { - wl_callback_destroy(win->frame_callback); - win->frame_callback = NULL; - } - - render_wait_for_preapply_damage(term); - shm_purge(term->render.chains.search); - shm_purge(term->render.chains.scrollback_indicator); - shm_purge(term->render.chains.render_timer); - shm_purge(term->render.chains.grid); - shm_purge(term->render.chains.url); - shm_purge(term->render.chains.csd); - shm_purge(term->render.chains.tab_bar); - shm_purge(term->render.chains.tab_overview); - shm_purge(term->render.chains.overlay); - - /* Switch to the new active tab if needed */ - if (was_active) { - struct terminal *next = win->tabs[win->active_tab]; - win->term = next; - next->active_surface = term->active_surface; - - /* Update keyboard and mouse focus */ - bool has_kbd_focus = false; - tll_foreach(next->wl->seats, it) { - struct seat *seat = &it->item; - if (seat->kbd_focus == term) - seat->kbd_focus = next; - if (seat->mouse_focus == term) - seat->mouse_focus = next; - if (seat->kbd_focus == next) - has_kbd_focus = true; - } - - if (has_kbd_focus) { - term_kbd_focus_in(next); - term_visual_focus_in(next); - } else { - term_visual_focus_out(next); - } - - render_resize(next, - (int)roundf(term->width / term->scale), - (int)roundf(term->height / term->scale), - RESIZE_FORCE); - render_refresh_csd(next); - render_refresh_tab_bar(next); - term_xcursor_update(next); - } else { - /* Just update the tab bar to reflect removed tab */ - render_refresh_tab_bar(win->term); - } + term_visual_focus_out(next); } - term->window = NULL; + render_resize(next, (int)roundf(term->width / term->scale), + (int)roundf(term->height / term->scale), RESIZE_FORCE); + render_refresh_csd(next); + render_refresh_tab_bar(next); + term_xcursor_update(next); + } else { + /* Just update the tab bar to reflect removed tab */ + render_refresh_tab_bar(win->term); + } } - mtx_lock(&term->render.workers.lock); - xassert(tll_length(term->render.workers.queue) == 0); + term->window = NULL; + } - /* Count livinig threads - we may get here when only some of the - * threads have been successfully started */ - size_t worker_count = 0; - if (term->render.workers.threads != NULL) { - for (size_t i = 0; i < term->render.workers.count; i++, worker_count++) { - if (term->render.workers.threads[i] == 0) - break; - } + mtx_lock(&term->render.workers.lock); + xassert(tll_length(term->render.workers.queue) == 0); - for (size_t i = 0; i < worker_count; i++) { - sem_post(&term->render.workers.start); - tll_push_back(term->render.workers.queue, -2); - } - } - mtx_unlock(&term->render.workers.lock); - - key_binding_unref(term->wl->key_binding_manager, term->conf); - - urls_reset(term); - - free(term->vt.osc.data); - free(term->vt.osc8.uri); - - composed_free(term->composed); - - free(term->app_id); - free(term->window_title); - tll_free_and_free(term->window_title_stack, free); - - for (size_t i = 0; i < sizeof(term->fonts) / sizeof(term->fonts[0]); i++) - fcft_destroy(term->fonts[i]); - for (size_t i = 0; i < 4; i++) - free(term->font_sizes[i]); - - - free_custom_glyphs( - &term->custom_glyphs.box_drawing, GLYPH_BOX_DRAWING_COUNT); - free_custom_glyphs( - &term->custom_glyphs.braille, GLYPH_BRAILLE_COUNT); - free_custom_glyphs( - &term->custom_glyphs.legacy, GLYPH_LEGACY_COUNT); - free_custom_glyphs( - &term->custom_glyphs.octants, GLYPH_OCTANTS_COUNT); - - free(term->search.buf); - free(term->search.last.buf); - - /* Free search history */ - { - struct search_history_entry *e = term->search.history_head; - while (e != NULL) { - struct search_history_entry *next = e->next; - free(e->buf); - free(e); - e = next; - } + /* Count livinig threads - we may get here when only some of the + * threads have been successfully started */ + size_t worker_count = 0; + if (term->render.workers.threads != NULL) { + for (size_t i = 0; i < term->render.workers.count; i++, worker_count++) { + if (term->render.workers.threads[i] == 0) + break; } - /* Free compiled regex if any */ - if (term->search.regex_compiled != NULL) { - regfree(term->search.regex_compiled); - free(term->search.regex_compiled); + for (size_t i = 0; i < worker_count; i++) { + sem_post(&term->render.workers.start); + tll_push_back(term->render.workers.queue, -2); } + } + mtx_unlock(&term->render.workers.lock); - if (term->render.workers.threads != NULL) { - for (size_t i = 0; i < term->render.workers.count; i++) { - if (term->render.workers.threads[i] != 0) - thrd_join(term->render.workers.threads[i], NULL); - } + key_binding_unref(term->wl->key_binding_manager, term->conf); + + urls_reset(term); + + free(term->vt.osc.data); + free(term->vt.osc8.uri); + + composed_free(term->composed); + + free(term->app_id); + free(term->window_title); + tll_free_and_free(term->window_title_stack, free); + + for (size_t i = 0; i < sizeof(term->fonts) / sizeof(term->fonts[0]); i++) + fcft_destroy(term->fonts[i]); + for (size_t i = 0; i < 4; i++) + free(term->font_sizes[i]); + + free_custom_glyphs(&term->custom_glyphs.box_drawing, GLYPH_BOX_DRAWING_COUNT); + free_custom_glyphs(&term->custom_glyphs.braille, GLYPH_BRAILLE_COUNT); + free_custom_glyphs(&term->custom_glyphs.legacy, GLYPH_LEGACY_COUNT); + free_custom_glyphs(&term->custom_glyphs.octants, GLYPH_OCTANTS_COUNT); + + free(term->search.buf); + free(term->search.last.buf); + + /* Free search history */ + { + struct search_history_entry *e = term->search.history_head; + while (e != NULL) { + struct search_history_entry *next = e->next; + free(e->buf); + free(e); + e = next; } - free(term->render.workers.threads); - mtx_destroy(&term->render.workers.preapplied_damage.lock); - cnd_destroy(&term->render.workers.preapplied_damage.cond); - mtx_destroy(&term->render.workers.lock); - sem_destroy(&term->render.workers.start); - sem_destroy(&term->render.workers.done); - xassert(tll_length(term->render.workers.queue) == 0); - tll_free(term->render.workers.queue); + } - shm_unref(term->render.last_buf); - shm_chain_free(term->render.chains.grid); - shm_chain_free(term->render.chains.search); - shm_chain_free(term->render.chains.scrollback_indicator); - shm_chain_free(term->render.chains.render_timer); - shm_chain_free(term->render.chains.url); - shm_chain_free(term->render.chains.csd); - shm_chain_free(term->render.chains.overlay); - shm_chain_free(term->render.chains.tab_bar); - shm_chain_free(term->render.chains.tab_overview); - pixman_region32_fini(&term->render.last_overlay_clip); + /* Free compiled regex if any */ + if (term->search.regex_compiled != NULL) { + regfree(term->search.regex_compiled); + free(term->search.regex_compiled); + } - tll_free(term->tab_stops); - - tll_foreach(term->ptmx_buffers, it) { - free(it->item.data); - tll_remove(term->ptmx_buffers, it); - } - tll_foreach(term->ptmx_paste_buffers, it) { - free(it->item.data); - tll_remove(term->ptmx_paste_buffers, it); + if (term->render.workers.threads != NULL) { + for (size_t i = 0; i < term->render.workers.count; i++) { + if (term->render.workers.threads[i] != 0) + thrd_join(term->render.workers.threads[i], NULL); } + } + free(term->render.workers.threads); + mtx_destroy(&term->render.workers.preapplied_damage.lock); + cnd_destroy(&term->render.workers.preapplied_damage.cond); + mtx_destroy(&term->render.workers.lock); + sem_destroy(&term->render.workers.start); + sem_destroy(&term->render.workers.done); + xassert(tll_length(term->render.workers.queue) == 0); + tll_free(term->render.workers.queue); - notify_free(term, &term->kitty_notification); - tll_foreach(term->active_notifications, it) { - notify_free(term, &it->item); - tll_remove(term->active_notifications, it); - } + shm_unref(term->render.last_buf); + shm_chain_free(term->render.chains.grid); + shm_chain_free(term->render.chains.search); + shm_chain_free(term->render.chains.scrollback_indicator); + shm_chain_free(term->render.chains.render_timer); + shm_chain_free(term->render.chains.url); + shm_chain_free(term->render.chains.csd); + shm_chain_free(term->render.chains.overlay); + shm_chain_free(term->render.chains.tab_bar); + shm_chain_free(term->render.chains.tab_overview); + pixman_region32_fini(&term->render.last_overlay_clip); - for (size_t i = 0; i < ALEN(term->notification_icons); i++) - notify_icon_free(&term->notification_icons[i]); + tll_free(term->tab_stops); - sixel_fini(term); + tll_foreach(term->ptmx_buffers, it) { + free(it->item.data); + tll_remove(term->ptmx_buffers, it); + } + tll_foreach(term->ptmx_paste_buffers, it) { + free(it->item.data); + tll_remove(term->ptmx_paste_buffers, it); + } - term_ime_reset(term); + notify_free(term, &term->kitty_notification); + tll_foreach(term->active_notifications, it) { + notify_free(term, &it->item); + tll_remove(term->active_notifications, it); + } - grid_free(&term->normal); - grid_free(&term->alt); - grid_free(term->interactive_resizing.grid); - free(term->interactive_resizing.grid); + for (size_t i = 0; i < ALEN(term->notification_icons); i++) + notify_icon_free(&term->notification_icons[i]); - free(term->foot_exe); - free(term->cwd); - free(term->mouse_user_cursor); - free(term->color_stack.stack); + sixel_fini(term); - int ret = EXIT_SUCCESS; + term_ime_reset(term); - if (term->slave > 0) { - /* We'll deal with this explicitly */ - reaper_del(term->reaper, term->slave); + grid_free(&term->normal); + grid_free(&term->alt); + grid_free(term->interactive_resizing.grid); + free(term->interactive_resizing.grid); - int exit_status; + free(term->foot_exe); + free(term->cwd); + free(term->mouse_user_cursor); + free(term->color_stack.stack); - if (term->shutdown.client_has_terminated) - exit_status = term->shutdown.exit_status; - else { - LOG_DBG("initiating blocking terminate of slave; " - "sending SIGHUP to PID=%u", term->slave); + int ret = EXIT_SUCCESS; - kill(-term->slave, SIGHUP); + if (term->slave > 0) { + /* We'll deal with this explicitly */ + reaper_del(term->reaper, term->slave); - /* - * we've closed the ptxm, and sent SIGTERM to the client - * application. It *should* exit... - * - * But, since it is possible to write clients that ignore - * this, we need to handle it in *some* way. - * - * So, what we do is register a SIGALRM handler, and configure a 30 - * second alarm. If the slave hasn't died after this time, we send - * it a SIGKILL, - * - * Note that this solution is *not* asynchronous, and any - * other events etc will be ignored during this time. This of - * course only applies to a 'foot --server' instance, where - * there might be other terminals running. - */ - struct sigaction action = {.sa_handler = &sig_alarm}; - sigemptyset(&action.sa_mask); - sigaction(SIGALRM, &action, NULL); + int exit_status; - /* Wait, then send SIGTERM, wait again, then send SIGKILL */ - int next_signal = SIGTERM; + if (term->shutdown.client_has_terminated) + exit_status = term->shutdown.exit_status; + else { + LOG_DBG("initiating blocking terminate of slave; " + "sending SIGHUP to PID=%u", + term->slave); + + kill(-term->slave, SIGHUP); + + /* + * we've closed the ptxm, and sent SIGTERM to the client + * application. It *should* exit... + * + * But, since it is possible to write clients that ignore + * this, we need to handle it in *some* way. + * + * So, what we do is register a SIGALRM handler, and configure a 30 + * second alarm. If the slave hasn't died after this time, we send + * it a SIGKILL, + * + * Note that this solution is *not* asynchronous, and any + * other events etc will be ignored during this time. This of + * course only applies to a 'foot --server' instance, where + * there might be other terminals running. + */ + struct sigaction action = {.sa_handler = &sig_alarm}; + sigemptyset(&action.sa_mask); + sigaction(SIGALRM, &action, NULL); + + /* Wait, then send SIGTERM, wait again, then send SIGKILL */ + int next_signal = SIGTERM; + + alarm_raised = 0; + alarm(30); + + while (true) { + int r = waitpid(term->slave, &exit_status, 0); + + if (r == term->slave) + break; + + if (r == -1) { + xassert(errno == EINTR); + + if (alarm_raised) { + LOG_DBG("slave (PID=%u) has not terminated yet, " + "sending: %s (%d)", + term->slave, next_signal == SIGTERM ? "SIGTERM" : "SIGKILL", + next_signal); + + kill(-term->slave, next_signal); + next_signal = SIGKILL; alarm_raised = 0; alarm(30); - - while (true) { - int r = waitpid(term->slave, &exit_status, 0); - - if (r == term->slave) - break; - - if (r == -1) { - xassert(errno == EINTR); - - if (alarm_raised) { - LOG_DBG("slave (PID=%u) has not terminated yet, " - "sending: %s (%d)", term->slave, - next_signal == SIGTERM ? "SIGTERM" : "SIGKILL", - next_signal); - - kill(-term->slave, next_signal); - next_signal = SIGKILL; - - alarm_raised = 0; - alarm(30); - } - } - } - - /* Cancel alarm */ - alarm(0); - action.sa_handler = SIG_DFL; - sigaction(SIGALRM, &action, NULL); + } } + } - ret = EXIT_FAILURE; - if (WIFEXITED(exit_status)) { - ret = WEXITSTATUS(exit_status); - LOG_DBG("slave exited with code %d", ret); - } else if (WIFSIGNALED(exit_status)) { - ret = WTERMSIG(exit_status); - LOG_WARN("slave exited with signal %d (%s)", ret, strsignal(ret)); - } else { - LOG_WARN("slave exited for unknown reason (status = 0x%08x)", - exit_status); - } + /* Cancel alarm */ + alarm(0); + action.sa_handler = SIG_DFL; + sigaction(SIGALRM, &action, NULL); } - free(term); + ret = EXIT_FAILURE; + if (WIFEXITED(exit_status)) { + ret = WEXITSTATUS(exit_status); + LOG_DBG("slave exited with code %d", ret); + } else if (WIFSIGNALED(exit_status)) { + ret = WTERMSIG(exit_status); + LOG_WARN("slave exited with signal %d (%s)", ret, strsignal(ret)); + } else { + LOG_WARN("slave exited for unknown reason (status = 0x%08x)", + exit_status); + } + } + + free(term); #if defined(__GLIBC__) - if (!malloc_trim(0)) - LOG_WARN("failed to trim memory"); + if (!malloc_trim(0)) + LOG_WARN("failed to trim memory"); #endif - return ret; + return ret; } -static inline void -erase_cell_range(struct terminal *term, struct row *row, int start, int end) -{ - xassert(start < term->cols); - xassert(end < term->cols); +static inline void erase_cell_range(struct terminal *term, struct row *row, + int start, int end) { + xassert(start < term->cols); + xassert(end < term->cols); - row->dirty = true; + row->dirty = true; - const enum color_source bg_src = term->vt.attrs.bg_src; + const enum color_source bg_src = term->vt.attrs.bg_src; - if (unlikely(bg_src != COLOR_DEFAULT)) { - for (int col = start; col <= end; col++) { - struct cell *c = &row->cells[col]; - c->wc = 0; - c->attrs = (struct attributes){.bg_src = bg_src, .bg = term->vt.attrs.bg}; - } - } else - memset(&row->cells[start], 0, (end - start + 1) * sizeof(row->cells[0])); - - if (unlikely(row->extra != NULL)) { - grid_row_uri_range_erase(row, start, end); - grid_row_underline_range_erase(row, start, end); + if (unlikely(bg_src != COLOR_DEFAULT)) { + for (int col = start; col <= end; col++) { + struct cell *c = &row->cells[col]; + c->wc = 0; + c->attrs = (struct attributes){.bg_src = bg_src, .bg = term->vt.attrs.bg}; } + } else + memset(&row->cells[start], 0, (end - start + 1) * sizeof(row->cells[0])); + + if (unlikely(row->extra != NULL)) { + grid_row_uri_range_erase(row, start, end); + grid_row_underline_range_erase(row, start, end); + } } -static inline void -erase_line(struct terminal *term, struct row *row) -{ - erase_cell_range(term, row, 0, term->cols - 1); - row->linebreak = true; - row->shell_integration.prompt_marker = false; - row->shell_integration.cmd_start = -1; - row->shell_integration.cmd_end = -1; +static inline void erase_line(struct terminal *term, struct row *row) { + erase_cell_range(term, row, 0, term->cols - 1); + row->linebreak = true; + row->shell_integration.prompt_marker = false; + row->shell_integration.cmd_start = -1; + row->shell_integration.cmd_end = -1; } -static void -term_theme_apply(struct terminal *term, const struct color_theme *theme) -{ - term->colors.fg = theme->fg; - term->colors.bg = theme->bg; - term->colors.alpha = theme->alpha; - term->colors.cursor_fg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.text; - term->colors.cursor_bg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.cursor; - term->colors.selection_fg = theme->selection_fg; - term->colors.selection_bg = theme->selection_bg; - memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); +static void term_theme_apply(struct terminal *term, + const struct color_theme *theme) { + term->colors.fg = theme->fg; + term->colors.bg = theme->bg; + term->colors.alpha = theme->alpha; + term->colors.cursor_fg = + (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.text; + term->colors.cursor_bg = + (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.cursor; + term->colors.selection_fg = theme->selection_fg; + term->colors.selection_bg = theme->selection_bg; + memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); } -void -term_reset(struct terminal *term, bool hard) -{ - LOG_INFO("%s resetting the terminal", hard ? "hard" : "soft"); +void term_reset(struct terminal *term, bool hard) { + LOG_INFO("%s resetting the terminal", hard ? "hard" : "soft"); - term->cursor_keys_mode = CURSOR_KEYS_NORMAL; - term->keypad_keys_mode = KEYPAD_NUMERICAL; - term->reverse = false; - term->hide_cursor = false; - term->reverse_wrap = true; - term->auto_margin = true; - term->insert_mode = false; - term->bracketed_paste = false; - term->focus_events = false; - term->num_lock_modifier = true; - term->bell_action_enabled = true; - term->mouse_tracking = MOUSE_NONE; - term->mouse_reporting = MOUSE_NORMAL; - term->charsets.selected = G0; - term->charsets.set[G0] = CHARSET_ASCII; - term->charsets.set[G1] = CHARSET_ASCII; - term->charsets.set[G2] = CHARSET_ASCII; - term->charsets.set[G3] = CHARSET_ASCII; - term->saved_charsets = term->charsets; - tll_free_and_free(term->window_title_stack, free); - term_set_window_title(term, term->conf->title); - term_set_app_id(term, NULL); + term->cursor_keys_mode = CURSOR_KEYS_NORMAL; + term->keypad_keys_mode = KEYPAD_NUMERICAL; + term->reverse = false; + term->hide_cursor = false; + term->reverse_wrap = true; + term->auto_margin = true; + term->insert_mode = false; + term->bracketed_paste = false; + term->focus_events = false; + term->num_lock_modifier = true; + term->bell_action_enabled = true; + term->mouse_tracking = MOUSE_NONE; + term->mouse_reporting = MOUSE_NORMAL; + term->charsets.selected = G0; + term->charsets.set[G0] = CHARSET_ASCII; + term->charsets.set[G1] = CHARSET_ASCII; + term->charsets.set[G2] = CHARSET_ASCII; + term->charsets.set[G3] = CHARSET_ASCII; + term->saved_charsets = term->charsets; + tll_free_and_free(term->window_title_stack, free); + term_set_window_title(term, term->conf->title); + term_set_app_id(term, NULL); - term_set_user_mouse_cursor(term, NULL); + term_set_user_mouse_cursor(term, NULL); - term->modify_other_keys_2 = false; - memset(term->normal.kitty_kbd.flags, 0, sizeof(term->normal.kitty_kbd.flags)); - memset(term->alt.kitty_kbd.flags, 0, sizeof(term->alt.kitty_kbd.flags)); - term->normal.kitty_kbd.idx = term->alt.kitty_kbd.idx = 0; + term->modify_other_keys_2 = false; + memset(term->normal.kitty_kbd.flags, 0, sizeof(term->normal.kitty_kbd.flags)); + memset(term->alt.kitty_kbd.flags, 0, sizeof(term->alt.kitty_kbd.flags)); + term->normal.kitty_kbd.idx = term->alt.kitty_kbd.idx = 0; - term->scroll_region.start = 0; - term->scroll_region.end = term->rows; + term->scroll_region.start = 0; + term->scroll_region.end = term->rows; - free(term->vt.osc8.uri); - free(term->vt.osc.data); + free(term->vt.osc8.uri); + free(term->vt.osc.data); - term->vt = (struct vt){ - .state = 0, /* STATE_GROUND */ - }; + term->vt = (struct vt){ + .state = 0, /* STATE_GROUND */ + }; - if (term->grid == &term->alt) { - term->grid = &term->normal; - selection_cancel(term); - } + if (term->grid == &term->alt) { + term->grid = &term->normal; + selection_cancel(term); + } - term->meta.esc_prefix = true; - term->meta.eight_bit = true; + term->meta.esc_prefix = true; + term->meta.eight_bit = true; - tll_foreach(term->normal.sixel_images, it) { - sixel_destroy(&it->item); - tll_remove(term->normal.sixel_images, it); - } - tll_foreach(term->alt.sixel_images, it) { - sixel_destroy(&it->item); - tll_remove(term->alt.sixel_images, it); - } + tll_foreach(term->normal.sixel_images, it) { + sixel_destroy(&it->item); + tll_remove(term->normal.sixel_images, it); + } + tll_foreach(term->alt.sixel_images, it) { + sixel_destroy(&it->item); + tll_remove(term->alt.sixel_images, it); + } - notify_free(term, &term->kitty_notification); - tll_foreach(term->active_notifications, it) { - notify_free(term, &it->item); - tll_remove(term->active_notifications, it); - } + notify_free(term, &term->kitty_notification); + tll_foreach(term->active_notifications, it) { + notify_free(term, &it->item); + tll_remove(term->active_notifications, it); + } - for (size_t i = 0; i < ALEN(term->notification_icons); i++) - notify_icon_free(&term->notification_icons[i]); + for (size_t i = 0; i < ALEN(term->notification_icons); i++) + notify_icon_free(&term->notification_icons[i]); - term->grapheme_shaping = term->conf->tweak.grapheme_shaping; + term->grapheme_shaping = term->conf->tweak.grapheme_shaping; #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED - term_ime_enable(term); + term_ime_enable(term); #endif - term->bits_affecting_ascii_printer.value = 0; - term_update_ascii_printer(term); + term->bits_affecting_ascii_printer.value = 0; + term_update_ascii_printer(term); - if (!hard) - return; + if (!hard) + return; - const struct color_theme *theme = NULL; + const struct color_theme *theme = NULL; - switch (term->conf->initial_color_theme) { - case COLOR_THEME_DARK: theme = &term->conf->colors_dark; break; - case COLOR_THEME_LIGHT: theme = &term->conf->colors_light; break; - case COLOR_THEME_1: BUG("COLOR_THEME_1 should not be used"); break; - case COLOR_THEME_2: BUG("COLOR_THEME_2 should not be used"); break; + switch (term->conf->initial_color_theme) { + case COLOR_THEME_DARK: + theme = &term->conf->colors_dark; + break; + case COLOR_THEME_LIGHT: + theme = &term->conf->colors_light; + break; + case COLOR_THEME_1: + BUG("COLOR_THEME_1 should not be used"); + break; + case COLOR_THEME_2: + BUG("COLOR_THEME_2 should not be used"); + break; + } + + term->flash.active = false; + term->blink.state = BLINK_ON; + fdm_del(term->fdm, term->blink.fd); + term->blink.fd = -1; + term_theme_apply(term, theme); + term->colors.active_theme = term->conf->initial_color_theme; + free(term->color_stack.stack); + term->color_stack.stack = NULL; + term->color_stack.size = 0; + term->color_stack.idx = 0; + term->origin = ORIGIN_ABSOLUTE; + term->normal.cursor.lcf = false; + term->alt.cursor.lcf = false; + term->normal.cursor = (struct cursor){.point = {0, 0}}; + term->normal.saved_cursor = (struct cursor){.point = {0, 0}}; + term->alt.cursor = (struct cursor){.point = {0, 0}}; + term->alt.saved_cursor = (struct cursor){.point = {0, 0}}; + term->cursor_style = term->conf->cursor.style; + term->cursor_blink.decset = false; + term->cursor_blink.deccsusr = term->conf->cursor.blink.enabled; + term_cursor_blink_update(term); + selection_cancel(term); + term->normal.offset = term->normal.view = 0; + term->alt.offset = term->alt.view = 0; + for (size_t i = 0; i < term->rows; i++) { + struct row *r = grid_row_and_alloc(&term->normal, i); + erase_line(term, r); + } + for (size_t i = 0; i < term->rows; i++) { + struct row *r = grid_row_and_alloc(&term->alt, i); + erase_line(term, r); + } + for (size_t i = term->rows; i < term->normal.num_rows; i++) { + grid_row_free(term->normal.rows[i]); + term->normal.rows[i] = NULL; + } + for (size_t i = term->rows; i < term->alt.num_rows; i++) { + grid_row_free(term->alt.rows[i]); + term->alt.rows[i] = NULL; + } + term->normal.cur_row = term->normal.rows[0]; + term->alt.cur_row = term->alt.rows[0]; + tll_free(term->normal.scroll_damage); + tll_free(term->alt.scroll_damage); + term->render.last_cursor.row = NULL; + term_damage_all(term); + + term->sixel.scrolling = true; + term->sixel.cursor_right_of_graphics = false; + term->sixel.use_private_palette = true; + term->sixel.max_width = SIXEL_MAX_WIDTH; + term->sixel.max_height = SIXEL_MAX_HEIGHT; + term->sixel.palette_size = SIXEL_MAX_COLORS; + free(term->sixel.private_palette); + free(term->sixel.shared_palette); + term->sixel.private_palette = term->sixel.shared_palette = NULL; +} + +static bool term_font_size_adjust_by_points(struct terminal *term, + float amount) { + const struct config *conf = term->conf; + const float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; + + for (size_t i = 0; i < 4; i++) { + const struct config_font_list *font_list = &conf->fonts[i]; + + for (size_t j = 0; j < font_list->count; j++) { + struct config_font *font = &term->font_sizes[i][j]; + float old_pt_size = font->pt_size; + + if (font->px_size > 0) + old_pt_size = font->px_size * 72. / dpi; + + font->pt_size = fmaxf(old_pt_size + amount, 0.); + font->px_size = -1; } + } - term->flash.active = false; - term->blink.state = BLINK_ON; - fdm_del(term->fdm, term->blink.fd); term->blink.fd = -1; - term_theme_apply(term, theme); - term->colors.active_theme = term->conf->initial_color_theme; - free(term->color_stack.stack); - term->color_stack.stack = NULL; - term->color_stack.size = 0; - term->color_stack.idx = 0; - term->origin = ORIGIN_ABSOLUTE; - term->normal.cursor.lcf = false; - term->alt.cursor.lcf = false; - term->normal.cursor = (struct cursor){.point = {0, 0}}; - term->normal.saved_cursor = (struct cursor){.point = {0, 0}}; - term->alt.cursor = (struct cursor){.point = {0, 0}}; - term->alt.saved_cursor = (struct cursor){.point = {0, 0}}; - term->cursor_style = term->conf->cursor.style; - term->cursor_blink.decset = false; - term->cursor_blink.deccsusr = term->conf->cursor.blink.enabled; - term_cursor_blink_update(term); - selection_cancel(term); - term->normal.offset = term->normal.view = 0; - term->alt.offset = term->alt.view = 0; - for (size_t i = 0; i < term->rows; i++) { - struct row *r = grid_row_and_alloc(&term->normal, i); - erase_line(term, r); + return reload_fonts(term, true); +} + +static bool term_font_size_adjust_by_pixels(struct terminal *term, int amount) { + const struct config *conf = term->conf; + const float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; + + for (size_t i = 0; i < 4; i++) { + const struct config_font_list *font_list = &conf->fonts[i]; + + for (size_t j = 0; j < font_list->count; j++) { + struct config_font *font = &term->font_sizes[i][j]; + int old_px_size = font->px_size; + + if (font->px_size <= 0) + old_px_size = font->pt_size * dpi / 72.; + + font->px_size = max(old_px_size + amount, 1); } - for (size_t i = 0; i < term->rows; i++) { - struct row *r = grid_row_and_alloc(&term->alt, i); - erase_line(term, r); + } + + return reload_fonts(term, true); +} + +static bool term_font_size_adjust_by_percent(struct terminal *term, + bool increment, float percent) { + const struct config *conf = term->conf; + const float multiplier = increment ? 1. + percent : 1. / (1. + percent); + + for (size_t i = 0; i < 4; i++) { + const struct config_font_list *font_list = &conf->fonts[i]; + + for (size_t j = 0; j < font_list->count; j++) { + struct config_font *font = &term->font_sizes[i][j]; + + if (font->px_size > 0) + font->px_size = max(font->px_size * multiplier, 1); + else + font->pt_size = fmax(font->pt_size * multiplier, 0); } - for (size_t i = term->rows; i < term->normal.num_rows; i++) { - grid_row_free(term->normal.rows[i]); - term->normal.rows[i] = NULL; - } - for (size_t i = term->rows; i < term->alt.num_rows; i++) { - grid_row_free(term->alt.rows[i]); - term->alt.rows[i] = NULL; - } - term->normal.cur_row = term->normal.rows[0]; - term->alt.cur_row = term->alt.rows[0]; - tll_free(term->normal.scroll_damage); - tll_free(term->alt.scroll_damage); - term->render.last_cursor.row = NULL; - term_damage_all(term); + } - term->sixel.scrolling = true; - term->sixel.cursor_right_of_graphics = false; - term->sixel.use_private_palette = true; - term->sixel.max_width = SIXEL_MAX_WIDTH; - term->sixel.max_height = SIXEL_MAX_HEIGHT; - term->sixel.palette_size = SIXEL_MAX_COLORS; - free(term->sixel.private_palette); - free(term->sixel.shared_palette); - term->sixel.private_palette = term->sixel.shared_palette = NULL; + return reload_fonts(term, true); } -static bool -term_font_size_adjust_by_points(struct terminal *term, float amount) -{ - const struct config *conf = term->conf; - const float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; +bool term_font_size_increase(struct terminal *term) { + const struct config *conf = term->conf; + const struct font_size_adjustment *inc_dec = &conf->font_size_adjustment; - for (size_t i = 0; i < 4; i++) { - const struct config_font_list *font_list = &conf->fonts[i]; - - for (size_t j = 0; j < font_list->count; j++) { - struct config_font *font = &term->font_sizes[i][j]; - float old_pt_size = font->pt_size; - - if (font->px_size > 0) - old_pt_size = font->px_size * 72. / dpi; - - font->pt_size = fmaxf(old_pt_size + amount, 0.); - font->px_size = -1; - } - } - - return reload_fonts(term, true); + if (inc_dec->percent > 0.) + return term_font_size_adjust_by_percent(term, true, inc_dec->percent); + else if (inc_dec->pt_or_px.px > 0) + return term_font_size_adjust_by_pixels(term, inc_dec->pt_or_px.px); + else + return term_font_size_adjust_by_points(term, inc_dec->pt_or_px.pt); } -static bool -term_font_size_adjust_by_pixels(struct terminal *term, int amount) -{ - const struct config *conf = term->conf; - const float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; +bool term_font_size_decrease(struct terminal *term) { + const struct config *conf = term->conf; + const struct font_size_adjustment *inc_dec = &conf->font_size_adjustment; - for (size_t i = 0; i < 4; i++) { - const struct config_font_list *font_list = &conf->fonts[i]; - - for (size_t j = 0; j < font_list->count; j++) { - struct config_font *font = &term->font_sizes[i][j]; - int old_px_size = font->px_size; - - if (font->px_size <= 0) - old_px_size = font->pt_size * dpi / 72.; - - font->px_size = max(old_px_size + amount, 1); - } - } - - return reload_fonts(term, true); + if (inc_dec->percent > 0.) + return term_font_size_adjust_by_percent(term, false, inc_dec->percent); + else if (inc_dec->pt_or_px.px > 0) + return term_font_size_adjust_by_pixels(term, -inc_dec->pt_or_px.px); + else + return term_font_size_adjust_by_points(term, -inc_dec->pt_or_px.pt); } -static bool -term_font_size_adjust_by_percent(struct terminal *term, bool increment, float percent) -{ - const struct config *conf = term->conf; - const float multiplier = increment - ? 1. + percent - : 1. / (1. + percent); - - for (size_t i = 0; i < 4; i++) { - const struct config_font_list *font_list = &conf->fonts[i]; - - for (size_t j = 0; j < font_list->count; j++) { - struct config_font *font = &term->font_sizes[i][j]; - - if (font->px_size > 0) - font->px_size = max(font->px_size * multiplier, 1); - else - font->pt_size = fmax(font->pt_size * multiplier, 0); - } - } - - return reload_fonts(term, true); +bool term_font_size_reset(struct terminal *term) { + return load_fonts_from_conf(term); } -bool -term_font_size_increase(struct terminal *term) -{ - const struct config *conf = term->conf; - const struct font_size_adjustment *inc_dec = &conf->font_size_adjustment; - - if (inc_dec->percent > 0.) - return term_font_size_adjust_by_percent(term, true, inc_dec->percent); - else if (inc_dec->pt_or_px.px > 0) - return term_font_size_adjust_by_pixels(term, inc_dec->pt_or_px.px); - else - return term_font_size_adjust_by_points(term, inc_dec->pt_or_px.pt); +bool term_fractional_scaling(const struct terminal *term) { + return term->wl->fractional_scale_manager != NULL && + term->wl->viewporter != NULL && term->window->scale > 0.; } -bool -term_font_size_decrease(struct terminal *term) -{ - const struct config *conf = term->conf; - const struct font_size_adjustment *inc_dec = &conf->font_size_adjustment; - - if (inc_dec->percent > 0.) - return term_font_size_adjust_by_percent(term, false, inc_dec->percent); - else if (inc_dec->pt_or_px.px > 0) - return term_font_size_adjust_by_pixels(term, -inc_dec->pt_or_px.px); - else - return term_font_size_adjust_by_points(term, -inc_dec->pt_or_px.pt); +bool term_preferred_buffer_scale(const struct terminal *term) { + return term->window->preferred_buffer_scale > 0; } -bool -term_font_size_reset(struct terminal *term) -{ - return load_fonts_from_conf(term); +bool term_update_scale(struct terminal *term) { + const struct wl_window *win = term->window; + + /* + * We have a number of "sources" we can use as scale. We choose + * the scale in the following order: + * + * - "preferred" scale, from the fractional-scale-v1 protocol + * - "preferred" scale, from wl_compositor version 6. + NOTE: if the compositor advertises version 6 we must use 1.0 + until wl_surface.preferred_buffer_scale is sent + * - scaling factor of output we most recently were mapped on + * - if we're not mapped, use the last known scaling factor + * - if we're not mapped, and we don't have a last known scaling + * factor, use the scaling factor from the first available + * output. + * - if there aren't any outputs available, use 1.0 + */ + const float new_scale = + (term_fractional_scaling(term) ? win->scale + : term_preferred_buffer_scale(term) ? win->preferred_buffer_scale + : tll_length(win->on_outputs) > 0 ? tll_back(win->on_outputs)->scale + : term->scale_before_unmap > 0. ? term->scale_before_unmap + : tll_length(term->wl->monitors) > 0 + ? tll_front(term->wl->monitors).scale + : 1.); + + if (new_scale == term->scale) + return false; + + LOG_DBG("scaling factor changed: %.2f -> %.2f", term->scale, new_scale); + term->scale_before_unmap = new_scale; + term->scale = new_scale; + return true; } -bool -term_fractional_scaling(const struct terminal *term) -{ - return term->wl->fractional_scale_manager != NULL && - term->wl->viewporter != NULL && - term->window->scale > 0.; +bool term_font_dpi_changed(struct terminal *term, float old_scale) { + float dpi = get_font_dpi(term); + xassert(term->scale > 0.); + + bool was_scaled_using_dpi = term->font_is_sized_by_dpi; + bool will_scale_using_dpi = term->conf->dpi_aware; + + bool need_font_reload = + was_scaled_using_dpi != will_scale_using_dpi || + (will_scale_using_dpi ? term->font_dpi != dpi : old_scale != term->scale); + + if (need_font_reload) { + LOG_DBG("DPI/scale change: DPI-aware=%s, " + "DPI: %.2f -> %.2f, scale: %.2f -> %.2f, " + "sizing font based on monitor's %s", + term->conf->dpi_aware ? "yes" : "no", term->font_dpi, dpi, + old_scale, term->scale, + will_scale_using_dpi ? "DPI" : "scaling factor"); + } + + term->font_dpi = dpi; + term->font_dpi_before_unmap = dpi; + term->font_is_sized_by_dpi = will_scale_using_dpi; + + if (!need_font_reload) + return false; + + return reload_fonts(term, false); } -bool -term_preferred_buffer_scale(const struct terminal *term) -{ - return term->window->preferred_buffer_scale > 0; -} +void term_font_subpixel_changed(struct terminal *term) { + enum fcft_subpixel subpixel = get_font_subpixel(term); -bool -term_update_scale(struct terminal *term) -{ - const struct wl_window *win = term->window; - - /* - * We have a number of "sources" we can use as scale. We choose - * the scale in the following order: - * - * - "preferred" scale, from the fractional-scale-v1 protocol - * - "preferred" scale, from wl_compositor version 6. - NOTE: if the compositor advertises version 6 we must use 1.0 - until wl_surface.preferred_buffer_scale is sent - * - scaling factor of output we most recently were mapped on - * - if we're not mapped, use the last known scaling factor - * - if we're not mapped, and we don't have a last known scaling - * factor, use the scaling factor from the first available - * output. - * - if there aren't any outputs available, use 1.0 - */ - const float new_scale = (term_fractional_scaling(term) - ? win->scale - : term_preferred_buffer_scale(term) - ? win->preferred_buffer_scale - : tll_length(win->on_outputs) > 0 - ? tll_back(win->on_outputs)->scale - : term->scale_before_unmap > 0. - ? term->scale_before_unmap - : tll_length(term->wl->monitors) > 0 - ? tll_front(term->wl->monitors).scale - : 1.); - - if (new_scale == term->scale) - return false; - - LOG_DBG("scaling factor changed: %.2f -> %.2f", term->scale, new_scale); - term->scale_before_unmap = new_scale; - term->scale = new_scale; - return true; -} - -bool -term_font_dpi_changed(struct terminal *term, float old_scale) -{ - float dpi = get_font_dpi(term); - xassert(term->scale > 0.); - - bool was_scaled_using_dpi = term->font_is_sized_by_dpi; - bool will_scale_using_dpi = term->conf->dpi_aware; - - bool need_font_reload = - was_scaled_using_dpi != will_scale_using_dpi || - (will_scale_using_dpi - ? term->font_dpi != dpi - : old_scale != term->scale); - - if (need_font_reload) { - LOG_DBG("DPI/scale change: DPI-aware=%s, " - "DPI: %.2f -> %.2f, scale: %.2f -> %.2f, " - "sizing font based on monitor's %s", - term->conf->dpi_aware ? "yes" : "no", - term->font_dpi, dpi, old_scale, term->scale, - will_scale_using_dpi ? "DPI" : "scaling factor"); - } - - term->font_dpi = dpi; - term->font_dpi_before_unmap = dpi; - term->font_is_sized_by_dpi = will_scale_using_dpi; - - if (!need_font_reload) - return false; - - return reload_fonts(term, false); -} - -void -term_font_subpixel_changed(struct terminal *term) -{ - enum fcft_subpixel subpixel = get_font_subpixel(term); - - if (term->font_subpixel == subpixel) - return; + if (term->font_subpixel == subpixel) + return; #if defined(_DEBUG) && LOG_ENABLE_DBG - static const char *const str[] = { - [FCFT_SUBPIXEL_DEFAULT] = "default", - [FCFT_SUBPIXEL_NONE] = "disabled", - [FCFT_SUBPIXEL_HORIZONTAL_RGB] = "RGB", - [FCFT_SUBPIXEL_HORIZONTAL_BGR] = "BGR", - [FCFT_SUBPIXEL_VERTICAL_RGB] = "V-RGB", - [FCFT_SUBPIXEL_VERTICAL_BGR] = "V-BGR", - }; + static const char *const str[] = { + [FCFT_SUBPIXEL_DEFAULT] = "default", + [FCFT_SUBPIXEL_NONE] = "disabled", + [FCFT_SUBPIXEL_HORIZONTAL_RGB] = "RGB", + [FCFT_SUBPIXEL_HORIZONTAL_BGR] = "BGR", + [FCFT_SUBPIXEL_VERTICAL_RGB] = "V-RGB", + [FCFT_SUBPIXEL_VERTICAL_BGR] = "V-BGR", + }; - LOG_DBG("subpixel mode changed: %s -> %s", str[term->font_subpixel], str[subpixel]); + LOG_DBG("subpixel mode changed: %s -> %s", str[term->font_subpixel], + str[subpixel]); #endif - term->font_subpixel = subpixel; - term_damage_view(term); - render_refresh(term); + term->font_subpixel = subpixel; + term_damage_view(term); + render_refresh(term); } -int -term_font_baseline(const struct terminal *term) -{ - const struct fcft_font *font = term->fonts[0]; - const int line_height = term->cell_height; - const int font_height = font->ascent + font->descent; +int term_font_baseline(const struct terminal *term) { + const struct fcft_font *font = term->fonts[0]; + const int line_height = term->cell_height; + const int font_height = font->ascent + font->descent; + /* + * Center glyph on the line *if* using a custom line height, + * otherwise the baseline is simply 'descent' pixels above the + * bottom of the cell + */ + const int glyph_top_y = term->font_line_height.px >= 0 + ? round((line_height - font_height) / 2.) + : 0; + + return term->font_y_ofs + line_height - glyph_top_y - font->descent; +} + +void term_damage_rows(struct terminal *term, int start, int end) { + xassert(start <= end); + for (int r = start; r <= end; r++) { + struct row *row = grid_row(term->grid, r); + row->dirty = true; + for (int c = 0; c < term->grid->num_cols; c++) + row->cells[c].attrs.clean = 0; + } +} + +void term_damage_rows_in_view(struct terminal *term, int start, int end) { + xassert(start <= end); + for (int r = start; r <= end; r++) { + struct row *row = grid_row_in_view(term->grid, r); + row->dirty = true; + for (int c = 0; c < term->grid->num_cols; c++) + row->cells[c].attrs.clean = 0; + } +} + +void term_damage_all(struct terminal *term) { + term_damage_rows(term, 0, term->rows - 1); +} + +void term_damage_view(struct terminal *term) { + term_damage_rows_in_view(term, 0, term->rows - 1); +} + +void term_damage_cursor(struct terminal *term) { + term->grid->cur_row->cells[term->grid->cursor.point.col].attrs.clean = 0; + term->grid->cur_row->dirty = true; +} + +void term_damage_margins(struct terminal *term) { term->render.margins = true; } + +void term_damage_color(struct terminal *term, enum color_source src, int idx) { + xassert(src == COLOR_DEFAULT || src == COLOR_BASE256); + + for (int r = 0; r < term->rows; r++) { + struct row *row = grid_row_in_view(term->grid, r); + struct cell *cell = &row->cells[0]; + const struct cell *end = &row->cells[term->cols]; + + for (; cell < end; cell++) { + bool dirty = false; + + switch (cell->attrs.fg_src) { + case COLOR_BASE16: + case COLOR_BASE256: + if (src == COLOR_BASE256 && cell->attrs.fg == idx) + dirty = true; + break; + + case COLOR_DEFAULT: + if (src == COLOR_DEFAULT) { + /* Doesn't matter whether we've updated the + default foreground, or background, we still + want to dirty this cell, to be sure we handle + all cases of color inversion/reversal */ + dirty = true; + } + break; + + case COLOR_RGB: + /* Not affected */ + break; + } + + switch (cell->attrs.bg_src) { + case COLOR_BASE16: + case COLOR_BASE256: + if (src == COLOR_BASE256 && cell->attrs.bg == idx) + dirty = true; + break; + + case COLOR_DEFAULT: + if (src == COLOR_DEFAULT) { + /* Doesn't matter whether we've updated the + default foreground, or background, we still + want to dirty this cell, to be sure we handle + all cases of color inversion/reversal */ + dirty = true; + } + break; + + case COLOR_RGB: + /* Not affected */ + break; + } + + if (dirty) { + cell->attrs.clean = 0; + row->dirty = true; + } + } + + /* Colored underlines */ + if (row->extra != NULL) { + const struct row_ranges *underlines = &row->extra->underline_ranges; + + for (int i = 0; i < underlines->count; i++) { + const struct row_range *range = &underlines->v[i]; + + /* Underline colors are either default, or + BASE256/RGB, but never BASE16 */ + xassert(range->underline.color_src == COLOR_DEFAULT || + range->underline.color_src == COLOR_BASE256 || + range->underline.color_src == COLOR_RGB); + + if (range->underline.color_src == src) { + struct cell *c = &row->cells[range->start]; + const struct cell *e = &row->cells[range->end + 1]; + + for (; c < e; c++) + c->attrs.clean = 0; + + row->dirty = true; + } + } + } + } +} + +void term_damage_scroll(struct terminal *term, enum damage_type damage_type, + struct scroll_region region, int lines) { + if (likely(tll_length(term->grid->scroll_damage) > 0)) { + struct damage *dmg = &tll_back(term->grid->scroll_damage); + + if (likely(dmg->type == damage_type && dmg->region.start == region.start && + dmg->region.end == region.end)) { + /* Make sure we don't overflow... */ + int new_line_count = (int)dmg->lines + lines; + if (likely(new_line_count <= UINT16_MAX)) { + dmg->lines = new_line_count; + return; + } + } + } + struct damage dmg = { + .type = damage_type, + .region = region, + .lines = lines, + }; + tll_push_back(term->grid->scroll_damage, dmg); +} + +void term_erase(struct terminal *term, int start_row, int start_col, + int end_row, int end_col) { + xassert(start_row <= end_row); + xassert(start_col <= end_col || start_row < end_row); + + if (start_row == end_row) { + struct row *row = grid_row(term->grid, start_row); + erase_cell_range(term, row, start_col, end_col); + sixel_overwrite_by_row(term, start_row, start_col, end_col - start_col + 1); + return; + } + + xassert(end_row > start_row); + + erase_cell_range(term, grid_row(term->grid, start_row), start_col, + term->cols - 1); + sixel_overwrite_by_row(term, start_row, start_col, term->cols - start_col); + + for (int r = start_row + 1; r < end_row; r++) + erase_line(term, grid_row(term->grid, r)); + sixel_overwrite_by_rectangle(term, start_row + 1, 0, end_row - start_row, + term->cols); + + erase_cell_range(term, grid_row(term->grid, end_row), 0, end_col); + sixel_overwrite_by_row(term, end_row, 0, end_col + 1); +} + +void term_erase_scrollback(struct terminal *term) { + const struct grid *grid = term->grid; + const int num_rows = grid->num_rows; + const int mask = num_rows - 1; + + const int scrollback_history_size = num_rows - term->rows; + if (scrollback_history_size == 0) + return; + + const int start = (grid->offset + term->rows) & mask; + const int end = (grid->offset - 1) & mask; + + const int rel_start = grid_row_abs_to_sb(grid, term->rows, start); + const int rel_end = grid_row_abs_to_sb(grid, term->rows, end); + + const int sel_start = selection_get_start(term).row; + const int sel_end = selection_get_end(term).row; + + if (sel_end >= 0) { /* - * Center glyph on the line *if* using a custom line height, - * otherwise the baseline is simply 'descent' pixels above the - * bottom of the cell + * Cancel selection if it touches any of the rows in the + * scrollback, since we can't have the selection reference + * soon-to-be deleted rows. + * + * This is done by range checking the selection range against + * the scrollback range. + * + * To make this comparison simpler, the start/end absolute row + * numbers are "rebased" against the scrollback start, where + * row 0 is the *first* row in the scrollback. A high number + * thus means the row is further *down* in the scrollback, + * closer to the screen bottom. */ - const int glyph_top_y = term->font_line_height.px >= 0 - ? round((line_height - font_height) / 2.) - : 0; - return term->font_y_ofs + line_height - glyph_top_y - font->descent; -} + const int rel_sel_start = grid_row_abs_to_sb(grid, term->rows, sel_start); + const int rel_sel_end = grid_row_abs_to_sb(grid, term->rows, sel_end); -void -term_damage_rows(struct terminal *term, int start, int end) -{ - xassert(start <= end); - for (int r = start; r <= end; r++) { - struct row *row = grid_row(term->grid, r); - row->dirty = true; - for (int c = 0; c < term->grid->num_cols; c++) - row->cells[c].attrs.clean = 0; + if ((rel_sel_start <= rel_start && rel_sel_end >= rel_start) || + (rel_sel_start <= rel_end && rel_sel_end >= rel_end) || + (rel_sel_start >= rel_start && rel_sel_end <= rel_end)) { + selection_cancel(term); } -} + } -void -term_damage_rows_in_view(struct terminal *term, int start, int end) -{ - xassert(start <= end); - for (int r = start; r <= end; r++) { - struct row *row = grid_row_in_view(term->grid, r); - row->dirty = true; - for (int c = 0; c < term->grid->num_cols; c++) - row->cells[c].attrs.clean = 0; + tll_foreach(term->grid->sixel_images, it) { + struct sixel *six = &it->item; + const int six_start = grid_row_abs_to_sb(grid, term->rows, six->pos.row); + const int six_end = + grid_row_abs_to_sb(grid, term->rows, six->pos.row + six->rows - 1); + + if ((six_start <= rel_start && six_end >= rel_start) || + (six_start <= rel_end && six_end >= rel_end) || + (six_start >= rel_start && six_end <= rel_end)) { + sixel_destroy(six); + tll_remove(term->grid->sixel_images, it); } -} + } -void -term_damage_all(struct terminal *term) -{ - term_damage_rows(term, 0, term->rows - 1); -} + for (int i = start;; i = (i + 1) & mask) { + struct row *row = term->grid->rows[i]; + if (row != NULL) { + if (term->render.last_cursor.row == row) + term->render.last_cursor.row = NULL; -void -term_damage_view(struct terminal *term) -{ - term_damage_rows_in_view(term, 0, term->rows - 1); -} - -void -term_damage_cursor(struct terminal *term) -{ - term->grid->cur_row->cells[term->grid->cursor.point.col].attrs.clean = 0; - term->grid->cur_row->dirty = true; -} - -void -term_damage_margins(struct terminal *term) -{ - term->render.margins = true; -} - -void -term_damage_color(struct terminal *term, enum color_source src, int idx) -{ - xassert(src == COLOR_DEFAULT || src == COLOR_BASE256); - - for (int r = 0; r < term->rows; r++) { - struct row *row = grid_row_in_view(term->grid, r); - struct cell *cell = &row->cells[0]; - const struct cell *end = &row->cells[term->cols]; - - for (; cell < end; cell++) { - bool dirty = false; - - switch (cell->attrs.fg_src) { - case COLOR_BASE16: - case COLOR_BASE256: - if (src == COLOR_BASE256 && cell->attrs.fg == idx) - dirty = true; - break; - - case COLOR_DEFAULT: - if (src == COLOR_DEFAULT) { - /* Doesn't matter whether we've updated the - default foreground, or background, we still - want to dirty this cell, to be sure we handle - all cases of color inversion/reversal */ - dirty = true; - } - break; - - case COLOR_RGB: - /* Not affected */ - break; - } - - switch (cell->attrs.bg_src) { - case COLOR_BASE16: - case COLOR_BASE256: - if (src == COLOR_BASE256 && cell->attrs.bg == idx) - dirty = true; - break; - - case COLOR_DEFAULT: - if (src == COLOR_DEFAULT) { - /* Doesn't matter whether we've updated the - default foreground, or background, we still - want to dirty this cell, to be sure we handle - all cases of color inversion/reversal */ - dirty = true; - } - break; - - case COLOR_RGB: - /* Not affected */ - break; - } - - if (dirty) { - cell->attrs.clean = 0; - row->dirty = true; - } - } - - /* Colored underlines */ - if (row->extra != NULL) { - const struct row_ranges *underlines = &row->extra->underline_ranges; - - for (int i = 0; i < underlines->count; i++) { - const struct row_range *range = &underlines->v[i]; - - /* Underline colors are either default, or - BASE256/RGB, but never BASE16 */ - xassert(range->underline.color_src == COLOR_DEFAULT || - range->underline.color_src == COLOR_BASE256 || - range->underline.color_src == COLOR_RGB); - - if (range->underline.color_src == src) { - struct cell *c = &row->cells[range->start]; - const struct cell *e = &row->cells[range->end + 1]; - - for (; c < e; c++) - c->attrs.clean = 0; - - row->dirty = true; - } - } - } - } -} - -void -term_damage_scroll(struct terminal *term, enum damage_type damage_type, - struct scroll_region region, int lines) -{ - if (likely(tll_length(term->grid->scroll_damage) > 0)) { - struct damage *dmg = &tll_back(term->grid->scroll_damage); - - if (likely( - dmg->type == damage_type && - dmg->region.start == region.start && - dmg->region.end == region.end)) - { - /* Make sure we don't overflow... */ - int new_line_count = (int)dmg->lines + lines; - if (likely(new_line_count <= UINT16_MAX)) { - dmg->lines = new_line_count; - return; - } - } - } - struct damage dmg = { - .type = damage_type, - .region = region, - .lines = lines, - }; - tll_push_back(term->grid->scroll_damage, dmg); -} - -void -term_erase(struct terminal *term, int start_row, int start_col, - int end_row, int end_col) -{ - xassert(start_row <= end_row); - xassert(start_col <= end_col || start_row < end_row); - - if (start_row == end_row) { - struct row *row = grid_row(term->grid, start_row); - erase_cell_range(term, row, start_col, end_col); - sixel_overwrite_by_row(term, start_row, start_col, end_col - start_col + 1); - return; + grid_row_free(row); + term->grid->rows[i] = NULL; } - xassert(end_row > start_row); + if (i == end) + break; + } - erase_cell_range( - term, grid_row(term->grid, start_row), start_col, term->cols - 1); - sixel_overwrite_by_row(term, start_row, start_col, term->cols - start_col); - - for (int r = start_row + 1; r < end_row; r++) - erase_line(term, grid_row(term->grid, r)); - sixel_overwrite_by_rectangle( - term, start_row + 1, 0, end_row - start_row, term->cols); - - erase_cell_range(term, grid_row(term->grid, end_row), 0, end_col); - sixel_overwrite_by_row(term, end_row, 0, end_col + 1); -} - -void -term_erase_scrollback(struct terminal *term) -{ - const struct grid *grid = term->grid; - const int num_rows = grid->num_rows; - const int mask = num_rows - 1; - - const int scrollback_history_size = num_rows - term->rows; - if (scrollback_history_size == 0) - return; - - const int start = (grid->offset + term->rows) & mask; - const int end = (grid->offset - 1) & mask; - - const int rel_start = grid_row_abs_to_sb(grid, term->rows, start); - const int rel_end = grid_row_abs_to_sb(grid, term->rows, end); - - const int sel_start = selection_get_start(term).row; - const int sel_end = selection_get_end(term).row; - - if (sel_end >= 0) { - /* - * Cancel selection if it touches any of the rows in the - * scrollback, since we can't have the selection reference - * soon-to-be deleted rows. - * - * This is done by range checking the selection range against - * the scrollback range. - * - * To make this comparison simpler, the start/end absolute row - * numbers are "rebased" against the scrollback start, where - * row 0 is the *first* row in the scrollback. A high number - * thus means the row is further *down* in the scrollback, - * closer to the screen bottom. - */ - - const int rel_sel_start = grid_row_abs_to_sb(grid, term->rows, sel_start); - const int rel_sel_end = grid_row_abs_to_sb(grid, term->rows, sel_end); - - if ((rel_sel_start <= rel_start && rel_sel_end >= rel_start) || - (rel_sel_start <= rel_end && rel_sel_end >= rel_end) || - (rel_sel_start >= rel_start && rel_sel_end <= rel_end)) - { - selection_cancel(term); - } - } - - tll_foreach(term->grid->sixel_images, it) { - struct sixel *six = &it->item; - const int six_start = grid_row_abs_to_sb(grid, term->rows, six->pos.row); - const int six_end = grid_row_abs_to_sb( - grid, term->rows, six->pos.row + six->rows - 1); - - if ((six_start <= rel_start && six_end >= rel_start) || - (six_start <= rel_end && six_end >= rel_end) || - (six_start >= rel_start && six_end <= rel_end)) - { - sixel_destroy(six); - tll_remove(term->grid->sixel_images, it); - } - } - - for (int i = start;; i = (i + 1) & mask) { - struct row *row = term->grid->rows[i]; - if (row != NULL) { - if (term->render.last_cursor.row == row) - term->render.last_cursor.row = NULL; - - grid_row_free(row); - term->grid->rows[i] = NULL; - } - - if (i == end) - break; - } - - term->grid->view = term->grid->offset; + term->grid->view = term->grid->offset; #if defined(_DEBUG) - for (int i = 0; i < term->rows; i++) { - xassert(grid_row_in_view(term->grid, i) != NULL); - } + for (int i = 0; i < term->rows; i++) { + xassert(grid_row_in_view(term->grid, i) != NULL); + } #endif - term_damage_view(term); + term_damage_view(term); } -UNITTEST -{ - const int scrollback_rows = 16; - const int term_rows = 5; - const int cols = 5; +UNITTEST { + const int scrollback_rows = 16; + const int term_rows = 5; + const int cols = 5; - struct fdm *fdm = fdm_init(); - xassert(fdm != NULL); + struct fdm *fdm = fdm_init(); + xassert(fdm != NULL); - struct terminal term = { - .fdm = fdm, - .rows = term_rows, - .cols = cols, - .normal = { - .rows = xcalloc(scrollback_rows, sizeof(term.normal.rows[0])), - .num_rows = scrollback_rows, - .num_cols = cols, - }, - .grid = &term.normal, - .selection = { - .coords = { - .start = {-1, -1}, - .end = {-1, -1}, - }, - .kind = SELECTION_NONE, - .auto_scroll = { - .fd = -1, - }, - }, - }; + struct terminal term = { + .fdm = fdm, + .rows = term_rows, + .cols = cols, + .normal = + { + .rows = xcalloc(scrollback_rows, sizeof(term.normal.rows[0])), + .num_rows = scrollback_rows, + .num_cols = cols, + }, + .grid = &term.normal, + .selection = + { + .coords = + { + .start = {-1, -1}, + .end = {-1, -1}, + }, + .kind = SELECTION_NONE, + .auto_scroll = + { + .fd = -1, + }, + }, + }; -#define populate_scrollback() do { \ - for (int i = 0; i < scrollback_rows; i++) { \ - if (term.normal.rows[i] == NULL) { \ - struct row *r = xcalloc(1, sizeof(*term.normal.rows[i])); \ - r->cells = xcalloc(cols, sizeof(r->cells[0])); \ - term.normal.rows[i] = r; \ - } \ - } \ - } while (0) +#define populate_scrollback() \ + do { \ + for (int i = 0; i < scrollback_rows; i++) { \ + if (term.normal.rows[i] == NULL) { \ + struct row *r = xcalloc(1, sizeof(*term.normal.rows[i])); \ + r->cells = xcalloc(cols, sizeof(r->cells[0])); \ + term.normal.rows[i] = r; \ + } \ + } \ + } while (0) - /* - * Test case 1 - no selection, just verify all rows except those - * on screen have been deleted. - */ + /* + * Test case 1 - no selection, just verify all rows except those + * on screen have been deleted. + */ - populate_scrollback(); - term.normal.offset = 11; - term_erase_scrollback(&term); - for (int i = 0; i < scrollback_rows; i++) { - if (i >= term.normal.offset && i < term.normal.offset + term_rows) - xassert(term.normal.rows[i] != NULL); - else - xassert(term.normal.rows[i] == NULL); + populate_scrollback(); + term.normal.offset = 11; + term_erase_scrollback(&term); + for (int i = 0; i < scrollback_rows; i++) { + if (i >= term.normal.offset && i < term.normal.offset + term_rows) + xassert(term.normal.rows[i] != NULL); + else + xassert(term.normal.rows[i] == NULL); + } + + /* + * Test case 2 - selection that touches the scrollback. Verify the + * selection is cancelled. + */ + + term.normal.offset = 14; /* Screen covers rows 14,15,0,1,2 */ + + /* Selection covers rows 15,0,1,2,3 */ + term.selection.coords.start = (struct coord){.row = 15}; + term.selection.coords.end = (struct coord){.row = 19}; + term.selection.kind = SELECTION_CHAR_WISE; + + populate_scrollback(); + term_erase_scrollback(&term); + xassert(term.selection.coords.start.row < 0); + xassert(term.selection.coords.end.row < 0); + xassert(term.selection.kind == SELECTION_NONE); + + /* + * Test case 3 - selection that does *not* touch the + * scrollback. Verify the selection is *not* cancelled. + */ + + /* Selection covers rows 15,0 */ + term.selection.coords.start = (struct coord){.row = 15}; + term.selection.coords.end = (struct coord){.row = 16}; + term.selection.kind = SELECTION_CHAR_WISE; + + populate_scrollback(); + term_erase_scrollback(&term); + xassert(term.selection.coords.start.row == 15); + xassert(term.selection.coords.end.row == 16); + xassert(term.selection.kind == SELECTION_CHAR_WISE); + + term.selection.coords.start = (struct coord){-1, -1}; + term.selection.coords.end = (struct coord){-1, -1}; + term.selection.kind = SELECTION_NONE; + + /* + * Test case 4 - sixel that touch the scrollback + */ + + struct sixel six = { + .rows = 5, + .pos = + { + .row = 15, + }, + }; + tll_push_back(term.normal.sixel_images, six); + populate_scrollback(); + term_erase_scrollback(&term); + xassert(tll_length(term.normal.sixel_images) == 0); + + /* + * Test case 5 - sixel that does *not* touch the scrollback + */ + six.rows = 3; + tll_push_back(term.normal.sixel_images, six); + populate_scrollback(); + term_erase_scrollback(&term); + xassert(tll_length(term.normal.sixel_images) == 1); + + /* Cleanup */ + tll_free(term.normal.sixel_images); + xassert(term.selection.auto_scroll.fd == -1); + for (int i = 0; i < scrollback_rows; i++) + grid_row_free(term.normal.rows[i]); + free(term.normal.rows); + fdm_destroy(fdm); +} + +int term_row_rel_to_abs(const struct terminal *term, int row) { + switch (term->origin) { + case ORIGIN_ABSOLUTE: + return min(row, term->rows - 1); + + case ORIGIN_RELATIVE: + return min(row + term->scroll_region.start, term->scroll_region.end - 1); + } + + BUG("Invalid cursor_origin value"); + return -1; +} + +void term_cursor_to(struct terminal *term, int row, int col) { + xassert(row < term->rows); + xassert(col < term->cols); + + term->grid->cursor.lcf = false; + + term->grid->cursor.point.col = col; + term->grid->cursor.point.row = row; + + term_reset_grapheme_state(term); + + term->grid->cur_row = grid_row(term->grid, row); +} + +void term_cursor_home(struct terminal *term) { + term_cursor_to(term, term_row_rel_to_abs(term, 0), 0); +} + +void term_cursor_col(struct terminal *term, int col) { + xassert(col < term->cols); + + term->grid->cursor.lcf = false; + term->grid->cursor.point.col = col; + term_reset_grapheme_state(term); +} + +void term_cursor_left(struct terminal *term, int count) { + int move_amount = min(term->grid->cursor.point.col, count); + term->grid->cursor.point.col -= move_amount; + xassert(term->grid->cursor.point.col >= 0); + term->grid->cursor.lcf = false; + term_reset_grapheme_state(term); +} + +void term_cursor_right(struct terminal *term, int count) { + int move_amount = min(term->cols - term->grid->cursor.point.col - 1, count); + term->grid->cursor.point.col += move_amount; + xassert(term->grid->cursor.point.col < term->cols); + term->grid->cursor.lcf = false; + term_reset_grapheme_state(term); +} + +void term_cursor_up(struct terminal *term, int count) { + int top = term->origin == ORIGIN_ABSOLUTE ? 0 : term->scroll_region.start; + xassert(term->grid->cursor.point.row >= top); + + int move_amount = min(term->grid->cursor.point.row - top, count); + term_cursor_to(term, term->grid->cursor.point.row - move_amount, + term->grid->cursor.point.col); +} + +void term_cursor_down(struct terminal *term, int count) { + int bottom = + term->origin == ORIGIN_ABSOLUTE ? term->rows : term->scroll_region.end; + xassert(bottom >= term->grid->cursor.point.row); + + int move_amount = min(bottom - term->grid->cursor.point.row - 1, count); + term_cursor_to(term, term->grid->cursor.point.row + move_amount, + term->grid->cursor.point.col); +} + +static bool cursor_blink_rearm_timer(struct terminal *term) { + if (term->cursor_blink.fd < 0) { + int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (fd < 0) { + LOG_ERRNO("failed to create cursor blink timer FD"); + return false; } - /* - * Test case 2 - selection that touches the scrollback. Verify the - * selection is cancelled. - */ - - term.normal.offset = 14; /* Screen covers rows 14,15,0,1,2 */ - - /* Selection covers rows 15,0,1,2,3 */ - term.selection.coords.start = (struct coord){.row = 15}; - term.selection.coords.end = (struct coord){.row = 19}; - term.selection.kind = SELECTION_CHAR_WISE; - - populate_scrollback(); - term_erase_scrollback(&term); - xassert(term.selection.coords.start.row < 0); - xassert(term.selection.coords.end.row < 0); - xassert(term.selection.kind == SELECTION_NONE); - - /* - * Test case 3 - selection that does *not* touch the - * scrollback. Verify the selection is *not* cancelled. - */ - - /* Selection covers rows 15,0 */ - term.selection.coords.start = (struct coord){.row = 15}; - term.selection.coords.end = (struct coord){.row = 16}; - term.selection.kind = SELECTION_CHAR_WISE; - - populate_scrollback(); - term_erase_scrollback(&term); - xassert(term.selection.coords.start.row == 15); - xassert(term.selection.coords.end.row == 16); - xassert(term.selection.kind == SELECTION_CHAR_WISE); - - term.selection.coords.start = (struct coord){-1, -1}; - term.selection.coords.end = (struct coord){-1, -1}; - term.selection.kind = SELECTION_NONE; - - /* - * Test case 4 - sixel that touch the scrollback - */ - - struct sixel six = { - .rows = 5, - .pos = { - .row = 15, - }, - }; - tll_push_back(term.normal.sixel_images, six); - populate_scrollback(); - term_erase_scrollback(&term); - xassert(tll_length(term.normal.sixel_images) == 0); - - /* - * Test case 5 - sixel that does *not* touch the scrollback - */ - six.rows = 3; - tll_push_back(term.normal.sixel_images, six); - populate_scrollback(); - term_erase_scrollback(&term); - xassert(tll_length(term.normal.sixel_images) == 1); - - /* Cleanup */ - tll_free(term.normal.sixel_images); - xassert(term.selection.auto_scroll.fd == -1); - for (int i = 0; i < scrollback_rows; i++) - grid_row_free(term.normal.rows[i]); - free(term.normal.rows); - fdm_destroy(fdm); -} - -int -term_row_rel_to_abs(const struct terminal *term, int row) -{ - switch (term->origin) { - case ORIGIN_ABSOLUTE: - return min(row, term->rows - 1); - - case ORIGIN_RELATIVE: - return min(row + term->scroll_region.start, term->scroll_region.end - 1); + if (!fdm_add(term->fdm, fd, EPOLLIN, &fdm_cursor_blink, term)) { + close(fd); + return false; } - BUG("Invalid cursor_origin value"); - return -1; -} + term->cursor_blink.fd = fd; + } -void -term_cursor_to(struct terminal *term, int row, int col) -{ - xassert(row < term->rows); - xassert(col < term->cols); + const int rate_ms = term->conf->cursor.blink.rate_ms; + const long secs = rate_ms / 1000; + const long nsecs = (rate_ms % 1000) * 1000000; - term->grid->cursor.lcf = false; + const struct itimerspec timer = { + .it_value = {.tv_sec = secs, .tv_nsec = nsecs}, + .it_interval = {.tv_sec = secs, .tv_nsec = nsecs}, + }; - term->grid->cursor.point.col = col; - term->grid->cursor.point.row = row; - - term_reset_grapheme_state(term); - - term->grid->cur_row = grid_row(term->grid, row); -} - -void -term_cursor_home(struct terminal *term) -{ - term_cursor_to(term, term_row_rel_to_abs(term, 0), 0); -} - -void -term_cursor_col(struct terminal *term, int col) -{ - xassert(col < term->cols); - - term->grid->cursor.lcf = false; - term->grid->cursor.point.col = col; - term_reset_grapheme_state(term); -} - -void -term_cursor_left(struct terminal *term, int count) -{ - int move_amount = min(term->grid->cursor.point.col, count); - term->grid->cursor.point.col -= move_amount; - xassert(term->grid->cursor.point.col >= 0); - term->grid->cursor.lcf = false; - term_reset_grapheme_state(term); -} - -void -term_cursor_right(struct terminal *term, int count) -{ - int move_amount = min(term->cols - term->grid->cursor.point.col - 1, count); - term->grid->cursor.point.col += move_amount; - xassert(term->grid->cursor.point.col < term->cols); - term->grid->cursor.lcf = false; - term_reset_grapheme_state(term); -} - -void -term_cursor_up(struct terminal *term, int count) -{ - int top = term->origin == ORIGIN_ABSOLUTE ? 0 : term->scroll_region.start; - xassert(term->grid->cursor.point.row >= top); - - int move_amount = min(term->grid->cursor.point.row - top, count); - term_cursor_to(term, term->grid->cursor.point.row - move_amount, term->grid->cursor.point.col); -} - -void -term_cursor_down(struct terminal *term, int count) -{ - int bottom = term->origin == ORIGIN_ABSOLUTE ? term->rows : term->scroll_region.end; - xassert(bottom >= term->grid->cursor.point.row); - - int move_amount = min(bottom - term->grid->cursor.point.row - 1, count); - term_cursor_to(term, term->grid->cursor.point.row + move_amount, term->grid->cursor.point.col); -} - -static bool -cursor_blink_rearm_timer(struct terminal *term) -{ - if (term->cursor_blink.fd < 0) { - int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); - if (fd < 0) { - LOG_ERRNO("failed to create cursor blink timer FD"); - return false; - } - - if (!fdm_add(term->fdm, fd, EPOLLIN, &fdm_cursor_blink, term)) { - close(fd); - return false; - } - - term->cursor_blink.fd = fd; - } - - const int rate_ms = term->conf->cursor.blink.rate_ms; - const long secs = rate_ms / 1000; - const long nsecs = (rate_ms % 1000) * 1000000; - - const struct itimerspec timer = { - .it_value = {.tv_sec = secs, .tv_nsec = nsecs}, - .it_interval = {.tv_sec = secs, .tv_nsec = nsecs}, - }; - - if (timerfd_settime(term->cursor_blink.fd, 0, &timer, NULL) < 0) { - LOG_ERRNO("failed to arm cursor blink timer"); - fdm_del(term->fdm, term->cursor_blink.fd); - term->cursor_blink.fd = -1; - return false; - } - - return true; -} - -static bool -cursor_blink_disarm_timer(struct terminal *term) -{ + if (timerfd_settime(term->cursor_blink.fd, 0, &timer, NULL) < 0) { + LOG_ERRNO("failed to arm cursor blink timer"); fdm_del(term->fdm, term->cursor_blink.fd); term->cursor_blink.fd = -1; - return true; + return false; + } + + return true; } -void -term_cursor_blink_update(struct terminal *term) -{ - bool enable = term->cursor_blink.decset || term->cursor_blink.deccsusr; - bool activate = !term->shutdown.in_progress && enable && term->visual_focus; - - LOG_DBG("decset=%d, deccsrusr=%d, focus=%d, shutting-down=%d, enable=%d, activate=%d", - term->cursor_blink.decset, term->cursor_blink.deccsusr, - term->visual_focus, term->shutdown.in_progress, - enable, activate); - - if (activate && term->cursor_blink.fd < 0) { - term->cursor_blink.state = CURSOR_BLINK_ON; - cursor_blink_rearm_timer(term); - } else if (!activate && term->cursor_blink.fd >= 0) - cursor_blink_disarm_timer(term); +static bool cursor_blink_disarm_timer(struct terminal *term) { + fdm_del(term->fdm, term->cursor_blink.fd); + term->cursor_blink.fd = -1; + return true; } -static bool -selection_on_top_region(const struct terminal *term, - struct scroll_region region) -{ - return region.start > 0 && - selection_on_rows(term, 0, region.start - 1); +void term_cursor_blink_update(struct terminal *term) { + bool enable = term->cursor_blink.decset || term->cursor_blink.deccsusr; + bool activate = !term->shutdown.in_progress && enable && term->visual_focus; + + LOG_DBG("decset=%d, deccsrusr=%d, focus=%d, shutting-down=%d, enable=%d, " + "activate=%d", + term->cursor_blink.decset, term->cursor_blink.deccsusr, + term->visual_focus, term->shutdown.in_progress, enable, activate); + + if (activate && term->cursor_blink.fd < 0) { + term->cursor_blink.state = CURSOR_BLINK_ON; + cursor_blink_rearm_timer(term); + } else if (!activate && term->cursor_blink.fd >= 0) + cursor_blink_disarm_timer(term); } -static bool -selection_on_bottom_region(const struct terminal *term, - struct scroll_region region) -{ - return region.end < term->rows && - selection_on_rows(term, region.end, term->rows - 1); +static bool selection_on_top_region(const struct terminal *term, + struct scroll_region region) { + return region.start > 0 && selection_on_rows(term, 0, region.start - 1); } -void -term_scroll_partial(struct terminal *term, struct scroll_region region, int rows) -{ - LOG_DBG("scroll: rows=%d, region.start=%d, region.end=%d", - rows, region.start, region.end); - - /* Verify scroll amount has been clamped */ - xassert(rows <= region.end - region.start); - - /* Cancel selections that cannot be scrolled */ - if (unlikely(term->selection.coords.end.row >= 0)) { - /* - * Selection is (partly) inside either the top or bottom - * scrolling regions, or on (at least one) of the lines - * scrolled in (i.e. reused lines). - */ - if (selection_on_top_region(term, region) || - selection_on_bottom_region(term, region)) - { - selection_cancel(term); - } else - selection_scroll_up(term, rows); - } - - sixel_scroll_up(term, rows); - - /* How many lines from the scrollback start is the current viewport? */ - int view_sb_start_distance = grid_row_abs_to_sb( - term->grid, term->rows, term->grid->view); - - bool view_follows = term->grid->view == term->grid->offset; - term->grid->offset += rows; - term->grid->offset &= term->grid->num_rows - 1; - - if (likely(view_follows)) { - term_damage_scroll(term, DAMAGE_SCROLL, region, rows); - selection_view_down(term, term->grid->offset); - term->grid->view = term->grid->offset; - } else if (unlikely(rows > view_sb_start_distance)) { - /* Part of current view is being scrolled out */ - int new_view = grid_row_sb_to_abs(term->grid, term->rows, 0); - selection_view_down(term, new_view); - cmd_scrollback_down(term, rows - view_sb_start_distance); - } - - /* Top non-scrolling region. */ - for (int i = region.start - 1; i >= 0; i--) - grid_swap_row(term->grid, i - rows, i); - - /* Bottom non-scrolling region */ - for (int i = term->rows - 1; i >= region.end; i--) - grid_swap_row(term->grid, i - rows, i); - - /* Erase scrolled in lines */ - for (int r = region.end - rows; r < region.end; r++) { - struct row *row = grid_row_and_alloc(term->grid, r); - erase_line(term, row); - } - - term->grid->cur_row = grid_row(term->grid, term->grid->cursor.point.row); - -#if defined(_DEBUG) - for (int r = 0; r < term->rows; r++) - xassert(grid_row(term->grid, r) != NULL); -#endif +static bool selection_on_bottom_region(const struct terminal *term, + struct scroll_region region) { + return region.end < term->rows && + selection_on_rows(term, region.end, term->rows - 1); } -void -term_scroll(struct terminal *term, int rows) -{ - term_scroll_partial(term, term->scroll_region, rows); -} +void term_scroll_partial(struct terminal *term, struct scroll_region region, + int rows) { + LOG_DBG("scroll: rows=%d, region.start=%d, region.end=%d", rows, region.start, + region.end); -void -term_scroll_reverse_partial(struct terminal *term, - struct scroll_region region, int rows) -{ - LOG_DBG("scroll reverse: rows=%d, region.start=%d, region.end=%d", - rows, region.start, region.end); + /* Verify scroll amount has been clamped */ + xassert(rows <= region.end - region.start); - /* Verify scroll amount has been clamped */ - xassert(rows <= region.end - region.start); + /* Cancel selections that cannot be scrolled */ + if (unlikely(term->selection.coords.end.row >= 0)) { + /* + * Selection is (partly) inside either the top or bottom + * scrolling regions, or on (at least one) of the lines + * scrolled in (i.e. reused lines). + */ + if (selection_on_top_region(term, region) || + selection_on_bottom_region(term, region)) { + selection_cancel(term); + } else + selection_scroll_up(term, rows); + } - /* Cancel selections that cannot be scrolled */ - if (unlikely(term->selection.coords.end.row >= 0)) { - /* - * Selection is (partly) inside either the top or bottom - * scrolling regions, or on (at least one) of the lines - * scrolled in (i.e. reused lines). - */ - if (selection_on_top_region(term, region) || - selection_on_bottom_region(term, region)) - { - selection_cancel(term); - } else - selection_scroll_down(term, rows); - } + sixel_scroll_up(term, rows); - /* Unallocate scrolled out lines */ - for (int r = region.end - rows; r < region.end; r++) { - const int abs_r = grid_row_absolute(term->grid, r); - struct row *row = term->grid->rows[abs_r]; + /* How many lines from the scrollback start is the current viewport? */ + int view_sb_start_distance = + grid_row_abs_to_sb(term->grid, term->rows, term->grid->view); - grid_row_free(row); - term->grid->rows[abs_r] = NULL; - - if (term->render.last_cursor.row == row) - term->render.last_cursor.row = NULL; - } - - sixel_scroll_down(term, rows); - - const bool view_follows = term->grid->view == term->grid->offset; - term->grid->offset -= rows; - term->grid->offset += term->grid->num_rows; - term->grid->offset &= term->grid->num_rows - 1; - - /* How many lines from the scrollback start is the current viewport? */ - const int view_sb_start_distance = grid_row_abs_to_sb( - term->grid, term->rows, term->grid->view); - const int offset_sb_start_distance = grid_row_abs_to_sb( - term->grid, term->rows, term->grid->offset); - - xassert(term->grid->offset >= 0); - xassert(term->grid->offset < term->grid->num_rows); - - if (view_follows) { - term_damage_scroll(term, DAMAGE_SCROLL_REVERSE, region, rows); - selection_view_up(term, term->grid->offset); - term->grid->view = term->grid->offset; - } else if (unlikely(view_sb_start_distance > offset_sb_start_distance)) { - /* Part of current view is being scrolled out */ - int new_view = term->grid->offset; - selection_view_up(term, new_view); - term->grid->view = new_view; - } - - /* Bottom non-scrolling region */ - for (int i = region.end + rows; i < term->rows + rows; i++) - grid_swap_row(term->grid, i, i - rows); - - /* Top non-scrolling region */ - for (int i = 0 + rows; i < region.start + rows; i++) - grid_swap_row(term->grid, i, i - rows); - - /* Erase scrolled in lines */ - for (int r = region.start; r < region.start + rows; r++) { - struct row *row = grid_row_and_alloc(term->grid, r); - erase_line(term, row); - } - - if (unlikely(view_sb_start_distance > offset_sb_start_distance)) - term_damage_view(term); - - term->grid->cur_row = grid_row(term->grid, term->grid->cursor.point.row); - -#if defined(_DEBUG) - for (int r = 0; r < term->rows; r++) - xassert(grid_row(term->grid, r) != NULL); - for (int r = 0; r < term->rows; r++) - xassert(grid_row_in_view(term->grid, r) != NULL); -#endif -} - -void -term_scroll_reverse(struct terminal *term, int rows) -{ - term_scroll_reverse_partial(term, term->scroll_region, rows); -} - -void -term_carriage_return(struct terminal *term) -{ - term_cursor_left(term, term->grid->cursor.point.col); -} - -void -term_linefeed(struct terminal *term) -{ - term->grid->cursor.lcf = false; - - if (term->grid->cursor.point.row == term->scroll_region.end - 1) - term_scroll(term, 1); - else - term_cursor_down(term, 1); - - term_reset_grapheme_state(term); -} - -void -term_reverse_index(struct terminal *term) -{ - if (term->grid->cursor.point.row == term->scroll_region.start) - term_scroll_reverse(term, 1); - else - term_cursor_up(term, 1); -} - -void -term_reset_view(struct terminal *term) -{ - if (term->grid->view == term->grid->offset) - return; + bool view_follows = term->grid->view == term->grid->offset; + term->grid->offset += rows; + term->grid->offset &= term->grid->num_rows - 1; + if (likely(view_follows)) { + term_damage_scroll(term, DAMAGE_SCROLL, region, rows); + selection_view_down(term, term->grid->offset); term->grid->view = term->grid->offset; + } else if (unlikely(rows > view_sb_start_distance)) { + /* Part of current view is being scrolled out */ + int new_view = grid_row_sb_to_abs(term->grid, term->rows, 0); + selection_view_down(term, new_view); + cmd_scrollback_down(term, rows - view_sb_start_distance); + } + + /* Top non-scrolling region. */ + for (int i = region.start - 1; i >= 0; i--) + grid_swap_row(term->grid, i - rows, i); + + /* Bottom non-scrolling region */ + for (int i = term->rows - 1; i >= region.end; i--) + grid_swap_row(term->grid, i - rows, i); + + /* Erase scrolled in lines */ + for (int r = region.end - rows; r < region.end; r++) { + struct row *row = grid_row_and_alloc(term->grid, r); + erase_line(term, row); + } + + term->grid->cur_row = grid_row(term->grid, term->grid->cursor.point.row); + +#if defined(_DEBUG) + for (int r = 0; r < term->rows; r++) + xassert(grid_row(term->grid, r) != NULL); +#endif +} + +void term_scroll(struct terminal *term, int rows) { + term_scroll_partial(term, term->scroll_region, rows); +} + +void term_scroll_reverse_partial(struct terminal *term, + struct scroll_region region, int rows) { + LOG_DBG("scroll reverse: rows=%d, region.start=%d, region.end=%d", rows, + region.start, region.end); + + /* Verify scroll amount has been clamped */ + xassert(rows <= region.end - region.start); + + /* Cancel selections that cannot be scrolled */ + if (unlikely(term->selection.coords.end.row >= 0)) { + /* + * Selection is (partly) inside either the top or bottom + * scrolling regions, or on (at least one) of the lines + * scrolled in (i.e. reused lines). + */ + if (selection_on_top_region(term, region) || + selection_on_bottom_region(term, region)) { + selection_cancel(term); + } else + selection_scroll_down(term, rows); + } + + /* Unallocate scrolled out lines */ + for (int r = region.end - rows; r < region.end; r++) { + const int abs_r = grid_row_absolute(term->grid, r); + struct row *row = term->grid->rows[abs_r]; + + grid_row_free(row); + term->grid->rows[abs_r] = NULL; + + if (term->render.last_cursor.row == row) + term->render.last_cursor.row = NULL; + } + + sixel_scroll_down(term, rows); + + const bool view_follows = term->grid->view == term->grid->offset; + term->grid->offset -= rows; + term->grid->offset += term->grid->num_rows; + term->grid->offset &= term->grid->num_rows - 1; + + /* How many lines from the scrollback start is the current viewport? */ + const int view_sb_start_distance = + grid_row_abs_to_sb(term->grid, term->rows, term->grid->view); + const int offset_sb_start_distance = + grid_row_abs_to_sb(term->grid, term->rows, term->grid->offset); + + xassert(term->grid->offset >= 0); + xassert(term->grid->offset < term->grid->num_rows); + + if (view_follows) { + term_damage_scroll(term, DAMAGE_SCROLL_REVERSE, region, rows); + selection_view_up(term, term->grid->offset); + term->grid->view = term->grid->offset; + } else if (unlikely(view_sb_start_distance > offset_sb_start_distance)) { + /* Part of current view is being scrolled out */ + int new_view = term->grid->offset; + selection_view_up(term, new_view); + term->grid->view = new_view; + } + + /* Bottom non-scrolling region */ + for (int i = region.end + rows; i < term->rows + rows; i++) + grid_swap_row(term->grid, i, i - rows); + + /* Top non-scrolling region */ + for (int i = 0 + rows; i < region.start + rows; i++) + grid_swap_row(term->grid, i, i - rows); + + /* Erase scrolled in lines */ + for (int r = region.start; r < region.start + rows; r++) { + struct row *row = grid_row_and_alloc(term->grid, r); + erase_line(term, row); + } + + if (unlikely(view_sb_start_distance > offset_sb_start_distance)) term_damage_view(term); + + term->grid->cur_row = grid_row(term->grid, term->grid->cursor.point.row); + +#if defined(_DEBUG) + for (int r = 0; r < term->rows; r++) + xassert(grid_row(term->grid, r) != NULL); + for (int r = 0; r < term->rows; r++) + xassert(grid_row_in_view(term->grid, r) != NULL); +#endif } -void -term_save_cursor(struct terminal *term) -{ - term->grid->saved_cursor = term->grid->cursor; - term->vt.saved_attrs = term->vt.attrs; - term->saved_charsets = term->charsets; +void term_scroll_reverse(struct terminal *term, int rows) { + term_scroll_reverse_partial(term, term->scroll_region, rows); } -void -term_restore_cursor(struct terminal *term, const struct cursor *cursor) -{ - int row = min(cursor->point.row, term->rows - 1); - int col = min(cursor->point.col, term->cols - 1); - - term_cursor_to(term, row, col); - term->grid->cursor.lcf = cursor->lcf; - - term->vt.attrs = term->vt.saved_attrs; - term->charsets = term->saved_charsets; - - term->bits_affecting_ascii_printer.charset = - term->charsets.set[term->charsets.selected] != CHARSET_ASCII; - term_update_ascii_printer(term); +void term_carriage_return(struct terminal *term) { + term_cursor_left(term, term->grid->cursor.point.col); } -void -term_visual_focus_in(struct terminal *term) -{ - if (term->visual_focus) - return; +void term_linefeed(struct terminal *term) { + term->grid->cursor.lcf = false; - term->visual_focus = true; - term_cursor_blink_update(term); - render_refresh_csd(term); + if (term->grid->cursor.point.row == term->scroll_region.end - 1) + term_scroll(term, 1); + else + term_cursor_down(term, 1); + + term_reset_grapheme_state(term); } -void -term_visual_focus_out(struct terminal *term) -{ - if (!term->visual_focus) - return; - - term->visual_focus = false; - term_cursor_blink_update(term); - render_refresh_csd(term); +void term_reverse_index(struct terminal *term) { + if (term->grid->cursor.point.row == term->scroll_region.start) + term_scroll_reverse(term, 1); + else + term_cursor_up(term, 1); } -void -term_kbd_focus_in(struct terminal *term) -{ - if (term->kbd_focus) - return; +void term_reset_view(struct terminal *term) { + if (term->grid->view == term->grid->offset) + return; - term->kbd_focus = true; - - if (term->render.urgency) { - term->render.urgency = false; - term_damage_margins(term); - } - - cursor_refresh(term); - - if (term->focus_events) - term_to_slave(term, "\033[I", 3); + term->grid->view = term->grid->offset; + term_damage_view(term); } -void -term_kbd_focus_out(struct terminal *term) -{ - if (!term->kbd_focus) - return; +void term_save_cursor(struct terminal *term) { + term->grid->saved_cursor = term->grid->cursor; + term->vt.saved_attrs = term->vt.attrs; + term->saved_charsets = term->charsets; +} - tll_foreach(term->wl->seats, it) - if (it->item.kbd_focus == term) - return; +void term_restore_cursor(struct terminal *term, const struct cursor *cursor) { + int row = min(cursor->point.row, term->rows - 1); + int col = min(cursor->point.col, term->cols - 1); + + term_cursor_to(term, row, col); + term->grid->cursor.lcf = cursor->lcf; + + term->vt.attrs = term->vt.saved_attrs; + term->charsets = term->saved_charsets; + + term->bits_affecting_ascii_printer.charset = + term->charsets.set[term->charsets.selected] != CHARSET_ASCII; + term_update_ascii_printer(term); +} + +void term_visual_focus_in(struct terminal *term) { + if (term->visual_focus) + return; + + term->visual_focus = true; + term_cursor_blink_update(term); + render_refresh_csd(term); +} + +void term_visual_focus_out(struct terminal *term) { + if (!term->visual_focus) + return; + + term->visual_focus = false; + term_cursor_blink_update(term); + render_refresh_csd(term); +} + +void term_kbd_focus_in(struct terminal *term) { + if (term->kbd_focus) + return; + + term->kbd_focus = true; + + if (term->render.urgency) { + term->render.urgency = false; + term_damage_margins(term); + } + + cursor_refresh(term); + + if (term->focus_events) + term_to_slave(term, "\033[I", 3); +} + +void term_kbd_focus_out(struct terminal *term) { + if (!term->kbd_focus) + return; + + tll_foreach(term->wl->seats, it) if (it->item.kbd_focus == term) return; #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED - if (term_ime_reset(term)) - render_refresh(term); + if (term_ime_reset(term)) + render_refresh(term); #endif - term->kbd_focus = false; - cursor_refresh(term); + term->kbd_focus = false; + cursor_refresh(term); - if (term->focus_events) - term_to_slave(term, "\033[O", 3); + if (term->focus_events) + term_to_slave(term, "\033[O", 3); } -static int -linux_mouse_button_to_x(int button) -{ - /* Note: on X11, scroll events where reported as buttons. Not so - * on Wayland. We manually map scroll events to custom "button" - * defines (BTN_WHEEL_*). - */ - switch (button) { - case BTN_LEFT: return 1; - case BTN_MIDDLE: return 2; - case BTN_RIGHT: return 3; - case BTN_WHEEL_BACK: return 4; /* Foot custom define */ - case BTN_WHEEL_FORWARD: return 5; /* Foot custom define */ - case BTN_WHEEL_LEFT: return 6; /* Foot custom define */ - case BTN_WHEEL_RIGHT: return 7; /* Foot custom define */ - case BTN_SIDE: return 8; - case BTN_EXTRA: return 9; - case BTN_FORWARD: return 10; - case BTN_BACK: return 11; - case BTN_TASK: return 12; /* Guessing... */ +static int linux_mouse_button_to_x(int button) { + /* Note: on X11, scroll events where reported as buttons. Not so + * on Wayland. We manually map scroll events to custom "button" + * defines (BTN_WHEEL_*). + */ + switch (button) { + case BTN_LEFT: + return 1; + case BTN_MIDDLE: + return 2; + case BTN_RIGHT: + return 3; + case BTN_WHEEL_BACK: + return 4; /* Foot custom define */ + case BTN_WHEEL_FORWARD: + return 5; /* Foot custom define */ + case BTN_WHEEL_LEFT: + return 6; /* Foot custom define */ + case BTN_WHEEL_RIGHT: + return 7; /* Foot custom define */ + case BTN_SIDE: + return 8; + case BTN_EXTRA: + return 9; + case BTN_FORWARD: + return 10; + case BTN_BACK: + return 11; + case BTN_TASK: + return 12; /* Guessing... */ - default: - LOG_WARN("unrecognized mouse button: %d (0x%x)", button, button); - return -1; - } + default: + LOG_WARN("unrecognized mouse button: %d (0x%x)", button, button); + return -1; + } } -static int -encode_xbutton(int xbutton) -{ - switch (xbutton) { - case 1: case 2: case 3: - return xbutton - 1; +static int encode_xbutton(int xbutton) { + switch (xbutton) { + case 1: + case 2: + case 3: + return xbutton - 1; - case 4: case 5: case 6: case 7: - /* Like button 1 and 2, but with 64 added */ - return xbutton - 4 + 64; + case 4: + case 5: + case 6: + case 7: + /* Like button 1 and 2, but with 64 added */ + return xbutton - 4 + 64; - case 8: case 9: case 10: case 11: - /* Similar to 4 and 5, but adding 128 instead of 64 */ - return xbutton - 8 + 128; + case 8: + case 9: + case 10: + case 11: + /* Similar to 4 and 5, but adding 128 instead of 64 */ + return xbutton - 8 + 128; - default: - LOG_ERR("cannot encode X mouse button: %d", xbutton); - return -1; - } + default: + LOG_ERR("cannot encode X mouse button: %d", xbutton); + return -1; + } } -static void -report_mouse_click(struct terminal *term, int encoded_button, int row, int col, - int row_pixels, int col_pixels, bool release) -{ - char response[128]; +static void report_mouse_click(struct terminal *term, int encoded_button, + int row, int col, int row_pixels, int col_pixels, + bool release) { + char response[128]; - switch (term->mouse_reporting) { - case MOUSE_NORMAL: { - int encoded_col = 32 + col + 1; - int encoded_row = 32 + row + 1; - if (encoded_col > 255 || encoded_row > 255) - return; + switch (term->mouse_reporting) { + case MOUSE_NORMAL: { + int encoded_col = 32 + col + 1; + int encoded_row = 32 + row + 1; + if (encoded_col > 255 || encoded_row > 255) + return; - snprintf(response, sizeof(response), "\033[M%c%c%c", - 32 + (release ? 3 : encoded_button), encoded_col, encoded_row); - break; - } + snprintf(response, sizeof(response), "\033[M%c%c%c", + 32 + (release ? 3 : encoded_button), encoded_col, encoded_row); + break; + } - case MOUSE_SGR: - snprintf(response, sizeof(response), "\033[<%d;%d;%d%c", - encoded_button, col + 1, row + 1, release ? 'm' : 'M'); - break; + case MOUSE_SGR: + snprintf(response, sizeof(response), "\033[<%d;%d;%d%c", encoded_button, + col + 1, row + 1, release ? 'm' : 'M'); + break; - case MOUSE_SGR_PIXELS: { - const int bounded_col = max(col_pixels, 0); - const int bounded_row = max(row_pixels, 0); - snprintf(response, sizeof(response), "\033[<%d;%d;%d%c", - encoded_button, bounded_col + 1, bounded_row + 1, release ? 'm' : 'M'); - break; - } + case MOUSE_SGR_PIXELS: { + const int bounded_col = max(col_pixels, 0); + const int bounded_row = max(row_pixels, 0); + snprintf(response, sizeof(response), "\033[<%d;%d;%d%c", encoded_button, + bounded_col + 1, bounded_row + 1, release ? 'm' : 'M'); + break; + } - case MOUSE_URXVT: - snprintf(response, sizeof(response), "\033[%d;%d;%dM", - 32 + (release ? 3 : encoded_button), col + 1, row + 1); - break; + case MOUSE_URXVT: + snprintf(response, sizeof(response), "\033[%d;%d;%dM", + 32 + (release ? 3 : encoded_button), col + 1, row + 1); + break; - case MOUSE_UTF8: - /* Unimplemented */ - return; - } + case MOUSE_UTF8: + /* Unimplemented */ + return; + } - term_to_slave(term, response, strlen(response)); + term_to_slave(term, response, strlen(response)); } -static void -report_mouse_motion(struct terminal *term, int encoded_button, int row, int col, int row_pixels, int col_pixels) -{ - report_mouse_click(term, encoded_button, row, col, row_pixels, col_pixels, false); +static void report_mouse_motion(struct terminal *term, int encoded_button, + int row, int col, int row_pixels, + int col_pixels) { + report_mouse_click(term, encoded_button, row, col, row_pixels, col_pixels, + false); } -bool -term_mouse_grabbed(const struct terminal *term, const struct seat *seat) -{ +bool term_mouse_grabbed(const struct terminal *term, const struct seat *seat) { + /* + * Mouse is grabbed by us, regardless of whether mouse tracking + * has been enabled or not. + */ + + xkb_mod_mask_t mods; + get_current_modifiers(seat, &mods, NULL, 0, true); + + const struct key_binding_set *bindings = + key_binding_for(term->wl->key_binding_manager, term->conf, seat); + const xkb_mod_mask_t override_modmask = bindings->selection_overrides; + bool override_mods_pressed = (mods & override_modmask) == override_modmask; + + return term->mouse_tracking == MOUSE_NONE || + (seat->kbd_focus == term && override_mods_pressed); +} + +void term_mouse_down(struct terminal *term, int button, int row, int col, + int row_pixels, int col_pixels, bool _shift, bool _alt, + bool _ctrl) { + /* Map libevent button event code to X button number */ + int xbutton = linux_mouse_button_to_x(button); + if (xbutton == -1) + return; + + int encoded = encode_xbutton(xbutton); + if (encoded == -1) + return; + + bool has_focus = term->kbd_focus; + bool shift = has_focus ? _shift : false; + bool alt = has_focus ? _alt : false; + bool ctrl = has_focus ? _ctrl : false; + + encoded += (shift ? 4 : 0) + (alt ? 8 : 0) + (ctrl ? 16 : 0); + + switch (term->mouse_tracking) { + case MOUSE_NONE: + break; + + case MOUSE_CLICK: + case MOUSE_DRAG: + case MOUSE_MOTION: + report_mouse_click(term, encoded, row, col, row_pixels, col_pixels, false); + break; + + case MOUSE_X10: + /* Never enabled */ + BUG("X10 mouse mode not implemented"); + break; + } +} + +void term_mouse_up(struct terminal *term, int button, int row, int col, + int row_pixels, int col_pixels, bool _shift, bool _alt, + bool _ctrl) { + /* Map libevent button event code to X button number */ + int xbutton = linux_mouse_button_to_x(button); + if (xbutton == -1) + return; + + if (xbutton == 4 || xbutton == 5) { + /* No release events for vertical scroll wheel buttons */ + return; + } + + int encoded = encode_xbutton(xbutton); + if (encoded == -1) + return; + + bool has_focus = term->kbd_focus; + bool shift = has_focus ? _shift : false; + bool alt = has_focus ? _alt : false; + bool ctrl = has_focus ? _ctrl : false; + + encoded += (shift ? 4 : 0) + (alt ? 8 : 0) + (ctrl ? 16 : 0); + + switch (term->mouse_tracking) { + case MOUSE_NONE: + break; + + case MOUSE_CLICK: + case MOUSE_DRAG: + case MOUSE_MOTION: + report_mouse_click(term, encoded, row, col, row_pixels, col_pixels, true); + break; + + case MOUSE_X10: + /* Never enabled */ + BUG("X10 mouse mode not implemented"); + break; + } +} + +void term_mouse_motion(struct terminal *term, int button, int row, int col, + int row_pixels, int col_pixels, bool _shift, bool _alt, + bool _ctrl) { + int encoded = 0; + + if (button != 0) { + /* Map libevent button event code to X button number */ + int xbutton = linux_mouse_button_to_x(button); + if (xbutton == -1) + return; + + encoded = encode_xbutton(xbutton); + if (encoded == -1) + return; + } else + encoded = 3; /* "released" */ + + bool has_focus = term->kbd_focus; + bool shift = has_focus ? _shift : false; + bool alt = has_focus ? _alt : false; + bool ctrl = has_focus ? _ctrl : false; + + encoded += 32; /* Motion event */ + encoded += (shift ? 4 : 0) + (alt ? 8 : 0) + (ctrl ? 16 : 0); + + switch (term->mouse_tracking) { + case MOUSE_NONE: + case MOUSE_CLICK: + return; + + case MOUSE_DRAG: + if (button == 0) + return; + /* FALLTHROUGH */ + + case MOUSE_MOTION: + report_mouse_motion(term, encoded, row, col, row_pixels, col_pixels); + break; + + case MOUSE_X10: + /* Never enabled */ + BUG("X10 mouse mode not implemented"); + break; + } +} + +void term_xcursor_update_for_seat(struct terminal *term, struct seat *seat) { + enum cursor_shape shape = CURSOR_SHAPE_NONE; + + switch (term->active_surface) { + case TERM_SURF_GRID: + if (seat->pointer.hidden) + shape = CURSOR_SHAPE_HIDDEN; + + else if (cursor_string_to_server_shape(term->mouse_user_cursor, + term->wl->shape_manager_version) != + 0 || + render_xcursor_is_valid(seat, term->mouse_user_cursor)) { + shape = CURSOR_SHAPE_CUSTOM; + } + + else if (term_mouse_grabbed(term, seat)) { + shape = CURSOR_SHAPE_TEXT; + } + + else + shape = CURSOR_SHAPE_LEFT_PTR; + break; + + case TERM_SURF_TITLE: + case TERM_SURF_BUTTON_MINIMIZE: + case TERM_SURF_BUTTON_MAXIMIZE: + case TERM_SURF_BUTTON_CLOSE: + shape = CURSOR_SHAPE_LEFT_PTR; + break; + + case TERM_SURF_TAB_BAR: { + size_t idx = term_tab_bar_hit_test(term, seat->mouse.x, seat->mouse.y); + shape = (idx != SIZE_MAX) ? CURSOR_SHAPE_POINTER : CURSOR_SHAPE_LEFT_PTR; + break; + } + + case TERM_SURF_TAB_OVERVIEW: + shape = CURSOR_SHAPE_POINTER; + break; + + case TERM_SURF_BORDER_LEFT: + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: + shape = xcursor_for_csd_border(term, seat->mouse.x, seat->mouse.y); + break; + + case TERM_SURF_NONE: + return; + } + + if (shape == CURSOR_SHAPE_NONE) + BUG("xcursor not set"); + + render_xcursor_set(seat, term, shape); +} + +void term_xcursor_update(struct terminal *term) { + tll_foreach(term->wl->seats, it) + term_xcursor_update_for_seat(term, &it->item); +} + +void term_set_window_title(struct terminal *term, const char *title) { + if (term->conf->locked_title && term->window_title_has_been_set) + return; + + if (term->window_title != NULL && streq(term->window_title, title)) + return; + + if (!is_valid_utf8_and_printable(title)) { + /* It's an xdg_toplevel::set_title() protocol violation to set + a title with an invalid UTF-8 sequence */ + LOG_WARN("%s: title is not valid UTF-8, ignoring", title); + return; + } + + free(term->window_title); + term->window_title = xstrdup(title); + render_refresh_title(term); + render_refresh_tab_bar(term); + term->window_title_has_been_set = true; +} + +void term_set_app_id(struct terminal *term, const char *app_id) { + if (app_id != NULL && *app_id == '\0') + app_id = NULL; + + if (term->app_id == NULL && app_id == NULL) + return; + + if (term->app_id != NULL && app_id != NULL && streq(term->app_id, app_id)) + return; + + if (app_id != NULL && !is_valid_utf8_and_printable(app_id)) { + LOG_WARN("%s: app-id is not valid UTF-8, ignoring", app_id); + return; + } + + free(term->app_id); + if (app_id != NULL) { + term->app_id = xstrdup(app_id); + } else { + term->app_id = NULL; + } + + const size_t length = app_id != NULL ? strlen(app_id) : 0; + if (length > 2048) { /* - * Mouse is grabbed by us, regardless of whether mouse tracking - * has been enabled or not. + * Not sure if there's a limit in the protocol, or the + * libwayland implementation, or e.g. wlroots, but too long + * app-id's (not e.g. title) causes at least river and sway to + * peg the CPU at 100%, and stop sending e.g. frame callbacks. + * */ + term->app_id[2048] = '\0'; + } - xkb_mod_mask_t mods; - get_current_modifiers(seat, &mods, NULL, 0, true); - - const struct key_binding_set *bindings = - key_binding_for(term->wl->key_binding_manager, term->conf, seat); - const xkb_mod_mask_t override_modmask = bindings->selection_overrides; - bool override_mods_pressed = (mods & override_modmask) == override_modmask; - - return term->mouse_tracking == MOUSE_NONE || - (seat->kbd_focus == term && override_mods_pressed); + render_refresh_app_id(term); + render_refresh_icon(term); } -void -term_mouse_down(struct terminal *term, int button, int row, int col, - int row_pixels, int col_pixels, - bool _shift, bool _alt, bool _ctrl) -{ - /* Map libevent button event code to X button number */ - int xbutton = linux_mouse_button_to_x(button); - if (xbutton == -1) - return; +const char *term_icon(const struct terminal *term) { + const char *app_id = term->app_id != NULL ? term->app_id : term->conf->app_id; - int encoded = encode_xbutton(xbutton); - if (encoded == -1) - return; - - - bool has_focus = term->kbd_focus; - bool shift = has_focus ? _shift : false; - bool alt = has_focus ? _alt : false; - bool ctrl = has_focus ? _ctrl : false; - - encoded += (shift ? 4 : 0) + (alt ? 8 : 0) + (ctrl ? 16 : 0); - - switch (term->mouse_tracking) { - case MOUSE_NONE: - break; - - case MOUSE_CLICK: - case MOUSE_DRAG: - case MOUSE_MOTION: - report_mouse_click(term, encoded, row, col, row_pixels, col_pixels, false); - break; - - case MOUSE_X10: - /* Never enabled */ - BUG("X10 mouse mode not implemented"); - break; - } -} - -void -term_mouse_up(struct terminal *term, int button, int row, int col, - int row_pixels, int col_pixels, - bool _shift, bool _alt, bool _ctrl) -{ - /* Map libevent button event code to X button number */ - int xbutton = linux_mouse_button_to_x(button); - if (xbutton == -1) - return; - - if (xbutton == 4 || xbutton == 5) { - /* No release events for vertical scroll wheel buttons */ - return; - } - - int encoded = encode_xbutton(xbutton); - if (encoded == -1) - return; - - bool has_focus = term->kbd_focus; - bool shift = has_focus ? _shift : false; - bool alt = has_focus ? _alt : false; - bool ctrl = has_focus ? _ctrl : false; - - encoded += (shift ? 4 : 0) + (alt ? 8 : 0) + (ctrl ? 16 : 0); - - switch (term->mouse_tracking) { - case MOUSE_NONE: - break; - - case MOUSE_CLICK: - case MOUSE_DRAG: - case MOUSE_MOTION: - report_mouse_click(term, encoded, row, col, row_pixels, col_pixels, true); - break; - - case MOUSE_X10: - /* Never enabled */ - BUG("X10 mouse mode not implemented"); - break; - } -} - -void -term_mouse_motion(struct terminal *term, int button, int row, int col, - int row_pixels, int col_pixels, - bool _shift, bool _alt, bool _ctrl) -{ - int encoded = 0; - - if (button != 0) { - /* Map libevent button event code to X button number */ - int xbutton = linux_mouse_button_to_x(button); - if (xbutton == -1) - return; - - encoded = encode_xbutton(xbutton); - if (encoded == -1) - return; - } else - encoded = 3; /* "released" */ - - bool has_focus = term->kbd_focus; - bool shift = has_focus ? _shift : false; - bool alt = has_focus ? _alt : false; - bool ctrl = has_focus ? _ctrl : false; - - encoded += 32; /* Motion event */ - encoded += (shift ? 4 : 0) + (alt ? 8 : 0) + (ctrl ? 16 : 0); - - switch (term->mouse_tracking) { - case MOUSE_NONE: - case MOUSE_CLICK: - return; - - case MOUSE_DRAG: - if (button == 0) - return; - /* FALLTHROUGH */ - - case MOUSE_MOTION: - report_mouse_motion(term, encoded, row, col, row_pixels, col_pixels); - break; - - case MOUSE_X10: - /* Never enabled */ - BUG("X10 mouse mode not implemented"); - break; - } -} - -void -term_xcursor_update_for_seat(struct terminal *term, struct seat *seat) -{ - enum cursor_shape shape = CURSOR_SHAPE_NONE; - - switch (term->active_surface) { - case TERM_SURF_GRID: - if (seat->pointer.hidden) - shape = CURSOR_SHAPE_HIDDEN; - - else if (cursor_string_to_server_shape( - term->mouse_user_cursor, - term->wl->shape_manager_version) != 0 || - render_xcursor_is_valid(seat, term->mouse_user_cursor)) - { - shape = CURSOR_SHAPE_CUSTOM; - } - - else if (term_mouse_grabbed(term, seat)) { - shape = CURSOR_SHAPE_TEXT; - } - - else - shape = CURSOR_SHAPE_LEFT_PTR; - break; - - case TERM_SURF_TITLE: - case TERM_SURF_BUTTON_MINIMIZE: - case TERM_SURF_BUTTON_MAXIMIZE: - case TERM_SURF_BUTTON_CLOSE: - shape = CURSOR_SHAPE_LEFT_PTR; - break; - - case TERM_SURF_TAB_BAR: { - size_t idx = term_tab_bar_hit_test(term, seat->mouse.x, seat->mouse.y); - shape = (idx != SIZE_MAX) ? CURSOR_SHAPE_POINTER : CURSOR_SHAPE_LEFT_PTR; - break; - } - - case TERM_SURF_TAB_OVERVIEW: - shape = CURSOR_SHAPE_POINTER; - break; - - case TERM_SURF_BORDER_LEFT: - case TERM_SURF_BORDER_RIGHT: - case TERM_SURF_BORDER_TOP: - case TERM_SURF_BORDER_BOTTOM: - shape = xcursor_for_csd_border(term, seat->mouse.x, seat->mouse.y); - break; - - case TERM_SURF_NONE: - return; - } - - if (shape == CURSOR_SHAPE_NONE) - BUG("xcursor not set"); - - render_xcursor_set(seat, term, shape); -} - -void -term_xcursor_update(struct terminal *term) -{ - tll_foreach(term->wl->seats, it) - term_xcursor_update_for_seat(term, &it->item); -} - -void -term_set_window_title(struct terminal *term, const char *title) -{ - if (term->conf->locked_title && term->window_title_has_been_set) - return; - - if (term->window_title != NULL && streq(term->window_title, title)) - return; - - if (!is_valid_utf8_and_printable(title)) { - /* It's an xdg_toplevel::set_title() protocol violation to set - a title with an invalid UTF-8 sequence */ - LOG_WARN("%s: title is not valid UTF-8, ignoring", title); - return; - } - - free(term->window_title); - term->window_title = xstrdup(title); - render_refresh_title(term); - render_refresh_tab_bar(term); - term->window_title_has_been_set = true; -} - -void -term_set_app_id(struct terminal *term, const char *app_id) -{ - if (app_id != NULL && *app_id == '\0') - app_id = NULL; - - if (term->app_id == NULL && app_id == NULL) - return; - - if (term->app_id != NULL && app_id != NULL && streq(term->app_id, app_id)) - return; - - if (app_id != NULL && !is_valid_utf8_and_printable(app_id)) { - LOG_WARN("%s: app-id is not valid UTF-8, ignoring", app_id); - return; - } - - free(term->app_id); - if (app_id != NULL) { - term->app_id = xstrdup(app_id); - } else { - term->app_id = NULL; - } - - const size_t length = app_id != NULL ? strlen(app_id) : 0; - if (length > 2048) { - /* - * Not sure if there's a limit in the protocol, or the - * libwayland implementation, or e.g. wlroots, but too long - * app-id's (not e.g. title) causes at least river and sway to - * peg the CPU at 100%, and stop sending e.g. frame callbacks. - * - */ - term->app_id[2048] = '\0'; - } - - render_refresh_app_id(term); - render_refresh_icon(term); -} - -const char * -term_icon(const struct terminal *term) -{ - const char *app_id = - term->app_id != NULL ? term->app_id : term->conf->app_id; - - return + return #if 0 term->window_icon != NULL ? term->window_icon : - #endif - streq(app_id, "footclient") - ? "foot" - : app_id; +#endif + streq(app_id, "footclient") ? "foot" : app_id; } -void -term_flash(struct terminal *term, unsigned duration_ms) -{ - LOG_DBG("FLASH for %ums", duration_ms); +void term_flash(struct terminal *term, unsigned duration_ms) { + LOG_DBG("FLASH for %ums", duration_ms); - struct itimerspec alarm = { - .it_value = {.tv_sec = 0, .tv_nsec = duration_ms * 1000000}, - }; + struct itimerspec alarm = { + .it_value = {.tv_sec = 0, .tv_nsec = duration_ms * 1000000}, + }; - if (timerfd_settime(term->flash.fd, 0, &alarm, NULL) < 0) - LOG_ERRNO("failed to arm flash timer"); - else { - term->flash.active = true; - } + if (timerfd_settime(term->flash.fd, 0, &alarm, NULL) < 0) + LOG_ERRNO("failed to arm flash timer"); + else { + term->flash.active = true; + } } -void -term_bell(struct terminal *term) -{ +void term_bell(struct terminal *term) { - if (!term->bell_action_enabled) - return; + if (!term->bell_action_enabled) + return; - if (term->conf->bell.urgent && !term->kbd_focus) { - if (!wayl_win_set_urgent(term->window)) { - /* - * Urgency (xdg-activation) is relatively new in - * Wayland. Fallback to our old, "faked", urgency - - * rendering our window margins in red - */ - term->render.urgency = true; - term_damage_margins(term); - } + if (term->conf->bell.urgent && !term->kbd_focus) { + if (!wayl_win_set_urgent(term->window)) { + /* + * Urgency (xdg-activation) is relatively new in + * Wayland. Fallback to our old, "faked", urgency - + * rendering our window margins in red + */ + term->render.urgency = true; + term_damage_margins(term); } + } - if (term->conf->bell.system_bell) - wayl_win_ring_bell(term->window); + if (term->conf->bell.system_bell) + wayl_win_ring_bell(term->window); - if (term->conf->bell.notify) { - notify_notify(term, &(struct notification){ - .title = xstrdup("Bell"), - .body = xstrdup("Bell in terminal"), - .expire_time = -1, - .focus = true, - }); - } + if (term->conf->bell.notify) { + notify_notify(term, &(struct notification){ + .title = xstrdup("Bell"), + .body = xstrdup("Bell in terminal"), + .expire_time = -1, + .focus = true, + }); + } - if (term->conf->bell.flash) - term_flash(term, 100); + if (term->conf->bell.flash) + term_flash(term, 100); - if ((term->conf->bell.command.argv.args != NULL) && - (!term->kbd_focus || term->conf->bell.command_focused)) - { - int devnull = open("/dev/null", O_RDONLY); - spawn(term->reaper, NULL, term->conf->bell.command.argv.args, - devnull, -1, -1, NULL, NULL, NULL); + if ((term->conf->bell.command.argv.args != NULL) && + (!term->kbd_focus || term->conf->bell.command_focused)) { + int devnull = open("/dev/null", O_RDONLY); + spawn(term->reaper, NULL, term->conf->bell.command.argv.args, devnull, -1, + -1, NULL, NULL, NULL); - if (devnull >= 0) - close(devnull); - } + if (devnull >= 0) + close(devnull); + } } -bool -term_spawn_new(const struct terminal *term) -{ - char *argv[4]; - int argc = 0; +bool term_spawn_new(const struct terminal *term) { + char *argv[4]; + int argc = 0; - argv[argc++] = term->foot_exe; - if (term->conf->conf_path != NULL) { - argv[argc++] = "--config"; - argv[argc++] = term->conf->conf_path; - } - argv[argc] = NULL; + argv[argc++] = term->foot_exe; + if (term->conf->conf_path != NULL) { + argv[argc++] = "--config"; + argv[argc++] = term->conf->conf_path; + } + argv[argc] = NULL; - return spawn( - term->reaper, term->cwd, argv, - -1, -1, -1, NULL, NULL, NULL) >= 0; + return spawn(term->reaper, term->cwd, argv, -1, -1, -1, NULL, NULL, NULL) >= + 0; } -void -term_enable_app_sync_updates(struct terminal *term) -{ - term->render.app_sync_updates.enabled = true; +void term_enable_app_sync_updates(struct terminal *term) { + term->render.app_sync_updates.enabled = true; - if (timerfd_settime( - term->render.app_sync_updates.timer_fd, 0, - &(struct itimerspec){.it_value = {.tv_sec = 1}}, NULL) < 0) - { - LOG_ERR("failed to arm timer for application synchronized updates"); - } + if (timerfd_settime(term->render.app_sync_updates.timer_fd, 0, + &(struct itimerspec){.it_value = {.tv_sec = 1}}, + NULL) < 0) { + LOG_ERR("failed to arm timer for application synchronized updates"); + } - /* Disable pending refresh *iff* the grid is the *only* thing - * scheduled to be re-rendered */ - if (!term->render.refresh.csd && !term->render.refresh.search && - !term->render.pending.csd && !term->render.pending.search) - { - term->render.refresh.grid = false; - term->render.pending.grid = false; - } + /* Disable pending refresh *iff* the grid is the *only* thing + * scheduled to be re-rendered */ + if (!term->render.refresh.csd && !term->render.refresh.search && + !term->render.pending.csd && !term->render.pending.search) { + term->render.refresh.grid = false; + term->render.pending.grid = false; + } - /* Disarm delayed rendering timers */ - timerfd_settime( - term->delayed_render_timer.lower_fd, 0, - &(struct itimerspec){{0}}, NULL); - timerfd_settime( - term->delayed_render_timer.upper_fd, 0, - &(struct itimerspec){{0}}, NULL); - term->delayed_render_timer.is_armed = false; + /* Disarm delayed rendering timers */ + timerfd_settime(term->delayed_render_timer.lower_fd, 0, + &(struct itimerspec){{0}}, NULL); + timerfd_settime(term->delayed_render_timer.upper_fd, 0, + &(struct itimerspec){{0}}, NULL); + term->delayed_render_timer.is_armed = false; } -void -term_disable_app_sync_updates(struct terminal *term) -{ - if (!term->render.app_sync_updates.enabled) - return; +void term_disable_app_sync_updates(struct terminal *term) { + if (!term->render.app_sync_updates.enabled) + return; - term->render.app_sync_updates.enabled = false; - render_refresh(term); + term->render.app_sync_updates.enabled = false; + render_refresh(term); - /* Reset timers */ - timerfd_settime( - term->render.app_sync_updates.timer_fd, 0, - &(struct itimerspec){{0}}, NULL); + /* Reset timers */ + timerfd_settime(term->render.app_sync_updates.timer_fd, 0, + &(struct itimerspec){{0}}, NULL); } -static inline void -print_linewrap(struct terminal *term) -{ - if (likely(!term->grid->cursor.lcf)) { - /* Not and end of line */ - return; - } +static inline void print_linewrap(struct terminal *term) { + if (likely(!term->grid->cursor.lcf)) { + /* Not and end of line */ + return; + } - if (unlikely(!term->auto_margin)) { - /* Auto-wrap disabled */ - return; - } + if (unlikely(!term->auto_margin)) { + /* Auto-wrap disabled */ + return; + } - term->grid->cur_row->linebreak = false; - term->grid->cursor.lcf = false; + term->grid->cur_row->linebreak = false; + term->grid->cursor.lcf = false; - const int row = term->grid->cursor.point.row; + const int row = term->grid->cursor.point.row; - if (row == term->scroll_region.end - 1) - term_scroll(term, 1); - else { - const int new_row = min(row + 1, term->rows - 1); - term->grid->cursor.point.row = new_row; - term->grid->cur_row = grid_row(term->grid, new_row); - } + if (row == term->scroll_region.end - 1) + term_scroll(term, 1); + else { + const int new_row = min(row + 1, term->rows - 1); + term->grid->cursor.point.row = new_row; + term->grid->cur_row = grid_row(term->grid, new_row); + } - term->grid->cursor.point.col = 0; + term->grid->cursor.point.col = 0; } -static inline void -print_insert(struct terminal *term, int width) -{ - if (likely(!term->insert_mode)) - return; +static inline void print_insert(struct terminal *term, int width) { + if (likely(!term->insert_mode)) + return; - xassert(width > 0); + xassert(width > 0); - struct row *row = term->grid->cur_row; - const size_t move_count = max(0, term->cols - term->grid->cursor.point.col - width); + struct row *row = term->grid->cur_row; + const size_t move_count = + max(0, term->cols - term->grid->cursor.point.col - width); - memmove( - &row->cells[term->grid->cursor.point.col + width], - &row->cells[term->grid->cursor.point.col], - move_count * sizeof(struct cell)); + memmove(&row->cells[term->grid->cursor.point.col + width], + &row->cells[term->grid->cursor.point.col], + move_count * sizeof(struct cell)); - /* Mark moved cells as dirty */ - for (size_t i = term->grid->cursor.point.col + width; i < term->cols; i++) - row->cells[i].attrs.clean = 0; + /* Mark moved cells as dirty */ + for (size_t i = term->grid->cursor.point.col + width; i < term->cols; i++) + row->cells[i].attrs.clean = 0; } -static void -print_spacer(struct terminal *term, int col, int remaining) -{ - struct grid *grid = term->grid; - struct row *row = grid->cur_row; - struct cell *cell = &row->cells[col]; +static void print_spacer(struct terminal *term, int col, int remaining) { + struct grid *grid = term->grid; + struct row *row = grid->cur_row; + struct cell *cell = &row->cells[col]; - cell->wc = CELL_SPACER + remaining; - cell->attrs = (struct attributes){0}; + cell->wc = CELL_SPACER + remaining; + cell->attrs = (struct attributes){0}; } /* @@ -4466,898 +4324,823 @@ print_spacer(struct terminal *term, int col, int remaining) * Limitations: * - double width characters not supported */ -void -term_fill(struct terminal *term, int r, int c, uint8_t data, size_t count, - bool use_sgr_attrs) -{ - struct row *row = grid_row(term->grid, r); - row->dirty = true; +void term_fill(struct terminal *term, int r, int c, uint8_t data, size_t count, + bool use_sgr_attrs) { + struct row *row = grid_row(term->grid, r); + row->dirty = true; - xassert(c + count <= term->cols); + xassert(c + count <= term->cols); - struct attributes attrs = use_sgr_attrs - ? term->vt.attrs - : (struct attributes){0}; + struct attributes attrs = + use_sgr_attrs ? term->vt.attrs : (struct attributes){0}; - const struct cell *last = &row->cells[c + count]; - for (struct cell *cell = &row->cells[c]; cell < last; cell++) { - cell->wc = data; - cell->attrs = attrs; + const struct cell *last = &row->cells[c + count]; + for (struct cell *cell = &row->cells[c]; cell < last; cell++) { + cell->wc = data; + cell->attrs = attrs; - /* TODO: why do we print the URI here, and then erase it below? */ - if (unlikely(use_sgr_attrs && term->vt.osc8.uri != NULL)) { - grid_row_uri_range_put(row, c, term->vt.osc8.uri, term->vt.osc8.id); + /* TODO: why do we print the URI here, and then erase it below? */ + if (unlikely(use_sgr_attrs && term->vt.osc8.uri != NULL)) { + grid_row_uri_range_put(row, c, term->vt.osc8.uri, term->vt.osc8.id); - switch (term->conf->url.osc8_underline) { - case OSC8_UNDERLINE_ALWAYS: - cell->attrs.url = true; - break; + switch (term->conf->url.osc8_underline) { + case OSC8_UNDERLINE_ALWAYS: + cell->attrs.url = true; + break; - case OSC8_UNDERLINE_URL_MODE: - break; - } - } - - if (unlikely(use_sgr_attrs && - (term->vt.underline.style > UNDERLINE_SINGLE || - term->vt.underline.color_src != COLOR_DEFAULT))) - { - grid_row_underline_range_put(row, c, term->vt.underline); - } + case OSC8_UNDERLINE_URL_MODE: + break; + } } - if (unlikely(row->extra != NULL)) { - if (likely(term->vt.osc8.uri != NULL)) - grid_row_uri_range_erase(row, c, c + count - 1); - - if (likely(term->vt.underline.style <= UNDERLINE_SINGLE && - term->vt.underline.color_src == COLOR_DEFAULT)) - { - /* No extended/styled underlines active, so erase any such - attributes at the target columns */ - grid_row_underline_range_erase(row, c, c + count - 1); - } + if (unlikely(use_sgr_attrs && + (term->vt.underline.style > UNDERLINE_SINGLE || + term->vt.underline.color_src != COLOR_DEFAULT))) { + grid_row_underline_range_put(row, c, term->vt.underline); } + } + + if (unlikely(row->extra != NULL)) { + if (likely(term->vt.osc8.uri != NULL)) + grid_row_uri_range_erase(row, c, c + count - 1); + + if (likely(term->vt.underline.style <= UNDERLINE_SINGLE && + term->vt.underline.color_src == COLOR_DEFAULT)) { + /* No extended/styled underlines active, so erase any such + attributes at the target columns */ + grid_row_underline_range_erase(row, c, c + count - 1); + } + } } -void -term_print(struct terminal *term, char32_t wc, int width, bool insert_mode_disable) -{ - xassert(width > 0); +void term_print(struct terminal *term, char32_t wc, int width, + bool insert_mode_disable) { + xassert(width > 0); - struct grid *grid = term->grid; + struct grid *grid = term->grid; - if (unlikely(term->charsets.set[term->charsets.selected] == CHARSET_GRAPHIC) && - wc >= 0x60 && wc <= 0x7e) - { - /* 0x60 - 0x7e */ - static const char32_t vt100_0[] = { - U'◆', U'▒', U'␉', U'␌', U'␍', U'␊', U'°', U'±', /* ` - g */ - U'␤', U'␋', U'┘', U'┐', U'┌', U'└', U'┼', U'⎺', /* h - o */ - U'⎻', U'─', U'⎼', U'⎽', U'├', U'┤', U'┴', U'┬', /* p - w */ - U'│', U'≤', U'≥', U'π', U'≠', U'£', U'·', /* x - ~ */ - }; + if (unlikely(term->charsets.set[term->charsets.selected] == + CHARSET_GRAPHIC) && + wc >= 0x60 && wc <= 0x7e) { + /* 0x60 - 0x7e */ + static const char32_t vt100_0[] = { + U'◆', U'▒', U'␉', U'␌', U'␍', U'␊', U'°', U'±', /* ` - g */ + U'␤', U'␋', U'┘', U'┐', U'┌', U'└', U'┼', U'⎺', /* h - o */ + U'⎻', U'─', U'⎼', U'⎽', U'├', U'┤', U'┴', U'┬', /* p - w */ + U'│', U'≤', U'≥', U'π', U'≠', U'£', U'·', /* x - ~ */ + }; - xassert(width == 1); - wc = vt100_0[wc - 0x60]; - } + xassert(width == 1); + wc = vt100_0[wc - 0x60]; + } + print_linewrap(term); + if (!insert_mode_disable) + print_insert(term, width); + + int col = grid->cursor.point.col; + + if (unlikely(width > 1) && likely(term->auto_margin) && + col + width > term->cols) { + /* Multi-column character that doesn't fit on current line - + * pad with spacers */ + for (size_t i = col; i < term->cols; i++) + print_spacer(term, i, 0); + + /* And force a line-wrap */ + grid->cursor.lcf = 1; print_linewrap(term); - if (!insert_mode_disable) - print_insert(term, width); + col = 0; + } - int col = grid->cursor.point.col; + sixel_overwrite_at_cursor(term, width); - if (unlikely(width > 1) && likely(term->auto_margin) && - col + width > term->cols) - { - /* Multi-column character that doesn't fit on current line - - * pad with spacers */ - for (size_t i = col; i < term->cols; i++) - print_spacer(term, i, 0); + /* *Must* get current cell *after* linewrap+insert */ + struct row *row = grid->cur_row; + row->dirty = true; + row->linebreak = true; - /* And force a line-wrap */ - grid->cursor.lcf = 1; - print_linewrap(term); - col = 0; + struct cell *cell = &row->cells[col]; + cell->wc = term->vt.last_printed = wc; + cell->attrs = term->vt.attrs; + + if (unlikely(term->vt.osc8.uri != NULL)) { + for (int i = 0; i < width && (col + i) < term->cols; i++) { + grid_row_uri_range_put(row, col + i, term->vt.osc8.uri, term->vt.osc8.id); } - sixel_overwrite_at_cursor(term, width); + switch (term->conf->url.osc8_underline) { + case OSC8_UNDERLINE_ALWAYS: + cell->attrs.url = true; + break; - /* *Must* get current cell *after* linewrap+insert */ - struct row *row = grid->cur_row; - row->dirty = true; - row->linebreak = true; - - struct cell *cell = &row->cells[col]; - cell->wc = term->vt.last_printed = wc; - cell->attrs = term->vt.attrs; - - if (unlikely(term->vt.osc8.uri != NULL)) { - for (int i = 0; i < width && (col + i) < term->cols; i++) { - grid_row_uri_range_put( - row, col + i, term->vt.osc8.uri, term->vt.osc8.id); - } - - switch (term->conf->url.osc8_underline) { - case OSC8_UNDERLINE_ALWAYS: - cell->attrs.url = true; - break; - - case OSC8_UNDERLINE_URL_MODE: - break; - } - } else if (row->extra != NULL) - grid_row_uri_range_erase(row, col, col + width - 1); - - if (unlikely(term->vt.underline.style > UNDERLINE_SINGLE || - term->vt.underline.color_src != COLOR_DEFAULT)) - { - grid_row_underline_range_put(row, col, term->vt.underline); - } else if (row->extra != NULL) - grid_row_underline_range_erase(row, col, col + width - 1); - - /* Advance cursor the 'additional' columns while dirty:ing the cells */ - for (int i = 1; i < width && (col + 1) < term->cols; i++) { - col++; - print_spacer(term, col, width - i); + case OSC8_UNDERLINE_URL_MODE: + break; } + } else if (row->extra != NULL) + grid_row_uri_range_erase(row, col, col + width - 1); - xassert(col < term->cols); + if (unlikely(term->vt.underline.style > UNDERLINE_SINGLE || + term->vt.underline.color_src != COLOR_DEFAULT)) { + grid_row_underline_range_put(row, col, term->vt.underline); + } else if (row->extra != NULL) + grid_row_underline_range_erase(row, col, col + width - 1); - /* Advance cursor */ - if (unlikely(++col >= term->cols)) { - grid->cursor.lcf = true; - col--; - } else - xassert(!grid->cursor.lcf); + /* Advance cursor the 'additional' columns while dirty:ing the cells */ + for (int i = 1; i < width && (col + 1) < term->cols; i++) { + col++; + print_spacer(term, col, width - i); + } - grid->cursor.point.col = col; + xassert(col < term->cols); + + /* Advance cursor */ + if (unlikely(++col >= term->cols)) { + grid->cursor.lcf = true; + col--; + } else + xassert(!grid->cursor.lcf); + + grid->cursor.point.col = col; } -static void -ascii_printer_generic(struct terminal *term, char32_t wc) -{ - term_print(term, wc, 1, false); +static void ascii_printer_generic(struct terminal *term, char32_t wc) { + term_print(term, wc, 1, false); } -static void -ascii_printer_fast(struct terminal *term, char32_t wc) -{ - struct grid *grid = term->grid; +static void ascii_printer_fast(struct terminal *term, char32_t wc) { + struct grid *grid = term->grid; - xassert(term->charsets.set[term->charsets.selected] == CHARSET_ASCII); - xassert(!term->insert_mode); - xassert(tll_length(grid->sixel_images) == 0); + xassert(term->charsets.set[term->charsets.selected] == CHARSET_ASCII); + xassert(!term->insert_mode); + xassert(tll_length(grid->sixel_images) == 0); - print_linewrap(term); + print_linewrap(term); - /* *Must* get current cell *after* linewrap+insert */ - int col = grid->cursor.point.col; - const int uri_start = col; + /* *Must* get current cell *after* linewrap+insert */ + int col = grid->cursor.point.col; + const int uri_start = col; - struct row *row = grid->cur_row; - row->dirty = true; - row->linebreak = true; + struct row *row = grid->cur_row; + row->dirty = true; + row->linebreak = true; - struct cell *cell = &row->cells[col]; - cell->wc = term->vt.last_printed = wc; - cell->attrs = term->vt.attrs; + struct cell *cell = &row->cells[col]; + cell->wc = term->vt.last_printed = wc; + cell->attrs = term->vt.attrs; - /* Advance cursor */ - if (unlikely(++col >= term->cols)) { - xassert(col == term->cols); - grid->cursor.lcf = true; - col--; - } else - xassert(!grid->cursor.lcf); + /* Advance cursor */ + if (unlikely(++col >= term->cols)) { + xassert(col == term->cols); + grid->cursor.lcf = true; + col--; + } else + xassert(!grid->cursor.lcf); - grid->cursor.point.col = col; + grid->cursor.point.col = col; - if (unlikely(row->extra != NULL)) { - grid_row_uri_range_erase(row, uri_start, uri_start); - grid_row_underline_range_erase(row, uri_start, uri_start); - } + if (unlikely(row->extra != NULL)) { + grid_row_uri_range_erase(row, uri_start, uri_start); + grid_row_underline_range_erase(row, uri_start, uri_start); + } } -static void -ascii_printer_single_shift(struct terminal *term, char32_t wc) -{ - ascii_printer_generic(term, wc); - term->charsets.selected = term->charsets.saved; +static void ascii_printer_single_shift(struct terminal *term, char32_t wc) { + ascii_printer_generic(term, wc); + term->charsets.selected = term->charsets.saved; - term->bits_affecting_ascii_printer.charset = - term->charsets.set[term->charsets.selected] != CHARSET_ASCII; - term_update_ascii_printer(term); + term->bits_affecting_ascii_printer.charset = + term->charsets.set[term->charsets.selected] != CHARSET_ASCII; + term_update_ascii_printer(term); } -void -term_update_ascii_printer(struct terminal *term) -{ - _Static_assert(sizeof(term->bits_affecting_ascii_printer) == sizeof(uint8_t), "bad size"); +void term_update_ascii_printer(struct terminal *term) { + _Static_assert(sizeof(term->bits_affecting_ascii_printer) == sizeof(uint8_t), + "bad size"); - void (*new_printer)(struct terminal *term, char32_t wc) = - unlikely(term->bits_affecting_ascii_printer.value != 0) - ? &ascii_printer_generic - : &ascii_printer_fast; + void (*new_printer)(struct terminal *term, char32_t wc) = + unlikely(term->bits_affecting_ascii_printer.value != 0) + ? &ascii_printer_generic + : &ascii_printer_fast; #if defined(_DEBUG) && LOG_ENABLE_DBG - if (term->ascii_printer != new_printer) { - LOG_DBG("switching ASCII printer %s -> %s", - term->ascii_printer == &ascii_printer_fast ? "fast" : "generic", - new_printer == &ascii_printer_fast ? "fast" : "generic"); - } + if (term->ascii_printer != new_printer) { + LOG_DBG("switching ASCII printer %s -> %s", + term->ascii_printer == &ascii_printer_fast ? "fast" : "generic", + new_printer == &ascii_printer_fast ? "fast" : "generic"); + } #endif - term->ascii_printer = new_printer; + term->ascii_printer = new_printer; } -void -term_single_shift(struct terminal *term, enum charset_designator idx) -{ - term->charsets.saved = term->charsets.selected; - term->charsets.selected = idx; - term->ascii_printer = &ascii_printer_single_shift; +void term_single_shift(struct terminal *term, enum charset_designator idx) { + term->charsets.saved = term->charsets.selected; + term->charsets.selected = idx; + term->ascii_printer = &ascii_printer_single_shift; } #if defined(FOOT_GRAPHEME_CLUSTERING) -static int -emoji_vs_compare(const void *_key, const void *_entry) -{ - const struct emoji_vs *key = _key; - const struct emoji_vs *entry = _entry; +static int emoji_vs_compare(const void *_key, const void *_entry) { + const struct emoji_vs *key = _key; + const struct emoji_vs *entry = _entry; - uint32_t cp = key->start; + uint32_t cp = key->start; - if (cp < entry->start) - return -1; - else if (cp > entry->end) - return 1; - else - return 0; + if (cp < entry->start) + return -1; + else if (cp > entry->end) + return 1; + else + return 0; } -UNITTEST -{ - /* Verify the emoji_vs list is sorted */ - int64_t last_end = -1; +UNITTEST { + /* Verify the emoji_vs list is sorted */ + int64_t last_end = -1; - for (size_t i = 0; i < sizeof(emoji_vs) / sizeof(emoji_vs[0]); i++) { - const struct emoji_vs *vs = &emoji_vs[i]; - xassert(vs->start <= vs->end); - xassert(vs->start > last_end); - xassert(vs->vs15 || vs->vs16); - last_end = vs->end; - } + for (size_t i = 0; i < sizeof(emoji_vs) / sizeof(emoji_vs[0]); i++) { + const struct emoji_vs *vs = &emoji_vs[i]; + xassert(vs->start <= vs->end); + xassert(vs->start > last_end); + xassert(vs->vs15 || vs->vs16); + last_end = vs->end; + } } #endif -void -term_process_and_print_non_ascii(struct terminal *term, char32_t wc) -{ - int width = c32width(wc); - bool insert_mode_disable = false; - const bool grapheme_clustering = term->grapheme_shaping; +void term_process_and_print_non_ascii(struct terminal *term, char32_t wc) { + int width = c32width(wc); + bool insert_mode_disable = false; + const bool grapheme_clustering = term->grapheme_shaping; #if !defined(FOOT_GRAPHEME_CLUSTERING) - xassert(!grapheme_clustering); + xassert(!grapheme_clustering); #endif - if (term->grid->cursor.point.col > 0 && - (grapheme_clustering || - (!grapheme_clustering && width == 0 && wc >= 0x300))) - { - int col = term->grid->cursor.point.col; - if (!term->grid->cursor.lcf) - col--; + if (term->grid->cursor.point.col > 0 && + (grapheme_clustering || + (!grapheme_clustering && width == 0 && wc >= 0x300))) { + int col = term->grid->cursor.point.col; + if (!term->grid->cursor.lcf) + col--; - /* Skip past spacers */ - struct row *row = term->grid->cur_row; - while (row->cells[col].wc >= CELL_SPACER && col > 0) - col--; + /* Skip past spacers */ + struct row *row = term->grid->cur_row; + while (row->cells[col].wc >= CELL_SPACER && col > 0) + col--; - xassert(col >= 0 && col < term->cols); - char32_t base = row->cells[col].wc; - char32_t UNUSED last = base; + xassert(col >= 0 && col < term->cols); + char32_t base = row->cells[col].wc; + char32_t UNUSED last = base; - /* Is base cell already a cluster? */ - const struct composed *composed = - (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) + /* Is base cell already a cluster? */ + const struct composed *composed = + (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) ? composed_lookup(term->composed, base - CELL_COMB_CHARS_LO) : NULL; - uint32_t key; + uint32_t key; - if (composed != NULL) { - base = composed->chars[0]; - last = composed->chars[composed->count - 1]; - key = composed_key_from_key(composed->key, wc); - } else - key = composed_key_from_key(base, wc); + if (composed != NULL) { + base = composed->chars[0]; + last = composed->chars[composed->count - 1]; + key = composed_key_from_key(composed->key, wc); + } else + key = composed_key_from_key(base, wc); #if defined(FOOT_GRAPHEME_CLUSTERING) - if (grapheme_clustering) { - /* Check if we're on a grapheme cluster break */ - if (utf8proc_grapheme_break_stateful( - last, wc, &term->vt.grapheme_state)) - { - term_reset_grapheme_state(term); - goto out; - } - } + if (grapheme_clustering) { + /* Check if we're on a grapheme cluster break */ + if (utf8proc_grapheme_break_stateful(last, wc, + &term->vt.grapheme_state)) { + term_reset_grapheme_state(term); + goto out; + } + } #endif - int base_width = c32width(base); - if (base_width > 0) { - term->grid->cursor.point.col = col; - term->grid->cursor.lcf = false; - insert_mode_disable = true; + int base_width = c32width(base); + if (base_width > 0) { + term->grid->cursor.point.col = col; + term->grid->cursor.lcf = false; + insert_mode_disable = true; - if (composed == NULL) { - bool base_from_primary; - bool comb_from_primary; - bool pre_from_primary; + if (composed == NULL) { + bool base_from_primary; + bool comb_from_primary; + bool pre_from_primary; - char32_t precomposed = term->fonts[0] != NULL - ? fcft_precompose( - term->fonts[0], base, wc, &base_from_primary, - &comb_from_primary, &pre_from_primary) - : (char32_t)-1; + char32_t precomposed = + term->fonts[0] != NULL + ? fcft_precompose(term->fonts[0], base, wc, &base_from_primary, + &comb_from_primary, &pre_from_primary) + : (char32_t)-1; - int precomposed_width = c32width(precomposed); + int precomposed_width = c32width(precomposed); - /* - * Only use the pre-composed character if: - * - * 1. we *have* a pre-composed character - * 2. the width matches the base characters width - * 3. it's in the primary font, OR one of the base or - * combining characters are *not* from the primary - * font - */ + /* + * Only use the pre-composed character if: + * + * 1. we *have* a pre-composed character + * 2. the width matches the base characters width + * 3. it's in the primary font, OR one of the base or + * combining characters are *not* from the primary + * font + */ - if (precomposed != (char32_t)-1 && - precomposed_width == base_width && - (pre_from_primary || - !base_from_primary || - !comb_from_primary)) - { - wc = precomposed; - width = precomposed_width; - term_reset_grapheme_state(term); - goto out; - } - } + if (precomposed != (char32_t)-1 && precomposed_width == base_width && + (pre_from_primary || !base_from_primary || !comb_from_primary)) { + wc = precomposed; + width = precomposed_width; + term_reset_grapheme_state(term); + goto out; + } + } - size_t wanted_count = composed != NULL ? composed->count + 1 : 2; - if (wanted_count > 255) { - xassert(composed != NULL); + size_t wanted_count = composed != NULL ? composed->count + 1 : 2; + if (wanted_count > 255) { + xassert(composed != NULL); #if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG - LOG_WARN("combining character overflow:"); - LOG_WARN(" base: 0x%04x", composed->chars[0]); - for (size_t i = 1; i < composed->count; i++) - LOG_WARN(" cc: 0x%04x", composed->chars[i]); - LOG_ERR(" new: 0x%04x", wc); + LOG_WARN("combining character overflow:"); + LOG_WARN(" base: 0x%04x", composed->chars[0]); + for (size_t i = 1; i < composed->count; i++) + LOG_WARN(" cc: 0x%04x", composed->chars[i]); + LOG_ERR(" new: 0x%04x", wc); #endif - /* This is going to break anyway... */ - wanted_count--; - } + /* This is going to break anyway... */ + wanted_count--; + } - xassert(wanted_count <= 255); + xassert(wanted_count <= 255); - /* Check if we already have a match for the entire compose chain */ - const struct composed *cc = - composed_lookup_without_collision( - term->composed, &key, - composed != NULL ? composed->chars : &(char32_t){base}, - composed != NULL ? composed->count : 1, - wc, 0); + /* Check if we already have a match for the entire compose chain */ + const struct composed *cc = composed_lookup_without_collision( + term->composed, &key, + composed != NULL ? composed->chars : &(char32_t){base}, + composed != NULL ? composed->count : 1, wc, 0); - if (cc != NULL) { - /* We *do* have a match! */ - wc = CELL_COMB_CHARS_LO + cc->key; - width = cc->width; - goto out; - } else { - /* No match - allocate a new chain below */ - } + if (cc != NULL) { + /* We *do* have a match! */ + wc = CELL_COMB_CHARS_LO + cc->key; + width = cc->width; + goto out; + } else { + /* No match - allocate a new chain below */ + } - if (unlikely(term->composed_count >= - (CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO))) - { - /* We reached our maximum number of allowed composed - * character chains. Fall through here and print the - * current zero-width character to the current cell */ - LOG_WARN("maximum number of composed characters reached"); - term_reset_grapheme_state(term); - goto out; - } + if (unlikely(term->composed_count >= + (CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO))) { + /* We reached our maximum number of allowed composed + * character chains. Fall through here and print the + * current zero-width character to the current cell */ + LOG_WARN("maximum number of composed characters reached"); + term_reset_grapheme_state(term); + goto out; + } - /* Allocate new chain */ - struct composed *new_cc = xmalloc(sizeof(*new_cc)); - new_cc->chars = xmalloc(wanted_count * sizeof(new_cc->chars[0])); - new_cc->key = key; - new_cc->count = wanted_count; - new_cc->chars[0] = base; - new_cc->chars[wanted_count - 1] = wc; - new_cc->forced_width = composed != NULL ? composed->forced_width : 0; + /* Allocate new chain */ + struct composed *new_cc = xmalloc(sizeof(*new_cc)); + new_cc->chars = xmalloc(wanted_count * sizeof(new_cc->chars[0])); + new_cc->key = key; + new_cc->count = wanted_count; + new_cc->chars[0] = base; + new_cc->chars[wanted_count - 1] = wc; + new_cc->forced_width = composed != NULL ? composed->forced_width : 0; - if (composed != NULL) { - memcpy(&new_cc->chars[1], &composed->chars[1], - (wanted_count - 2) * sizeof(new_cc->chars[0])); - } + if (composed != NULL) { + memcpy(&new_cc->chars[1], &composed->chars[1], + (wanted_count - 2) * sizeof(new_cc->chars[0])); + } - const int grapheme_width = - composed != NULL ? composed->width : base_width; + const int grapheme_width = + composed != NULL ? composed->width : base_width; - switch (term->conf->tweak.grapheme_width_method) { - case GRAPHEME_WIDTH_MAX: - new_cc->width = max(grapheme_width, width); - break; + switch (term->conf->tweak.grapheme_width_method) { + case GRAPHEME_WIDTH_MAX: + new_cc->width = max(grapheme_width, width); + break; - case GRAPHEME_WIDTH_DOUBLE: - new_cc->width = min(grapheme_width + width, 2); + case GRAPHEME_WIDTH_DOUBLE: + new_cc->width = min(grapheme_width + width, 2); #if defined(FOOT_GRAPHEME_CLUSTERING) - /* Handle VS-15 and VS-16 variation selectors */ - if (unlikely(grapheme_clustering && - (wc == 0xfe0e || wc == 0xfe0f) && - new_cc->count == 2)) - { - const struct emoji_vs *vs = - bsearch( - &(struct emoji_vs){.start = new_cc->chars[0]}, - emoji_vs, sizeof(emoji_vs) / sizeof(emoji_vs[0]), - sizeof(struct emoji_vs), - &emoji_vs_compare); + /* Handle VS-15 and VS-16 variation selectors */ + if (unlikely(grapheme_clustering && (wc == 0xfe0e || wc == 0xfe0f) && + new_cc->count == 2)) { + const struct emoji_vs *vs = + bsearch(&(struct emoji_vs){.start = new_cc->chars[0]}, emoji_vs, + sizeof(emoji_vs) / sizeof(emoji_vs[0]), + sizeof(struct emoji_vs), &emoji_vs_compare); - if (vs != NULL) { - xassert(new_cc->chars[0] >= vs->start && - new_cc->chars[0] <= vs->end); + if (vs != NULL) { + xassert(new_cc->chars[0] >= vs->start && + new_cc->chars[0] <= vs->end); - /* Force a grapheme width of 1 for VS-15, and 2 for VS-16 */ - if (wc == 0xfe0e) { - if (vs->vs15) - new_cc->width = 1; - } else if (wc == 0xfe0f) { - if (vs->vs16) - new_cc->width = 2; - } - } - } -#endif - - break; - - case GRAPHEME_WIDTH_WCSWIDTH: - new_cc->width = grapheme_width + width; - break; + /* Force a grapheme width of 1 for VS-15, and 2 for VS-16 */ + if (wc == 0xfe0e) { + if (vs->vs15) + new_cc->width = 1; + } else if (wc == 0xfe0f) { + if (vs->vs16) + new_cc->width = 2; } - - term->composed_count++; - composed_insert(&term->composed, new_cc); - - wc = CELL_COMB_CHARS_LO + new_cc->key; - width = new_cc->forced_width > 0 ? new_cc->forced_width : new_cc->width; - - xassert(wc >= CELL_COMB_CHARS_LO); - xassert(wc <= CELL_COMB_CHARS_HI); - goto out; + } } - } else - term_reset_grapheme_state(term); +#endif + break; + + case GRAPHEME_WIDTH_WCSWIDTH: + new_cc->width = grapheme_width + width; + break; + } + + term->composed_count++; + composed_insert(&term->composed, new_cc); + + wc = CELL_COMB_CHARS_LO + new_cc->key; + width = new_cc->forced_width > 0 ? new_cc->forced_width : new_cc->width; + + xassert(wc >= CELL_COMB_CHARS_LO); + xassert(wc <= CELL_COMB_CHARS_HI); + goto out; + } + } else + term_reset_grapheme_state(term); out: - if (width > 0) - term_print(term, wc, width, insert_mode_disable); + if (width > 0) + term_print(term, wc, width, insert_mode_disable); } -enum term_surface -term_surface_kind(const struct terminal *term, const struct wl_surface *surface) -{ - if (likely(surface == term->window->surface.surf)) - return TERM_SURF_GRID; - else if (surface == term->window->csd.surface[CSD_SURF_TITLE].surface.surf) - return TERM_SURF_TITLE; - else if (surface == term->window->csd.surface[CSD_SURF_LEFT].surface.surf) - return TERM_SURF_BORDER_LEFT; - else if (surface == term->window->csd.surface[CSD_SURF_RIGHT].surface.surf) - return TERM_SURF_BORDER_RIGHT; - else if (surface == term->window->csd.surface[CSD_SURF_TOP].surface.surf) - return TERM_SURF_BORDER_TOP; - else if (surface == term->window->csd.surface[CSD_SURF_BOTTOM].surface.surf) - return TERM_SURF_BORDER_BOTTOM; - else if (surface == term->window->csd.surface[CSD_SURF_MINIMIZE].surface.surf) - return TERM_SURF_BUTTON_MINIMIZE; - else if (surface == term->window->csd.surface[CSD_SURF_MAXIMIZE].surface.surf) - return TERM_SURF_BUTTON_MAXIMIZE; - else if (surface == term->window->csd.surface[CSD_SURF_CLOSE].surface.surf) - return TERM_SURF_BUTTON_CLOSE; - else if (surface == term->window->tab_bar.surface.surf) - return TERM_SURF_TAB_BAR; - else if (surface == term->window->tab_overview.surface.surf) - return TERM_SURF_TAB_OVERVIEW; - else - return TERM_SURF_NONE; +enum term_surface term_surface_kind(const struct terminal *term, + const struct wl_surface *surface) { + if (likely(surface == term->window->surface.surf)) + return TERM_SURF_GRID; + else if (surface == term->window->csd.surface[CSD_SURF_TITLE].surface.surf) + return TERM_SURF_TITLE; + else if (surface == term->window->csd.surface[CSD_SURF_LEFT].surface.surf) + return TERM_SURF_BORDER_LEFT; + else if (surface == term->window->csd.surface[CSD_SURF_RIGHT].surface.surf) + return TERM_SURF_BORDER_RIGHT; + else if (surface == term->window->csd.surface[CSD_SURF_TOP].surface.surf) + return TERM_SURF_BORDER_TOP; + else if (surface == term->window->csd.surface[CSD_SURF_BOTTOM].surface.surf) + return TERM_SURF_BORDER_BOTTOM; + else if (surface == term->window->csd.surface[CSD_SURF_MINIMIZE].surface.surf) + return TERM_SURF_BUTTON_MINIMIZE; + else if (surface == term->window->csd.surface[CSD_SURF_MAXIMIZE].surface.surf) + return TERM_SURF_BUTTON_MAXIMIZE; + else if (surface == term->window->csd.surface[CSD_SURF_CLOSE].surface.surf) + return TERM_SURF_BUTTON_CLOSE; + else if (surface == term->window->tab_bar.surface.surf) + return TERM_SURF_TAB_BAR; + else if (surface == term->window->tab_overview.surface.surf) + return TERM_SURF_TAB_OVERVIEW; + else + return TERM_SURF_NONE; } -static bool -rows_to_text(const struct terminal *term, int start, int end, - int col_start, int col_end, char **text, size_t *len) -{ - struct extraction_context *ctx = extract_begin(SELECTION_NONE, true); - if (ctx == NULL) - return false; - - const int grid_rows = term->grid->num_rows; - int r = start; - - while (true) { - const struct row *row = term->grid->rows[r]; - xassert(row != NULL); - - const int c_end = r == end ? col_end : term->cols; - - for (int c = col_start; c < c_end; c++) { - if (!extract_one(term, row, &row->cells[c], c, ctx)) - goto out; - } - - if (r == end) - break; - - r++; - r &= grid_rows - 1; - - col_start = 0; - } - -out: - return extract_finish(ctx, text, len); -} - -bool -term_scrollback_to_text(const struct terminal *term, char **text, size_t *len) -{ - const int grid_rows = term->grid->num_rows; - int start = (term->grid->offset + term->rows) & (grid_rows - 1); - int end = (term->grid->offset + term->rows - 1) & (grid_rows - 1); - - xassert(start >= 0); - xassert(start < grid_rows); - xassert(end >= 0); - xassert(end < grid_rows); - - /* If scrollback isn't full yet, this may be NULL, so scan forward - * until we find the first non-NULL row */ - while (term->grid->rows[start] == NULL) { - start++; - start &= grid_rows - 1; - } - - while (term->grid->rows[end] == NULL) { - end--; - if (end < 0) - end += term->grid->num_rows; - } - - return rows_to_text(term, start, end, 0, term->cols, text, len); -} - -bool -term_view_to_text(const struct terminal *term, char **text, size_t *len) -{ - int start = grid_row_absolute_in_view(term->grid, 0); - int end = grid_row_absolute_in_view(term->grid, term->rows - 1); - return rows_to_text(term, start, end, 0, term->cols, text, len); -} - -bool -term_command_output_to_text(const struct terminal *term, char **text, size_t *len) -{ - int start_row = -1; - int end_row = -1; - int start_col = -1; - int end_col = -1; - - const struct grid *grid = term->grid; - const int sb_end = grid_row_absolute(grid, term->rows - 1); - const int sb_start = (sb_end + 1) & (grid->num_rows - 1); - int r = sb_end; - - while (start_row < 0) { - const struct row *row = grid->rows[r]; - if (row == NULL) - break; - - if (row->shell_integration.cmd_end >= 0) { - end_row = r; - end_col = row->shell_integration.cmd_end; - } - - if (end_row >= 0 && row->shell_integration.cmd_start >= 0) { - start_row = r; - start_col = row->shell_integration.cmd_start; - } - - if (r == sb_start) - break; - - r = (r - 1 + grid->num_rows) & (grid->num_rows - 1); - } - - if (start_row < 0) - return false; - - bool ret = rows_to_text(term, start_row, end_row, start_col, end_col, text, len); - if (!ret) - return false; - - /* - * If the FTCS_COMMAND_FINISHED marker was emitted at the *first* - * column, then the *entire* previous line is part of the command - * output. *Including* the newline, if any. - * - * Since rows_to_text() doesn't extract the column - * FTCS_COMMAND_FINISHED was emitted at (that would be wrong - - * FTCS_COMMAND_FINISHED is emitted *after* the command output, - * not at its last character), the extraction logic will not see - * the last newline (this is true for all non-line-wise selection - * types), and the extracted text will *not* end with a newline. - * - * Here we try to compensate for that. Note that if 'end_col' is - * not 0, then the command output only covers a partial row, and - * thus we do *not* want to append a newline. - */ - - if (end_col > 0) { - /* Command output covers partial row - don't append newline */ - return true; - } - - int next_to_last_row = (end_row - 1 + grid->num_rows) & (grid->num_rows - 1); - const struct row *row = grid->rows[next_to_last_row]; - - /* Add newline if last row has a hard linebreak */ - if (row->linebreak) { - char *new_text = xrealloc(*text, *len + 1 + 1); - - if (new_text == NULL) { - /* Ignore failure - use text as is (without inserting newline) */ - return true; - } - - *text = new_text; - (*len)++; - (*text)[*len - 1] = '\n'; - (*text)[*len] = '\0'; - } - - return true; -} - -bool -term_ime_is_enabled(const struct terminal *term) -{ -#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED - return term->ime_enabled; -#else +static bool rows_to_text(const struct terminal *term, int start, int end, + int col_start, int col_end, char **text, size_t *len) { + struct extraction_context *ctx = extract_begin(SELECTION_NONE, true); + if (ctx == NULL) return false; -#endif -} -void -term_ime_enable(struct terminal *term) -{ -#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED - if (term->ime_enabled) - return; + const int grid_rows = term->grid->num_rows; + int r = start; - LOG_DBG("IME enabled"); + while (true) { + const struct row *row = term->grid->rows[r]; + xassert(row != NULL); - term->ime_enabled = true; + const int c_end = r == end ? col_end : term->cols; - /* IME is per seat - enable on all seat currently focusing us */ - tll_foreach(term->wl->seats, it) { - if (it->item.kbd_focus == term) - ime_enable(&it->item); + for (int c = col_start; c < c_end; c++) { + if (!extract_one(term, row, &row->cells[c], c, ctx)) + goto out; } -#endif + + if (r == end) + break; + + r++; + r &= grid_rows - 1; + + col_start = 0; + } + +out: + return extract_finish(ctx, text, len); } -void -term_ime_disable(struct terminal *term) -{ -#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED - if (!term->ime_enabled) - return; +bool term_scrollback_to_text(const struct terminal *term, char **text, + size_t *len) { + const int grid_rows = term->grid->num_rows; + int start = (term->grid->offset + term->rows) & (grid_rows - 1); + int end = (term->grid->offset + term->rows - 1) & (grid_rows - 1); - LOG_DBG("IME disabled"); + xassert(start >= 0); + xassert(start < grid_rows); + xassert(end >= 0); + xassert(end < grid_rows); - term->ime_enabled = false; + /* If scrollback isn't full yet, this may be NULL, so scan forward + * until we find the first non-NULL row */ + while (term->grid->rows[start] == NULL) { + start++; + start &= grid_rows - 1; + } - /* IME is per seat - disable on all seat currently focusing us */ - tll_foreach(term->wl->seats, it) { - if (it->item.kbd_focus == term) - ime_disable(&it->item); + while (term->grid->rows[end] == NULL) { + end--; + if (end < 0) + end += term->grid->num_rows; + } + + return rows_to_text(term, start, end, 0, term->cols, text, len); +} + +bool term_view_to_text(const struct terminal *term, char **text, size_t *len) { + int start = grid_row_absolute_in_view(term->grid, 0); + int end = grid_row_absolute_in_view(term->grid, term->rows - 1); + return rows_to_text(term, start, end, 0, term->cols, text, len); +} + +bool term_command_output_to_text(const struct terminal *term, char **text, + size_t *len) { + int start_row = -1; + int end_row = -1; + int start_col = -1; + int end_col = -1; + + const struct grid *grid = term->grid; + const int sb_end = grid_row_absolute(grid, term->rows - 1); + const int sb_start = (sb_end + 1) & (grid->num_rows - 1); + int r = sb_end; + + while (start_row < 0) { + const struct row *row = grid->rows[r]; + if (row == NULL) + break; + + if (row->shell_integration.cmd_end >= 0) { + end_row = r; + end_col = row->shell_integration.cmd_end; } + + if (end_row >= 0 && row->shell_integration.cmd_start >= 0) { + start_row = r; + start_col = row->shell_integration.cmd_start; + } + + if (r == sb_start) + break; + + r = (r - 1 + grid->num_rows) & (grid->num_rows - 1); + } + + if (start_row < 0) + return false; + + bool ret = + rows_to_text(term, start_row, end_row, start_col, end_col, text, len); + if (!ret) + return false; + + /* + * If the FTCS_COMMAND_FINISHED marker was emitted at the *first* + * column, then the *entire* previous line is part of the command + * output. *Including* the newline, if any. + * + * Since rows_to_text() doesn't extract the column + * FTCS_COMMAND_FINISHED was emitted at (that would be wrong - + * FTCS_COMMAND_FINISHED is emitted *after* the command output, + * not at its last character), the extraction logic will not see + * the last newline (this is true for all non-line-wise selection + * types), and the extracted text will *not* end with a newline. + * + * Here we try to compensate for that. Note that if 'end_col' is + * not 0, then the command output only covers a partial row, and + * thus we do *not* want to append a newline. + */ + + if (end_col > 0) { + /* Command output covers partial row - don't append newline */ + return true; + } + + int next_to_last_row = (end_row - 1 + grid->num_rows) & (grid->num_rows - 1); + const struct row *row = grid->rows[next_to_last_row]; + + /* Add newline if last row has a hard linebreak */ + if (row->linebreak) { + char *new_text = xrealloc(*text, *len + 1 + 1); + + if (new_text == NULL) { + /* Ignore failure - use text as is (without inserting newline) */ + return true; + } + + *text = new_text; + (*len)++; + (*text)[*len - 1] = '\n'; + (*text)[*len] = '\0'; + } + + return true; +} + +bool term_ime_is_enabled(const struct terminal *term) { +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + return term->ime_enabled; +#else + return false; #endif } -bool -term_ime_reset(struct terminal *term) -{ - bool at_least_one_seat_was_reset = false; +void term_ime_enable(struct terminal *term) { +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (term->ime_enabled) + return; + + LOG_DBG("IME enabled"); + + term->ime_enabled = true; + + /* IME is per seat - enable on all seat currently focusing us */ + tll_foreach(term->wl->seats, it) { + if (it->item.kbd_focus == term) + ime_enable(&it->item); + } +#endif +} + +void term_ime_disable(struct terminal *term) { +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (!term->ime_enabled) + return; + + LOG_DBG("IME disabled"); + + term->ime_enabled = false; + + /* IME is per seat - disable on all seat currently focusing us */ + tll_foreach(term->wl->seats, it) { + if (it->item.kbd_focus == term) + ime_disable(&it->item); + } +#endif +} + +bool term_ime_reset(struct terminal *term) { + bool at_least_one_seat_was_reset = false; #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED - tll_foreach(term->wl->seats, it) { - struct seat *seat = &it->item; + tll_foreach(term->wl->seats, it) { + struct seat *seat = &it->item; - if (seat->kbd_focus != term) - continue; + if (seat->kbd_focus != term) + continue; - ime_reset_preedit(seat); - at_least_one_seat_was_reset = true; - } + ime_reset_preedit(seat); + at_least_one_seat_was_reset = true; + } #endif - return at_least_one_seat_was_reset; + return at_least_one_seat_was_reset; } -void -term_ime_set_cursor_rect(struct terminal *term, int x, int y, int width, - int height) -{ +void term_ime_set_cursor_rect(struct terminal *term, int x, int y, int width, + int height) { #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED - tll_foreach(term->wl->seats, it) { - if (it->item.kbd_focus == term) { - it->item.ime.cursor_rect.pending.x = x; - it->item.ime.cursor_rect.pending.y = y; - it->item.ime.cursor_rect.pending.width = width; - it->item.ime.cursor_rect.pending.height = height; - } + tll_foreach(term->wl->seats, it) { + if (it->item.kbd_focus == term) { + it->item.ime.cursor_rect.pending.x = x; + it->item.ime.cursor_rect.pending.y = y; + it->item.ime.cursor_rect.pending.width = width; + it->item.ime.cursor_rect.pending.height = height; } + } #endif } -void -term_osc8_open(struct terminal *term, uint64_t id, const char *uri) -{ - term_osc8_close(term); - xassert(term->vt.osc8.uri == NULL); +void term_osc8_open(struct terminal *term, uint64_t id, const char *uri) { + term_osc8_close(term); + xassert(term->vt.osc8.uri == NULL); - term->vt.osc8.id = id; - term->vt.osc8.uri = xstrdup(uri); + term->vt.osc8.id = id; + term->vt.osc8.uri = xstrdup(uri); - term->bits_affecting_ascii_printer.osc8 = true; - term_update_ascii_printer(term); + term->bits_affecting_ascii_printer.osc8 = true; + term_update_ascii_printer(term); } -void -term_osc8_close(struct terminal *term) -{ - free(term->vt.osc8.uri); - term->vt.osc8.uri = NULL; - term->vt.osc8.id = 0; - term->bits_affecting_ascii_printer.osc8 = false; - term_update_ascii_printer(term); +void term_osc8_close(struct terminal *term) { + free(term->vt.osc8.uri); + term->vt.osc8.uri = NULL; + term->vt.osc8.id = 0; + term->bits_affecting_ascii_printer.osc8 = false; + term_update_ascii_printer(term); } -void -term_set_user_mouse_cursor(struct terminal *term, const char *cursor) -{ - free(term->mouse_user_cursor); - term->mouse_user_cursor = cursor != NULL && strlen(cursor) > 0 - ? xstrdup(cursor) - : NULL; - term_xcursor_update(term); +void term_set_user_mouse_cursor(struct terminal *term, const char *cursor) { + free(term->mouse_user_cursor); + term->mouse_user_cursor = + cursor != NULL && strlen(cursor) > 0 ? xstrdup(cursor) : NULL; + term_xcursor_update(term); } -void -term_enable_size_notifications(struct terminal *term) -{ - /* Note: always send current size upon activation, regardless of - previous state */ - term->size_notifications = true; - term_send_size_notification(term); +void term_enable_size_notifications(struct terminal *term) { + /* Note: always send current size upon activation, regardless of + previous state */ + term->size_notifications = true; + term_send_size_notification(term); } -void -term_disable_size_notifications(struct terminal *term) -{ - if (!term->size_notifications) - return; +void term_disable_size_notifications(struct terminal *term) { + if (!term->size_notifications) + return; - term->size_notifications = false; + term->size_notifications = false; } -void -term_send_size_notification(struct terminal *term) -{ - if (!term->size_notifications) - return; +void term_send_size_notification(struct terminal *term) { + if (!term->size_notifications) + return; - const int height = term->height - term->margins.top - term->margins.bottom; - const int width = term->width - term->margins.left - term->margins.right; + const int height = term->height - term->margins.top - term->margins.bottom; + const int width = term->width - term->margins.left - term->margins.right; - char buf[128]; - const size_t n = xsnprintf( - buf, sizeof(buf), "\033[48;%d;%d;%d;%dt", - term->rows, term->cols, height, width); - term_to_slave(term, buf, n); + char buf[128]; + const size_t n = xsnprintf(buf, sizeof(buf), "\033[48;%d;%d;%d;%dt", + term->rows, term->cols, height, width); + term_to_slave(term, buf, n); } -void -term_theme_switch_to_dark(struct terminal *term) -{ - if (term->colors.active_theme == COLOR_THEME_DARK) - return; +void term_theme_switch_to_dark(struct terminal *term) { + if (term->colors.active_theme == COLOR_THEME_DARK) + return; - term_theme_apply(term, &term->conf->colors_dark); - term->colors.active_theme = COLOR_THEME_DARK; + term_theme_apply(term, &term->conf->colors_dark); + term->colors.active_theme = COLOR_THEME_DARK; - wayl_win_alpha_changed(term->window); - term_font_subpixel_changed(term); + wayl_win_alpha_changed(term->window); + term_font_subpixel_changed(term); - if (term->report_theme_changes) - term_to_slave(term, "\033[?997;1n", 9); + if (term->report_theme_changes) + term_to_slave(term, "\033[?997;1n", 9); - term_damage_view(term); - term_damage_margins(term); - render_refresh(term); + term_damage_view(term); + term_damage_margins(term); + render_refresh(term); } -void -term_theme_switch_to_light(struct terminal *term) -{ - if (term->colors.active_theme == COLOR_THEME_LIGHT) - return; +void term_theme_switch_to_light(struct terminal *term) { + if (term->colors.active_theme == COLOR_THEME_LIGHT) + return; + term_theme_apply(term, &term->conf->colors_light); + term->colors.active_theme = COLOR_THEME_LIGHT; + + wayl_win_alpha_changed(term->window); + term_font_subpixel_changed(term); + + if (term->report_theme_changes) + term_to_slave(term, "\033[?997;2n", 9); + + term_damage_view(term); + term_damage_margins(term); + render_refresh(term); +} + +void term_theme_toggle(struct terminal *term) { + if (term->colors.active_theme == COLOR_THEME_DARK) { term_theme_apply(term, &term->conf->colors_light); term->colors.active_theme = COLOR_THEME_LIGHT; - wayl_win_alpha_changed(term->window); - term_font_subpixel_changed(term); + if (term->report_theme_changes) + term_to_slave(term, "\033[?997;2n", 9); + } else { + term_theme_apply(term, &term->conf->colors_dark); + term->colors.active_theme = COLOR_THEME_DARK; if (term->report_theme_changes) - term_to_slave(term, "\033[?997;2n", 9); + term_to_slave(term, "\033[?997;1n", 9); + } - term_damage_view(term); - term_damage_margins(term); - render_refresh(term); + wayl_win_alpha_changed(term->window); + term_font_subpixel_changed(term); + + term_damage_view(term); + term_damage_margins(term); + render_refresh(term); } -void -term_theme_toggle(struct terminal *term) -{ - if (term->colors.active_theme == COLOR_THEME_DARK) { - term_theme_apply(term, &term->conf->colors_light); - term->colors.active_theme = COLOR_THEME_LIGHT; - - if (term->report_theme_changes) - term_to_slave(term, "\033[?997;2n", 9); - } else { - term_theme_apply(term, &term->conf->colors_dark); - term->colors.active_theme = COLOR_THEME_DARK; - - if (term->report_theme_changes) - term_to_slave(term, "\033[?997;1n", 9); - } - - wayl_win_alpha_changed(term->window); - term_font_subpixel_changed(term); - - term_damage_view(term); - term_damage_margins(term); - render_refresh(term); -} - -const struct color_theme * -term_theme_get(const struct terminal *term) -{ - return term->colors.active_theme == COLOR_THEME_DARK - ? &term->conf->colors_dark - : &term->conf->colors_light; +const struct color_theme *term_theme_get(const struct terminal *term) { + return term->colors.active_theme == COLOR_THEME_DARK + ? &term->conf->colors_dark + : &term->conf->colors_light; } diff --git a/terminal.h b/terminal.h index 985d2ae..e06b068 100644 --- a/terminal.h +++ b/terminal.h @@ -344,6 +344,15 @@ enum selection_direction {SELECTION_UNDIR, SELECTION_LEFT, SELECTION_RIGHT}; enum selection_scroll_direction {SELECTION_SCROLL_NOT, SELECTION_SCROLL_UP, SELECTION_SCROLL_DOWN}; enum search_direction { SEARCH_BACKWARD_SAME_POSITION, SEARCH_BACKWARD, SEARCH_FORWARD }; enum search_case_mode { SEARCH_CASE_SMART, SEARCH_CASE_SENSITIVE, SEARCH_CASE_INSENSITIVE }; +enum search_mode { + SEARCH_MODE_NORMAL, + SEARCH_MODE_SESSION_SAVE, + SEARCH_MODE_SESSION_LOAD, + SEARCH_MODE_SESSION_OVERWRITE_CONFIRM, + SEARCH_MODE_SESSION_SAVE_SECURE_NAME, + SEARCH_MODE_SESSION_SAVE_SECURE_PASSWORD, + SEARCH_MODE_SESSION_LOAD_PASSWORD, +}; struct ptmx_buffer { void *data; @@ -626,6 +635,7 @@ struct terminal { size_t len; size_t sz; size_t cursor; + enum search_mode mode; int original_view; bool view_followed_offset; @@ -865,6 +875,24 @@ struct terminal { char *foot_exe; char *cwd; + /* Active only while term->search.mode == SEARCH_MODE_SESSION_LOAD */ + struct { + char **all; /* All session names from disk */ + size_t all_count; + size_t *filtered; /* indices into all[] matching current input */ + size_t filtered_count; + size_t filtered_cap; + size_t sel; /* index within filtered[] */ + } session_picker; + + /* Name held across a multi-step session flow (overwrite confirm, secure + * save name->password, load password). */ + char *session_pending_name; + bool session_pending_secure; /* set when overwrite-confirm precedes a secure save */ + /* For LOAD_PASSWORD: heap-allocated session_state pulled from JSON. The + * new tab isn't spawned until decryption succeeds, so we hold it here. */ + void *session_pending_load_state; + bool grapheme_shaping; bool size_notifications; }; @@ -882,6 +910,15 @@ struct terminal *term_tab_new( int argc, char *const *argv, const char *const *envp, void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data); +/* + * Like term_tab_new but with an explicit override for the new tab's cwd. + * If override_cwd is NULL behaves identically to term_tab_new. + */ +struct terminal *term_tab_new_with_cwd( + struct terminal *primary, const char *override_cwd, + int argc, char *const *argv, const char *const *envp, + void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data); + void term_tab_switch(struct wl_window *win, size_t idx); void term_tab_close(struct terminal *term); size_t term_tab_bar_hit_test(const struct terminal *term, int x, int y);