From 4a2e5df554362e299f6b37fc2a0888bdbecf4aa3 Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Wed, 10 Dec 2025 20:47:32 +0100 Subject: [PATCH] Allow any theme to be dark or light, determine it automatically By definition, "[colors]" is a dark theme and the alternate theme ("[colors2]") is light. A user who doesn't know about this definition (or about "[colors2]"), might configure "[colors]" to be a light theme. This is a reasonable mistake because a. "colors" is an innocuous name b. users who failed to run "git merge-file ~/.config/foot/foot.ini $old_foot/foot.ini $new_foot/foot.ini" after upgrading foot might not have "colors2" in their config. The wrongly reported color theme (CSI 997) causes issues when apps use it for selecting colors. I don't know if any relevant app does, but learning this cost me some time, and maybe it's a good idea to address this, even though it's technically a user error. Solution 1: Stop responding to CSI 996. The Contour spec is [ambiguous](https://github.com/contour-terminal/contour/issues/1659#issuecomment-3596983337). Apps might want to prefer the more widely available OSC 10/11 for detecting dark/light mode. I think Vim does; and at least NeoVim still uses the Contour protocol to subscribe to notifications. We'd still send DSR 2031 notifications, however that would work just fine for apps like NeoVim that ignore the payload and only use it as trigger to query for the theme again via OSC 10/11. (Since we can only switch between two themes, we wouldn't even waste bandwidth.) Solution 2: Assuming the themes are only meant for the dark/light mode toggle, rename them to "colors-dark" and "colors-light", and maybe report color theme only for those (and not when the user has the legacy "colors" and "colors2"). Solution 3 (implemented here): Assuming the themes are intended to be used for things other than dark/light toggle, - have foot automatically detect whether the current theme is dark or light - if needed, allow users to override this (e.g. "colors.is_dark = true") I guess I have a slight preference for solution 2 because it seems relatively simple. But I don't know what's the goal. Apparently switching to dark/light mode at dusk/dawn is a feature of macOS and there are solutions like darkmon for other OSes, so I guess dynamic switching is a useful feature in principle. --- CHANGELOG.md | 1 + config.c | 38 ++++++++++++++++++++++++++++++++++++-- config.h | 5 +++++ doc/foot-ctlseqs.7.scd | 4 +--- doc/foot.ini.5.scd | 7 +------ foot.ini | 5 ++--- render.c | 12 ------------ subprojects/.wraplock | 0 terminal.c | 8 +++----- tests/meson.build | 1 + 10 files changed, 50 insertions(+), 31 deletions(-) create mode 100644 subprojects/.wraplock diff --git a/CHANGELOG.md b/CHANGELOG.md index a293c721..e76f985b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ ## Unreleased ### Added +* TODO * `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` diff --git a/config.c b/config.c index 515b088c..6c2d355d 100644 --- a/config.c +++ b/config.c @@ -25,6 +25,7 @@ #include "input.h" #include "key-binding.h" #include "macros.h" +#include "srgb.h" #include "tokenize.h" #include "util.h" #include "xmalloc.h" @@ -3185,6 +3186,16 @@ parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_ar errno = 0; } + struct color_theme *color_themes[] = {&conf->colors, &conf->colors2}; + for (int i = 0; i < ALEN(color_themes); i++) { + struct color_theme *color_theme = color_themes[i]; + if (color_theme->dim_blend_towards != DIM_BLEND_TOWARDS_INVALID) + continue; + color_theme->dim_blend_towards = + is_dark_theme(color_theme->fg, color_theme->bg) + ? DIM_BLEND_TOWARDS_BLACK : DIM_BLEND_TOWARDS_WHITE; + } + if (errno != 0) { LOG_AND_NOTIFY_ERRNO("failed to read from configuration"); if (errors_are_fatal) @@ -3442,7 +3453,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, + .dim_blend_towards = DIM_BLEND_TOWARDS_INVALID, .selection_fg = 0x80000000, /* Use default bg */ .selection_bg = 0x80000000, /* Use default fg */ .cursor = { @@ -3538,7 +3549,6 @@ 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); @@ -4096,6 +4106,30 @@ check_if_font_is_monospaced(const char *pattern, return is_monospaced; } +pixman_color_t +color_hex_to_pixman_srgb(uint32_t color, uint16_t alpha) +{ + return (pixman_color_t){ + .alpha = alpha, /* Consider alpha linear already? */ + .red = srgb_decode_8_to_16((color >> 16) & 0xff), + .green = srgb_decode_8_to_16((color >> 8) & 0xff), + .blue = srgb_decode_8_to_16((color >> 0) & 0xff), + }; +} + +// See https://en.wikipedia.org/wiki/Relative_luminance +static float relative_luminance(uint32_t rgb) +{ + pixman_color_t srgb = + color_hex_to_pixman_srgb(rgb, /*alpha=*/UINT16_MAX); + return 0.2126 * srgb.red + 0.7152 * srgb.green + 0.0722 * srgb.blue; +} + +bool is_dark_theme(uint32_t fg, uint32_t bg) +{ + return relative_luminance(bg) < relative_luminance(fg); +} + #if 0 xkb_mod_mask_t conf_modifiers_to_mask(const struct seat *seat, diff --git a/config.h b/config.h index fc5e290e..a4a01fac 100644 --- a/config.h +++ b/config.h @@ -148,6 +148,7 @@ struct color_theme { enum { DIM_BLEND_TOWARDS_BLACK, DIM_BLEND_TOWARDS_WHITE, + DIM_BLEND_TOWARDS_INVALID, } dim_blend_towards; enum { @@ -473,3 +474,7 @@ conf_modifiers_to_mask( #endif bool check_if_font_is_monospaced( const char *pattern, user_notifications_t *notifications); + +pixman_color_t +color_hex_to_pixman_srgb(uint32_t color, uint16_t alpha); +bool is_dark_theme(uint32_t fg, uint32_t bg); diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index 40906ebf..86f36492 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -664,9 +664,7 @@ manipulation sequences. The generic format is: : Query the current (color) theme mode : contour : The current color theme mode (light or dark) is reported as *CSI ? - 997 ; 1|2 n*, where *1* means dark and *2* light. By convention, the - primary theme in foot is considered dark, and the alternative theme - light. + 997 ; 1|2 n*, where *1* means dark and *2* light. # OSC diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index c9782895..20721056 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1138,17 +1138,12 @@ 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, except -for *dim-blend-towards*, which defaults to *white* instead. +keys and default values as the *colors* section. 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 diff --git a/foot.ini b/foot.ini index 2d170489..09950b64 100644 --- a/foot.ini +++ b/foot.ini @@ -132,7 +132,7 @@ # bright7=ffffff # bright white ## dimmed colors (see foot.ini(5) man page) -# dim-blend-towards=black +# dim-blend-towards= # dim0= # ... # dim7= @@ -171,8 +171,7 @@ [colors2] # Alternative color theme, see man page foot.ini(5) -# Same builtin defaults as [color], except for: -# dim-blend-towards=white +# Same builtin defaults as [color] [csd] # preferred=server diff --git a/render.c b/render.c index 1d0f08af..6e079016 100644 --- a/render.c +++ b/render.c @@ -40,7 +40,6 @@ #include "selection.h" #include "shm.h" #include "sixel.h" -#include "srgb.h" #include "url-mode.h" #include "util.h" #include "xmalloc.h" @@ -229,17 +228,6 @@ attrs_to_font(const struct terminal *term, const struct attributes *attrs) return term->fonts[idx]; } -static pixman_color_t -color_hex_to_pixman_srgb(uint32_t color, uint16_t alpha) -{ - return (pixman_color_t){ - .alpha = alpha, /* Consider alpha linear already? */ - .red = srgb_decode_8_to_16((color >> 16) & 0xff), - .green = srgb_decode_8_to_16((color >> 8) & 0xff), - .blue = srgb_decode_8_to_16((color >> 0) & 0xff), - }; -} - static inline pixman_color_t color_hex_to_pixman_with_alpha(uint32_t color, uint16_t alpha, bool srgb) { diff --git a/subprojects/.wraplock b/subprojects/.wraplock new file mode 100644 index 00000000..e69de29b diff --git a/terminal.c b/terminal.c index 3ef53daa..d24e03ca 100644 --- a/terminal.c +++ b/terminal.c @@ -1,4 +1,6 @@ #include "terminal.h" +#include "pixman.h" +#include #if defined(__GLIBC__) #include @@ -4785,10 +4787,6 @@ term_theme_toggle(struct terminal *term) /* * 1 - dark mode * 2 - light mode - * - * In foot, the themes aren't necessarily light/dark, - * but by convention, the primary theme is dark, and - * the alternative theme is light. */ void term_send_color_theme_mode(struct terminal* term) { @@ -4796,6 +4794,6 @@ void term_send_color_theme_mode(struct terminal* term) "\033[?997;2n", "\033[?997;1n", }; - const bool is_dark = term->colors.active_theme == COLOR_THEME1; + const bool is_dark = is_dark_theme(term->colors.fg, term->colors.bg); term_to_slave(term, reply[is_dark], 9); } diff --git a/tests/meson.build b/tests/meson.build index 9baa064c..3d126973 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -1,6 +1,7 @@ config_test = executable( 'test-config', 'test-config.c', + srgb_funcs, wl_proto_headers, link_with: [common, tokenize], dependencies: [pixman, xkb, fontconfig, wayland_client, fcft, tllist])