#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"
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;
.n_columns = n_columns,
.header = true,
.width = (size_t) -1,
+ .cell_height_max = (size_t) -1,
};
return TAKE_PTR(t);
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);
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) {
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)
/* 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)
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);
));
}
+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;
"5min 5min \n"));
test_issue_9549();
+ test_multiline();
return 0;
}