diff --git a/CHANGELOG.md b/CHANGELOG.md index ba5592740..9643fda36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ SPDX-License-Identifier: GPL-3.0-or-later ### Added - core: add `/theme` command with subcommands `list`, `apply`, `reset`, `save`, `delete`, `info`, automatic backup of current themable options before apply, and built-in "light" theme +- core: detect terminal background on first start and automatically apply the built-in "light" theme when a light terminal is detected - core: add `themable` flag on configuration options (auto-set for color options; explicit opt-in for string options containing `${color:...}` references via the `type|themable` syntax) - core: add option weechat.look.theme (informational, set by `/theme apply`) - core: add option weechat.look.theme_backup (boolean, default `on`) diff --git a/src/core/weechat.c b/src/core/weechat.c index 2ade07fa5..b8f4b0917 100644 --- a/src/core/weechat.c +++ b/src/core/weechat.c @@ -98,6 +98,7 @@ char *weechat_argv0 = NULL; /* WeeChat binary file name (argv[0])*/ int weechat_upgrading = 0; /* =1 if WeeChat is upgrading */ int weechat_first_start = 0; /* first start of WeeChat? */ time_t weechat_first_start_time = 0; /* start time (used by /uptime cmd) */ +int weechat_term_theme_light = 0; /* 1 if light theme detected */ int weechat_upgrade_count = 0; /* number of /upgrade done */ struct timeval weechat_current_start_timeval; /* start time used to display */ /* duration of /upgrade */ @@ -183,22 +184,34 @@ weechat_startup_message (void) if (weechat_first_start) { /* message on first run (when weechat.conf is created) */ - gui_chat_printf (NULL, ""); + gui_chat_printf (NULL, _("Welcome to WeeChat!")); gui_chat_printf ( NULL, - _("Welcome to WeeChat!\n" - "\n" - "If you are discovering WeeChat, it is recommended to read at " - "least the quickstart guide, and the user's guide if you have " - "some time; they explain main WeeChat concepts.\n" - "All WeeChat docs are available at: https://weechat.org/doc/\n" - "\n" - "Moreover, there is inline help with /help on all commands and " - "options (use Tab key to complete the name).\n" - "The command /fset can help to customize WeeChat.\n" - "\n" - "You can add and connect to an IRC server with /server and " + _("If you are discovering WeeChat, it is recommended to " + "read at least the quickstart guide, and the user's guide if " + "you have some time; they explain main WeeChat concepts.")); + gui_chat_printf ( + NULL, + _("All WeeChat docs are available at: %s"), + WEECHAT_WEBSITE_DOC); + gui_chat_printf ( + NULL, + _("Moreover, there is inline help with /help on all commands and " + "options (use Tab key to complete the name).")); + gui_chat_printf ( + NULL, + _("The command /fset can help to customize WeeChat.")); + gui_chat_printf ( + NULL, + _("You can add and connect to an IRC server with /server and " "/connect commands (see /help server).")); + if (weechat_term_theme_light) + { + gui_chat_printf ( + NULL, + _("The \"light\" theme will be automatically applied. " + "Use /theme reset to switch back to the default dark theme.")); + } gui_chat_printf (NULL, ""); gui_chat_printf (NULL, "---"); gui_chat_printf (NULL, ""); @@ -369,6 +382,9 @@ weechat_init (int argc, char *argv[], void (*gui_init_cb)(void)) * weechat_current_start_timeval.tv_usec) ^ getpid ()); + /* detect the terminal theme, before initializing the GUI */ + weechat_term_theme_light = gui_term_theme_is_light (); + weeurl_init (); /* initialize URL */ string_init (); /* initialize string */ signal_init (); /* initialize signals */ @@ -426,6 +442,13 @@ weechat_init (int argc, char *argv[], void (*gui_init_cb)(void)) weechat_doc_gen_ok = doc_generate (weechat_doc_gen_path); weechat_quit = 1; } + + if (weechat_first_start && isatty (STDOUT_FILENO) && !weechat_headless && !weechat_doc_gen) + { + /* switch to "light" theme if terminal background was detected as "light" */ + if (weechat_term_theme_light) + theme_apply ("light"); + } } /* diff --git a/src/core/weechat.h b/src/core/weechat.h index 9259e0978..1ec562534 100644 --- a/src/core/weechat.h +++ b/src/core/weechat.h @@ -55,6 +55,7 @@ #define WEECHAT_COPYRIGHT_DATE "(C) 2003-2026" #define WEECHAT_WEBSITE "https://weechat.org/" +#define WEECHAT_WEBSITE_DOC "https://weechat.org/doc/" #define WEECHAT_WEBSITE_DOWNLOAD "https://weechat.org/download/" #define WEECHAT_AUTHOR_NAME "Sébastien Helleu" #define WEECHAT_AUTHOR_EMAIL "flashcode@flashtux.org" diff --git a/src/gui/curses/gui-curses-term.c b/src/gui/curses/gui-curses-term.c index d31dee2f3..cb3c57642 100644 --- a/src/gui/curses/gui-curses-term.c +++ b/src/gui/curses/gui-curses-term.c @@ -25,6 +25,14 @@ #include "config.h" #endif +#include +#include +#include +#include +#include +#include +#include + #ifndef WEECHAT_HEADLESS #ifdef HAVE_NCURSESW_CURSES_H #ifdef __sun @@ -37,6 +45,8 @@ #endif /* HAVE_NCURSESW_CURSES_H */ #endif /* WEECHAT_HEADLESS */ +#include "../../core/weechat.h" + /* * Set "eat_newline_glitch" variable. @@ -56,3 +66,139 @@ gui_term_set_eat_newline_glitch (int value) (void) value; #endif } + +/* + * Auto-detects the terminal background as "light" or "dark". + * + * Best-effort, in order: + * + * 1. Environment variable COLORFGBG (set by rxvt, urxvt, Konsole, + * ...): "fg;bg" or "fg;default;bg"; the last ';'-separated + * component is the bg ANSI color index. 0-6 + 8 are dark, + * 7 + 9-15 are light. + * + * 2. OSC 11 escape query on /dev/tty: write "\033]11;?\033\\", + * wait up to 100 ms for a reply of the form + * "...rgb:RRRR/GGGG/BBBB..." (each component is 1-4 hex digits + * depending on the terminal). Classify the answer by Rec. 601 + * luminance computed on the high nibble of each component + * (resolution is plenty for a light/dark binary decision and + * avoids width-normalization headaches). + * + * /dev/tty is used rather than stdin/stdout so a pipe redirection + * does not defeat detection. The select() timeout caps how long a + * silent terminal can stall startup. Headless mode and any I/O error + * short-circuit to the safe default 0 (dark), which is also returned + * when both probes are inconclusive. + * + * MUST be called before curses init: it briefly puts the tty in raw + * mode and reads/writes escape sequences directly. + * + * Returns 1 if a light background is detected, otherwise 0 (0 is the + * preferred value when detection is unsure). + */ + +int +gui_term_theme_is_light (void) +{ + const char *colorfgbg, *p; + char *endptr; + long bg; + int fd, n, i, len; + struct termios old_attr, new_attr; + fd_set rfds; + struct timeval tv; + char buf[256], *q, *qend; + unsigned long comp_high[3], luminance; + + if (weechat_headless) + return 0; + + /* 1. COLORFGBG */ + colorfgbg = getenv ("COLORFGBG"); + if (colorfgbg && colorfgbg[0]) + { + p = strrchr (colorfgbg, ';'); + if (p) + { + bg = strtol (p + 1, &endptr, 10); + if (endptr != p + 1 && *endptr == '\0') + { + if (bg == 7 || (bg >= 9 && bg <= 15)) + return 1; + if ((bg >= 0 && bg <= 6) || bg == 8) + return 0; + } + } + } + + /* 2. OSC 11 query on the controlling terminal */ + fd = open ("/dev/tty", O_RDWR | O_NOCTTY); + if (fd < 0) + return 0; + + if (tcgetattr (fd, &old_attr) != 0) + { + close (fd); + return 0; + } + new_attr = old_attr; + new_attr.c_lflag &= (tcflag_t) ~(ICANON | ECHO); + new_attr.c_cc[VMIN] = 0; + new_attr.c_cc[VTIME] = 0; + if (tcsetattr (fd, TCSANOW, &new_attr) != 0) + { + close (fd); + return 0; + } + + n = -1; + if (write (fd, "\033]11;?\033\\", 8) == 8) + { + FD_ZERO (&rfds); + FD_SET (fd, &rfds); + tv.tv_sec = 0; + tv.tv_usec = 100000; /* 100 ms cap on terminals that won't reply */ + if (select (fd + 1, &rfds, NULL, NULL, &tv) > 0) + n = read (fd, buf, sizeof (buf) - 1); + } + + tcsetattr (fd, TCSANOW, &old_attr); + close (fd); + + if (n <= 0) + return 0; + buf[n] = '\0'; + + q = strstr (buf, "rgb:"); + if (!q) + return 0; + q += 4; + + for (i = 0; i < 3; i++) + { + qend = q; + while ((*qend >= '0' && *qend <= '9') + || (*qend >= 'a' && *qend <= 'f') + || (*qend >= 'A' && *qend <= 'F')) + qend++; + len = (int)(qend - q); + if (len < 1 || len > 4) + return 0; + /* high nibble of this component: width-independent brightness */ + comp_high[i] = strtoul (q, NULL, 16) >> ((len - 1) * 4); + q = qend; + if (i < 2) + { + if (*q != '/') + return 0; + q++; + } + } + + /* Rec. 601 with coefficients summing to 1000; max = 15 * 1000 */ + luminance = (299UL * comp_high[0] + + 587UL * comp_high[1] + + 114UL * comp_high[2]); + return (luminance > 7500) ? 1 : 0; +} diff --git a/src/gui/gui-main.h b/src/gui/gui-main.h index 01ea87ed1..3702842b9 100644 --- a/src/gui/gui-main.h +++ b/src/gui/gui-main.h @@ -31,5 +31,6 @@ extern void gui_main_end (int clean_exit); /* terminal functions (GUI dependent) */ extern void gui_term_set_eat_newline_glitch (int value); +extern int gui_term_theme_is_light (void); #endif /* WEECHAT_GUI_MAIN_H */ diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 2fed50c47..9567ea665 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -114,6 +114,7 @@ set(LIB_WEECHAT_UNIT_TESTS_CORE_SRC gui/test-gui-nick.cpp gui/test-gui-nicklist.cpp gui/curses/test-gui-curses-mouse.cpp + gui/curses/test-gui-curses-term.cpp scripts/test-scripts.cpp ) add_library(weechat_unit_tests_core STATIC ${LIB_WEECHAT_UNIT_TESTS_CORE_SRC}) diff --git a/tests/unit/gui/curses/test-gui-curses-term.cpp b/tests/unit/gui/curses/test-gui-curses-term.cpp new file mode 100644 index 000000000..9522f1a60 --- /dev/null +++ b/tests/unit/gui/curses/test-gui-curses-term.cpp @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: 2026 Sébastien Helleu + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * This file is part of WeeChat, the extensible chat client. + * + * WeeChat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * WeeChat is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with WeeChat. If not, see . + */ + +/* Test terminal functions (Curses interface) */ + +#include "CppUTest/TestHarness.h" + +extern "C" +{ +#include +#include + +extern int gui_term_theme_is_light (void); +} + +/* + * Asserts the value returned by gui_term_theme_is_light() when COLORFGBG + * is set to the given value (a definitive background index, so detection + * returns without falling back to the terminal query on /dev/tty). + */ + +#define WEE_CHECK_THEME(__result, __colorfgbg) \ + setenv ("COLORFGBG", __colorfgbg, 1); \ + LONGS_EQUAL(__result, gui_term_theme_is_light ()); + +TEST_GROUP(GuiCursesTerm) +{ +}; + +/* + * Test functions: + * gui_term_theme_is_light + */ + +TEST(GuiCursesTerm, ThemeIsLight) +{ + const char *saved_colorfgbg; + char *colorfgbg; + + /* save COLORFGBG to restore it at the end of the test */ + saved_colorfgbg = getenv ("COLORFGBG"); + colorfgbg = (saved_colorfgbg) ? strdup (saved_colorfgbg) : NULL; + + /* dark background ("fg;bg"): indices 0-6 and 8 */ + WEE_CHECK_THEME(0, "15;0"); + WEE_CHECK_THEME(0, "15;1"); + WEE_CHECK_THEME(0, "15;6"); + WEE_CHECK_THEME(0, "15;8"); + + /* light background ("fg;bg"): index 7 and 9-15 */ + WEE_CHECK_THEME(1, "0;7"); + WEE_CHECK_THEME(1, "0;9"); + WEE_CHECK_THEME(1, "0;15"); + + /* "fg;default;bg" form: last component is the background */ + WEE_CHECK_THEME(0, "0;default;0"); + WEE_CHECK_THEME(1, "0;default;15"); + + /* restore COLORFGBG */ + if (colorfgbg) + { + setenv ("COLORFGBG", colorfgbg, 1); + free (colorfgbg); + } + else + { + unsetenv ("COLORFGBG"); + } +} diff --git a/tests/unit/tests.cpp b/tests/unit/tests.cpp index 4732a8032..840d5c512 100644 --- a/tests/unit/tests.cpp +++ b/tests/unit/tests.cpp @@ -119,6 +119,7 @@ IMPORT_TEST_GROUP(GuiNick); IMPORT_TEST_GROUP(GuiNicklist); /* GUI - Curses */ IMPORT_TEST_GROUP(GuiCursesMouse); +IMPORT_TEST_GROUP(GuiCursesTerm); /* scripts */ IMPORT_TEST_GROUP(Scripts);