]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
format-table: natively support multiline cells
authorLennart Poettering <lennart@poettering.net>
Mon, 13 Jan 2020 10:20:02 +0000 (11:20 +0100)
committerLennart Poettering <lennart@poettering.net>
Mon, 13 Jan 2020 15:38:28 +0000 (16:38 +0100)
This adds native support for multiline cells.

src/shared/format-table.c
src/shared/format-table.h
src/test/test-format-table.c

index 62ba0c5df3b729efe407ee2840e08679eb680011..178bc78cc0f22f826cdaf885d43aa8df0b228d0a 100644 (file)
@@ -11,6 +11,7 @@
 #include "format-util.h"
 #include "gunicode.h"
 #include "in-addr-util.h"
+#include "locale-util.h"
 #include "memory-util.h"
 #include "pager.h"
 #include "parse-util.h"
@@ -119,6 +120,7 @@ struct Table {
         bool header;   /* Whether to show the header row? */
         size_t width;  /* If == 0 format this as wide as necessary. If (size_t) -1 format this to console
                         * width or less wide, but not wider. Otherwise the width to format this table in. */
+        size_t cell_height_max; /* Maximum number of lines per cell. (If there are more, ellipsis is shown. If (size_t) -1 then no limit is set, the default. == 0 is not allowed.) */
 
         TableData **data;
         size_t n_allocated;
@@ -147,6 +149,7 @@ Table *table_new_raw(size_t n_columns) {
                 .n_columns = n_columns,
                 .header = true,
                 .width = (size_t) -1,
+                .cell_height_max = (size_t) -1,
         };
 
         return TAKE_PTR(t);
@@ -963,6 +966,13 @@ void table_set_width(Table *t, size_t width) {
         t->width = width;
 }
 
+void table_set_cell_height_max(Table *t, size_t height) {
+        assert(t);
+        assert(height >= 1 || height == (size_t) -1);
+
+        t->cell_height_max = height;
+}
+
 int table_set_empty_string(Table *t, const char *empty) {
         assert(t);
 
@@ -1417,26 +1427,94 @@ static const char *table_data_format(Table *t, TableData *d) {
         return d->formatted;
 }
 
-static int table_data_requested_width(Table *table, TableData *d, size_t *ret) {
+static int console_width_height(
+                const char *s,
+                size_t *ret_width,
+                size_t *ret_height) {
+
+        size_t max_width = 0, height = 0;
+        const char *p;
+
+        assert(s);
+
+        /* Determine the width and height in console character cells the specified string needs. */
+
+        do {
+                size_t k;
+
+                p = strchr(s, '\n');
+                if (p) {
+                        _cleanup_free_ char *c = NULL;
+
+                        c = strndup(s, p - s);
+                        if (!c)
+                                return -ENOMEM;
+
+                        k = utf8_console_width(c);
+                        s = p + 1;
+                } else {
+                        k = utf8_console_width(s);
+                        s = NULL;
+                }
+                if (k == (size_t) -1)
+                        return -EINVAL;
+                if (k > max_width)
+                        max_width = k;
+
+                height++;
+        } while (!isempty(s));
+
+        if (ret_width)
+                *ret_width = max_width;
+
+        if (ret_height)
+                *ret_height = height;
+
+        return 0;
+}
+
+static int table_data_requested_width_height(
+                Table *table,
+                TableData *d,
+                size_t *ret_width,
+                size_t *ret_height) {
+
+        _cleanup_free_ char *truncated = NULL;
+        bool truncation_applied = false;
+        size_t width, height;
         const char *t;
-        size_t l;
+        int r;
 
         t = table_data_format(table, d);
         if (!t)
                 return -ENOMEM;
 
-        l = utf8_console_width(t);
-        if (l == (size_t) -1)
-                return -EINVAL;
+        if (table->cell_height_max != (size_t) -1) {
+                r = string_truncate_lines(t, table->cell_height_max, &truncated);
+                if (r < 0)
+                        return r;
+                if (r > 0)
+                        truncation_applied = true;
 
-        if (d->maximum_width != (size_t) -1 && l > d->maximum_width)
-                l = d->maximum_width;
+                t = truncated;
+        }
 
-        if (l < d->minimum_width)
-                l = d->minimum_width;
+        r = console_width_height(t, &width, &height);
+        if (r < 0)
+                return r;
 
-        *ret = l;
-        return 0;
+        if (d->maximum_width != (size_t) -1 && width > d->maximum_width)
+                width = d->maximum_width;
+
+        if (width < d->minimum_width)
+                width = d->minimum_width;
+
+        if (ret_width)
+                *ret_width = width;
+        if (ret_height)
+                *ret_height = height;
+
+        return truncation_applied;
 }
 
 static char *align_string_mem(const char *str, const char *url, size_t new_length, unsigned percent) {
@@ -1573,18 +1651,40 @@ int table_print(Table *t, FILE *f) {
 
                 for (j = 0; j < display_columns; j++) {
                         TableData *d;
-                        size_t req;
+                        size_t req_width, req_height;
 
                         assert_se(d = row[t->display_map ? t->display_map[j] : j]);
 
-                        r = table_data_requested_width(t, d, &req);
+                        r = table_data_requested_width_height(t, d, &req_width, &req_height);
                         if (r < 0)
                                 return r;
+                        if (r > 0) { /* Truncated because too many lines? */
+                                _cleanup_free_ char *last = NULL;
+                                const char *field;
+
+                                /* If we are going to show only the first few lines of a cell that has
+                                 * multiple make sure that we have enough space horizontally to show an
+                                 * ellipsis. Hence, let's figure out the last line, and account for its
+                                 * length plus ellipsis. */
+
+                                field = table_data_format(t, d);
+                                if (!field)
+                                        return -ENOMEM;
+
+                                assert_se(t->cell_height_max > 0);
+                                r = string_extract_line(field, t->cell_height_max-1, &last);
+                                if (r < 0)
+                                        return r;
+
+                                req_width = MAX(req_width,
+                                                utf8_console_width(last) +
+                                                utf8_console_width(special_glyph(SPECIAL_GLYPH_ELLIPSIS)));
+                        }
 
                         /* Determine the biggest width that any cell in this column would like to have */
                         if (requested_width[j] == (size_t) -1 ||
-                            requested_width[j] < req)
-                                requested_width[j] = req;
+                            requested_width[j] < req_width)
+                                requested_width[j] = req_width;
 
                         /* Determine the minimum width any cell in this column needs */
                         if (minimum_width[j] < d->minimum_width)
@@ -1731,6 +1831,8 @@ int table_print(Table *t, FILE *f) {
 
         /* Second pass: show output */
         for (i = t->header ? 0 : 1; i < n_rows; i++) {
+                size_t n_subline = 0;
+                bool more_sublines;
                 TableData **row;
 
                 if (sorted)
@@ -1738,69 +1840,113 @@ int table_print(Table *t, FILE *f) {
                 else
                         row = t->data + i * t->n_columns;
 
-                for (j = 0; j < display_columns; j++) {
-                        _cleanup_free_ char *buffer = NULL;
-                        const char *field;
-                        TableData *d;
-                        size_t l;
-
-                        assert_se(d = row[t->display_map ? t->display_map[j] : j]);
-
-                        field = table_data_format(t, d);
-                        if (!field)
-                                return -ENOMEM;
+                do {
+                        more_sublines = false;
 
-                        l = utf8_console_width(field);
-                        if (l > width[j]) {
-                                /* Field is wider than allocated space. Let's ellipsize */
-
-                                buffer = ellipsize(field, width[j], d->ellipsize_percent);
-                                if (!buffer)
-                                        return -ENOMEM;
-
-                                field = buffer;
+                        for (j = 0; j < display_columns; j++) {
+                                _cleanup_free_ char *buffer = NULL, *extracted = NULL;
+                                bool lines_truncated = false;
+                                const char *field;
+                                TableData *d;
+                                size_t l;
 
-                        } else if (l < width[j]) {
-                                /* Field is shorter than allocated space. Let's align with spaces */
+                                assert_se(d = row[t->display_map ? t->display_map[j] : j]);
 
-                                buffer = align_string_mem(field, d->url, width[j], d->align_percent);
-                                if (!buffer)
+                                field = table_data_format(t, d);
+                                if (!field)
                                         return -ENOMEM;
 
-                                field = buffer;
-                        }
-
-                        if (l >= width[j] && d->url) {
-                                _cleanup_free_ char *clickable = NULL;
-
-                                r = terminal_urlify(d->url, field, &clickable);
+                                r = string_extract_line(field, n_subline, &extracted);
                                 if (r < 0)
                                         return r;
-
-                                free_and_replace(buffer, clickable);
-                                field = buffer;
-                        }
-
-                        if (row == t->data) /* underline header line fully, including the column separator */
-                                fputs(ansi_underline(), f);
-
-                        if (j > 0)
-                                fputc(' ', f); /* column separator */
-
-                        if (table_data_color(d) && colors_enabled()) {
-                                if (row == t->data) /* first undo header underliner */
+                                if (r > 0) {
+                                        /* There are more lines to come */
+                                        if ((t->cell_height_max == (size_t) -1 || n_subline + 1 < t->cell_height_max))
+                                                more_sublines = true; /* There are more lines to come */
+                                        else
+                                                lines_truncated = true;
+                                }
+                                if (extracted)
+                                        field = extracted;
+
+                                l = utf8_console_width(field);
+                                if (l > width[j]) {
+                                        /* Field is wider than allocated space. Let's ellipsize */
+
+                                        buffer = ellipsize(field, width[j], /* ellipsize at the end if we truncated coming lines, otherwise honour configuration */
+                                                           lines_truncated ? 100 : d->ellipsize_percent);
+                                        if (!buffer)
+                                                return -ENOMEM;
+
+                                        field = buffer;
+                                } else {
+                                        if (lines_truncated) {
+                                                _cleanup_free_ char *padded = NULL;
+
+                                                /* We truncated more lines of this cell, let's add an
+                                                 * ellipsis. We first append it, but thta might make our
+                                                 * string grow above what we have space for, hence ellipsize
+                                                 * right after. This will truncate the ellipsis and add a new
+                                                 * one. */
+
+                                                padded = strjoin(field, special_glyph(SPECIAL_GLYPH_ELLIPSIS));
+                                                if (!padded)
+                                                        return -ENOMEM;
+
+                                                buffer = ellipsize(padded, width[j], 100);
+                                                if (!buffer)
+                                                        return -ENOMEM;
+
+                                                field = buffer;
+                                                l = utf8_console_width(field);
+                                        }
+
+                                        if (l < width[j]) {
+                                                _cleanup_free_ char *aligned = NULL;
+                                                /* Field is shorter than allocated space. Let's align with spaces */
+
+                                                aligned = align_string_mem(field, d->url, width[j], d->align_percent);
+                                                if (!aligned)
+                                                        return -ENOMEM;
+
+                                                free_and_replace(buffer, aligned);
+                                                field = buffer;
+                                        }
+                                }
+
+                                if (l >= width[j] && d->url) {
+                                        _cleanup_free_ char *clickable = NULL;
+
+                                        r = terminal_urlify(d->url, field, &clickable);
+                                        if (r < 0)
+                                                return r;
+
+                                        free_and_replace(buffer, clickable);
+                                        field = buffer;
+                                }
+
+                                if (row == t->data) /* underline header line fully, including the column separator */
+                                        fputs(ansi_underline(), f);
+
+                                if (j > 0)
+                                        fputc(' ', f); /* column separator */
+
+                                if (table_data_color(d) && colors_enabled()) {
+                                        if (row == t->data) /* first undo header underliner */
+                                                fputs(ANSI_NORMAL, f);
+
+                                        fputs(table_data_color(d), f);
+                                }
+
+                                fputs(field, f);
+
+                                if (colors_enabled() && (table_data_color(d) || row == t->data))
                                         fputs(ANSI_NORMAL, f);
-
-                                fputs(table_data_color(d), f);
                         }
 
-                        fputs(field, f);
-
-                        if (colors_enabled() && (table_data_color(d) || row == t->data))
-                                fputs(ANSI_NORMAL, f);
-                }
-
-                fputc('\n', f);
+                        fputc('\n', f);
+                        n_subline ++;
+                } while (more_sublines);
         }
 
         return fflush_and_check(f);
index 60af0a96d4b502317e80f5c0a2d65ac021d7cf14..cb9232b47ada5425543bbe19f6c84e45adec9561 100644 (file)
@@ -96,6 +96,7 @@ int table_add_many_internal(Table *t, TableDataType first_type, ...);
 
 void table_set_header(Table *table, bool b);
 void table_set_width(Table *t, size_t width);
+void table_set_cell_height_max(Table *t, size_t height);
 int table_set_empty_string(Table *t, const char *empty);
 int table_set_display(Table *t, size_t first_column, ...);
 int table_set_sort(Table *t, size_t first_column, ...);
index 96b1d77701336d3b8d9a3c4f815b7c00456c1df8..7bf62e48e20ea581d429a2416ac1dea39bbd8cf3 100644 (file)
@@ -31,6 +31,118 @@ static void test_issue_9549(void) {
                         ));
 }
 
+static void test_multiline(void) {
+        _cleanup_(table_unrefp) Table *table = NULL;
+        _cleanup_free_ char *formatted = NULL;
+
+        assert_se(table = table_new("foo", "bar"));
+
+        assert_se(table_set_align_percent(table, TABLE_HEADER_CELL(1), 100) >= 0);
+
+        assert_se(table_add_many(table,
+                                 TABLE_STRING, "three\ndifferent\nlines",
+                                 TABLE_STRING, "two\nlines\n") >= 0);
+
+        table_set_cell_height_max(table, 1);
+        assert_se(table_format(table, &formatted) >= 0);
+        fputs(formatted, stdout);
+        assert_se(streq(formatted,
+                        "FOO     BAR\n"
+                        "three… two…\n"));
+        formatted = mfree(formatted);
+
+        table_set_cell_height_max(table, 2);
+        assert_se(table_format(table, &formatted) >= 0);
+        fputs(formatted, stdout);
+        assert_se(streq(formatted,
+                        "FOO          BAR\n"
+                        "three        two\n"
+                        "different… lines\n"));
+        formatted = mfree(formatted);
+
+        table_set_cell_height_max(table, 3);
+        assert_se(table_format(table, &formatted) >= 0);
+        fputs(formatted, stdout);
+        assert_se(streq(formatted,
+                        "FOO         BAR\n"
+                        "three       two\n"
+                        "different lines\n"
+                        "lines          \n"));
+        formatted = mfree(formatted);
+
+        table_set_cell_height_max(table, (size_t) -1);
+        assert_se(table_format(table, &formatted) >= 0);
+        fputs(formatted, stdout);
+        assert_se(streq(formatted,
+                        "FOO         BAR\n"
+                        "three       two\n"
+                        "different lines\n"
+                        "lines          \n"));
+        formatted = mfree(formatted);
+
+        assert_se(table_add_many(table,
+                                 TABLE_STRING, "short",
+                                 TABLE_STRING, "a\npair") >= 0);
+
+        assert_se(table_add_many(table,
+                                 TABLE_STRING, "short2\n",
+                                 TABLE_STRING, "a\nfour\nline\ncell") >= 0);
+
+        table_set_cell_height_max(table, 1);
+        assert_se(table_format(table, &formatted) >= 0);
+        fputs(formatted, stdout);
+        assert_se(streq(formatted,
+                        "FOO     BAR\n"
+                        "three… two…\n"
+                        "short    a…\n"
+                        "short2   a…\n"));
+        formatted = mfree(formatted);
+
+        table_set_cell_height_max(table, 2);
+        assert_se(table_format(table, &formatted) >= 0);
+        fputs(formatted, stdout);
+        assert_se(streq(formatted,
+                        "FOO          BAR\n"
+                        "three        two\n"
+                        "different… lines\n"
+                        "short          a\n"
+                        "            pair\n"
+                        "short2         a\n"
+                        "           four…\n"));
+        formatted = mfree(formatted);
+
+        table_set_cell_height_max(table, 3);
+        assert_se(table_format(table, &formatted) >= 0);
+        fputs(formatted, stdout);
+        assert_se(streq(formatted,
+                        "FOO         BAR\n"
+                        "three       two\n"
+                        "different lines\n"
+                        "lines          \n"
+                        "short         a\n"
+                        "           pair\n"
+                        "short2        a\n"
+                        "           four\n"
+                        "          line…\n"));
+        formatted = mfree(formatted);
+
+        table_set_cell_height_max(table, (size_t) -1);
+        assert_se(table_format(table, &formatted) >= 0);
+        fputs(formatted, stdout);
+        assert_se(streq(formatted,
+                        "FOO         BAR\n"
+                        "three       two\n"
+                        "different lines\n"
+                        "lines          \n"
+                        "short         a\n"
+                        "           pair\n"
+                        "short2        a\n"
+                        "           four\n"
+                        "           line\n"
+                        "           cell\n"));
+        formatted = mfree(formatted);
+}
+
 int main(int argc, char *argv[]) {
 
         _cleanup_(table_unrefp) Table *t = NULL;
@@ -172,6 +284,7 @@ int main(int argc, char *argv[]) {
                                 "5min              5min                          \n"));
 
         test_issue_9549();
+        test_multiline();
 
         return 0;
 }