mirror of
https://github.com/weechat/weechat.git
synced 2026-06-12 14:14:48 +02:00
core: auto-detect terminal background and apply light theme on first start
Detect the terminal background (via COLORFGBG or an OSC 11 query) before GUI init and, on the first start, automatically apply the built-in "light" theme when a light terminal is detected. The function gui_term_theme_is_light returns an int (1 if light, 0 otherwise, 0 being the safe value when detection is unsure).
This commit is contained in:
@@ -17,6 +17,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`)
|
||||
|
||||
+36
-13
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -25,6 +25,14 @@
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/time.h>
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Sébastien Helleu <flashcode@flashtux.org>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* Test terminal functions (Curses interface) */
|
||||
|
||||
#include "CppUTest/TestHarness.h"
|
||||
|
||||
extern "C"
|
||||
{
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user