diff --git a/man/org.freedesktop.hostname1.xml b/man/org.freedesktop.hostname1.xml index 620ae83516..c70258459c 100644 --- a/man/org.freedesktop.hostname1.xml +++ b/man/org.freedesktop.hostname1.xml @@ -80,6 +80,8 @@ node /org/freedesktop/hostname1 { @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly s OperatingSystemPrettyName = '...'; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly s OperatingSystemFancyName = '...'; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly s OperatingSystemCPEName = '...'; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly t OperatingSystemSupportEnd = ...; @@ -170,6 +172,8 @@ node /org/freedesktop/hostname1 { + + @@ -285,9 +289,11 @@ node /org/freedesktop/hostname1 { KernelName, KernelRelease, and KernelVersion expose the kernel name (e.g. Linux), release (e.g. 5.0.0-11), and version (i.e. the build number, e.g. #11) as - reported by uname2. - OperatingSystemPrettyName, OperatingSystemCPEName, and - HomeURL expose the PRETTY_NAME=, CPE_NAME= and + reported by uname2. + + OperatingSystemPrettyName, OperatingSystemFancyName, + OperatingSystemCPEName, and HomeURL expose the + PRETTY_NAME=, FANCY_NAME=, CPE_NAME= and HOME_URL= fields from os-release5. The purpose of those properties is to allow remote clients to access this information over D-Bus. Local @@ -487,6 +493,7 @@ node /org/freedesktop/hostname1 { ChassisAssetTag, OperatingSystemImageID, OperatingSystemImageVersion, HardwareSKU, and HardwareVersion were added in version 258. + OperatingSystemFancyName was added in version 260. diff --git a/man/os-release.xml b/man/os-release.xml index 1ab7e652b8..8b652d78f0 100644 --- a/man/os-release.xml +++ b/man/os-release.xml @@ -191,9 +191,34 @@ A pretty operating system name in a format suitable for presentation to the user. May or may not contain a release code name or OS version of some kind, as suitable. If not - set, a default of PRETTY_NAME="Linux" may be used + set, a default of PRETTY_NAME="Linux" may be used. - Example: PRETTY_NAME="Fedora 17 (Beefy Miracle)". + Example: PRETTY_NAME="Fedora 17 (Beefy Miracle)" + + + + FANCY_NAME= + + Similar to PRETTY_NAME=, but may contain ANSI sequences and + fancy UTF-8 characters such as emojis. If defined this shall preferably used when displaying the OS + name on modern terminal emulators. If the terminal emulator does not support emojis, + PRETTY_NAME= shall be shown instead, possibly with + ANSI_COLOR= coloring. Use \033 to encode the ESC + character and \\ to encode the backslash character. + + + + Unlike PRETTY_NAME= this field must not contain information that is + present in other fields, in particular the version (already specified in + VERSION=) or the codename (already specified in + VERSION_CODENAME=). + + Example: + FANCY_NAME="🍅 \033[31mTomato\033[0;1mOS\033[0m" + + diff --git a/src/core/main.c b/src/core/main.c index 2676cd4c6b..0e5e201f2d 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -102,6 +102,7 @@ #include "umask-util.h" #include "unit-name.h" #include "user-util.h" +#include "utf8.h" #include "version.h" #include "virt.h" #include "watchdog.h" @@ -1389,37 +1390,76 @@ static int enforce_syscall_archs(Set *archs) { } static int os_release_status(void) { - _cleanup_free_ char *pretty_name = NULL, *name = NULL, *version = NULL, - *ansi_color = NULL, *support_end = NULL; + _cleanup_free_ char *pretty_name = NULL, *fancy_name = NULL, + *name = NULL, *version = NULL, *ansi_color = NULL, *support_end = NULL, *codename = NULL; int r; r = parse_os_release(NULL, - "PRETTY_NAME", &pretty_name, - "NAME", &name, - "VERSION", &version, - "ANSI_COLOR", &ansi_color, - "SUPPORT_END", &support_end); + "PRETTY_NAME", &pretty_name, + "FANCY_NAME", &fancy_name, + "NAME", &name, + "VERSION", &version, + "VERSION_CODENAME", &codename, + "ANSI_COLOR", &ansi_color, + "SUPPORT_END", &support_end); if (r < 0) return log_full_errno(r == -ENOENT ? LOG_DEBUG : LOG_WARNING, r, "Failed to read os-release file, ignoring: %m"); const char *label = os_release_pretty_name(pretty_name, name); - const char *color = empty_to_null(ansi_color) ?: "1"; if (show_status_on(arg_show_status)) { + const char *color = empty_to_null(ansi_color) ?: "1"; + + /* The fancy name may contain emoji characters and ANSI sequences. Don't use it if our locale + * doesn't allow that, or ANSI sequences are off, or if it is empty. */ + + if (!isempty(fancy_name)) { + _cleanup_free_ char *unescaped = NULL; + + /* Undo one level of C-style unescaping for this one */ + ssize_t l = cunescape(fancy_name, /* flags= */ 0, &unescaped); + if (l < 0) { + log_debug_errno(l, "Failed to unescape FANCY_NAME=, ignoring: %m"); + fancy_name = mfree(fancy_name); + } else { + free_and_replace(fancy_name, unescaped); + + /* FANCY_NAME= does not contain version/codename info (unlike PRETTY_NAME=), + * but in this context it makes sense to show them if defined, hence append + * them here. */ + if (version && !strextend(&fancy_name, " ", version)) + return log_oom(); + + if (codename && !strextend(&fancy_name, " (", codename, ")")) + return log_oom(); + } + } + + if (isempty(fancy_name) || + (!emoji_enabled() && !ascii_is_valid(fancy_name)) || + !log_get_show_color()) + fancy_name = mfree(fancy_name); + + if (!fancy_name && log_get_show_color()) { + fancy_name = strjoin("\x1B[", color, "m", label); + if (!fancy_name) + return log_oom(); + } + if (in_initrd()) { if (log_get_show_color()) status_printf(NULL, 0, - ANSI_HIGHLIGHT "Booting initrd of " ANSI_NORMAL "\x1B[%sm%s" ANSI_NORMAL ANSI_HIGHLIGHT "." ANSI_NORMAL, - color, label); + ANSI_HIGHLIGHT "Booting initrd of " ANSI_NORMAL "%s" ANSI_NORMAL ANSI_HIGHLIGHT "." ANSI_NORMAL, + fancy_name); else status_printf(NULL, 0, "Booting initrd of %s...", label); } else { if (log_get_show_color()) status_printf(NULL, 0, - "\n" ANSI_HIGHLIGHT "Welcome to " ANSI_NORMAL "\x1B[%sm%s" ANSI_NORMAL ANSI_HIGHLIGHT "!" ANSI_NORMAL "\n", - color, label); + "\n" ANSI_HIGHLIGHT "Welcome to " ANSI_NORMAL "%s" ANSI_NORMAL ANSI_HIGHLIGHT "!" ANSI_NORMAL "\n", + fancy_name); else status_printf(NULL, 0, "\nWelcome to %s!\n", diff --git a/src/hostname/hostnamectl.c b/src/hostname/hostnamectl.c index 227bcf4c50..ae1af41425 100644 --- a/src/hostname/hostnamectl.c +++ b/src/hostname/hostnamectl.c @@ -29,6 +29,7 @@ #include "runtime-scope.h" #include "string-util.h" #include "time-util.h" +#include "utf8.h" #include "verbs.h" static bool arg_ask_password = true; @@ -51,6 +52,7 @@ typedef struct StatusInfo { const char *kernel_name; const char *kernel_release; const char *os_pretty_name; + const char *os_fancy_name; const char *os_cpe_name; usec_t os_support_end; const char *os_image_id; @@ -234,7 +236,14 @@ static int print_status_info(StatusInfo *i) { return table_log_add_error(r); } - if (!isempty(i->os_pretty_name)) { + if (!isempty(i->os_fancy_name) && (emoji_enabled() || ascii_is_valid(i->os_fancy_name)) && colors_enabled()) { + r = table_add_many(table, + TABLE_FIELD, "Operating System", + TABLE_STRING_WITH_ANSI, i->os_fancy_name, + TABLE_SET_URL, i->home_url); + if (r < 0) + return table_log_add_error(r); + } else if (!isempty(i->os_pretty_name)) { r = table_add_many(table, TABLE_FIELD, "Operating System", TABLE_STRING, i->os_pretty_name, @@ -425,6 +434,7 @@ static int show_all_names(sd_bus *bus) { { "KernelName", "s", NULL, offsetof(StatusInfo, kernel_name) }, { "KernelRelease", "s", NULL, offsetof(StatusInfo, kernel_release) }, { "OperatingSystemPrettyName", "s", NULL, offsetof(StatusInfo, os_pretty_name) }, + { "OperatingSystemFancyName", "s", NULL, offsetof(StatusInfo, os_fancy_name) }, { "OperatingSystemCPEName", "s", NULL, offsetof(StatusInfo, os_cpe_name) }, { "OperatingSystemSupportEnd", "t", NULL, offsetof(StatusInfo, os_support_end) }, { "OperatingSystemImageID", "s", NULL, offsetof(StatusInfo, os_image_id) }, diff --git a/src/hostname/hostnamed.c b/src/hostname/hostnamed.c index fbe1b2fa7a..d2461981af 100644 --- a/src/hostname/hostnamed.c +++ b/src/hostname/hostnamed.c @@ -22,6 +22,7 @@ #include "device-private.h" #include "env-file.h" #include "env-util.h" +#include "escape.h" #include "extract-word.h" #include "fileio.h" #include "hashmap.h" @@ -75,6 +76,7 @@ typedef enum { PROP_OS_SUPPORT_END, PROP_OS_IMAGE_ID, PROP_OS_IMAGE_VERSION, + PROP_OS_FANCY_NAME, _PROP_MAX, _PROP_INVALID = -EINVAL, } HostProperty; @@ -192,7 +194,7 @@ static void context_read_machine_info(Context *c) { } static void context_read_os_release(Context *c) { - _cleanup_free_ char *os_name = NULL, *os_pretty_name = NULL; + _cleanup_free_ char *os_name = NULL, *os_pretty_name = NULL, *os_fancy_name = NULL, *os_ansi_color = NULL; struct stat current_stat = {}; int r; @@ -209,10 +211,13 @@ static void context_read_os_release(Context *c) { (UINT64_C(1) << PROP_OS_HOME_URL) | (UINT64_C(1) << PROP_OS_SUPPORT_END) | (UINT64_C(1) << PROP_OS_IMAGE_ID) | - (UINT64_C(1) << PROP_OS_IMAGE_VERSION)); + (UINT64_C(1) << PROP_OS_IMAGE_VERSION) | + (UINT64_C(1) << PROP_OS_FANCY_NAME)); r = parse_os_release(NULL, "PRETTY_NAME", &os_pretty_name, + "FANCY_NAME", &os_fancy_name, + "ANSI_COLOR", &os_ansi_color, "NAME", &os_name, "CPE_NAME", &c->data[PROP_OS_CPE_NAME], "HOME_URL", &c->data[PROP_OS_HOME_URL], @@ -225,6 +230,31 @@ static void context_read_os_release(Context *c) { if (free_and_strdup(&c->data[PROP_OS_PRETTY_NAME], os_release_pretty_name(os_pretty_name, os_name)) < 0) log_oom(); + if (!isempty(os_fancy_name)) { + _cleanup_free_ char *unescaped = NULL; + + /* We undo one level of C escapes on this */ + ssize_t l = cunescape(os_fancy_name, /* flags= */ 0, &unescaped); + if (l < 0) { + log_warning_errno(l, "Failed to unescape fancy OS name, ignoring: %m"); + os_fancy_name = mfree(os_fancy_name); + } else + free_and_replace(os_fancy_name, unescaped); + } + + if (isempty(os_fancy_name)) { + free(os_fancy_name); /* free if empty string */ + + if (isempty(os_ansi_color)) + os_fancy_name = strdup(c->data[PROP_OS_PRETTY_NAME]); + else + os_fancy_name = strjoin("\x1B[", os_ansi_color, "m", c->data[PROP_OS_PRETTY_NAME]); + if (!os_fancy_name) + log_oom(); + } + + free_and_replace(c->data[PROP_OS_FANCY_NAME], os_fancy_name); + c->etc_os_release_stat = current_stat; } @@ -1705,6 +1735,7 @@ static int build_describe_response(Context *c, bool privileged, sd_json_variant SD_JSON_BUILD_PAIR_STRING("KernelRelease", u.release), SD_JSON_BUILD_PAIR_STRING("KernelVersion", u.version), SD_JSON_BUILD_PAIR_STRING("OperatingSystemPrettyName", c->data[PROP_OS_PRETTY_NAME]), + SD_JSON_BUILD_PAIR_STRING("OperatingSystemFancyName", c->data[PROP_OS_FANCY_NAME]), SD_JSON_BUILD_PAIR_STRING("OperatingSystemCPEName", c->data[PROP_OS_CPE_NAME]), SD_JSON_BUILD_PAIR_STRING("OperatingSystemHomeURL", c->data[PROP_OS_HOME_URL]), JSON_BUILD_PAIR_FINITE_USEC("OperatingSystemSupportEnd", eol), @@ -1781,6 +1812,7 @@ static const sd_bus_vtable hostname_vtable[] = { SD_BUS_PROPERTY("KernelRelease", "s", property_get_uname_field, offsetof(struct utsname, release), SD_BUS_VTABLE_ABSOLUTE_OFFSET|SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("KernelVersion", "s", property_get_uname_field, offsetof(struct utsname, version), SD_BUS_VTABLE_ABSOLUTE_OFFSET|SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("OperatingSystemPrettyName", "s", property_get_os_release_field, offsetof(Context, data[PROP_OS_PRETTY_NAME]), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("OperatingSystemFancyName", "s", property_get_os_release_field, offsetof(Context, data[PROP_OS_FANCY_NAME]), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("OperatingSystemCPEName", "s", property_get_os_release_field, offsetof(Context, data[PROP_OS_CPE_NAME]), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("OperatingSystemSupportEnd", "t", property_get_os_support_end, 0, SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("HomeURL", "s", property_get_os_release_field, offsetof(Context, data[PROP_OS_HOME_URL]), SD_BUS_VTABLE_PROPERTY_CONST), diff --git a/src/shared/format-table.c b/src/shared/format-table.c index 9e366ce513..9f6c46afb7 100644 --- a/src/shared/format-table.c +++ b/src/shared/format-table.c @@ -283,6 +283,7 @@ static size_t table_data_size(TableDataType type, const void *data) { return 0; case TABLE_STRING: + case TABLE_STRING_WITH_ANSI: case TABLE_PATH: case TABLE_PATH_BASENAME: case TABLE_FIELD: @@ -529,7 +530,7 @@ int table_add_cell_stringf_full(Table *t, TableCell **ret_cell, TableDataType dt int r; assert(t); - assert(IN_SET(dt, TABLE_STRING, TABLE_PATH, TABLE_PATH_BASENAME, TABLE_FIELD, TABLE_HEADER, TABLE_VERSION)); + assert(IN_SET(dt, TABLE_STRING, TABLE_STRING_WITH_ANSI, TABLE_PATH, TABLE_PATH_BASENAME, TABLE_FIELD, TABLE_HEADER, TABLE_VERSION)); va_start(ap, format); r = vasprintf(&buffer, format, ap); @@ -933,6 +934,7 @@ int table_add_many_internal(Table *t, TableDataType first_type, ...) { break; case TABLE_STRING: + case TABLE_STRING_WITH_ANSI: case TABLE_PATH: case TABLE_PATH_BASENAME: case TABLE_FIELD: @@ -1393,6 +1395,7 @@ static int cell_data_compare(TableData *a, size_t index_a, TableData *b, size_t switch (a->type) { case TABLE_STRING: + case TABLE_STRING_WITH_ANSI: case TABLE_FIELD: case TABLE_HEADER: return strcmp(a->string, b->string); @@ -1574,7 +1577,13 @@ static char* format_strv_width(char **strv, size_t column_width) { return buf; } -static const char *table_data_format(Table *t, TableData *d, bool avoid_uppercasing, size_t column_width, bool *have_soft) { +static const char *table_data_format( + Table *t, + TableData *d, + bool avoid_uppercasing, + size_t column_width, + bool *have_soft) { + assert(d); if (d->formatted && @@ -1587,6 +1596,7 @@ static const char *table_data_format(Table *t, TableData *d, bool avoid_uppercas return table_ersatz_string(t); case TABLE_STRING: + case TABLE_STRING_WITH_ANSI: case TABLE_PATH: case TABLE_PATH_BASENAME: case TABLE_FIELD: @@ -2068,6 +2078,42 @@ static const char *table_data_format(Table *t, TableData *d, bool avoid_uppercas return d->formatted; } +static const char *table_data_format_strip_ansi( + Table *t, + TableData *d, + bool avoid_uppercasing, + size_t column_width, + bool *have_soft, + char **ret_buffer) { + + /* Just like table_data_format() but strips ANSI sequences for ANSI fields. */ + + assert(ret_buffer); + + const char *c; + + c = table_data_format(t, d, avoid_uppercasing, column_width, have_soft); + if (!c) + return NULL; + + if (d->type != TABLE_STRING_WITH_ANSI) { + /* Shortcut: we do not consider ANSI sequences for all other column types, hence return the + * original string as-is */ + *ret_buffer = NULL; + return c; + } + + _cleanup_free_ char *s = strdup(c); + if (!s) + return NULL; + + if (!strip_tab_ansi(&s, /* isz= */ NULL, /* highlight= */ NULL)) + return NULL; + + *ret_buffer = TAKE_PTR(s); + return *ret_buffer; +} + static int console_width_height( const char *s, size_t *ret_width, @@ -2122,14 +2168,20 @@ static int table_data_requested_width_height( size_t *ret_height, bool *have_soft) { - _cleanup_free_ char *truncated = NULL; + _cleanup_free_ char *truncated = NULL, *buffer = NULL; bool truncation_applied = false; size_t width, height; + bool soft = false; const char *t; int r; - bool soft = false; - t = table_data_format(table, d, false, available_width, &soft); + t = table_data_format_strip_ansi( + table, + d, + /* avoid_uppercasing= */ false, + available_width, + &soft, + &buffer); if (!t) return -ENOMEM; @@ -2352,7 +2404,7 @@ int table_print(Table *t, FILE *f) { if (r < 0) return r; if (r > 0) { /* Truncated because too many lines? */ - _cleanup_free_ char *last = NULL; + _cleanup_free_ char *last = NULL, *buffer = NULL; const char *field; /* If we are going to show only the first few lines of a cell that has @@ -2360,9 +2412,13 @@ int table_print(Table *t, FILE *f) { * ellipsis. Hence, let's figure out the last line, and account for its * length plus ellipsis. */ - field = table_data_format(t, d, false, - width ? width[j] : SIZE_MAX, - &any_soft); + field = table_data_format_strip_ansi( + t, + d, + /* avoid_uppercasing= */ false, + width ? width[j] : SIZE_MAX, + &any_soft, + &buffer); if (!field) return -ENOMEM; @@ -2550,7 +2606,7 @@ int table_print(Table *t, FILE *f) { more_sublines = false; for (size_t j = 0; j < display_columns; j++) { - _cleanup_free_ char *buffer = NULL, *extracted = NULL; + _cleanup_free_ char *buffer = NULL, *stripped_ansi_buffer = NULL, *extracted = NULL; bool lines_truncated = false; const char *field, *color = NULL, *underline = NULL; TableData *d; @@ -2558,7 +2614,21 @@ int table_print(Table *t, FILE *f) { assert_se(d = row[t->display_map ? t->display_map[j] : j]); - field = table_data_format(t, d, false, width[j], NULL); + if (colors_enabled()) + field = table_data_format( + t, + d, + /* avoid_uppercasing= */ false, + width[j], + /* have_soft= */ NULL); + else + field = table_data_format_strip_ansi( + t, + d, + /* avoid_uppercasing= */ false, + width[j], + /* have_soft= */ NULL, + &stripped_ansi_buffer); if (!field) return -ENOMEM; @@ -2665,7 +2735,8 @@ int table_print(Table *t, FILE *f) { fputs(field, f); - if (color || underline) + /* Reset color afterwards if colors was set or the string to output contained ANSI sequences. */ + if (color || underline || (d->type == TABLE_STRING_WITH_ANSI && colors_enabled())) fputs(ANSI_NORMAL, f); gap_color = d->rgap_color; @@ -2920,6 +2991,18 @@ static int table_data_to_json(TableData *d, sd_json_variant **ret) { SD_JSON_BUILD_UNSIGNED(major(d->devnum)), SD_JSON_BUILD_UNSIGNED(minor(d->devnum)))); + case TABLE_STRING_WITH_ANSI: { + _cleanup_free_ char *s = strdup(d->string); + if (!s) + return -ENOMEM; + + /* We strip the ANSI data when outputting to JSON */ + if (!strip_tab_ansi(&s, /* isz= */ NULL, /* highlight= */ NULL)) + return -ENOMEM; + + return sd_json_variant_new_string(ret, s); + } + default: return -EINVAL; } @@ -2963,7 +3046,7 @@ char* table_mangle_to_json_field_name(const char *str) { } static int table_make_json_field_name(Table *t, TableData *d, char **ret) { - _cleanup_free_ char *mangled = NULL; + _cleanup_free_ char *mangled = NULL, *buffer = NULL; const char *n; assert(t); @@ -2973,7 +3056,13 @@ static int table_make_json_field_name(Table *t, TableData *d, char **ret) { if (IN_SET(d->type, TABLE_HEADER, TABLE_FIELD)) n = d->string; else { - n = table_data_format(t, d, /* avoid_uppercasing= */ true, SIZE_MAX, NULL); + n = table_data_format_strip_ansi( + t, + d, + /* avoid_uppercasing= */ true, + /* column_width= */ SIZE_MAX, + /* have_soft= */ NULL, + &buffer); if (!n) return -ENOMEM; } diff --git a/src/shared/format-table.h b/src/shared/format-table.h index 2147b82317..56dd18fa66 100644 --- a/src/shared/format-table.h +++ b/src/shared/format-table.h @@ -10,6 +10,7 @@ typedef enum TableDataType { TABLE_EMPTY, TABLE_STRING, + TABLE_STRING_WITH_ANSI, /* like the above, but contains ANSI sequences/TABs. They will be stripped when outputing to JSON */ TABLE_HEADER, /* in regular mode: the cells in the first row, that carry the column names */ TABLE_FIELD, /* in vertical mode: the cells in the first column, that carry the field names */ TABLE_STRV, diff --git a/src/shared/varlink-io.systemd.Hostname.c b/src/shared/varlink-io.systemd.Hostname.c index fb265b7511..e10f113046 100644 --- a/src/shared/varlink-io.systemd.Hostname.c +++ b/src/shared/varlink-io.systemd.Hostname.c @@ -20,7 +20,10 @@ static SD_VARLINK_DEFINE_METHOD( SD_VARLINK_DEFINE_OUTPUT(KernelName, SD_VARLINK_STRING, 0), SD_VARLINK_DEFINE_OUTPUT(KernelRelease, SD_VARLINK_STRING, 0), SD_VARLINK_DEFINE_OUTPUT(KernelVersion, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("'Pretty' name of the OS. This is the primary OS identifier that is suitable for presentation to the user. Typically includes a version too, but doesn't have to."), SD_VARLINK_DEFINE_OUTPUT(OperatingSystemPrettyName, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("'Fancy' name of the OS; may contain non-ASCII Unicode chars, as well as basic ANSI sequences. This is similar to 'OperatingSystemPrettyName', but is preferably used on terminals that support ANSI sequences and full Unicode."), + SD_VARLINK_DEFINE_OUTPUT(OperatingSystemFancyName, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_OUTPUT(OperatingSystemCPEName, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_OUTPUT(OperatingSystemHomeURL, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_OUTPUT(OperatingSystemSupportEnd, SD_VARLINK_INT, SD_VARLINK_NULLABLE), diff --git a/src/test/test-format-table.c b/src/test/test-format-table.c index ff886a3d5e..a2e142791f 100644 --- a/src/test/test-format-table.c +++ b/src/test/test-format-table.c @@ -4,6 +4,8 @@ #include #include "alloc-util.h" +#include "ansi-color.h" +#include "env-util.h" #include "format-table.h" #include "json-util.h" #include "terminal-util.h" @@ -835,9 +837,80 @@ TEST(table_bps) { "2500000000 2.3G 2.5Gbps\n"); } +TEST(table_ansi) { + _cleanup_(table_unrefp) Table *table = NULL; + + ASSERT_NOT_NULL((table = table_new("foo", "bar", "baz", "kkk"))); + + ASSERT_OK(table_add_many(table, + TABLE_STRING, "hallo", + TABLE_STRING_WITH_ANSI, "knuerz" ANSI_HIGHLIGHT_RED "red" ANSI_HIGHLIGHT_GREEN "green", + TABLE_STRING_WITH_ANSI, "noansi", + TABLE_STRING_WITH_ANSI, ANSI_GREY "thisisgrey")); + + unsigned saved_columns = columns(); + bool saved_color = colors_enabled(); + _cleanup_free_ char *saved_term = NULL; + const char *e = getenv("TERM"); + if (e) + ASSERT_NOT_NULL((saved_term = strdup(e))); + + ASSERT_OK_ERRNO(setenv("COLUMNS", "200", /* overwrite= */ true)); + ASSERT_OK_ERRNO(setenv("SYSTEMD_COLORS", "1", /* overwrite= */ true)); + ASSERT_OK_ERRNO(setenv("TERM", FALLBACK_TERM, /* overwrite= */ true)); + reset_terminal_feature_caches(); + + _cleanup_free_ char *formatted = NULL; + ASSERT_OK(table_format(table, &formatted)); + + ASSERT_STREQ(formatted, + ANSI_ADD_UNDERLINE "FOO " ANSI_NORMAL + ANSI_ADD_UNDERLINE " " ANSI_NORMAL + ANSI_ADD_UNDERLINE "BAR " ANSI_NORMAL + ANSI_ADD_UNDERLINE " " ANSI_NORMAL + ANSI_ADD_UNDERLINE "BAZ " ANSI_NORMAL + ANSI_ADD_UNDERLINE " " ANSI_NORMAL + ANSI_ADD_UNDERLINE "KKK " ANSI_NORMAL "\n" + "hallo knuerz" ANSI_HIGHLIGHT_RED "red" ANSI_HIGHLIGHT_GREEN "green" ANSI_NORMAL + " noansi" ANSI_NORMAL + " " ANSI_GREY "thisisgrey" ANSI_NORMAL "\n"); + + /* Validate that color is correctly stripped */ + ASSERT_OK_ERRNO(setenv("SYSTEMD_COLORS", "0", /* overwrite= */ true)); + reset_terminal_feature_caches(); + + formatted = mfree(formatted); + ASSERT_OK(table_format(table, &formatted)); + + ASSERT_STREQ(formatted, + "FOO BAR BAZ KKK\n" + "hallo knuerzredgreen noansi thisisgrey\n"); + + ASSERT_OK(table_print(table, /* f= */ NULL)); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *j = NULL, *jj = NULL; + + ASSERT_OK(table_to_json(table, &j)); + + ASSERT_OK(sd_json_build(&jj, + SD_JSON_BUILD_ARRAY( + SD_JSON_BUILD_OBJECT( + SD_JSON_BUILD_PAIR_STRING("foo", "hallo"), + SD_JSON_BUILD_PAIR_STRING("bar", "knuerzredgreen"), + SD_JSON_BUILD_PAIR_STRING("baz", "noansi"), + SD_JSON_BUILD_PAIR_STRING("kkk", "thisisgrey"))))); + ASSERT_TRUE(sd_json_variant_equal(j, jj)); + + ASSERT_OK(sd_json_variant_dump(j, SD_JSON_FORMAT_COLOR_AUTO|SD_JSON_FORMAT_PRETTY_AUTO, /* f= */ NULL, /* prefix= */ NULL)); + + ASSERT_OK(setenvf("COLUMNS", /* overwrite= */ true, "%u", saved_columns)); + ASSERT_OK(setenvf("SYSTEMD_COLORS", /* overwrite= */ true, "%i", saved_color)); + ASSERT_OK(set_unset_env("TERM", saved_term, /* overwrite= */ true)); +} + static int intro(void) { - ASSERT_OK(setenv("SYSTEMD_COLORS", "0", 1)); - ASSERT_OK(setenv("COLUMNS", "40", 1)); + ASSERT_OK_ERRNO(setenv("SYSTEMD_COLORS", "0", /* overwrite= */ true)); + ASSERT_OK_ERRNO(setenv("COLUMNS", "40", /* overwrite= */ true)); return EXIT_SUCCESS; }