From 728dbaeffb3e72872253c50ca5d1c100cc532634 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 17 Mar 2025 09:34:49 +0100 Subject: [PATCH 1/3] exec: when we have no $TERM configuration, and we default to vt220, also set $COLORTERM When we configure a serial or VM terminal and have no $TERM configuration, then we default to vt220 as a fallback. This is a pretty safe bet, since the termcap/terminfo definitions for vt220 are relatively widely available (much like vt100), and (in contrast to vt100) it supports pageup/pagedown keys. vt220 is a terminal without color support however, but we do want color support, because in 2025 there's really no terminal emulator without color in this world. The $COLORTERM env var is used my many emulators and tools to communicate that ANSI color support is available, despite what $TERM says. Hence, let's tweak systemd's logic to also set $COLORTERM in case we set the vt220 $TERM fallback. This means we define an ahistoric frankenterminal: a vt220 (that historically definitely didn't have color) that is explicitly configured to have color. One effect of this is that coreutils' dircolors command will start to output color sequences in systemd's serial or VM terminals. (Since it actually honours $COLORTERM). Also note that systemd itself checks $COLORTERM since a long time, hence it makes sense for us to also set it. Note that this unfortunately doesn't have the desired effect of propagating $COLORTERM into any getty shell sessions yet. That's because util-linux' login package currently fiters $COLORTERM (but lets $TERM though). I filed a bug about that here: https://github.com/util-linux/util-linux/issues/3463 --- src/core/exec-invoke.c | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index 2cee136fee..6bb4584a8e 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -1912,7 +1912,7 @@ static int build_environment( assert(cgroup_context); assert(ret); -#define N_ENV_VARS 20 +#define N_ENV_VARS 21 our_env = new0(char*, N_ENV_VARS + _EXEC_DIRECTORY_TYPE_MAX); if (!our_env) return -ENOMEM; @@ -2048,8 +2048,28 @@ static int build_environment( term = cmdline; } - if (!term) + if (!term) { + /* If no precise $TERM is known and we pick a fallback default, then let's also set + * $COLORTERM=truecolor. That's because our fallback default is vt220, which is + * generally a safe bet (as it supports PageUp/PageDown unlike vt100, and is quite + * universally available in terminfo/termcap), except for the fact that real DEC + * vt220 gear never actually supported color. Most tools these days generate color on + * vt220 anyway, ignoring the physical capabilities of the real hardware, but some + * tools actually believe in the historical truth. Which is unfortunate since *we* + * *don't* care about the historical truth, we just want sane defaults if nothing + * better is explicitly configured. It's 2025 after all, at the time of writing, + * pretty much all terminal emulators actually *do* support color, hence if we don't + * know any better let's explicitly claim color support via $COLORTERM. Or in other + * words: we now explicitly claim to be connected to a franken-vt220 with true color + * support. */ + x = strdup("COLORTERM=truecolor"); + if (!x) + return -ENOMEM; + + our_env[n_env++] = x; + term = default_term_for_tty(tty_path); + } x = strjoin("TERM=", term); if (!x) From 19aff5f775386a34224f710b88457c1e6bdf0e2f Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 17 Mar 2025 09:45:16 +0100 Subject: [PATCH 2/3] main: explicitly pick up $COLORTERM + $NO_COLOR from kernel cmdline where we pick up $TERM I think we should work towards always picking up the triplet of $TERM + $COLORTERM + $NO_COLOR where we so far picked up $TERM only. I think it's safe to say that at this time, $TERM is not enough anymore to clearly communicate terminal feature support. Hence, teach PID 1 to pick $COLORTERM + $NO_COLOR wherever we pick up $TERM. --- src/core/main.c | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/core/main.c b/src/core/main.c index dc93c42f17..bca6c96b4e 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -1558,8 +1558,6 @@ static int bump_unix_max_dgram_qlen(void) { } static int fixup_environment(void) { - _cleanup_free_ char *term = NULL; - const char *t; int r; /* Only fix up the environment when we are started as PID 1 */ @@ -1575,19 +1573,29 @@ static int fixup_environment(void) { * not have support for color mode for example. * * However if TERM was configured through the kernel command line then leave it alone. */ + _cleanup_free_ char *term = NULL; r = proc_cmdline_get_key("TERM", 0, &term); if (r < 0) return r; - - if (r == 0) { + if (r > 0) { + /* If we pick up $TERM, then also pick up $COLORTERM, $NO_COLOR */ + FOREACH_STRING(v, "COLORTERM", "NO_COLOR") { + _cleanup_free_ char *vv = NULL; + r = proc_cmdline_get_key(v, 0, &vv); + if (r < 0) + return r; + if (r > 0 && setenv(v, vv, /* overwrite= */ true) < 0) + return -errno; + } + } else { + /* If no $TERM is set then look for the per-tty variable instead */ r = proc_cmdline_get_key("systemd.tty.term.console", 0, &term); if (r < 0) return r; } - t = term ?: default_term_for_tty("/dev/console"); - - if (setenv("TERM", t, 1) < 0) + const char *t = term ?: default_term_for_tty("/dev/console"); + if (setenv("TERM", t, /* overwrite= */ true) < 0) return -errno; /* The kernels sets HOME=/ for init. Let's undo this. */ From afdca6c6c2b84aad69f783e0c9395fa2bfc515cc Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 17 Mar 2025 09:52:52 +0100 Subject: [PATCH 3/3] nspawn,run,machinectl,socket-activate: propagate $COLORTERM + $NO_COLOR When we pass information about our calling terminal on to some service or command we invoke, propagate $COLORTERM + $NO_COLOR in addition to $TERM, in order to always consider the triplet of the three env vars the real deal. --- src/machine/machinectl.c | 18 ++++++----- src/nspawn/nspawn.c | 16 ++++++++-- src/run/run.c | 45 +++++++++++++++++++-------- src/socket-activate/socket-activate.c | 2 +- 4 files changed, 56 insertions(+), 25 deletions(-) diff --git a/src/machine/machinectl.c b/src/machine/machinectl.c index b7b7130fdb..366446c306 100644 --- a/src/machine/machinectl.c +++ b/src/machine/machinectl.c @@ -1362,15 +1362,17 @@ static int shell_machine(int argc, char *argv[], void *userdata) { return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Shell only supported on local machines."); - /* Pass $TERM to shell session, if not explicitly specified. */ - if (!strv_find_prefix(arg_setenv, "TERM=")) { - const char *t; + /* Pass $TERM & Co. to shell session, if not explicitly specified. */ + FOREACH_STRING(v, "TERM=", "COLORTERM=", "NO_COLOR=") { + if (strv_find_prefix(arg_setenv, v)) + continue; - t = strv_find_prefix(environ, "TERM="); - if (t) { - if (strv_extend(&arg_setenv, t) < 0) - return log_oom(); - } + const char *t = strv_find_prefix(environ, v); + if (!t) + continue; + + if (strv_extend(&arg_setenv, t) < 0) + return log_oom(); } (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index f19014016f..e3c5d6b732 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -3337,6 +3337,8 @@ static int inner_child( (char*) "PATH=" DEFAULT_PATH_COMPAT, NULL, /* container */ NULL, /* TERM */ + NULL, /* COLORTERM */ + NULL, /* NO_COLOR */ NULL, /* HOME */ NULL, /* USER */ NULL, /* LOGNAME */ @@ -3588,9 +3590,17 @@ static int inner_child( /* LXC sets container=lxc, so follow the scheme here */ envp[n_env++] = strjoina("container=", arg_container_service_name); - /* Propagate $TERM unless we are invoked in pipe mode and stdin/stdout/stderr don't refer to a TTY */ - const char *term = (arg_console_mode != CONSOLE_PIPE || on_tty()) ? strv_find_prefix(environ, "TERM=") : NULL; - envp[n_env++] = (char*) (term ?: "TERM=dumb"); + /* Propagate $TERM & Co. unless we are invoked in pipe mode and stdin/stdout/stderr don't refer to a TTY */ + if (arg_console_mode != CONSOLE_PIPE || on_tty()) { + FOREACH_STRING(v, "TERM=", "COLORTERM=", "NO_COLOR=") { + char *t = strv_find_prefix(environ, v); + if (!t) + continue; + + envp[n_env++] = t; + } + } else + envp[n_env++] = (char*) "TERM=dumb"; if (home || !uid_is_valid(arg_uid) || arg_uid == 0) if (asprintf(envp + n_env++, "HOME=%s", home ?: "/root") < 0) diff --git a/src/run/run.c b/src/run/run.c index 523993cffd..b9a4ecd2e4 100644 --- a/src/run/run.c +++ b/src/run/run.c @@ -1334,29 +1334,48 @@ static int transient_service_set_properties(sd_bus_message *m, const char *pty_p send_term = false; if (send_term != 0) { - const char *e; + const char *e, *colorterm = NULL, *no_color = NULL; - /* Propagate $TERM only if we are actually connected to a TTY */ + /* Propagate $TERM + $COLORTERM + $NO_COLOR if we are actually connected to a TTY */ if (isatty_safe(STDIN_FILENO) || isatty_safe(STDOUT_FILENO) || isatty_safe(STDERR_FILENO)) { - e = getenv("TERM"); + e = strv_find_prefix(environ, "TERM="); send_term = !!e; + + if (send_term) { + /* If we send $TERM along, then also propagate $COLORTERM + $NO_COLOR right with it */ + colorterm = strv_find_prefix(environ, "COLORTERM="); + no_color = strv_find_prefix(environ, "NO_COLOR="); + } } else /* If we are not connected to any TTY ourselves, then send TERM=dumb, but only if we * really need to (because we actually allocated a TTY for the service) */ - e = "dumb"; + e = "TERM=dumb"; if (send_term > 0) { - _cleanup_free_ char *n = NULL; - - n = strjoin("TERM=", e); - if (!n) - return log_oom(); - - r = sd_bus_message_append(m, - "(sv)", - "Environment", "as", 1, n); + r = sd_bus_message_append( + m, + "(sv)", + "Environment", "as", 1, e); if (r < 0) return bus_log_create_error(r); + + if (colorterm) { + r = sd_bus_message_append( + m, + "(sv)", + "Environment", "as", 1, colorterm); + if (r < 0) + return bus_log_create_error(r); + } + + if (no_color) { + r = sd_bus_message_append( + m, + "(sv)", + "Environment", "as", 1, no_color); + if (r < 0) + return bus_log_create_error(r); + } } } diff --git a/src/socket-activate/socket-activate.c b/src/socket-activate/socket-activate.c index 134ed3f0de..ac663d0323 100644 --- a/src/socket-activate/socket-activate.c +++ b/src/socket-activate/socket-activate.c @@ -132,7 +132,7 @@ static int exec_process(const char *name, char **argv, int start_fd, size_t n_fd return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--inetd only supported for single file descriptors."); - FOREACH_STRING(var, "TERM", "PATH", "USER", "HOME") { + FOREACH_STRING(var, "TERM", "COLORTERM", "NO_COLOR", "PATH", "USER", "HOME") { const char *n; n = strv_find_prefix(environ, var);