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.
This commit is contained in:
Lennart Poettering
2026-01-21 21:32:49 +01:00
parent adcc87f802
commit f42dc5ea18
3 changed files with 179 additions and 16 deletions
+75 -2
View File
@@ -4,6 +4,8 @@
#include <unistd.h>
#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;
}