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