]> git.ipfire.org Git - thirdparty/systemd.git/blame - src/shared/format-table.c
output-mode: add new helper OUTPUT_MODE_IS_JSON()
[thirdparty/systemd.git] / src / shared / format-table.c
CommitLineData
1960e736
LP
1/* SPDX-License-Identifier: LGPL-2.1+ */
2
3#include <stdio_ext.h>
4
5#include "alloc-util.h"
6#include "fd-util.h"
7#include "fileio.h"
8#include "format-table.h"
9#include "gunicode.h"
10#include "pager.h"
11#include "parse-util.h"
165ca566 12#include "pretty-print.h"
1960e736
LP
13#include "string-util.h"
14#include "terminal-util.h"
15#include "time-util.h"
16#include "utf8.h"
17#include "util.h"
18
19#define DEFAULT_WEIGHT 100
20
21/*
22 A few notes on implementation details:
23
24 - TableCell is a 'fake' structure, it's just used as data type to pass references to specific cell positions in the
25 table. It can be easily converted to an index number and back.
26
27 - TableData is where the actual data is stored: it encapsulates the data and formatting for a specific cell. It's
28 'pseudo-immutable' and ref-counted. When a cell's data's formatting is to be changed, we duplicate the object if the
29 ref-counting is larger than 1. Note that TableData and its ref-counting is mostly not visible to the outside. The
30 outside only sees Table and TableCell.
31
32 - The Table object stores a simple one-dimensional array of references to TableData objects, one row after the
33 previous one.
34
35 - There's no special concept of a "row" or "column" in the table, and no special concept of the "header" row. It's all
36 derived from the cell index: we know how many cells are to be stored in a row, and can determine the rest from
37 that. The first row is always the header row. If header display is turned off we simply skip outputting the first
38 row. Also, when sorting rows we always leave the first row where it is, as the header shouldn't move.
39
475d8599 40 - Note because there's no row and no column object some properties that might be appropriate as row/column properties
1960e736
LP
41 are exposed as cell properties instead. For example, the "weight" of a column (which is used to determine where to
42 add/remove space preferable when expanding/compressing tables horizontally) is actually made the "weight" of a
43 cell. Given that we usually need it per-column though we will calculate the average across every cell of the column
44 instead.
45
46 - To make things easy, when cells are added without any explicit configured formatting, then we'll copy the formatting
47 from the same cell in the previous cell. This is particularly useful for the "weight" of the cell (see above), as
48 this means setting the weight of the cells of the header row will nicely propagate to all cells in the other rows.
49*/
50
51typedef struct TableData {
52 unsigned n_ref;
53 TableDataType type;
54
55 size_t minimum_width; /* minimum width for the column */
56 size_t maximum_width; /* maximum width for the column */
57 unsigned weight; /* the horizontal weight for this column, in case the table is expanded/compressed */
58 unsigned ellipsize_percent; /* 0 … 100, where to place the ellipsis when compression is needed */
59 unsigned align_percent; /* 0 … 100, where to pad with spaces when expanding is needed. 0: left-aligned, 100: right-aligned */
60
61 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 */
165ca566 62 char *url; /* A URL to use for a clickable hyperlink */
1960e736
LP
63 char *formatted; /* A cached textual representation of the cell data, before ellipsation/alignment */
64
65 union {
66 uint8_t data[0]; /* data is generic array */
67 bool boolean;
68 usec_t timestamp;
69 usec_t timespan;
70 uint64_t size;
71 char string[0];
72 uint32_t uint32;
a4661181
LP
73 uint64_t uint64;
74 int percent; /* we use 'int' as datatype for percent values in order to match the result of parse_percent() */
1960e736
LP
75 /* … add more here as we start supporting more cell data types … */
76 };
77} TableData;
78
79static size_t TABLE_CELL_TO_INDEX(TableCell *cell) {
ee7b9f1d 80 size_t i;
1960e736
LP
81
82 assert(cell);
83
ee7b9f1d 84 i = PTR_TO_SIZE(cell);
1960e736
LP
85 assert(i > 0);
86
87 return i-1;
88}
89
90static TableCell* TABLE_INDEX_TO_CELL(size_t index) {
91 assert(index != (size_t) -1);
ee7b9f1d 92 return SIZE_TO_PTR(index + 1);
1960e736
LP
93}
94
95struct Table {
96 size_t n_columns;
97 size_t n_cells;
98
99 bool header; /* Whether to show the header row? */
100 size_t width; /* If != (size_t) -1 the width to format this table in */
101
102 TableData **data;
103 size_t n_allocated;
104
105 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 */
106 size_t n_display_map;
107
108 size_t *sort_map; /* The columns to order rows by, in order of preference. */
109 size_t n_sort_map;
a2c73e2d
LP
110
111 bool *reverse_map;
1960e736
LP
112};
113
114Table *table_new_raw(size_t n_columns) {
115 _cleanup_(table_unrefp) Table *t = NULL;
116
117 assert(n_columns > 0);
118
119 t = new(Table, 1);
120 if (!t)
121 return NULL;
122
123 *t = (struct Table) {
124 .n_columns = n_columns,
125 .header = true,
126 .width = (size_t) -1,
127 };
128
129 return TAKE_PTR(t);
130}
131
132Table *table_new_internal(const char *first_header, ...) {
133 _cleanup_(table_unrefp) Table *t = NULL;
134 size_t n_columns = 1;
135 va_list ap;
136 int r;
137
138 assert(first_header);
139
140 va_start(ap, first_header);
141 for (;;) {
142 const char *h;
143
144 h = va_arg(ap, const char*);
145 if (!h)
146 break;
147
148 n_columns++;
149 }
150 va_end(ap);
151
152 t = table_new_raw(n_columns);
153 if (!t)
154 return NULL;
155
156 r = table_add_cell(t, NULL, TABLE_STRING, first_header);
157 if (r < 0)
158 return NULL;
159
160 va_start(ap, first_header);
161 for (;;) {
162 const char *h;
163
164 h = va_arg(ap, const char*);
165 if (!h)
166 break;
167
168 r = table_add_cell(t, NULL, TABLE_STRING, h);
169 if (r < 0) {
170 va_end(ap);
171 return NULL;
172 }
173 }
174 va_end(ap);
175
176 assert(t->n_columns == t->n_cells);
177 return TAKE_PTR(t);
178}
179
8301aa0b
YW
180static TableData *table_data_free(TableData *d) {
181 assert(d);
1960e736
LP
182
183 free(d->formatted);
165ca566
LP
184 free(d->url);
185
1960e736
LP
186 return mfree(d);
187}
188
8301aa0b 189DEFINE_PRIVATE_TRIVIAL_REF_UNREF_FUNC(TableData, table_data, table_data_free);
1960e736
LP
190DEFINE_TRIVIAL_CLEANUP_FUNC(TableData*, table_data_unref);
191
1960e736
LP
192Table *table_unref(Table *t) {
193 size_t i;
194
195 if (!t)
196 return NULL;
197
198 for (i = 0; i < t->n_cells; i++)
199 table_data_unref(t->data[i]);
200
201 free(t->data);
202 free(t->display_map);
203 free(t->sort_map);
a2c73e2d 204 free(t->reverse_map);
1960e736
LP
205
206 return mfree(t);
207}
208
209static size_t table_data_size(TableDataType type, const void *data) {
210
211 switch (type) {
212
213 case TABLE_EMPTY:
214 return 0;
215
216 case TABLE_STRING:
217 return strlen(data) + 1;
218
219 case TABLE_BOOLEAN:
220 return sizeof(bool);
221
222 case TABLE_TIMESTAMP:
223 case TABLE_TIMESPAN:
224 return sizeof(usec_t);
225
226 case TABLE_SIZE:
a4661181 227 case TABLE_UINT64:
1960e736
LP
228 return sizeof(uint64_t);
229
230 case TABLE_UINT32:
231 return sizeof(uint32_t);
232
a4661181
LP
233 case TABLE_PERCENT:
234 return sizeof(int);
235
1960e736
LP
236 default:
237 assert_not_reached("Uh? Unexpected cell type");
238 }
239}
240
241static bool table_data_matches(
242 TableData *d,
243 TableDataType type,
244 const void *data,
245 size_t minimum_width,
246 size_t maximum_width,
247 unsigned weight,
248 unsigned align_percent,
249 unsigned ellipsize_percent) {
250
251 size_t k, l;
252 assert(d);
253
254 if (d->type != type)
255 return false;
256
257 if (d->minimum_width != minimum_width)
258 return false;
259
260 if (d->maximum_width != maximum_width)
261 return false;
262
263 if (d->weight != weight)
264 return false;
265
266 if (d->align_percent != align_percent)
267 return false;
268
269 if (d->ellipsize_percent != ellipsize_percent)
270 return false;
271
272 k = table_data_size(type, data);
273 l = table_data_size(d->type, d->data);
274
275 if (k != l)
276 return false;
277
88db94fa 278 return memcmp_safe(data, d->data, l) == 0;
1960e736
LP
279}
280
281static TableData *table_data_new(
282 TableDataType type,
283 const void *data,
284 size_t minimum_width,
285 size_t maximum_width,
286 unsigned weight,
287 unsigned align_percent,
288 unsigned ellipsize_percent) {
289
290 size_t data_size;
291 TableData *d;
292
293 data_size = table_data_size(type, data);
294
295 d = malloc0(offsetof(TableData, data) + data_size);
296 if (!d)
297 return NULL;
298
299 d->n_ref = 1;
300 d->type = type;
301 d->minimum_width = minimum_width;
302 d->maximum_width = maximum_width;
303 d->weight = weight;
304 d->align_percent = align_percent;
305 d->ellipsize_percent = ellipsize_percent;
306 memcpy_safe(d->data, data, data_size);
307
308 return d;
309}
310
311int table_add_cell_full(
312 Table *t,
313 TableCell **ret_cell,
314 TableDataType type,
315 const void *data,
316 size_t minimum_width,
317 size_t maximum_width,
318 unsigned weight,
319 unsigned align_percent,
320 unsigned ellipsize_percent) {
321
322 _cleanup_(table_data_unrefp) TableData *d = NULL;
323 TableData *p;
324
325 assert(t);
326 assert(type >= 0);
327 assert(type < _TABLE_DATA_TYPE_MAX);
328
329 /* Determine the cell adjacent to the current one, but one row up */
330 if (t->n_cells >= t->n_columns)
331 assert_se(p = t->data[t->n_cells - t->n_columns]);
332 else
333 p = NULL;
334
335 /* If formatting parameters are left unspecified, copy from the previous row */
336 if (minimum_width == (size_t) -1)
337 minimum_width = p ? p->minimum_width : 1;
338
339 if (weight == (unsigned) -1)
340 weight = p ? p->weight : DEFAULT_WEIGHT;
341
342 if (align_percent == (unsigned) -1)
343 align_percent = p ? p->align_percent : 0;
344
345 if (ellipsize_percent == (unsigned) -1)
346 ellipsize_percent = p ? p->ellipsize_percent : 100;
347
348 assert(align_percent <= 100);
349 assert(ellipsize_percent <= 100);
350
351 /* Small optimization: Pretty often adjacent cells in two subsequent lines have the same data and
352 * formatting. Let's see if we can reuse the cell data and ref it once more. */
353
354 if (p && table_data_matches(p, type, data, minimum_width, maximum_width, weight, align_percent, ellipsize_percent))
355 d = table_data_ref(p);
356 else {
357 d = table_data_new(type, data, minimum_width, maximum_width, weight, align_percent, ellipsize_percent);
358 if (!d)
359 return -ENOMEM;
360 }
361
362 if (!GREEDY_REALLOC(t->data, t->n_allocated, MAX(t->n_cells + 1, t->n_columns)))
363 return -ENOMEM;
364
365 if (ret_cell)
366 *ret_cell = TABLE_INDEX_TO_CELL(t->n_cells);
367
368 t->data[t->n_cells++] = TAKE_PTR(d);
369
370 return 0;
371}
372
373int table_dup_cell(Table *t, TableCell *cell) {
374 size_t i;
375
376 assert(t);
377
378 /* Add the data of the specified cell a second time as a new cell to the end. */
379
380 i = TABLE_CELL_TO_INDEX(cell);
381 if (i >= t->n_cells)
382 return -ENXIO;
383
384 if (!GREEDY_REALLOC(t->data, t->n_allocated, MAX(t->n_cells + 1, t->n_columns)))
385 return -ENOMEM;
386
387 t->data[t->n_cells++] = table_data_ref(t->data[i]);
388 return 0;
389}
390
391static int table_dedup_cell(Table *t, TableCell *cell) {
165ca566 392 _cleanup_free_ char *curl = NULL;
1960e736
LP
393 TableData *nd, *od;
394 size_t i;
395
396 assert(t);
397
398 /* Helper call that ensures the specified cell's data object has a ref count of 1, which we can use before
399 * changing a cell's formatting without effecting every other cell's formatting that shares the same data */
400
401 i = TABLE_CELL_TO_INDEX(cell);
402 if (i >= t->n_cells)
403 return -ENXIO;
404
405 assert_se(od = t->data[i]);
406 if (od->n_ref == 1)
407 return 0;
408
409 assert(od->n_ref > 1);
410
165ca566
LP
411 if (od->url) {
412 curl = strdup(od->url);
413 if (!curl)
414 return -ENOMEM;
415 }
416
417 nd = table_data_new(
418 od->type,
419 od->data,
420 od->minimum_width,
421 od->maximum_width,
422 od->weight,
423 od->align_percent,
424 od->ellipsize_percent);
1960e736
LP
425 if (!nd)
426 return -ENOMEM;
427
13b0d4d7 428 nd->color = od->color;
165ca566 429 nd->url = TAKE_PTR(curl);
13b0d4d7 430
1960e736
LP
431 table_data_unref(od);
432 t->data[i] = nd;
433
434 assert(nd->n_ref == 1);
435
436 return 1;
437}
438
439static TableData *table_get_data(Table *t, TableCell *cell) {
440 size_t i;
441
442 assert(t);
443 assert(cell);
444
445 /* Get the data object of the specified cell, or NULL if it doesn't exist */
446
447 i = TABLE_CELL_TO_INDEX(cell);
448 if (i >= t->n_cells)
449 return NULL;
450
451 assert(t->data[i]);
452 assert(t->data[i]->n_ref > 0);
453
454 return t->data[i];
455}
456
457int table_set_minimum_width(Table *t, TableCell *cell, size_t minimum_width) {
458 int r;
459
460 assert(t);
461 assert(cell);
462
463 if (minimum_width == (size_t) -1)
464 minimum_width = 1;
465
466 r = table_dedup_cell(t, cell);
467 if (r < 0)
468 return r;
469
470 table_get_data(t, cell)->minimum_width = minimum_width;
471 return 0;
472}
473
474int table_set_maximum_width(Table *t, TableCell *cell, size_t maximum_width) {
475 int r;
476
477 assert(t);
478 assert(cell);
479
480 r = table_dedup_cell(t, cell);
481 if (r < 0)
482 return r;
483
484 table_get_data(t, cell)->maximum_width = maximum_width;
485 return 0;
486}
487
488int table_set_weight(Table *t, TableCell *cell, unsigned weight) {
489 int r;
490
491 assert(t);
492 assert(cell);
493
494 if (weight == (unsigned) -1)
495 weight = DEFAULT_WEIGHT;
496
497 r = table_dedup_cell(t, cell);
498 if (r < 0)
499 return r;
500
501 table_get_data(t, cell)->weight = weight;
502 return 0;
503}
504
505int table_set_align_percent(Table *t, TableCell *cell, unsigned percent) {
506 int r;
507
508 assert(t);
509 assert(cell);
510
511 if (percent == (unsigned) -1)
512 percent = 0;
513
514 assert(percent <= 100);
515
516 r = table_dedup_cell(t, cell);
517 if (r < 0)
518 return r;
519
520 table_get_data(t, cell)->align_percent = percent;
521 return 0;
522}
523
524int table_set_ellipsize_percent(Table *t, TableCell *cell, unsigned percent) {
525 int r;
526
527 assert(t);
528 assert(cell);
529
530 if (percent == (unsigned) -1)
531 percent = 100;
532
533 assert(percent <= 100);
534
535 r = table_dedup_cell(t, cell);
536 if (r < 0)
537 return r;
538
539 table_get_data(t, cell)->ellipsize_percent = percent;
540 return 0;
541}
542
543int table_set_color(Table *t, TableCell *cell, const char *color) {
544 int r;
545
546 assert(t);
547 assert(cell);
548
549 r = table_dedup_cell(t, cell);
550 if (r < 0)
551 return r;
552
553 table_get_data(t, cell)->color = empty_to_null(color);
554 return 0;
555}
556
165ca566
LP
557int table_set_url(Table *t, TableCell *cell, const char *url) {
558 _cleanup_free_ char *copy = NULL;
559 int r;
560
561 assert(t);
562 assert(cell);
563
564 if (url) {
565 copy = strdup(url);
566 if (!copy)
567 return -ENOMEM;
568 }
569
570 r = table_dedup_cell(t, cell);
571 if (r < 0)
572 return r;
573
574 return free_and_replace(table_get_data(t, cell)->url, copy);
575}
576
27e730e6
LP
577int table_update(Table *t, TableCell *cell, TableDataType type, const void *data) {
578 _cleanup_free_ char *curl = NULL;
579 TableData *nd, *od;
580 size_t i;
581
582 assert(t);
583 assert(cell);
584
585 i = TABLE_CELL_TO_INDEX(cell);
586 if (i >= t->n_cells)
587 return -ENXIO;
588
589 assert_se(od = t->data[i]);
590
591 if (od->url) {
592 curl = strdup(od->url);
593 if (!curl)
594 return -ENOMEM;
595 }
596
597 nd = table_data_new(
598 type,
599 data,
600 od->minimum_width,
601 od->maximum_width,
602 od->weight,
603 od->align_percent,
604 od->ellipsize_percent);
605 if (!nd)
606 return -ENOMEM;
607
608 nd->color = od->color;
609 nd->url = TAKE_PTR(curl);
610
611 table_data_unref(od);
612 t->data[i] = nd;
613
614 return 0;
615}
616
1960e736
LP
617int table_add_many_internal(Table *t, TableDataType first_type, ...) {
618 TableDataType type;
619 va_list ap;
620 int r;
621
622 assert(t);
623 assert(first_type >= 0);
624 assert(first_type < _TABLE_DATA_TYPE_MAX);
625
626 type = first_type;
627
628 va_start(ap, first_type);
629 for (;;) {
630 const void *data;
631 union {
632 uint64_t size;
633 usec_t usec;
634 uint32_t uint32;
a4661181
LP
635 uint64_t uint64;
636 int percent;
1960e736
LP
637 bool b;
638 } buffer;
639
640 switch (type) {
641
642 case TABLE_EMPTY:
643 data = NULL;
644 break;
645
646 case TABLE_STRING:
647 data = va_arg(ap, const char *);
648 break;
649
650 case TABLE_BOOLEAN:
5d904a6a 651 buffer.b = va_arg(ap, int);
1960e736
LP
652 data = &buffer.b;
653 break;
654
655 case TABLE_TIMESTAMP:
656 case TABLE_TIMESPAN:
657 buffer.usec = va_arg(ap, usec_t);
658 data = &buffer.usec;
659 break;
660
661 case TABLE_SIZE:
662 buffer.size = va_arg(ap, uint64_t);
663 data = &buffer.size;
664 break;
665
666 case TABLE_UINT32:
667 buffer.uint32 = va_arg(ap, uint32_t);
668 data = &buffer.uint32;
669 break;
670
a4661181
LP
671 case TABLE_UINT64:
672 buffer.uint64 = va_arg(ap, uint64_t);
673 data = &buffer.uint64;
674 break;
675
676 case TABLE_PERCENT:
677 buffer.percent = va_arg(ap, int);
678 data = &buffer.percent;
679 break;
680
1960e736
LP
681 case _TABLE_DATA_TYPE_MAX:
682 /* Used as end marker */
683 va_end(ap);
684 return 0;
685
686 default:
687 assert_not_reached("Uh? Unexpected data type.");
688 }
689
690 r = table_add_cell(t, NULL, type, data);
691 if (r < 0) {
692 va_end(ap);
693 return r;
694 }
695
696 type = va_arg(ap, TableDataType);
697 }
698}
699
700void table_set_header(Table *t, bool b) {
701 assert(t);
702
703 t->header = b;
704}
705
706void table_set_width(Table *t, size_t width) {
707 assert(t);
708
709 t->width = width;
710}
711
712int table_set_display(Table *t, size_t first_column, ...) {
713 size_t allocated, column;
714 va_list ap;
715
716 assert(t);
717
718 allocated = t->n_display_map;
719 column = first_column;
720
721 va_start(ap, first_column);
722 for (;;) {
723 assert(column < t->n_columns);
724
725 if (!GREEDY_REALLOC(t->display_map, allocated, MAX(t->n_columns, t->n_display_map+1))) {
726 va_end(ap);
727 return -ENOMEM;
728 }
729
730 t->display_map[t->n_display_map++] = column;
731
732 column = va_arg(ap, size_t);
733 if (column == (size_t) -1)
734 break;
735
736 }
f20f4a77 737 va_end(ap);
1960e736
LP
738
739 return 0;
740}
741
742int table_set_sort(Table *t, size_t first_column, ...) {
743 size_t allocated, column;
744 va_list ap;
745
746 assert(t);
747
748 allocated = t->n_sort_map;
749 column = first_column;
750
751 va_start(ap, first_column);
752 for (;;) {
753 assert(column < t->n_columns);
754
755 if (!GREEDY_REALLOC(t->sort_map, allocated, MAX(t->n_columns, t->n_sort_map+1))) {
756 va_end(ap);
757 return -ENOMEM;
758 }
759
760 t->sort_map[t->n_sort_map++] = column;
761
762 column = va_arg(ap, size_t);
763 if (column == (size_t) -1)
764 break;
765 }
f20f4a77 766 va_end(ap);
1960e736
LP
767
768 return 0;
769}
770
771static int cell_data_compare(TableData *a, size_t index_a, TableData *b, size_t index_b) {
772 assert(a);
773 assert(b);
774
775 if (a->type == b->type) {
776
777 /* We only define ordering for cells of the same data type. If cells with different data types are
778 * compared we follow the order the cells were originally added in */
779
780 switch (a->type) {
781
782 case TABLE_STRING:
783 return strcmp(a->string, b->string);
784
785 case TABLE_BOOLEAN:
786 if (!a->boolean && b->boolean)
787 return -1;
788 if (a->boolean && !b->boolean)
789 return 1;
790 return 0;
791
792 case TABLE_TIMESTAMP:
6dd91b36 793 return CMP(a->timestamp, b->timestamp);
1960e736
LP
794
795 case TABLE_TIMESPAN:
6dd91b36 796 return CMP(a->timespan, b->timespan);
1960e736
LP
797
798 case TABLE_SIZE:
6dd91b36 799 return CMP(a->size, b->size);
1960e736
LP
800
801 case TABLE_UINT32:
6dd91b36 802 return CMP(a->uint32, b->uint32);
1960e736 803
a4661181
LP
804 case TABLE_UINT64:
805 return CMP(a->uint64, b->uint64);
806
807 case TABLE_PERCENT:
808 return CMP(a->percent, b->percent);
809
1960e736
LP
810 default:
811 ;
812 }
813 }
814
815 /* Generic fallback using the orginal order in which the cells where added. */
6dd91b36 816 return CMP(index_a, index_b);
1960e736
LP
817}
818
ba0a7bfb 819static int table_data_compare(const size_t *a, const size_t *b, Table *t) {
1960e736
LP
820 size_t i;
821 int r;
822
823 assert(t);
824 assert(t->sort_map);
825
826 /* Make sure the header stays at the beginning */
827 if (*a < t->n_columns && *b < t->n_columns)
828 return 0;
829 if (*a < t->n_columns)
830 return -1;
831 if (*b < t->n_columns)
832 return 1;
833
834 /* Order other lines by the sorting map */
835 for (i = 0; i < t->n_sort_map; i++) {
836 TableData *d, *dd;
837
838 d = t->data[*a + t->sort_map[i]];
839 dd = t->data[*b + t->sort_map[i]];
840
841 r = cell_data_compare(d, *a, dd, *b);
842 if (r != 0)
a2c73e2d 843 return t->reverse_map && t->reverse_map[t->sort_map[i]] ? -r : r;
1960e736
LP
844 }
845
846 /* Order identical lines by the order there were originally added in */
ba0a7bfb 847 return CMP(*a, *b);
1960e736
LP
848}
849
850static const char *table_data_format(TableData *d) {
851 assert(d);
852
853 if (d->formatted)
854 return d->formatted;
855
856 switch (d->type) {
857 case TABLE_EMPTY:
858 return "";
859
860 case TABLE_STRING:
861 return d->string;
862
863 case TABLE_BOOLEAN:
864 return yes_no(d->boolean);
865
866 case TABLE_TIMESTAMP: {
867 _cleanup_free_ char *p;
868
869 p = new(char, FORMAT_TIMESTAMP_MAX);
870 if (!p)
871 return NULL;
872
873 if (!format_timestamp(p, FORMAT_TIMESTAMP_MAX, d->timestamp))
874 return "n/a";
875
876 d->formatted = TAKE_PTR(p);
877 break;
878 }
879
880 case TABLE_TIMESPAN: {
881 _cleanup_free_ char *p;
882
883 p = new(char, FORMAT_TIMESPAN_MAX);
884 if (!p)
885 return NULL;
886
887 if (!format_timespan(p, FORMAT_TIMESPAN_MAX, d->timestamp, 0))
888 return "n/a";
889
890 d->formatted = TAKE_PTR(p);
891 break;
892 }
893
894 case TABLE_SIZE: {
895 _cleanup_free_ char *p;
896
897 p = new(char, FORMAT_BYTES_MAX);
898 if (!p)
899 return NULL;
900
901 if (!format_bytes(p, FORMAT_BYTES_MAX, d->size))
902 return "n/a";
903
904 d->formatted = TAKE_PTR(p);
905 break;
906 }
907
908 case TABLE_UINT32: {
909 _cleanup_free_ char *p;
910
911 p = new(char, DECIMAL_STR_WIDTH(d->uint32) + 1);
912 if (!p)
913 return NULL;
914
915 sprintf(p, "%" PRIu32, d->uint32);
916 d->formatted = TAKE_PTR(p);
917 break;
918 }
919
a4661181
LP
920 case TABLE_UINT64: {
921 _cleanup_free_ char *p;
922
923 p = new(char, DECIMAL_STR_WIDTH(d->uint64) + 1);
924 if (!p)
925 return NULL;
926
927 sprintf(p, "%" PRIu64, d->uint64);
928 d->formatted = TAKE_PTR(p);
929 break;
930 }
931
932 case TABLE_PERCENT: {
933 _cleanup_free_ char *p;
934
935 p = new(char, DECIMAL_STR_WIDTH(d->percent) + 2);
936 if (!p)
937 return NULL;
938
939 sprintf(p, "%i%%" , d->percent);
940 d->formatted = TAKE_PTR(p);
941 break;
942 }
943
1960e736
LP
944 default:
945 assert_not_reached("Unexpected type?");
946 }
947
1960e736
LP
948 return d->formatted;
949}
950
951static int table_data_requested_width(TableData *d, size_t *ret) {
952 const char *t;
953 size_t l;
954
955 t = table_data_format(d);
956 if (!t)
957 return -ENOMEM;
958
959 l = utf8_console_width(t);
960 if (l == (size_t) -1)
961 return -EINVAL;
962
963 if (d->maximum_width != (size_t) -1 && l > d->maximum_width)
964 l = d->maximum_width;
965
966 if (l < d->minimum_width)
967 l = d->minimum_width;
968
969 *ret = l;
970 return 0;
971}
972
165ca566
LP
973static char *align_string_mem(const char *str, const char *url, size_t new_length, unsigned percent) {
974 size_t w = 0, space, lspace, old_length, clickable_length;
975 _cleanup_free_ char *clickable = NULL;
1960e736
LP
976 const char *p;
977 char *ret;
978 size_t i;
165ca566 979 int r;
1960e736
LP
980
981 /* As with ellipsize_mem(), 'old_length' is a byte size while 'new_length' is a width in character cells */
982
983 assert(str);
984 assert(percent <= 100);
985
cfc01c1e 986 old_length = strlen(str);
1960e736 987
165ca566
LP
988 if (url) {
989 r = terminal_urlify(url, str, &clickable);
990 if (r < 0)
991 return NULL;
992
993 clickable_length = strlen(clickable);
994 } else
995 clickable_length = old_length;
996
1960e736
LP
997 /* Determine current width on screen */
998 p = str;
999 while (p < str + old_length) {
1000 char32_t c;
1001
1002 if (utf8_encoded_to_unichar(p, &c) < 0) {
1003 p++, w++; /* count invalid chars as 1 */
1004 continue;
1005 }
1006
1007 p = utf8_next_char(p);
1008 w += unichar_iswide(c) ? 2 : 1;
1009 }
1010
1011 /* Already wider than the target, if so, don't do anything */
1012 if (w >= new_length)
165ca566 1013 return clickable ? TAKE_PTR(clickable) : strdup(str);
1960e736
LP
1014
1015 /* How much spaces shall we add? An how much on the left side? */
1016 space = new_length - w;
1017 lspace = space * percent / 100U;
1018
165ca566 1019 ret = new(char, space + clickable_length + 1);
1960e736
LP
1020 if (!ret)
1021 return NULL;
1022
1023 for (i = 0; i < lspace; i++)
1024 ret[i] = ' ';
165ca566
LP
1025 memcpy(ret + lspace, clickable ?: str, clickable_length);
1026 for (i = lspace + clickable_length; i < space + clickable_length; i++)
1960e736
LP
1027 ret[i] = ' ';
1028
165ca566 1029 ret[space + clickable_length] = 0;
1960e736
LP
1030 return ret;
1031}
1032
1033int table_print(Table *t, FILE *f) {
1034 size_t n_rows, *minimum_width, *maximum_width, display_columns, *requested_width,
1035 i, j, table_minimum_width, table_maximum_width, table_requested_width, table_effective_width,
1036 *width;
1037 _cleanup_free_ size_t *sorted = NULL;
1038 uint64_t *column_weight, weight_sum;
1039 int r;
1040
1041 assert(t);
1042
1043 if (!f)
1044 f = stdout;
1045
1046 /* Ensure we have no incomplete rows */
1047 assert(t->n_cells % t->n_columns == 0);
1048
1049 n_rows = t->n_cells / t->n_columns;
1050 assert(n_rows > 0); /* at least the header row must be complete */
1051
1052 if (t->sort_map) {
1053 /* If sorting is requested, let's calculate an index table we use to lookup the actual index to display with. */
1054
1055 sorted = new(size_t, n_rows);
1056 if (!sorted)
1057 return -ENOMEM;
1058
1059 for (i = 0; i < n_rows; i++)
1060 sorted[i] = i * t->n_columns;
1061
ba0a7bfb 1062 typesafe_qsort_r(sorted, n_rows, table_data_compare, t);
1960e736
LP
1063 }
1064
1065 if (t->display_map)
1066 display_columns = t->n_display_map;
1067 else
1068 display_columns = t->n_columns;
1069
1070 assert(display_columns > 0);
1071
1072 minimum_width = newa(size_t, display_columns);
1073 maximum_width = newa(size_t, display_columns);
1074 requested_width = newa(size_t, display_columns);
1075 width = newa(size_t, display_columns);
1076 column_weight = newa0(uint64_t, display_columns);
1077
1078 for (j = 0; j < display_columns; j++) {
1079 minimum_width[j] = 1;
1080 maximum_width[j] = (size_t) -1;
1081 requested_width[j] = (size_t) -1;
1082 }
1083
1084 /* First pass: determine column sizes */
1085 for (i = t->header ? 0 : 1; i < n_rows; i++) {
1086 TableData **row;
1087
1088 /* Note that we don't care about ordering at this time, as we just want to determine column sizes,
1089 * hence we don't care for sorted[] during the first pass. */
1090 row = t->data + i * t->n_columns;
1091
1092 for (j = 0; j < display_columns; j++) {
1093 TableData *d;
1094 size_t req;
1095
1096 assert_se(d = row[t->display_map ? t->display_map[j] : j]);
1097
1098 r = table_data_requested_width(d, &req);
1099 if (r < 0)
1100 return r;
1101
1102 /* Determine the biggest width that any cell in this column would like to have */
1103 if (requested_width[j] == (size_t) -1 ||
1104 requested_width[j] < req)
1105 requested_width[j] = req;
1106
1107 /* Determine the minimum width any cell in this column needs */
1108 if (minimum_width[j] < d->minimum_width)
1109 minimum_width[j] = d->minimum_width;
1110
1111 /* Determine the maximum width any cell in this column needs */
1112 if (d->maximum_width != (size_t) -1 &&
1113 (maximum_width[j] == (size_t) -1 ||
1114 maximum_width[j] > d->maximum_width))
1115 maximum_width[j] = d->maximum_width;
1116
1117 /* Determine the full columns weight */
1118 column_weight[j] += d->weight;
1119 }
1120 }
1121
1122 /* One space between each column */
1123 table_requested_width = table_minimum_width = table_maximum_width = display_columns - 1;
1124
1125 /* Calculate the total weight for all columns, plus the minimum, maximum and requested width for the table. */
1126 weight_sum = 0;
1127 for (j = 0; j < display_columns; j++) {
1128 weight_sum += column_weight[j];
1129
1130 table_minimum_width += minimum_width[j];
1131
1132 if (maximum_width[j] == (size_t) -1)
1133 table_maximum_width = (size_t) -1;
1134 else
1135 table_maximum_width += maximum_width[j];
1136
1137 table_requested_width += requested_width[j];
1138 }
1139
1140 /* Calculate effective table width */
1141 if (t->width == (size_t) -1)
1142 table_effective_width = pager_have() ? table_requested_width : MIN(table_requested_width, columns());
1143 else
1144 table_effective_width = t->width;
1145
1146 if (table_maximum_width != (size_t) -1 && table_effective_width > table_maximum_width)
1147 table_effective_width = table_maximum_width;
1148
1149 if (table_effective_width < table_minimum_width)
1150 table_effective_width = table_minimum_width;
1151
1152 if (table_effective_width >= table_requested_width) {
1153 size_t extra;
1154
1155 /* We have extra room, let's distribute it among columns according to their weights. We first provide
1156 * each column with what it asked for and the distribute the rest. */
1157
1158 extra = table_effective_width - table_requested_width;
1159
1160 for (j = 0; j < display_columns; j++) {
1161 size_t delta;
1162
1163 if (weight_sum == 0)
1164 width[j] = requested_width[j] + extra / (display_columns - j); /* Avoid division by zero */
1165 else
1166 width[j] = requested_width[j] + (extra * column_weight[j]) / weight_sum;
1167
1168 if (maximum_width[j] != (size_t) -1 && width[j] > maximum_width[j])
1169 width[j] = maximum_width[j];
1170
1171 if (width[j] < minimum_width[j])
1172 width[j] = minimum_width[j];
1173
1174 assert(width[j] >= requested_width[j]);
1175 delta = width[j] - requested_width[j];
1176
1177 /* Subtract what we just added from the rest */
1178 if (extra > delta)
1179 extra -= delta;
1180 else
1181 extra = 0;
1182
1183 assert(weight_sum >= column_weight[j]);
1184 weight_sum -= column_weight[j];
1185 }
1186
1187 } else {
1188 /* We need to compress the table, columns can't get what they asked for. We first provide each column
1189 * with the minimum they need, and then distribute anything left. */
1190 bool finalize = false;
1191 size_t extra;
1192
1193 extra = table_effective_width - table_minimum_width;
1194
1195 for (j = 0; j < display_columns; j++)
1196 width[j] = (size_t) -1;
1197
1198 for (;;) {
1199 bool restart = false;
1200
1201 for (j = 0; j < display_columns; j++) {
1202 size_t delta, w;
1203
1204 /* Did this column already get something assigned? If so, let's skip to the next */
1205 if (width[j] != (size_t) -1)
1206 continue;
1207
1208 if (weight_sum == 0)
1209 w = minimum_width[j] + extra / (display_columns - j); /* avoid division by zero */
1210 else
1211 w = minimum_width[j] + (extra * column_weight[j]) / weight_sum;
1212
1213 if (w >= requested_width[j]) {
1214 /* Never give more than requested. If we hit a column like this, there's more
1215 * space to allocate to other columns which means we need to restart the
1216 * iteration. However, if we hit a column like this, let's assign it the space
1217 * it wanted for good early.*/
1218
1219 w = requested_width[j];
1220 restart = true;
1221
1222 } else if (!finalize)
1223 continue;
1224
1225 width[j] = w;
1226
1227 assert(w >= minimum_width[j]);
1228 delta = w - minimum_width[j];
1229
1230 assert(delta <= extra);
1231 extra -= delta;
1232
1233 assert(weight_sum >= column_weight[j]);
1234 weight_sum -= column_weight[j];
1235
a26db0bc 1236 if (restart && !finalize)
1960e736
LP
1237 break;
1238 }
1239
a26db0bc 1240 if (finalize)
1960e736 1241 break;
1960e736
LP
1242
1243 if (!restart)
1244 finalize = true;
1245 }
1246 }
1247
1248 /* Second pass: show output */
1249 for (i = t->header ? 0 : 1; i < n_rows; i++) {
1250 TableData **row;
1251
1252 if (sorted)
1253 row = t->data + sorted[i];
1254 else
1255 row = t->data + i * t->n_columns;
1256
1257 for (j = 0; j < display_columns; j++) {
1258 _cleanup_free_ char *buffer = NULL;
1259 const char *field;
1260 TableData *d;
1261 size_t l;
1262
1263 assert_se(d = row[t->display_map ? t->display_map[j] : j]);
1264
1265 field = table_data_format(d);
1266 if (!field)
1267 return -ENOMEM;
1268
1269 l = utf8_console_width(field);
1270 if (l > width[j]) {
1271 /* Field is wider than allocated space. Let's ellipsize */
1272
21e4e3e0 1273 buffer = ellipsize(field, width[j], d->ellipsize_percent);
1960e736
LP
1274 if (!buffer)
1275 return -ENOMEM;
1276
1277 field = buffer;
1278
1279 } else if (l < width[j]) {
1280 /* Field is shorter than allocated space. Let's align with spaces */
1281
165ca566 1282 buffer = align_string_mem(field, d->url, width[j], d->align_percent);
1960e736
LP
1283 if (!buffer)
1284 return -ENOMEM;
1285
1286 field = buffer;
1287 }
1288
165ca566
LP
1289 if (l >= width[j] && d->url) {
1290 _cleanup_free_ char *clickable = NULL;
1291
1292 r = terminal_urlify(d->url, field, &clickable);
1293 if (r < 0)
1294 return r;
1295
1296 free_and_replace(buffer, clickable);
1297 field = buffer;
1298 }
1299
30d98de0
LP
1300 if (row == t->data) /* underline header line fully, including the column separator */
1301 fputs(ansi_underline(), f);
1302
1960e736
LP
1303 if (j > 0)
1304 fputc(' ', f); /* column separator */
1305
30d98de0
LP
1306 if (d->color && colors_enabled()) {
1307 if (row == t->data) /* first undo header underliner */
1308 fputs(ANSI_NORMAL, f);
1309
1960e736 1310 fputs(d->color, f);
30d98de0 1311 }
1960e736
LP
1312
1313 fputs(field, f);
1314
30d98de0 1315 if (colors_enabled() && (d->color || row == t->data))
a22318e5 1316 fputs(ANSI_NORMAL, f);
1960e736
LP
1317 }
1318
1319 fputc('\n', f);
1320 }
1321
1322 return fflush_and_check(f);
1323}
1324
1325int table_format(Table *t, char **ret) {
1326 _cleanup_fclose_ FILE *f = NULL;
1327 char *buf = NULL;
1328 size_t sz = 0;
1329 int r;
1330
1331 f = open_memstream(&buf, &sz);
1332 if (!f)
1333 return -ENOMEM;
1334
1335 (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
1336
1337 r = table_print(t, f);
1338 if (r < 0)
1339 return r;
1340
1341 f = safe_fclose(f);
1342
1343 *ret = buf;
1344
1345 return 0;
1346}
1347
1348size_t table_get_rows(Table *t) {
1349 if (!t)
1350 return 0;
1351
1352 assert(t->n_columns > 0);
1353 return t->n_cells / t->n_columns;
1354}
1355
1356size_t table_get_columns(Table *t) {
1357 if (!t)
1358 return 0;
1359
1360 assert(t->n_columns > 0);
1361 return t->n_columns;
1362}
a2c73e2d
LP
1363
1364int table_set_reverse(Table *t, size_t column, bool b) {
1365 assert(t);
1366 assert(column < t->n_columns);
1367
1368 if (!t->reverse_map) {
1369 if (!b)
1370 return 0;
1371
1372 t->reverse_map = new0(bool, t->n_columns);
1373 if (!t->reverse_map)
1374 return -ENOMEM;
1375 }
1376
1377 t->reverse_map[column] = b;
1378 return 0;
1379}
9314ead7
LP
1380
1381TableCell *table_get_cell(Table *t, size_t row, size_t column) {
1382 size_t i;
1383
1384 assert(t);
1385
1386 if (column >= t->n_columns)
1387 return NULL;
1388
1389 i = row * t->n_columns + column;
1390 if (i >= t->n_cells)
1391 return NULL;
1392
1393 return TABLE_INDEX_TO_CELL(i);
1394}
62d99b39
LP
1395
1396const void *table_get(Table *t, TableCell *cell) {
1397 TableData *d;
1398
1399 assert(t);
1400
1401 d = table_get_data(t, cell);
1402 if (!d)
1403 return NULL;
1404
1405 return d->data;
1406}
1407
1408const void* table_get_at(Table *t, size_t row, size_t column) {
1409 TableCell *cell;
1410
1411 cell = table_get_cell(t, row, column);
1412 if (!cell)
1413 return NULL;
1414
1415 return table_get(t, cell);
1416}