]> git.ipfire.org Git - thirdparty/systemd.git/blame - src/shared/format-table.c
format-table: optionally allow reversing the sort order for a column
[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
278 return memcmp(data, d->data, l) == 0;
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
1960e736
LP
577int table_add_many_internal(Table *t, TableDataType first_type, ...) {
578 TableDataType type;
579 va_list ap;
580 int r;
581
582 assert(t);
583 assert(first_type >= 0);
584 assert(first_type < _TABLE_DATA_TYPE_MAX);
585
586 type = first_type;
587
588 va_start(ap, first_type);
589 for (;;) {
590 const void *data;
591 union {
592 uint64_t size;
593 usec_t usec;
594 uint32_t uint32;
a4661181
LP
595 uint64_t uint64;
596 int percent;
1960e736
LP
597 bool b;
598 } buffer;
599
600 switch (type) {
601
602 case TABLE_EMPTY:
603 data = NULL;
604 break;
605
606 case TABLE_STRING:
607 data = va_arg(ap, const char *);
608 break;
609
610 case TABLE_BOOLEAN:
5d904a6a 611 buffer.b = va_arg(ap, int);
1960e736
LP
612 data = &buffer.b;
613 break;
614
615 case TABLE_TIMESTAMP:
616 case TABLE_TIMESPAN:
617 buffer.usec = va_arg(ap, usec_t);
618 data = &buffer.usec;
619 break;
620
621 case TABLE_SIZE:
622 buffer.size = va_arg(ap, uint64_t);
623 data = &buffer.size;
624 break;
625
626 case TABLE_UINT32:
627 buffer.uint32 = va_arg(ap, uint32_t);
628 data = &buffer.uint32;
629 break;
630
a4661181
LP
631 case TABLE_UINT64:
632 buffer.uint64 = va_arg(ap, uint64_t);
633 data = &buffer.uint64;
634 break;
635
636 case TABLE_PERCENT:
637 buffer.percent = va_arg(ap, int);
638 data = &buffer.percent;
639 break;
640
1960e736
LP
641 case _TABLE_DATA_TYPE_MAX:
642 /* Used as end marker */
643 va_end(ap);
644 return 0;
645
646 default:
647 assert_not_reached("Uh? Unexpected data type.");
648 }
649
650 r = table_add_cell(t, NULL, type, data);
651 if (r < 0) {
652 va_end(ap);
653 return r;
654 }
655
656 type = va_arg(ap, TableDataType);
657 }
658}
659
660void table_set_header(Table *t, bool b) {
661 assert(t);
662
663 t->header = b;
664}
665
666void table_set_width(Table *t, size_t width) {
667 assert(t);
668
669 t->width = width;
670}
671
672int table_set_display(Table *t, size_t first_column, ...) {
673 size_t allocated, column;
674 va_list ap;
675
676 assert(t);
677
678 allocated = t->n_display_map;
679 column = first_column;
680
681 va_start(ap, first_column);
682 for (;;) {
683 assert(column < t->n_columns);
684
685 if (!GREEDY_REALLOC(t->display_map, allocated, MAX(t->n_columns, t->n_display_map+1))) {
686 va_end(ap);
687 return -ENOMEM;
688 }
689
690 t->display_map[t->n_display_map++] = column;
691
692 column = va_arg(ap, size_t);
693 if (column == (size_t) -1)
694 break;
695
696 }
f20f4a77 697 va_end(ap);
1960e736
LP
698
699 return 0;
700}
701
702int table_set_sort(Table *t, size_t first_column, ...) {
703 size_t allocated, column;
704 va_list ap;
705
706 assert(t);
707
708 allocated = t->n_sort_map;
709 column = first_column;
710
711 va_start(ap, first_column);
712 for (;;) {
713 assert(column < t->n_columns);
714
715 if (!GREEDY_REALLOC(t->sort_map, allocated, MAX(t->n_columns, t->n_sort_map+1))) {
716 va_end(ap);
717 return -ENOMEM;
718 }
719
720 t->sort_map[t->n_sort_map++] = column;
721
722 column = va_arg(ap, size_t);
723 if (column == (size_t) -1)
724 break;
725 }
f20f4a77 726 va_end(ap);
1960e736
LP
727
728 return 0;
729}
730
731static int cell_data_compare(TableData *a, size_t index_a, TableData *b, size_t index_b) {
732 assert(a);
733 assert(b);
734
735 if (a->type == b->type) {
736
737 /* We only define ordering for cells of the same data type. If cells with different data types are
738 * compared we follow the order the cells were originally added in */
739
740 switch (a->type) {
741
742 case TABLE_STRING:
743 return strcmp(a->string, b->string);
744
745 case TABLE_BOOLEAN:
746 if (!a->boolean && b->boolean)
747 return -1;
748 if (a->boolean && !b->boolean)
749 return 1;
750 return 0;
751
752 case TABLE_TIMESTAMP:
6dd91b36 753 return CMP(a->timestamp, b->timestamp);
1960e736
LP
754
755 case TABLE_TIMESPAN:
6dd91b36 756 return CMP(a->timespan, b->timespan);
1960e736
LP
757
758 case TABLE_SIZE:
6dd91b36 759 return CMP(a->size, b->size);
1960e736
LP
760
761 case TABLE_UINT32:
6dd91b36 762 return CMP(a->uint32, b->uint32);
1960e736 763
a4661181
LP
764 case TABLE_UINT64:
765 return CMP(a->uint64, b->uint64);
766
767 case TABLE_PERCENT:
768 return CMP(a->percent, b->percent);
769
1960e736
LP
770 default:
771 ;
772 }
773 }
774
775 /* Generic fallback using the orginal order in which the cells where added. */
6dd91b36 776 return CMP(index_a, index_b);
1960e736
LP
777}
778
ba0a7bfb 779static int table_data_compare(const size_t *a, const size_t *b, Table *t) {
1960e736
LP
780 size_t i;
781 int r;
782
783 assert(t);
784 assert(t->sort_map);
785
786 /* Make sure the header stays at the beginning */
787 if (*a < t->n_columns && *b < t->n_columns)
788 return 0;
789 if (*a < t->n_columns)
790 return -1;
791 if (*b < t->n_columns)
792 return 1;
793
794 /* Order other lines by the sorting map */
795 for (i = 0; i < t->n_sort_map; i++) {
796 TableData *d, *dd;
797
798 d = t->data[*a + t->sort_map[i]];
799 dd = t->data[*b + t->sort_map[i]];
800
801 r = cell_data_compare(d, *a, dd, *b);
802 if (r != 0)
a2c73e2d 803 return t->reverse_map && t->reverse_map[t->sort_map[i]] ? -r : r;
1960e736
LP
804 }
805
806 /* Order identical lines by the order there were originally added in */
ba0a7bfb 807 return CMP(*a, *b);
1960e736
LP
808}
809
810static const char *table_data_format(TableData *d) {
811 assert(d);
812
813 if (d->formatted)
814 return d->formatted;
815
816 switch (d->type) {
817 case TABLE_EMPTY:
818 return "";
819
820 case TABLE_STRING:
821 return d->string;
822
823 case TABLE_BOOLEAN:
824 return yes_no(d->boolean);
825
826 case TABLE_TIMESTAMP: {
827 _cleanup_free_ char *p;
828
829 p = new(char, FORMAT_TIMESTAMP_MAX);
830 if (!p)
831 return NULL;
832
833 if (!format_timestamp(p, FORMAT_TIMESTAMP_MAX, d->timestamp))
834 return "n/a";
835
836 d->formatted = TAKE_PTR(p);
837 break;
838 }
839
840 case TABLE_TIMESPAN: {
841 _cleanup_free_ char *p;
842
843 p = new(char, FORMAT_TIMESPAN_MAX);
844 if (!p)
845 return NULL;
846
847 if (!format_timespan(p, FORMAT_TIMESPAN_MAX, d->timestamp, 0))
848 return "n/a";
849
850 d->formatted = TAKE_PTR(p);
851 break;
852 }
853
854 case TABLE_SIZE: {
855 _cleanup_free_ char *p;
856
857 p = new(char, FORMAT_BYTES_MAX);
858 if (!p)
859 return NULL;
860
861 if (!format_bytes(p, FORMAT_BYTES_MAX, d->size))
862 return "n/a";
863
864 d->formatted = TAKE_PTR(p);
865 break;
866 }
867
868 case TABLE_UINT32: {
869 _cleanup_free_ char *p;
870
871 p = new(char, DECIMAL_STR_WIDTH(d->uint32) + 1);
872 if (!p)
873 return NULL;
874
875 sprintf(p, "%" PRIu32, d->uint32);
876 d->formatted = TAKE_PTR(p);
877 break;
878 }
879
a4661181
LP
880 case TABLE_UINT64: {
881 _cleanup_free_ char *p;
882
883 p = new(char, DECIMAL_STR_WIDTH(d->uint64) + 1);
884 if (!p)
885 return NULL;
886
887 sprintf(p, "%" PRIu64, d->uint64);
888 d->formatted = TAKE_PTR(p);
889 break;
890 }
891
892 case TABLE_PERCENT: {
893 _cleanup_free_ char *p;
894
895 p = new(char, DECIMAL_STR_WIDTH(d->percent) + 2);
896 if (!p)
897 return NULL;
898
899 sprintf(p, "%i%%" , d->percent);
900 d->formatted = TAKE_PTR(p);
901 break;
902 }
903
1960e736
LP
904 default:
905 assert_not_reached("Unexpected type?");
906 }
907
1960e736
LP
908 return d->formatted;
909}
910
911static int table_data_requested_width(TableData *d, size_t *ret) {
912 const char *t;
913 size_t l;
914
915 t = table_data_format(d);
916 if (!t)
917 return -ENOMEM;
918
919 l = utf8_console_width(t);
920 if (l == (size_t) -1)
921 return -EINVAL;
922
923 if (d->maximum_width != (size_t) -1 && l > d->maximum_width)
924 l = d->maximum_width;
925
926 if (l < d->minimum_width)
927 l = d->minimum_width;
928
929 *ret = l;
930 return 0;
931}
932
165ca566
LP
933static char *align_string_mem(const char *str, const char *url, size_t new_length, unsigned percent) {
934 size_t w = 0, space, lspace, old_length, clickable_length;
935 _cleanup_free_ char *clickable = NULL;
1960e736
LP
936 const char *p;
937 char *ret;
938 size_t i;
165ca566 939 int r;
1960e736
LP
940
941 /* As with ellipsize_mem(), 'old_length' is a byte size while 'new_length' is a width in character cells */
942
943 assert(str);
944 assert(percent <= 100);
945
cfc01c1e 946 old_length = strlen(str);
1960e736 947
165ca566
LP
948 if (url) {
949 r = terminal_urlify(url, str, &clickable);
950 if (r < 0)
951 return NULL;
952
953 clickable_length = strlen(clickable);
954 } else
955 clickable_length = old_length;
956
1960e736
LP
957 /* Determine current width on screen */
958 p = str;
959 while (p < str + old_length) {
960 char32_t c;
961
962 if (utf8_encoded_to_unichar(p, &c) < 0) {
963 p++, w++; /* count invalid chars as 1 */
964 continue;
965 }
966
967 p = utf8_next_char(p);
968 w += unichar_iswide(c) ? 2 : 1;
969 }
970
971 /* Already wider than the target, if so, don't do anything */
972 if (w >= new_length)
165ca566 973 return clickable ? TAKE_PTR(clickable) : strdup(str);
1960e736
LP
974
975 /* How much spaces shall we add? An how much on the left side? */
976 space = new_length - w;
977 lspace = space * percent / 100U;
978
165ca566 979 ret = new(char, space + clickable_length + 1);
1960e736
LP
980 if (!ret)
981 return NULL;
982
983 for (i = 0; i < lspace; i++)
984 ret[i] = ' ';
165ca566
LP
985 memcpy(ret + lspace, clickable ?: str, clickable_length);
986 for (i = lspace + clickable_length; i < space + clickable_length; i++)
1960e736
LP
987 ret[i] = ' ';
988
165ca566 989 ret[space + clickable_length] = 0;
1960e736
LP
990 return ret;
991}
992
993int table_print(Table *t, FILE *f) {
994 size_t n_rows, *minimum_width, *maximum_width, display_columns, *requested_width,
995 i, j, table_minimum_width, table_maximum_width, table_requested_width, table_effective_width,
996 *width;
997 _cleanup_free_ size_t *sorted = NULL;
998 uint64_t *column_weight, weight_sum;
999 int r;
1000
1001 assert(t);
1002
1003 if (!f)
1004 f = stdout;
1005
1006 /* Ensure we have no incomplete rows */
1007 assert(t->n_cells % t->n_columns == 0);
1008
1009 n_rows = t->n_cells / t->n_columns;
1010 assert(n_rows > 0); /* at least the header row must be complete */
1011
1012 if (t->sort_map) {
1013 /* If sorting is requested, let's calculate an index table we use to lookup the actual index to display with. */
1014
1015 sorted = new(size_t, n_rows);
1016 if (!sorted)
1017 return -ENOMEM;
1018
1019 for (i = 0; i < n_rows; i++)
1020 sorted[i] = i * t->n_columns;
1021
ba0a7bfb 1022 typesafe_qsort_r(sorted, n_rows, table_data_compare, t);
1960e736
LP
1023 }
1024
1025 if (t->display_map)
1026 display_columns = t->n_display_map;
1027 else
1028 display_columns = t->n_columns;
1029
1030 assert(display_columns > 0);
1031
1032 minimum_width = newa(size_t, display_columns);
1033 maximum_width = newa(size_t, display_columns);
1034 requested_width = newa(size_t, display_columns);
1035 width = newa(size_t, display_columns);
1036 column_weight = newa0(uint64_t, display_columns);
1037
1038 for (j = 0; j < display_columns; j++) {
1039 minimum_width[j] = 1;
1040 maximum_width[j] = (size_t) -1;
1041 requested_width[j] = (size_t) -1;
1042 }
1043
1044 /* First pass: determine column sizes */
1045 for (i = t->header ? 0 : 1; i < n_rows; i++) {
1046 TableData **row;
1047
1048 /* Note that we don't care about ordering at this time, as we just want to determine column sizes,
1049 * hence we don't care for sorted[] during the first pass. */
1050 row = t->data + i * t->n_columns;
1051
1052 for (j = 0; j < display_columns; j++) {
1053 TableData *d;
1054 size_t req;
1055
1056 assert_se(d = row[t->display_map ? t->display_map[j] : j]);
1057
1058 r = table_data_requested_width(d, &req);
1059 if (r < 0)
1060 return r;
1061
1062 /* Determine the biggest width that any cell in this column would like to have */
1063 if (requested_width[j] == (size_t) -1 ||
1064 requested_width[j] < req)
1065 requested_width[j] = req;
1066
1067 /* Determine the minimum width any cell in this column needs */
1068 if (minimum_width[j] < d->minimum_width)
1069 minimum_width[j] = d->minimum_width;
1070
1071 /* Determine the maximum width any cell in this column needs */
1072 if (d->maximum_width != (size_t) -1 &&
1073 (maximum_width[j] == (size_t) -1 ||
1074 maximum_width[j] > d->maximum_width))
1075 maximum_width[j] = d->maximum_width;
1076
1077 /* Determine the full columns weight */
1078 column_weight[j] += d->weight;
1079 }
1080 }
1081
1082 /* One space between each column */
1083 table_requested_width = table_minimum_width = table_maximum_width = display_columns - 1;
1084
1085 /* Calculate the total weight for all columns, plus the minimum, maximum and requested width for the table. */
1086 weight_sum = 0;
1087 for (j = 0; j < display_columns; j++) {
1088 weight_sum += column_weight[j];
1089
1090 table_minimum_width += minimum_width[j];
1091
1092 if (maximum_width[j] == (size_t) -1)
1093 table_maximum_width = (size_t) -1;
1094 else
1095 table_maximum_width += maximum_width[j];
1096
1097 table_requested_width += requested_width[j];
1098 }
1099
1100 /* Calculate effective table width */
1101 if (t->width == (size_t) -1)
1102 table_effective_width = pager_have() ? table_requested_width : MIN(table_requested_width, columns());
1103 else
1104 table_effective_width = t->width;
1105
1106 if (table_maximum_width != (size_t) -1 && table_effective_width > table_maximum_width)
1107 table_effective_width = table_maximum_width;
1108
1109 if (table_effective_width < table_minimum_width)
1110 table_effective_width = table_minimum_width;
1111
1112 if (table_effective_width >= table_requested_width) {
1113 size_t extra;
1114
1115 /* We have extra room, let's distribute it among columns according to their weights. We first provide
1116 * each column with what it asked for and the distribute the rest. */
1117
1118 extra = table_effective_width - table_requested_width;
1119
1120 for (j = 0; j < display_columns; j++) {
1121 size_t delta;
1122
1123 if (weight_sum == 0)
1124 width[j] = requested_width[j] + extra / (display_columns - j); /* Avoid division by zero */
1125 else
1126 width[j] = requested_width[j] + (extra * column_weight[j]) / weight_sum;
1127
1128 if (maximum_width[j] != (size_t) -1 && width[j] > maximum_width[j])
1129 width[j] = maximum_width[j];
1130
1131 if (width[j] < minimum_width[j])
1132 width[j] = minimum_width[j];
1133
1134 assert(width[j] >= requested_width[j]);
1135 delta = width[j] - requested_width[j];
1136
1137 /* Subtract what we just added from the rest */
1138 if (extra > delta)
1139 extra -= delta;
1140 else
1141 extra = 0;
1142
1143 assert(weight_sum >= column_weight[j]);
1144 weight_sum -= column_weight[j];
1145 }
1146
1147 } else {
1148 /* We need to compress the table, columns can't get what they asked for. We first provide each column
1149 * with the minimum they need, and then distribute anything left. */
1150 bool finalize = false;
1151 size_t extra;
1152
1153 extra = table_effective_width - table_minimum_width;
1154
1155 for (j = 0; j < display_columns; j++)
1156 width[j] = (size_t) -1;
1157
1158 for (;;) {
1159 bool restart = false;
1160
1161 for (j = 0; j < display_columns; j++) {
1162 size_t delta, w;
1163
1164 /* Did this column already get something assigned? If so, let's skip to the next */
1165 if (width[j] != (size_t) -1)
1166 continue;
1167
1168 if (weight_sum == 0)
1169 w = minimum_width[j] + extra / (display_columns - j); /* avoid division by zero */
1170 else
1171 w = minimum_width[j] + (extra * column_weight[j]) / weight_sum;
1172
1173 if (w >= requested_width[j]) {
1174 /* Never give more than requested. If we hit a column like this, there's more
1175 * space to allocate to other columns which means we need to restart the
1176 * iteration. However, if we hit a column like this, let's assign it the space
1177 * it wanted for good early.*/
1178
1179 w = requested_width[j];
1180 restart = true;
1181
1182 } else if (!finalize)
1183 continue;
1184
1185 width[j] = w;
1186
1187 assert(w >= minimum_width[j]);
1188 delta = w - minimum_width[j];
1189
1190 assert(delta <= extra);
1191 extra -= delta;
1192
1193 assert(weight_sum >= column_weight[j]);
1194 weight_sum -= column_weight[j];
1195
a26db0bc 1196 if (restart && !finalize)
1960e736
LP
1197 break;
1198 }
1199
a26db0bc 1200 if (finalize)
1960e736 1201 break;
1960e736
LP
1202
1203 if (!restart)
1204 finalize = true;
1205 }
1206 }
1207
1208 /* Second pass: show output */
1209 for (i = t->header ? 0 : 1; i < n_rows; i++) {
1210 TableData **row;
1211
1212 if (sorted)
1213 row = t->data + sorted[i];
1214 else
1215 row = t->data + i * t->n_columns;
1216
1217 for (j = 0; j < display_columns; j++) {
1218 _cleanup_free_ char *buffer = NULL;
1219 const char *field;
1220 TableData *d;
1221 size_t l;
1222
1223 assert_se(d = row[t->display_map ? t->display_map[j] : j]);
1224
1225 field = table_data_format(d);
1226 if (!field)
1227 return -ENOMEM;
1228
1229 l = utf8_console_width(field);
1230 if (l > width[j]) {
1231 /* Field is wider than allocated space. Let's ellipsize */
1232
21e4e3e0 1233 buffer = ellipsize(field, width[j], d->ellipsize_percent);
1960e736
LP
1234 if (!buffer)
1235 return -ENOMEM;
1236
1237 field = buffer;
1238
1239 } else if (l < width[j]) {
1240 /* Field is shorter than allocated space. Let's align with spaces */
1241
165ca566 1242 buffer = align_string_mem(field, d->url, width[j], d->align_percent);
1960e736
LP
1243 if (!buffer)
1244 return -ENOMEM;
1245
1246 field = buffer;
1247 }
1248
165ca566
LP
1249 if (l >= width[j] && d->url) {
1250 _cleanup_free_ char *clickable = NULL;
1251
1252 r = terminal_urlify(d->url, field, &clickable);
1253 if (r < 0)
1254 return r;
1255
1256 free_and_replace(buffer, clickable);
1257 field = buffer;
1258 }
1259
1960e736
LP
1260 if (j > 0)
1261 fputc(' ', f); /* column separator */
1262
a22318e5 1263 if (d->color && colors_enabled())
1960e736
LP
1264 fputs(d->color, f);
1265
1266 fputs(field, f);
1267
a22318e5
LP
1268 if (d->color && colors_enabled())
1269 fputs(ANSI_NORMAL, f);
1960e736
LP
1270 }
1271
1272 fputc('\n', f);
1273 }
1274
1275 return fflush_and_check(f);
1276}
1277
1278int table_format(Table *t, char **ret) {
1279 _cleanup_fclose_ FILE *f = NULL;
1280 char *buf = NULL;
1281 size_t sz = 0;
1282 int r;
1283
1284 f = open_memstream(&buf, &sz);
1285 if (!f)
1286 return -ENOMEM;
1287
1288 (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
1289
1290 r = table_print(t, f);
1291 if (r < 0)
1292 return r;
1293
1294 f = safe_fclose(f);
1295
1296 *ret = buf;
1297
1298 return 0;
1299}
1300
1301size_t table_get_rows(Table *t) {
1302 if (!t)
1303 return 0;
1304
1305 assert(t->n_columns > 0);
1306 return t->n_cells / t->n_columns;
1307}
1308
1309size_t table_get_columns(Table *t) {
1310 if (!t)
1311 return 0;
1312
1313 assert(t->n_columns > 0);
1314 return t->n_columns;
1315}
a2c73e2d
LP
1316
1317int table_set_reverse(Table *t, size_t column, bool b) {
1318 assert(t);
1319 assert(column < t->n_columns);
1320
1321 if (!t->reverse_map) {
1322 if (!b)
1323 return 0;
1324
1325 t->reverse_map = new0(bool, t->n_columns);
1326 if (!t->reverse_map)
1327 return -ENOMEM;
1328 }
1329
1330 t->reverse_map[column] = b;
1331 return 0;
1332}