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