]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/shared/format-table.c
format-table: use right field for display
[thirdparty/systemd.git] / src / shared / format-table.c
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"
12 #include "pretty-print.h"
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
40 - Note because there's no row and no column object some properties that might be appropriate as row/column properties
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
51 typedef 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 */
62 char *url; /* A URL to use for a clickable hyperlink */
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;
73 uint64_t uint64;
74 int percent; /* we use 'int' as datatype for percent values in order to match the result of parse_percent() */
75 /* … add more here as we start supporting more cell data types … */
76 };
77 } TableData;
78
79 static size_t TABLE_CELL_TO_INDEX(TableCell *cell) {
80 size_t i;
81
82 assert(cell);
83
84 i = PTR_TO_SIZE(cell);
85 assert(i > 0);
86
87 return i-1;
88 }
89
90 static TableCell* TABLE_INDEX_TO_CELL(size_t index) {
91 assert(index != (size_t) -1);
92 return SIZE_TO_PTR(index + 1);
93 }
94
95 struct 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;
110
111 bool *reverse_map;
112 };
113
114 Table *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
132 Table *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
180 static TableData *table_data_free(TableData *d) {
181 assert(d);
182
183 free(d->formatted);
184 free(d->url);
185
186 return mfree(d);
187 }
188
189 DEFINE_PRIVATE_TRIVIAL_REF_UNREF_FUNC(TableData, table_data, table_data_free);
190 DEFINE_TRIVIAL_CLEANUP_FUNC(TableData*, table_data_unref);
191
192 Table *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);
204 free(t->reverse_map);
205
206 return mfree(t);
207 }
208
209 static 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:
227 case TABLE_UINT64:
228 return sizeof(uint64_t);
229
230 case TABLE_UINT32:
231 return sizeof(uint32_t);
232
233 case TABLE_PERCENT:
234 return sizeof(int);
235
236 default:
237 assert_not_reached("Uh? Unexpected cell type");
238 }
239 }
240
241 static 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
278 return memcmp_safe(data, d->data, l) == 0;
279 }
280
281 static 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
311 int 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
373 int 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
391 static int table_dedup_cell(Table *t, TableCell *cell) {
392 _cleanup_free_ char *curl = NULL;
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
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);
425 if (!nd)
426 return -ENOMEM;
427
428 nd->color = od->color;
429 nd->url = TAKE_PTR(curl);
430
431 table_data_unref(od);
432 t->data[i] = nd;
433
434 assert(nd->n_ref == 1);
435
436 return 1;
437 }
438
439 static 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
457 int 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
474 int 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
488 int 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
505 int 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
524 int 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
543 int 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
557 int 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
577 int 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
617 int 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;
635 uint64_t uint64;
636 int percent;
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:
651 buffer.b = va_arg(ap, int);
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
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
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
700 void table_set_header(Table *t, bool b) {
701 assert(t);
702
703 t->header = b;
704 }
705
706 void table_set_width(Table *t, size_t width) {
707 assert(t);
708
709 t->width = width;
710 }
711
712 int 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 }
737 va_end(ap);
738
739 return 0;
740 }
741
742 int 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 }
766 va_end(ap);
767
768 return 0;
769 }
770
771 static 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:
793 return CMP(a->timestamp, b->timestamp);
794
795 case TABLE_TIMESPAN:
796 return CMP(a->timespan, b->timespan);
797
798 case TABLE_SIZE:
799 return CMP(a->size, b->size);
800
801 case TABLE_UINT32:
802 return CMP(a->uint32, b->uint32);
803
804 case TABLE_UINT64:
805 return CMP(a->uint64, b->uint64);
806
807 case TABLE_PERCENT:
808 return CMP(a->percent, b->percent);
809
810 default:
811 ;
812 }
813 }
814
815 /* Generic fallback using the orginal order in which the cells where added. */
816 return CMP(index_a, index_b);
817 }
818
819 static int table_data_compare(const size_t *a, const size_t *b, Table *t) {
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)
843 return t->reverse_map && t->reverse_map[t->sort_map[i]] ? -r : r;
844 }
845
846 /* Order identical lines by the order there were originally added in */
847 return CMP(*a, *b);
848 }
849
850 static 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->timespan, 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
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
944 default:
945 assert_not_reached("Unexpected type?");
946 }
947
948 return d->formatted;
949 }
950
951 static 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
973 static 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;
976 const char *p;
977 char *ret;
978 size_t i;
979 int r;
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
986 old_length = strlen(str);
987
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
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)
1013 return clickable ? TAKE_PTR(clickable) : strdup(str);
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
1019 ret = new(char, space + clickable_length + 1);
1020 if (!ret)
1021 return NULL;
1022
1023 for (i = 0; i < lspace; i++)
1024 ret[i] = ' ';
1025 memcpy(ret + lspace, clickable ?: str, clickable_length);
1026 for (i = lspace + clickable_length; i < space + clickable_length; i++)
1027 ret[i] = ' ';
1028
1029 ret[space + clickable_length] = 0;
1030 return ret;
1031 }
1032
1033 int 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
1062 typesafe_qsort_r(sorted, n_rows, table_data_compare, t);
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
1236 if (restart && !finalize)
1237 break;
1238 }
1239
1240 if (finalize)
1241 break;
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
1273 buffer = ellipsize(field, width[j], d->ellipsize_percent);
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
1282 buffer = align_string_mem(field, d->url, width[j], d->align_percent);
1283 if (!buffer)
1284 return -ENOMEM;
1285
1286 field = buffer;
1287 }
1288
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
1300 if (row == t->data) /* underline header line fully, including the column separator */
1301 fputs(ansi_underline(), f);
1302
1303 if (j > 0)
1304 fputc(' ', f); /* column separator */
1305
1306 if (d->color && colors_enabled()) {
1307 if (row == t->data) /* first undo header underliner */
1308 fputs(ANSI_NORMAL, f);
1309
1310 fputs(d->color, f);
1311 }
1312
1313 fputs(field, f);
1314
1315 if (colors_enabled() && (d->color || row == t->data))
1316 fputs(ANSI_NORMAL, f);
1317 }
1318
1319 fputc('\n', f);
1320 }
1321
1322 return fflush_and_check(f);
1323 }
1324
1325 int 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
1348 size_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
1356 size_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 }
1363
1364 int 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 }
1380
1381 TableCell *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 }
1395
1396 const 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
1408 const 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 }