From 692b22cbbb24efa91cb24446d189bc8c2535c1ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 16 Jul 2025 08:31:42 +0200 Subject: [PATCH 01/84] changelog: add new 'unreleased' section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8985905e..6121955f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.23.0](#1-23-0) * [1.22.3](#1-22-3) * [1.22.2](#1-22-2) @@ -63,6 +64,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.23.0 ### Added From cc290fa9b0c36e90f6af978f839c083d05f22a95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 17 Jul 2025 10:40:20 +0200 Subject: [PATCH 02/84] url-mode: assign label keys in reverse order The _last_ URL is often the one you are interested in, and with this change, it is always assigned the first (and thus the same) key. Closes #2140 --- CHANGELOG.md | 8 ++++++++ url-mode.c | 8 ++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6121955f..df52e27d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,14 @@ ## Unreleased ### Added ### Changed + +* URL labels are now assigned in reverse order, from bottom to + top. This ensures the **last** URL (which is often the one you are + interested in) is always assigned the same key ([#2140][2140]). + +[2140]: https://codeberg.org/dnkl/foot/issues/2140 + + ### Deprecated ### Removed ### Fixed diff --git a/url-mode.c b/url-mode.c index 35843219..c25396cd 100644 --- a/url-mode.c +++ b/url-mode.c @@ -634,12 +634,12 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) size_t combo_idx = 0; - tll_foreach(*urls, it) { + tll_rforeach(*urls, it) { bool id_already_seen = false; /* Look for already processed URLs where both the URI and the * ID matches */ - tll_foreach(*urls, it2) { + tll_rforeach(*urls, it2) { if (&it->item == &it2->item) break; @@ -659,7 +659,7 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) * them; if so, reuse the *same* key combo. */ bool url_already_seen = false; - tll_foreach(*urls, it2) { + tll_rforeach(*urls, it2) { if (&it->item == &it2->item) break; @@ -679,7 +679,7 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) free(combos[i]); #if defined(_DEBUG) && LOG_ENABLE_DBG - tll_foreach(*urls, it) { + tll_rforeach(*urls, it) { if (it->item.key == NULL) continue; From 01387f9593294f4eb467a162f245e3c712635f35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 17 Jul 2025 10:18:17 +0200 Subject: [PATCH 03/84] main: SIGUSR1 selects the first color theme, SIGUSR2 the second Before this patch, SIGUSR1 toggled between [colors] and [colors2]. Now, SIGUSR1 changes to [colors], regardless of what the current color theme is, and SIGUSR2 changes to [colors2]. Closes #2144 --- CHANGELOG.md | 4 ++++ main.c | 22 +++++++++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df52e27d..af7fe73a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,8 +71,12 @@ * URL labels are now assigned in reverse order, from bottom to top. This ensures the **last** URL (which is often the one you are interested in) is always assigned the same key ([#2140][2140]). +* Sending `SIGUSR1` no longer **toggles** between `[colors]` and + `[colors2]`, but explicitly changes to `[colors]`. `SIGUSR2` changes + to `[colors2]` ([#2144][2144]). [2140]: https://codeberg.org/dnkl/foot/issues/2140 +[2144]: https://codeberg.org/dnkl/foot/issues/2144 ### Deprecated diff --git a/main.c b/main.c index 37bbb6a7..517d8460 100644 --- a/main.c +++ b/main.c @@ -46,13 +46,22 @@ fdm_sigint(struct fdm *fdm, int signo, void *data) } static bool -fdm_sigusr1(struct fdm *fdm, int signo, void *data) +fdm_sigusr(struct fdm *fdm, int signo, void *data) { struct wayland *wayl = data; - tll_foreach(wayl->terms, it) { - struct terminal *term = it->item; - term_theme_toggle(term); + xassert(signo == SIGUSR1 || signo == SIGUSR2); + + if (signo == SIGUSR1) { + tll_foreach(wayl->terms, it) { + struct terminal *term = it->item; + term_theme_switch_to_1(term); + } + } else { + tll_foreach(wayl->terms, it) { + struct terminal *term = it->item; + term_theme_switch_to_2(term); + } } return true; @@ -621,8 +630,11 @@ main(int argc, char *const *argv) goto out; } - if (!fdm_signal_add(fdm, SIGUSR1, &fdm_sigusr1, wayl)) + if (!fdm_signal_add(fdm, SIGUSR1, &fdm_sigusr, wayl) || + !fdm_signal_add(fdm, SIGUSR2, &fdm_sigusr, wayl)) + { goto out; + } struct sigaction sig_ign = {.sa_handler = SIG_IGN}; sigemptyset(&sig_ign.sa_mask); From 57ae3bb89c6bf50e70e831e3658e59a6877e9817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 18 Jul 2025 17:24:18 +0200 Subject: [PATCH 04/84] main: unregister SIGUSR2 on exit --- main.c | 1 + 1 file changed, 1 insertion(+) diff --git a/main.c b/main.c index 517d8460..f97f21b5 100644 --- a/main.c +++ b/main.c @@ -672,6 +672,7 @@ out: key_binding_manager_destroy(key_binding_manager); reaper_destroy(reaper); fdm_signal_del(fdm, SIGUSR1); + fdm_signal_del(fdm, SIGUSR2); fdm_signal_del(fdm, SIGTERM); fdm_signal_del(fdm, SIGINT); fdm_destroy(fdm); From 7ab43ebf7464ab72e2041de0a1112feaa81ea89e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 21 Jul 2025 13:49:57 +0200 Subject: [PATCH 05/84] shm: don't set pixman_fmt_without_alpha twice When selecting 16-bit surfaces, we set pixman_fmt_without_alpha twice, and never set pixman_fmt_with_alpha. This caused 10-bit surfaces to be used instead, since it checks if pixman_fmt_with_alpha has been overridden or not. --- CHANGELOG.md | 4 ++++ shm.c | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af7fe73a..cfbbde53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,10 @@ ### Deprecated ### Removed ### Fixed + +* 10-bit surfaces sometimes used instead of 16-bit. + + ### Security ### Contributors diff --git a/shm.c b/shm.c index b586b504..4680c12c 100644 --- a/shm.c +++ b/shm.c @@ -994,7 +994,7 @@ shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, pixman_fmt_without_alpha = PIXMAN_a16b16g16r16; shm_fmt_without_alpha = WL_SHM_FORMAT_XBGR16161616; - pixman_fmt_without_alpha = PIXMAN_a16b16g16r16; + pixman_fmt_with_alpha = PIXMAN_a16b16g16r16; shm_fmt_with_alpha = WL_SHM_FORMAT_ABGR16161616; if (!have_logged) { From 21db6a6cdc68a5a8b9561d07d3ef69e11d2a077e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 21 Jul 2025 15:28:52 +0200 Subject: [PATCH 06/84] fdm: when logging signal related errors, include the signal name Since sigabbrev_np() is GNU only, provide a fallback function that returns "SIG" when sigabbrev_np() doesn't exist (for example, on FreeBSD). --- fdm.c | 30 ++++++++++++++++++++++++------ meson.build | 6 ++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/fdm.c b/fdm.c index ea30443b..4822cd97 100644 --- a/fdm.c +++ b/fdm.c @@ -18,6 +18,18 @@ #include "debug.h" #include "xmalloc.h" +#if !defined(SIGABBREV_NP) +#include + +static const char * +sigabbrev_np(int sig) +{ + static char buf[16]; + snprintf(buf, sizeof(buf), "<%d>", sig); + return buf; +} +#endif + struct fd_handler { int fd; int events; @@ -113,7 +125,8 @@ fdm_destroy(struct fdm *fdm) for (int i = 0; i < SIGRTMAX; i++) { if (fdm->signal_handlers[i].callback != NULL) - LOG_WARN("handler for signal %d not removed", i); + LOG_WARN("handler for signal %d (SIG%s) not removed", + i, sigabbrev_np(i)); } if (tll_length(fdm->hooks_low) > 0 || @@ -338,7 +351,8 @@ bool fdm_signal_add(struct fdm *fdm, int signo, fdm_signal_handler_t handler, void *data) { if (fdm->signal_handlers[signo].callback != NULL) { - LOG_ERR("signal %d already has a handler", signo); + LOG_ERR("signal %d (SIG%s) already has a handler", + signo, sigabbrev_np(signo)); return false; } @@ -347,14 +361,16 @@ fdm_signal_add(struct fdm *fdm, int signo, fdm_signal_handler_t handler, void *d sigaddset(&mask, signo); if (sigprocmask(SIG_BLOCK, &mask, &original) < 0) { - LOG_ERRNO("failed to block signal %d", signo); + LOG_ERRNO("failed to block signal %d (SIG%s)", + signo, sigabbrev_np(signo)); return false; } struct sigaction action = {.sa_handler = &signal_handler}; sigemptyset(&action.sa_mask); if (sigaction(signo, &action, NULL) < 0) { - LOG_ERRNO("failed to set signal handler for signal %d", signo); + LOG_ERRNO("failed to set signal handler for signal %d (SIG%s)", + signo, sigabbrev_np(signo)); sigprocmask(SIG_SETMASK, &original, NULL); return false; } @@ -374,7 +390,8 @@ fdm_signal_del(struct fdm *fdm, int signo) struct sigaction action = {.sa_handler = SIG_DFL}; sigemptyset(&action.sa_mask); if (sigaction(signo, &action, NULL) < 0) { - LOG_ERRNO("failed to restore signal handler for signal %d", signo); + LOG_ERRNO("failed to restore signal handler for signal %d (SIG%s)", + signo, sigabbrev_np(signo)); return false; } @@ -386,7 +403,8 @@ fdm_signal_del(struct fdm *fdm, int signo) sigemptyset(&mask); sigaddset(&mask, signo); if (sigprocmask(SIG_UNBLOCK, &mask, NULL) < 0) { - LOG_ERRNO("failed to unblock signal %d", signo); + LOG_ERRNO("failed to unblock signal %d (SIG%s)", + signo, sigabbrev_np(signo)); return false; } diff --git a/meson.build b/meson.build index b9994c11..8a072dcf 100644 --- a/meson.build +++ b/meson.build @@ -25,6 +25,12 @@ if cc.has_function('execvpe', add_project_arguments('-DEXECVPE', language: 'c') endif +if cc.has_function('sigabbrev_np', + args: ['-D_GNU_SOURCE'], + prefix: '#include ') + add_project_arguments('-DSIGABBREV_NP', language: 'c') +endif + utmp_backend = get_option('utmp-backend') if utmp_backend == 'auto' host_os = host_machine.system() From 42be74214a0b2cd1b4245b262c99cbd5030a0e0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 22 Jul 2025 13:30:00 +0200 Subject: [PATCH 07/84] term: make sure the color table is populated *before* the slave process is spawned --- terminal.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/terminal.c b/terminal.c index 135f21ce..2e23f749 100644 --- a/terminal.c +++ b/terminal.c @@ -1409,6 +1409,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, 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]; @@ -1443,8 +1444,6 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, xassert(tll_length(term->wl->monitors) > 0); term->scale = tll_front(term->wl->monitors).scale; - memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); - /* Initialize the Wayland window backend */ if ((term->window = wayl_win_init(term, token)) == NULL) goto err; From fcde74a18150a1722f2be2b7990c2f9b45268854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 22 Jul 2025 13:30:28 +0200 Subject: [PATCH 08/84] osc: color reset: read default color from currently active theme --- CHANGELOG.md | 2 ++ osc.c | 69 ++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfbbde53..96040e86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,8 @@ ### Fixed * 10-bit surfaces sometimes used instead of 16-bit. +* OSC-104/110/111/112/117/119 (reset colors) not taking the currently + active theme into account. ### Security diff --git a/osc.c b/osc.c index 834029c1..0b492564 100644 --- a/osc.c +++ b/osc.c @@ -1515,10 +1515,14 @@ osc_dispatch(struct terminal *term) case 104: { /* Reset Color Number 'c' (whole table if no parameter) */ + const struct color_theme *theme = + term->colors.active_theme == COLOR_THEME1 + ? &term->conf->colors + : &term->conf->colors2; + if (string[0] == '\0') { LOG_DBG("resetting all colors"); - for (size_t i = 0; i < ALEN(term->colors.table); i++) - term->colors.table[i] = term->conf->colors.table[i]; + memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); term_damage_view(term); } @@ -1540,7 +1544,7 @@ osc_dispatch(struct terminal *term) } LOG_DBG("resetting color #%u", idx); - term->colors.table[idx] = term->conf->colors.table[idx]; + term->colors.table[idx] = theme->table[idx]; term_damage_color(term, COLOR_BASE256, idx); } @@ -1553,16 +1557,28 @@ osc_dispatch(struct terminal *term) case 110: /* Reset default text foreground color */ LOG_DBG("resetting foreground color"); - term->colors.fg = term->conf->colors.fg; + + const struct color_theme *theme = + term->colors.active_theme == COLOR_THEME1 + ? &term->conf->colors + : &term->conf->colors2; + + term->colors.fg = theme->fg; term_damage_color(term, COLOR_DEFAULT, 0); break; case 111: { /* Reset default text background color */ LOG_DBG("resetting background color"); - bool alpha_changed = term->colors.alpha != term->conf->colors.alpha; - term->colors.bg = term->conf->colors.bg; - term->colors.alpha = term->conf->colors.alpha; + const struct color_theme *theme = + term->colors.active_theme == COLOR_THEME1 + ? &term->conf->colors + : &term->conf->colors2; + + bool alpha_changed = term->colors.alpha != theme->alpha; + + term->colors.bg = theme->bg; + term->colors.alpha = theme->alpha; if (alpha_changed) { wayl_win_alpha_changed(term->window); @@ -1574,10 +1590,16 @@ osc_dispatch(struct terminal *term) break; } - case 112: + case 112: { LOG_DBG("resetting cursor color"); - term->colors.cursor_fg = term->conf->colors.cursor.text; - term->colors.cursor_bg = term->conf->colors.cursor.cursor; + + const struct color_theme *theme = + term->colors.active_theme == COLOR_THEME1 + ? &term->conf->colors + : &term->conf->colors2; + + term->colors.cursor_fg = theme->cursor.text; + term->colors.cursor_bg = theme->cursor.cursor; if (term->conf->colors.use_custom.cursor) { term->colors.cursor_fg |= 1u << 31; @@ -1586,16 +1608,31 @@ osc_dispatch(struct terminal *term) term_damage_cursor(term); break; + } - case 117: + case 117: { LOG_DBG("resetting selection background color"); - term->colors.selection_bg = term->conf->colors.selection_bg; - break; - case 119: - LOG_DBG("resetting selection foreground color"); - term->colors.selection_fg = term->conf->colors.selection_fg; + const struct color_theme *theme = + term->colors.active_theme == COLOR_THEME1 + ? &term->conf->colors + : &term->conf->colors2; + + term->colors.selection_bg = theme->selection_bg; break; + } + + case 119: { + LOG_DBG("resetting selection foreground color"); + + const struct color_theme *theme = + term->colors.active_theme == COLOR_THEME1 + ? &term->conf->colors + : &term->conf->colors2; + + term->colors.selection_fg = theme->selection_fg; + break; + } case 133: /* From 95e8b18c125c1ad2835ae59204dd0f8b5feaf64b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 23 Jul 2025 08:27:59 +0200 Subject: [PATCH 09/84] changelog: prepare for 1.23.1 --- CHANGELOG.md | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96040e86..6d720f0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.23.1](#1-23-1) * [1.23.0](#1-23-0) * [1.22.3](#1-22-3) * [1.22.2](#1-22-2) @@ -64,8 +64,8 @@ * [1.2.0](#1-2-0) -## Unreleased -### Added +## 1.23.1 + ### Changed * URL labels are now assigned in reverse order, from bottom to @@ -79,8 +79,6 @@ [2144]: https://codeberg.org/dnkl/foot/issues/2144 -### Deprecated -### Removed ### Fixed * 10-bit surfaces sometimes used instead of 16-bit. @@ -88,10 +86,6 @@ active theme into account. -### Security -### Contributors - - ## 1.23.0 ### Added From 43620935a169c03ab5744d12f5917269ba92567e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 23 Jul 2025 08:28:13 +0200 Subject: [PATCH 10/84] meson: bump version to 1.23.1 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 8a072dcf..0b7bbc17 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.23.0', + version: '1.23.1', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From 86d63f08ba118f0051b6941353d0a888f4987d75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 23 Jul 2025 08:31:30 +0200 Subject: [PATCH 11/84] changelog: add new 'unreleased' section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d720f0a..c628155c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.23.1](#1-23-1) * [1.23.0](#1-23-0) * [1.22.3](#1-22-3) @@ -64,6 +65,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.23.1 ### Changed From f873aa904db36ed9477deaba5ee2f4906c4d7831 Mon Sep 17 00:00:00 2001 From: Tobias Mock Date: Mon, 21 Jul 2025 23:28:02 +0200 Subject: [PATCH 12/84] Add tinted variant of modus-vivendi theme --- themes/modus-vivendi-tinted | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 themes/modus-vivendi-tinted diff --git a/themes/modus-vivendi-tinted b/themes/modus-vivendi-tinted new file mode 100644 index 00000000..67cf02a0 --- /dev/null +++ b/themes/modus-vivendi-tinted @@ -0,0 +1,25 @@ +# -*- conf -*- +# +# modus-vivendi-tinted +# See: https://protesilaos.com/emacs/modus-themes +# + +[colors] +background=0d0e1c +foreground=ffffff +regular0=000000 +regular1=ff5f59 +regular2=44bc44 +regular3=d0bc00 +regular4=2fafff +regular5=feacd0 +regular6=00d3d0 +regular7=a6a6a6 +bright0=595959 +bright1=ff6b55 +bright2=ff6b55 +bright3=fec43f +bright4=fec43f +bright5=b6a0ff +bright6=6ae4b9 +bright7=777777 From 83303bd2a461b5917de51d8740999cbdb46e7986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 29 Jul 2025 11:18:49 +0200 Subject: [PATCH 13/84] url-mode: for some reason we sorted the label letters before assigning them Don't do this. Now that we **don't** sort them, the first letter chosen by the user is always assigned to the bottom most URL. Closes #2140 (again) --- CHANGELOG.md | 5 +++++ url-mode.c | 12 ------------ 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c628155c..e405459e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,11 @@ ## Unreleased ### Added ### Changed + +* The label letters are no longer sorted before being assigned to URLs + ([#2140]2140[]). + + ### Deprecated ### Removed ### Fixed diff --git a/url-mode.c b/url-mode.c index c25396cd..19d95356 100644 --- a/url-mode.c +++ b/url-mode.c @@ -557,14 +557,6 @@ urls_collect(const struct terminal *term, enum url_action action, remove_overlapping(urls, term->grid->num_cols); } -static int -c32cmp_qsort_wrapper(const void *_a, const void *_b) -{ - const char32_t *a = *(const char32_t **)_a; - const char32_t *b = *(const char32_t **)_b; - return c32cmp(a, b); -} - static void generate_key_combos(const struct config *conf, size_t count, char32_t *combos[static count]) @@ -607,10 +599,6 @@ generate_key_combos(const struct config *conf, } free(hints); - /* Sorting is a kind of shuffle, since we're sorting on the - * *reversed* strings */ - qsort(combos, count, sizeof(char32_t *), &c32cmp_qsort_wrapper); - /* Reverse all strings */ for (size_t i = 0; i < count; i++) { const size_t len = c32len(combos[i]); From 7636f264a818eb11b155be051d012a616984a5a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 26 Jul 2025 12:21:51 +0200 Subject: [PATCH 14/84] slave: remove more environment variables set by other terminals This ensures applications don't mistake foot for another terminal emulator. Not that applications _should_ rely on environment variables, but some do anyway... --- slave.c | 50 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/slave.c b/slave.c index 47e59e87..62899372 100644 --- a/slave.c +++ b/slave.c @@ -436,8 +436,54 @@ slave_spawn(int ptmx, int argc, const char *cwd, char *const *argv, add_to_env(&custom_env, "COLORTERM", "truecolor"); add_to_env(&custom_env, "PWD", cwd); - del_from_env(&custom_env, "TERM_PROGRAM"); - del_from_env(&custom_env, "TERM_PROGRAM_VERSION"); + del_from_env(&custom_env, "TERM_PROGRAM"); /* Wezterm, Ghostty */ + del_from_env(&custom_env, "TERM_PROGRAM_VERSION"); /* Wezterm, Ghostty */ + del_from_env(&custom_env, "TERMINAL_NAME"); /* Contour */ + del_from_env(&custom_env, "TERMINAL_VERSION_STRING"); /* Contour */ + del_from_env(&custom_env, "TERMINAL_VERSION_TRIPLE"); /* Contour */ + + /* XTerm specific */ + del_from_env(&custom_env, "XTERM_SHELL"); + del_from_env(&custom_env, "XTERM_VERSION"); + del_from_env(&custom_env, "XTERM_LOCALE"); + + /* Mlterm specific */ + del_from_env(&custom_env, "MLTERM"); + + /* Zutty specific */ + del_from_env(&custom_env, "ZUTTY_VERSION"); + + /* Ghostty specific */ + del_from_env(&custom_env, "GHOSTTY_BIN_DIR"); + del_from_env(&custom_env, "GHOSTTY_SHELL_INTEGRATION_NO_SUDO"); + del_from_env(&custom_env, "GHOSTTY_RESOURCES_DIR"); + + /* Kitty specific */ + del_from_env(&custom_env, "KITTY_WINDOW_ID"); + del_from_env(&custom_env, "KITTY_PID"); + del_from_env(&custom_env, "KITTY_PUBLIC_KEY"); + del_from_env(&custom_env, "KITTY_INSTALLATION_DIR"); + + /* Contour specific */ + del_from_env(&custom_env, "CONTOUR_PROFILE"); + + /* Wezterm specific */ + del_from_env(&custom_env, "WEZTERM_PANE"); + del_from_env(&custom_env, "WEZTERM_EXECUTABLE"); + del_from_env(&custom_env, "WEZTERM_CONFIG_FILE"); + del_from_env(&custom_env, "WEZTERM_EXECUTABLE_DIR"); + del_from_env(&custom_env, "WEZTERM_UNIX_SOCKET"); + del_from_env(&custom_env, "WEZTERM_CONFIG_DIR"); + + /* Alacritty specific */ + del_from_env(&custom_env, "ALACRITTY_LOG"); + del_from_env(&custom_env, "ALACRITTY_WINDOW_ID"); + del_from_env(&custom_env, "ALACRITTY_SOCKET"); + + /* VTE, gnome-terminal, kgx etc */ + del_from_env(&custom_env, "VTE_VERSION"); + del_from_env(&custom_env, "GNOME_TERMINAL_SERVICE"); + del_from_env(&custom_env, "GNOME_TERMINAL_SCREEN"); #if defined(FOOT_TERMINFO_PATH) add_to_env(&custom_env, "TERMINFO", FOOT_TERMINFO_PATH); From 6eedc88d70520456fbb89db2600e762e722fe313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 30 Jul 2025 12:23:39 +0200 Subject: [PATCH 15/84] server: sigusr1/2: update conf object with the "new" theme When sending SIGUSR1/SIGUSR2 to a server process, all currently running client instances change their theme. But before this patch, all future instances used the original theme. With this patch, the server owned config object is updated with the selected theme, thus making new instances use the same theme as well. --- main.c | 36 +++++++++++++++++++++++------------- server.c | 24 ++++++++++++++++++++++-- server.h | 5 ++++- 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/main.c b/main.c index f97f21b5..1afbd16d 100644 --- a/main.c +++ b/main.c @@ -45,23 +45,28 @@ fdm_sigint(struct fdm *fdm, int signo, void *data) return true; } +struct sigusr_context { + struct terminal *term; + struct server *server; +}; + static bool fdm_sigusr(struct fdm *fdm, int signo, void *data) { - struct wayland *wayl = data; - xassert(signo == SIGUSR1 || signo == SIGUSR2); - if (signo == SIGUSR1) { - tll_foreach(wayl->terms, it) { - struct terminal *term = it->item; - term_theme_switch_to_1(term); - } + struct sigusr_context *ctx = data; + + if (ctx->server != NULL) { + if (signo == SIGUSR1) + server_global_theme_switch_to_1(ctx->server); + else + server_global_theme_switch_to_2(ctx->server); } else { - tll_foreach(wayl->terms, it) { - struct terminal *term = it->item; - term_theme_switch_to_2(term); - } + if (signo == SIGUSR1) + term_theme_switch_to_1(ctx->term); + else + term_theme_switch_to_2(ctx->term); } return true; @@ -630,8 +635,13 @@ main(int argc, char *const *argv) goto out; } - if (!fdm_signal_add(fdm, SIGUSR1, &fdm_sigusr, wayl) || - !fdm_signal_add(fdm, SIGUSR2, &fdm_sigusr, wayl)) + struct sigusr_context sigusr_context = { + .term = term, + .server = server, + }; + + if (!fdm_signal_add(fdm, SIGUSR1, &fdm_sigusr, &sigusr_context) || + !fdm_signal_add(fdm, SIGUSR2, &fdm_sigusr, &sigusr_context)) { goto out; } diff --git a/server.c b/server.c index 22dd473b..97c1915d 100644 --- a/server.c +++ b/server.c @@ -30,7 +30,7 @@ struct client; struct terminal_instance; struct server { - const struct config *conf; + struct config *conf; struct fdm *fdm; struct reaper *reaper; struct wayland *wayl; @@ -505,7 +505,7 @@ prepare_socket(int fd) } struct server * -server_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, +server_init(struct config *conf, struct fdm *fdm, struct reaper *reaper, struct wayland *wayl) { int fd; @@ -617,3 +617,23 @@ server_destroy(struct server *server) unlink(server->sock_path); free(server); } + +void +server_global_theme_switch_to_1(struct server *server) +{ + server->conf->initial_color_theme = COLOR_THEME1; + tll_foreach(server->clients, it) + term_theme_switch_to_1(it->item->instance->terminal); + tll_foreach(server->terminals, it) + term_theme_switch_to_1(it->item->terminal); +} + +void +server_global_theme_switch_to_2(struct server *server) +{ + server->conf->initial_color_theme = COLOR_THEME2; + tll_foreach(server->clients, it) + term_theme_switch_to_2(it->item->instance->terminal); + tll_foreach(server->terminals, it) + term_theme_switch_to_2(it->item->terminal); +} diff --git a/server.h b/server.h index 50797540..6adfe7c6 100644 --- a/server.h +++ b/server.h @@ -6,6 +6,9 @@ #include "wayland.h" struct server; -struct server *server_init(const struct config *conf, struct fdm *fdm, +struct server *server_init(struct config *conf, struct fdm *fdm, struct reaper *reaper, struct wayland *wayl); void server_destroy(struct server *server); + +void server_global_theme_switch_to_1(struct server *server); +void server_global_theme_switch_to_2(struct server *server); From 3b8d59f4760070801e197aea1c7d6dc6392aafdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 30 Jul 2025 12:25:13 +0200 Subject: [PATCH 16/84] doc: foot: document SIGUSR1/SIGUSR2 --- doc/foot.1.scd | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/doc/foot.1.scd b/doc/foot.1.scd index f868c12c..2aef83ca 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -689,6 +689,21 @@ variables may be defined in *foot.ini*(5). In addition to the variables listed above, custom environment variables to unset may be defined in *foot.ini*(5). +# Signals + +The following signals have special meaning in foot: + +- SIGUSR1: switch to color theme 1 (i.e. use the *[colors]* section). +- SIGUSR2: switch to color theme 2 (i.e. use the *[colors2]* section)- + +Note: you can send SIGUSR1/SIGUSR2 to a *foot --server* process too, +in which case all client instances will switch theme. Furthermore, all +future client instances will also use the selected theme. + +Sending SIGUSR1/SIGUSR2 to a footclient instance is currently not +supported. + + # BUGS Please report bugs to https://codeberg.org/dnkl/foot/issues From b1b2162416cc0633b6432b0af48e79de6a5540d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 30 Jul 2025 12:25:21 +0200 Subject: [PATCH 17/84] doc: foot.ini: mention SIGUSR1/SIGUSR2 and reference foot(1) --- doc/foot.ini.5.scd | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 74b3f35b..7a7fb781 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -371,7 +371,8 @@ empty string to be set, but it must be quoted: *KEY=""*) Use the *color-theme-switch-1*, *color-theme-switch-2* and *color-theme-toggle* key bindings to switch between the two themes - at runtime. + at runtime, or send SIGUSR1/SIGUSR2 to the foot process (see + *foot*(1) for details). Default: _1_ @@ -1446,6 +1447,9 @@ e.g. *search-start=none*. *color-theme-toggle* toggles between the primary and alternative color themes. + Note: you can also send SIGUSR1/SIGUSR2 to the foot process to + change the theme (see *foot*(1) for details.) + Default: _none_ *quit* From 70d99a80513f574ac6d74989282b3481572513ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 30 Jul 2025 12:38:14 +0200 Subject: [PATCH 18/84] changelog: SIGUSR changes in the server --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e405459e..dfd2b9db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,9 @@ * The label letters are no longer sorted before being assigned to URLs ([#2140]2140[]). +* Sending SIGUSR1/SIGUSR2 to a `foot --server` process now causes + newly spawned client instances to use the selected theme, instead of + the original one. ### Deprecated From b13a8f12d2d360e0923ee30ff40f25b245576bb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 31 Jul 2025 17:37:19 +0200 Subject: [PATCH 19/84] server/client: add support for sending SIGUSR to footclient This patch adds the IPC infrastructure necessary to propagate SIGUSR1/SIGUSR2 from a footclient process to the server process. By targeting a particular footclient instance, only that particular instance changes theme. This is different from when targeting the server process, where all instances change theme. Closes #2156 --- CHANGELOG.md | 4 +++ client-protocol.h | 14 ++++++++++ client.c | 65 ++++++++++++++++++++++++++++++++++++++++---- doc/foot.1.scd | 6 ++-- doc/footclient.1.scd | 15 ++++++++++ server.c | 59 +++++++++++++++++++++++++++++++++++++--- 6 files changed, 151 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfd2b9db..dd5730a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,10 @@ * Sending SIGUSR1/SIGUSR2 to a `foot --server` process now causes newly spawned client instances to use the selected theme, instead of the original one. +* SIGUSR1/SIGUSR2 can now be sent to `footclient` processes, to change + the theme of that particular instance ([#2156][2156]). + +[2156]: https://codeberg.org/dnkl/foot/issues/2156 ### Deprecated diff --git a/client-protocol.h b/client-protocol.h index 505825f6..efd601d7 100644 --- a/client-protocol.h +++ b/client-protocol.h @@ -29,3 +29,17 @@ struct client_data { } __attribute__((packed)); _Static_assert(sizeof(struct client_data) == 10, "protocol struct size error"); + +enum client_ipc_code { + FOOT_IPC_SIGUSR, +}; + +struct client_ipc_hdr { + enum client_ipc_code ipc_code; + uint8_t size; +} __attribute__((packed)); + + +struct client_ipc_sigusr { + int signo; +} __attribute__((packed)); diff --git a/client.c b/client.c index e76f2d51..aa5302be 100644 --- a/client.c +++ b/client.c @@ -33,13 +33,20 @@ struct string { typedef tll(struct string) string_list_t; static volatile sig_atomic_t aborted = 0; +static volatile sig_atomic_t sigusr = 0; static void -sig_handler(int signo) +sigint_handler(int signo) { aborted = 1; } +static void +sigusr_handler(int signo) +{ + sigusr = signo; +} + static ssize_t sendall(int sock, const void *_buf, size_t len) { @@ -507,15 +514,63 @@ main(int argc, char *const *argv) if (!send_string_list(fd, &envp)) goto err; - struct sigaction sa = {.sa_handler = &sig_handler}; - sigemptyset(&sa.sa_mask); - if (sigaction(SIGINT, &sa, NULL) < 0 || sigaction(SIGTERM, &sa, NULL) < 0) { + struct sigaction sa_int = {.sa_handler = &sigint_handler}; + struct sigaction sa_usr = {.sa_handler = &sigusr_handler}; + sigemptyset(&sa_int.sa_mask); + sigemptyset(&sa_usr.sa_mask); + + if (sigaction(SIGINT, &sa_int, NULL) < 0 || + sigaction(SIGTERM, &sa_int, NULL) < 0 || + sigaction(SIGUSR1, &sa_usr, NULL) < 0 || + sigaction(SIGUSR2, &sa_usr, NULL) < 0) + { LOG_ERRNO("failed to register signal handlers"); goto err; } int exit_code; - ssize_t rcvd = recv(fd, &exit_code, sizeof(exit_code), 0); + ssize_t rcvd = -1; + + while (true) { + rcvd = recv(fd, &exit_code, sizeof(exit_code), 0); + + const int got_sigusr = sigusr; + sigusr = 0; + + if (rcvd < 0 && errno == EINTR) { + if (aborted) + break; + else if (got_sigusr != 0) { + LOG_DBG("sending sigusr %d to server", got_sigusr); + + struct { + struct client_ipc_hdr hdr; + struct client_ipc_sigusr sigusr; + } ipc = { + .hdr = { + .ipc_code = FOOT_IPC_SIGUSR, + .size = sizeof(struct client_ipc_sigusr), + }, + .sigusr = { + .signo = got_sigusr, + }, + }; + + ssize_t count = send(fd, &ipc, sizeof(ipc), 0); + if (count < 0) { + LOG_ERRNO("failed to send SIGUSR IPC to server"); + goto err; + } else if ((size_t)count != sizeof(ipc)) { + LOG_ERR("failed to send SIGUSR IPC to server"); + goto err; + } + } + + continue; + } + + break; + } if (rcvd == -1 && errno == EINTR) xassert(aborted); diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 2aef83ca..8d968a6e 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -694,14 +694,14 @@ variables to unset may be defined in *foot.ini*(5). The following signals have special meaning in foot: - SIGUSR1: switch to color theme 1 (i.e. use the *[colors]* section). -- SIGUSR2: switch to color theme 2 (i.e. use the *[colors2]* section)- +- SIGUSR2: switch to color theme 2 (i.e. use the *[colors2]* section). Note: you can send SIGUSR1/SIGUSR2 to a *foot --server* process too, in which case all client instances will switch theme. Furthermore, all future client instances will also use the selected theme. -Sending SIGUSR1/SIGUSR2 to a footclient instance is currently not -supported. +You can also send SIGUSR1/SIGUSR2 to a footclient instance, see +*footclient*(1) for details. # BUGS diff --git a/doc/footclient.1.scd b/doc/footclient.1.scd index 365689af..e4f6d350 100644 --- a/doc/footclient.1.scd +++ b/doc/footclient.1.scd @@ -189,6 +189,21 @@ variables may be defined in *foot.ini*(5). In addition to the variables listed above, custom environment variables to unset may be defined in *foot.ini*(5). +# Signals + +The following signals have special meaning in footclient: + +- SIGUSR1: switch to color theme 1 (i.e. use the *[colors]* section). +- SIGUSR2: switch to color theme 2 (i.e. use the *[colors2]* section). + +When sending SIGUSR1/SIGUSR2 to a footclient instance, the theme is +changed in that instance only. This is different from when you send +SIGUSR1/SIGUSR2 to the server process, where all instances change the +theme. + +Note: for obvious reasons, this is not supported when footclient is +started with *--no-wait*. + # SEE ALSO *foot*(1) diff --git a/server.c b/server.c index 97c1915d..3d4b5725 100644 --- a/server.c +++ b/server.c @@ -156,10 +156,61 @@ fdm_client(struct fdm *fdm, int fd, int events, void *data) xassert(events & EPOLLIN); if (client->instance != NULL) { - uint8_t dummy[128]; - ssize_t count = read(fd, dummy, sizeof(dummy)); - LOG_WARN("client unexpectedly sent %zd bytes", count); - return true; /* TODO: shutdown instead? */ + struct client_ipc_hdr ipc_hdr; + ssize_t count = read(fd, &ipc_hdr, sizeof(ipc_hdr)); + + if (count != sizeof(ipc_hdr)) { + LOG_WARN("client unexpectedly sent %zd bytes", count); + return true; /* TODO: shutdown instead? */ + } + + switch (ipc_hdr.ipc_code) { + case FOOT_IPC_SIGUSR: { + xassert(ipc_hdr.size == sizeof(struct client_ipc_sigusr)); + + struct client_ipc_sigusr sigusr; + count = read(fd, &sigusr, sizeof(sigusr)); + if (count < 0) { + LOG_ERRNO("failed to read SIGUSR IPC data from client"); + return true; /* TODO: shutdown instead? */ + } + + if ((size_t)count != sizeof(sigusr)) { + LOG_ERR("failed to read SIGUSR IPC data from client"); + return true; /* TODO: shutdown instead? */ + } + + switch (sigusr.signo) { + case SIGUSR1: + term_theme_switch_to_1(client->instance->terminal); + break; + + case SIGUSR2: + term_theme_switch_to_2(client->instance->terminal); + break; + + default: + LOG_ERR( + "client sent bad SIGUSR number: %d " + "(expected SIGUSR1=%d or SIGUSR2=%d)", + sigusr.signo, SIGUSR1, SIGUSR2); + break; + } + + return true; + } + + default: + LOG_WARN( + "client sent unrecognized IPC (0x%04x), ignoring %hhu bytes", + ipc_hdr.ipc_code, ipc_hdr.size); + + /* TODO: slightly broken, since not all data is guaranteed + to be readable yet */ + uint8_t dummy[ipc_hdr.size]; + read(fd, dummy, ipc_hdr.size); + return true; + } } if (client->buffer.data == NULL) { From 72d9a13c0c6b6ee4b56a38f508c2e8d5c56616b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 1 Aug 2025 09:41:37 +0200 Subject: [PATCH 20/84] server: fix compilation error: return value ignored --- server.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.c b/server.c index 3d4b5725..6b3e5094 100644 --- a/server.c +++ b/server.c @@ -208,7 +208,7 @@ fdm_client(struct fdm *fdm, int fd, int events, void *data) /* TODO: slightly broken, since not all data is guaranteed to be readable yet */ uint8_t dummy[ipc_hdr.size]; - read(fd, dummy, ipc_hdr.size); + (void)!!read(fd, dummy, ipc_hdr.size); return true; } } From ed7652db5056c3658afbc11c04e164e77da18650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 25 Aug 2025 14:26:44 +0200 Subject: [PATCH 21/84] config: value_to_*(): don't overwrite result variable on error Some of the value_to_*() functions wrote directly to the output variable, even when the value was invalid. This often resulted in the an actual configuration option (i.e. a member in the config struct) to be overwritten by an invalid value. For example, -o initial-color-theme=0 would set conf->initial_color_theme to -1, resulting in a crash later, when initializing a terminal instance. --- CHANGELOG.md | 7 ++++++- config.c | 20 ++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd5730a1..2a2e3347 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,7 +70,7 @@ ### Changed * The label letters are no longer sorted before being assigned to URLs - ([#2140]2140[]). + ([#2140][2140]). * Sending SIGUSR1/SIGUSR2 to a `foot --server` process now causes newly spawned client instances to use the selected theme, instead of the original one. @@ -83,6 +83,11 @@ ### Deprecated ### Removed ### Fixed + +* Invalid configuration values overriding valid ones in surprising + ways. + + ### Security ### Contributors diff --git a/config.c b/config.c index 77dc3a73..1d539bcb 100644 --- a/config.c +++ b/config.c @@ -474,8 +474,12 @@ str_to_ulong(const char *s, int base, unsigned long *res) errno = 0; char *end = NULL; - *res = strtoul(s, &end, base); - return errno == 0 && *end == '\0'; + unsigned long v = strtoul(s, &end, base); + if (!(errno == 0 && *end == '\0')) + return false; + + *res = v; + return true; } static bool NOINLINE @@ -544,12 +548,13 @@ value_to_float(struct context *ctx, float *res) errno = 0; char *end = NULL; - *res = strtof(s, &end); + float v = strtof(s, &end); if (!(errno == 0 && *end == '\0')) { LOG_CONTEXTUAL_ERR("invalid decimal value"); return false; } + *res = v; return true; } @@ -641,7 +646,6 @@ value_to_enum(struct context *ctx, const char **value_map, int *res) valid_values[idx - 2] = '\0'; LOG_CONTEXTUAL_ERR("not one of %s", valid_values); - *res = -1; return false; } @@ -690,14 +694,18 @@ value_to_two_colors(struct context *ctx, goto out; } + uint32_t a, b; + ctx->value = first_as_str; - if (!value_to_color(ctx, first, allow_alpha)) + if (!value_to_color(ctx, &a, allow_alpha)) goto out; ctx->value = second_as_str; - if (!value_to_color(ctx, second, allow_alpha)) + if (!value_to_color(ctx, &b, allow_alpha)) goto out; + *first = a; + *second = b; ret = true; out: From f0e36e35cb65bdb92dbe24d00a9cc6bc8d72a458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 30 Aug 2025 08:18:31 +0200 Subject: [PATCH 22/84] input: unit test: check pipe2() return value Fixes compilation failures with clang, in release mode. --- input.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/input.c b/input.c index fe90a7f0..fe5a8001 100644 --- a/input.c +++ b/input.c @@ -1878,7 +1878,7 @@ keyboard_modifiers(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, UNITTEST { int chan[2]; - pipe2(chan, O_CLOEXEC); + xassert(pipe2(chan, O_CLOEXEC) == 0); xassert(chan[0] >= 0); xassert(chan[1] >= 0); From 298196365c06c13dd16a74e34e03d9de93670473 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Thu, 7 Aug 2025 08:18:38 -0400 Subject: [PATCH 23/84] config: add 'uppercase-regex-insert' This makes the "uppercase hint character inserts selected text" behavior added in #1975 configurable, as it can have unexpected behavior for some users. It defaults to "on", preserving the new behavior of `foot`, after Fixes #2159. --- CHANGELOG.md | 7 +++++++ config.c | 4 ++++ config.h | 1 + doc/foot.ini.5.scd | 7 +++++++ foot.ini | 2 ++ tests/test-config.c | 1 + url-mode.c | 3 ++- 7 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a2e3347..57144549 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,13 @@ ## Unreleased ### Added + +* The `uppercase-regex-insert` option controls whether an uppercase hint + character will insert the selected text into the prompt in `regex-copy` + or `show-urls-copy` mode. It defaults to `true`. ([#2159][2159]). + +[2159]: https://codeberg.org/dnkl/foot/issues/2159 + ### Changed * The label letters are no longer sorted before being assigned to URLs diff --git a/config.c b/config.c index 1d539bcb..0de1a1be 100644 --- a/config.c +++ b/config.c @@ -1119,6 +1119,9 @@ parse_section_main(struct context *ctx) (int *)&conf->initial_color_theme); } + else if (streq(key, "uppercase-regex-insert")) + return value_to_bool(ctx, &conf->uppercase_regex_insert); + else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; @@ -3383,6 +3386,7 @@ config_load(struct config *conf, const char *conf_path, .strikeout_thickness = {.pt = 0., .px = -1}, .dpi_aware = false, .gamma_correct = false, + .uppercase_regex_insert = true, .security = { .osc52 = OSC52_ENABLED, }, diff --git a/config.h b/config.h index 315f7e24..86fb2a8f 100644 --- a/config.h +++ b/config.h @@ -247,6 +247,7 @@ struct config { bool dpi_aware; bool gamma_correct; + bool uppercase_regex_insert; struct config_font_list fonts[4]; struct font_size_adjustment font_size_adjustment; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 7a7fb781..3a057d88 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -231,6 +231,13 @@ empty string to be set, but it must be quoted: *KEY=""*) Default: _no_. +*upppercase-regex-insert* + Boolean. When enabled, inputting an uppercase hint character in + *show-urls-copy* or *regex-copy* mode will insert the selected + text into the prompt in addition to copying it to the clipboard. + + Default: _yes_ + *box-drawings-uses-font-glyphs* Boolean. When disabled, foot generates box/line drawing characters itself. The are several advantages to doing this instead of using diff --git a/foot.ini b/foot.ini index 73fdb7ab..44ed5785 100644 --- a/foot.ini +++ b/foot.ini @@ -40,6 +40,8 @@ # utmp-helper=/usr/lib/utempter/utempter # When utmp backend is ‘libutempter’ (Linux) # utmp-helper=/usr/libexec/ulog-helper # When utmp backend is ‘ulog’ (FreeBSD) +# uppercase-regex-insert=yes + [environment] # name=value diff --git a/tests/test-config.c b/tests/test-config.c index bab57788..8c0805f4 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -491,6 +491,7 @@ test_section_main(void) test_boolean(&ctx, &parse_section_main, "locked-title", &conf.locked_title); test_boolean(&ctx, &parse_section_main, "dpi-aware", &conf.dpi_aware); test_boolean(&ctx, &parse_section_main, "gamma-correct-blending", &conf.gamma_correct); + test_boolean(&ctx, &parse_section_main, "uppercase-regex-insert", &conf.uppercase_regex_insert); test_pt_or_px(&ctx, &parse_section_main, "font-size-adjustment", &conf.font_size_adjustment.pt_or_px); /* TODO: test ‘N%’ values too */ test_pt_or_px(&ctx, &parse_section_main, "line-height", &conf.line_height); diff --git a/url-mode.c b/url-mode.c index 19d95356..44809f5f 100644 --- a/url-mode.c +++ b/url-mode.c @@ -283,7 +283,8 @@ urls_input(struct seat *seat, struct terminal *term, if (match) { // If the last hint character was uppercase, copy and paste - activate_url(seat, term, match, serial, wc == toc32upper(wc)); + bool insert = term->conf->uppercase_regex_insert && wc == toc32upper(wc); + activate_url(seat, term, match, serial, insert); switch (match->action) { case URL_ACTION_COPY: From 1d9ac3f611f1b81983b095a2f96e36dba4d24da9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 31 Aug 2025 11:42:56 +0200 Subject: [PATCH 24/84] doc: foot.ini: typo: upppercase -> uppercase --- doc/foot.ini.5.scd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 3a057d88..7550fd13 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -231,7 +231,7 @@ empty string to be set, but it must be quoted: *KEY=""*) Default: _no_. -*upppercase-regex-insert* +*uppercase-regex-insert* Boolean. When enabled, inputting an uppercase hint character in *show-urls-copy* or *regex-copy* mode will insert the selected text into the prompt in addition to copying it to the clipboard. From 65528f455d0d7753da73365fb39b39473a87d8b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 9 Sep 2025 17:34:02 +0200 Subject: [PATCH 25/84] meson: utempter del has no argument This fixes an issue where we didn't record a logout record when using the libutempter backend. --- CHANGELOG.md | 3 ++- meson.build | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57144549..4536f6ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,7 +93,8 @@ * Invalid configuration values overriding valid ones in surprising ways. - +* Bug where the libutempter utmp backend did not record logouts + correctly. ### Security ### Contributors diff --git a/meson.build b/meson.build index 0b7bbc17..56f4a31c 100644 --- a/meson.build +++ b/meson.build @@ -53,7 +53,7 @@ if utmp_backend == 'none' elif utmp_backend == 'libutempter' utmp_add = 'add' utmp_del = 'del' - utmp_del_have_argument = true + utmp_del_have_argument = false if utmp_default_helper_path == 'auto' utmp_default_helper_path = join_paths('/usr', get_option('libdir'), 'utempter', 'utempter') endif From efc39097e5a939c4e6f54bf80d24871bfeeaf80b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 9 Sep 2025 17:34:54 +0200 Subject: [PATCH 26/84] term: no need to pass ptmx as stdout to utempter --- terminal.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/terminal.c b/terminal.c index 2e23f749..421a3e65 100644 --- a/terminal.c +++ b/terminal.c @@ -199,7 +199,7 @@ add_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx) return true; char *const argv[] = {conf->utmp_helper_path, UTMP_ADD, getenv("WAYLAND_DISPLAY"), NULL}; - return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL, NULL, NULL) >= 0; + return spawn(reaper, NULL, argv, ptmx, -1, -1, NULL, NULL, NULL) >= 0; #else return true; #endif @@ -223,7 +223,7 @@ del_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx) ; char *const argv[] = {conf->utmp_helper_path, UTMP_DEL, del_argument, NULL}; - return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL, NULL, NULL) >= 0; + return spawn(reaper, NULL, argv, ptmx, -1, -1, NULL, NULL, NULL) >= 0; #else return true; #endif From f715f3b55fb094e6ab1fdc18dcf8956227c495c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 12 Sep 2025 10:18:06 +0200 Subject: [PATCH 27/84] changelog: prepare for 1.24.0 --- CHANGELOG.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4536f6ea..7fa3f0ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.24.0](#1-24-0) * [1.23.1](#1-23-1) * [1.23.0](#1-23-0) * [1.22.3](#1-22-3) @@ -65,7 +65,8 @@ * [1.2.0](#1-2-0) -## Unreleased +## 1.24.0 + ### Added * The `uppercase-regex-insert` option controls whether an uppercase hint @@ -87,8 +88,6 @@ [2156]: https://codeberg.org/dnkl/foot/issues/2156 -### Deprecated -### Removed ### Fixed * Invalid configuration values overriding valid ones in surprising @@ -96,9 +95,11 @@ * Bug where the libutempter utmp backend did not record logouts correctly. -### Security ### Contributors +* Ryan Roden-Corrent +* Tobias Mock + ## 1.23.1 From fa0fd2f50f41d8fc47241dd576be42a2f29d6530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 12 Sep 2025 10:18:33 +0200 Subject: [PATCH 28/84] meson: bump version to 1.24.0 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 56f4a31c..305df13c 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.23.1', + version: '1.24.0', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From c34f0633075769a2564e10d359feeca604373ae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 12 Sep 2025 10:22:21 +0200 Subject: [PATCH 29/84] changelog: add new 'unreleased' section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fa3f0ea..3144a125 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.24.0](#1-24-0) * [1.23.1](#1-23-1) * [1.23.0](#1-23-0) @@ -65,6 +66,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.24.0 ### Added From 44a674edb86f2f8db3304d76faf567346dcd7d94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 25 Sep 2025 16:57:41 +0200 Subject: [PATCH 30/84] term: erase: use erase_line() whenever a range corresponds to a full line --- terminal.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/terminal.c b/terminal.c index 421a3e65..e27a8fd9 100644 --- a/terminal.c +++ b/terminal.c @@ -2648,7 +2648,10 @@ term_erase(struct terminal *term, int start_row, int start_col, if (start_row == end_row) { struct row *row = grid_row(term->grid, start_row); - erase_cell_range(term, row, start_col, end_col); + if (unlikely(start_col == 0 && end_col == term->cols - 1)) + erase_line(term, row); + else + erase_cell_range(term, row, start_col, end_col); sixel_overwrite_by_row(term, start_row, start_col, end_col - start_col + 1); return; } @@ -2664,7 +2667,10 @@ term_erase(struct terminal *term, int start_row, int start_col, 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); + if (unlikely(end_col == term->cols - 1)) + erase_line(term, grid_row(term->grid, end_row)); + else + erase_cell_range(term, grid_row(term->grid, end_row), 0, end_col); sixel_overwrite_by_row(term, end_row, 0, end_col + 1); } From 1dfa86c93ac3585b1b8b6ff3d8617176a57942c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 4 Oct 2025 07:21:15 +0200 Subject: [PATCH 31/84] Revert "term: erase: use erase_line() whenever a range corresponds to a full line" This reverts commit 44a674edb86f2f8db3304d76faf567346dcd7d94. It caused a regression with prompt markers, in at least fish+starship. --- terminal.c | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/terminal.c b/terminal.c index e27a8fd9..421a3e65 100644 --- a/terminal.c +++ b/terminal.c @@ -2648,10 +2648,7 @@ term_erase(struct terminal *term, int start_row, int start_col, if (start_row == end_row) { struct row *row = grid_row(term->grid, start_row); - if (unlikely(start_col == 0 && end_col == term->cols - 1)) - erase_line(term, row); - else - erase_cell_range(term, row, start_col, end_col); + erase_cell_range(term, row, start_col, end_col); sixel_overwrite_by_row(term, start_row, start_col, end_col - start_col + 1); return; } @@ -2667,10 +2664,7 @@ term_erase(struct terminal *term, int start_row, int start_col, sixel_overwrite_by_rectangle( term, start_row + 1, 0, end_row - start_row, term->cols); - if (unlikely(end_col == term->cols - 1)) - erase_line(term, grid_row(term->grid, end_row)); - else - erase_cell_range(term, grid_row(term->grid, end_row), 0, end_col); + erase_cell_range(term, grid_row(term->grid, end_row), 0, end_col); sixel_overwrite_by_row(term, end_row, 0, end_col + 1); } From 80951ab7a6b3bcc4dd3c30a5454bf648d3d9ed93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 4 Oct 2025 09:24:47 +0200 Subject: [PATCH 32/84] term: osc8: tag *all* cells in a multi-column character as an URI When we print a character to the grid, we must also update its OSC-8 state if an OSC-8 URI is currently active. For double-width characters, this was only being done for the first cell. This causes the labels in URL mode to be off, as the link was effectively chopped up into multiple pieces. Closes #2179 --- CHANGELOG.md | 7 +++++++ terminal.c | 8 +++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3144a125..2b0a7247 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,13 @@ ### Deprecated ### Removed ### Fixed + +* URL labels misplaces when URL contains double-width characters + ([#2179][2179]). + +[2179]: https://codeberg.org/dnkl/foot/issues/2179 + + ### Security ### Contributors diff --git a/terminal.c b/terminal.c index 421a3e65..60506d07 100644 --- a/terminal.c +++ b/terminal.c @@ -4005,9 +4005,11 @@ term_print(struct terminal *term, char32_t wc, int width, bool insert_mode_disab cell->wc = term->vt.last_printed = wc; cell->attrs = term->vt.attrs; - if (term->vt.osc8.uri != NULL) { - grid_row_uri_range_put( - row, col, term->vt.osc8.uri, term->vt.osc8.id); + 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: From fac399415452edba6f20986e5f8fa479863582fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 4 Oct 2025 09:29:56 +0200 Subject: [PATCH 33/84] config: add tweak.min-stride-alignment This allows the user to configure the value by which a surface buffer's stride must be an even multiple of. This can be used to ensure the stride meets the GPU driver's requirements for direct import. Defaults to 256. Set to 0 to disable. Closes #2182 --- config.c | 5 +++++ config.h | 1 + main.c | 1 + shm.c | 17 ++++++++++++++++- shm.h | 3 +++ tests/test-config.c | 3 +++ 6 files changed, 29 insertions(+), 1 deletion(-) diff --git a/config.c b/config.c index 0de1a1be..459a1de9 100644 --- a/config.c +++ b/config.c @@ -2848,6 +2848,10 @@ parse_section_tweak(struct context *ctx) #endif } + else if (streq(key, "min-stride-alignment")) { + return value_to_uint32(ctx, 10, &conf->tweak.min_stride_alignment); + } + else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; @@ -3496,6 +3500,7 @@ config_load(struct config *conf, const char *conf_path, .font_monospace_warn = true, .sixel = true, .surface_bit_depth = SHM_BITS_AUTO, + .min_stride_alignment = 256, }, .touch = { diff --git a/config.h b/config.h index 86fb2a8f..11439d3a 100644 --- a/config.h +++ b/config.h @@ -435,6 +435,7 @@ struct config { bool font_monospace_warn; bool sixel; enum shm_bit_depth surface_bit_depth; + uint32_t min_stride_alignment; } tweak; struct { diff --git a/main.c b/main.c index 1afbd16d..b6a0d825 100644 --- a/main.c +++ b/main.c @@ -597,6 +597,7 @@ main(int argc, char *const *argv) } shm_set_max_pool_size(conf.tweak.max_shm_pool_size); + shm_set_min_stride_alignment(conf.tweak.min_stride_alignment); if ((fdm = fdm_init()) == NULL) goto out; diff --git a/shm.c b/shm.c index 4680c12c..7b13db9b 100644 --- a/shm.c +++ b/shm.c @@ -13,7 +13,6 @@ #include -#include #include #define LOG_MODULE "shm" @@ -21,6 +20,7 @@ #include "log.h" #include "debug.h" #include "macros.h" +#include "stride.h" #include "xmalloc.h" #if !defined(MAP_UNINITIALIZED) @@ -61,6 +61,8 @@ static off_t max_pool_size = 512 * 1024 * 1024; static bool can_punch_hole = false; static bool can_punch_hole_initialized = false; +static size_t min_stride_alignment = 0; + struct buffer_pool { int fd; /* memfd */ struct wl_shm_pool *wl_pool; @@ -113,6 +115,12 @@ shm_set_max_pool_size(off_t _max_pool_size) max_pool_size = _max_pool_size; } +void +shm_set_min_stride_alignment(size_t _min_stride_alignment) +{ + min_stride_alignment = _min_stride_alignment; +} + static void buffer_destroy_dont_close(struct buffer *buf) { @@ -342,6 +350,13 @@ get_new_buffers(struct buffer_chain *chain, size_t count, ? chain->pixman_fmt_with_alpha : chain->pixman_fmt_without_alpha, widths[i]); + + if (min_stride_alignment > 0) { + const size_t m = min_stride_alignment; + stride[i] = (stride[i] + m - 1) / m * m; + } + + xassert(min_stride_alignment == 0 || stride[i] % min_stride_alignment == 0); sizes[i] = stride[i] * heights[i]; total_size += sizes[i]; } diff --git a/shm.h b/shm.h index 8f8c406a..6050f1c7 100644 --- a/shm.h +++ b/shm.h @@ -42,7 +42,10 @@ struct buffer { }; void shm_fini(void); + +/* TODO: combine into shm_init() */ void shm_set_max_pool_size(off_t max_pool_size); +void shm_set_min_stride_alignment(size_t min_stride_alignment); struct buffer_chain; struct buffer_chain *shm_chain_new( diff --git a/tests/test-config.c b/tests/test-config.c index 8c0805f4..64b61540 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -1413,6 +1413,9 @@ test_section_tweak(void) test_float(&ctx, &parse_section_tweak, "bold-text-in-bright-amount", &conf.bold_in_bright.amount); + test_uint32(&ctx, &parse_section_tweak, "min-stride-alignment", + &conf.tweak.min_stride_alignment); + #if 0 /* Must be equal to, or less than INT32_MAX */ test_uint32(&ctx, &parse_section_tweak, "max-shm-pool-size-mb", &conf.tweak.max_shm_pool_size); From bd994eda1c816d5430a9c4cdb7089e43c47d74ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 4 Oct 2025 10:50:38 +0200 Subject: [PATCH 34/84] shm: page-align the memfd size (also needed for GPU direct import) --- shm.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/shm.c b/shm.c index 7b13db9b..31ea67ed 100644 --- a/shm.c +++ b/shm.c @@ -239,7 +239,6 @@ static const struct wl_buffer_listener buffer_listener = { .release = &buffer_release, }; -#if __SIZEOF_POINTER__ == 8 static size_t page_size(void) { @@ -256,7 +255,6 @@ page_size(void) xassert(size > 0); return size; } -#endif static bool instantiate_offset(struct buffer_private *buf, off_t new_offset) @@ -398,9 +396,11 @@ get_new_buffers(struct buffer_chain *chain, size_t count, goto err; } + const size_t page_sz = page_size(); + #if __SIZEOF_POINTER__ == 8 off_t offset = chain->scrollable && max_pool_size > 0 - ? (max_pool_size / 4) & ~(page_size() - 1) + ? (max_pool_size / 4) & ~(page_sz - 1) : 0; off_t memfd_size = chain->scrollable && max_pool_size > 0 ? max_pool_size @@ -410,7 +410,8 @@ get_new_buffers(struct buffer_chain *chain, size_t count, off_t memfd_size = total_size; #endif - xassert(chain->scrollable || (offset == 0 && memfd_size == total_size)); + /* Page align */ + memfd_size = (memfd_size + page_sz - 1) & ~(page_sz - 1); LOG_DBG("memfd-size: %lu, initial offset: %lu", memfd_size, offset); @@ -442,6 +443,9 @@ get_new_buffers(struct buffer_chain *chain, size_t count, memfd_size = total_size; chain->scrollable = false; + /* Page align */ + memfd_size = (memfd_size + page_sz - 1) & ~(page_sz - 1); + if (ftruncate(pool_fd, memfd_size) < 0) { LOG_ERRNO("failed to set size of SHM backing memory file"); goto err; From e43ea3676fe359864a8a22a31beed5bf289f367c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 4 Oct 2025 15:38:35 +0200 Subject: [PATCH 35/84] doc: foot.ini: document tweak.min-stride-alignment --- doc/foot.ini.5.scd | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 7550fd13..56b76be7 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -2031,6 +2031,32 @@ any of these options. Default: _512_. Maximum allowed: _2048_ (2GB). +*min-stride-alignment* + This option controls the minimum stride alignment, in bytes, when + allocating SHM buffers. + + In some circumstances, a compositor can import foot's SHM buffers + directly to the GPU, without copying the buffer to GPU memory + (typically on integrated graphics). Different drivers have + different requirements for this, and one of those requirements is + typically the stride alignment. At the time of writing, AMD GPUs + require 256-byte alignment. + + Note that doing a direct import typically disables immediate + buffer release (if the compositor supports that), which means foot + has to double buffer. This adds a performance penalty in foot, but + the overall system performance should still be better. + + If you are not using integrated graphics, or if the compositor + does not support GPU direct imports, this option has close to zero + impact. You can save a small amount of memory by setting this to + 0. + + Ultimately, it is up to the compositor to decide whether to do + immediate buffer releases, or try to optimize GPU imports. + + Default: _256_ + *sixel* Boolean. When enabled, foot will process sixel images. Default: _yes_ From bb314425ef9c2069e3b17122b7f81ecc7d527a81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 4 Oct 2025 15:40:20 +0200 Subject: [PATCH 36/84] changelog: shm buffer stride alignment --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b0a7247..a2aaf488 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,16 @@ ## Unreleased ### Added ### Changed + +* SHM buffer sizes are now rounded up to nearest page size, and their + stride is always an even multiple of 256 bytes (by default, + configurable by setting `tweak.min-stride-alignment`). This allows + compositor to directly import foot's SHM buffers to the GPU, with + e.g. integrated graphics ([#2182][2182]). + +[2182]: https://codeberg.org/dnkl/foot/issues/2182 + + ### Deprecated ### Removed ### Fixed From 299186a6547f6e038ee6f3822caf9a0fdfabceef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 5 Oct 2025 10:48:36 +0200 Subject: [PATCH 37/84] render: when double-buffering, pre-apply previous frame's damage early Foot likes it when compositor releases buffer immediately, as that means we only have to re-render the cells that have changed since the last frame. For various reasons, not all compositors do this. In this case, foot is typically forced to switch between two buffers, i.e. double-buffer. In this case, each frame starts with copying over the damage from the previous frame, to the new frame. Then we start rendering the updated cells. Bringing over the previous frame's damage can be slow, if the changed area was large (e.g. when scrolling one or a few lines, or on full screen updates). It's also done single-threaded. Thus it not only slows down frame rendering, but pauses everything else (i.e. input processing). All in all, it reduces performance and increases input latency. But we don't have to wait until it's time to render a frame to copy over the previous frame's damage. We can do that as soon as the compositor has released the buffer (for the frame _before_ the previous frame). And we can do this in a thread. This frees up foot to continue processing input, and reduces frame rendering time since we can now start rendering the modified cells immediately, without first doing a large memcpy(3). In worst case scenarios (or perhaps we should consider them best case scenarios...), I've seen up to a 10x performance increase in frame rendering times (this obviously does *not* include the time it takes to copy over the previous frame's damage, since that doesn't affect neither input processing nor frame rendering). Implemented by adding a callback mechanism to the shm abstraction layer. Use it for the grid buffers, and kick off a thread that copies the previous frame's damage, and resets the buffers age to 0 (so that foot understands it can start render to it immediately when it later needs to render a frame). Since we have certain way of knowing if a compositor releases buffers immediately or not, use a bit of heuristics; if we see 10 consecutive non-immediate releases (that is, we reset the counter as soon as we do see an immediate release), this new "pre-apply damage" logic is enabled. It can be force-disabled with tweak.pre-apply-damage=no. We also need to take care to wait for the thread before resetting the render's "last_buf" pointer (or we'll SEGFAULT in the thread...). We must also ensure we wait for the thread to finish before we start rendering a new frame. Under normal circumstances, the wait time is always 0, the thread has almost always finished long before we need to render the next frame. But it _can_ happen. Closes #2188 --- CHANGELOG.md | 7 ++ config.c | 7 +- config.h | 1 + doc/foot.ini.5.scd | 35 ++++++++ pgo/pgo.c | 5 +- render.c | 200 ++++++++++++++++++++++++++++++++++++++++++--- render.h | 2 + shm.c | 24 +++++- shm.h | 3 +- terminal.c | 19 +++-- terminal.h | 10 +++ 11 files changed, 287 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2aaf488..65f0bbab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,13 @@ ## Unreleased ### Added + +* Performance increased and input latency decreased on compositors + that do not release SHM buffers immediately ([#2188][2188]). + +[2188]: https://codeberg.org/dnkl/foot/issues/2188 + + ### Changed * SHM buffer sizes are now rounded up to nearest page size, and their diff --git a/config.c b/config.c index 459a1de9..06817247 100644 --- a/config.c +++ b/config.c @@ -2848,9 +2848,11 @@ parse_section_tweak(struct context *ctx) #endif } - else if (streq(key, "min-stride-alignment")) { + 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); @@ -3501,6 +3503,7 @@ config_load(struct config *conf, const char *conf_path, .sixel = true, .surface_bit_depth = SHM_BITS_AUTO, .min_stride_alignment = 256, + .preapply_damage = true, }, .touch = { diff --git a/config.h b/config.h index 11439d3a..5b7ff11e 100644 --- a/config.h +++ b/config.h @@ -436,6 +436,7 @@ struct config { bool sixel; enum shm_bit_depth surface_bit_depth; uint32_t min_stride_alignment; + bool preapply_damage; } tweak; struct { diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 56b76be7..7b08d5d4 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -2093,6 +2093,41 @@ any of these options. Default: _auto_ +*pre-apply-damage* + Boolean. When enabled, foot will attempt to "pre-apply" the damage + from the last frame when foot is forced to double-buffer + (i.e. when the compositor does not release SHM buffers + immediately). All text after this assumes the compositor is not + releasing buffers immediately. + + When this option is disabled, each time foot needs to render a + frame, it has to first copy over areas that changed in the last + frame (i.e. all changes between the last two frames). This is + basically a *memcpy*(3), which can be slow if the changed area is + large. It is also done on the main thread, which means foot cannot + do anything else at the same time; no other rendering, no VT + parsing. After the changes have been brought over to the new + frame, foot proceeds with rendering the cells that has changed + between the last frame and the new frame. + + When this open is enabled, the changes between the last two frames + are brought over to what will become the next frame before foot + starts rendering the next frame. As soon as the compositor + releases the previous buffer (typically right after foot has + pushed a new frame), foot kicks off a thread that copies over the + changes to the newly released buffer. Since this is done in a + thread, foot can continue processing input at the same + time. Later, when it is time to render a new frame, the changes + have already been transferred, and foot can immediately start with + the actual rendering. + + Thus, having this option enabled improves both performance + (copying the last two frames' changes is threaded), and improves + input latency (rending the next frame no longer has to first bring + over the changes between the last two frames). + + Default: _yes_ + # SEE ALSO *foot*(1), *footclient*(1) diff --git a/pgo/pgo.c b/pgo/pgo.c index 757dcd06..4ff4111c 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -74,6 +74,8 @@ void render_refresh_icon(struct terminal *term) {} void render_overlay(struct terminal *term) {} +void render_buffer_release_callback(struct buffer *buf, void *data) {} + bool render_xcursor_is_valid(const struct seat *seat, const char *cursor) { @@ -206,7 +208,8 @@ enum shm_bit_depth shm_chain_bit_depth(const struct buffer_chain *chain) { retur struct buffer_chain * shm_chain_new( struct wayland *wayl, bool scrollable, size_t pix_instances, - enum shm_bit_depth desired_bit_depth) + enum shm_bit_depth desired_bit_depth, + void (*release_cb)(struct buffer *buf, void *data), void *cb_data) { return NULL; } diff --git a/render.c b/render.c index 1c24bafa..35752125 100644 --- a/render.c +++ b/render.c @@ -2224,6 +2224,56 @@ render_worker_thread(void *_ctx) 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; + } } } }; @@ -2231,6 +2281,22 @@ render_worker_thread(void *_ctx) return -1; } +static void +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) { @@ -3113,14 +3179,6 @@ force_full_repaint(struct terminal *term, struct buffer *buf) static void reapply_old_damage(struct terminal *term, struct buffer *new, struct buffer *old) { - static int counter = 0; - static bool have_warned = false; - if (!have_warned && ++counter > 5) { - LOG_WARN("compositor is not releasing buffers immediately; " - "expect lower rendering performance"); - have_warned = true; - } - if (new->age > 1) { memcpy(new->data, old->data, new->height * new->stride); return; @@ -3251,7 +3309,18 @@ grid_render(struct terminal *term) if (term->shutdown.in_progress) return; - struct timespec start_time, start_double_buffering = {0}, stop_double_buffering = {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}; + + /* 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); + wait_for_preapply_damage(term); + clock_gettime(CLOCK_MONOTONIC, &stop_wait_preapply); + } if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) clock_gettime(CLOCK_MONOTONIC, &start_time); @@ -3269,6 +3338,8 @@ grid_render(struct terminal *term) 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 || @@ -3285,9 +3356,27 @@ grid_render(struct terminal *term) 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) { + LOG_WARN("compositor is not releasing buffers immediately; " + "expect lower rendering performance"); + have_warned = true; + } + } + 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; } if (term->render.last_buf != NULL) { @@ -3515,27 +3604,40 @@ grid_render(struct terminal *term) 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 rendering, %lds %9ldns double buffering)", + "(%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); + double_buffering_time.tv_nsec, + (long)preapply_damage.tv_sec, + preapply_damage.tv_nsec); break; case RENDER_TIMER_OSD: @@ -4295,6 +4397,7 @@ delayed_reflow_of_normal_grid(struct terminal *term) term->interactive_resizing.old_hide_cursor = false; /* Invalidate render pointers */ + wait_for_preapply_damage(term); shm_unref(term->render.last_buf); term->render.last_buf = NULL; term->render.last_cursor.row = NULL; @@ -4869,6 +4972,7 @@ damage_view: tll_free(term->normal.scroll_damage); tll_free(term->alt.scroll_damage); + wait_for_preapply_damage(term); shm_unref(term->render.last_buf); term->render.last_buf = NULL; term_damage_view(term); @@ -5267,3 +5371,77 @@ render_xcursor_set(struct seat *seat, struct terminal *term, 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/render.h b/render.h index 81d2a905..e21eaca8 100644 --- a/render.h +++ b/render.h @@ -47,3 +47,5 @@ struct csd_data { }; struct csd_data get_csd_data(const struct terminal *term, enum csd_surface surf_idx); + +void render_buffer_release_callback(struct buffer *buf, void *data); diff --git a/shm.c b/shm.c index 31ea67ed..72b32f16 100644 --- a/shm.c +++ b/shm.c @@ -87,6 +87,9 @@ struct buffer_private { bool with_alpha; bool scrollable; + + void (*release_cb)(struct buffer *buf, void *data); + void *cb_data; }; struct buffer_chain { @@ -100,6 +103,9 @@ struct buffer_chain { pixman_format_code_t pixman_fmt_with_alpha; enum wl_shm_format shm_format_with_alpha; + + void (*release_cb)(struct buffer *buf, void *data); + void *cb_data; }; static tll(struct buffer_private *) deferred; @@ -232,6 +238,10 @@ buffer_release(void *data, struct wl_buffer *wl_buffer) xassert(found); if (!found) LOG_WARN("deferred delete: buffer not on the 'deferred' list"); + } else { + if (buffer->release_cb != NULL) { + buffer->release_cb(&buffer->public, buffer->cb_data); + } } } @@ -516,6 +526,8 @@ get_new_buffers(struct buffer_chain *chain, size_t count, .offset = 0, .size = sizes[i], .scrollable = chain->scrollable, + .release_cb = chain->release_cb, + .cb_data = chain->cb_data, }; if (!instantiate_offset(buf, offset)) { @@ -623,7 +635,7 @@ shm_get_buffer(struct buffer_chain *chain, int width, int height, bool with_alph * reuse. Pick the "youngest" one, and mark the * other one for purging */ if (buf->public.age < cached->public.age) { - shm_unref(&cached->public); + //shm_unref(&cached->public); cached = buf; } else { /* @@ -634,8 +646,8 @@ shm_get_buffer(struct buffer_chain *chain, int width, int height, bool with_alph * should be safe; "our" tll_foreach() already * holds the next pointer. */ - if (buffer_unref_no_remove_from_chain(buf)) - tll_remove(chain->bufs, it); + //if (buffer_unref_no_remove_from_chain(buf)) + // tll_remove(chain->bufs, it); } } } @@ -994,7 +1006,8 @@ shm_unref(struct buffer *_buf) struct buffer_chain * shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, - enum shm_bit_depth desired_bit_depth) + enum shm_bit_depth desired_bit_depth, + void (*release_cb)(struct buffer *buf, void *data), void *cb_data) { pixman_format_code_t pixman_fmt_without_alpha = PIXMAN_x8r8g8b8; enum wl_shm_format shm_fmt_without_alpha = WL_SHM_FORMAT_XRGB8888; @@ -1090,6 +1103,9 @@ shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, .pixman_fmt_with_alpha = pixman_fmt_with_alpha, .shm_format_with_alpha = shm_fmt_with_alpha, + + .release_cb = release_cb, + .cb_data = cb_data, }; return chain; } diff --git a/shm.h b/shm.h index 6050f1c7..84eb4386 100644 --- a/shm.h +++ b/shm.h @@ -50,7 +50,8 @@ void shm_set_min_stride_alignment(size_t min_stride_alignment); struct buffer_chain; struct buffer_chain *shm_chain_new( struct wayland *wayl, bool scrollable, size_t pix_instances, - enum shm_bit_depth desired_bit_depth); + enum shm_bit_depth desired_bit_depth, + void (*release_cb)(struct buffer *buf, void *data), void *cb_data); void shm_chain_free(struct buffer_chain *chain); enum shm_bit_depth shm_chain_bit_depth(const struct buffer_chain *chain); diff --git a/terminal.c b/terminal.c index 60506d07..36f8513b 100644 --- a/terminal.c +++ b/terminal.c @@ -719,6 +719,9 @@ initialize_render_workers(struct terminal *term) 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])); @@ -1356,13 +1359,13 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .render = { .chains = { .grid = shm_chain_new(wayl, true, 1 + conf->render_worker_count, - desired_bit_depth), - .search = shm_chain_new(wayl, false, 1 ,desired_bit_depth), - .scrollback_indicator = shm_chain_new(wayl, false, 1, desired_bit_depth), - .render_timer = shm_chain_new(wayl, false, 1, desired_bit_depth), - .url = shm_chain_new(wayl, false, 1, desired_bit_depth), - .csd = shm_chain_new(wayl, false, 1, desired_bit_depth), - .overlay = shm_chain_new(wayl, false, 1, desired_bit_depth), + 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), }, .scrollback_lines = conf->scrollback.lines, .app_sync_updates.timer_fd = app_sync_updates_fd, @@ -1893,6 +1896,8 @@ term_destroy(struct terminal *term) } } 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); diff --git a/terminal.h b/terminal.h index 88371b07..364d57b3 100644 --- a/terminal.h +++ b/terminal.h @@ -706,6 +706,14 @@ struct terminal { tll(int) queue; thrd_t *threads; struct buffer *buf; + + struct { + mtx_t lock; + cnd_t cond; + struct buffer *buf; + struct timespec start; + struct timespec stop; + } preapplied_damage; } workers; /* Last rendered cursor position */ @@ -716,6 +724,8 @@ struct terminal { } last_cursor; struct buffer *last_buf; /* Buffer we rendered to last time */ + size_t frames_since_last_immediate_release; + bool preapply_last_frame_damage; enum overlay_style last_overlay_style; struct buffer *last_overlay_buf; From fd88c6c61c52b75789c7f9d723815b67aed2984f Mon Sep 17 00:00:00 2001 From: Charalampos Mitrodimas Date: Thu, 2 Oct 2025 00:29:34 +0300 Subject: [PATCH 38/84] wayland: restore opacity after exiting fullscreen When exiting fullscreen mode, the window's transparency was not being restored, leaving it opaque until another window was fullscreened. This occurred because the Wayland opaque region was set based only on the configured alpha value, without considering the fullscreen state. Since commit 899b768b74 ("render: disable transparency when we're fullscreened") transparency is disabled during fullscreen to avoid compositor-mandated black backgrounds affecting the intended colors. However, the opaque region was not being updated when the fullscreen state changed. Fixes: https://codeberg.org/dnkl/foot/issues/2180 Signed-off-by: Charalampos Mitrodimas --- wayland.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/wayland.c b/wayland.c index 5f68ecf7..59b2a33e 100644 --- a/wayland.c +++ b/wayland.c @@ -1064,6 +1064,7 @@ xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, bool wasnt_configured = !win->is_configured; bool was_resizing = win->is_resizing; + bool was_fullscreen = win->is_fullscreen; bool csd_was_enabled = win->csd_mode == CSD_YES && !win->is_fullscreen; int new_width = win->configure.width; int new_height = win->configure.height; @@ -1096,6 +1097,10 @@ xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, else if (csd_was_enabled && !enable_csd) csd_destroy(win); + /* Update opaque region if fullscreen state changed */ + if (was_fullscreen != win->is_fullscreen) + wayl_win_alpha_changed(win); + if (enable_csd && new_width > 0 && new_height > 0) { if (wayl_win_csd_titlebar_visible(win)) new_height -= win->term->conf->csd.title_height; @@ -2401,7 +2406,13 @@ wayl_win_alpha_changed(struct wl_window *win) { struct terminal *term = win->term; - if (term->colors.alpha == 0xffff) { + /* + * When fullscreened, transparency is disabled (see render.c). + * Update the opaque region to match. + */ + bool is_opaque = term->colors.alpha == 0xffff || win->is_fullscreen; + + if (is_opaque) { struct wl_region *region = wl_compositor_create_region( term->wl->compositor); From e308a4733e4149b661457c9a275e48d9dbeaf23f Mon Sep 17 00:00:00 2001 From: Matthias Heyman Date: Fri, 26 Sep 2025 10:59:48 +0200 Subject: [PATCH 39/84] fix: jump labels are more readable --- themes/modus-operandi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/themes/modus-operandi b/themes/modus-operandi index 5e3a9fd6..2d417bb5 100644 --- a/themes/modus-operandi +++ b/themes/modus-operandi @@ -22,3 +22,5 @@ bright4=2544bb bright5=5317ac bright6=005a5f bright7=ffffff + +jump-labels=dce0e8 0000ff From 371837ef7b23b3e9f574069671d06a96e73526aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 10 Oct 2025 10:36:41 +0200 Subject: [PATCH 40/84] changelog: updated jump label colors in modus-operandi --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65f0bbab..314f7b38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,8 @@ configurable by setting `tweak.min-stride-alignment`). This allows compositor to directly import foot's SHM buffers to the GPU, with e.g. integrated graphics ([#2182][2182]). +* Jump label colors in the modus-operandi theme, for improved + readability. [2182]: https://codeberg.org/dnkl/foot/issues/2182 From 7ed36c10334c0b2480fa026238f8f1791d2f9439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 10 Oct 2025 11:10:38 +0200 Subject: [PATCH 41/84] config: add colors.dim-blend-towards=black|white Before this patch, we always blended towards black when dimming text. However, with light color themes, it usually looks better if we dim towards white instead. This option allows you to choose which color to blend towards. The default is 'black' in '[colors]', and 'white' in '[colors2]'. Closes #2187 --- CHANGELOG.md | 4 ++++ config.c | 15 ++++++++++++++- config.h | 5 +++++ doc/foot.ini.5.scd | 14 ++++++++++++-- foot.ini | 3 +++ render.c | 9 ++++++++- tests/test-config.c | 5 +++++ 7 files changed, 51 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 314f7b38..76183459 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,8 +71,12 @@ * Performance increased and input latency decreased on compositors that do not release SHM buffers immediately ([#2188][2188]). +* `colors{,2}.dim-blend-towards=black|white` option, allowing you to + select towards which color to blend when dimming text. Defaults to + `black` in `[colors]`, and `white` in `[colors2]` ([#2187][2187]). [2188]: https://codeberg.org/dnkl/foot/issues/2188 +[2187]: https://codeberg.org/dnkl/foot/issues/2187 ### Changed diff --git a/config.c b/config.c index 06817247..4449d9c2 100644 --- a/config.c +++ b/config.c @@ -1519,7 +1519,7 @@ parse_color_theme(struct context *ctx, struct color_theme *theme) return true; } - else if (strcmp(key, "alpha-mode") == 0) { + else if (streq(key, "alpha-mode")) { _Static_assert(sizeof(theme->alpha_mode) == sizeof(int), "enum is not 32-bit"); @@ -1529,6 +1529,16 @@ parse_color_theme(struct context *ctx, struct color_theme *theme) (int *)&theme->alpha_mode); } + else if (streq(key, "dim-blend-towards")) { + _Static_assert(sizeof(theme->dim_blend_towards) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"black", "white", NULL}, + (int *)&theme->dim_blend_towards); + } + else { LOG_CONTEXTUAL_ERR("not valid option"); return false; @@ -3428,6 +3438,7 @@ config_load(struct config *conf, const char *conf_path, .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 = { @@ -3523,6 +3534,8 @@ config_load(struct config *conf, const char *conf_path, memcpy(conf->colors.table, default_color_table, sizeof(default_color_table)); memcpy(conf->colors.sixel, default_sixel_colors, sizeof(default_sixel_colors)); memcpy(&conf->colors2, &conf->colors, sizeof(conf->colors)); + conf->colors2.dim_blend_towards = DIM_BLEND_TOWARDS_WHITE; + parse_modifiers(XKB_MOD_NAME_SHIFT, 5, &conf->mouse.selection_override_modifiers); tokenize_cmdline( diff --git a/config.h b/config.h index 5b7ff11e..37b3259f 100644 --- a/config.h +++ b/config.h @@ -145,6 +145,11 @@ struct color_theme { uint32_t dim[8]; uint32_t sixel[16]; + enum { + DIM_BLEND_TOWARDS_BLACK, + DIM_BLEND_TOWARDS_WHITE, + } dim_blend_towards; + enum { ALPHA_MODE_DEFAULT, ALPHA_MODE_MATCHING, diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 7b08d5d4..8697add2 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1031,7 +1031,8 @@ dark theme (since the default theme is dark). a color value, and a "dim" attribute. By default, foot implements this by blending the current color - with black. This is a generic approach that applies to both + with black or white, depending on what the *dim-blend-towards* + option is set to . This is a generic approach that applies to both colors from the 256-color palette, as well as 24-bit RGB colors. You can change this behavior by setting the *dimN* options. When @@ -1086,6 +1087,14 @@ dark theme (since the default theme is dark). Default: _default_ +*dim-blend-towards* + Which color to blend towards when "auto" dimming a color (see + *dim0*..*dim7* above). One of *black* or *white*. Blending towards + black makes the text darker, while blending towards white makes it + whiter (but still dimmer than normal text). + + Default: _black_ (*colors*), _white_ (*colors2*) + *selection-foreground*, *selection-background* Foreground (text) and background color to use in selected text. Default: _inverse foreground/background_. @@ -1124,7 +1133,8 @@ dark theme (since the default theme is dark). # SECTION: colors2 This section defines an alternative color theme. It has the exact same -keys as the *colors* section. The default values are the same. +keys as the *colors* section. The default values are the same, except +for *dim-blend-towards*, which defaults to *white* instead. Note that values are not inherited. That is, if you set a value in *colors*, but not in *colors2*, the value from *colors* is not diff --git a/foot.ini b/foot.ini index 44ed5785..2d170489 100644 --- a/foot.ini +++ b/foot.ini @@ -132,6 +132,7 @@ # bright7=ffffff # bright white ## dimmed colors (see foot.ini(5) man page) +# dim-blend-towards=black # dim0= # ... # dim7= @@ -170,6 +171,8 @@ [colors2] # Alternative color theme, see man page foot.ini(5) +# Same builtin defaults as [color], except for: +# dim-blend-towards=white [csd] # preferred=server diff --git a/render.c b/render.c index 35752125..fd721395 100644 --- a/render.c +++ b/render.c @@ -312,7 +312,14 @@ color_dim(const struct terminal *term, uint32_t color) } } - return color_blend_towards(color, 0x00000000, conf->dim.amount); + const struct color_theme *theme = term->colors.active_theme == COLOR_THEME1 + ? &conf->colors + : &conf->colors2; + + return color_blend_towards( + color, + theme->dim_blend_towards == DIM_BLEND_TOWARDS_BLACK ? 0x00000000 : 0x00ffffff, + conf->dim.amount); } static inline uint32_t diff --git a/tests/test-config.c b/tests/test-config.c index 64b61540..c442e700 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -753,6 +753,11 @@ test_section_colors(void) (int []){ALPHA_MODE_DEFAULT, ALPHA_MODE_MATCHING, ALPHA_MODE_ALL}, (int *)&conf.colors.alpha_mode); + test_enum(&ctx, &parse_section_colors, "dim-blend-towards", 2, + (const char *[]){"black", "white"}, + (int []){DIM_BLEND_TOWARDS_BLACK, DIM_BLEND_TOWARDS_WHITE}, + (int *)&conf.colors.dim_blend_towards); + for (size_t i = 0; i < 255; i++) { char key_name[4]; sprintf(key_name, "%zu", i); From 96605bf52fa3794ee2f13739831ad92229b4b3a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 11 Oct 2025 10:05:26 +0200 Subject: [PATCH 42/84] extract: number of spaces after the tab shouldn't include the tab cell itself This fixes an off by one, where we sometimes "ate" an extra space when extracting contents with tabs. This happened if the tab (and its subsequent spaces) were followed by an additional space. Closes #2194 --- CHANGELOG.md | 3 +++ extract.c | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76183459..3935889a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,8 +98,11 @@ * URL labels misplaces when URL contains double-width characters ([#2179][2179]). +* One space too much consumed when copying (or pipe:ing) contents with + tabs ([#2194][2194]) [2179]: https://codeberg.org/dnkl/foot/issues/2179 +[2194]: https://codeberg.org/dnkl/foot/issues/2194 ### Security diff --git a/extract.c b/extract.c index 31c32248..cd9a0c95 100644 --- a/extract.c +++ b/extract.c @@ -256,8 +256,8 @@ extract_one(const struct terminal *term, const struct row *row, } } - xassert(next_tab_stop >= col); - ctx->tab_spaces_left = next_tab_stop - col; + if (next_tab_stop > col) + ctx->tab_spaces_left = next_tab_stop - col - 1; } } From dbf18ba444e728ecddcc0e15fc44cc1fc0590b53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 15 Oct 2025 09:41:52 +0200 Subject: [PATCH 43/84] wayland: always render a new frame after a fullscreen change This is needed, since we disable alpha in fullscreen, and since we use different image buffer formats (XRGB vs. ARGB) when we have alpha vs. when we don't (and fullscreen always disables alpha). Normally, this happens anyway, as the window is resized when going in or out from fullscreen. But, it's technically possible for a compositor to change an application's fullscreen state without resizing the window. --- CHANGELOG.md | 4 ++++ wayland.c | 27 +++++++++++++++++++-------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3935889a..ed8dff69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,10 @@ ([#2179][2179]). * One space too much consumed when copying (or pipe:ing) contents with tabs ([#2194][2194]) +* Ensure we render a new frame when changing fullscreen state. Before, + this was automatically done if the window was also resized. But, it + is possible for a compositor to change an application's fullscreen + state without resizing the window. [2179]: https://codeberg.org/dnkl/foot/issues/2179 [2194]: https://codeberg.org/dnkl/foot/issues/2194 diff --git a/wayland.c b/wayland.c index 59b2a33e..bac087fb 100644 --- a/wayland.c +++ b/wayland.c @@ -1097,10 +1097,6 @@ xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, else if (csd_was_enabled && !enable_csd) csd_destroy(win); - /* Update opaque region if fullscreen state changed */ - if (was_fullscreen != win->is_fullscreen) - wayl_win_alpha_changed(win); - if (enable_csd && new_width > 0 && new_height > 0) { if (wayl_win_csd_titlebar_visible(win)) new_height -= win->term->conf->csd.title_height; @@ -1139,11 +1135,26 @@ xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, else term_visual_focus_out(term); - if (!resized && !term->render.pending.grid) { + /* + * Update opaque region if fullscreen state changed, also need to + * render, since we use different buffer types with and without + * alpha + */ + if (was_fullscreen != win->is_fullscreen) { + wayl_win_alpha_changed(win); + render_refresh(term); + } + + const bool will_render_soon = resized || + term->render.refresh.grid || + term->render.pending.grid; + + if (!will_render_soon) { /* - * If we didn't resize, we won't be committing a new surface - * anytime soon. Some compositors require a commit in - * combination with an ack - make them happy. + * If we didn't resize, and aren't refreshing for other + * reasons, we won't be committing a new surface anytime + * soon. Some compositors require a commit in combination with + * an ack - make them happy. */ wl_surface_commit(win->surface.surf); } From 612adda3842fcdbbc9b7e1e2f46be02a224da1eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 16 Oct 2025 08:45:07 +0200 Subject: [PATCH 44/84] render: don't warn about immediate buffer release if pre-apply-damage has been activated --- render.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render.c b/render.c index fd721395..1d0f08af 100644 --- a/render.c +++ b/render.c @@ -3372,7 +3372,7 @@ grid_render(struct terminal *term) { LOG_INFO("enabling pre-applied frame damage"); term->render.preapply_last_frame_damage = true; - } else if (!have_warned) { + } 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; From dc5a921d2c5561f6d8f857ab3483e2fd1c59026f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 16 Oct 2025 08:46:36 +0200 Subject: [PATCH 45/84] changelog: prepare for 1.25.0 --- CHANGELOG.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed8dff69..ce7d318d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.25.0](#1-25-0) * [1.24.0](#1-24-0) * [1.23.1](#1-23-1) * [1.23.0](#1-23-0) @@ -66,7 +66,8 @@ * [1.2.0](#1-2-0) -## Unreleased +## 1.25.0 + ### Added * Performance increased and input latency decreased on compositors @@ -92,8 +93,6 @@ [2182]: https://codeberg.org/dnkl/foot/issues/2182 -### Deprecated -### Removed ### Fixed * URL labels misplaces when URL contains double-width characters @@ -109,9 +108,11 @@ [2194]: https://codeberg.org/dnkl/foot/issues/2194 -### Security ### Contributors +* Charalampos Mitrodimas +* Matthias Heyman + ## 1.24.0 From b44a62724cd51c7fecdfd9d1b41a3691b11a4c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 16 Oct 2025 08:46:58 +0200 Subject: [PATCH 46/84] meson: bump version to 1.25.0 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 305df13c..a1d0104d 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.24.0', + version: '1.25.0', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From 82e75851e41fd245dc08e71f1618ae4aeede7fb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 16 Oct 2025 08:50:31 +0200 Subject: [PATCH 47/84] changelog: add new 'unreleased' section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce7d318d..d2eebef8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.25.0](#1-25-0) * [1.24.0](#1-24-0) * [1.23.1](#1-23-1) @@ -66,6 +67,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.25.0 ### Added From 558760446932f0f22c794084285cf998462dd706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 18 Oct 2025 08:23:53 +0200 Subject: [PATCH 48/84] input: keymap(): use a goto-label on error, to ensure we always close the keymap FD --- input.c | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/input.c b/input.c index fe5a8001..44a99e3b 100644 --- a/input.c +++ b/input.c @@ -576,23 +576,20 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, /* Verify keymap is in a format we understand */ switch ((enum wl_keyboard_keymap_format)format) { case WL_KEYBOARD_KEYMAP_FORMAT_NO_KEYMAP: - close(fd); - return; + goto err; case WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1: break; default: LOG_WARN("unrecognized keymap format: %u", format); - close(fd); - return; + 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"); - close(fd); - return; + goto err; } while (map_str[size - 1] == '\0') @@ -605,6 +602,8 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, } + munmap(map_str, size); + if (seat->kbd.xkb_keymap != NULL) { seat->kbd.xkb_state = xkb_state_new(seat->kbd.xkb_keymap); @@ -685,10 +684,10 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, seat->kbd.key_arrow_down = xkb_keymap_key_by_name(seat->kbd.xkb_keymap, "DOWN"); } - munmap(map_str, size); - close(fd); - key_binding_load_keymap(wayl->key_binding_manager, seat); + +err: + close(fd); } static void From 19466a21d8b7e580ef611ca56b81e7568fc44b37 Mon Sep 17 00:00:00 2001 From: Andrei Date: Fri, 24 Oct 2025 11:08:57 -0700 Subject: [PATCH 49/84] doc: foot.ini: fix typo --- doc/foot.ini.5.scd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 8697add2..2b57c467 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -2120,7 +2120,7 @@ any of these options. frame, foot proceeds with rendering the cells that has changed between the last frame and the new frame. - When this open is enabled, the changes between the last two frames + When this option is enabled, the changes between the last two frames are brought over to what will become the next frame before foot starts rendering the next frame. As soon as the compositor releases the previous buffer (typically right after foot has From 71de0c45bc38ee773f86bb4d48f96719c8abb568 Mon Sep 17 00:00:00 2001 From: c4llv07e Date: Mon, 27 Oct 2025 13:24:07 +0300 Subject: [PATCH 50/84] char32: add helper functions to work with c32 case --- char32.c | 22 ++++++++++++++++++++++ char32.h | 15 +++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/char32.c b/char32.c index 827cef8d..3d6c2c78 100644 --- a/char32.c +++ b/char32.c @@ -53,6 +53,14 @@ UNITTEST xassert(c32cmp(U"b", U"a") > 0); } +UNITTEST +{ + xassert(c32ncmp(U"foo", U"foot", 3) == 0); + xassert(c32ncmp(U"foot", U"FOOT", 4) > 0); + xassert(c32ncmp(U"a", U"b", 1) < 0); + xassert(c32ncmp(U"bb", U"aa", 2) > 0); +} + UNITTEST { char32_t copy[16]; @@ -127,6 +135,20 @@ UNITTEST xassert(c32cmp(dst, U"foobar12345678") == 0); } +UNITTEST +{ + xassert(!isc32upper(U'a')); + xassert(isc32upper(U'A')); + xassert(!isc32upper(U'a')); +} + +UNITTEST +{ + xassert(hasc32upper(U"abc1A")); + xassert(!hasc32upper(U"abc1_aaa")); + xassert(!hasc32upper(U"")); +} + UNITTEST { char32_t *c = xc32dup(U"foobar"); diff --git a/char32.h b/char32.h index 6a5eb080..dcb412ce 100644 --- a/char32.h +++ b/char32.h @@ -20,6 +20,10 @@ static inline int c32cmp(const char32_t *s1, const char32_t *s2) { return wcscmp((const wchar_t *)s1, (const wchar_t *)s2); } +static inline int c32ncmp(const char32_t *s1, const char32_t *s2, size_t n) { + return wcsncmp((const wchar_t *)s1, (const wchar_t *)s2, n); +} + static inline char32_t *c32ncpy(char32_t *dst, const char32_t *src, size_t n) { return (char32_t *)wcsncpy((wchar_t *)dst, (const wchar_t *)src, n); } @@ -60,6 +64,10 @@ static inline char32_t toc32upper(char32_t c) { return (char32_t)towupper((wint_t)c); } +static inline bool isc32upper(char32_t c32) { + return iswupper((wint_t)c32); +} + static inline bool isc32space(char32_t c32) { return iswspace((wint_t)c32); } @@ -72,6 +80,13 @@ static inline bool isc32graph(char32_t c32) { return iswgraph((wint_t)c32); } +static inline bool hasc32upper(const char32_t *s) { + for (int i = 0; s[i] != '\0'; i++) { + if (isc32upper(s[i])) return true; + } + return false; +} + static inline int c32width(char32_t c) { #if defined(FOOT_GRAPHEME_CLUSTERING) return utf8proc_charwidth((utf8proc_int32_t)c); From 5ae4955e834831b5680baaad7c4f974e407ffb7a Mon Sep 17 00:00:00 2001 From: c4llv07e Date: Mon, 27 Oct 2025 13:25:48 +0300 Subject: [PATCH 51/84] search: use case insensitive search only if there's no uppercase in search --- CHANGELOG.md | 2 ++ search.c | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2eebef8..81f7a168 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,8 @@ e.g. integrated graphics ([#2182][2182]). * Jump label colors in the modus-operandi theme, for improved readability. +* Scrollback search is now case sensitive when the search string + contains at least one upper case character. [2182]: https://codeberg.org/dnkl/foot/issues/2182 diff --git a/search.c b/search.c index dda84e6d..5a2b6236 100644 --- a/search.c +++ b/search.c @@ -283,8 +283,13 @@ matches_cell(const struct terminal *term, const struct cell *cell, size_t search if (composed == NULL && base == 0 && term->search.buf[search_ofs] == U' ') return 1; - if (c32ncasecmp(&base, &term->search.buf[search_ofs], 1) != 0) - return -1; + if (hasc32upper(term->search.buf)) { + 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) From 143f220527a0cffebcecbd5eaa0dccd5e009122b Mon Sep 17 00:00:00 2001 From: Ronan Pigott Date: Fri, 31 Oct 2025 15:11:53 -0700 Subject: [PATCH 52/84] search: do not emit composing keys When we are in the composing state for XCompose key sequences, we should not add the compose component keys to the search buffer. --- CHANGELOG.md | 4 ++++ search.c | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81f7a168..f00b5d15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,10 @@ ### Deprecated ### Removed ### Fixed + +* Search mode: composing keys not ignored. + + ### Security ### Contributors diff --git a/search.c b/search.c index 5a2b6236..5228bf61 100644 --- a/search.c +++ b/search.c @@ -1484,7 +1484,8 @@ search_input(struct seat *seat, struct terminal *term, 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) { + } else if (compose_status == XKB_COMPOSE_CANCELLED || + compose_status == XKB_COMPOSE_COMPOSING) { count = 0; } else { count = xkb_state_key_get_utf8( From 9728ada0289c446585325a74ded9c94958d9ab24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 30 Oct 2025 06:29:51 +0100 Subject: [PATCH 53/84] csi: focus mode (private mode 1004): send focus event immediate, when enabled This lets the application now the current state, without having to wait for the user to switch focus. Fixes #2202 --- CHANGELOG.md | 8 ++++++++ csi.c | 2 ++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f00b5d15..21d2c5f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,14 @@ ## Unreleased ### Added ### Changed + +* When enabling _"focus mode"_ (private mode 1004), foot now sends a + focus event immediately, to inform the application what the current + state is ([#2202][2202]). + +[2202]: https://codeberg.org/dnkl/foot/issues/2202 + + ### Deprecated ### Removed ### Fixed diff --git a/csi.c b/csi.c index 437fd8bc..c5f616ac 100644 --- a/csi.c +++ b/csi.c @@ -422,6 +422,8 @@ decset_decrst(struct terminal *term, unsigned param, bool enable) case 1004: term->focus_events = enable; + if (enable) + term_to_slave(term, term->kbd_focus ? "\033[I" : "\033[O", 3); break; case 1005: From 1fce0e69f5ec2c24d518917c0538a4d762b8a1e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 1 Nov 2025 08:12:52 +0100 Subject: [PATCH 54/84] changelog: case sensitive scrollback search: move to correct release --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21d2c5f5..e7f71ed5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,8 @@ * When enabling _"focus mode"_ (private mode 1004), foot now sends a focus event immediately, to inform the application what the current state is ([#2202][2202]). +* Scrollback search is now case sensitive when the search string + contains at least one upper case character. [2202]: https://codeberg.org/dnkl/foot/issues/2202 @@ -112,8 +114,6 @@ e.g. integrated graphics ([#2182][2182]). * Jump label colors in the modus-operandi theme, for improved readability. -* Scrollback search is now case sensitive when the search string - contains at least one upper case character. [2182]: https://codeberg.org/dnkl/foot/issues/2182 From 5cb8ff2e9c51c528589e5e1ef78b9a6bd554fc0d Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Fri, 7 Nov 2025 07:32:11 +0100 Subject: [PATCH 55/84] Fix assertion failure triple-clicking line with quote in last column By default, triple-click tries to select quoted strings within a logical line. This also works if the line spans multiple screen lines. If there is a quote character in the last column: printf %"$COLUMNS"s \'; printf wrapped; sleep inf and I triple-click on the following soft-wrapped line, there's an assertion failure because the column next to the quote is out of range. The quote position has been found by walking at least one cell backwards from "pos". This means that if the quote position is in the very last column, there must be a row below. Also move the assertion to be a pre-condition, though that's debatable. --- selection.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/selection.c b/selection.c index d7aa617a..f07396a5 100644 --- a/selection.c +++ b/selection.c @@ -19,6 +19,7 @@ #include "char32.h" #include "commands.h" #include "config.h" +#include "debug.h" #include "extract.h" #include "grid.h" #include "misc.h" @@ -558,9 +559,15 @@ selection_find_quote_left(struct terminal *term, struct coord *pos, if (*quote_char == '\0' ? (wc == '"' || wc == '\'') : wc == *quote_char) { - pos->row = next_row; - pos->col = next_col + 1; - xassert(pos->col < term->cols); + xassert(next_col + 1 <= term->cols); + if (next_col + 1 == term->cols) { + xassert(next_row < pos->row); + pos->row = next_row + 1; + pos->col = 0; + } else { + pos->row = next_row; + pos->col = next_col + 1; + } *quote_char = wc; return true; From c9abab08079a0b6eeb4f3fc9beb80eb5fd621730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 12 Nov 2025 07:46:34 +0100 Subject: [PATCH 56/84] changelog: triple-click when there's a quote in the last column --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7f71ed5..654ba94f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,8 @@ ### Fixed * Search mode: composing keys not ignored. +* Crash when triple-clicking a soft-wrapped line and there is a quote + character in the last column. ### Security From fc9625678fc7e295e7a7e03a5d47db21d4be3010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 12 Nov 2025 11:04:25 +0100 Subject: [PATCH 57/84] config: add toplevel-tag=TAG Add support for the new xdg-toplevel-tag-v1 Wayland protocol, by exposing a new config option, `toplevel-tag`, and a corresponding command option, `--toplevel-tag` (in both `foot` and `footclient`). This can help the compositor with session management, or custom window rules. Closes #2212 --- CHANGELOG.md | 9 +++++++++ client.c | 12 ++++++++++++ completions/bash/foot | 5 +++-- completions/bash/footclient | 5 +++-- completions/fish/foot.fish | 1 + completions/fish/footclient.fish | 1 + completions/zsh/_foot | 1 + completions/zsh/_footclient | 1 + config.c | 6 ++++++ config.h | 1 + doc/foot.1.scd | 5 +++++ doc/foot.ini.5.scd | 5 +++++ doc/footclient.1.scd | 5 +++++ foot-features.c | 6 ++++++ main.c | 7 +++++++ meson.build | 7 ++++++- tests/test-config.c | 1 + wayland.c | 33 +++++++++++++++++++++++++++++++- wayland.h | 8 ++++++++ 19 files changed, 113 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 654ba94f..a293c721 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,15 @@ ## Unreleased ### Added + +* `toplevel-tag` option (and `--toplevel-tag` command line options to + `foot` and `footclient`), allowing you to set a custom toplevel + tag. The compositor must implement the new `xdg-toplevel-tag-v1` + Wayland protocol ([#2212][2212]). + +[2212]: https://codeberg.org/dnkl/foot/issues/2212 + + ### Changed * When enabling _"focus mode"_ (private mode 1004), foot now sends a diff --git a/client.c b/client.c index aa5302be..85bc7fda 100644 --- a/client.c +++ b/client.c @@ -76,6 +76,7 @@ print_usage(const char *prog_name) " -t,--term=TERM value to set the environment variable TERM to (" FOOT_DEFAULT_TERM ")\n" " -T,--title=TITLE initial window title (foot)\n" " -a,--app-id=ID window application ID (foot)\n" + " --toplevel-tag=TAG set a custom toplevel tag\n" " -w,--window-size-pixels=WIDTHxHEIGHT initial width and height, in pixels\n" " -W,--window-size-chars=WIDTHxHEIGHT initial width and height, in characters\n" " -m,--maximized start in maximized mode\n" @@ -137,6 +138,10 @@ send_string_list(int fd, const string_list_t *string_list) return true; } +enum { + TOPLEVEL_TAG_OPTION = CHAR_MAX + 1, +}; + int main(int argc, char *const *argv) { @@ -151,6 +156,7 @@ main(int argc, char *const *argv) {"term", required_argument, NULL, 't'}, {"title", required_argument, NULL, 'T'}, {"app-id", required_argument, NULL, 'a'}, + {"toplevel-tag", required_argument, NULL, TOPLEVEL_TAG_OPTION}, {"window-size-pixels", required_argument, NULL, 'w'}, {"window-size-chars", required_argument, NULL, 'W'}, {"maximized", no_argument, NULL, 'm'}, @@ -220,6 +226,12 @@ main(int argc, char *const *argv) goto err; break; + case TOPLEVEL_TAG_OPTION: + snprintf(buf, sizeof(buf), "toplevel-tag=%s", optarg); + if (!push_string(&overrides, buf, &total_len)) + goto err; + break; + case 'L': if (!push_string(&overrides, "login-shell=yes", &total_len)) goto err; diff --git a/completions/bash/foot b/completions/bash/foot index 25aa2c49..e27be2fa 100644 --- a/completions/bash/foot +++ b/completions/bash/foot @@ -6,6 +6,7 @@ _foot() local cur prev flags word commands match previous_words i offset flags=( "--app-id" + "--toplevel-tag" "--check-config" "--config" "--font" @@ -40,7 +41,7 @@ _foot() for word in "${previous_words[@]}" ; do match=$(printf "$commands" | grep -Fx "$word" 2>/dev/null) if [[ ! -z "$match" ]] ; then - if [[ ${COMP_WORDS[i-1]} =~ ^(--app-id|--config|--font|--log-level|--pty|--term|--title|--window-size-pixels|--window-size-chars|--working-directory)$ ]] ; then + if [[ ${COMP_WORDS[i-1]} =~ ^(--app-id|--toplevel-tag|--config|--font|--log-level|--pty|--term|--title|--window-size-pixels|--window-size-chars|--working-directory)$ ]] ; then (( i++ )) continue fi @@ -75,7 +76,7 @@ _foot() COMPREPLY=( $(compgen -W "none error warning info" -- ${cur}) ) ;; --log-colorize|-l) COMPREPLY=( $(compgen -W "never always auto" -- ${cur}) ) ;; - --app-id|--help|--override|--pty|--title|--version|--window-size-chars|--window-size-pixels|--check-config|-[ahoTvWwC]) + --app-id|--toplevel-tag|--help|--override|--pty|--title|--version|--window-size-chars|--window-size-pixels|--check-config|-[ahoTvWwC]) # Don't autocomplete for these flags : ;; *) diff --git a/completions/bash/footclient b/completions/bash/footclient index 62abdd65..c7f1df4e 100644 --- a/completions/bash/footclient +++ b/completions/bash/footclient @@ -6,6 +6,7 @@ _footclient() local cur prev flags word commands match previous_words i offset flags=( "--app-id" + "--toplevel-tag" "--fullscreen" "--help" "--hold" @@ -35,7 +36,7 @@ _footclient() for word in "${previous_words[@]}" ; do match=$(printf "$commands" | grep -Fx "$word" 2>/dev/null) if [[ ! -z "$match" ]] ; then - if [[ ${COMP_WORDS[i-1]} =~ ^(--app-id|--log-level|--server-socket|--term|--title|--window-size-pixels|--window-size-chars|--working-directory)$ ]] ; then + if [[ ${COMP_WORDS[i-1]} =~ ^(--app-id|--toplevel-tag|--log-level|--server-socket|--term|--title|--window-size-pixels|--window-size-chars|--working-directory)$ ]] ; then (( i++ )) continue fi @@ -67,7 +68,7 @@ _footclient() COMPREPLY=( $(compgen -W "none error warning info" -- ${cur}) ) ;; --log-colorize|-l) COMPREPLY=( $(compgen -W "never always auto" -- ${cur}) ) ;; - --app-id|--help|--override|--title|--version|--window-size-chars|--window-size-pixels|-[ahoTvWw]) + --app-id|--toplevel-tag|--help|--override|--title|--version|--window-size-chars|--window-size-pixels|-[ahoTvWw]) # Don't autocomplete for these flags : ;; *) diff --git a/completions/fish/foot.fish b/completions/fish/foot.fish index 0053d18d..21b42d3d 100644 --- a/completions/fish/foot.fish +++ b/completions/fish/foot.fish @@ -6,6 +6,7 @@ complete -c foot -x -s f -l font -a "(fc-list : family | sed 's/,/ complete -c foot -x -s t -l term -a '(find /usr/share/terminfo -type f -printf "%f\n")' -d "value to set the environment variable TERM to (foot)" complete -c foot -x -s T -l title -d "initial window title" complete -c foot -x -s a -l app-id -d "value to set the app-id property on the Wayland window to (foot)" +complete -c foot -x -l toplevel-tag -d "value to set the toplevel-tag property on the Wayland window to" complete -c foot -s m -l maximized -d "start in maximized mode" complete -c foot -s F -l fullscreen -d "start in fullscreen mode" complete -c foot -s L -l login-shell -d "start shell as a login shell" diff --git a/completions/fish/footclient.fish b/completions/fish/footclient.fish index df3e1273..03624796 100644 --- a/completions/fish/footclient.fish +++ b/completions/fish/footclient.fish @@ -2,6 +2,7 @@ complete -c footclient -x -a "(__fish_complete_subcom complete -c footclient -x -s t -l term -a '(find /usr/share/terminfo -type f -printf "%f\n")' -d "value to set the environment variable TERM to (foot)" complete -c footclient -x -s T -l title -d "initial window title" complete -c footclient -x -s a -l app-id -d "value to set the app-id property on the Wayland window to (foot)" +complete -c footclient -x -l toplevel-tag -d "value to set the toplevel-tag property on the Wayland window to" complete -c footclient -s m -l maximized -d "start in maximized mode" complete -c footclient -s F -l fullscreen -d "start in fullscreen mode" complete -c footclient -s L -l login-shell -d "start shell as a login shell" diff --git a/completions/zsh/_foot b/completions/zsh/_foot index 2a0dc7b0..0fd83b3c 100644 --- a/completions/zsh/_foot +++ b/completions/zsh/_foot @@ -9,6 +9,7 @@ _arguments \ '(-t --term)'{-t,--term}'[value to set the environment variable TERM to (foot)]:term:->terms' \ '(-T --title)'{-T,--title}'[initial window title]:()' \ '(-a --app-id)'{-a,--app-id}'[value to set the app-id property on the Wayland window to (foot)]:()' \ + '--toplevel-tag=[value to set the toplevel-tag property on the Wayland window to]:()' \ '(-m --maximized)'{-m,--maximized}'[start in maximized mode]' \ '(-F --fullscreen)'{-F,--fullscreen}'[start in fullscreen mode]' \ '(-L --login-shell)'{-L,--login-shell}'[start shell as a login shell]' \ diff --git a/completions/zsh/_footclient b/completions/zsh/_footclient index c14d65d5..12f29d7a 100644 --- a/completions/zsh/_footclient +++ b/completions/zsh/_footclient @@ -5,6 +5,7 @@ _arguments \ '(-t --term)'{-t,--term}'[value to set the environment variable TERM to (foot)]:term:->terms' \ '(-T --title)'{-T,--title}'[initial window title]:()' \ '(-a --app-id)'{-a,--app-id}'[value to set the app-id property on the Wayland window to (foot)]:()' \ + '--toplevel-tag=[value to set the toplevel-tag property on the Wayland window to]:()' \ '(-m --maximized)'{-m,--maximized}'[start in maximized mode]' \ '(-F --fullscreen)'{-F,--fullscreen}'[start in fullscreen mode]' \ '(-L --login-shell)'{-L,--login-shell}'[start shell as a login shell]' \ diff --git a/config.c b/config.c index 4449d9c2..515b088c 100644 --- a/config.c +++ b/config.c @@ -923,6 +923,9 @@ parse_section_main(struct context *ctx) 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; @@ -3371,6 +3374,7 @@ config_load(struct config *conf, const char *conf_path, .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, @@ -3823,6 +3827,7 @@ config_clone(const struct config *old) 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); @@ -3922,6 +3927,7 @@ config_free(struct config *conf) 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); diff --git a/config.h b/config.h index 37b3259f..fc5e290e 100644 --- a/config.h +++ b/config.h @@ -219,6 +219,7 @@ struct config { char *shell; char *title; char *app_id; + char *toplevel_tag; char32_t *word_delimiters; bool login_shell; bool locked_title; diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 8d968a6e..cbf22f5b 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -67,6 +67,11 @@ the foot command line Value to set the *app-id* property on the Wayland window to. Default: _foot_ (normal mode), or _footclient_ (server mode). +*toplevel-tag*=_TAG_ + Value to set the *toplevel-tag* property on the Wayland window + to. The compositor can use this value for session management, + window rules etc. Default: _not set_ + *-m*,*--maximized* Start in maximized mode. If both *--maximized* and *--fullscreen* are specified, the _last_ one takes precedence. diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 2b57c467..c9782895 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -429,6 +429,11 @@ empty string to be set, but it must be quoted: *KEY=""*) apply window management rules. Default: _foot_ (normal mode), or _footclient_ (server mode). +*toplevel-tag* + Value to set the *toplevel-tag* property on the Wayland window + to. The compositor can use this value for session management, + window rules etc. Default: _not set_ + *bold-text-in-bright* Semi-boolean. When enabled, bold text is rendered in a brighter color (in addition to using a bold font). The color is brightened diff --git a/doc/footclient.1.scd b/doc/footclient.1.scd index e4f6d350..edf3e9f3 100644 --- a/doc/footclient.1.scd +++ b/doc/footclient.1.scd @@ -33,6 +33,11 @@ terminal has terminated. Value to set the *app-id* property on the Wayland window to. Default: _foot_ (normal mode), or _footclient_ (server mode). +*toplevel-tag*=_TAG_ + Value to set the *toplevel-tag* property on the Wayland window + to. The compositor can use this value for session management, + window rules etc. Default: _not set_ + *-w*,*--window-size-pixels*=_WIDTHxHEIGHT_ Set initial window width and height, in pixels. Default: _700x500_. diff --git a/foot-features.c b/foot-features.c index 1b5bf7fd..f701533c 100644 --- a/foot-features.c +++ b/foot-features.c @@ -22,6 +22,12 @@ const char version_and_features[] = " -graphemes" #endif +#if defined(HAVE_XDG_TOPLEVEL_TAG) + " +toplevel-tag" +#else + " -toplevel-tag" +#endif + #if !defined(NDEBUG) " +assertions" #else diff --git a/main.c b/main.c index b6a0d825..b933e1c1 100644 --- a/main.c +++ b/main.c @@ -84,6 +84,7 @@ print_usage(const char *prog_name) " -t,--term=TERM value to set the environment variable TERM to (" FOOT_DEFAULT_TERM ")\n" " -T,--title=TITLE initial window title (foot)\n" " -a,--app-id=ID window application ID (foot)\n" + " --toplevel-tag=TAG set a custom toplevel tag\n" " -m,--maximized start in maximized mode\n" " -F,--fullscreen start in fullscreen mode\n" " -L,--login-shell start shell as a login shell\n" @@ -185,6 +186,7 @@ sanitize_signals(void) enum { PTY_OPTION = CHAR_MAX + 1, + TOPLEVEL_TAG_OPTION = CHAR_MAX + 2, }; int @@ -214,6 +216,7 @@ main(int argc, char *const *argv) {"term", required_argument, NULL, 't'}, {"title", required_argument, NULL, 'T'}, {"app-id", required_argument, NULL, 'a'}, + {"toplevel-tag", required_argument, NULL, TOPLEVEL_TAG_OPTION}, {"login-shell", no_argument, NULL, 'L'}, {"working-directory", required_argument, NULL, 'D'}, {"font", required_argument, NULL, 'f'}, @@ -285,6 +288,10 @@ main(int argc, char *const *argv) tll_push_back(overrides, xstrjoin("app-id=", optarg)); break; + case TOPLEVEL_TAG_OPTION: + tll_push_back(overrides, xstrjoin("toplevel-tag=", optarg)); + break; + case 'D': { struct stat st; if (stat(optarg, &st) < 0 || !(st.st_mode & S_IFDIR)) { diff --git a/meson.build b/meson.build index a1d0104d..aa8342ab 100644 --- a/meson.build +++ b/meson.build @@ -182,7 +182,12 @@ wl_proto_xml = [ wayland_protocols_datadir / 'staging/xdg-toplevel-icon/xdg-toplevel-icon-v1.xml', wayland_protocols_datadir / 'staging/xdg-system-bell/xdg-system-bell-v1.xml', wayland_protocols_datadir / 'staging/color-management/color-management-v1.xml', - ] +] + +if (wayland_protocols.version().version_compare('>=1.43')) + wl_proto_xml += [wayland_protocols_datadir / 'staging/xdg-toplevel-tag/xdg-toplevel-tag-v1.xml'] + add_project_arguments('-DHAVE_XDG_TOPLEVEL_TAG=1', language: 'c') +endif foreach prot : wl_proto_xml wl_proto_headers += custom_target( diff --git a/tests/test-config.c b/tests/test-config.c index c442e700..268733db 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -482,6 +482,7 @@ test_section_main(void) test_string(&ctx, &parse_section_main, "shell", &conf.shell); test_string(&ctx, &parse_section_main, "term", &conf.term); test_string(&ctx, &parse_section_main, "app-id", &conf.app_id); + test_string(&ctx, &parse_section_main, "toplevel-tag", &conf.toplevel_tag); test_string(&ctx, &parse_section_main, "utmp-helper", &conf.utmp_helper_path); test_c32string(&ctx, &parse_section_main, "word-delimiters", &conf.word_delimiters); diff --git a/wayland.c b/wayland.c index bac087fb..6785c52d 100644 --- a/wayland.c +++ b/wayland.c @@ -1548,6 +1548,17 @@ handle_global(void *data, struct wl_registry *registry, wayl->color_management.manager, &color_manager_listener, wayl); } +#if defined(HAVE_XDG_TOPLEVEL_TAG) + else if (streq(interface, xdg_toplevel_tag_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->toplevel_tag_manager = wl_registry_bind( + wayl->registry, name, &xdg_toplevel_tag_manager_v1_interface, required); + } +#endif + #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED else if (streq(interface, zwp_text_input_manager_v3_interface.name)) { const uint32_t required = 1; @@ -1791,7 +1802,7 @@ wayl_init(struct fdm *fdm, struct key_binding_manager *key_binding_manager, } if (wayl->toplevel_icon_manager == NULL) { - LOG_WARN("compositor does not implement the XDG toplevel icon protocol"); + LOG_WARN("compositor does not implement the xdg-toplevel-icon protocol"); } #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED @@ -1870,6 +1881,11 @@ wayl_destroy(struct wayland *wayl) zwp_text_input_manager_v3_destroy(wayl->text_input_manager); #endif +#if defined(HAVE_XDG_TOPLEVEL_TAG) + if (wayl->toplevel_tag_manager != NULL) + xdg_toplevel_tag_manager_v1_destroy(wayl->toplevel_tag_manager); +#endif + if (wayl->color_management.img_description != NULL) wp_image_description_v1_destroy(wayl->color_management.img_description); if (wayl->color_management.manager != NULL) @@ -1995,6 +2011,21 @@ wayl_win_init(struct terminal *term, const char *token) xdg_toplevel_set_app_id(win->xdg_toplevel, conf->app_id); +#if defined(HAVE_XDG_TOPLEVEL_TAG) + if (conf->toplevel_tag != NULL && conf->toplevel_tag[0] != '\0') { + if (wayl->toplevel_tag_manager != NULL) { + xdg_toplevel_tag_manager_v1_set_toplevel_tag( + wayl->toplevel_tag_manager, win->xdg_toplevel, conf->toplevel_tag); + + /* TODO: the description is recommended to be the tag, but translated */ + xdg_toplevel_tag_manager_v1_set_toplevel_description( + wayl->toplevel_tag_manager, win->xdg_toplevel, conf->toplevel_tag); + } else { + LOG_WARN("compositor does not implement the xdg-toplevel-tag protocol"); + } + } +#endif + if (wayl->toplevel_icon_manager != NULL) { const char *app_id = term->app_id != NULL ? term->app_id : term->conf->app_id; diff --git a/wayland.h b/wayland.h index eb1c35a3..140c2058 100644 --- a/wayland.h +++ b/wayland.h @@ -23,6 +23,10 @@ #include #include +#if defined(HAVE_XDG_TOPLEVEL_TAG) + #include +#endif + #include #include @@ -481,6 +485,10 @@ struct wayland { struct wp_presentation *presentation; uint32_t presentation_clock_id; +#if defined(HAVE_XDG_TOPLEVEL_TAG) + struct xdg_toplevel_tag_manager_v1 *toplevel_tag_manager; +#endif + #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED struct zwp_text_input_manager_v3 *text_input_manager; #endif From be19ca2b2074c522be489d0f4760d48a09265045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 29 Nov 2025 09:47:22 +0100 Subject: [PATCH 58/84] client: add missing (for CHAR_MAX) Closes #2221 --- client.c | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/client.c b/client.c index 85bc7fda..befd3ab0 100644 --- a/client.c +++ b/client.c @@ -1,12 +1,13 @@ -#include -#include -#include -#include -#include -#include -#include -#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include From 55f838869443bb34a502c238301e22b4d6f52073 Mon Sep 17 00:00:00 2001 From: Whyme Lyu Date: Mon, 1 Dec 2025 18:38:58 +0800 Subject: [PATCH 59/84] doc: remove duplicated ctrl+shift+w in foot(1) --- doc/foot.1.scd | 3 --- 1 file changed, 3 deletions(-) diff --git a/doc/foot.1.scd b/doc/foot.1.scd index cbf22f5b..60ba622b 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -257,9 +257,6 @@ These keyboard shortcuts affect the search selection: *ctrl*+*shift*+*left* Extend current selection to the left to the last word boundary. -*ctrl*+*shift*+*w* - Extend the current selection to the right to the last whitespace. - *shift*+*down* Extend current selection down one line From 65bd79b77d0acf3fcf6be1ecfe5a4a3ce2e1151a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 10 Dec 2025 08:48:41 +0100 Subject: [PATCH 60/84] term: reverse-scroll: fix crash when viewport ends up outside the (new) scrollback If the viewport has been scrolled up, it is possible for a reverse-scroll (rin) to cause the viewport to point to lines outside the scrollback. This is an issue if the scrollback isn't full, since in that case, the viewport will contain NULL lines. This will potentially trigger assertions in a couple of different places. Example backtrace: #2 0x555555cd230c in bug ../../debug.c:44 #3 0x555555ad485e in grid_row_in_view ../../grid.h:83 #4 0x555555b15a89 in grid_render ../../render.c:3465 #5 0x555555b3b0ab in fdm_hook_refresh_pending_terminals ../../render.c:5165 #6 0x555555a74980 in fdm_poll ../../fdm.c:435 #7 0x555555ac2b85 in main ../../main.c:676 Detect when this happens, and force-move the viewport to ensure it is valid. Closes #2232 --- CHANGELOG.md | 4 ++++ terminal.c | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a293c721..77d7d772 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -96,6 +96,10 @@ * Search mode: composing keys not ignored. * Crash when triple-clicking a soft-wrapped line and there is a quote character in the last column. +* Crash when reverse-scrolling (terminfo capability `rin`) such that + the current viewport ends up outside the scrollback ([#2232][2232]). + +[2232]: https://codeberg.org/dnkl/foot/issues/2232 ### Security diff --git a/terminal.c b/terminal.c index 36f8513b..e70250d8 100644 --- a/terminal.c +++ b/terminal.c @@ -3165,11 +3165,17 @@ term_scroll_reverse_partial(struct terminal *term, sixel_scroll_down(term, rows); - bool view_follows = term->grid->view == term->grid->offset; + 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); @@ -3177,6 +3183,11 @@ term_scroll_reverse_partial(struct terminal *term, 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 */ @@ -3193,11 +3204,16 @@ term_scroll_reverse_partial(struct terminal *term, 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 } From ac6d7660dd77c5418593a35cd623bf198ac93fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 10 Dec 2025 08:54:24 +0100 Subject: [PATCH 61/84] ci: codespell: ignore 'rin' --- .woodpecker.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 340ba241..843c9afc 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -14,7 +14,7 @@ steps: - python3 -m venv codespell-venv - source codespell-venv/bin/activate - pip install codespell - - codespell -Lser,doas,zar README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd + - codespell -Lser,doas,zar,rin README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd - deactivate - name: subprojects From 6e533231b016684a32a1975ce2e33ae3ae38b4c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 10 Dec 2025 09:39:51 +0100 Subject: [PATCH 62/84] term: mouse SGR mode: don't emit negative CSI values When reporting the column/row pixel value in mouse SGR mode, we emitted negative values when the cursor was being dragged outside the window. Unfortunately, negative values aren't allowed in CSI parameters, as '-' is an intermediate value. It was done this way, to be consistent with XTerm behavior. Allegedly, XTerm has changed its behavior in patch 404. With that in mind, and seeing that foot has never emitted negative values in any other mouse mode, let's stop emitting negative values in SGR mode too. Closes #2226 --- CHANGELOG.md | 3 +++ terminal.c | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77d7d772..85dc3762 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,8 +85,11 @@ state is ([#2202][2202]). * Scrollback search is now case sensitive when the search string contains at least one upper case character. +* Mouse tracking in SGR pixel mode no longer emits negative column/row + pixel values ([#2226][2226]). [2202]: https://codeberg.org/dnkl/foot/issues/2202 +[2226]: https://codeberg.org/dnkl/foot/issues/2226 ### Deprecated diff --git a/terminal.c b/terminal.c index e70250d8..3749416b 100644 --- a/terminal.c +++ b/terminal.c @@ -3420,10 +3420,13 @@ report_mouse_click(struct terminal *term, int encoded_button, int row, int col, encoded_button, col + 1, row + 1, release ? 'm' : 'M'); break; - case MOUSE_SGR_PIXELS: + 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, col_pixels + 1, row_pixels + 1, release ? 'm' : 'M'); + encoded_button, bounded_col + 1, bounded_row + 1, release ? 'm' : 'M'); break; + } case MOUSE_URXVT: snprintf(response, sizeof(response), "\033[%d;%d;%dM", From 15ebc433baabd799252609e28bf79afd892b33a4 Mon Sep 17 00:00:00 2001 From: Yaakov Selkowitz Date: Tue, 16 Dec 2025 22:10:39 -0500 Subject: [PATCH 63/84] Fix discarded const qualifiers from string functions This is a new warning in GCC 15 that is being promoted to an error due to the werror=true in meson.build. --- notify.c | 2 +- osc.c | 2 +- tokenize.c | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/notify.c b/notify.c index e8688180..e454b03b 100644 --- a/notify.c +++ b/notify.c @@ -114,7 +114,7 @@ consume_stdout(struct notification *notif, bool eof) while (left > 0) { line = data; size_t len = left; - char *eol = memchr(line, '\n', left); + char *eol = (char *)memchr(line, '\n', left); if (eol != NULL) { *eol = '\0'; diff --git a/osc.c b/osc.c index 0b492564..375eae5c 100644 --- a/osc.c +++ b/osc.c @@ -513,7 +513,7 @@ osc_uri(struct terminal *term, char *string) key_value = strtok_r(NULL, ":", &ctx)) { const char *key = key_value; - char *operator = strchr(key_value, '='); + char *operator = (char *)strchr(key_value, '='); if (operator == NULL) continue; diff --git a/tokenize.c b/tokenize.c index 77cc3f1a..70ceb39b 100644 --- a/tokenize.c +++ b/tokenize.c @@ -45,7 +45,7 @@ tokenize_cmdline(const char *cmdline, char ***argv) size_t idx = 0; while (*p != '\0') { - char *end = strchr(search_start, delim); + char *end = (char *)strchr(search_start, delim); if (end == NULL) { if (delim != ' ') { LOG_ERR("unterminated %s quote", delim == '"' ? "double" : "single"); From 4e96780eef048baa7370499c64a33210b4e0406d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 16 Dec 2025 14:56:42 +0100 Subject: [PATCH 64/84] shm: revert part of 299186a6547f6e038ee6f3822caf9a0fdfabceef 299186a6547f6e038ee6f3822caf9a0fdfabceef introduced a regression, where we don't handle SHM buffer "hiccups" correctly. If foot, for some reason is forced to render a frame "too soon", we might end up having multiple buffers "in flight" (i.e. committed to the compositor). This could happen if the compositor pushes multiple configure events rapidly, for example. Or anything else that forces foot to render something "immediately", without waiting for a frame callback. The compositor typically releases both buffers at the same time (or close to it), so the _next_ time we want to render a frame, we have *two* buffers to pick between. The problem here is that after 299186a6547f6e038ee6f3822caf9a0fdfabceef, foot no longer purges the additional buffer(s), but keeps all of them around. This messes up foot's age tracking, and the _next_ time we're forced to pull two buffers (without the compositor releasing the first one in between), we try to apply damage tracking that is no longer valid. This results in visual glitches. This never self-repairs, and we're stuck with visual glitches until the window is resized, and we're forced to allocate completely new buffers. It is unclear why 299186a6547f6e038ee6f3822caf9a0fdfabceef stopped removing the buffers. It was likely done early in the development, and is no longer needed. So far, I haven't noticed any bugs by re-introducing the buffer purging, but further testing is needed. --- CHANGELOG.md | 1 + shm.c | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85dc3762..70ab43b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,6 +101,7 @@ character in the last column. * Crash when reverse-scrolling (terminfo capability `rin`) such that the current viewport ends up outside the scrollback ([#2232][2232]). +* Regression: visual glitches in rare circumstances. [2232]: https://codeberg.org/dnkl/foot/issues/2232 diff --git a/shm.c b/shm.c index 72b32f16..f488d6b6 100644 --- a/shm.c +++ b/shm.c @@ -628,14 +628,14 @@ shm_get_buffer(struct buffer_chain *chain, int width, int height, bool with_alph else #endif { - if (cached == NULL) + if (cached == NULL) { cached = buf; - else { + } else { /* We have multiple buffers eligible for * reuse. Pick the "youngest" one, and mark the * other one for purging */ if (buf->public.age < cached->public.age) { - //shm_unref(&cached->public); + shm_unref(&cached->public); cached = buf; } else { /* @@ -646,8 +646,8 @@ shm_get_buffer(struct buffer_chain *chain, int width, int height, bool with_alph * should be safe; "our" tll_foreach() already * holds the next pointer. */ - //if (buffer_unref_no_remove_from_chain(buf)) - // tll_remove(chain->bufs, it); + if (buffer_unref_no_remove_from_chain(buf)) + tll_remove(chain->bufs, it); } } } From cf2b390f6e096e7a2ca93d4dece153eb13261a2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 19 Dec 2025 09:29:06 +0100 Subject: [PATCH 65/84] config: add [colors-dark] and [colors-light], replacing [colors] and [colors2] The main reason for having two color sections is to be able to switch between dark and light. Thus, it's better if the section names reflect this, rather than the more generic 'colors' and 'colors2' (which was the dark one and which was the light one, now again?) When the second color section was added, we kept the original name, colors, to make sure we didn't break existing configurations, and third-party themes. However, in the long run, it's probably better to be specific in the section naming, to avoid confusion. So, add 'colors-dark', and 'colors-light'. Keep 'colors' and 'colors2' as aliases for now, but mark them as deprecated. They WILL be removed in a future release. Also rename the option values for initial-color-theme, from 1/2, to dark/light. Keep the old ones for now, marked as deprecated. Update all bundled themes to use the new names. In the light-only themes (i.e. themes that define a single, light, theme), use colors-light, and set initial-color-theme=light. Possible improvements: disable color switching if only one color section has been explicitly configured (todo: figure out how to handle the default color theme values...) --- CHANGELOG.md | 7 + config.c | 128 ++++++++++++++--- config.h | 10 +- csi.c | 2 +- doc/foot.1.scd | 4 +- doc/foot.ini.5.scd | 57 ++++---- doc/footclient.1.scd | 4 +- foot.ini | 6 +- input.c | 6 +- key-binding.h | 2 + main.c | 8 +- osc.c | 44 +++--- render.c | 60 ++++---- server.c | 20 +-- server.h | 4 +- sixel.c | 6 +- terminal.c | 38 +++--- terminal.h | 4 +- tests/test-config.c | 196 +++++++++++++++++++-------- themes/aeroroot | 2 +- themes/alacritty | 2 +- themes/apprentice | 2 +- themes/ayu-mirage | 2 +- themes/catppuccin-frappe | 2 +- themes/catppuccin-latte | 5 +- themes/catppuccin-macchiato | 2 +- themes/catppuccin-mocha | 2 +- themes/chiba-dark | 2 +- themes/derp | 2 +- themes/deus | 2 +- themes/dracula | 2 +- themes/dracula-iterm | 2 +- themes/electrophoretic | 5 +- themes/gruvbox | 4 +- themes/gruvbox-dark | 2 +- themes/gruvbox-light | 5 +- themes/hacktober | 2 +- themes/iterm | 2 +- themes/jetbrains-darcula | 2 +- themes/kitty | 2 +- themes/material-amber | 5 +- themes/material-design | 2 +- themes/modus-operandi | 6 +- themes/modus-vivendi | 2 +- themes/modus-vivendi-tinted | 2 +- themes/molokai | 2 +- themes/monokai-pro | 2 +- themes/moonfly | 2 +- themes/neon | 2 +- themes/night-owl | 2 +- themes/nightfly | 2 +- themes/noirblaze | 2 +- themes/nord | 2 +- themes/nordiq | 2 +- themes/nvim | 4 +- themes/nvim-dark | 2 +- themes/nvim-light | 5 +- themes/onedark | 2 +- themes/onehalf-dark | 2 +- themes/panda | 2 +- themes/paper-color | 4 +- themes/paper-color-dark | 2 +- themes/paper-color-light | 5 +- themes/poimandres | 2 +- themes/rezza | 2 +- themes/rose-pine | 2 +- themes/rose-pine-dawn | 6 +- themes/rose-pine-moon | 2 +- themes/selenized | 4 +- themes/selenized-black | 2 +- themes/selenized-dark | 2 +- themes/selenized-light | 5 +- themes/selenized-white | 5 +- themes/solarized | 4 +- themes/solarized-dark | 2 +- themes/solarized-dark-normal-brights | 2 +- themes/solarized-light | 5 +- themes/solarized-normal-brights | 4 +- themes/srcery | 2 +- themes/starlight | 2 +- themes/tango | 2 +- themes/tempus-autumn | 2 +- themes/tempus-classic | 2 +- themes/tempus-dawn | 6 +- themes/tempus-day | 5 +- themes/tempus-dusk | 2 +- themes/tempus-fugit | 5 +- themes/tempus-future | 2 +- themes/tempus-night | 2 +- themes/tempus-past | 5 +- themes/tempus-rift | 2 +- themes/tempus-spring | 2 +- themes/tempus-summer | 2 +- themes/tempus-tempest | 2 +- themes/tempus-totus | 5 +- themes/tempus-warp | 2 +- themes/tempus-winter | 2 +- themes/tokyonight-light | 5 +- themes/tokyonight-night | 2 +- themes/tokyonight-storm | 2 +- themes/visibone | 2 +- themes/xterm | 2 +- themes/zenburn | 2 +- 103 files changed, 542 insertions(+), 298 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70ab43b3..ae9feb54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,8 @@ `foot` and `footclient`), allowing you to set a custom toplevel tag. The compositor must implement the new `xdg-toplevel-tag-v1` Wayland protocol ([#2212][2212]). +* `[colors-dark]` section to `foot.ini`. Replaces `[colors]`. +* `[colors-light]` section to `foot.ini`. Replaces `[colors2]`. [2212]: https://codeberg.org/dnkl/foot/issues/2212 @@ -93,6 +95,11 @@ ### Deprecated + +* `[colors]` section in `foot.ini`. Use `[colors-dark]` instead. +* `[colors2]` section in `foot.ini`. Use `[colors-light]` instead. + + ### Removed ### Fixed diff --git a/config.c b/config.c index 515b088c..0340d418 100644 --- a/config.c +++ b/config.c @@ -144,6 +144,8 @@ static const char *const binding_action_map[] = { [BIND_ACTION_REGEX_COPY] = "regex-copy", [BIND_ACTION_THEME_SWITCH_1] = "color-theme-switch-1", [BIND_ACTION_THEME_SWITCH_2] = "color-theme-switch-2", + [BIND_ACTION_THEME_SWITCH_DARK] = "color-theme-switch-dark", + [BIND_ACTION_THEME_SWITCH_LIGHT] = "color-theme-switch-light", [BIND_ACTION_THEME_TOGGLE] = "color-theme-toggle", /* Mouse-specific actions */ @@ -1118,8 +1120,40 @@ parse_section_main(struct context *ctx) sizeof(conf->initial_color_theme) == sizeof(int), "enum is not 32-bit"); - return value_to_enum(ctx, (const char*[]){"1", "2", NULL}, - (int *)&conf->initial_color_theme); + 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")) @@ -1555,16 +1589,44 @@ parse_color_theme(struct context *ctx, struct color_theme *theme) 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) { - return parse_color_theme(ctx, &ctx->conf->colors); + 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) { - return parse_color_theme(ctx, &ctx->conf->colors2); + 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 @@ -1610,14 +1672,14 @@ parse_section_cursor(struct context *ctx) if (!value_to_two_colors( ctx, - &conf->colors.cursor.text, - &conf->colors.cursor.cursor, + &conf->colors_dark.cursor.text, + &conf->colors_dark.cursor.cursor, false)) { return false; } - conf->colors.use_custom.cursor = true; + conf->colors_dark.use_custom.cursor = true; return true; } @@ -2268,6 +2330,29 @@ parse_key_binding_section(struct context *ctx, 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; @@ -2958,8 +3043,8 @@ enum section { SECTION_SCROLLBACK, SECTION_URL, SECTION_REGEX, - SECTION_COLORS, - SECTION_COLORS2, + SECTION_COLORS_DARK, + SECTION_COLORS_LIGHT, SECTION_CURSOR, SECTION_MOUSE, SECTION_CSD, @@ -2971,6 +3056,11 @@ enum section { SECTION_ENVIRONMENT, SECTION_TWEAK, SECTION_TOUCH, + + /* Deprecated */ + SECTION_COLORS, + SECTION_COLORS2, + SECTION_COUNT, }; @@ -2989,8 +3079,8 @@ static const struct { [SECTION_SCROLLBACK] = {&parse_section_scrollback, "scrollback"}, [SECTION_URL] = {&parse_section_url, "url"}, [SECTION_REGEX] = {&parse_section_regex, "regex", true}, - [SECTION_COLORS] = {&parse_section_colors, "colors"}, - [SECTION_COLORS2] = {&parse_section_colors2, "colors2"}, + [SECTION_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"}, @@ -3002,6 +3092,10 @@ static const struct { [SECTION_ENVIRONMENT] = {&parse_section_environment, "environment"}, [SECTION_TWEAK] = {&parse_section_tweak, "tweak"}, [SECTION_TOUCH] = {&parse_section_touch, "touch"}, + + /* Deprecated */ + [SECTION_COLORS] = {&parse_section_colors, "colors"}, + [SECTION_COLORS2] = {&parse_section_colors2, "colors2"}, }; static_assert(ALEN(section_info) == SECTION_COUNT, "section info array size mismatch"); @@ -3435,7 +3529,7 @@ config_load(struct config *conf, const char *conf_path, }, .multiplier = 3., }, - .colors = { + .colors_dark = { .fg = default_foreground, .bg = default_background, .flash = 0x7f7f00, @@ -3455,7 +3549,7 @@ config_load(struct config *conf, const char *conf_path, .url = false, }, }, - .initial_color_theme = COLOR_THEME1, + .initial_color_theme = COLOR_THEME_DARK, .cursor = { .style = CURSOR_BLOCK, .unfocused_style = CURSOR_UNFOCUSED_HOLLOW, @@ -3535,10 +3629,10 @@ config_load(struct config *conf, const char *conf_path, .notifications = tll_init(), }; - memcpy(conf->colors.table, default_color_table, sizeof(default_color_table)); - memcpy(conf->colors.sixel, default_sixel_colors, sizeof(default_sixel_colors)); - memcpy(&conf->colors2, &conf->colors, sizeof(conf->colors)); - conf->colors2.dim_blend_towards = DIM_BLEND_TOWARDS_WHITE; + 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); diff --git a/config.h b/config.h index fc5e290e..9ca47753 100644 --- a/config.h +++ b/config.h @@ -195,8 +195,10 @@ struct color_theme { }; enum which_color_theme { - COLOR_THEME1, - COLOR_THEME2, + COLOR_THEME_DARK, + COLOR_THEME_LIGHT, + COLOR_THEME_1, /* Deprecated */ + COLOR_THEME_2, /* Deprecated */ }; enum shm_bit_depth { @@ -327,8 +329,8 @@ struct config { tll(struct custom_regex) custom_regexes; - struct color_theme colors; - struct color_theme colors2; + struct color_theme colors_dark; + struct color_theme colors_light; enum which_color_theme initial_color_theme; struct { diff --git a/csi.c b/csi.c index c5f616ac..7e0cf464 100644 --- a/csi.c +++ b/csi.c @@ -1578,7 +1578,7 @@ csi_dispatch(struct terminal *term, uint8_t final) int chars = snprintf( reply, sizeof(reply), "\033[?997;%dn", - term->colors.active_theme == COLOR_THEME1 ? 1 : 2); + term->colors.active_theme == COLOR_THEME_DARK ? 1 : 2); term_to_slave(term, reply, chars); break; diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 60ba622b..7058e96f 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -695,8 +695,8 @@ variables to unset may be defined in *foot.ini*(5). The following signals have special meaning in foot: -- SIGUSR1: switch to color theme 1 (i.e. use the *[colors]* section). -- SIGUSR2: switch to color theme 2 (i.e. use the *[colors2]* section). +- SIGUSR1: switch to the dark color theme (*[colors-dark]*). +- SIGUSR2: switch to the light color theme (*[colors-light]*). Note: you can send SIGUSR1/SIGUSR2 to a *foot --server* process too, in which case all client instances will switch theme. Furthermore, all diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index c9782895..8bff9629 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -24,7 +24,7 @@ commented out will usually be installed to */etc/xdg/foot/foot.ini*. Options are set using KEY=VALUE pairs: - *\[colors\]*++ + *\[colors-dark\]*++ *background=000000*++ *foreground=ffffff* @@ -371,12 +371,12 @@ empty string to be set, but it must be quoted: *KEY=""*) Default: _yes_ *initial-color-theme* - Selects which color theme to use, *1*, or *2*. + Selects which color theme to use, *dark*, or *light*. - *1* uses the colors defined in the *colors* section, while *2* - uses the colors from the *colors2* section. + *dark* uses the colors defined in the *colors-dark* section, while + *light* uses the colors from the *colors-light* section. - Use the *color-theme-switch-1*, *color-theme-switch-2* and + Use the *color-theme-switch-dark*, *color-theme-switch-light* and *color-theme-toggle* key bindings to switch between the two themes at runtime, or send SIGUSR1/SIGUSR2 to the foot process (see *foot*(1) for details). @@ -987,19 +987,24 @@ applications can change these at runtime. Default: _400_. -# SECTION: colors +# SECTION: colors-dark, colors-light -This section controls the 16 ANSI colors, the default foreground and -background colors, and the extended 256 color palette. Note that +These two sections controls the 16 ANSI colors, the default foreground +and background colors, and the extended 256 color palette. Note that applications can change these at runtime. The colors are in RRGGBB format (i.e. plain old 6-digit hex values, without prefix). That is, they do *not* have an alpha component. You can configure the background transparency with the _alpha_ option. -In the context of private mode 2031 (Dark and Light Mode detection), -the primary theme (i.e. the *colors* section) is considered to be the -dark theme (since the default theme is dark). +*colors-dark* is intended to define a dark color theme, and +*colors-light* is intended to define a light color theme. You can +switch between them using the *color-theme-switch-dark*, +*color-theme-switch-light* and *color-theme-toggle* key bindings, or +by sending SIGUSR1/SIGUSR2 to the foot process. + +The default theme used is *colors-dark*, unless +*initial-color-theme=light* has been set. *cursor* Two space separated RRGGBB values (i.e. plain old 6-digit hex @@ -1098,7 +1103,7 @@ dark theme (since the default theme is dark). black makes the text darker, while blending towards white makes it whiter (but still dimmer than normal text). - Default: _black_ (*colors*), _white_ (*colors2*) + Default: _black_ (*colors-dark*), _white_ (*colors-light*) *selection-foreground*, *selection-background* Foreground (text) and background color to use in selected @@ -1135,20 +1140,6 @@ dark theme (since the default theme is dark). Flash translucency. A value in the range 0.0-1.0, where 0.0 means completely transparent, and 1.0 is opaque. Default: _0.5_. -# SECTION: colors2 - -This section defines an alternative color theme. It has the exact same -keys as the *colors* section. The default values are the same, except -for *dim-blend-towards*, which defaults to *white* instead. - -Note that values are not inherited. That is, if you set a value in -*colors*, but not in *colors2*, the value from *colors* is not -inherited by *colors2*. - -In the context of private mode 2031 (Dark and Light Mode detection), -the alternative theme (i.e. the *colors2* section) is considered to be -the light theme (since the default, the primary theme, is dark). - # SECTION: csd This section controls the look of the _CSDs_ (Client Side @@ -1455,16 +1446,16 @@ e.g. *search-start=none*. Default: _Control+Shift+u_. -*color-theme-switch-1*, *color-theme-switch-2*, *color-theme-toggle* - Switch between the primary color theme (defined in the *colors* - section), and the alternative color theme (defined in the - *colors2* section). +*color-theme-switch-dark*, *color-theme-switch-dark*, *color-theme-toggle* + Switch between the dark color theme (defined in the *colors-dark* + section), and the light color theme (defined in the *colors-light* + section). - *color-theme-switch-1* applies the primary color theme regardless + *color-theme-switch-dark* applies the dark color theme regardless of which color theme is currently active. - *color-theme-switch-2* applies the alternative color theme regardless - of which color theme is currently active. + *color-theme-switch-light* applies the light color theme + regardless of which color theme is currently active. *color-theme-toggle* toggles between the primary and alternative color themes. diff --git a/doc/footclient.1.scd b/doc/footclient.1.scd index edf3e9f3..ad865913 100644 --- a/doc/footclient.1.scd +++ b/doc/footclient.1.scd @@ -198,8 +198,8 @@ variables to unset may be defined in *foot.ini*(5). The following signals have special meaning in footclient: -- SIGUSR1: switch to color theme 1 (i.e. use the *[colors]* section). -- SIGUSR2: switch to color theme 2 (i.e. use the *[colors2]* section). +- SIGUSR1: switch to the dark color theme (*[colors-dark]*). +- SIGUSR2: switch to the light color theme (*[colors-light]*). When sending SIGUSR1/SIGUSR2 to a footclient instance, the theme is changed in that instance only. This is different from when you send diff --git a/foot.ini b/foot.ini index 2d170489..a9b4b83d 100644 --- a/foot.ini +++ b/foot.ini @@ -24,7 +24,7 @@ # dpi-aware=no # gamma-correct-blending=no -# initial-color-theme=1 +# initial-color-theme=dark # initial-window-size-pixels=700x500 # Or, # initial-window-size-chars= # initial-window-mode=windowed @@ -101,7 +101,7 @@ [touch] # long-press-delay=400 -[colors] +[colors-dark] # alpha=1.0 # alpha-mode=default # Can be `default`, `matching` or `all` # background=242424 @@ -169,7 +169,7 @@ # search-box-match= # black-on-yellow # urls= -[colors2] +[colors-light] # Alternative color theme, see man page foot.ini(5) # Same builtin defaults as [color], except for: # dim-blend-towards=white diff --git a/input.c b/input.c index 44a99e3b..80b028ac 100644 --- a/input.c +++ b/input.c @@ -486,11 +486,13 @@ execute_binding(struct seat *seat, struct terminal *term, return true; case BIND_ACTION_THEME_SWITCH_1: - term_theme_switch_to_1(term); + case BIND_ACTION_THEME_SWITCH_DARK: + term_theme_switch_to_dark(term); return true; case BIND_ACTION_THEME_SWITCH_2: - term_theme_switch_to_2(term); + case BIND_ACTION_THEME_SWITCH_LIGHT: + term_theme_switch_to_light(term); return true; case BIND_ACTION_THEME_TOGGLE: diff --git a/key-binding.h b/key-binding.h index 5f0c1f1e..c4a04e99 100644 --- a/key-binding.h +++ b/key-binding.h @@ -45,6 +45,8 @@ enum bind_action_normal { BIND_ACTION_REGEX_COPY, BIND_ACTION_THEME_SWITCH_1, BIND_ACTION_THEME_SWITCH_2, + BIND_ACTION_THEME_SWITCH_DARK, + BIND_ACTION_THEME_SWITCH_LIGHT, BIND_ACTION_THEME_TOGGLE, /* Mouse specific actions - i.e. they require a mouse coordinate */ diff --git a/main.c b/main.c index b933e1c1..9db77d0c 100644 --- a/main.c +++ b/main.c @@ -59,14 +59,14 @@ fdm_sigusr(struct fdm *fdm, int signo, void *data) if (ctx->server != NULL) { if (signo == SIGUSR1) - server_global_theme_switch_to_1(ctx->server); + server_global_theme_switch_to_dark(ctx->server); else - server_global_theme_switch_to_2(ctx->server); + server_global_theme_switch_to_light(ctx->server); } else { if (signo == SIGUSR1) - term_theme_switch_to_1(ctx->term); + term_theme_switch_to_dark(ctx->term); else - term_theme_switch_to_2(ctx->term); + term_theme_switch_to_light(ctx->term); } return true; diff --git a/osc.c b/osc.c index 375eae5c..9407e7b8 100644 --- a/osc.c +++ b/osc.c @@ -1459,9 +1459,9 @@ osc_dispatch(struct terminal *term) case 11: term->colors.bg = color; if (!have_alpha) { - alpha = term->colors.active_theme == COLOR_THEME1 - ? term->conf->colors.alpha - : term->conf->colors2.alpha; + alpha = term->colors.active_theme == COLOR_THEME_DARK + ? term->conf->colors_dark.alpha + : term->conf->colors_light.alpha; } const bool changed = term->colors.alpha != alpha; @@ -1516,9 +1516,9 @@ osc_dispatch(struct terminal *term) /* Reset Color Number 'c' (whole table if no parameter) */ const struct color_theme *theme = - term->colors.active_theme == COLOR_THEME1 - ? &term->conf->colors - : &term->conf->colors2; + term->colors.active_theme == COLOR_THEME_DARK + ? &term->conf->colors_dark + : &term->conf->colors_light; if (string[0] == '\0') { LOG_DBG("resetting all colors"); @@ -1559,9 +1559,9 @@ osc_dispatch(struct terminal *term) LOG_DBG("resetting foreground color"); const struct color_theme *theme = - term->colors.active_theme == COLOR_THEME1 - ? &term->conf->colors - : &term->conf->colors2; + term->colors.active_theme == COLOR_THEME_DARK + ? &term->conf->colors_dark + : &term->conf->colors_light; term->colors.fg = theme->fg; term_damage_color(term, COLOR_DEFAULT, 0); @@ -1571,9 +1571,9 @@ osc_dispatch(struct terminal *term) LOG_DBG("resetting background color"); const struct color_theme *theme = - term->colors.active_theme == COLOR_THEME1 - ? &term->conf->colors - : &term->conf->colors2; + term->colors.active_theme == COLOR_THEME_DARK + ? &term->conf->colors_dark + : &term->conf->colors_light; bool alpha_changed = term->colors.alpha != theme->alpha; @@ -1594,14 +1594,14 @@ osc_dispatch(struct terminal *term) LOG_DBG("resetting cursor color"); const struct color_theme *theme = - term->colors.active_theme == COLOR_THEME1 - ? &term->conf->colors - : &term->conf->colors2; + term->colors.active_theme == COLOR_THEME_DARK + ? &term->conf->colors_dark + : &term->conf->colors_light; term->colors.cursor_fg = theme->cursor.text; term->colors.cursor_bg = theme->cursor.cursor; - if (term->conf->colors.use_custom.cursor) { + if (term->conf->colors_dark.use_custom.cursor) { term->colors.cursor_fg |= 1u << 31; term->colors.cursor_bg |= 1u << 31; } @@ -1614,9 +1614,9 @@ osc_dispatch(struct terminal *term) LOG_DBG("resetting selection background color"); const struct color_theme *theme = - term->colors.active_theme == COLOR_THEME1 - ? &term->conf->colors - : &term->conf->colors2; + term->colors.active_theme == COLOR_THEME_DARK + ? &term->conf->colors_dark + : &term->conf->colors_light; term->colors.selection_bg = theme->selection_bg; break; @@ -1626,9 +1626,9 @@ osc_dispatch(struct terminal *term) LOG_DBG("resetting selection foreground color"); const struct color_theme *theme = - term->colors.active_theme == COLOR_THEME1 - ? &term->conf->colors - : &term->conf->colors2; + term->colors.active_theme == COLOR_THEME_DARK + ? &term->conf->colors_dark + : &term->conf->colors_light; term->colors.selection_fg = theme->selection_fg; break; diff --git a/render.c b/render.c index 1d0f08af..ac8ece37 100644 --- a/render.c +++ b/render.c @@ -293,7 +293,7 @@ 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.use_custom.dim; + const uint8_t custom_dim = conf->colors_dark.use_custom.dim; if (unlikely(custom_dim != 0)) { for (size_t i = 0; i < 8; i++) { @@ -302,7 +302,7 @@ color_dim(const struct terminal *term, uint32_t color) if (term->colors.table[0 + i] == color) { /* "Regular" color, return the corresponding "dim" */ - return conf->colors.dim[i]; + return conf->colors_dark.dim[i]; } else if (term->colors.table[8 + i] == color) { @@ -312,9 +312,9 @@ color_dim(const struct terminal *term, uint32_t color) } } - const struct color_theme *theme = term->colors.active_theme == COLOR_THEME1 - ? &conf->colors - : &conf->colors2; + const struct color_theme *theme = term->colors.active_theme == COLOR_THEME_DARK + ? &conf->colors_dark + : &conf->colors_light; return color_blend_towards( color, @@ -776,7 +776,7 @@ render_cell(struct terminal *term, pixman_image_t *pix, } else if (!term->window->is_fullscreen && term->colors.alpha != 0xffff) { - switch (term->conf->colors.alpha_mode) { + switch (term->conf->colors_dark.alpha_mode) { case ALPHA_MODE_DEFAULT: { if (cell->attrs.bg_src == COLOR_DEFAULT) { alpha = term->colors.alpha; @@ -1175,8 +1175,8 @@ render_cell(struct terminal *term, pixman_image_t *pix, if (unlikely(cell->attrs.url)) { pixman_color_t url_color = color_hex_to_pixman( - term->conf->colors.use_custom.url - ? term->conf->colors.url + term->conf->colors_dark.use_custom.url + ? term->conf->colors_dark.url : term->colors.table[3], gamma_correct); draw_underline(term, pix, font, &url_color, x, y, cell_cols); @@ -1991,8 +1991,8 @@ render_overlay(struct terminal *term) case OVERLAY_FLASH: color = color_hex_to_pixman_with_alpha( - term->conf->colors.flash, - term->conf->colors.flash_alpha, + term->conf->colors_dark.flash, + term->conf->colors_dark.flash_alpha, wayl_do_linear_blending(term->wl, term->conf)); break; @@ -2510,10 +2510,10 @@ render_csd_title(struct terminal *term, const struct csd_data *info, uint32_t bg = term->conf->csd.color.title_set ? term->conf->csd.color.title - : 0xffu << 24 | term->conf->colors.fg; + : 0xffu << 24 | term->conf->colors_dark.fg; uint32_t fg = term->conf->csd.color.buttons_set ? term->conf->csd.color.buttons - : term->conf->colors.bg; + : term->conf->colors_dark.bg; if (!term->visual_focus) { bg = color_dim(term, bg); @@ -2607,7 +2607,7 @@ render_csd_border(struct terminal *term, enum csd_surface surf_idx, 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.fg; + 0xffu << 24 | term->conf->colors_dark.fg; if (!term->visual_focus) _color = color_dim(term, _color); @@ -2627,7 +2627,7 @@ static pixman_color_t get_csd_button_fg_color(const struct terminal *term) { const struct config *conf = term->conf; - uint32_t _color = conf->colors.bg; + uint32_t _color = conf->colors_dark.bg; uint16_t alpha = 0xffff; if (conf->csd.color.buttons_set) { @@ -2872,7 +2872,7 @@ render_csd_button(struct terminal *term, enum csd_surface surf_idx, switch (surf_idx) { case CSD_SURF_MINIMIZE: - _color = term->conf->colors.table[4]; /* blue */ + _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 && @@ -2880,7 +2880,7 @@ render_csd_button(struct terminal *term, enum csd_surface surf_idx, break; case CSD_SURF_MAXIMIZE: - _color = term->conf->colors.table[2]; /* green */ + _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 && @@ -2888,7 +2888,7 @@ render_csd_button(struct terminal *term, enum csd_surface surf_idx, break; case CSD_SURF_CLOSE: - _color = term->conf->colors.table[1]; /* red */ + _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 && @@ -3117,9 +3117,9 @@ render_scrollback_position(struct terminal *term) uint32_t fg = term->colors.table[0]; uint32_t bg = term->colors.table[8 + 4]; - if (term->conf->colors.use_custom.scrollback_indicator) { - fg = term->conf->colors.scrollback_indicator.fg; - bg = term->conf->colors.scrollback_indicator.bg; + 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( @@ -3799,18 +3799,18 @@ render_search_box(struct terminal *term) const bool is_match = term->search.match_len == text_len; const bool custom_colors = is_match - ? term->conf->colors.use_custom.search_box_match - : term->conf->colors.use_custom.search_box_no_match; + ? term->conf->colors_dark.use_custom.search_box_match + : term->conf->colors_dark.use_custom.search_box_no_match; /* Background - yellow on empty/match, red on mismatch (default) */ const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); const pixman_color_t color = color_hex_to_pixman( is_match ? (custom_colors - ? term->conf->colors.search_box.match.bg + ? term->conf->colors_dark.search_box.match.bg : term->colors.table[3]) : (custom_colors - ? term->conf->colors.search_box.no_match.bg + ? term->conf->colors_dark.search_box.no_match.bg : term->colors.table[1]), gamma_correct); @@ -3832,8 +3832,8 @@ render_search_box(struct terminal *term) pixman_color_t fg = color_hex_to_pixman( custom_colors ? (is_match - ? term->conf->colors.search_box.match.fg - : term->conf->colors.search_box.no_match.fg) + ? term->conf->colors_dark.search_box.match.fg + : term->conf->colors_dark.search_box.no_match.fg) : term->colors.table[0], gamma_correct); @@ -4254,11 +4254,11 @@ render_urls(struct terminal *term) struct buffer *bufs[render_count]; shm_get_many(chain, render_count, widths, heights, bufs, false); - uint32_t fg = term->conf->colors.use_custom.jump_label - ? term->conf->colors.jump_label.fg + 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.use_custom.jump_label - ? term->conf->colors.jump_label.bg + 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++) { diff --git a/server.c b/server.c index 6b3e5094..25963325 100644 --- a/server.c +++ b/server.c @@ -182,11 +182,11 @@ fdm_client(struct fdm *fdm, int fd, int events, void *data) switch (sigusr.signo) { case SIGUSR1: - term_theme_switch_to_1(client->instance->terminal); + term_theme_switch_to_dark(client->instance->terminal); break; case SIGUSR2: - term_theme_switch_to_2(client->instance->terminal); + term_theme_switch_to_light(client->instance->terminal); break; default: @@ -670,21 +670,21 @@ server_destroy(struct server *server) } void -server_global_theme_switch_to_1(struct server *server) +server_global_theme_switch_to_dark(struct server *server) { - server->conf->initial_color_theme = COLOR_THEME1; + server->conf->initial_color_theme = COLOR_THEME_DARK; tll_foreach(server->clients, it) - term_theme_switch_to_1(it->item->instance->terminal); + term_theme_switch_to_dark(it->item->instance->terminal); tll_foreach(server->terminals, it) - term_theme_switch_to_1(it->item->terminal); + term_theme_switch_to_dark(it->item->terminal); } void -server_global_theme_switch_to_2(struct server *server) +server_global_theme_switch_to_light(struct server *server) { - server->conf->initial_color_theme = COLOR_THEME2; + server->conf->initial_color_theme = COLOR_THEME_LIGHT; tll_foreach(server->clients, it) - term_theme_switch_to_2(it->item->instance->terminal); + term_theme_switch_to_light(it->item->instance->terminal); tll_foreach(server->terminals, it) - term_theme_switch_to_2(it->item->terminal); + term_theme_switch_to_light(it->item->terminal); } diff --git a/server.h b/server.h index 6adfe7c6..683ad74d 100644 --- a/server.h +++ b/server.h @@ -10,5 +10,5 @@ struct server *server_init(struct config *conf, struct fdm *fdm, struct reaper *reaper, struct wayland *wayl); void server_destroy(struct server *server); -void server_global_theme_switch_to_1(struct server *server); -void server_global_theme_switch_to_2(struct server *server); +void server_global_theme_switch_to_dark(struct server *server); +void server_global_theme_switch_to_light(struct server *server); diff --git a/sixel.c b/sixel.c index c5ef01a1..07b97f46 100644 --- a/sixel.c +++ b/sixel.c @@ -137,7 +137,7 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) } const size_t active_palette_entries = min( - ALEN(term->conf->colors.sixel), term->sixel.palette_size); + ALEN(term->conf->colors_dark.sixel), term->sixel.palette_size); if (term->sixel.use_private_palette) { xassert(term->sixel.private_palette == NULL); @@ -145,7 +145,7 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) term->sixel.palette_size, sizeof(term->sixel.private_palette[0])); memcpy( - term->sixel.private_palette, term->conf->colors.sixel, + term->sixel.private_palette, term->conf->colors_dark.sixel, active_palette_entries * sizeof(term->sixel.private_palette[0])); if (term->sixel.linear_blending || term->sixel.use_10bit) { @@ -164,7 +164,7 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) term->sixel.palette_size, sizeof(term->sixel.shared_palette[0])); memcpy( - term->sixel.shared_palette, term->conf->colors.sixel, + term->sixel.shared_palette, term->conf->colors_dark.sixel, active_palette_entries * sizeof(term->sixel.shared_palette[0])); if (term->sixel.linear_blending || term->sixel.use_10bit) { diff --git a/terminal.c b/terminal.c index 3749416b..b670d606 100644 --- a/terminal.c +++ b/terminal.c @@ -1271,8 +1271,10 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, const struct color_theme *theme = NULL; switch (conf->initial_color_theme) { - case COLOR_THEME1: theme = &conf->colors; break; - case COLOR_THEME2: theme = &conf->colors2; break; + 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 */ @@ -2177,8 +2179,10 @@ term_reset(struct terminal *term, bool hard) const struct color_theme *theme = NULL; switch (term->conf->initial_color_theme) { - case COLOR_THEME1: theme = &term->conf->colors; break; - case COLOR_THEME2: theme = &term->conf->colors2; break; + 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; @@ -4742,13 +4746,13 @@ term_send_size_notification(struct terminal *term) } void -term_theme_switch_to_1(struct terminal *term) +term_theme_switch_to_dark(struct terminal *term) { - if (term->colors.active_theme == COLOR_THEME1) + if (term->colors.active_theme == COLOR_THEME_DARK) return; - term_theme_apply(term, &term->conf->colors); - term->colors.active_theme = COLOR_THEME1; + 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); @@ -4762,13 +4766,13 @@ term_theme_switch_to_1(struct terminal *term) } void -term_theme_switch_to_2(struct terminal *term) +term_theme_switch_to_light(struct terminal *term) { - if (term->colors.active_theme == COLOR_THEME2) + if (term->colors.active_theme == COLOR_THEME_LIGHT) return; - term_theme_apply(term, &term->conf->colors2); - term->colors.active_theme = COLOR_THEME2; + 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); @@ -4784,15 +4788,15 @@ term_theme_switch_to_2(struct terminal *term) void term_theme_toggle(struct terminal *term) { - if (term->colors.active_theme == COLOR_THEME1) { - term_theme_apply(term, &term->conf->colors2); - term->colors.active_theme = COLOR_THEME2; + 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); - term->colors.active_theme = COLOR_THEME1; + 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); diff --git a/terminal.h b/terminal.h index 364d57b3..fe39341d 100644 --- a/terminal.h +++ b/terminal.h @@ -994,8 +994,8 @@ void term_enable_size_notifications(struct terminal *term); void term_disable_size_notifications(struct terminal *term); void term_send_size_notification(struct terminal *term); -void term_theme_switch_to_1(struct terminal *term); -void term_theme_switch_to_2(struct terminal *term); +void term_theme_switch_to_dark(struct terminal *term); +void term_theme_switch_to_light(struct terminal *term); void term_theme_toggle(struct terminal *term); static inline void term_reset_grapheme_state(struct terminal *term) diff --git a/tests/test-config.c b/tests/test-config.c index 268733db..f83a9beb 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -521,6 +521,14 @@ test_section_main(void) (int []){STARTUP_WINDOWED, STARTUP_MAXIMIZED, STARTUP_FULLSCREEN}, (int *)&conf.startup_mode); + test_enum( + &ctx, &parse_section_main, "initial-color-theme", + 2, + (const char *[]){"dark", "light", "1", "2"}, + (int []){COLOR_THEME_DARK, COLOR_THEME_LIGHT, + COLOR_THEME_DARK, COLOR_THEME_LIGHT}, + (int *)&conf.initial_color_theme); + /* TODO: font (custom) */ /* TODO: include (custom) */ /* TODO: bold-text-in-bright (enum/boolean) */ @@ -695,78 +703,157 @@ test_section_touch(void) } static void -test_section_colors(void) +test_section_colors_dark(void) { struct config conf = {0}; struct context ctx = { - .conf = &conf, .section = "colors", .path = "unittest"}; + .conf = &conf, .section = "colors-dark", .path = "unittest"}; test_invalid_key(&ctx, &parse_section_colors, "invalid-key"); - test_color(&ctx, &parse_section_colors, "foreground", false, &conf.colors.fg); - test_color(&ctx, &parse_section_colors, "background", false, &conf.colors.bg); - test_color(&ctx, &parse_section_colors, "regular0", false, &conf.colors.table[0]); - test_color(&ctx, &parse_section_colors, "regular1", false, &conf.colors.table[1]); - test_color(&ctx, &parse_section_colors, "regular2", false, &conf.colors.table[2]); - test_color(&ctx, &parse_section_colors, "regular3", false, &conf.colors.table[3]); - test_color(&ctx, &parse_section_colors, "regular4", false, &conf.colors.table[4]); - test_color(&ctx, &parse_section_colors, "regular5", false, &conf.colors.table[5]); - test_color(&ctx, &parse_section_colors, "regular6", false, &conf.colors.table[6]); - test_color(&ctx, &parse_section_colors, "regular7", false, &conf.colors.table[7]); - test_color(&ctx, &parse_section_colors, "bright0", false, &conf.colors.table[8]); - test_color(&ctx, &parse_section_colors, "bright1", false, &conf.colors.table[9]); - test_color(&ctx, &parse_section_colors, "bright2", false, &conf.colors.table[10]); - test_color(&ctx, &parse_section_colors, "bright3", false, &conf.colors.table[11]); - test_color(&ctx, &parse_section_colors, "bright4", false, &conf.colors.table[12]); - test_color(&ctx, &parse_section_colors, "bright5", false, &conf.colors.table[13]); - test_color(&ctx, &parse_section_colors, "bright6", false, &conf.colors.table[14]); - test_color(&ctx, &parse_section_colors, "bright7", false, &conf.colors.table[15]); - test_color(&ctx, &parse_section_colors, "dim0", false, &conf.colors.dim[0]); - test_color(&ctx, &parse_section_colors, "dim1", false, &conf.colors.dim[1]); - test_color(&ctx, &parse_section_colors, "dim2", false, &conf.colors.dim[2]); - test_color(&ctx, &parse_section_colors, "dim3", false, &conf.colors.dim[3]); - test_color(&ctx, &parse_section_colors, "dim4", false, &conf.colors.dim[4]); - test_color(&ctx, &parse_section_colors, "dim5", false, &conf.colors.dim[5]); - test_color(&ctx, &parse_section_colors, "dim6", false, &conf.colors.dim[6]); - test_color(&ctx, &parse_section_colors, "dim7", false, &conf.colors.dim[7]); - test_color(&ctx, &parse_section_colors, "selection-foreground", false, &conf.colors.selection_fg); - test_color(&ctx, &parse_section_colors, "selection-background", false, &conf.colors.selection_bg); - test_color(&ctx, &parse_section_colors, "urls", false, &conf.colors.url); - test_two_colors(&ctx, &parse_section_colors, "jump-labels", false, - &conf.colors.jump_label.fg, - &conf.colors.jump_label.bg); - test_two_colors(&ctx, &parse_section_colors, "scrollback-indicator", false, - &conf.colors.scrollback_indicator.fg, - &conf.colors.scrollback_indicator.bg); - test_two_colors(&ctx, &parse_section_colors, "search-box-no-match", false, - &conf.colors.search_box.no_match.fg, - &conf.colors.search_box.no_match.bg); - test_two_colors(&ctx, &parse_section_colors, "search-box-match", false, - &conf.colors.search_box.match.fg, - &conf.colors.search_box.match.bg); + test_color(&ctx, &parse_section_colors_dark, "foreground", false, &conf.colors_dark.fg); + test_color(&ctx, &parse_section_colors_dark, "background", false, &conf.colors_dark.bg); + test_color(&ctx, &parse_section_colors_dark, "regular0", false, &conf.colors_dark.table[0]); + test_color(&ctx, &parse_section_colors_dark, "regular1", false, &conf.colors_dark.table[1]); + test_color(&ctx, &parse_section_colors_dark, "regular2", false, &conf.colors_dark.table[2]); + test_color(&ctx, &parse_section_colors_dark, "regular3", false, &conf.colors_dark.table[3]); + test_color(&ctx, &parse_section_colors_dark, "regular4", false, &conf.colors_dark.table[4]); + test_color(&ctx, &parse_section_colors_dark, "regular5", false, &conf.colors_dark.table[5]); + test_color(&ctx, &parse_section_colors_dark, "regular6", false, &conf.colors_dark.table[6]); + test_color(&ctx, &parse_section_colors_dark, "regular7", false, &conf.colors_dark.table[7]); + test_color(&ctx, &parse_section_colors_dark, "bright0", false, &conf.colors_dark.table[8]); + test_color(&ctx, &parse_section_colors_dark, "bright1", false, &conf.colors_dark.table[9]); + test_color(&ctx, &parse_section_colors_dark, "bright2", false, &conf.colors_dark.table[10]); + test_color(&ctx, &parse_section_colors_dark, "bright3", false, &conf.colors_dark.table[11]); + test_color(&ctx, &parse_section_colors_dark, "bright4", false, &conf.colors_dark.table[12]); + test_color(&ctx, &parse_section_colors_dark, "bright5", false, &conf.colors_dark.table[13]); + test_color(&ctx, &parse_section_colors_dark, "bright6", false, &conf.colors_dark.table[14]); + test_color(&ctx, &parse_section_colors_dark, "bright7", false, &conf.colors_dark.table[15]); + test_color(&ctx, &parse_section_colors_dark, "dim0", false, &conf.colors_dark.dim[0]); + test_color(&ctx, &parse_section_colors_dark, "dim1", false, &conf.colors_dark.dim[1]); + test_color(&ctx, &parse_section_colors_dark, "dim2", false, &conf.colors_dark.dim[2]); + test_color(&ctx, &parse_section_colors_dark, "dim3", false, &conf.colors_dark.dim[3]); + test_color(&ctx, &parse_section_colors_dark, "dim4", false, &conf.colors_dark.dim[4]); + test_color(&ctx, &parse_section_colors_dark, "dim5", false, &conf.colors_dark.dim[5]); + test_color(&ctx, &parse_section_colors_dark, "dim6", false, &conf.colors_dark.dim[6]); + test_color(&ctx, &parse_section_colors_dark, "dim7", false, &conf.colors_dark.dim[7]); + test_color(&ctx, &parse_section_colors_dark, "selection-foreground", false, &conf.colors_dark.selection_fg); + test_color(&ctx, &parse_section_colors_dark, "selection-background", false, &conf.colors_dark.selection_bg); + test_color(&ctx, &parse_section_colors_dark, "urls", false, &conf.colors_dark.url); + test_two_colors(&ctx, &parse_section_colors_dark, "jump-labels", false, + &conf.colors_dark.jump_label.fg, + &conf.colors_dark.jump_label.bg); + test_two_colors(&ctx, &parse_section_colors_dark, "scrollback-indicator", false, + &conf.colors_dark.scrollback_indicator.fg, + &conf.colors_dark.scrollback_indicator.bg); + test_two_colors(&ctx, &parse_section_colors_dark, "search-box-no-match", false, + &conf.colors_dark.search_box.no_match.fg, + &conf.colors_dark.search_box.no_match.bg); + test_two_colors(&ctx, &parse_section_colors_dark, "search-box-match", false, + &conf.colors_dark.search_box.match.fg, + &conf.colors_dark.search_box.match.bg); - test_two_colors(&ctx, &parse_section_colors, "cursor", false, - &conf.colors.cursor.text, - &conf.colors.cursor.cursor); + test_two_colors(&ctx, &parse_section_colors_dark, "cursor", false, + &conf.colors_dark.cursor.text, + &conf.colors_dark.cursor.cursor); - test_enum(&ctx, &parse_section_colors, "alpha-mode", 3, + test_enum(&ctx, &parse_section_colors_dark, "alpha-mode", 3, (const char *[]){"default", "matching", "all"}, (int []){ALPHA_MODE_DEFAULT, ALPHA_MODE_MATCHING, ALPHA_MODE_ALL}, - (int *)&conf.colors.alpha_mode); + (int *)&conf.colors_dark.alpha_mode); - test_enum(&ctx, &parse_section_colors, "dim-blend-towards", 2, + test_enum(&ctx, &parse_section_colors_dark, "dim-blend-towards", 2, (const char *[]){"black", "white"}, (int []){DIM_BLEND_TOWARDS_BLACK, DIM_BLEND_TOWARDS_WHITE}, - (int *)&conf.colors.dim_blend_towards); + (int *)&conf.colors_dark.dim_blend_towards); for (size_t i = 0; i < 255; i++) { char key_name[4]; sprintf(key_name, "%zu", i); - test_color(&ctx, &parse_section_colors, key_name, false, - &conf.colors.table[i]); + test_color(&ctx, &parse_section_colors_dark, key_name, false, + &conf.colors_dark.table[i]); } - test_invalid_key(&ctx, &parse_section_colors, "256"); + test_invalid_key(&ctx, &parse_section_colors_dark, "256"); + + /* TODO: alpha (float in range 0-1, converted to uint16_t) */ + + config_free(&conf); +} + +static void +test_section_colors_light(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "colors-light", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_colors, "invalid-key"); + + test_color(&ctx, &parse_section_colors_light, "foreground", false, &conf.colors_light.fg); + test_color(&ctx, &parse_section_colors_light, "background", false, &conf.colors_light.bg); + test_color(&ctx, &parse_section_colors_light, "regular0", false, &conf.colors_light.table[0]); + test_color(&ctx, &parse_section_colors_light, "regular1", false, &conf.colors_light.table[1]); + test_color(&ctx, &parse_section_colors_light, "regular2", false, &conf.colors_light.table[2]); + test_color(&ctx, &parse_section_colors_light, "regular3", false, &conf.colors_light.table[3]); + test_color(&ctx, &parse_section_colors_light, "regular4", false, &conf.colors_light.table[4]); + test_color(&ctx, &parse_section_colors_light, "regular5", false, &conf.colors_light.table[5]); + test_color(&ctx, &parse_section_colors_light, "regular6", false, &conf.colors_light.table[6]); + test_color(&ctx, &parse_section_colors_light, "regular7", false, &conf.colors_light.table[7]); + test_color(&ctx, &parse_section_colors_light, "bright0", false, &conf.colors_light.table[8]); + test_color(&ctx, &parse_section_colors_light, "bright1", false, &conf.colors_light.table[9]); + test_color(&ctx, &parse_section_colors_light, "bright2", false, &conf.colors_light.table[10]); + test_color(&ctx, &parse_section_colors_light, "bright3", false, &conf.colors_light.table[11]); + test_color(&ctx, &parse_section_colors_light, "bright4", false, &conf.colors_light.table[12]); + test_color(&ctx, &parse_section_colors_light, "bright5", false, &conf.colors_light.table[13]); + test_color(&ctx, &parse_section_colors_light, "bright6", false, &conf.colors_light.table[14]); + test_color(&ctx, &parse_section_colors_light, "bright7", false, &conf.colors_light.table[15]); + test_color(&ctx, &parse_section_colors_light, "dim0", false, &conf.colors_light.dim[0]); + test_color(&ctx, &parse_section_colors_light, "dim1", false, &conf.colors_light.dim[1]); + test_color(&ctx, &parse_section_colors_light, "dim2", false, &conf.colors_light.dim[2]); + test_color(&ctx, &parse_section_colors_light, "dim3", false, &conf.colors_light.dim[3]); + test_color(&ctx, &parse_section_colors_light, "dim4", false, &conf.colors_light.dim[4]); + test_color(&ctx, &parse_section_colors_light, "dim5", false, &conf.colors_light.dim[5]); + test_color(&ctx, &parse_section_colors_light, "dim6", false, &conf.colors_light.dim[6]); + test_color(&ctx, &parse_section_colors_light, "dim7", false, &conf.colors_light.dim[7]); + test_color(&ctx, &parse_section_colors_light, "selection-foreground", false, &conf.colors_light.selection_fg); + test_color(&ctx, &parse_section_colors_light, "selection-background", false, &conf.colors_light.selection_bg); + test_color(&ctx, &parse_section_colors_light, "urls", false, &conf.colors_light.url); + test_two_colors(&ctx, &parse_section_colors_light, "jump-labels", false, + &conf.colors_light.jump_label.fg, + &conf.colors_light.jump_label.bg); + test_two_colors(&ctx, &parse_section_colors_light, "scrollback-indicator", false, + &conf.colors_light.scrollback_indicator.fg, + &conf.colors_light.scrollback_indicator.bg); + test_two_colors(&ctx, &parse_section_colors_light, "search-box-no-match", false, + &conf.colors_light.search_box.no_match.fg, + &conf.colors_light.search_box.no_match.bg); + test_two_colors(&ctx, &parse_section_colors_light, "search-box-match", false, + &conf.colors_light.search_box.match.fg, + &conf.colors_light.search_box.match.bg); + + test_two_colors(&ctx, &parse_section_colors_light, "cursor", false, + &conf.colors_light.cursor.text, + &conf.colors_light.cursor.cursor); + + test_enum(&ctx, &parse_section_colors_light, "alpha-mode", 3, + (const char *[]){"default", "matching", "all"}, + (int []){ALPHA_MODE_DEFAULT, ALPHA_MODE_MATCHING, ALPHA_MODE_ALL}, + (int *)&conf.colors_light.alpha_mode); + + test_enum(&ctx, &parse_section_colors_light, "dim-blend-towards", 2, + (const char *[]){"black", "white"}, + (int []){DIM_BLEND_TOWARDS_BLACK, DIM_BLEND_TOWARDS_WHITE}, + (int *)&conf.colors_light.dim_blend_towards); + + for (size_t i = 0; i < 255; i++) { + char key_name[4]; + sprintf(key_name, "%zu", i); + test_color(&ctx, &parse_section_colors_light, key_name, false, + &conf.colors_light.table[i]); + } + + test_invalid_key(&ctx, &parse_section_colors_light, "256"); /* TODO: alpha (float in range 0-1, converted to uint16_t) */ @@ -1444,7 +1531,8 @@ main(int argc, const char *const *argv) test_section_cursor(); test_section_mouse(); test_section_touch(); - test_section_colors(); + test_section_colors_dark(); + test_section_colors_light(); test_section_csd(); test_section_key_bindings(); test_section_key_bindings_collisions(); diff --git a/themes/aeroroot b/themes/aeroroot index 2a0e0985..dbeb2e81 100644 --- a/themes/aeroroot +++ b/themes/aeroroot @@ -1,7 +1,7 @@ # -*- conf -*- # Aero root theme -[colors] +[colors-dark] cursor=1a1a1a 9fd5f5 foreground=dedeef background=1a1a1a diff --git a/themes/alacritty b/themes/alacritty index 14503887..68d1c68c 100644 --- a/themes/alacritty +++ b/themes/alacritty @@ -1,7 +1,7 @@ # -*- conf -*- # Alacritty -[colors] +[colors-dark] cursor = 181818 56d8c9 background= 181818 foreground= d8d8d8 diff --git a/themes/apprentice b/themes/apprentice index 6b67d21d..291ab8db 100644 --- a/themes/apprentice +++ b/themes/apprentice @@ -1,7 +1,7 @@ # -*- conf -*- # https://github.com/romainl/Apprentice -[colors] +[colors-dark] cursor=262626 6c6c6c foreground=bcbcbc background=262626 diff --git a/themes/ayu-mirage b/themes/ayu-mirage index 4646e418..2d9b6b54 100644 --- a/themes/ayu-mirage +++ b/themes/ayu-mirage @@ -2,7 +2,7 @@ # theme: Ayu Mirage # description: a theme based on Ayu Mirage for Sublime Text (original: https://github.com/dempfi/ayu) -[colors] +[colors-dark] cursor = ffcc66 665a44 foreground = cccac2 background = 242936 diff --git a/themes/catppuccin-frappe b/themes/catppuccin-frappe index 44bef16c..3acae600 100644 --- a/themes/catppuccin-frappe +++ b/themes/catppuccin-frappe @@ -1,7 +1,7 @@ # _*_ conf _*_ # Catppuccin Frappe -[colors] +[colors-dark] foreground=c6d0f5 background=303446 diff --git a/themes/catppuccin-latte b/themes/catppuccin-latte index d0b90e64..ca7a7aae 100644 --- a/themes/catppuccin-latte +++ b/themes/catppuccin-latte @@ -1,7 +1,10 @@ # _*_ conf _*_ # Catppuccin Latte -[colors] +[main] +initial-color-theme=light + +[colors-light] foreground=4c4f69 background=eff1f5 diff --git a/themes/catppuccin-macchiato b/themes/catppuccin-macchiato index ae8adab8..8f5ea36e 100644 --- a/themes/catppuccin-macchiato +++ b/themes/catppuccin-macchiato @@ -1,7 +1,7 @@ # _*_ conf _*_ # Catppuccin Macchiato -[colors] +[colors-dark] foreground=cad3f5 background=24273a diff --git a/themes/catppuccin-mocha b/themes/catppuccin-mocha index d29eb0ec..7d98dc0f 100644 --- a/themes/catppuccin-mocha +++ b/themes/catppuccin-mocha @@ -1,7 +1,7 @@ # _*_ conf _*_ # Catppuccin Mocha -[colors] +[colors-dark] foreground=cdd6f4 background=1e1e2e diff --git a/themes/chiba-dark b/themes/chiba-dark index 8727f684..ffaf6cb2 100644 --- a/themes/chiba-dark +++ b/themes/chiba-dark @@ -3,7 +3,7 @@ # author: ayushnix (https://sr.ht/~ayushnix) # description: A dark theme with bright cyberpunk colors (WCAG AAA compliant) -[colors] +[colors-dark] cursor = 181818 cdcdcd foreground = cdcdcd background = 181818 diff --git a/themes/derp b/themes/derp index 45eed752..42af3377 100644 --- a/themes/derp +++ b/themes/derp @@ -1,7 +1,7 @@ # -*- conf -*- # Derp -[colors] +[colors-dark] cursor=000000 ffffff foreground=ffffff background=000000 diff --git a/themes/deus b/themes/deus index 0d52e55b..69c44944 100644 --- a/themes/deus +++ b/themes/deus @@ -2,7 +2,7 @@ # Deus # Color palette based on: https://github.com/ajmwagar/vim-deus -[colors] +[colors-dark] cursor=2c323b eaeaea background=2c323b foreground=eaeaea diff --git a/themes/dracula b/themes/dracula index 008fc150..82994203 100644 --- a/themes/dracula +++ b/themes/dracula @@ -1,7 +1,7 @@ # -*- conf -*- # Dracula -[colors] +[colors-dark] cursor=282a36 f8f8f2 foreground=f8f8f2 background=282a36 diff --git a/themes/dracula-iterm b/themes/dracula-iterm index 249bb6ab..b75ddd9c 100644 --- a/themes/dracula-iterm +++ b/themes/dracula-iterm @@ -1,7 +1,7 @@ # -*- conf -*- # Dracula iTerm2 variant -[colors] +[colors-dark] cursor=ffffff bbbbbb foreground=f8f8f2 background=1e1f29 diff --git a/themes/electrophoretic b/themes/electrophoretic index e0bf6e79..8bc022ea 100644 --- a/themes/electrophoretic +++ b/themes/electrophoretic @@ -5,7 +5,10 @@ # text and the white background. # author: Eugen Rahaian -[colors] +[main] +initial-color-theme=light + +[colors-light] cursor=ffffff 515151 background= ffffff foreground= 000000 diff --git a/themes/gruvbox b/themes/gruvbox index 6bc97352..e44f3ea9 100644 --- a/themes/gruvbox +++ b/themes/gruvbox @@ -1,7 +1,7 @@ # -*- conf -*- # Gruvbox -[colors] +[colors-dark] background=282828 foreground=ebdbb2 regular0=282828 @@ -21,7 +21,7 @@ bright5=d3869b bright6=8ec07c bright7=ebdbb2 -[colors2] +[colors-light] background=fbf1c7 foreground=3c3836 regular0=fbf1c7 diff --git a/themes/gruvbox-dark b/themes/gruvbox-dark index 73207199..c5dadcc5 100644 --- a/themes/gruvbox-dark +++ b/themes/gruvbox-dark @@ -1,7 +1,7 @@ # -*- conf -*- # Gruvbox -[colors] +[colors-dark] background=282828 foreground=ebdbb2 regular0=282828 diff --git a/themes/gruvbox-light b/themes/gruvbox-light index 6a7a2416..6b616612 100644 --- a/themes/gruvbox-light +++ b/themes/gruvbox-light @@ -1,7 +1,10 @@ # -*- conf -*- # Gruvbox - Light -[colors] +[main] +initial-color-theme=light + +[colors-light] background=fbf1c7 foreground=3c3836 regular0=fbf1c7 diff --git a/themes/hacktober b/themes/hacktober index dfcc4c7e..ecdb18fb 100644 --- a/themes/hacktober +++ b/themes/hacktober @@ -1,6 +1,6 @@ # -*- conf -*- -[colors] +[colors-dark] cursor=141414 c9c9c9 foreground=c9c9c9 background=141414 diff --git a/themes/iterm b/themes/iterm index 45b1a0bf..c5ffc190 100644 --- a/themes/iterm +++ b/themes/iterm @@ -2,7 +2,7 @@ # this foot theme is based on alacritty iterm theme: # https://github.com/alacritty/alacritty-theme/blob/master/themes/iterm.toml -[colors] +[colors-dark] foreground=fffbf6 background=101421 diff --git a/themes/jetbrains-darcula b/themes/jetbrains-darcula index e6997848..0092b795 100644 --- a/themes/jetbrains-darcula +++ b/themes/jetbrains-darcula @@ -2,7 +2,7 @@ # JetBrains Darcula # Palette based on the same theme from https://github.com/dexpota/kitty-themes -[colors] +[colors-dark] cursor=202020 ffffff background=202020 foreground=adadad diff --git a/themes/kitty b/themes/kitty index f43eea9d..81fd003e 100644 --- a/themes/kitty +++ b/themes/kitty @@ -1,6 +1,6 @@ # -*- conf -*- -[colors] +[colors-dark] cursor=111111 cccccc foreground=dddddd background=000000 diff --git a/themes/material-amber b/themes/material-amber index 27983833..69126aa0 100644 --- a/themes/material-amber +++ b/themes/material-amber @@ -2,7 +2,10 @@ # Material Amber # Based on material.io guidelines with Amber 50 background -[colors] +[main] +initial-color-theme=light + +[colors-light] cursor=fff8e1 21201d foreground = 21201d background = fff8e1 diff --git a/themes/material-design b/themes/material-design index 4a9e008a..bf1d0a6b 100644 --- a/themes/material-design +++ b/themes/material-design @@ -2,7 +2,7 @@ # Material # From https://github.com/MartinSeeler/iterm2-material-design -[colors] +[colors-dark] foreground=ECEFF1 background=263238 regular0=546E7A # black diff --git a/themes/modus-operandi b/themes/modus-operandi index 2d417bb5..6baca2f7 100644 --- a/themes/modus-operandi +++ b/themes/modus-operandi @@ -3,7 +3,11 @@ # modus-operandi # See: https://protesilaos.com/emacs/modus-themes # -[colors] + +[main] +initial-color-theme=light + +[colors-light] background=ffffff foreground=000000 regular0=000000 diff --git a/themes/modus-vivendi b/themes/modus-vivendi index 82b1075d..9ee670ec 100644 --- a/themes/modus-vivendi +++ b/themes/modus-vivendi @@ -4,7 +4,7 @@ # See: https://protesilaos.com/emacs/modus-themes # -[colors] +[colors-dark] background=000000 foreground=ffffff regular0=000000 diff --git a/themes/modus-vivendi-tinted b/themes/modus-vivendi-tinted index 67cf02a0..6a61fc79 100644 --- a/themes/modus-vivendi-tinted +++ b/themes/modus-vivendi-tinted @@ -4,7 +4,7 @@ # See: https://protesilaos.com/emacs/modus-themes # -[colors] +[colors-dark] background=0d0e1c foreground=ffffff regular0=000000 diff --git a/themes/molokai b/themes/molokai index c3935f69..19e1b6fa 100644 --- a/themes/molokai +++ b/themes/molokai @@ -2,7 +2,7 @@ # Molokai # Based on zhou13's at https://github.com/zhou13/molokai-terminal/blob/master/xterm/Xresources -[colors] +[colors-dark] background=1B1D1E foreground=CCCCCC regular0=1B1D1E diff --git a/themes/monokai-pro b/themes/monokai-pro index 5d9f31a9..3044da91 100644 --- a/themes/monokai-pro +++ b/themes/monokai-pro @@ -1,7 +1,7 @@ # -*- conf -*- # Monokai Pro -[colors] +[colors-dark] background=2D2A2E foreground=FCFCFA regular0=403E41 diff --git a/themes/moonfly b/themes/moonfly index 0dbe0e95..b30e3156 100644 --- a/themes/moonfly +++ b/themes/moonfly @@ -2,7 +2,7 @@ # moonfly # Based on https://github.com/bluz71/vim-moonfly-colors -[colors] +[colors-dark] cursor = 080808 9e9e9e foreground = b2b2b2 background = 080808 diff --git a/themes/neon b/themes/neon index d11a36d0..74884e03 100644 --- a/themes/neon +++ b/themes/neon @@ -6,7 +6,7 @@ # https://xcolors.net/neon # -[colors] +[colors-dark] foreground=f8f8f8 background=171717 regular0=171717 diff --git a/themes/night-owl b/themes/night-owl index 43a5c054..e9e40404 100644 --- a/themes/night-owl +++ b/themes/night-owl @@ -1,7 +1,7 @@ # _*_ conf _*_ # Night Owl -[colors] +[colors-dark] cursor=011627 80a4c2 foreground=d6deeb background=011627 diff --git a/themes/nightfly b/themes/nightfly index 37205f0f..ccdd183a 100644 --- a/themes/nightfly +++ b/themes/nightfly @@ -2,7 +2,7 @@ # nightfly # Based on https://github.com/bluz71/vim-nightfly-guicolors -[colors] +[colors-dark] cursor = 080808 9ca1aa foreground = acb4c2 background = 011627 diff --git a/themes/noirblaze b/themes/noirblaze index 42daf11b..b21055a4 100644 --- a/themes/noirblaze +++ b/themes/noirblaze @@ -3,7 +3,7 @@ # https://github.com/n1ghtmare/noirblaze-kitty -[colors] +[colors-dark] cursor=121212 ff0088 foreground=d5d5d5 background=121212 diff --git a/themes/nord b/themes/nord index 9b988ad6..eb2fdf0f 100644 --- a/themes/nord +++ b/themes/nord @@ -6,7 +6,7 @@ # this specific foot theme is based on nord-alacritty: # https://github.com/arcticicestudio/nord-alacritty/blob/develop/src/nord.yml -[colors] +[colors-dark] cursor = 2e3440 d8dee9 foreground = d8dee9 background = 2e3440 diff --git a/themes/nordiq b/themes/nordiq index 0df5c7de..1efccba6 100644 --- a/themes/nordiq +++ b/themes/nordiq @@ -1,7 +1,7 @@ # -*- conf -*- # Nordiq -[colors] +[colors-dark] cursor=eeeeee 9f515a foreground=dbdee9 background=0e1420 diff --git a/themes/nvim b/themes/nvim index bf629c0a..74dd1ac6 100644 --- a/themes/nvim +++ b/themes/nvim @@ -3,7 +3,7 @@ # Uses the dark color palette from the default Neovim color scheme # See: https://github.com/neovim/neovim/blob/fb6c059dc55c8d594102937be4dd70f5ff51614a/src/nvim/highlight_group.c#L419 -[colors] +[colors-dark] cursor=14161b e0e2ea # NvimDarkGrey2 NvimLightGrey2 foreground=e0e2ea # NvimLightGrey2 background=14161b # NvimDarkGrey2 @@ -29,7 +29,7 @@ bright5=ffcaff # NvimLightMagenta bright6=8cf8f7 # NvimLightCyan bright7=eef1f8 # NvimLightGrey1 -[colors2] +[colors-light] cursor=e0e2ea 14161b # NvimLightGrey2 NvimDarkGrey2 foreground=14161b # NvimDarkGrey2 background=e0e2ea # NvimLightGrey2 diff --git a/themes/nvim-dark b/themes/nvim-dark index 9a177770..fe3afb74 100644 --- a/themes/nvim-dark +++ b/themes/nvim-dark @@ -3,7 +3,7 @@ # Uses the dark color palette from the default Neovim color scheme # See: https://github.com/neovim/neovim/blob/fb6c059dc55c8d594102937be4dd70f5ff51614a/src/nvim/highlight_group.c#L419 -[colors] +[colors-dark] cursor=14161b e0e2ea # NvimDarkGrey2 NvimLightGrey2 foreground=e0e2ea # NvimLightGrey2 background=14161b # NvimDarkGrey2 diff --git a/themes/nvim-light b/themes/nvim-light index aca4e156..fd8943b1 100644 --- a/themes/nvim-light +++ b/themes/nvim-light @@ -3,7 +3,10 @@ # Uses the light color palette from the default Neovim color scheme # See: https://github.com/neovim/neovim/blob/fb6c059dc55c8d594102937be4dd70f5ff51614a/src/nvim/highlight_group.c#L334 -[colors] +[main] +initial-color-theme=light + +[colors-light] cursor=e0e2ea 14161b # NvimLightGrey2 NvimDarkGrey2 foreground=14161b # NvimDarkGrey2 background=e0e2ea # NvimLightGrey2 diff --git a/themes/onedark b/themes/onedark index 0932960b..6d66e87e 100644 --- a/themes/onedark +++ b/themes/onedark @@ -1,7 +1,7 @@ # OneDark # Palette based on the same theme from https://github.com/dexpota/kitty-themes -[colors] +[colors-dark] cursor=111111 cccccc foreground=979eab background=282c34 diff --git a/themes/onehalf-dark b/themes/onehalf-dark index 1adc9e23..1faca455 100644 --- a/themes/onehalf-dark +++ b/themes/onehalf-dark @@ -7,7 +7,7 @@ # + cursor colors from: # https://github.com/sonph/onehalf/blob/master/iterm/OneHalfDark.itermcolors -[colors] +[colors-dark] cursor=dcdfe4 a3b3cc foreground=dcdfe4 background=282c34 diff --git a/themes/panda b/themes/panda index b02c7e9f..2c1dc7c5 100644 --- a/themes/panda +++ b/themes/panda @@ -1,7 +1,7 @@ # -*- conf -*- # http://panda.siamak.me/ -[colors] +[colors-dark] # alpha=1.0 background=1D1E20 foreground=F0F0F0 diff --git a/themes/paper-color b/themes/paper-color index f158c148..09934925 100644 --- a/themes/paper-color +++ b/themes/paper-color @@ -2,7 +2,7 @@ # PaperColorDark # Palette based on https://github.com/NLKNguyen/papercolor-theme -[colors] +[colors-dark] cursor=1c1c1c eeeeee background=1c1c1c foreground=eeeeee @@ -25,7 +25,7 @@ bright7=5f8787 # bright white # selection-foreground=1c1c1c # selection-background=af87d7 -[colors2] +[colors-light] cursor=eeeeee 444444 background=eeeeee foreground=444444 diff --git a/themes/paper-color-dark b/themes/paper-color-dark index 991bcc9d..26260c6f 100644 --- a/themes/paper-color-dark +++ b/themes/paper-color-dark @@ -2,7 +2,7 @@ # PaperColorDark # Palette based on https://github.com/NLKNguyen/papercolor-theme -[colors] +[colors-dark] cursor=1c1c1c eeeeee background=1c1c1c foreground=eeeeee diff --git a/themes/paper-color-light b/themes/paper-color-light index b8a6ceec..2f7a8003 100644 --- a/themes/paper-color-light +++ b/themes/paper-color-light @@ -2,7 +2,10 @@ # PaperColor Light # Palette based on https://github.com/NLKNguyen/papercolor-theme -[colors] +[main] +initial-color-theme=light +xs +[colors-light] cursor=eeeeee 444444 background=eeeeee foreground=444444 diff --git a/themes/poimandres b/themes/poimandres index b4edc175..a2123ac5 100644 --- a/themes/poimandres +++ b/themes/poimandres @@ -1,7 +1,7 @@ # Based on Poimandres color theme for kitti terminal emulator # https://github.com/ubmit/poimandres-kitty -[colors] +[colors-dark] cursor=1b1e28 ffffff foreground=a6accd background=1b1e28 diff --git a/themes/rezza b/themes/rezza index 56814a77..62a08cc2 100644 --- a/themes/rezza +++ b/themes/rezza @@ -13,7 +13,7 @@ # and also posted here: # https://forums.debian.net/viewtopic.php?t=29981 -[colors] +[colors-dark] foreground = cccccc background = 191911 diff --git a/themes/rose-pine b/themes/rose-pine index 2cae00e8..b9aa7e2a 100644 --- a/themes/rose-pine +++ b/themes/rose-pine @@ -1,7 +1,7 @@ # -*- conf -*- # Rosé Pine -[colors] +[colors-dark] cursor=191724 e0def4 background=191724 foreground=e0def4 diff --git a/themes/rose-pine-dawn b/themes/rose-pine-dawn index 674c7a21..d2742c72 100644 --- a/themes/rose-pine-dawn +++ b/themes/rose-pine-dawn @@ -1,7 +1,11 @@ # -*- conf -*- # Rosé Pine Dawn -[colors] +[main] +initial-color-theme=light + + +[colors-light] cursor=faf4ed 575279 background=faf4ed foreground=575279 diff --git a/themes/rose-pine-moon b/themes/rose-pine-moon index cbc81451..51b9a33a 100644 --- a/themes/rose-pine-moon +++ b/themes/rose-pine-moon @@ -1,7 +1,7 @@ # -*- conf -*- # Rosé Pine Moon -[colors] +[colors-dark] cursor=232136 e0def4 background=232136 foreground=e0def4 diff --git a/themes/selenized b/themes/selenized index cde35723..83fea617 100644 --- a/themes/selenized +++ b/themes/selenized @@ -1,7 +1,7 @@ # -*- conf -*- # Selenized dark -[colors] +[colors-dark] cursor = 103c48 53d6c7 background= 103c48 foreground= adbcbc @@ -24,7 +24,7 @@ bright5= ff84cd bright6= 53d6c7 bright7= cad8d9 -[colors2] +[colors-light] cursor=fbf3db 00978a background= fbf3db foreground= 53676d diff --git a/themes/selenized-black b/themes/selenized-black index 591751f0..8a93187e 100644 --- a/themes/selenized-black +++ b/themes/selenized-black @@ -1,7 +1,7 @@ # -*- conf -*- # Selenized black -[colors] +[colors-dark] cursor = 181818 56d8c9 background= 181818 foreground= b9b9b9 diff --git a/themes/selenized-dark b/themes/selenized-dark index 5d062dec..8ace1c05 100644 --- a/themes/selenized-dark +++ b/themes/selenized-dark @@ -1,7 +1,7 @@ # -*- conf -*- # Selenized dark -[colors] +[colors-dark] cursor = 103c48 53d6c7 background= 103c48 foreground= adbcbc diff --git a/themes/selenized-light b/themes/selenized-light index 04dffbea..c842fc3c 100644 --- a/themes/selenized-light +++ b/themes/selenized-light @@ -1,7 +1,10 @@ # -*- conf -*- # Selenized light -[colors] +[main] +initial-color-theme=light + +[colors-light] cursor=fbf3db 00978a background= fbf3db foreground= 53676d diff --git a/themes/selenized-white b/themes/selenized-white index 5a7d68b2..659bf814 100644 --- a/themes/selenized-white +++ b/themes/selenized-white @@ -1,7 +1,10 @@ # -*- conf -*- # Selenized white -[colors] +[main] +initial-color-theme=light + +[colors-light] cursor=ffffff 009a8a background= ffffff foreground= 474747 diff --git a/themes/solarized b/themes/solarized index 335c738e..f1844b3c 100644 --- a/themes/solarized +++ b/themes/solarized @@ -2,7 +2,7 @@ # Solarized dark+light # Dark -[colors] +[colors-dark] cursor= 002b36 93a1a1 background= 002b36 foreground= 839496 @@ -25,7 +25,7 @@ bright7= fdf6e3 # Light -[colors2] +[colors-light] cursor= fdf6e3 586e75 background= fdf6e3 foreground= 657b83 diff --git a/themes/solarized-dark b/themes/solarized-dark index 4997eb4a..6335fa0f 100644 --- a/themes/solarized-dark +++ b/themes/solarized-dark @@ -1,7 +1,7 @@ # -*- conf -*- # Solarized dark -[colors] +[colors-dark] cursor= 002b36 93a1a1 background= 002b36 foreground= 839496 diff --git a/themes/solarized-dark-normal-brights b/themes/solarized-dark-normal-brights index f0c2172d..7b608110 100644 --- a/themes/solarized-dark-normal-brights +++ b/themes/solarized-dark-normal-brights @@ -1,7 +1,7 @@ # -*- conf -*- # Solarized dark -[colors] +[colors-dark] cursor= 002b36 93a1a1 background= 002b36 foreground= 839496 diff --git a/themes/solarized-light b/themes/solarized-light index 3d750277..db27be43 100644 --- a/themes/solarized-light +++ b/themes/solarized-light @@ -1,7 +1,10 @@ # -*- conf -*- # Solarized light -[colors] +[main] +initial-color-theme=light + +[colors-light] cursor= fdf6e3 586e75 background= fdf6e3 foreground= 657b83 diff --git a/themes/solarized-normal-brights b/themes/solarized-normal-brights index a7724cd3..3bd3c189 100644 --- a/themes/solarized-normal-brights +++ b/themes/solarized-normal-brights @@ -9,7 +9,7 @@ # encoding to sRGB again. # Dark -[colors] +[colors-dark] cursor= 002b36 93a1a1 background= 002b36 foreground= 839496 @@ -32,7 +32,7 @@ bright7= ffffff # Light -[colors2] +[colors-light] cursor= fdf6e3 586e75 background= fdf6e3 foreground= 657b83 diff --git a/themes/srcery b/themes/srcery index 54966707..612c82cc 100644 --- a/themes/srcery +++ b/themes/srcery @@ -1,6 +1,6 @@ # srcery -[colors] +[colors-dark] background= 1c1b19 foreground= fce8c3 regular0= 1c1b19 diff --git a/themes/starlight b/themes/starlight index ed39f277..81ce1a5f 100644 --- a/themes/starlight +++ b/themes/starlight @@ -1,7 +1,7 @@ # -*- conf -*- # Theme: starlight V4 (https://github.com/CosmicToast/starlight) -[colors] +[colors-dark] foreground = FFFFFF background = 242424 diff --git a/themes/tango b/themes/tango index a93d29cb..5ea43f63 100644 --- a/themes/tango +++ b/themes/tango @@ -1,7 +1,7 @@ # -*- conf -*- # Tango -[colors] +[colors-dark] cursor=000000 babdb6 foreground=babdb6 background=000000 diff --git a/themes/tempus-autumn b/themes/tempus-autumn index 74228e90..214478bb 100644 --- a/themes/tempus-autumn +++ b/themes/tempus-autumn @@ -3,7 +3,7 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a palette inspired by earthly colours (WCAG AA compliant) -[colors] +[colors-dark] #cursor = 302420 a9a2a6 foreground = a9a2a6 background = 302420 diff --git a/themes/tempus-classic b/themes/tempus-classic index b35dc5e5..95b37b76 100644 --- a/themes/tempus-classic +++ b/themes/tempus-classic @@ -3,7 +3,7 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with warm hues (WCAG AA compliant) -[colors] +[colors-dark] #cursor = 232323 aeadaf foreground = aeadaf background = 232323 diff --git a/themes/tempus-dawn b/themes/tempus-dawn index dc45f29d..c288544e 100644 --- a/themes/tempus-dawn +++ b/themes/tempus-dawn @@ -3,7 +3,11 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme with a soft, slightly desaturated palette (WCAG AA compliant) -[colors] +[main] +initial-color-theme=light + + +[colors-light] #cursor = eff0f2 4a4b4e foreground = 4a4b4e background = eff0f2 diff --git a/themes/tempus-day b/themes/tempus-day index 1df70137..03454f04 100644 --- a/themes/tempus-day +++ b/themes/tempus-day @@ -3,7 +3,10 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme with warm colours (WCAG AA compliant) -[colors] +[main] +initial-color-theme=light + +[colors-light] #cursor = f8f2e5 464340 foreground = 464340 background = f8f2e5 diff --git a/themes/tempus-dusk b/themes/tempus-dusk index 5b4d1bea..cd27aaaa 100644 --- a/themes/tempus-dusk +++ b/themes/tempus-dusk @@ -3,7 +3,7 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a deep blue-ish, slightly desaturated palette (WCAG AA compliant) -[colors] +[colors-dark] #cursor = 1f252d a2a8ba foreground = a2a8ba background = 1f252d diff --git a/themes/tempus-fugit b/themes/tempus-fugit index ebd082fe..b9dce351 100644 --- a/themes/tempus-fugit +++ b/themes/tempus-fugit @@ -3,7 +3,10 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light, pleasant theme optimised for long writing/coding sessions (WCAG AA compliant) -[colors] +[main] +initial-color-theme=light + +[colors-light] #cursor = fff5f3 4d595f foreground = 4d595f background = fff5f3 diff --git a/themes/tempus-future b/themes/tempus-future index c97d379d..1f8c3c79 100644 --- a/themes/tempus-future +++ b/themes/tempus-future @@ -3,7 +3,7 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with colours inspired by concept art of outer space (WCAG AAA compliant) -[colors] +[colors-dark] #cursor = 090a18 b4abac foreground = b4abac background = 090a18 diff --git a/themes/tempus-night b/themes/tempus-night index 7c97681d..aae80f02 100644 --- a/themes/tempus-night +++ b/themes/tempus-night @@ -3,7 +3,7 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: High contrast dark theme with bright colours (WCAG AAA compliant) -[colors] +[colors-dark] #cursor = 1a1a1a e0e0e0 foreground = e0e0e0 background = 1a1a1a diff --git a/themes/tempus-past b/themes/tempus-past index af408b00..5f90ddf1 100644 --- a/themes/tempus-past +++ b/themes/tempus-past @@ -3,7 +3,10 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme inspired by old vaporwave concept art (WCAG AA compliant) -[colors] +[main] +initial-color-theme=light + +[colors-light] #cursor = f3f2f4 53545b foreground = 53545b background = f3f2f4 diff --git a/themes/tempus-rift b/themes/tempus-rift index e0cea4da..8add657a 100644 --- a/themes/tempus-rift +++ b/themes/tempus-rift @@ -3,7 +3,7 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a subdued palette on the green side of the spectrum (WCAG AA compliant) -[colors] +[colors-dark] #cursor = 162c22 bbbcbc foreground = bbbcbc background = 162c22 diff --git a/themes/tempus-spring b/themes/tempus-spring index b98be3b4..eb15a1be 100644 --- a/themes/tempus-spring +++ b/themes/tempus-spring @@ -3,7 +3,7 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a palette inspired by early spring colours (WCAG AA compliant) -[colors] +[colors-dark] #cursor = 283a37 b5b8b7 foreground = b5b8b7 background = 283a37 diff --git a/themes/tempus-summer b/themes/tempus-summer index cd904010..74c8faa2 100644 --- a/themes/tempus-summer +++ b/themes/tempus-summer @@ -3,7 +3,7 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with colours inspired by summer evenings by the sea (WCAG AA compliant) -[colors] +[colors-dark] #cursor = 202c3d a0abae foreground = a0abae background = 202c3d diff --git a/themes/tempus-tempest b/themes/tempus-tempest index 2c84454e..f1cf55bf 100644 --- a/themes/tempus-tempest +++ b/themes/tempus-tempest @@ -3,7 +3,7 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: A green-scale, subtle theme for late night hackers (WCAG AAA compliant) -[colors] +[colors-dark] #cursor = 282b2b b6e0ca foreground = b6e0ca background = 282b2b diff --git a/themes/tempus-totus b/themes/tempus-totus index 3eb21644..fae6ede3 100644 --- a/themes/tempus-totus +++ b/themes/tempus-totus @@ -3,7 +3,10 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme for prose or for coding in an open space (WCAG AAA compliant) -[colors] +[main] +initial-color-theme=light + +[colors-light] #cursor = ffffff 4a484d foreground = 4a484d background = ffffff diff --git a/themes/tempus-warp b/themes/tempus-warp index 911fb266..906b3f37 100644 --- a/themes/tempus-warp +++ b/themes/tempus-warp @@ -3,7 +3,7 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a vibrant palette (WCAG AA compliant) -[colors] +[colors-dark] #cursor = 001514 a29fa0 foreground = a29fa0 background = 001514 diff --git a/themes/tempus-winter b/themes/tempus-winter index e4307142..dc95128b 100644 --- a/themes/tempus-winter +++ b/themes/tempus-winter @@ -3,7 +3,7 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a palette inspired by winter nights at the city (WCAG AA compliant) -[colors] +[colors-dark] #cursor = 202427 8da3b8 foreground = 8da3b8 background = 202427 diff --git a/themes/tokyonight-light b/themes/tokyonight-light index ffcae689..359a31b9 100644 --- a/themes/tokyonight-light +++ b/themes/tokyonight-light @@ -2,7 +2,10 @@ # Reference: https://github.com/tokyo-night/tokyo-night-vscode-theme/blob/master/themes/tokyo-night-light-color-theme.json -[colors] +[main] +initial-color-theme=light + +[colors-light] background=d6d8df foreground=343b58 regular0=343b58 diff --git a/themes/tokyonight-night b/themes/tokyonight-night index f789e1bd..58037f72 100644 --- a/themes/tokyonight-night +++ b/themes/tokyonight-night @@ -1,6 +1,6 @@ # -*- conf -*- -[colors] +[colors-dark] background=1a1b26 foreground=c0caf5 regular0=15161E diff --git a/themes/tokyonight-storm b/themes/tokyonight-storm index 074b4697..4dbbf6c6 100644 --- a/themes/tokyonight-storm +++ b/themes/tokyonight-storm @@ -1,6 +1,6 @@ # -*- conf -*- -[colors] +[colors-dark] background=24283b foreground=c0caf5 regular0=1D202F diff --git a/themes/visibone b/themes/visibone index 9979bee0..b989b36b 100644 --- a/themes/visibone +++ b/themes/visibone @@ -1,7 +1,7 @@ # -*- conf -*- # VisiBone -[colors] +[colors-dark] cursor=010101 ffffff foreground=ffffff background=010101 diff --git a/themes/xterm b/themes/xterm index bf17f5e7..a9382fd8 100644 --- a/themes/xterm +++ b/themes/xterm @@ -1,7 +1,7 @@ # -*- conf -*- # The default palette of xterm. -[colors] +[colors-dark] foreground=e5e5e5 background=000000 regular0=000000 # black diff --git a/themes/zenburn b/themes/zenburn index bace080c..37a26812 100644 --- a/themes/zenburn +++ b/themes/zenburn @@ -1,6 +1,6 @@ # -*- conf -*- -[colors] +[colors-dark] foreground=dcdccc background=111111 From 1caba0d993c7bac68b555efb5d6dfab2577fcd02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 20 Dec 2025 15:56:32 +0100 Subject: [PATCH 66/84] config: remove deprecated config option cursor.color This option was deprecated in 1.23.0. Use colors-{dark,light}.cursor instead. --- CHANGELOG.md | 5 +++++ config.c | 22 ---------------------- 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae9feb54..3a73893d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,6 +101,11 @@ ### Removed + +* `cursor.color` config option (deprecated in 1.23.0). Use + `colors-{dark,light}.cursor` instead. + + ### Fixed * Search mode: composing keys not ignored. diff --git a/config.c b/config.c index 0340d418..14e836c1 100644 --- a/config.c +++ b/config.c @@ -1661,28 +1661,6 @@ parse_section_cursor(struct context *ctx) else if (streq(key, "blink-rate")) return value_to_uint32(ctx, 10, &conf->cursor.blink.rate_ms); - else if (streq(key, "color")) { - LOG_WARN("%s:%d: cursor.color: deprecated; use colors.cursor instead", - ctx->path, ctx->lineno); - - user_notification_add( - &conf->notifications, - USER_NOTIFICATION_DEPRECATED, - xstrdup("cursor.color: use colors.cursor instead")); - - if (!value_to_two_colors( - ctx, - &conf->colors_dark.cursor.text, - &conf->colors_dark.cursor.cursor, - false)) - { - return false; - } - - conf->colors_dark.use_custom.cursor = true; - return true; - } - else if (streq(key, "beam-thickness")) return value_to_pt_or_px(ctx, &conf->cursor.beam_thickness); From aa26676c43edf2cfcc3543a443a25e881e34473e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 12 Nov 2025 10:22:17 +0100 Subject: [PATCH 67/84] builtin terminfo: add custom 'query-os-name' Inspired by Kitty's 'kitty-query-os_name'. Notable changes: * Drop kitty prefix * os_name -> os-name * Use "uname -s" without any transformations (e.g. no lower-casing) $ ./utils/xtgettcap query-os-name reply: (44 chars): P1+r71756572792d6f732d6e616d65=4C696E7578\ query-os-name=Linux Closes #2209 --- CHANGELOG.md | 3 +++ README.md | 4 ++++ scripts/generate-builtin-terminfo.py | 2 ++ 3 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a73893d..6fa7439e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,8 +76,11 @@ Wayland protocol ([#2212][2212]). * `[colors-dark]` section to `foot.ini`. Replaces `[colors]`. * `[colors-light]` section to `foot.ini`. Replaces `[colors2]`. +* `XTGETTCAP`: added `query-os-name`, returning the OS foot is + compiled for (e.g. _'Linux'_) ([#2209][2209]). [2212]: https://codeberg.org/dnkl/foot/issues/2212 +[2209]: https://codeberg.org/dnkl/foot/issues/2209 ### Changed diff --git a/README.md b/README.md index e8f3c8cd..7ee771ba 100644 --- a/README.md +++ b/README.md @@ -641,6 +641,10 @@ All replies are in `tigetstr()` format. That is, given the same capability name, foot's reply is identical to what `tigetstr()` would have returned. +In addition to queries for terminfo entries, the `query-os-name` query +will be answered with a response of the form `uname=$(uname -s)`, +where `$(uname -s)` is the name of the OS foot was compiled for. + # Credits diff --git a/scripts/generate-builtin-terminfo.py b/scripts/generate-builtin-terminfo.py index 28b31b57..6a6ba68c 100755 --- a/scripts/generate-builtin-terminfo.py +++ b/scripts/generate-builtin-terminfo.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import argparse +import os import re import sys @@ -185,6 +186,7 @@ def main(): entry.add_capability(StringCapability('TN', target_entry_name)) entry.add_capability(StringCapability('name', target_entry_name)) entry.add_capability(IntCapability('RGB', 8)) # 8 bits per channel + entry.add_capability(StringCapability('query-os-name', os.uname().sysname)) terminfo_parts = [] for cap in sorted(entry.caps.values()): From 4cb17f5ae65e129765794986d36fbfc33c0c65bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 24 Dec 2025 11:33:28 +0100 Subject: [PATCH 68/84] csi: make sure the ASCII printer function is updated on plain underlines Otherwise, a sequence like \E[4:2;4m # Enable double-underline, then immediately switch to single Will switch to the slow printer, and then get stuck there even though we immediately switch to plain underlines (which don't need the slow printer). --- csi.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/csi.c b/csi.c index 7e0cf464..0cf8e203 100644 --- a/csi.c +++ b/csi.c @@ -117,9 +117,9 @@ csi_sgr(struct terminal *term) style > UNDERLINE_SINGLE; break; } - - term_update_ascii_printer(term); - } + } else + term->bits_affecting_ascii_printer.underline_style = false; + term_update_ascii_printer(term); break; } case 5: term->vt.attrs.blink = true; break; From ca278398b101bc6c44ad6df0c48e068660ac5323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 26 Dec 2025 12:33:03 +0100 Subject: [PATCH 69/84] pyproject.toml: add initial mypy configuration --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..52106805 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[tool.mypy] +files = '$MYPY_CONFIG_FILE_DIR/scripts' +strict = true From cb1e152d995d2430c73bf322ced5e23f40fcb73f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 26 Dec 2025 13:12:43 +0100 Subject: [PATCH 70/84] pyproject.toml: add initial pyright configuration --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 52106805..d561b976 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,6 @@ +[tool.pyright] +strict = ['scripts'] + [tool.mypy] files = '$MYPY_CONFIG_FILE_DIR/scripts' strict = true From bbebe0f330fc3606aeb1896af70175d2fa81716d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 26 Dec 2025 13:13:01 +0100 Subject: [PATCH 71/84] scripts: mypy fixes --- scripts/benchmark.py | 8 +- scripts/generate-alt-random-writes.py | 23 +++-- scripts/generate-builtin-terminfo.py | 85 +++++++++++-------- scripts/generate-emoji-variation-sequences.py | 7 +- scripts/srgb.py | 5 +- 5 files changed, 77 insertions(+), 51 deletions(-) diff --git a/scripts/benchmark.py b/scripts/benchmark.py index 5483dac1..fe820d9b 100755 --- a/scripts/benchmark.py +++ b/scripts/benchmark.py @@ -11,7 +11,7 @@ import termios from datetime import datetime -def main(): +def main() -> None: parser = argparse.ArgumentParser() parser.add_argument('files', type=argparse.FileType('rb'), nargs='+') parser.add_argument('--iterations', type=int, default=20) @@ -24,12 +24,12 @@ def main(): termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0))) - times = {name: [] for name in [f.name for f in args.files]} + times: dict[str, list[float]] = {name: [] for name in [f.name for f in args.files]} for f in args.files: bench_bytes = f.read() - for i in range(args.iterations): + for _ in range(args.iterations): start = datetime.now() sys.stdout.buffer.write(bench_bytes) stop = datetime.now() @@ -48,4 +48,4 @@ def main(): if __name__ == '__main__': - sys.exit(main()) + main() diff --git a/scripts/generate-alt-random-writes.py b/scripts/generate-alt-random-writes.py index 656a2b9d..7ad1460c 100755 --- a/scripts/generate-alt-random-writes.py +++ b/scripts/generate-alt-random-writes.py @@ -8,6 +8,8 @@ import struct import sys import termios +from typing import Any + class ColorVariant(enum.IntEnum): NONE = enum.auto() @@ -17,7 +19,7 @@ class ColorVariant(enum.IntEnum): RGB = enum.auto() -def main(): +def main() -> None: parser = argparse.ArgumentParser() parser.add_argument( 'out', type=argparse.FileType(mode='w'), nargs='?', help='name of output file') @@ -38,10 +40,16 @@ def main(): opts = parser.parse_args() out = opts.out if opts.out is not None else sys.stdout + lines: int | None = None + cols: int | None = None + width: int | None = None + height: int | None = None + if opts.rows is None or opts.cols is None: try: - def dummy(*args): + def dummy(*args: Any) -> None: """Need a handler installed for sigwait() to trigger.""" + _ = args pass signal.signal(signal.SIGWINCH, dummy) @@ -53,6 +61,9 @@ def main(): termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0))) + assert width is not None + assert height is not None + if width > 0 and height > 0: break @@ -71,9 +82,11 @@ def main(): if opts.rows is not None: lines = opts.rows + assert lines is not None height = 15 * lines # PGO helper binary hardcodes cell height to 15px if opts.cols is not None: cols = opts.cols + assert cols is not None width = 8 * cols # PGO help binary hardcodes cell width to 8px if lines is None or cols is None or height is None or width is None: @@ -190,8 +203,8 @@ def main(): # The sixel 'alphabet' sixels = '?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~' - last_pos = None - last_size = None + last_pos: tuple[int, int] | None = None + last_size: tuple[int, int] = 0, 0 for _ in range(20): if last_pos is not None and random.randrange(2): @@ -254,4 +267,4 @@ def main(): if __name__ == '__main__': - sys.exit(main()) + main() diff --git a/scripts/generate-builtin-terminfo.py b/scripts/generate-builtin-terminfo.py index 6a6ba68c..515153a2 100755 --- a/scripts/generate-builtin-terminfo.py +++ b/scripts/generate-builtin-terminfo.py @@ -3,13 +3,10 @@ import argparse import os import re -import sys - -from typing import Dict, Union class Capability: - def __init__(self, name: str, value: Union[bool, int, str]): + def __init__(self, name: str, value: bool | int | str) -> None: self._name = name self._value = value @@ -18,30 +15,42 @@ class Capability: return self._name @property - def value(self) -> Union[bool, int, str]: + def value(self) -> bool | int | str: return self._value - def __lt__(self, other): + def __lt__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented return self._name < other._name - def __le__(self, other): + def __le__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented return self._name <= other._name - def __eq__(self, other): + def __eq__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented return self._name == other._name - def __ne__(self, other): - return self._name != other._name + def __ne__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented + return bool(self._name != other._name) - def __gt__(self, other): - return self._name > other._name + def __gt__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented + return bool(self._name > other._name) - def __ge__(self, other): + def __ge__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented return self._name >= other._name class BoolCapability(Capability): - def __init__(self, name: str): + def __init__(self, name: str) -> None: super().__init__(name, True) @@ -50,11 +59,11 @@ class IntCapability(Capability): class StringCapability(Capability): - def __init__(self, name: str, value: str): + def __init__(self, name: str, value: str) -> None: # see terminfo(5) for valid escape sequences # Control characters - def translate_ctrl_chr(m): + def translate_ctrl_chr(m: re.Match[str]) -> str: ctrl = m.group(1) if ctrl == '?': return '\\x7f' @@ -83,10 +92,10 @@ class StringCapability(Capability): class Fragment: - def __init__(self, name: str, description: str): + def __init__(self, name: str, description: str) -> None: self._name = name self._description = description - self._caps = {} + self._caps = dict[str, Capability]() @property def name(self) -> str: @@ -97,18 +106,18 @@ class Fragment: return self._description @property - def caps(self) -> Dict[str, Capability]: + def caps(self) -> dict[str, Capability]: return self._caps - def add_capability(self, cap: Capability): + def add_capability(self, cap: Capability) -> None: assert cap.name not in self._caps self._caps[cap.name] = cap - def del_capability(self, name: str): + def del_capability(self, name: str) -> None: del self._caps[name] -def main(): +def main() -> None: parser = argparse.ArgumentParser() parser.add_argument('source_entry_name') parser.add_argument('source', type=argparse.FileType('r')) @@ -121,15 +130,15 @@ def main(): source = opts.source target = opts.target - lines = [] - for l in source.readlines(): - l = l.strip() - if l.startswith('#'): + lines = list[str]() + for line in source.readlines(): + line = line.strip() + if line.startswith('#'): continue - lines.append(l) + lines.append(line) - fragments = {} - cur_fragment = None + fragments = dict[str, Fragment]() + cur_fragment: Fragment | None = None for m in re.finditer( r'(?P(?P[-+\w@]+)\|(?P.+?),)|' @@ -148,17 +157,20 @@ def main(): elif m.group('bool_cap') is not None: name = m.group('bool_name') + assert cur_fragment is not None cur_fragment.add_capability(BoolCapability(name)) elif m.group('int_cap') is not None: name = m.group('int_name') - value = int(m.group('int_val'), 0) - cur_fragment.add_capability(IntCapability(name, value)) + int_value = int(m.group('int_val'), 0) + assert cur_fragment is not None + cur_fragment.add_capability(IntCapability(name, int_value)) elif m.group('str_cap') is not None: name = m.group('str_name') - value = m.group('str_val') - cur_fragment.add_capability(StringCapability(name, value)) + str_value = m.group('str_val') + assert cur_fragment is not None + cur_fragment.add_capability(StringCapability(name, str_value)) else: assert False @@ -167,6 +179,9 @@ def main(): for frag in fragments.values(): for cap in frag.caps.values(): if cap.name == 'use': + assert isinstance(cap, StringCapability) + assert isinstance(cap.value, str) + use_frag = fragments[cap.value] for use_cap in use_frag.caps.values(): frag.add_capability(use_cap) @@ -188,7 +203,7 @@ def main(): entry.add_capability(IntCapability('RGB', 8)) # 8 bits per channel entry.add_capability(StringCapability('query-os-name', os.uname().sysname)) - terminfo_parts = [] + terminfo_parts = list[str]() for cap in sorted(entry.caps.values()): name = cap.name value = str(cap.value) @@ -212,4 +227,4 @@ def main(): if __name__ == '__main__': - sys.exit(main()) + main() diff --git a/scripts/generate-emoji-variation-sequences.py b/scripts/generate-emoji-variation-sequences.py index e05b6290..17172d20 100644 --- a/scripts/generate-emoji-variation-sequences.py +++ b/scripts/generate-emoji-variation-sequences.py @@ -1,11 +1,10 @@ #!/usr/bin/env python3 import argparse -import sys class Codepoint: - def __init__(self, start: int, end: None|int = None): + def __init__(self, start: int, end: None | int = None) -> None: self.start = start self.end = start if end is None else end self.vs15 = False @@ -15,7 +14,7 @@ class Codepoint: return f'{self.start:x}-{self.end:x}, vs15={self.vs15}, vs16={self.vs16}' -def main(): +def main() -> None: parser = argparse.ArgumentParser() parser.add_argument('input', type=argparse.FileType('r')) parser.add_argument('output', type=argparse.FileType('w')) @@ -100,4 +99,4 @@ def main(): if __name__ == '__main__': - sys.exit(main()) + main() diff --git a/scripts/srgb.py b/scripts/srgb.py index 12056956..a6aa0f4a 100755 --- a/scripts/srgb.py +++ b/scripts/srgb.py @@ -2,7 +2,6 @@ import argparse import math -import sys # Note: we use a pure gamma 2.2 function, rather than the piece-wise @@ -17,7 +16,7 @@ def linear_to_srgb(f: float) -> float: return math.pow(f, 1 / 2.2) -def main(): +def main() -> None: parser = argparse.ArgumentParser() parser.add_argument('c_output', type=argparse.FileType('w')) parser.add_argument('h_output', type=argparse.FileType('w')) @@ -68,4 +67,4 @@ def main(): if __name__ == '__main__': - sys.exit(main()) + main() From 6ab2e2d9ebc247b4c4112d12a16ebc2b7b0f7c74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 26 Dec 2025 13:15:01 +0100 Subject: [PATCH 72/84] ci: run mypy + ruff check --- .woodpecker.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 843c9afc..ef96efcb 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -17,6 +17,24 @@ steps: - codespell -Lser,doas,zar,rin README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd - deactivate + - name: mypy + when: + - event: [manual, pull_request] + - event: [push, tag] + branch: [master, releases/*] + image: alpine:edge + commands: + - apk add openssl + - apk add python3 + - apk add py3-pip + - python3 -m venv mypy-venv + - source mypy-venv/bin/activate + - pip install mypy + - pip install ruff + - mypy + - ruff check + - deactivate + - name: subprojects when: - event: [manual, pull_request] From ee682abac876cf558627e39415ebc92d2431c964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 26 Dec 2025 14:13:14 +0100 Subject: [PATCH 73/84] mypy: no need to declare None as return type for __init__ --- scripts/generate-builtin-terminfo.py | 8 ++++---- scripts/generate-emoji-variation-sequences.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/generate-builtin-terminfo.py b/scripts/generate-builtin-terminfo.py index 515153a2..c10373d3 100755 --- a/scripts/generate-builtin-terminfo.py +++ b/scripts/generate-builtin-terminfo.py @@ -6,7 +6,7 @@ import re class Capability: - def __init__(self, name: str, value: bool | int | str) -> None: + def __init__(self, name: str, value: bool | int | str): self._name = name self._value = value @@ -50,7 +50,7 @@ class Capability: class BoolCapability(Capability): - def __init__(self, name: str) -> None: + def __init__(self, name: str): super().__init__(name, True) @@ -59,7 +59,7 @@ class IntCapability(Capability): class StringCapability(Capability): - def __init__(self, name: str, value: str) -> None: + def __init__(self, name: str, value: str): # see terminfo(5) for valid escape sequences # Control characters @@ -92,7 +92,7 @@ class StringCapability(Capability): class Fragment: - def __init__(self, name: str, description: str) -> None: + def __init__(self, name: str, description: str): self._name = name self._description = description self._caps = dict[str, Capability]() diff --git a/scripts/generate-emoji-variation-sequences.py b/scripts/generate-emoji-variation-sequences.py index 17172d20..57e881c7 100644 --- a/scripts/generate-emoji-variation-sequences.py +++ b/scripts/generate-emoji-variation-sequences.py @@ -4,7 +4,7 @@ import argparse class Codepoint: - def __init__(self, start: int, end: None | int = None) -> None: + def __init__(self, start: int, end: None | int = None): self.start = start self.end = start if end is None else end self.vs15 = False From b3cb180e443ce108b3f051d5eccd80474a058ac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 26 Dec 2025 14:42:51 +0100 Subject: [PATCH 74/84] codespell: use pyproject.toml to define options and exceptions --- .woodpecker.yaml | 2 +- CODE_OF_CONDUCT.md | 2 +- pyproject.toml | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index ef96efcb..35d52d67 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -14,7 +14,7 @@ steps: - python3 -m venv codespell-venv - source codespell-venv/bin/activate - pip install codespell - - codespell -Lser,doas,zar,rin README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd + - codespell - deactivate - name: mypy diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 4b652df6..26ab32a5 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -53,7 +53,7 @@ decisions when appropriate. Participants in the foot community are expected to uphold the described standards not only in official community spaces (issue trackers, IRC channels, etc.) but in all public spaces. The Code of Conduct however does acknowledge -that people are fallible and that it is possible to truely correct a past +that people are fallible and that it is possible to truly correct a past pattern of unacceptable behavior. That is to say, the scope of the Code of Conduct does not necessarily extend into the distant past. diff --git a/pyproject.toml b/pyproject.toml index d561b976..654e632d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,3 +4,7 @@ strict = ['scripts'] [tool.mypy] files = '$MYPY_CONFIG_FILE_DIR/scripts' strict = true + +[tool.codespell] +skip = 'pyproject.toml,./subprojects,./pkg,./src,./bld,foot.info,./unicode,.*-venv' +ignore-regex = 'terminfo capability `rin`|\* Simon Ser|\* \[zar\]\(https://codeberg.org/zar\)|iterm theme|iterm.toml|iterm/OneHalfDark.itermcolors' \ No newline at end of file From 41679e64a84977225e193f4eb734ab0a0eab1e09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 26 Dec 2025 15:00:18 +0100 Subject: [PATCH 75/84] box-drawing: fenv.h: remove, not needed anymore --- box-drawing.c | 1 - 1 file changed, 1 deletion(-) diff --git a/box-drawing.c b/box-drawing.c index 421ff54d..e69d9648 100644 --- a/box-drawing.c +++ b/box-drawing.c @@ -2,7 +2,6 @@ #include #include -#include #include #define LOG_MODULE "box-drawing" From bb6968c2847e99ba89a7597ff5b8a31fb5dd9434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 26 Dec 2025 17:23:46 +0100 Subject: [PATCH 76/84] ci: combine the codespell and mypy stages They both need python and a venv, so let's combine them, to avoid having to install the same things twice. --- .woodpecker.yaml | 22 ++++------------------ pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 35d52d67..5b4fa587 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -1,7 +1,7 @@ # -*- yaml -*- steps: - - name: codespell + - name: pychecks when: - event: [manual, pull_request] - event: [push, tag] @@ -11,26 +11,12 @@ steps: - apk add openssl - apk add python3 - apk add py3-pip - - python3 -m venv codespell-venv - - source codespell-venv/bin/activate + - python3 -m venv venv + - source venv/bin/activate - pip install codespell - - codespell - - deactivate - - - name: mypy - when: - - event: [manual, pull_request] - - event: [push, tag] - branch: [master, releases/*] - image: alpine:edge - commands: - - apk add openssl - - apk add python3 - - apk add py3-pip - - python3 -m venv mypy-venv - - source mypy-venv/bin/activate - pip install mypy - pip install ruff + - codespell - mypy - ruff check - deactivate diff --git a/pyproject.toml b/pyproject.toml index 654e632d..f5fc08a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,5 +6,5 @@ files = '$MYPY_CONFIG_FILE_DIR/scripts' strict = true [tool.codespell] -skip = 'pyproject.toml,./subprojects,./pkg,./src,./bld,foot.info,./unicode,.*-venv' +skip = 'pyproject.toml,./subprojects,./pkg,./src,./bld,foot.info,./unicode,./venv' ignore-regex = 'terminfo capability `rin`|\* Simon Ser|\* \[zar\]\(https://codeberg.org/zar\)|iterm theme|iterm.toml|iterm/OneHalfDark.itermcolors' \ No newline at end of file From 53e8fbbdec5779d59341b37e0b630b06ecf6951a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 26 Dec 2025 17:25:28 +0100 Subject: [PATCH 77/84] ci: python: upgrade pip before installing python packages --- .woodpecker.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 5b4fa587..900251a7 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -13,6 +13,7 @@ steps: - apk add py3-pip - python3 -m venv venv - source venv/bin/activate + - python -m pip install --upgrade pip - pip install codespell - pip install mypy - pip install ruff From 42e04c5c8741f739295f7da0b1bb1effda95547f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 28 Dec 2025 11:37:54 +0100 Subject: [PATCH 78/84] csi: secondary DA: fix comment; we don't use an XTerm version number --- csi.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/csi.c b/csi.c index 0cf8e203..87af215e 100644 --- a/csi.c +++ b/csi.c @@ -1644,10 +1644,10 @@ csi_dispatch(struct terminal *term, uint8_t final) * 64 - vt520 * 65 - vt525 * - * Param 2 - firmware version - * xterm uses its version number. We use an xterm - * version number too, since e.g. Emacs uses this to - * determine level of support. + * 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. From b78cc92322dd2ae431dbbb70bc681b4d603d844d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 4 Jan 2026 07:57:25 +0100 Subject: [PATCH 79/84] shm: don't bother with xrgb surfaces, always use argb Before this patch, foot used xrgb surfaces for all fully opaque surfaces, and only used argb surfaces for the main window when the user enabled translucency. However, several compositors have damage-like issues when we switch between opaque and non-opaque surfaces (for example, when switching color theme, or when toggling fullscreen). Since the performance benefit of using non-alpha aware surfaces are likely minor (if there's any measurable performance difference at all!), lets workaround these compositor issues by always using argb surfaces. --- CHANGELOG.md | 5 ++++ render.c | 17 +++++------ shm.c | 84 ++++++++++++++++------------------------------------ shm.h | 5 ++-- sixel.c | 4 +-- wayland.c | 3 -- wayland.h | 3 -- 7 files changed, 42 insertions(+), 79 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fa7439e..0462666c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,6 +92,11 @@ contains at least one upper case character. * Mouse tracking in SGR pixel mode no longer emits negative column/row pixel values ([#2226][2226]). +* Foot now always uses ARGB SHM surfaces. In earlier versions, XRGB + surfaces were used for opaque surfaces. Unfortunately, several + compositors had issues when foot switched between ARGB and XRGB + surfaces (for example when switching color theme, or toggling + fullscreen). [2202]: https://codeberg.org/dnkl/foot/issues/2202 [2226]: https://codeberg.org/dnkl/foot/issues/2226 diff --git a/render.c b/render.c index ac8ece37..86244434 100644 --- a/render.c +++ b/render.c @@ -2012,7 +2012,7 @@ render_overlay(struct terminal *term) } struct buffer *buf = shm_get_buffer( - term->render.chains.overlay, term->width, term->height, true); + 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() */ @@ -2970,7 +2970,7 @@ render_csd(struct terminal *term) } struct buffer *bufs[CSD_SURF_COUNT]; - shm_get_many(term->render.chains.csd, CSD_SURF_COUNT, widths, heights, bufs, true); + 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]); @@ -3110,7 +3110,7 @@ render_scrollback_position(struct terminal *term) } struct buffer_chain *chain = term->render.chains.scrollback_indicator; - struct buffer *buf = shm_get_buffer(chain, width, height, false); + struct buffer *buf = shm_get_buffer(chain, width, height); wl_subsurface_set_position( win->scrollback_indicator.sub, roundf(x / scale), roundf(y / scale)); @@ -3153,7 +3153,7 @@ render_render_timer(struct terminal *term, struct timespec render_time) height = roundf(scale * ceilf(height / scale)); struct buffer_chain *chain = term->render.chains.render_timer; - struct buffer *buf = shm_get_buffer(chain, width, height, false); + struct buffer *buf = shm_get_buffer(chain, width, height); wl_subsurface_set_position( win->render_timer.sub, @@ -3336,10 +3336,7 @@ grid_render(struct terminal *term) xassert(term->height > 0); struct buffer_chain *chain = term->render.chains.grid; - bool use_alpha = !term->window->is_fullscreen && - term->colors.alpha != 0xffff; - struct buffer *buf = shm_get_buffer( - chain, term->width, term->height, use_alpha); + 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); @@ -3787,7 +3784,7 @@ render_search_box(struct terminal *term) 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, true); + struct buffer *buf = shm_get_buffer(chain, width, height); pixman_region32_t clip; pixman_region32_init_rect(&clip, 0, 0, width, height); @@ -4252,7 +4249,7 @@ render_urls(struct terminal *term) struct buffer_chain *chain = term->render.chains.url; struct buffer *bufs[render_count]; - shm_get_many(chain, render_count, widths, heights, bufs, false); + shm_get_many(chain, render_count, widths, heights, bufs); uint32_t fg = term->conf->colors_dark.use_custom.jump_label ? term->conf->colors_dark.jump_label.fg diff --git a/shm.c b/shm.c index f488d6b6..5c1573ad 100644 --- a/shm.c +++ b/shm.c @@ -84,7 +84,6 @@ struct buffer_private { struct buffer_pool *pool; off_t offset; /* Offset into memfd where data begins */ size_t size; - bool with_alpha; bool scrollable; @@ -98,11 +97,8 @@ struct buffer_chain { size_t pix_instances; bool scrollable; - pixman_format_code_t pixman_fmt_without_alpha; - enum wl_shm_format shm_format_without_alpha; - - pixman_format_code_t pixman_fmt_with_alpha; - enum wl_shm_format shm_format_with_alpha; + pixman_format_code_t pixman_fmt; + enum wl_shm_format shm_format; void (*release_cb)(struct buffer *buf, void *data); void *cb_data; @@ -285,9 +281,7 @@ instantiate_offset(struct buffer_private *buf, off_t new_offset) wl_buf = wl_shm_pool_create_buffer( pool->wl_pool, new_offset, buf->public.width, buf->public.height, buf->public.stride, - buf->with_alpha - ? buf->chain->shm_format_with_alpha - : buf->chain->shm_format_without_alpha); + buf->chain->shm_format); if (wl_buf == NULL) { LOG_ERR("failed to create SHM buffer"); @@ -297,9 +291,7 @@ instantiate_offset(struct buffer_private *buf, off_t new_offset) /* One pixman image for each worker thread (do we really need multiple?) */ for (size_t i = 0; i < buf->public.pix_instances; i++) { pix[i] = pixman_image_create_bits_no_clear( - buf->with_alpha - ? buf->chain->pixman_fmt_with_alpha - : buf->chain->pixman_fmt_without_alpha, + buf->chain->pixman_fmt, buf->public.width, buf->public.height, (uint32_t *)mmapped, buf->public.stride); @@ -334,8 +326,7 @@ err: static void NOINLINE get_new_buffers(struct buffer_chain *chain, size_t count, int widths[static count], int heights[static count], - struct buffer *bufs[static count], bool with_alpha, - bool immediate_purge) + struct buffer *bufs[static count], bool immediate_purge) { xassert(count == 1 || !chain->scrollable); /* @@ -354,10 +345,7 @@ get_new_buffers(struct buffer_chain *chain, size_t count, size_t total_size = 0; for (size_t i = 0; i < count; i++) { stride[i] = stride_for_format_and_width( - with_alpha - ? chain->pixman_fmt_with_alpha - : chain->pixman_fmt_without_alpha, - widths[i]); + chain->pixman_fmt, widths[i]); if (min_stride_alignment > 0) { const size_t m = min_stride_alignment; @@ -521,7 +509,6 @@ get_new_buffers(struct buffer_chain *chain, size_t count, .chain = chain, .ref_count = immediate_purge ? 0 : 1, .busy = true, - .with_alpha = with_alpha, .pool = pool, .offset = 0, .size = sizes[i], @@ -593,13 +580,13 @@ shm_did_not_use_buf(struct buffer *_buf) void shm_get_many(struct buffer_chain *chain, size_t count, int widths[static count], int heights[static count], - struct buffer *bufs[static count], bool with_alpha) + struct buffer *bufs[static count]) { - get_new_buffers(chain, count, widths, heights, bufs, with_alpha, true); + get_new_buffers(chain, count, widths, heights, bufs, true); } struct buffer * -shm_get_buffer(struct buffer_chain *chain, int width, int height, bool with_alpha) +shm_get_buffer(struct buffer_chain *chain, int width, int height) { LOG_DBG( "chain=%p: looking for a reusable %dx%d buffer " @@ -610,9 +597,7 @@ shm_get_buffer(struct buffer_chain *chain, int width, int height, bool with_alph tll_foreach(chain->bufs, it) { struct buffer_private *buf = it->item; - if (buf->public.width != width || buf->public.height != height || - with_alpha != buf->with_alpha) - { + if (buf->public.width != width || buf->public.height != height) { LOG_DBG("purging mismatching buffer %p", (void *)buf); if (buffer_unref_no_remove_from_chain(buf)) tll_remove(chain->bufs, it); @@ -663,7 +648,7 @@ shm_get_buffer(struct buffer_chain *chain, int width, int height, bool with_alph } struct buffer *ret; - get_new_buffers(chain, 1, &width, &height, &ret, with_alpha, false); + get_new_buffers(chain, 1, &width, &height, &ret, false); return ret; } @@ -1009,11 +994,8 @@ shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, enum shm_bit_depth desired_bit_depth, void (*release_cb)(struct buffer *buf, void *data), void *cb_data) { - pixman_format_code_t pixman_fmt_without_alpha = PIXMAN_x8r8g8b8; - enum wl_shm_format shm_fmt_without_alpha = WL_SHM_FORMAT_XRGB8888; - - pixman_format_code_t pixman_fmt_with_alpha = PIXMAN_a8r8g8b8; - enum wl_shm_format shm_fmt_with_alpha = WL_SHM_FORMAT_ARGB8888; + pixman_format_code_t pixman_fmt = PIXMAN_a8r8g8b8; + enum wl_shm_format shm_fmt = WL_SHM_FORMAT_ARGB8888; static bool have_logged = false; static bool have_logged_10_fallback = false; @@ -1022,12 +1004,9 @@ shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, static bool have_logged_16_fallback = false; if (desired_bit_depth == SHM_BITS_16) { - if (wayl->shm_have_abgr161616 && wayl->shm_have_xbgr161616) { - pixman_fmt_without_alpha = PIXMAN_a16b16g16r16; - shm_fmt_without_alpha = WL_SHM_FORMAT_XBGR16161616; - - pixman_fmt_with_alpha = PIXMAN_a16b16g16r16; - shm_fmt_with_alpha = WL_SHM_FORMAT_ABGR16161616; + if (wayl->shm_have_abgr161616) { + pixman_fmt = PIXMAN_a16b16g16r16; + shm_fmt = WL_SHM_FORMAT_ABGR16161616; if (!have_logged) { have_logged = true; @@ -1045,15 +1024,10 @@ shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, } #endif - if (desired_bit_depth >= SHM_BITS_10 && - pixman_fmt_with_alpha == PIXMAN_a8r8g8b8) - { - if (wayl->shm_have_argb2101010 && wayl->shm_have_xrgb2101010) { - pixman_fmt_without_alpha = PIXMAN_x2r10g10b10; - shm_fmt_without_alpha = WL_SHM_FORMAT_XRGB2101010; - - pixman_fmt_with_alpha = PIXMAN_a2r10g10b10; - shm_fmt_with_alpha = WL_SHM_FORMAT_ARGB2101010; + if (desired_bit_depth >= SHM_BITS_10 && pixman_fmt == PIXMAN_a8r8g8b8) { + if (wayl->shm_have_argb2101010) { + pixman_fmt = PIXMAN_a2r10g10b10; + shm_fmt = WL_SHM_FORMAT_ARGB2101010; if (!have_logged) { have_logged = true; @@ -1061,12 +1035,9 @@ shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, } } - else if (wayl->shm_have_abgr2101010 && wayl->shm_have_xbgr2101010) { - pixman_fmt_without_alpha = PIXMAN_x2b10g10r10; - shm_fmt_without_alpha = WL_SHM_FORMAT_XBGR2101010; - - pixman_fmt_with_alpha = PIXMAN_a2b10g10r10; - shm_fmt_with_alpha = WL_SHM_FORMAT_ABGR2101010; + else if (wayl->shm_have_abgr2101010) { + pixman_fmt = PIXMAN_a2b10g10r10; + shm_fmt = WL_SHM_FORMAT_ABGR2101010; if (!have_logged) { have_logged = true; @@ -1098,11 +1069,8 @@ shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, .pix_instances = pix_instances, .scrollable = scrollable, - .pixman_fmt_without_alpha = pixman_fmt_without_alpha, - .shm_format_without_alpha = shm_fmt_without_alpha, - - .pixman_fmt_with_alpha = pixman_fmt_with_alpha, - .shm_format_with_alpha = shm_fmt_with_alpha, + .pixman_fmt = pixman_fmt, + .shm_format = shm_fmt, .release_cb = release_cb, .cb_data = cb_data, @@ -1129,7 +1097,7 @@ shm_chain_free(struct buffer_chain *chain) enum shm_bit_depth shm_chain_bit_depth(const struct buffer_chain *chain) { - const pixman_format_code_t fmt = chain->pixman_fmt_with_alpha; + const pixman_format_code_t fmt = chain->pixman_fmt; return fmt == PIXMAN_a8r8g8b8 ? SHM_BITS_8 diff --git a/shm.h b/shm.h index 84eb4386..c58a8531 100644 --- a/shm.h +++ b/shm.h @@ -65,8 +65,7 @@ enum shm_bit_depth shm_chain_bit_depth(const struct buffer_chain *chain); * * A newly allocated buffer has an age of 1234. */ -struct buffer *shm_get_buffer( - struct buffer_chain *chain, int width, int height, bool with_alpha); +struct buffer *shm_get_buffer(struct buffer_chain *chain, int width, int height); /* * Returns many buffers, described by 'info', all sharing the same SHM * buffer pool. @@ -84,7 +83,7 @@ struct buffer *shm_get_buffer( void shm_get_many( struct buffer_chain *chain, size_t count, int widths[static count], int heights[static count], - struct buffer *bufs[static count], bool with_alpha); + struct buffer *bufs[static count]); void shm_did_not_use_buf(struct buffer *buf); diff --git a/sixel.c b/sixel.c index 07b97f46..294864fd 100644 --- a/sixel.c +++ b/sixel.c @@ -125,12 +125,12 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) * that assumes 32-bit pixels). */ if (shm_chain_bit_depth(term->render.chains.grid) >= SHM_BITS_10) { - if (term->wl->shm_have_argb2101010 && term->wl->shm_have_xrgb2101010) { + if (term->wl->shm_have_argb2101010) { term->sixel.use_10bit = true; term->sixel.pixman_fmt = PIXMAN_a2r10g10b10; } - else if (term->wl->shm_have_abgr2101010 && term->wl->shm_have_xbgr2101010) { + else if (term->wl->shm_have_abgr2101010) { term->sixel.use_10bit = true; term->sixel.pixman_fmt = PIXMAN_a2b10g10r10; } diff --git a/wayland.c b/wayland.c index 6785c52d..958e535d 100644 --- a/wayland.c +++ b/wayland.c @@ -240,11 +240,8 @@ shm_format(void *data, struct wl_shm *wl_shm, uint32_t format) struct wayland *wayl = data; switch (format) { - case WL_SHM_FORMAT_XRGB2101010: wayl->shm_have_xrgb2101010 = true; break; case WL_SHM_FORMAT_ARGB2101010: wayl->shm_have_argb2101010 = true; break; - case WL_SHM_FORMAT_XBGR2101010: wayl->shm_have_xbgr2101010 = true; break; case WL_SHM_FORMAT_ABGR2101010: wayl->shm_have_abgr2101010 = true; break; - case WL_SHM_FORMAT_XBGR16161616: wayl->shm_have_xbgr161616 = true; break; case WL_SHM_FORMAT_ABGR16161616: wayl->shm_have_abgr161616 = true; break; } diff --git a/wayland.h b/wayland.h index 140c2058..1b1c1f4c 100644 --- a/wayland.h +++ b/wayland.h @@ -502,11 +502,8 @@ struct wayland { bool use_shm_release; bool shm_have_argb2101010:1; - bool shm_have_xrgb2101010:1; bool shm_have_abgr2101010:1; - bool shm_have_xbgr2101010:1; bool shm_have_abgr161616:1; - bool shm_have_xbgr161616:1; }; struct wayland *wayl_init( From e2a989785ab32a39aaa8d49a45b8a94b12a12fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 10 Jan 2026 07:35:25 +0100 Subject: [PATCH 80/84] input: execute: add missing 'return true' to a couple of switch cases Without this, the input handling code won't understand the key/mouse event was consumed (i.e. triggered a shortcut), and will continue processing normally (e.g. sending event to the client application). --- input.c | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/input.c b/input.c index 80b028ac..e8e84fb7 100644 --- a/input.c +++ b/input.c @@ -120,10 +120,14 @@ execute_binding(struct seat *seat, struct terminal *term, case BIND_ACTION_SCROLLBACK_UP_MOUSE: if (term->grid == &term->alt) { - if (term->alt_scrolling) + if (term->alt_scrolling) { alternate_scroll(seat, amount, BTN_BACK); - } else - cmd_scrollback_up(term, amount); + return true; + } + } else { + cmd_scrollback_up(term, amount); + return true; + } break; case BIND_ACTION_SCROLLBACK_DOWN_PAGE: @@ -149,10 +153,14 @@ execute_binding(struct seat *seat, struct terminal *term, case BIND_ACTION_SCROLLBACK_DOWN_MOUSE: if (term->grid == &term->alt) { - if (term->alt_scrolling) + if (term->alt_scrolling) { alternate_scroll(seat, amount, BTN_FORWARD); - } else + return true; + } + } else { cmd_scrollback_down(term, amount); + return true; + } break; case BIND_ACTION_SCROLLBACK_HOME: @@ -535,7 +543,7 @@ execute_binding(struct seat *seat, struct terminal *term, case BIND_ACTION_SELECT_QUOTE: selection_start( term, seat->mouse.col, seat->mouse.row, SELECTION_QUOTE_WISE, false); - break; + return true; case BIND_ACTION_SELECT_ROW: selection_start( From 3a2eb80d83d59d194a3d07da227431c634892ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 10 Jan 2026 07:36:17 +0100 Subject: [PATCH 81/84] input: ignore release events after a keyboard shortcut was triggered This fixes an issue with the kitty keyboard protocol, where 'release' events associated with a shortcut was sent to the client application. Example: user triggers "scroll up". We scroll up. No key event(s) are sent to the client application. Then the user releases the keys. we don't do any shortcut handling on release events, and so we continue with the normal input processing. If the kitty keyboard protocol has been enabled (and specifically, release event reporting has been enabled), then we'll emit a 'release' escape sequence. This in itself is wrong, since the client application never saw the corresponding press event. But we _also_ reset the viewport. The effect (in this example), is that it's impossible to scroll up in the scrollback history. Note that we don't ignore _any_ release event, only the release event for the (final) symbol that triggered the shortcut. This should allow e.g. modifier keys release events to be processed normally, if released before the shortcut key. This is somewhat important, since the client application will have received press events for the modifier keys leading up to the shortcut (if modifier press/release events have been enabled in the kitty keyboard protocol - _Report all keys as escape codes_). Closes #2257 --- CHANGELOG.md | 3 +++ input.c | 24 ++++++++++++++++++++++++ wayland.h | 2 ++ 3 files changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0462666c..a69f58b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -122,8 +122,11 @@ * Crash when reverse-scrolling (terminfo capability `rin`) such that the current viewport ends up outside the scrollback ([#2232][2232]). * Regression: visual glitches in rare circumstances. +* Key release events for shortcuts being sent to the client + application (kitty keyboard protocol only) ([#2257][2257]). [2232]: https://codeberg.org/dnkl/foot/issues/2232 +[2257]: https://codeberg.org/dnkl/foot/issues/2257 ### Security diff --git a/input.c b/input.c index e8e84fb7..aa6b7f1d 100644 --- a/input.c +++ b/input.c @@ -1605,6 +1605,9 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, 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); @@ -1706,6 +1709,7 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, if (bind->k.sym == raw_syms[i] && execute_binding(seat, term, bind, serial, 1)) { + seat->kbd.last_shortcut_sym = sym; goto maybe_repeat; } } @@ -1719,6 +1723,7 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, bind->mods == (mods & ~consumed) && execute_binding(seat, term, bind, serial, 1)) { + seat->kbd.last_shortcut_sym = sym; goto maybe_repeat; } } @@ -1734,12 +1739,31 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, if (code->item == key && execute_binding(seat, term, bind, serial, 1)) { + seat->kbd.last_shortcut_sym = sym; goto maybe_repeat; } } } } + 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; + goto maybe_repeat; + } + /* * Keys generating escape sequences */ diff --git a/wayland.h b/wayland.h index 1b1c1f4c..6247875a 100644 --- a/wayland.h +++ b/wayland.h @@ -151,6 +151,8 @@ struct seat { bool alt; bool ctrl; bool super; + + xkb_keysym_t last_shortcut_sym; } kbd; /* Pointer state */ From 6fbb9b7d3b2b43e69ca3ce56823fdb9f65a74890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 28 Jan 2026 09:21:57 +0100 Subject: [PATCH 82/84] sixel: force a height of at least one sixel when explicitly resizing Applications often prefix the sixel with a raster attributes (RA) sequence, where they tell us how large the sixel is. Strictly speaking, this just tells us the size of the area to clear, but we use it as a hint and pre-allocates the image buffer. It's important to stress that it is valid to emit a MxN RA, and then write sixel data outside of that area. Foot handles this, in _most_ cases. We didn't handle the corner case Mx0. I.e. a width > 0, but height == 0. No image buffer was allocated, and we also failed to detect a resize was necessary when the application started printing sixel data. Much of this is for performance reason; we only check the minimum necessary. For example, we only check if going outside the pre-allocated *column* while printing sixels. *Rows* are checked on a graphical newline. In other words, the *current* row has to be valid when writing sixels. And in case of Mx0, it wasn't. Fix by forcing a height of at least one sixel (typically 6 pixels). Closes #2267 --- CHANGELOG.md | 3 +++ sixel.c | 3 +++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a69f58b8..ce1a7d8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -124,9 +124,12 @@ * Regression: visual glitches in rare circumstances. * Key release events for shortcuts being sent to the client application (kitty keyboard protocol only) ([#2257][2257]). +* Crash when application emits sixel RA with a height of 0, a width > + 0, and then starts writing sixel data ([#2267][2267]). [2232]: https://codeberg.org/dnkl/foot/issues/2232 [2257]: https://codeberg.org/dnkl/foot/issues/2257 +[2267]: https://codeberg.org/dnkl/foot/issues/2267 ### Security diff --git a/sixel.c b/sixel.c index 294864fd..187f1348 100644 --- a/sixel.c +++ b/sixel.c @@ -1559,6 +1559,9 @@ resize(struct terminal *term, int new_width_mutable, int new_height_mutable) new_height_mutable = term->sixel.max_height; } + if (unlikely(new_height_mutable == 0)) { + new_height_mutable = 6 * term->sixel.pan; + } uint32_t *old_data = term->sixel.image.data; const int old_width = term->sixel.image.width; From 0bf193ef8122b2fd412438c0e405fb085cd62a1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 2 Feb 2026 11:19:07 +0100 Subject: [PATCH 83/84] osc-8: don't log URL + ID when closing --- osc.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osc.c b/osc.c index 9407e7b8..909fd484 100644 --- a/osc.c +++ b/osc.c @@ -525,12 +525,14 @@ osc_uri(struct terminal *term, char *string) id = sdbm_hash(value); } - LOG_DBG("OSC-8: URL=%s, id=%" PRIu64, uri, id); - if (uri[0] == '\0') + if (uri[0] == '\0') { + LOG_DBG("OSC-8: close"); term_osc8_close(term); - else + } else { + LOG_DBG("OSC-8: URL=%s, id=%" PRIu64, uri, id); term_osc8_open(term, id, uri); + } } static void From c291194a4e593bbbb91420e81fa0111508084448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 28 Jan 2026 09:44:57 +0100 Subject: [PATCH 84/84] wayland: wait for pre-apply damage thread before destroying a terminal instance It's possible, but unlikely, that we've pushed a "pre-apply damage" job to the renderer thread queue (or that we've pushed it, and the a thread is now working on it) when we shutdown a terminal instance. This is sometimes caught in an assertion in term_destroy(), where we check the queue length is 0. Other times, or in release builds, we might crash in the thread, or in the shutdown logic when freeing the buffer chains associated with the terminal instance. Fix by ensuring there's no pre-apply damage operation queued, or running, before shutting down a terminal instance. Closes #2263 --- CHANGELOG.md | 3 +++ render.c | 10 +++++----- render.h | 1 + wayland.c | 2 ++ 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce1a7d8c..cee4ddef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -126,10 +126,13 @@ application (kitty keyboard protocol only) ([#2257][2257]). * Crash when application emits sixel RA with a height of 0, a width > 0, and then starts writing sixel data ([#2267][2267]). +* Crash if shutting down terminal instance while a "pre-apply damage" + thread is running ([#2263][2263]). [2232]: https://codeberg.org/dnkl/foot/issues/2232 [2257]: https://codeberg.org/dnkl/foot/issues/2257 [2267]: https://codeberg.org/dnkl/foot/issues/2267 +[2263]: https://codeberg.org/dnkl/foot/issues/2263 ### Security diff --git a/render.c b/render.c index 86244434..3aa7d543 100644 --- a/render.c +++ b/render.c @@ -2288,8 +2288,8 @@ render_worker_thread(void *_ctx) return -1; } -static void -wait_for_preapply_damage(struct terminal *term) +void +render_wait_for_preapply_damage(struct terminal *term) { if (!term->render.preapply_last_frame_damage) return; @@ -3325,7 +3325,7 @@ grid_render(struct terminal *term) term->render.workers.preapplied_damage.buf != NULL)) { clock_gettime(CLOCK_MONOTONIC, &start_wait_preapply); - wait_for_preapply_damage(term); + render_wait_for_preapply_damage(term); clock_gettime(CLOCK_MONOTONIC, &stop_wait_preapply); } @@ -4401,7 +4401,7 @@ delayed_reflow_of_normal_grid(struct terminal *term) term->interactive_resizing.old_hide_cursor = false; /* Invalidate render pointers */ - wait_for_preapply_damage(term); + render_wait_for_preapply_damage(term); shm_unref(term->render.last_buf); term->render.last_buf = NULL; term->render.last_cursor.row = NULL; @@ -4976,7 +4976,7 @@ damage_view: tll_free(term->normal.scroll_damage); tll_free(term->alt.scroll_damage); - wait_for_preapply_damage(term); + render_wait_for_preapply_damage(term); shm_unref(term->render.last_buf); term->render.last_buf = NULL; term_damage_view(term); diff --git a/render.h b/render.h index e21eaca8..e6674ab2 100644 --- a/render.h +++ b/render.h @@ -49,3 +49,4 @@ struct csd_data { struct csd_data get_csd_data(const struct terminal *term, enum csd_surface surf_idx); void render_buffer_release_callback(struct buffer *buf, void *data); +void render_wait_for_preapply_damage(struct terminal *term); diff --git a/wayland.c b/wayland.c index 958e535d..59df991a 100644 --- a/wayland.c +++ b/wayland.c @@ -2129,6 +2129,8 @@ wayl_win_destroy(struct wl_window *win) struct terminal *term = win->term; + render_wait_for_preapply_damage(term); + if (win->csd.move_timeout_fd != -1) close(win->csd.move_timeout_fd);