better support for $COLORTERM (#36770)

I recently noticed that our serial/VM terminals did not get fedora's
color shell prompt, nor got color support in "ls".

I spend a bit of time investigating and it's all a bit of a mess. If we
don't have any idea what kind of terminal we are talking to via serial
or hypervisor console then we so far just set TERM=vt220 as a reasonable
fallback: vt220 is quite universally defined in terminfo/termcap, and it
supports pageup/pagedown (unlike vt100).

However, real vt220 DEC terminals did not support color, and hence
termcap/terminfo says "no color, sorry". Which sucks, but actually
neither coreutils' "ls" (via `dircolors`) nor fedora's color shell
prompt actually care for termcap/terminfo. So why don't we get color?

In the coreutils case: it has it's own mini-database of terminals. A
very skewed one, where TERM=vt100 enables colors (and DEC vt100
definitely never ever had color support!), but vt220 does not. However,
what it actually does is check $COLORTERM. If that's set then it would
enable color.

In the fedora color prmpt case: it tries to derive color support by
looking for the word "color" in $TERM. Horrible hack if you ask me...

In order to make things better I did a bunch of things:

1. I think the idea of actually having a fully correct and up-to-date
termcap/terminfo database is kinda illusionary these days. But
apparently regarding color support $COLORTERM kinda took it place.
coreutils cares, and systemd itself cares too. To some point at least:
we consume it to determine color support, but we never propagate it in
nspawn, run0 and so on. So this PR fixes that.

2. Also, we are kinda stuck with vt220 I guess as default fallback for
serial terminals. But let's tweak it, and set $COLORTERM=truecolor as
default too. this means we default to a vt220 terminal, but with color.
Which is an ahistorical thing to do, but I think it's the best way out.

3. I also filed a bug against util-linux asking them to treat $COLORTERM
like $TERM, and let it propagate from getty into login shell:
https://github.com/util-linux/util-linux/issues/3463 – With that we
should get color support in ls by default now.

4. I also asked coreutils to treat vt220 the same as they already treat
vt100 and simply do color, even if though that's ahistorical:
https://github.com/coreutils/coreutils/issues/96

5. I then asked the fedora color prompt package to check $COLORTERM:
https://bugzilla.redhat.com/show_bug.cgi?id=2352650

6. I also asked the fedora ssh package to propagate $COLORTERM to remote
hosts by default, like they already cover $TERM. terminal emulators set
both these days generally, hence this would make sense.
https://bugzilla.redhat.com/show_bug.cgi?id=2352653

7. while at it, I figured it makes sense to not only propagate/consume
$COLORTERM at the same time as $TERM, but also consider $NO_COLOR. In
contrast to $COLORTERM for which no spec seems to exist, that one
actually does have a spec: https://no-color.org/

It might make sense for those interested in other distros than Fedora to
maybe ask for similar changes for their ssh and color shell prompt
packages (if they have something coresponding).
This commit is contained in:
Lennart Poettering
2025-03-17 13:17:06 +01:00
committed by GitHub
6 changed files with 93 additions and 34 deletions
+22 -2
View File
@@ -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)
+15 -7
View File
@@ -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. */
+10 -8
View File
@@ -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);
+13 -3
View File
@@ -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)
+32 -13
View File
@@ -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);
}
}
}
+1 -1
View File
@@ -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);