| |
| |
| #include <ctype.h> |
| #include <stdio_ext.h> |
| |
| #include "alloc-util.h" |
| #include "fd-util.h" |
| #include "fileio.h" |
| #include "format-table.h" |
| #include "gunicode.h" |
| #include "pager.h" |
| #include "parse-util.h" |
| #include "string-util.h" |
| #include "terminal-util.h" |
| #include "time-util.h" |
| #include "utf8.h" |
| #include "util.h" |
| |
| #define DEFAULT_WEIGHT 100 |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| typedef struct TableData { |
| unsigned n_ref; |
| TableDataType type; |
| |
| size_t minimum_width; |
| size_t maximum_width; |
| unsigned weight; |
| unsigned ellipsize_percent; |
| unsigned align_percent; |
| |
| bool uppercase; |
| |
| const char *color; |
| char *url; |
| char *formatted; |
| |
| union { |
| uint8_t data[0]; |
| bool boolean; |
| usec_t timestamp; |
| usec_t timespan; |
| uint64_t size; |
| char string[0]; |
| uint32_t uint32; |
| uint64_t uint64; |
| int percent; |
| |
| }; |
| } TableData; |
| |
| static size_t TABLE_CELL_TO_INDEX(TableCell *cell) { |
| unsigned i; |
| |
| assert(cell); |
| |
| i = PTR_TO_UINT(cell); |
| assert(i > 0); |
| |
| return i-1; |
| } |
| |
| static TableCell* TABLE_INDEX_TO_CELL(size_t index) { |
| assert(index != (size_t) -1); |
| return UINT_TO_PTR((unsigned) (index + 1)); |
| } |
| |
| struct Table { |
| size_t n_columns; |
| size_t n_cells; |
| |
| bool header; |
| size_t width; |
| |
| TableData **data; |
| size_t n_allocated; |
| |
| size_t *display_map; |
| size_t n_display_map; |
| |
| size_t *sort_map; |
| size_t n_sort_map; |
| |
| bool *reverse_map; |
| }; |
| |
| Table *table_new_raw(size_t n_columns) { |
| _cleanup_(table_unrefp) Table *t = NULL; |
| |
| assert(n_columns > 0); |
| |
| t = new(Table, 1); |
| if (!t) |
| return NULL; |
| |
| *t = (struct Table) { |
| .n_columns = n_columns, |
| .header = true, |
| .width = (size_t) -1, |
| }; |
| |
| return TAKE_PTR(t); |
| } |
| |
| Table *table_new_internal(const char *first_header, ...) { |
| _cleanup_(table_unrefp) Table *t = NULL; |
| size_t n_columns = 1; |
| const char *h; |
| va_list ap; |
| int r; |
| |
| assert(first_header); |
| |
| va_start(ap, first_header); |
| for (;;) { |
| h = va_arg(ap, const char*); |
| if (!h) |
| break; |
| |
| n_columns++; |
| } |
| va_end(ap); |
| |
| t = table_new_raw(n_columns); |
| if (!t) |
| return NULL; |
| |
| va_start(ap, first_header); |
| for (h = first_header; h; h = va_arg(ap, const char*)) { |
| TableCell *cell; |
| |
| r = table_add_cell(t, &cell, TABLE_STRING, h); |
| if (r < 0) { |
| va_end(ap); |
| return NULL; |
| } |
| |
| |
| r = table_set_uppercase(t, cell, true); |
| if (r < 0) { |
| va_end(ap); |
| return NULL; |
| } |
| } |
| va_end(ap); |
| |
| assert(t->n_columns == t->n_cells); |
| return TAKE_PTR(t); |
| } |
| |
| static TableData *table_data_unref(TableData *d) { |
| if (!d) |
| return NULL; |
| |
| assert(d->n_ref > 0); |
| d->n_ref--; |
| |
| if (d->n_ref > 0) |
| return NULL; |
| |
| free(d->formatted); |
| free(d->url); |
| |
| return mfree(d); |
| } |
| |
| DEFINE_TRIVIAL_CLEANUP_FUNC(TableData*, table_data_unref); |
| |
| static TableData *table_data_ref(TableData *d) { |
| if (!d) |
| return NULL; |
| |
| assert(d->n_ref > 0); |
| d->n_ref++; |
| |
| return d; |
| } |
| |
| Table *table_unref(Table *t) { |
| size_t i; |
| |
| if (!t) |
| return NULL; |
| |
| for (i = 0; i < t->n_cells; i++) |
| table_data_unref(t->data[i]); |
| |
| free(t->data); |
| free(t->display_map); |
| free(t->sort_map); |
| free(t->reverse_map); |
| |
| return mfree(t); |
| } |
| |
| static size_t table_data_size(TableDataType type, const void *data) { |
| |
| switch (type) { |
| |
| case TABLE_EMPTY: |
| return 0; |
| |
| case TABLE_STRING: |
| return strlen(data) + 1; |
| |
| case TABLE_BOOLEAN: |
| return sizeof(bool); |
| |
| case TABLE_TIMESTAMP: |
| case TABLE_TIMESPAN: |
| return sizeof(usec_t); |
| |
| case TABLE_SIZE: |
| case TABLE_UINT64: |
| return sizeof(uint64_t); |
| |
| case TABLE_UINT32: |
| return sizeof(uint32_t); |
| |
| case TABLE_PERCENT: |
| return sizeof(int); |
| |
| default: |
| assert_not_reached("Uh? Unexpected cell type"); |
| } |
| } |
| |
| static bool table_data_matches( |
| TableData *d, |
| TableDataType type, |
| const void *data, |
| size_t minimum_width, |
| size_t maximum_width, |
| unsigned weight, |
| unsigned align_percent, |
| unsigned ellipsize_percent) { |
| |
| size_t k, l; |
| assert(d); |
| |
| if (d->type != type) |
| return false; |
| |
| if (d->minimum_width != minimum_width) |
| return false; |
| |
| if (d->maximum_width != maximum_width) |
| return false; |
| |
| if (d->weight != weight) |
| return false; |
| |
| if (d->align_percent != align_percent) |
| return false; |
| |
| if (d->ellipsize_percent != ellipsize_percent) |
| return false; |
| |
| |
| if (d->color) |
| return false; |
| if (d->url) |
| return false; |
| if (d->uppercase) |
| return false; |
| |
| k = table_data_size(type, data); |
| l = table_data_size(d->type, d->data); |
| |
| if (k != l) |
| return false; |
| |
| return memcmp_safe(data, d->data, l) == 0; |
| } |
| |
| static TableData *table_data_new( |
| TableDataType type, |
| const void *data, |
| size_t minimum_width, |
| size_t maximum_width, |
| unsigned weight, |
| unsigned align_percent, |
| unsigned ellipsize_percent) { |
| |
| size_t data_size; |
| TableData *d; |
| |
| data_size = table_data_size(type, data); |
| |
| d = malloc0(offsetof(TableData, data) + data_size); |
| if (!d) |
| return NULL; |
| |
| d->n_ref = 1; |
| d->type = type; |
| d->minimum_width = minimum_width; |
| d->maximum_width = maximum_width; |
| d->weight = weight; |
| d->align_percent = align_percent; |
| d->ellipsize_percent = ellipsize_percent; |
| memcpy_safe(d->data, data, data_size); |
| |
| return d; |
| } |
| |
| int table_add_cell_full( |
| Table *t, |
| TableCell **ret_cell, |
| TableDataType type, |
| const void *data, |
| size_t minimum_width, |
| size_t maximum_width, |
| unsigned weight, |
| unsigned align_percent, |
| unsigned ellipsize_percent) { |
| |
| _cleanup_(table_data_unrefp) TableData *d = NULL; |
| TableData *p; |
| |
| assert(t); |
| assert(type >= 0); |
| assert(type < _TABLE_DATA_TYPE_MAX); |
| |
| |
| if (t->n_cells >= t->n_columns) |
| assert_se(p = t->data[t->n_cells - t->n_columns]); |
| else |
| p = NULL; |
| |
| |
| if (minimum_width == (size_t) -1) |
| minimum_width = p ? p->minimum_width : 1; |
| |
| if (weight == (unsigned) -1) |
| weight = p ? p->weight : DEFAULT_WEIGHT; |
| |
| if (align_percent == (unsigned) -1) |
| align_percent = p ? p->align_percent : 0; |
| |
| if (ellipsize_percent == (unsigned) -1) |
| ellipsize_percent = p ? p->ellipsize_percent : 100; |
| |
| assert(align_percent <= 100); |
| assert(ellipsize_percent <= 100); |
| |
| |
| |
| |
| if (p && table_data_matches(p, type, data, minimum_width, maximum_width, weight, align_percent, ellipsize_percent)) |
| d = table_data_ref(p); |
| else { |
| d = table_data_new(type, data, minimum_width, maximum_width, weight, align_percent, ellipsize_percent); |
| if (!d) |
| return -ENOMEM; |
| } |
| |
| if (!GREEDY_REALLOC(t->data, t->n_allocated, MAX(t->n_cells + 1, t->n_columns))) |
| return -ENOMEM; |
| |
| if (ret_cell) |
| *ret_cell = TABLE_INDEX_TO_CELL(t->n_cells); |
| |
| t->data[t->n_cells++] = TAKE_PTR(d); |
| |
| return 0; |
| } |
| |
| int table_dup_cell(Table *t, TableCell *cell) { |
| size_t i; |
| |
| assert(t); |
| |
| |
| |
| i = TABLE_CELL_TO_INDEX(cell); |
| if (i >= t->n_cells) |
| return -ENXIO; |
| |
| if (!GREEDY_REALLOC(t->data, t->n_allocated, MAX(t->n_cells + 1, t->n_columns))) |
| return -ENOMEM; |
| |
| t->data[t->n_cells++] = table_data_ref(t->data[i]); |
| return 0; |
| } |
| |
| static int table_dedup_cell(Table *t, TableCell *cell) { |
| _cleanup_free_ char *curl = NULL; |
| TableData *nd, *od; |
| size_t i; |
| |
| assert(t); |
| |
| |
| |
| |
| i = TABLE_CELL_TO_INDEX(cell); |
| if (i >= t->n_cells) |
| return -ENXIO; |
| |
| assert_se(od = t->data[i]); |
| if (od->n_ref == 1) |
| return 0; |
| |
| assert(od->n_ref > 1); |
| |
| if (od->url) { |
| curl = strdup(od->url); |
| if (!curl) |
| return -ENOMEM; |
| } |
| |
| nd = table_data_new( |
| od->type, |
| od->data, |
| od->minimum_width, |
| od->maximum_width, |
| od->weight, |
| od->align_percent, |
| od->ellipsize_percent); |
| if (!nd) |
| return -ENOMEM; |
| |
| nd->color = od->color; |
| nd->url = TAKE_PTR(curl); |
| nd->uppercase = od->uppercase; |
| |
| table_data_unref(od); |
| t->data[i] = nd; |
| |
| assert(nd->n_ref == 1); |
| |
| return 1; |
| } |
| |
| static TableData *table_get_data(Table *t, TableCell *cell) { |
| size_t i; |
| |
| assert(t); |
| assert(cell); |
| |
| |
| |
| i = TABLE_CELL_TO_INDEX(cell); |
| if (i >= t->n_cells) |
| return NULL; |
| |
| assert(t->data[i]); |
| assert(t->data[i]->n_ref > 0); |
| |
| return t->data[i]; |
| } |
| |
| int table_set_minimum_width(Table *t, TableCell *cell, size_t minimum_width) { |
| int r; |
| |
| assert(t); |
| assert(cell); |
| |
| if (minimum_width == (size_t) -1) |
| minimum_width = 1; |
| |
| r = table_dedup_cell(t, cell); |
| if (r < 0) |
| return r; |
| |
| table_get_data(t, cell)->minimum_width = minimum_width; |
| return 0; |
| } |
| |
| int table_set_maximum_width(Table *t, TableCell *cell, size_t maximum_width) { |
| int r; |
| |
| assert(t); |
| assert(cell); |
| |
| r = table_dedup_cell(t, cell); |
| if (r < 0) |
| return r; |
| |
| table_get_data(t, cell)->maximum_width = maximum_width; |
| return 0; |
| } |
| |
| int table_set_weight(Table *t, TableCell *cell, unsigned weight) { |
| int r; |
| |
| assert(t); |
| assert(cell); |
| |
| if (weight == (unsigned) -1) |
| weight = DEFAULT_WEIGHT; |
| |
| r = table_dedup_cell(t, cell); |
| if (r < 0) |
| return r; |
| |
| table_get_data(t, cell)->weight = weight; |
| return 0; |
| } |
| |
| int table_set_align_percent(Table *t, TableCell *cell, unsigned percent) { |
| int r; |
| |
| assert(t); |
| assert(cell); |
| |
| if (percent == (unsigned) -1) |
| percent = 0; |
| |
| assert(percent <= 100); |
| |
| r = table_dedup_cell(t, cell); |
| if (r < 0) |
| return r; |
| |
| table_get_data(t, cell)->align_percent = percent; |
| return 0; |
| } |
| |
| int table_set_ellipsize_percent(Table *t, TableCell *cell, unsigned percent) { |
| int r; |
| |
| assert(t); |
| assert(cell); |
| |
| if (percent == (unsigned) -1) |
| percent = 100; |
| |
| assert(percent <= 100); |
| |
| r = table_dedup_cell(t, cell); |
| if (r < 0) |
| return r; |
| |
| table_get_data(t, cell)->ellipsize_percent = percent; |
| return 0; |
| } |
| |
| int table_set_color(Table *t, TableCell *cell, const char *color) { |
| int r; |
| |
| assert(t); |
| assert(cell); |
| |
| r = table_dedup_cell(t, cell); |
| if (r < 0) |
| return r; |
| |
| table_get_data(t, cell)->color = empty_to_null(color); |
| return 0; |
| } |
| |
| int table_set_url(Table *t, TableCell *cell, const char *url) { |
| _cleanup_free_ char *copy = NULL; |
| int r; |
| |
| assert(t); |
| assert(cell); |
| |
| if (url) { |
| copy = strdup(url); |
| if (!copy) |
| return -ENOMEM; |
| } |
| |
| r = table_dedup_cell(t, cell); |
| if (r < 0) |
| return r; |
| |
| return free_and_replace(table_get_data(t, cell)->url, copy); |
| } |
| |
| int table_set_uppercase(Table *t, TableCell *cell, bool b) { |
| TableData *d; |
| int r; |
| |
| assert(t); |
| assert(cell); |
| |
| r = table_dedup_cell(t, cell); |
| if (r < 0) |
| return r; |
| |
| assert_se(d = table_get_data(t, cell)); |
| |
| if (d->uppercase == b) |
| return 0; |
| |
| d->formatted = mfree(d->formatted); |
| d->uppercase = b; |
| return 1; |
| } |
| |
| int table_update(Table *t, TableCell *cell, TableDataType type, const void *data) { |
| _cleanup_free_ char *curl = NULL; |
| TableData *nd, *od; |
| size_t i; |
| |
| assert(t); |
| assert(cell); |
| |
| i = TABLE_CELL_TO_INDEX(cell); |
| if (i >= t->n_cells) |
| return -ENXIO; |
| |
| assert_se(od = t->data[i]); |
| |
| if (od->url) { |
| curl = strdup(od->url); |
| if (!curl) |
| return -ENOMEM; |
| } |
| |
| nd = table_data_new( |
| type, |
| data, |
| od->minimum_width, |
| od->maximum_width, |
| od->weight, |
| od->align_percent, |
| od->ellipsize_percent); |
| if (!nd) |
| return -ENOMEM; |
| |
| nd->color = od->color; |
| nd->url = TAKE_PTR(curl); |
| nd->uppercase = od->uppercase; |
| |
| table_data_unref(od); |
| t->data[i] = nd; |
| |
| return 0; |
| } |
| |
| int table_add_many_internal(Table *t, TableDataType first_type, ...) { |
| TableDataType type; |
| va_list ap; |
| int r; |
| |
| assert(t); |
| assert(first_type >= 0); |
| assert(first_type < _TABLE_DATA_TYPE_MAX); |
| |
| type = first_type; |
| |
| va_start(ap, first_type); |
| for (;;) { |
| const void *data; |
| union { |
| uint64_t size; |
| usec_t usec; |
| uint32_t uint32; |
| uint64_t uint64; |
| int percent; |
| bool b; |
| } buffer; |
| |
| switch (type) { |
| |
| case TABLE_EMPTY: |
| data = NULL; |
| break; |
| |
| case TABLE_STRING: |
| data = va_arg(ap, const char *); |
| break; |
| |
| case TABLE_BOOLEAN: |
| buffer.b = va_arg(ap, int); |
| data = &buffer.b; |
| break; |
| |
| case TABLE_TIMESTAMP: |
| case TABLE_TIMESPAN: |
| buffer.usec = va_arg(ap, usec_t); |
| data = &buffer.usec; |
| break; |
| |
| case TABLE_SIZE: |
| buffer.size = va_arg(ap, uint64_t); |
| data = &buffer.size; |
| break; |
| |
| case TABLE_UINT32: |
| buffer.uint32 = va_arg(ap, uint32_t); |
| data = &buffer.uint32; |
| break; |
| |
| case TABLE_UINT64: |
| buffer.uint64 = va_arg(ap, uint64_t); |
| data = &buffer.uint64; |
| break; |
| |
| case TABLE_PERCENT: |
| buffer.percent = va_arg(ap, int); |
| data = &buffer.percent; |
| break; |
| |
| case _TABLE_DATA_TYPE_MAX: |
| |
| va_end(ap); |
| return 0; |
| |
| default: |
| assert_not_reached("Uh? Unexpected data type."); |
| } |
| |
| r = table_add_cell(t, NULL, type, data); |
| if (r < 0) { |
| va_end(ap); |
| return r; |
| } |
| |
| type = va_arg(ap, TableDataType); |
| } |
| } |
| |
| void table_set_header(Table *t, bool b) { |
| assert(t); |
| |
| t->header = b; |
| } |
| |
| void table_set_width(Table *t, size_t width) { |
| assert(t); |
| |
| t->width = width; |
| } |
| |
| int table_set_display(Table *t, size_t first_column, ...) { |
| size_t allocated, column; |
| va_list ap; |
| |
| assert(t); |
| |
| allocated = t->n_display_map; |
| column = first_column; |
| |
| va_start(ap, first_column); |
| for (;;) { |
| assert(column < t->n_columns); |
| |
| if (!GREEDY_REALLOC(t->display_map, allocated, MAX(t->n_columns, t->n_display_map+1))) { |
| va_end(ap); |
| return -ENOMEM; |
| } |
| |
| t->display_map[t->n_display_map++] = column; |
| |
| column = va_arg(ap, size_t); |
| if (column == (size_t) -1) |
| break; |
| |
| } |
| va_end(ap); |
| |
| return 0; |
| } |
| |
| int table_set_sort(Table *t, size_t first_column, ...) { |
| size_t allocated, column; |
| va_list ap; |
| |
| assert(t); |
| |
| allocated = t->n_sort_map; |
| column = first_column; |
| |
| va_start(ap, first_column); |
| for (;;) { |
| assert(column < t->n_columns); |
| |
| if (!GREEDY_REALLOC(t->sort_map, allocated, MAX(t->n_columns, t->n_sort_map+1))) { |
| va_end(ap); |
| return -ENOMEM; |
| } |
| |
| t->sort_map[t->n_sort_map++] = column; |
| |
| column = va_arg(ap, size_t); |
| if (column == (size_t) -1) |
| break; |
| } |
| va_end(ap); |
| |
| return 0; |
| } |
| |
| static int cell_data_compare(TableData *a, size_t index_a, TableData *b, size_t index_b) { |
| assert(a); |
| assert(b); |
| |
| if (a->type == b->type) { |
| |
| |
| |
| |
| switch (a->type) { |
| |
| case TABLE_STRING: |
| return strcmp(a->string, b->string); |
| |
| case TABLE_BOOLEAN: |
| if (!a->boolean && b->boolean) |
| return -1; |
| if (a->boolean && !b->boolean) |
| return 1; |
| return 0; |
| |
| case TABLE_TIMESTAMP: |
| if (a->timestamp < b->timestamp) |
| return -1; |
| if (a->timestamp > b->timestamp) |
| return 1; |
| return 0; |
| |
| case TABLE_TIMESPAN: |
| if (a->timespan < b->timespan) |
| return -1; |
| if (a->timespan > b->timespan) |
| return 1; |
| return 0; |
| |
| case TABLE_SIZE: |
| if (a->size < b->size) |
| return -1; |
| if (a->size > b->size) |
| return 1; |
| return 0; |
| |
| case TABLE_UINT32: |
| if (a->uint32 < b->uint32) |
| return -1; |
| if (a->uint32 > b->uint32) |
| return 1; |
| return 0; |
| |
| case TABLE_UINT64: |
| return CMP(a->uint64, b->uint64); |
| |
| case TABLE_PERCENT: |
| return CMP(a->percent, b->percent); |
| |
| default: |
| ; |
| } |
| } |
| |
| |
| if (index_a < index_b) |
| return -1; |
| if (index_a > index_b) |
| return 1; |
| |
| return 0; |
| } |
| |
| static int table_data_compare(const void *x, const void *y, void *userdata) { |
| const size_t *a = x, *b = y; |
| Table *t = userdata; |
| size_t i; |
| int r; |
| |
| assert(t); |
| assert(t->sort_map); |
| |
| |
| if (*a < t->n_columns && *b < t->n_columns) |
| return 0; |
| if (*a < t->n_columns) |
| return -1; |
| if (*b < t->n_columns) |
| return 1; |
| |
| |
| for (i = 0; i < t->n_sort_map; i++) { |
| TableData *d, *dd; |
| |
| d = t->data[*a + t->sort_map[i]]; |
| dd = t->data[*b + t->sort_map[i]]; |
| |
| r = cell_data_compare(d, *a, dd, *b); |
| if (r != 0) |
| return t->reverse_map && t->reverse_map[t->sort_map[i]] ? -r : r; |
| } |
| |
| |
| if (*a < *b) |
| return -1; |
| if (*a > *b) |
| return 1; |
| |
| return 0; |
| } |
| |
| static const char *table_data_format(TableData *d) { |
| assert(d); |
| |
| if (d->formatted) |
| return d->formatted; |
| |
| switch (d->type) { |
| case TABLE_EMPTY: |
| return ""; |
| |
| case TABLE_STRING: |
| if (d->uppercase) { |
| char *p, *q; |
| |
| d->formatted = new(char, strlen(d->string) + 1); |
| if (!d->formatted) |
| return NULL; |
| |
| for (p = d->string, q = d->formatted; *p; p++, q++) |
| *q = (char) toupper((unsigned char) *p); |
| *q = 0; |
| |
| return d->formatted; |
| } |
| |
| return d->string; |
| |
| case TABLE_BOOLEAN: |
| return yes_no(d->boolean); |
| |
| case TABLE_TIMESTAMP: { |
| _cleanup_free_ char *p; |
| |
| p = new(char, FORMAT_TIMESTAMP_MAX); |
| if (!p) |
| return NULL; |
| |
| if (!format_timestamp(p, FORMAT_TIMESTAMP_MAX, d->timestamp)) |
| return "n/a"; |
| |
| d->formatted = TAKE_PTR(p); |
| break; |
| } |
| |
| case TABLE_TIMESPAN: { |
| _cleanup_free_ char *p; |
| |
| p = new(char, FORMAT_TIMESPAN_MAX); |
| if (!p) |
| return NULL; |
| |
| if (!format_timespan(p, FORMAT_TIMESPAN_MAX, d->timespan, 0)) |
| return "n/a"; |
| |
| d->formatted = TAKE_PTR(p); |
| break; |
| } |
| |
| case TABLE_SIZE: { |
| _cleanup_free_ char *p; |
| |
| p = new(char, FORMAT_BYTES_MAX); |
| if (!p) |
| return NULL; |
| |
| if (!format_bytes(p, FORMAT_BYTES_MAX, d->size)) |
| return "n/a"; |
| |
| d->formatted = TAKE_PTR(p); |
| break; |
| } |
| |
| case TABLE_UINT32: { |
| _cleanup_free_ char *p; |
| |
| p = new(char, DECIMAL_STR_WIDTH(d->uint32) + 1); |
| if (!p) |
| return NULL; |
| |
| sprintf(p, "%" PRIu32, d->uint32); |
| d->formatted = TAKE_PTR(p); |
| break; |
| } |
| |
| case TABLE_UINT64: { |
| _cleanup_free_ char *p; |
| |
| p = new(char, DECIMAL_STR_WIDTH(d->uint64) + 1); |
| if (!p) |
| return NULL; |
| |
| sprintf(p, "%" PRIu64, d->uint64); |
| d->formatted = TAKE_PTR(p); |
| break; |
| } |
| |
| case TABLE_PERCENT: { |
| _cleanup_free_ char *p; |
| |
| p = new(char, DECIMAL_STR_WIDTH(d->percent) + 2); |
| if (!p) |
| return NULL; |
| |
| sprintf(p, "%i%%" , d->percent); |
| d->formatted = TAKE_PTR(p); |
| break; |
| } |
| |
| default: |
| assert_not_reached("Unexpected type?"); |
| } |
| |
| return d->formatted; |
| } |
| |
| static int table_data_requested_width(TableData *d, size_t *ret) { |
| const char *t; |
| size_t l; |
| |
| t = table_data_format(d); |
| if (!t) |
| return -ENOMEM; |
| |
| l = utf8_console_width(t); |
| if (l == (size_t) -1) |
| return -EINVAL; |
| |
| if (d->maximum_width != (size_t) -1 && l > d->maximum_width) |
| l = d->maximum_width; |
| |
| if (l < d->minimum_width) |
| l = d->minimum_width; |
| |
| *ret = l; |
| return 0; |
| } |
| |
| static char *align_string_mem(const char *str, const char *url, size_t new_length, unsigned percent) { |
| size_t w = 0, space, lspace, old_length, clickable_length; |
| _cleanup_free_ char *clickable = NULL; |
| const char *p; |
| char *ret; |
| size_t i; |
| int r; |
| |
| |
| |
| assert(str); |
| assert(percent <= 100); |
| |
| old_length = strlen(str); |
| |
| if (url) { |
| r = terminal_urlify(url, str, &clickable); |
| if (r < 0) |
| return NULL; |
| |
| clickable_length = strlen(clickable); |
| } else |
| clickable_length = old_length; |
| |
| |
| p = str; |
| while (p < str + old_length) { |
| char32_t c; |
| |
| if (utf8_encoded_to_unichar(p, &c) < 0) { |
| p++, w++; |
| continue; |
| } |
| |
| p = utf8_next_char(p); |
| w += unichar_iswide(c) ? 2 : 1; |
| } |
| |
| |
| if (w >= new_length) |
| return clickable ? TAKE_PTR(clickable) : strdup(str); |
| |
| |
| space = new_length - w; |
| lspace = space * percent / 100U; |
| |
| ret = new(char, space + clickable_length + 1); |
| if (!ret) |
| return NULL; |
| |
| for (i = 0; i < lspace; i++) |
| ret[i] = ' '; |
| memcpy(ret + lspace, clickable ?: str, clickable_length); |
| for (i = lspace + clickable_length; i < space + clickable_length; i++) |
| ret[i] = ' '; |
| |
| ret[space + clickable_length] = 0; |
| return ret; |
| } |
| |
| int table_print(Table *t, FILE *f) { |
| size_t n_rows, *minimum_width, *maximum_width, display_columns, *requested_width, |
| i, j, table_minimum_width, table_maximum_width, table_requested_width, table_effective_width, |
| *width; |
| _cleanup_free_ size_t *sorted = NULL; |
| uint64_t *column_weight, weight_sum; |
| int r; |
| |
| assert(t); |
| |
| if (!f) |
| f = stdout; |
| |
| |
| assert(t->n_cells % t->n_columns == 0); |
| |
| n_rows = t->n_cells / t->n_columns; |
| assert(n_rows > 0); |
| |
| if (t->sort_map) { |
| |
| |
| sorted = new(size_t, n_rows); |
| if (!sorted) |
| return -ENOMEM; |
| |
| for (i = 0; i < n_rows; i++) |
| sorted[i] = i * t->n_columns; |
| |
| qsort_r_safe(sorted, n_rows, sizeof(size_t), table_data_compare, t); |
| } |
| |
| if (t->display_map) |
| display_columns = t->n_display_map; |
| else |
| display_columns = t->n_columns; |
| |
| assert(display_columns > 0); |
| |
| minimum_width = newa(size_t, display_columns); |
| maximum_width = newa(size_t, display_columns); |
| requested_width = newa(size_t, display_columns); |
| width = newa(size_t, display_columns); |
| column_weight = newa0(uint64_t, display_columns); |
| |
| for (j = 0; j < display_columns; j++) { |
| minimum_width[j] = 1; |
| maximum_width[j] = (size_t) -1; |
| requested_width[j] = (size_t) -1; |
| } |
| |
| |
| for (i = t->header ? 0 : 1; i < n_rows; i++) { |
| TableData **row; |
| |
| |
| |
| row = t->data + i * t->n_columns; |
| |
| for (j = 0; j < display_columns; j++) { |
| TableData *d; |
| size_t req; |
| |
| assert_se(d = row[t->display_map ? t->display_map[j] : j]); |
| |
| r = table_data_requested_width(d, &req); |
| if (r < 0) |
| return r; |
| |
| |
| if (requested_width[j] == (size_t) -1 || |
| requested_width[j] < req) |
| requested_width[j] = req; |
| |
| |
| if (minimum_width[j] < d->minimum_width) |
| minimum_width[j] = d->minimum_width; |
| |
| |
| if (d->maximum_width != (size_t) -1 && |
| (maximum_width[j] == (size_t) -1 || |
| maximum_width[j] > d->maximum_width)) |
| maximum_width[j] = d->maximum_width; |
| |
| |
| column_weight[j] += d->weight; |
| } |
| } |
| |
| |
| table_requested_width = table_minimum_width = table_maximum_width = display_columns - 1; |
| |
| |
| weight_sum = 0; |
| for (j = 0; j < display_columns; j++) { |
| weight_sum += column_weight[j]; |
| |
| table_minimum_width += minimum_width[j]; |
| |
| if (maximum_width[j] == (size_t) -1) |
| table_maximum_width = (size_t) -1; |
| else |
| table_maximum_width += maximum_width[j]; |
| |
| table_requested_width += requested_width[j]; |
| } |
| |
| |
| if (t->width == (size_t) -1) |
| table_effective_width = pager_have() ? table_requested_width : MIN(table_requested_width, columns()); |
| else |
| table_effective_width = t->width; |
| |
| if (table_maximum_width != (size_t) -1 && table_effective_width > table_maximum_width) |
| table_effective_width = table_maximum_width; |
| |
| if (table_effective_width < table_minimum_width) |
| table_effective_width = table_minimum_width; |
| |
| if (table_effective_width >= table_requested_width) { |
| size_t extra; |
| |
| |
| |
| |
| extra = table_effective_width - table_requested_width; |
| |
| for (j = 0; j < display_columns; j++) { |
| size_t delta; |
| |
| if (weight_sum == 0) |
| width[j] = requested_width[j] + extra / (display_columns - j); |
| else |
| width[j] = requested_width[j] + (extra * column_weight[j]) / weight_sum; |
| |
| if (maximum_width[j] != (size_t) -1 && width[j] > maximum_width[j]) |
| width[j] = maximum_width[j]; |
| |
| if (width[j] < minimum_width[j]) |
| width[j] = minimum_width[j]; |
| |
| assert(width[j] >= requested_width[j]); |
| delta = width[j] - requested_width[j]; |
| |
| |
| if (extra > delta) |
| extra -= delta; |
| else |
| extra = 0; |
| |
| assert(weight_sum >= column_weight[j]); |
| weight_sum -= column_weight[j]; |
| } |
| |
| } else { |
| |
| |
| bool finalize = false; |
| size_t extra; |
| |
| extra = table_effective_width - table_minimum_width; |
| |
| for (j = 0; j < display_columns; j++) |
| width[j] = (size_t) -1; |
| |
| for (;;) { |
| bool restart = false; |
| |
| for (j = 0; j < display_columns; j++) { |
| size_t delta, w; |
| |
| |
| if (width[j] != (size_t) -1) |
| continue; |
| |
| if (weight_sum == 0) |
| w = minimum_width[j] + extra / (display_columns - j); |
| else |
| w = minimum_width[j] + (extra * column_weight[j]) / weight_sum; |
| |
| if (w >= requested_width[j]) { |
| |
| |
| |
| |
| |
| w = requested_width[j]; |
| restart = true; |
| |
| } else if (!finalize) |
| continue; |
| |
| width[j] = w; |
| |
| assert(w >= minimum_width[j]); |
| delta = w - minimum_width[j]; |
| |
| assert(delta <= extra); |
| extra -= delta; |
| |
| assert(weight_sum >= column_weight[j]); |
| weight_sum -= column_weight[j]; |
| |
| if (restart) |
| break; |
| } |
| |
| if (finalize) { |
| assert(!restart); |
| break; |
| } |
| |
| if (!restart) |
| finalize = true; |
| } |
| } |
| |
| |
| for (i = t->header ? 0 : 1; i < n_rows; i++) { |
| TableData **row; |
| |
| if (sorted) |
| row = t->data + sorted[i]; |
| else |
| row = t->data + i * t->n_columns; |
| |
| for (j = 0; j < display_columns; j++) { |
| _cleanup_free_ char *buffer = NULL; |
| const char *field; |
| TableData *d; |
| size_t l; |
| |
| assert_se(d = row[t->display_map ? t->display_map[j] : j]); |
| |
| field = table_data_format(d); |
| if (!field) |
| return -ENOMEM; |
| |
| l = utf8_console_width(field); |
| if (l > width[j]) { |
| |
| |
| buffer = ellipsize(field, width[j], d->ellipsize_percent); |
| if (!buffer) |
| return -ENOMEM; |
| |
| field = buffer; |
| |
| } else if (l < width[j]) { |
| |
| |
| buffer = align_string_mem(field, d->url, width[j], d->align_percent); |
| if (!buffer) |
| return -ENOMEM; |
| |
| field = buffer; |
| } |
| |
| if (l >= width[j] && d->url) { |
| _cleanup_free_ char *clickable = NULL; |
| |
| r = terminal_urlify(d->url, field, &clickable); |
| if (r < 0) |
| return r; |
| |
| free_and_replace(buffer, clickable); |
| field = buffer; |
| } |
| |
| if (row == t->data) |
| fputs(ansi_underline(), f); |
| |
| if (j > 0) |
| fputc(' ', f); |
| |
| if (d->color && colors_enabled()) { |
| if (row == t->data) |
| fputs(ANSI_NORMAL, f); |
| |
| fputs(d->color, f); |
| } |
| |
| fputs(field, f); |
| |
| if (colors_enabled() && (d->color || row == t->data)) |
| fputs(ANSI_NORMAL, f); |
| } |
| |
| fputc('\n', f); |
| } |
| |
| return fflush_and_check(f); |
| } |
| |
| int table_format(Table *t, char **ret) { |
| _cleanup_fclose_ FILE *f = NULL; |
| char *buf = NULL; |
| size_t sz = 0; |
| int r; |
| |
| f = open_memstream(&buf, &sz); |
| if (!f) |
| return -ENOMEM; |
| |
| (void) __fsetlocking(f, FSETLOCKING_BYCALLER); |
| |
| r = table_print(t, f); |
| if (r < 0) |
| return r; |
| |
| f = safe_fclose(f); |
| |
| *ret = buf; |
| |
| return 0; |
| } |
| |
| size_t table_get_rows(Table *t) { |
| if (!t) |
| return 0; |
| |
| assert(t->n_columns > 0); |
| return t->n_cells / t->n_columns; |
| } |
| |
| size_t table_get_columns(Table *t) { |
| if (!t) |
| return 0; |
| |
| assert(t->n_columns > 0); |
| return t->n_columns; |
| } |
| |
| int table_set_reverse(Table *t, size_t column, bool b) { |
| assert(t); |
| assert(column < t->n_columns); |
| |
| if (!t->reverse_map) { |
| if (!b) |
| return 0; |
| |
| t->reverse_map = new0(bool, t->n_columns); |
| if (!t->reverse_map) |
| return -ENOMEM; |
| } |
| |
| t->reverse_map[column] = b; |
| return 0; |
| } |
| |
| TableCell *table_get_cell(Table *t, size_t row, size_t column) { |
| size_t i; |
| |
| assert(t); |
| |
| if (column >= t->n_columns) |
| return NULL; |
| |
| i = row * t->n_columns + column; |
| if (i >= t->n_cells) |
| return NULL; |
| |
| return TABLE_INDEX_TO_CELL(i); |
| } |
| |
| const void *table_get(Table *t, TableCell *cell) { |
| TableData *d; |
| |
| assert(t); |
| |
| d = table_get_data(t, cell); |
| if (!d) |
| return NULL; |
| |
| return d->data; |
| } |
| |
| const void* table_get_at(Table *t, size_t row, size_t column) { |
| TableCell *cell; |
| |
| cell = table_get_cell(t, row, column); |
| if (!cell) |
| return NULL; |
| |
| return table_get(t, cell); |
| } |