1 /* SPDX-License-Identifier: LGPL-2.1+ */
6 #include "alloc-util.h"
9 #include "format-table.h"
10 #include "format-util.h"
12 #include "memory-util.h"
14 #include "parse-util.h"
15 #include "pretty-print.h"
16 #include "sort-util.h"
17 #include "string-util.h"
19 #include "terminal-util.h"
20 #include "time-util.h"
24 #define DEFAULT_WEIGHT 100
27 A few notes on implementation details:
29 - TableCell is a 'fake' structure, it's just used as data type to pass references to specific cell positions in the
30 table. It can be easily converted to an index number and back.
32 - TableData is where the actual data is stored: it encapsulates the data and formatting for a specific cell. It's
33 'pseudo-immutable' and ref-counted. When a cell's data's formatting is to be changed, we duplicate the object if the
34 ref-counting is larger than 1. Note that TableData and its ref-counting is mostly not visible to the outside. The
35 outside only sees Table and TableCell.
37 - The Table object stores a simple one-dimensional array of references to TableData objects, one row after the
40 - There's no special concept of a "row" or "column" in the table, and no special concept of the "header" row. It's all
41 derived from the cell index: we know how many cells are to be stored in a row, and can determine the rest from
42 that. The first row is always the header row. If header display is turned off we simply skip outputting the first
43 row. Also, when sorting rows we always leave the first row where it is, as the header shouldn't move.
45 - Note because there's no row and no column object some properties that might be appropriate as row/column properties
46 are exposed as cell properties instead. For example, the "weight" of a column (which is used to determine where to
47 add/remove space preferable when expanding/compressing tables horizontally) is actually made the "weight" of a
48 cell. Given that we usually need it per-column though we will calculate the average across every cell of the column
51 - To make things easy, when cells are added without any explicit configured formatting, then we'll copy the formatting
52 from the same cell in the previous cell. This is particularly useful for the "weight" of the cell (see above), as
53 this means setting the weight of the cells of the header row will nicely propagate to all cells in the other rows.
56 typedef struct TableData
{
60 size_t minimum_width
; /* minimum width for the column */
61 size_t maximum_width
; /* maximum width for the column */
62 unsigned weight
; /* the horizontal weight for this column, in case the table is expanded/compressed */
63 unsigned ellipsize_percent
; /* 0 … 100, where to place the ellipsis when compression is needed */
64 unsigned align_percent
; /* 0 … 100, where to pad with spaces when expanding is needed. 0: left-aligned, 100: right-aligned */
66 bool uppercase
; /* Uppercase string on display */
68 const char *color
; /* ANSI color string to use for this cell. When written to terminal should not move cursor. Will automatically be reset after the cell */
69 char *url
; /* A URL to use for a clickable hyperlink */
70 char *formatted
; /* A cached textual representation of the cell data, before ellipsation/alignment */
73 uint8_t data
[0]; /* data is generic array */
85 int percent
; /* we use 'int' as datatype for percent values in order to match the result of parse_percent() */
87 /* … add more here as we start supporting more cell data types … */
91 static size_t TABLE_CELL_TO_INDEX(TableCell
*cell
) {
96 i
= PTR_TO_SIZE(cell
);
102 static TableCell
* TABLE_INDEX_TO_CELL(size_t index
) {
103 assert(index
!= (size_t) -1);
104 return SIZE_TO_PTR(index
+ 1);
111 bool header
; /* Whether to show the header row? */
112 size_t width
; /* If != (size_t) -1 the width to format this table in */
117 size_t *display_map
; /* List of columns to show (by their index). It's fine if columns are listed multiple times or not at all */
118 size_t n_display_map
;
120 size_t *sort_map
; /* The columns to order rows by, in order of preference. */
126 Table
*table_new_raw(size_t n_columns
) {
127 _cleanup_(table_unrefp
) Table
*t
= NULL
;
129 assert(n_columns
> 0);
135 *t
= (struct Table
) {
136 .n_columns
= n_columns
,
138 .width
= (size_t) -1,
144 Table
*table_new_internal(const char *first_header
, ...) {
145 _cleanup_(table_unrefp
) Table
*t
= NULL
;
146 size_t n_columns
= 1;
151 assert(first_header
);
153 va_start(ap
, first_header
);
155 h
= va_arg(ap
, const char*);
163 t
= table_new_raw(n_columns
);
167 va_start(ap
, first_header
);
168 for (h
= first_header
; h
; h
= va_arg(ap
, const char*)) {
171 r
= table_add_cell(t
, &cell
, TABLE_STRING
, h
);
177 /* Make the table header uppercase */
178 r
= table_set_uppercase(t
, cell
, true);
186 assert(t
->n_columns
== t
->n_cells
);
190 static TableData
*table_data_free(TableData
*d
) {
199 DEFINE_PRIVATE_TRIVIAL_REF_UNREF_FUNC(TableData
, table_data
, table_data_free
);
200 DEFINE_TRIVIAL_CLEANUP_FUNC(TableData
*, table_data_unref
);
202 Table
*table_unref(Table
*t
) {
208 for (i
= 0; i
< t
->n_cells
; i
++)
209 table_data_unref(t
->data
[i
]);
212 free(t
->display_map
);
214 free(t
->reverse_map
);
219 static size_t table_data_size(TableDataType type
, const void *data
) {
227 return strlen(data
) + 1;
232 case TABLE_TIMESTAMP
:
233 case TABLE_TIMESTAMP_UTC
:
234 case TABLE_TIMESTAMP_RELATIVE
:
236 case TABLE_TIMESPAN_MSEC
:
237 return sizeof(usec_t
);
243 return sizeof(uint64_t);
247 return sizeof(uint32_t);
256 assert_not_reached("Uh? Unexpected cell type");
260 static bool table_data_matches(
264 size_t minimum_width
,
265 size_t maximum_width
,
267 unsigned align_percent
,
268 unsigned ellipsize_percent
) {
276 if (d
->minimum_width
!= minimum_width
)
279 if (d
->maximum_width
!= maximum_width
)
282 if (d
->weight
!= weight
)
285 if (d
->align_percent
!= align_percent
)
288 if (d
->ellipsize_percent
!= ellipsize_percent
)
291 /* If a color/url/uppercase flag is set, refuse to merge */
299 k
= table_data_size(type
, data
);
300 l
= table_data_size(d
->type
, d
->data
);
305 return memcmp_safe(data
, d
->data
, l
) == 0;
308 static TableData
*table_data_new(
311 size_t minimum_width
,
312 size_t maximum_width
,
314 unsigned align_percent
,
315 unsigned ellipsize_percent
) {
320 data_size
= table_data_size(type
, data
);
322 d
= malloc0(offsetof(TableData
, data
) + data_size
);
328 d
->minimum_width
= minimum_width
;
329 d
->maximum_width
= maximum_width
;
331 d
->align_percent
= align_percent
;
332 d
->ellipsize_percent
= ellipsize_percent
;
333 memcpy_safe(d
->data
, data
, data_size
);
338 int table_add_cell_full(
340 TableCell
**ret_cell
,
343 size_t minimum_width
,
344 size_t maximum_width
,
346 unsigned align_percent
,
347 unsigned ellipsize_percent
) {
349 _cleanup_(table_data_unrefp
) TableData
*d
= NULL
;
354 assert(type
< _TABLE_DATA_TYPE_MAX
);
356 /* Determine the cell adjacent to the current one, but one row up */
357 if (t
->n_cells
>= t
->n_columns
)
358 assert_se(p
= t
->data
[t
->n_cells
- t
->n_columns
]);
362 /* If formatting parameters are left unspecified, copy from the previous row */
363 if (minimum_width
== (size_t) -1)
364 minimum_width
= p
? p
->minimum_width
: 1;
366 if (weight
== (unsigned) -1)
367 weight
= p
? p
->weight
: DEFAULT_WEIGHT
;
369 if (align_percent
== (unsigned) -1)
370 align_percent
= p
? p
->align_percent
: 0;
372 if (ellipsize_percent
== (unsigned) -1)
373 ellipsize_percent
= p
? p
->ellipsize_percent
: 100;
375 assert(align_percent
<= 100);
376 assert(ellipsize_percent
<= 100);
378 /* Small optimization: Pretty often adjacent cells in two subsequent lines have the same data and
379 * formatting. Let's see if we can reuse the cell data and ref it once more. */
381 if (p
&& table_data_matches(p
, type
, data
, minimum_width
, maximum_width
, weight
, align_percent
, ellipsize_percent
))
382 d
= table_data_ref(p
);
384 d
= table_data_new(type
, data
, minimum_width
, maximum_width
, weight
, align_percent
, ellipsize_percent
);
389 if (!GREEDY_REALLOC(t
->data
, t
->n_allocated
, MAX(t
->n_cells
+ 1, t
->n_columns
)))
393 *ret_cell
= TABLE_INDEX_TO_CELL(t
->n_cells
);
395 t
->data
[t
->n_cells
++] = TAKE_PTR(d
);
400 int table_add_cell_stringf(Table
*t
, TableCell
**ret_cell
, const char *format
, ...) {
401 _cleanup_free_
char *buffer
= NULL
;
405 va_start(ap
, format
);
406 r
= vasprintf(&buffer
, format
, ap
);
411 return table_add_cell(t
, ret_cell
, TABLE_STRING
, buffer
);
414 int table_dup_cell(Table
*t
, TableCell
*cell
) {
419 /* Add the data of the specified cell a second time as a new cell to the end. */
421 i
= TABLE_CELL_TO_INDEX(cell
);
425 if (!GREEDY_REALLOC(t
->data
, t
->n_allocated
, MAX(t
->n_cells
+ 1, t
->n_columns
)))
428 t
->data
[t
->n_cells
++] = table_data_ref(t
->data
[i
]);
432 static int table_dedup_cell(Table
*t
, TableCell
*cell
) {
433 _cleanup_free_
char *curl
= NULL
;
439 /* Helper call that ensures the specified cell's data object has a ref count of 1, which we can use before
440 * changing a cell's formatting without effecting every other cell's formatting that shares the same data */
442 i
= TABLE_CELL_TO_INDEX(cell
);
446 assert_se(od
= t
->data
[i
]);
450 assert(od
->n_ref
> 1);
453 curl
= strdup(od
->url
);
465 od
->ellipsize_percent
);
469 nd
->color
= od
->color
;
470 nd
->url
= TAKE_PTR(curl
);
471 nd
->uppercase
= od
->uppercase
;
473 table_data_unref(od
);
476 assert(nd
->n_ref
== 1);
481 static TableData
*table_get_data(Table
*t
, TableCell
*cell
) {
487 /* Get the data object of the specified cell, or NULL if it doesn't exist */
489 i
= TABLE_CELL_TO_INDEX(cell
);
494 assert(t
->data
[i
]->n_ref
> 0);
499 int table_set_minimum_width(Table
*t
, TableCell
*cell
, size_t minimum_width
) {
505 if (minimum_width
== (size_t) -1)
508 r
= table_dedup_cell(t
, cell
);
512 table_get_data(t
, cell
)->minimum_width
= minimum_width
;
516 int table_set_maximum_width(Table
*t
, TableCell
*cell
, size_t maximum_width
) {
522 r
= table_dedup_cell(t
, cell
);
526 table_get_data(t
, cell
)->maximum_width
= maximum_width
;
530 int table_set_weight(Table
*t
, TableCell
*cell
, unsigned weight
) {
536 if (weight
== (unsigned) -1)
537 weight
= DEFAULT_WEIGHT
;
539 r
= table_dedup_cell(t
, cell
);
543 table_get_data(t
, cell
)->weight
= weight
;
547 int table_set_align_percent(Table
*t
, TableCell
*cell
, unsigned percent
) {
553 if (percent
== (unsigned) -1)
556 assert(percent
<= 100);
558 r
= table_dedup_cell(t
, cell
);
562 table_get_data(t
, cell
)->align_percent
= percent
;
566 int table_set_ellipsize_percent(Table
*t
, TableCell
*cell
, unsigned percent
) {
572 if (percent
== (unsigned) -1)
575 assert(percent
<= 100);
577 r
= table_dedup_cell(t
, cell
);
581 table_get_data(t
, cell
)->ellipsize_percent
= percent
;
585 int table_set_color(Table
*t
, TableCell
*cell
, const char *color
) {
591 r
= table_dedup_cell(t
, cell
);
595 table_get_data(t
, cell
)->color
= empty_to_null(color
);
599 int table_set_url(Table
*t
, TableCell
*cell
, const char *url
) {
600 _cleanup_free_
char *copy
= NULL
;
612 r
= table_dedup_cell(t
, cell
);
616 return free_and_replace(table_get_data(t
, cell
)->url
, copy
);
619 int table_set_uppercase(Table
*t
, TableCell
*cell
, bool b
) {
626 r
= table_dedup_cell(t
, cell
);
630 assert_se(d
= table_get_data(t
, cell
));
632 if (d
->uppercase
== b
)
635 d
->formatted
= mfree(d
->formatted
);
640 int table_update(Table
*t
, TableCell
*cell
, TableDataType type
, const void *data
) {
641 _cleanup_free_
char *curl
= NULL
;
648 i
= TABLE_CELL_TO_INDEX(cell
);
652 assert_se(od
= t
->data
[i
]);
655 curl
= strdup(od
->url
);
667 od
->ellipsize_percent
);
671 nd
->color
= od
->color
;
672 nd
->url
= TAKE_PTR(curl
);
673 nd
->uppercase
= od
->uppercase
;
675 table_data_unref(od
);
681 int table_add_many_internal(Table
*t
, TableDataType first_type
, ...) {
684 TableCell
*last_cell
= NULL
;
688 assert(first_type
>= 0);
689 assert(first_type
< _TABLE_DATA_TYPE_MAX
);
693 va_start(ap
, first_type
);
717 data
= va_arg(ap
, const char *);
721 buffer
.b
= va_arg(ap
, int);
725 case TABLE_TIMESTAMP
:
726 case TABLE_TIMESTAMP_UTC
:
727 case TABLE_TIMESTAMP_RELATIVE
:
729 case TABLE_TIMESPAN_MSEC
:
730 buffer
.usec
= va_arg(ap
, usec_t
);
736 buffer
.size
= va_arg(ap
, uint64_t);
741 buffer
.int_val
= va_arg(ap
, int);
742 data
= &buffer
.int_val
;
746 buffer
.int32
= va_arg(ap
, int32_t);
747 data
= &buffer
.int32
;
751 buffer
.int64
= va_arg(ap
, int64_t);
752 data
= &buffer
.int64
;
756 buffer
.uint_val
= va_arg(ap
, unsigned);
757 data
= &buffer
.uint_val
;
761 buffer
.uint32
= va_arg(ap
, uint32_t);
762 data
= &buffer
.uint32
;
766 buffer
.uint64
= va_arg(ap
, uint64_t);
767 data
= &buffer
.uint64
;
771 buffer
.percent
= va_arg(ap
, int);
772 data
= &buffer
.percent
;
776 buffer
.ifindex
= va_arg(ap
, int);
777 data
= &buffer
.ifindex
;
780 case TABLE_SET_MINIMUM_WIDTH
: {
781 size_t w
= va_arg(ap
, size_t);
783 r
= table_set_minimum_width(t
, last_cell
, w
);
787 case TABLE_SET_MAXIMUM_WIDTH
: {
788 size_t w
= va_arg(ap
, size_t);
789 r
= table_set_maximum_width(t
, last_cell
, w
);
793 case TABLE_SET_WEIGHT
: {
794 unsigned w
= va_arg(ap
, unsigned);
795 r
= table_set_weight(t
, last_cell
, w
);
799 case TABLE_SET_ALIGN_PERCENT
: {
800 unsigned p
= va_arg(ap
, unsigned);
801 r
= table_set_align_percent(t
, last_cell
, p
);
805 case TABLE_SET_ELLIPSIZE_PERCENT
: {
806 unsigned p
= va_arg(ap
, unsigned);
807 r
= table_set_ellipsize_percent(t
, last_cell
, p
);
811 case TABLE_SET_COLOR
: {
812 const char *c
= va_arg(ap
, const char*);
813 r
= table_set_color(t
, last_cell
, c
);
817 case TABLE_SET_URL
: {
818 const char *u
= va_arg(ap
, const char*);
819 r
= table_set_url(t
, last_cell
, u
);
823 case TABLE_SET_UPPERCASE
: {
824 int u
= va_arg(ap
, int);
825 r
= table_set_uppercase(t
, last_cell
, u
);
829 case _TABLE_DATA_TYPE_MAX
:
830 /* Used as end marker */
835 assert_not_reached("Uh? Unexpected data type.");
838 if (type
< _TABLE_DATA_TYPE_MAX
)
839 r
= table_add_cell(t
, &last_cell
, type
, data
);
846 type
= va_arg(ap
, TableDataType
);
850 void table_set_header(Table
*t
, bool b
) {
856 void table_set_width(Table
*t
, size_t width
) {
862 int table_set_display(Table
*t
, size_t first_column
, ...) {
863 size_t allocated
, column
;
868 allocated
= t
->n_display_map
;
869 column
= first_column
;
871 va_start(ap
, first_column
);
873 assert(column
< t
->n_columns
);
875 if (!GREEDY_REALLOC(t
->display_map
, allocated
, MAX(t
->n_columns
, t
->n_display_map
+1))) {
880 t
->display_map
[t
->n_display_map
++] = column
;
882 column
= va_arg(ap
, size_t);
883 if (column
== (size_t) -1)
892 int table_set_sort(Table
*t
, size_t first_column
, ...) {
893 size_t allocated
, column
;
898 allocated
= t
->n_sort_map
;
899 column
= first_column
;
901 va_start(ap
, first_column
);
903 assert(column
< t
->n_columns
);
905 if (!GREEDY_REALLOC(t
->sort_map
, allocated
, MAX(t
->n_columns
, t
->n_sort_map
+1))) {
910 t
->sort_map
[t
->n_sort_map
++] = column
;
912 column
= va_arg(ap
, size_t);
913 if (column
== (size_t) -1)
921 static int cell_data_compare(TableData
*a
, size_t index_a
, TableData
*b
, size_t index_b
) {
925 if (a
->type
== b
->type
) {
927 /* We only define ordering for cells of the same data type. If cells with different data types are
928 * compared we follow the order the cells were originally added in */
933 return strcmp(a
->string
, b
->string
);
936 if (!a
->boolean
&& b
->boolean
)
938 if (a
->boolean
&& !b
->boolean
)
942 case TABLE_TIMESTAMP
:
943 case TABLE_TIMESTAMP_UTC
:
944 case TABLE_TIMESTAMP_RELATIVE
:
945 return CMP(a
->timestamp
, b
->timestamp
);
948 case TABLE_TIMESPAN_MSEC
:
949 return CMP(a
->timespan
, b
->timespan
);
953 return CMP(a
->size
, b
->size
);
956 return CMP(a
->int_val
, b
->int_val
);
959 return CMP(a
->int32
, b
->int32
);
962 return CMP(a
->int64
, b
->int64
);
965 return CMP(a
->uint_val
, b
->uint_val
);
968 return CMP(a
->uint32
, b
->uint32
);
971 return CMP(a
->uint64
, b
->uint64
);
974 return CMP(a
->percent
, b
->percent
);
977 return CMP(a
->ifindex
, b
->ifindex
);
984 /* Generic fallback using the original order in which the cells where added. */
985 return CMP(index_a
, index_b
);
988 static int table_data_compare(const size_t *a
, const size_t *b
, Table
*t
) {
995 /* Make sure the header stays at the beginning */
996 if (*a
< t
->n_columns
&& *b
< t
->n_columns
)
998 if (*a
< t
->n_columns
)
1000 if (*b
< t
->n_columns
)
1003 /* Order other lines by the sorting map */
1004 for (i
= 0; i
< t
->n_sort_map
; i
++) {
1007 d
= t
->data
[*a
+ t
->sort_map
[i
]];
1008 dd
= t
->data
[*b
+ t
->sort_map
[i
]];
1010 r
= cell_data_compare(d
, *a
, dd
, *b
);
1012 return t
->reverse_map
&& t
->reverse_map
[t
->sort_map
[i
]] ? -r
: r
;
1015 /* Order identical lines by the order there were originally added in */
1019 static const char *table_data_format(TableData
*d
) {
1023 return d
->formatted
;
1033 d
->formatted
= new(char, strlen(d
->string
) + 1);
1037 for (p
= d
->string
, q
= d
->formatted
; *p
; p
++, q
++)
1038 *q
= (char) toupper((unsigned char) *p
);
1041 return d
->formatted
;
1047 return yes_no(d
->boolean
);
1049 case TABLE_TIMESTAMP
:
1050 case TABLE_TIMESTAMP_UTC
:
1051 case TABLE_TIMESTAMP_RELATIVE
: {
1052 _cleanup_free_
char *p
;
1055 p
= new(char, FORMAT_TIMESTAMP_MAX
);
1059 if (d
->type
== TABLE_TIMESTAMP
)
1060 ret
= format_timestamp(p
, FORMAT_TIMESTAMP_MAX
, d
->timestamp
);
1061 else if (d
->type
== TABLE_TIMESTAMP_UTC
)
1062 ret
= format_timestamp_utc(p
, FORMAT_TIMESTAMP_MAX
, d
->timestamp
);
1064 ret
= format_timestamp_relative(p
, FORMAT_TIMESTAMP_MAX
, d
->timestamp
);
1068 d
->formatted
= TAKE_PTR(p
);
1072 case TABLE_TIMESPAN
:
1073 case TABLE_TIMESPAN_MSEC
: {
1074 _cleanup_free_
char *p
;
1076 p
= new(char, FORMAT_TIMESPAN_MAX
);
1080 if (!format_timespan(p
, FORMAT_TIMESPAN_MAX
, d
->timespan
,
1081 d
->type
== TABLE_TIMESPAN
? 0 : USEC_PER_MSEC
))
1084 d
->formatted
= TAKE_PTR(p
);
1089 _cleanup_free_
char *p
;
1091 p
= new(char, FORMAT_BYTES_MAX
);
1095 if (!format_bytes(p
, FORMAT_BYTES_MAX
, d
->size
))
1098 d
->formatted
= TAKE_PTR(p
);
1103 _cleanup_free_
char *p
;
1106 p
= new(char, FORMAT_BYTES_MAX
+2);
1110 if (!format_bytes_full(p
, FORMAT_BYTES_MAX
, d
->size
, 0))
1114 strscpy(p
+ n
, FORMAT_BYTES_MAX
+ 2 - n
, "bps");
1116 d
->formatted
= TAKE_PTR(p
);
1121 _cleanup_free_
char *p
;
1123 p
= new(char, DECIMAL_STR_WIDTH(d
->int_val
) + 1);
1127 sprintf(p
, "%i", d
->int_val
);
1128 d
->formatted
= TAKE_PTR(p
);
1133 _cleanup_free_
char *p
;
1135 p
= new(char, DECIMAL_STR_WIDTH(d
->int32
) + 1);
1139 sprintf(p
, "%" PRIi32
, d
->int32
);
1140 d
->formatted
= TAKE_PTR(p
);
1145 _cleanup_free_
char *p
;
1147 p
= new(char, DECIMAL_STR_WIDTH(d
->int64
) + 1);
1151 sprintf(p
, "%" PRIi64
, d
->int64
);
1152 d
->formatted
= TAKE_PTR(p
);
1157 _cleanup_free_
char *p
;
1159 p
= new(char, DECIMAL_STR_WIDTH(d
->uint_val
) + 1);
1163 sprintf(p
, "%u", d
->uint_val
);
1164 d
->formatted
= TAKE_PTR(p
);
1168 case TABLE_UINT32
: {
1169 _cleanup_free_
char *p
;
1171 p
= new(char, DECIMAL_STR_WIDTH(d
->uint32
) + 1);
1175 sprintf(p
, "%" PRIu32
, d
->uint32
);
1176 d
->formatted
= TAKE_PTR(p
);
1180 case TABLE_UINT64
: {
1181 _cleanup_free_
char *p
;
1183 p
= new(char, DECIMAL_STR_WIDTH(d
->uint64
) + 1);
1187 sprintf(p
, "%" PRIu64
, d
->uint64
);
1188 d
->formatted
= TAKE_PTR(p
);
1192 case TABLE_PERCENT
: {
1193 _cleanup_free_
char *p
;
1195 p
= new(char, DECIMAL_STR_WIDTH(d
->percent
) + 2);
1199 sprintf(p
, "%i%%" , d
->percent
);
1200 d
->formatted
= TAKE_PTR(p
);
1204 case TABLE_IFINDEX
: {
1205 _cleanup_free_
char *p
;
1206 char name
[IF_NAMESIZE
+ 1];
1208 if (format_ifname(d
->ifindex
, name
)) {
1213 if (asprintf(&p
, "%i" , d
->ifindex
) < 0)
1217 d
->formatted
= TAKE_PTR(p
);
1222 assert_not_reached("Unexpected type?");
1225 return d
->formatted
;
1228 static int table_data_requested_width(TableData
*d
, size_t *ret
) {
1232 t
= table_data_format(d
);
1236 l
= utf8_console_width(t
);
1237 if (l
== (size_t) -1)
1240 if (d
->maximum_width
!= (size_t) -1 && l
> d
->maximum_width
)
1241 l
= d
->maximum_width
;
1243 if (l
< d
->minimum_width
)
1244 l
= d
->minimum_width
;
1250 static char *align_string_mem(const char *str
, const char *url
, size_t new_length
, unsigned percent
) {
1251 size_t w
= 0, space
, lspace
, old_length
, clickable_length
;
1252 _cleanup_free_
char *clickable
= NULL
;
1258 /* As with ellipsize_mem(), 'old_length' is a byte size while 'new_length' is a width in character cells */
1261 assert(percent
<= 100);
1263 old_length
= strlen(str
);
1266 r
= terminal_urlify(url
, str
, &clickable
);
1270 clickable_length
= strlen(clickable
);
1272 clickable_length
= old_length
;
1274 /* Determine current width on screen */
1276 while (p
< str
+ old_length
) {
1279 if (utf8_encoded_to_unichar(p
, &c
) < 0) {
1280 p
++, w
++; /* count invalid chars as 1 */
1284 p
= utf8_next_char(p
);
1285 w
+= unichar_iswide(c
) ? 2 : 1;
1288 /* Already wider than the target, if so, don't do anything */
1289 if (w
>= new_length
)
1290 return clickable
? TAKE_PTR(clickable
) : strdup(str
);
1292 /* How much spaces shall we add? An how much on the left side? */
1293 space
= new_length
- w
;
1294 lspace
= space
* percent
/ 100U;
1296 ret
= new(char, space
+ clickable_length
+ 1);
1300 for (i
= 0; i
< lspace
; i
++)
1302 memcpy(ret
+ lspace
, clickable
?: str
, clickable_length
);
1303 for (i
= lspace
+ clickable_length
; i
< space
+ clickable_length
; i
++)
1306 ret
[space
+ clickable_length
] = 0;
1310 int table_print(Table
*t
, FILE *f
) {
1311 size_t n_rows
, *minimum_width
, *maximum_width
, display_columns
, *requested_width
,
1312 i
, j
, table_minimum_width
, table_maximum_width
, table_requested_width
, table_effective_width
,
1314 _cleanup_free_
size_t *sorted
= NULL
;
1315 uint64_t *column_weight
, weight_sum
;
1323 /* Ensure we have no incomplete rows */
1324 assert(t
->n_cells
% t
->n_columns
== 0);
1326 n_rows
= t
->n_cells
/ t
->n_columns
;
1327 assert(n_rows
> 0); /* at least the header row must be complete */
1330 /* If sorting is requested, let's calculate an index table we use to lookup the actual index to display with. */
1332 sorted
= new(size_t, n_rows
);
1336 for (i
= 0; i
< n_rows
; i
++)
1337 sorted
[i
] = i
* t
->n_columns
;
1339 typesafe_qsort_r(sorted
, n_rows
, table_data_compare
, t
);
1343 display_columns
= t
->n_display_map
;
1345 display_columns
= t
->n_columns
;
1347 assert(display_columns
> 0);
1349 minimum_width
= newa(size_t, display_columns
);
1350 maximum_width
= newa(size_t, display_columns
);
1351 requested_width
= newa(size_t, display_columns
);
1352 width
= newa(size_t, display_columns
);
1353 column_weight
= newa0(uint64_t, display_columns
);
1355 for (j
= 0; j
< display_columns
; j
++) {
1356 minimum_width
[j
] = 1;
1357 maximum_width
[j
] = (size_t) -1;
1358 requested_width
[j
] = (size_t) -1;
1361 /* First pass: determine column sizes */
1362 for (i
= t
->header
? 0 : 1; i
< n_rows
; i
++) {
1365 /* Note that we don't care about ordering at this time, as we just want to determine column sizes,
1366 * hence we don't care for sorted[] during the first pass. */
1367 row
= t
->data
+ i
* t
->n_columns
;
1369 for (j
= 0; j
< display_columns
; j
++) {
1373 assert_se(d
= row
[t
->display_map
? t
->display_map
[j
] : j
]);
1375 r
= table_data_requested_width(d
, &req
);
1379 /* Determine the biggest width that any cell in this column would like to have */
1380 if (requested_width
[j
] == (size_t) -1 ||
1381 requested_width
[j
] < req
)
1382 requested_width
[j
] = req
;
1384 /* Determine the minimum width any cell in this column needs */
1385 if (minimum_width
[j
] < d
->minimum_width
)
1386 minimum_width
[j
] = d
->minimum_width
;
1388 /* Determine the maximum width any cell in this column needs */
1389 if (d
->maximum_width
!= (size_t) -1 &&
1390 (maximum_width
[j
] == (size_t) -1 ||
1391 maximum_width
[j
] > d
->maximum_width
))
1392 maximum_width
[j
] = d
->maximum_width
;
1394 /* Determine the full columns weight */
1395 column_weight
[j
] += d
->weight
;
1399 /* One space between each column */
1400 table_requested_width
= table_minimum_width
= table_maximum_width
= display_columns
- 1;
1402 /* Calculate the total weight for all columns, plus the minimum, maximum and requested width for the table. */
1404 for (j
= 0; j
< display_columns
; j
++) {
1405 weight_sum
+= column_weight
[j
];
1407 table_minimum_width
+= minimum_width
[j
];
1409 if (maximum_width
[j
] == (size_t) -1)
1410 table_maximum_width
= (size_t) -1;
1412 table_maximum_width
+= maximum_width
[j
];
1414 table_requested_width
+= requested_width
[j
];
1417 /* Calculate effective table width */
1418 if (t
->width
== (size_t) -1)
1419 table_effective_width
= pager_have() ? table_requested_width
: MIN(table_requested_width
, columns());
1421 table_effective_width
= t
->width
;
1423 if (table_maximum_width
!= (size_t) -1 && table_effective_width
> table_maximum_width
)
1424 table_effective_width
= table_maximum_width
;
1426 if (table_effective_width
< table_minimum_width
)
1427 table_effective_width
= table_minimum_width
;
1429 if (table_effective_width
>= table_requested_width
) {
1432 /* We have extra room, let's distribute it among columns according to their weights. We first provide
1433 * each column with what it asked for and the distribute the rest. */
1435 extra
= table_effective_width
- table_requested_width
;
1437 for (j
= 0; j
< display_columns
; j
++) {
1440 if (weight_sum
== 0)
1441 width
[j
] = requested_width
[j
] + extra
/ (display_columns
- j
); /* Avoid division by zero */
1443 width
[j
] = requested_width
[j
] + (extra
* column_weight
[j
]) / weight_sum
;
1445 if (maximum_width
[j
] != (size_t) -1 && width
[j
] > maximum_width
[j
])
1446 width
[j
] = maximum_width
[j
];
1448 if (width
[j
] < minimum_width
[j
])
1449 width
[j
] = minimum_width
[j
];
1451 assert(width
[j
] >= requested_width
[j
]);
1452 delta
= width
[j
] - requested_width
[j
];
1454 /* Subtract what we just added from the rest */
1460 assert(weight_sum
>= column_weight
[j
]);
1461 weight_sum
-= column_weight
[j
];
1465 /* We need to compress the table, columns can't get what they asked for. We first provide each column
1466 * with the minimum they need, and then distribute anything left. */
1467 bool finalize
= false;
1470 extra
= table_effective_width
- table_minimum_width
;
1472 for (j
= 0; j
< display_columns
; j
++)
1473 width
[j
] = (size_t) -1;
1476 bool restart
= false;
1478 for (j
= 0; j
< display_columns
; j
++) {
1481 /* Did this column already get something assigned? If so, let's skip to the next */
1482 if (width
[j
] != (size_t) -1)
1485 if (weight_sum
== 0)
1486 w
= minimum_width
[j
] + extra
/ (display_columns
- j
); /* avoid division by zero */
1488 w
= minimum_width
[j
] + (extra
* column_weight
[j
]) / weight_sum
;
1490 if (w
>= requested_width
[j
]) {
1491 /* Never give more than requested. If we hit a column like this, there's more
1492 * space to allocate to other columns which means we need to restart the
1493 * iteration. However, if we hit a column like this, let's assign it the space
1494 * it wanted for good early.*/
1496 w
= requested_width
[j
];
1499 } else if (!finalize
)
1504 assert(w
>= minimum_width
[j
]);
1505 delta
= w
- minimum_width
[j
];
1507 assert(delta
<= extra
);
1510 assert(weight_sum
>= column_weight
[j
]);
1511 weight_sum
-= column_weight
[j
];
1513 if (restart
&& !finalize
)
1525 /* Second pass: show output */
1526 for (i
= t
->header
? 0 : 1; i
< n_rows
; i
++) {
1530 row
= t
->data
+ sorted
[i
];
1532 row
= t
->data
+ i
* t
->n_columns
;
1534 for (j
= 0; j
< display_columns
; j
++) {
1535 _cleanup_free_
char *buffer
= NULL
;
1540 assert_se(d
= row
[t
->display_map
? t
->display_map
[j
] : j
]);
1542 field
= table_data_format(d
);
1546 l
= utf8_console_width(field
);
1548 /* Field is wider than allocated space. Let's ellipsize */
1550 buffer
= ellipsize(field
, width
[j
], d
->ellipsize_percent
);
1556 } else if (l
< width
[j
]) {
1557 /* Field is shorter than allocated space. Let's align with spaces */
1559 buffer
= align_string_mem(field
, d
->url
, width
[j
], d
->align_percent
);
1566 if (l
>= width
[j
] && d
->url
) {
1567 _cleanup_free_
char *clickable
= NULL
;
1569 r
= terminal_urlify(d
->url
, field
, &clickable
);
1573 free_and_replace(buffer
, clickable
);
1577 if (row
== t
->data
) /* underline header line fully, including the column separator */
1578 fputs(ansi_underline(), f
);
1581 fputc(' ', f
); /* column separator */
1583 if (d
->color
&& colors_enabled()) {
1584 if (row
== t
->data
) /* first undo header underliner */
1585 fputs(ANSI_NORMAL
, f
);
1592 if (colors_enabled() && (d
->color
|| row
== t
->data
))
1593 fputs(ANSI_NORMAL
, f
);
1599 return fflush_and_check(f
);
1602 int table_format(Table
*t
, char **ret
) {
1603 _cleanup_fclose_
FILE *f
= NULL
;
1608 f
= open_memstream_unlocked(&buf
, &sz
);
1612 r
= table_print(t
, f
);
1623 size_t table_get_rows(Table
*t
) {
1627 assert(t
->n_columns
> 0);
1628 return t
->n_cells
/ t
->n_columns
;
1631 size_t table_get_columns(Table
*t
) {
1635 assert(t
->n_columns
> 0);
1636 return t
->n_columns
;
1639 int table_set_reverse(Table
*t
, size_t column
, bool b
) {
1641 assert(column
< t
->n_columns
);
1643 if (!t
->reverse_map
) {
1647 t
->reverse_map
= new0(bool, t
->n_columns
);
1648 if (!t
->reverse_map
)
1652 t
->reverse_map
[column
] = b
;
1656 TableCell
*table_get_cell(Table
*t
, size_t row
, size_t column
) {
1661 if (column
>= t
->n_columns
)
1664 i
= row
* t
->n_columns
+ column
;
1665 if (i
>= t
->n_cells
)
1668 return TABLE_INDEX_TO_CELL(i
);
1671 const void *table_get(Table
*t
, TableCell
*cell
) {
1676 d
= table_get_data(t
, cell
);
1683 const void* table_get_at(Table
*t
, size_t row
, size_t column
) {
1686 cell
= table_get_cell(t
, row
, column
);
1690 return table_get(t
, cell
);
1693 static int table_data_to_json(TableData
*d
, JsonVariant
**ret
) {
1698 return json_variant_new_null(ret
);
1701 return json_variant_new_string(ret
, d
->string
);
1704 return json_variant_new_boolean(ret
, d
->boolean
);
1706 case TABLE_TIMESTAMP
:
1707 case TABLE_TIMESTAMP_UTC
:
1708 case TABLE_TIMESTAMP_RELATIVE
:
1709 if (d
->timestamp
== USEC_INFINITY
)
1710 return json_variant_new_null(ret
);
1712 return json_variant_new_unsigned(ret
, d
->timestamp
);
1714 case TABLE_TIMESPAN
:
1715 case TABLE_TIMESPAN_MSEC
:
1716 if (d
->timespan
== USEC_INFINITY
)
1717 return json_variant_new_null(ret
);
1719 return json_variant_new_unsigned(ret
, d
->timespan
);
1723 if (d
->size
== (size_t) -1)
1724 return json_variant_new_null(ret
);
1726 return json_variant_new_unsigned(ret
, d
->size
);
1729 return json_variant_new_integer(ret
, d
->int_val
);
1732 return json_variant_new_integer(ret
, d
->int32
);
1735 return json_variant_new_integer(ret
, d
->int64
);
1738 return json_variant_new_unsigned(ret
, d
->uint_val
);
1741 return json_variant_new_unsigned(ret
, d
->uint32
);
1744 return json_variant_new_unsigned(ret
, d
->uint64
);
1747 return json_variant_new_integer(ret
, d
->percent
);
1750 return json_variant_new_integer(ret
, d
->ifindex
);
1757 int table_to_json(Table
*t
, JsonVariant
**ret
) {
1758 JsonVariant
**rows
= NULL
, **elements
= NULL
;
1759 _cleanup_free_
size_t *sorted
= NULL
;
1760 size_t n_rows
, i
, j
, display_columns
;
1765 /* Ensure we have no incomplete rows */
1766 assert(t
->n_cells
% t
->n_columns
== 0);
1768 n_rows
= t
->n_cells
/ t
->n_columns
;
1769 assert(n_rows
> 0); /* at least the header row must be complete */
1772 /* If sorting is requested, let's calculate an index table we use to lookup the actual index to display with. */
1774 sorted
= new(size_t, n_rows
);
1780 for (i
= 0; i
< n_rows
; i
++)
1781 sorted
[i
] = i
* t
->n_columns
;
1783 typesafe_qsort_r(sorted
, n_rows
, table_data_compare
, t
);
1787 display_columns
= t
->n_display_map
;
1789 display_columns
= t
->n_columns
;
1790 assert(display_columns
> 0);
1792 elements
= new0(JsonVariant
*, display_columns
* 2);
1798 for (j
= 0; j
< display_columns
; j
++) {
1801 assert_se(d
= t
->data
[t
->display_map
? t
->display_map
[j
] : j
]);
1803 r
= table_data_to_json(d
, elements
+ j
*2);
1808 rows
= new0(JsonVariant
*, n_rows
-1);
1814 for (i
= 1; i
< n_rows
; i
++) {
1818 row
= t
->data
+ sorted
[i
];
1820 row
= t
->data
+ i
* t
->n_columns
;
1822 for (j
= 0; j
< display_columns
; j
++) {
1826 assert_se(d
= row
[t
->display_map
? t
->display_map
[j
] : j
]);
1829 elements
[k
] = json_variant_unref(elements
[k
]);
1831 r
= table_data_to_json(d
, elements
+ k
);
1836 r
= json_variant_new_object(rows
+ i
- 1, elements
, display_columns
* 2);
1841 r
= json_variant_new_array(ret
, rows
, n_rows
- 1);
1845 json_variant_unref_many(rows
, n_rows
-1);
1850 json_variant_unref_many(elements
, display_columns
*2);
1857 int table_print_json(Table
*t
, FILE *f
, JsonFormatFlags flags
) {
1858 _cleanup_(json_variant_unrefp
) JsonVariant
*v
= NULL
;
1866 r
= table_to_json(t
, &v
);
1870 json_variant_dump(v
, flags
, f
, NULL
);
1872 return fflush_and_check(f
);