cli: allow setting the colors with terminal-colors.d(5)

The present version of the specification is somewhat unclear at times,
Unclear points were discussed with the maintainers [1] and probably
some new version will address those.

https://www.spinics.net/lists/util-linux-ng/msg15222.html

Until then here's how the implementation copes with ambiguities
(after the discussion with util-linux maintainers):

1.) It is unclear whether multiple .schem files should override each
    other or be merged. We use the overriding behavior -- take the
    highest priority one and ignore the rest.

2.) We assume "name.schem" is more specific than "@term.schem".

3.) We assume the "Color name" are to be used as aliases for the color
    sequences and translate them to ANSI escape sequences.

4.) The "Escape sequences" are of no use since the specification
    pretty much assumes an ANSI terminal and none of the sequences make
    any sense in ANSI color codes. We don't support them.
    accept that.

5.) We don't implement TERMINAL_COLORS_DEBUG because it's unspecified
    what should it do.
This commit is contained in:
Lubomir Rintel 2018-03-29 12:11:07 +02:00
parent 31aa2cfe29
commit bcc9e58bfe
6 changed files with 548 additions and 11 deletions

View file

@ -3514,6 +3514,7 @@ clients_cli_nmcli_CPPFLAGS = \
-I$(srcdir)/clients/cli \
$(clients_cppflags) \
$(SANITIZER_EXEC_CFLAGS) \
-DSYSCONFDIR=\"$(sysconfdir)\" \
-DG_LOG_DOMAIN=\""nmcli"\" \
-DNETWORKMANAGER_COMPILATION=NM_NETWORKMANAGER_COMPILATION_CLIENT \
-DNMCLI_LOCALEDIR=\"$(datadir)/locale\"

View file

@ -7538,7 +7538,7 @@ editor_menu_main (NmCli *nmc, NMConnection *connection, const char *connection_t
} else
nmc->nmc_config_mutable.show_secrets = bb;
} else if (cmd_arg_p && matches (cmd_arg_p, "prompt-color")) {
g_debug ("Ignoring erroneous --prompt-color argument.\n");
g_debug ("Ignoring erroneous --prompt-color argument. Use terminal-colors.d(5) to set the prompt color.\n");
} else if (!cmd_arg_p) {
g_print (_("Current nmcli configuration:\n"));
g_print ("status-line: %s\n"

View file

@ -27,6 +27,7 @@ deps = [
]
cflags = clients_cflags + [
'-DSYSCONFDIR="@0@"'.format(nm_sysconfdir),
'-DG_LOG_DOMAIN="@0@"'.format(name),
'-DNMCLI_LOCALEDIR="@0@"'.format(nm_localedir)
]

View file

@ -329,22 +329,276 @@ matches_arg (NmCli *nmc, int *argc, char ***argv, const char *pattern, char **ar
return TRUE;
}
/*************************************************************************************/
typedef enum {
NMC_USE_COLOR_AUTO,
NMC_USE_COLOR_YES,
NMC_USE_COLOR_NO,
} NmcColorOption;
static void
set_colors (NmCli *nmc, NmcColorOption *color_option)
/* Checks whether a particular terminal-colors.d(5) file (.enabled, .disabled or .schem)
* exists. If contents is non-NULL, it returns the content. */
static gboolean
check_colors_file (NmCli *nmc, NmcColorOption *color_option,
const char *base_dir, const char *name, const char *term, const char *type,
char **contents)
{
if (*color_option == NMC_USE_COLOR_AUTO) {
if ( g_strcmp0 (g_getenv ("TERM"), "dumb") == 0
|| !isatty (STDOUT_FILENO))
*color_option = NMC_USE_COLOR_NO;
char *filename;
gboolean exists;
filename = g_strdup_printf ("%s/terminal-colors.d/%s%s%s%s%s",
base_dir,
name ? name : "",
term ? "@" : "", term ? term : "",
(name || term) ? "." : "",
type);
if (contents)
exists = g_file_get_contents (filename, contents, NULL, NULL);
else
exists = g_file_test (filename, G_FILE_TEST_EXISTS);
g_free (filename);
return exists;
}
static void
check_colors_files_for_term (NmCli *nmc, NmcColorOption *color_option,
const char *base_dir, const char *name, const char *term)
{
if ( *color_option == NMC_USE_COLOR_AUTO
&& check_colors_file (nmc, color_option, base_dir, name, term, "enable", NULL)) {
*color_option = NMC_USE_COLOR_YES;
}
switch (*color_option) {
if ( *color_option == NMC_USE_COLOR_AUTO
&& check_colors_file (nmc, color_option, base_dir, name, term, "disable", NULL)) {
*color_option = NMC_USE_COLOR_NO;
}
if (*color_option == NMC_USE_COLOR_NO) {
/* No need to bother any further. */
return;
}
if (nmc->palette_buffer == NULL)
check_colors_file (nmc, color_option, base_dir, name, term, "schem", &nmc->palette_buffer);
}
static void
check_colors_files_for_name (NmCli *nmc, NmcColorOption *color_option,
const char *base_dir, const char *name)
{
const gchar *term;
/* Take a shortcut if the directory is not there. */
if (!g_file_test (base_dir, G_FILE_TEST_EXISTS))
return;
term = g_getenv ("TERM");
if (term)
check_colors_files_for_term (nmc, color_option, base_dir, name, term);
check_colors_files_for_term (nmc, color_option, base_dir, name, NULL);
}
static void
check_colors_files_for_base_dir (NmCli *nmc, NmcColorOption *color_option,
const char *base_dir)
{
check_colors_files_for_name (nmc, color_option, base_dir, "nmcli");
check_colors_files_for_name (nmc, color_option, base_dir, NULL);
}
static const char *
resolve_color_alias (const char *color)
{
static const struct {
const char *name;
const char *alias;
} aliases[] = {
{ "reset", "0" },
{ "bold", "1" },
{ "white", "1;37" },
{ "halfbright", "2" },
{ "underscore", "4" },
{ "blink", "5" },
{ "reverse", "7" },
{ "black", "30" },
{ "red", "31" },
{ "green", "32" },
{ "brown", "33" },
{ "yellow", "33" }, /* well, yellow */
{ "blue", "34" },
{ "magenta", "35" },
{ "cyan", "36" },
{ "gray", "37" },
{ "darkgray", "90" },
{ "lightred", "91" },
{ "lightgreen", "92" },
{ "lightblue", "94" },
{ "lightmagenta", "95" },
{ "lightcyan", "96" },
{ "lightgray", "97" },
};
int i;
/* Shortcut literal sequences. */
if (g_ascii_isdigit (*color))
return color;
for (i = 0; i < G_N_ELEMENTS (aliases); i++) {
if (strcmp (color, aliases[i].name) == 0)
return aliases[i].alias;
}
return color;
}
static gboolean
parse_color_scheme (NmCli *nmc, GError **error)
{
char *p = nmc->palette_buffer;
const char *name;
const char *color;
const char *map[_NM_META_COLOR_NUM] = {
[NM_META_COLOR_NONE] = NULL,
[NM_META_COLOR_CONNECTION_ACTIVATED] = "connection-activated",
[NM_META_COLOR_CONNECTION_ACTIVATING] = "connection-activating",
[NM_META_COLOR_CONNECTION_DISCONNECTING] = "connection-disconnecting",
[NM_META_COLOR_CONNECTION_INVISIBLE] = "connection-invisible",
[NM_META_COLOR_CONNECTION_UNKNOWN] = "connection-unknown",
[NM_META_COLOR_CONNECTIVITY_FULL] = "connectivity-full",
[NM_META_COLOR_CONNECTIVITY_LIMITED] = "connectivity-limited",
[NM_META_COLOR_CONNECTIVITY_NONE] = "connectivity-none",
[NM_META_COLOR_CONNECTIVITY_PORTAL] = "connectivity-portal",
[NM_META_COLOR_CONNECTIVITY_UNKNOWN] = "connectivity-unknown",
[NM_META_COLOR_DEVICE_ACTIVATED] = "device-activated",
[NM_META_COLOR_DEVICE_ACTIVATING] = "device-activating",
[NM_META_COLOR_DEVICE_DISCONNECTED] = "device-disconnected",
[NM_META_COLOR_DEVICE_FIRMWARE_MISSING] = "device-firmware-missing",
[NM_META_COLOR_DEVICE_PLUGIN_MISSING] = "device-plugin-missing",
[NM_META_COLOR_DEVICE_UNAVAILABLE] = "device-unavailable",
[NM_META_COLOR_DEVICE_UNKNOWN] = "device-unknown",
[NM_META_COLOR_MANAGER_RUNNING] = "manager-running",
[NM_META_COLOR_MANAGER_STARTING] = "manager-starting",
[NM_META_COLOR_MANAGER_STOPPED] = "manager-stopped",
[NM_META_COLOR_PERMISSION_AUTH] = "permission-auth",
[NM_META_COLOR_PERMISSION_NO] = "permission-no",
[NM_META_COLOR_PERMISSION_UNKNOWN] = "permission-unknown",
[NM_META_COLOR_PERMISSION_YES] = "permission-yes",
[NM_META_COLOR_PROMPT] = "prompt",
[NM_META_COLOR_STATE_ASLEEP] = "state-asleep",
[NM_META_COLOR_STATE_CONNECTED_GLOBAL] = "state-connected-global",
[NM_META_COLOR_STATE_CONNECTED_LOCAL] = "state-connected-local",
[NM_META_COLOR_STATE_CONNECTED_SITE] = "state-connected-site",
[NM_META_COLOR_STATE_CONNECTING] = "state-connecting",
[NM_META_COLOR_STATE_DISCONNECTED] = "state-disconnected",
[NM_META_COLOR_STATE_DISCONNECTING] = "state-disconnecting",
[NM_META_COLOR_STATE_UNKNOWN] = "state-unknown",
[NM_META_COLOR_WIFI_SIGNAL_EXCELLENT] = "wifi-signal-excellent",
[NM_META_COLOR_WIFI_SIGNAL_FAIR] = "wifi-signal-fair",
[NM_META_COLOR_WIFI_SIGNAL_GOOD] = "wifi-signal-good",
[NM_META_COLOR_WIFI_SIGNAL_POOR] = "wifi-signal-poor",
[NM_META_COLOR_WIFI_SIGNAL_UNKNOWN] = "wifi-signal-unknown",
[NM_META_COLOR_DISABLED] = "disabled",
[NM_META_COLOR_ENABLED] = "enabled",
};
int i;
/* This reads through the raw color scheme file contents, identifying the
* color names and sequences, putting in terminating NULs in place, so that
* pointers into the buffer can readily be used as strings in the palette. */
while (1) {
/* Leading whitespace. */
while (nm_utils_is_separator (*p) || *p == '\n')
p++;
if (*p == '\0')
break;
/* Comments. */
if (*p == '#') {
while (*p != '\n' && *p != '\0')
p++;
continue;
}
/* Color name. */
name = p;
while (g_ascii_isgraph (*p))
p++;
if (*p == '\0') {
g_set_error (error, NMCLI_ERROR, 0,
_("Unexpected end of file following '%s'\n"), name);
return FALSE;
}
/* Separating whitespace. */
if (!nm_utils_is_separator (*p)) {
*p = '\0';
g_set_error (error, NMCLI_ERROR, 0,
_("Expected whitespace following '%s'\n"), name);
return FALSE;
}
while (nm_utils_is_separator (*p)) {
*p = '\0';
p++;
}
/* Color sequence. */
color = p;
if (!g_ascii_isgraph (*p)) {
g_set_error (error, NMCLI_ERROR, 0,
_("Expected a value for '%s'\n"), name);
return FALSE;
}
while (g_ascii_isgraph (*p))
p++;
/* Trailing whitespace. */
while (nm_utils_is_separator (*p)) {
*p = '\0';
p++;
}
if (*p != '\0') {
if (*p != '\n') {
g_set_error (error, NMCLI_ERROR, 0,
_("Expected a line break following '%s'\n"), color);
return FALSE;
}
*p = '\0';
p++;
}
/* All good, set the palette entry. */
for (i = NM_META_COLOR_NONE + 1; i < _NM_META_COLOR_NUM; i++) {
if (strcmp (map[i], name) == 0) {
nmc->nmc_config_mutable.palette[i] = resolve_color_alias (color);
break;
}
}
if (i == _NM_META_COLOR_NUM)
g_debug ("Ignoring an unrecognized color: '%s'\n", name);
}
return TRUE;
}
static void
set_colors (NmCli *nmc, NmcColorOption color_option)
{
GError *error = NULL;
if (color_option == NMC_USE_COLOR_AUTO) {
if ( g_strcmp0 (g_getenv ("TERM"), "dumb") == 0
|| !isatty (STDOUT_FILENO))
color_option = NMC_USE_COLOR_NO;
}
check_colors_files_for_base_dir (nmc, &color_option, g_get_user_config_dir ());
check_colors_files_for_base_dir (nmc, &color_option, SYSCONFDIR);
switch (color_option) {
case NMC_USE_COLOR_YES:
case NMC_USE_COLOR_AUTO:
nmc->nmc_config_mutable.use_colors = TRUE;
@ -353,8 +607,17 @@ set_colors (NmCli *nmc, NmcColorOption *color_option)
nmc->nmc_config_mutable.use_colors = FALSE;
break;
}
if (nmc->nmc_config_mutable.use_colors && nmc->palette_buffer) {
if (!parse_color_scheme (nmc, &error)) {
g_debug ("Error parsing color scheme: %s", error->message);
g_error_free (error);
}
}
}
/*************************************************************************************/
static gboolean
process_command_line (NmCli *nmc, int argc, char **argv)
{
@ -508,7 +771,7 @@ process_command_line (NmCli *nmc, int argc, char **argv)
if (nmc->required_fields)
nmc->nmc_config_mutable.overview = FALSE;
set_colors (nmc, &colors);
set_colors (nmc, colors);
/* Now run the requested command */
nmc_do_cmd (nmc, nmcli_cmds, *argv, argc, argv);
@ -674,6 +937,8 @@ nmc_cleanup (NmCli *nmc)
nmc->pager_pid = 0;
}
nm_clear_g_free (&nmc->palette_buffer);
nmc_polkit_agent_fini (nmc);
}

View file

@ -137,6 +137,8 @@ typedef struct _NmCli {
gboolean complete; /* Autocomplete the command line */
gboolean editor_status_line; /* Whether to display status line in connection editor */
gboolean editor_save_confirmation; /* Whether to ask for confirmation on saving connections with 'autoconnect=yes' */
char *palette_buffer; /* Buffer with sequences for terminal-colors.d(5)-based coloring. */
} NmCli;
extern NmCli nm_cli;

View file

@ -9,7 +9,7 @@
<!--
nmcli(1) manual page
Copyright 2010 - 2016 Red Hat, Inc.
Copyright 2010 - 2018 Red Hat, Inc.
Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.1
@ -183,6 +183,10 @@
<literal>yes</literal> enables colors, <literal>no</literal> disables them,
<literal>auto</literal> only produces colors when standard output is directed
to a terminal. The default value is <literal>auto</literal>.</para>
<para>The actual colors used are configured as described in
<citerefentry><refentrytitle>terminal-colors.d</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
Please refer to the <link linkend='colors' endterm='colors.title' /> section for a
list of color names supported by <command>nmcli</command>.</para>
</listitem>
</varlistentry>
@ -2035,6 +2039,269 @@ It's equivalent to the <literal>+bond.options 'option=value'</literal> syntax.</
</refsect1>
<refsect1 id='colors'><title id='colors.title'>Colors</title>
<para>Implicit coloring can be disabled by an empty file
<filename>/etc/terminal-colors.d/nmcli.disable</filename>.</para>
<para>See <citerefentry><refentrytitle>terminal-colors.d</refentrytitle><manvolnum>5</manvolnum></citerefentry>
for more details about colorization configuration.
The logical color names supported by <command>nmcli</command> are:</para>
<variablelist>
<varlistentry>
<term><option>connection-activated</option></term>
<listitem>
<para>A connection that is active.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>connection-activating</option></term>
<listitem>
<para>Connection that is being activated.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>connection-disconnecting</option></term>
<listitem>
<para>Connection that is being disconnected.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>connection-invisible</option></term>
<listitem>
<para>Connection whose details is the user not permitted to see.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>connectivity-full</option></term>
<listitem>
<para>Conectivity state when Internet is reachable.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>connectivity-limited</option></term>
<listitem>
<para>Conectivity state when only a local network reachable.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>connectivity-none</option></term>
<listitem>
<para>Conectivity state when the network is disconnected.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>connectivity-portal</option></term>
<listitem>
<para>Conectivity state when a captive portal hijacked the connection.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>connectivity-unknown</option></term>
<listitem>
<para>Conectivity state when a connectivity check didn't run.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>device-activated</option></term>
<listitem>
<para>Device that is connected.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>device-activating</option></term>
<listitem>
<para>Device that is being configured.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>device-disconnected</option></term>
<listitem>
<para>Device that is not connected.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>device-firmware-missing</option></term>
<listitem>
<para>Warning of a missing device firmware.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>device-plugin-missing</option></term>
<listitem>
<para>Warning of a missing device plugin.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>device-unavailable</option></term>
<listitem>
<para>Device that is not available for activation.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>manager-running</option></term>
<listitem>
<para>Notice that the NetworkManager daemon is available.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>manager-starting</option></term>
<listitem>
<para>Notice that the NetworkManager daemon is being initially connected.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>manager-stopped</option></term>
<listitem>
<para>Notice that the NetworkManager daemon is not available.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>permission-auth</option></term>
<listitem>
<para>An action that requires user authentication to get permission.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>permission-no</option></term>
<listitem>
<para>An action that is not permitted.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>permission-yes</option></term>
<listitem>
<para>An action that is permitted.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>prompt</option></term>
<listitem>
<para>Prompt in interactive mode.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>state-asleep</option></term>
<listitem>
<para>Indication that NetworkManager in suspended state.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>state-connected-global</option></term>
<listitem>
<para>Indication that NetworkManager in connected to Internet.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>state-connected-local</option></term>
<listitem>
<para>Indication that NetworkManager in local network.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>state-connected-site</option></term>
<listitem>
<para>Indication that NetworkManager in connected to networks other than Internet.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>state-connecting</option></term>
<listitem>
<para>Indication that NetworkManager is establishing a network connection.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>state-disconnected</option></term>
<listitem>
<para>Indication that NetworkManager is disconnected from a network.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>state-disconnecting</option></term>
<listitem>
<para>Indication that NetworkManager is being disconnected from a network.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>wifi-signal-excellent</option></term>
<listitem>
<para>Wi-Fi network with an excellent signal level.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>wifi-signal-fair</option></term>
<listitem>
<para>Wi-Fi network with a fair signal level.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>wifi-signal-good</option></term>
<listitem>
<para>Wi-Fi network with a good signal level.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>wifi-signal-poor</option></term>
<listitem>
<para>Wi-Fi network with a poor signal level.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>wifi-signal-unknown</option></term>
<listitem>
<para>Wi-Fi network that hasn't been actually seen (a hidden AP).</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>disabled</option></term>
<listitem>
<para>A property that is turned off.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>enabled</option></term>
<listitem>
<para>A property that is turned on.</para>
</listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1 id='environment_variables'><title>Environment Variables</title>
<para><command>nmcli</command>'s behavior is affected by the following
@ -2465,7 +2732,8 @@ It's equivalent to the <literal>+bond.options 'option=value'</literal> syntax.</
<link linkend='NetworkManager.conf'><citerefentry><refentrytitle>NetworkManager.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry></link>,
<link linkend='nm-settings'><citerefentry><refentrytitle>nm-settings</refentrytitle><manvolnum>5</manvolnum></citerefentry></link>,
<citerefentry><refentrytitle>nm-applet</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
<citerefentry><refentrytitle>nm-connection-editor</refentrytitle><manvolnum>1</manvolnum></citerefentry>.</para>
<citerefentry><refentrytitle>nm-connection-editor</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
<citerefentry><refentrytitle>terminal-colors.d</refentrytitle><manvolnum>5</manvolnum></citerefentry>.</para>
</refsect1>
</refentry>