From f42dc5ea18e920c70a8571309bfd66757f5e01cb Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 15 Jan 2026 08:51:05 +0100 Subject: [PATCH 1/5] format-table: add new string cell type that accepts ANSI sequences For various usecases it's useful that we can embed ANSI sequences in cells of tables. For example, I hope we can eventually switch "systemctl status" output to use the table formatter, and multiple of its fields contain ANSI sequences (since they pack multiple different pieces information into the same field, and highlight parts of it to communicate relevance of distinct parts). Add a distinct cell type for this, which gets special processing when we output to a terminal that doesn't support ANSI sequences, and to JSON: we strip the sequences. --- src/shared/format-table.c | 117 ++++++++++++++++++++++++++++++----- src/shared/format-table.h | 1 + src/test/test-format-table.c | 77 ++++++++++++++++++++++- 3 files changed, 179 insertions(+), 16 deletions(-) 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/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; } From a01cd5bed33ce48e4e46eea07bc60955d9eea3d3 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 12 Jan 2026 13:40:27 +0100 Subject: [PATCH 2/5] man: introduce FANCY_NAME field --- man/os-release.xml | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) 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" + + From d6749f35ae4078a9c2b69ae912a3a0b960df9de0 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 12 Jan 2026 13:44:54 +0100 Subject: [PATCH 3/5] pid1: show fancy name field at boot, if specified --- src/core/main.c | 64 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 12 deletions(-) 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", From 955446d2e4d434c184140c83a4404465d12e3ad1 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 12 Jan 2026 13:46:47 +0100 Subject: [PATCH 4/5] hostnamed: expose fancy OS name as a field --- man/org.freedesktop.hostname1.xml | 13 +++++++-- src/hostname/hostnamed.c | 36 ++++++++++++++++++++++-- src/shared/varlink-io.systemd.Hostname.c | 3 ++ 3 files changed, 47 insertions(+), 5 deletions(-) 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/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/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), From 1dab8472e057c2eb1bf437612165595719859bc3 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 14 Jan 2026 18:23:03 +0100 Subject: [PATCH 5/5] hostnamectl: show fancy name if available --- src/hostname/hostnamectl.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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) },