From bcc9e58bfe094cf1d8f865c9ced2ec74385c2192 Mon Sep 17 00:00:00 2001 From: Lubomir Rintel Date: Thu, 29 Mar 2018 12:11:07 +0200 Subject: [PATCH] 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. --- Makefile.am | 1 + clients/cli/connections.c | 2 +- clients/cli/meson.build | 1 + clients/cli/nmcli.c | 281 ++++++++++++++++++++++++++++++++++++-- clients/cli/nmcli.h | 2 + man/nmcli.xml | 272 +++++++++++++++++++++++++++++++++++- 6 files changed, 548 insertions(+), 11 deletions(-) diff --git a/Makefile.am b/Makefile.am index 26a6b8863e..808ed91160 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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\" diff --git a/clients/cli/connections.c b/clients/cli/connections.c index 8c4995845f..5fb78d467d 100644 --- a/clients/cli/connections.c +++ b/clients/cli/connections.c @@ -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" diff --git a/clients/cli/meson.build b/clients/cli/meson.build index 65317a0ac9..b7f5e19360 100644 --- a/clients/cli/meson.build +++ b/clients/cli/meson.build @@ -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) ] diff --git a/clients/cli/nmcli.c b/clients/cli/nmcli.c index 89d474c39b..24fa37c638 100644 --- a/clients/cli/nmcli.c +++ b/clients/cli/nmcli.c @@ -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); } diff --git a/clients/cli/nmcli.h b/clients/cli/nmcli.h index 55ca1d30ab..1b176a66a9 100644 --- a/clients/cli/nmcli.h +++ b/clients/cli/nmcli.h @@ -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; diff --git a/man/nmcli.xml b/man/nmcli.xml index 2619234e11..0787fd8baa 100644 --- a/man/nmcli.xml +++ b/man/nmcli.xml @@ -9,7 +9,7 @@