/* vi:set et sw=2 sts=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e-s: * Copyright © 2014 Red Hat, Inc * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * * Authors: * Alexander Larsson */ #include "config.h" #include #include "flatpak-table-printer.h" #include "flatpak-tty-utils-private.h" #include "flatpak-utils-private.h" #include #include #include #include typedef struct { char *text; int align; gboolean span; } Cell; static void free_cell (gpointer data) { Cell *cell = data; g_free (cell->text); g_free (cell); } typedef struct { GPtrArray *cells; char *key; } Row; static void free_row (gpointer data) { Row *row = data; g_ptr_array_free (row->cells, TRUE); g_free (row->key); g_free (row); } typedef struct { char *title; gboolean expand; FlatpakEllipsizeMode ellipsize; gboolean skip_unique; char *skip_unique_str; gboolean skip; } TableColumn; static void free_column (gpointer data) { TableColumn *column = data; g_free (column->title); g_free (column->skip_unique_str); g_free (column); } struct FlatpakTablePrinter { GPtrArray *columns; GPtrArray *rows; GHashTable *rows_ht; char *key; GPtrArray *current; int n_columns; }; FlatpakTablePrinter * flatpak_table_printer_new (void) { FlatpakTablePrinter *printer = g_new0 (FlatpakTablePrinter, 1); printer->columns = g_ptr_array_new_with_free_func (free_column); printer->rows = g_ptr_array_new_with_free_func ((GDestroyNotify) free_row); printer->rows_ht = g_hash_table_new (g_str_hash, g_str_equal); printer->current = g_ptr_array_new_with_free_func (free_cell); return printer; } void flatpak_table_printer_free (FlatpakTablePrinter *printer) { g_ptr_array_free (printer->columns, TRUE); g_ptr_array_free (printer->rows, TRUE); g_hash_table_destroy (printer->rows_ht); g_ptr_array_free (printer->current, TRUE); g_free (printer->key); g_free (printer); } static TableColumn * peek_table_column (FlatpakTablePrinter *printer, int column) { if (column < printer->columns->len) return g_ptr_array_index (printer->columns, column); return NULL; } static TableColumn * get_table_column (FlatpakTablePrinter *printer, int column) { TableColumn *col = NULL; if (column < printer->columns->len) col = g_ptr_array_index (printer->columns, column); if (col == NULL) { col = g_new0 (TableColumn, 1); g_ptr_array_insert (printer->columns, column, col); } return col; } void flatpak_table_printer_set_column_title (FlatpakTablePrinter *printer, int column, const char *text) { TableColumn *col = get_table_column (printer, column); col->title = g_strdup (text); } void flatpak_table_printer_set_columns (FlatpakTablePrinter *printer, Column *columns, gboolean defaults) { int i; for (i = 0; columns[i].name; i++) { flatpak_table_printer_set_column_title (printer, i, _(columns[i].title)); flatpak_table_printer_set_column_expand (printer, i, columns[i].expand); flatpak_table_printer_set_column_ellipsize (printer, i, columns[i].ellipsize); if (defaults && columns[i].skip_unique_if_default) flatpak_table_printer_set_column_skip_unique (printer, i, TRUE); } } void flatpak_table_printer_add_aligned_column (FlatpakTablePrinter *printer, const char *text, int align) { Cell *cell = g_new0 (Cell, 1); cell->text = text ? g_strdup (text) : g_strdup (""); cell->align = align; g_ptr_array_add (printer->current, cell); } void flatpak_table_printer_add_span (FlatpakTablePrinter *printer, const char *text) { Cell *cell = g_new0 (Cell, 1); cell->text = text ? g_strdup (text) : g_strdup (""); cell->align = -1; cell->span = TRUE; g_ptr_array_add (printer->current, cell); } static const char * find_decimal_point (const char *text) { struct lconv *locale_data; locale_data = localeconv (); return strstr (text, locale_data->decimal_point); } void flatpak_table_printer_add_decimal_column (FlatpakTablePrinter *printer, const char *text) { const char *decimal; int align = -1; decimal = find_decimal_point (text); if (decimal) align = decimal - text; flatpak_table_printer_add_aligned_column (printer, text, align); } void flatpak_table_printer_add_column (FlatpakTablePrinter *printer, const char *text) { flatpak_table_printer_add_aligned_column (printer, text, -1); } void flatpak_table_printer_take_column (FlatpakTablePrinter *printer, char *text) { flatpak_table_printer_add_aligned_column (printer, text, -1); g_free (text); } void flatpak_table_printer_add_column_len (FlatpakTablePrinter *printer, const char *text, gsize len) { Cell *cell = g_new0 (Cell, 1); cell->text = text ? g_strndup (text, len) : g_strdup (""); cell->align = -1; g_ptr_array_add (printer->current, cell); } void flatpak_table_printer_append_with_comma (FlatpakTablePrinter *printer, const char *text) { Cell *cell; char *new; g_assert (printer->current->len > 0); cell = g_ptr_array_index (printer->current, printer->current->len - 1); if (cell->text[0] != 0) new = g_strconcat (cell->text, ",", text, NULL); else new = g_strdup (text); g_free (cell->text); cell->text = new; } void flatpak_table_printer_append_with_comma_printf (FlatpakTablePrinter *printer, const char *format, ...) { va_list var_args; g_autofree char *s = NULL; va_start (var_args, format); s = g_strdup_vprintf (format, var_args); va_end (var_args); flatpak_table_printer_append_with_comma (printer, s); } void flatpak_table_printer_set_key (FlatpakTablePrinter *printer, const char *key) { printer->key = g_strdup (key); } static gint cmp_row (gconstpointer _row_a, gconstpointer _row_b, gpointer user_data) { const Row *row_a = *(const Row **) _row_a; const Row *row_b = *(const Row **) _row_b; GCompareFunc cmp = user_data; if (row_a == row_b || (row_a->key == NULL && row_b->key == NULL)) return 0; if (row_a->key == NULL) return -1; if (row_b->key == NULL) return 1; return cmp (row_a->key, row_b->key); } void flatpak_table_printer_sort (FlatpakTablePrinter *printer, GCompareFunc cmp) { g_ptr_array_sort_with_data (printer->rows, cmp_row, cmp); } int flatpak_table_printer_lookup_row (FlatpakTablePrinter *printer, const char *key) { gpointer value; if (g_hash_table_lookup_extended (printer->rows_ht, key, NULL, &value)) return GPOINTER_TO_INT(value); return -1; } void flatpak_table_printer_finish_row (FlatpakTablePrinter *printer) { Row *row; int row_nr = flatpak_table_printer_get_current_row (printer); if (printer->current->len == 0) return; /* Ignore empty rows */ printer->n_columns = MAX (printer->n_columns, printer->current->len); row = g_new0 (Row, 1); row->cells = g_steal_pointer (&printer->current); row->key = g_steal_pointer (&printer->key); g_ptr_array_add (printer->rows, row); if (row->key) g_hash_table_insert (printer->rows_ht, row->key, GINT_TO_POINTER (row_nr)); printer->current = g_ptr_array_new_with_free_func (free_cell); } /* Return how many terminal rows we produced (with wrapping to columns) * while skipping 'skip' many of them. 'skip' is updated to reflect * how many we skipped. */ static int print_row (GString *row_s, gboolean bold, int *skip, int columns) { int rows; const char *p, *end; int n_chars; g_strchomp (row_s->str); n_chars = cell_width (row_s->str); if (n_chars > 0) rows = (n_chars + columns - 1) / columns; else rows = 1; p = row_s->str; end = row_s->str + strlen (row_s->str); while (*skip > 0 && p <= end) { (*skip)--; p = cell_advance (p, columns); } if (p < end || p == row_s->str) { if (bold) g_print (FLATPAK_ANSI_BOLD_ON "%s" FLATPAK_ANSI_BOLD_OFF, p); else g_print ("%s", p); } g_string_truncate (row_s, 0); return rows; } static void string_add_spaces (GString *str, int count) { while (count-- > 0) g_string_append_c (str, ' '); } static gboolean column_is_unique (FlatpakTablePrinter *printer, int col) { TableColumn *column = get_table_column (printer, col); char *first_row = column->skip_unique_str; int i; for (i = 0; i < printer->rows->len; i++) { Row *row = g_ptr_array_index (printer->rows, i); if (col >= row->cells->len) continue; Cell *cell = g_ptr_array_index (row->cells, col); if (i == 0 && first_row == NULL) first_row = cell->text; else { if (g_strcmp0 (first_row, cell->text) != 0) return FALSE; } } return TRUE; } /* * This variant of flatpak_table_printer_print() takes a window width * and returns the number of rows that are generated by printing the * table to that width. It also takes a number of (terminal) rows * to skip at the beginning of the table. * * Care is taken to do the right thing if the skipping ends * in the middle of a wrapped table row. * * Note that unlike flatpak_table_printer_print(), this function does * not add a newline after the last table row. */ void flatpak_table_printer_print_full (FlatpakTablePrinter *printer, int skip, int columns, int *table_height, int *table_width) { g_autofree int *widths = NULL; g_autofree int *lwidths = NULL; g_autofree int *rwidths = NULL; g_autofree int *shrinks = NULL; g_autoptr(GString) row_s = g_string_new (""); int i, j; int rows = 0; int total_skip = skip; int width; int expand_columns; int shrink_columns; gboolean has_title; int expand_by, expand_extra; if (printer->current->len != 0) flatpak_table_printer_finish_row (printer); widths = g_new0 (int, printer->n_columns); lwidths = g_new0 (int, printer->n_columns); rwidths = g_new0 (int, printer->n_columns); shrinks = g_new0 (int, printer->n_columns); for (i = 0; i < printer->columns->len && i < printer->n_columns; i++) { TableColumn *col = g_ptr_array_index (printer->columns, i); if (col->skip_unique && column_is_unique (printer, i)) col->skip = TRUE; } has_title = FALSE; for (i = 0; i < printer->columns->len && i < printer->n_columns; i++) { TableColumn *col = g_ptr_array_index (printer->columns, i); if (col->skip) continue; if (col->title) { widths[i] = MAX (widths[i], cell_width (col->title)); has_title = TRUE; } } for (i = 0; i < printer->rows->len; i++) { Row *row = g_ptr_array_index (printer->rows, i); for (j = 0; j < row->cells->len; j++) { Cell *cell = g_ptr_array_index (row->cells, j); TableColumn *col = peek_table_column (printer, j); if (col && col->skip) continue; if (cell->span) width = 0; else width = cell_width (cell->text); widths[j] = MAX (widths[j], width); if (cell->align >= 0) { lwidths[j] = MAX (lwidths[j], cell->align); rwidths[j] = MAX (rwidths[j], width - cell->align); } } } width = printer->n_columns - 1; for (i = 0; i < printer->n_columns; i++) width += widths[i]; expand_columns = 0; shrink_columns = 0; for (i = 0; i < printer->columns->len; i++) { TableColumn *col = g_ptr_array_index (printer->columns, i); if (col && col->skip) continue; if (col && col->expand) expand_columns++; if (col && col->ellipsize) shrink_columns++; } expand_by = 0; expand_extra = 0; if (expand_columns > 0) { int excess = CLAMP (columns - width, 0, width / 2); expand_by = excess / expand_columns; expand_extra = excess % expand_columns; width += excess; } if (shrink_columns > 0) { int shortfall = MAX (width - columns, 0); int last; if (shortfall > 0) { int shrinkable = 0; int leftover = shortfall; /* We're distributing the shortfall so that wider columns * shrink proportionally more than narrower ones, while * avoiding to ellipsize the titles. */ for (i = 0; i < printer->columns->len && i < printer->n_columns; i++) { TableColumn *col = g_ptr_array_index (printer->columns, i); gboolean ellipsize = col ? col->ellipsize : FALSE; if (col && col->skip) continue; if (!ellipsize) continue; if (col && col->title) shrinkable += MAX (0, widths[i] - cell_width (col->title)); else shrinkable += MAX (0, widths[i] - 5); } for (i = 0; i < printer->columns->len && i < printer->n_columns; i++) { TableColumn *col = g_ptr_array_index (printer->columns, i); gboolean ellipsize = col ? col->ellipsize : FALSE; if (col && col->skip) continue; if (ellipsize) { int sh; if (col && col->title) sh = MAX (0, widths[i] - cell_width (col->title)); else sh = MAX (0, widths[i] - 5); shrinks[i] = MIN (shortfall * (sh / (double) shrinkable), widths[i]); leftover -= shrinks[i]; } } last = leftover + 1; while (leftover > 0 && leftover < last) { last = leftover; for (i = 0; i < printer->columns->len && i < printer->n_columns; i++) { TableColumn *col = g_ptr_array_index (printer->columns, i); gboolean ellipsize = col ? col->ellipsize : FALSE; if (col && col->skip) continue; if (ellipsize && shrinks[i] < widths[i]) { shrinks[i]++; leftover--; } if (leftover == 0) break; } } } for (i = 0; i < printer->n_columns; i++) width -= shrinks[i]; } if (flatpak_fancy_output () && has_title) { int grow = expand_extra; for (i = 0; i < printer->columns->len && i < printer->n_columns; i++) { TableColumn *col = g_ptr_array_index (printer->columns, i); char *title = col && col->title ? col->title : ""; gboolean expand = col ? col->expand : FALSE; gboolean ellipsize = col ? col->ellipsize : FALSE; int len = widths[i]; g_autofree char *freeme = NULL; if (col && col->skip) continue; if (expand_by > 0 && expand) { len += expand_by; if (grow > 0) { len++; grow--; } } if (shrinks[i] > 0 && ellipsize) { len -= shrinks[i]; freeme = title = ellipsize_string (title, len); } if (i > 0) g_string_append_c (row_s, ' '); g_string_append (row_s, title); string_add_spaces (row_s, len - cell_width (title)); } rows += print_row (row_s, TRUE, &skip, columns); } for (i = 0; i < printer->rows->len; i++) { Row *row = g_ptr_array_index (printer->rows, i); int grow = expand_extra; if (rows > total_skip) g_print ("\n"); for (j = 0; j < row->cells->len; j++) { TableColumn *col = peek_table_column (printer, j); gboolean expand = col ? col->expand : FALSE; gboolean ellipsize = col ? col->ellipsize : FALSE; Cell *cell = g_ptr_array_index (row->cells, j); char *text = cell->text; int len = widths[j]; g_autofree char *freeme = NULL; if (col && col->skip) continue; if (expand_by > 0 && expand) { len += expand_by; if (grow > 0) { len++; grow--; } } if (shrinks[j] > 0 && ellipsize) { len -= shrinks[j]; freeme = text = ellipsize_string_full (text, len, col->ellipsize); } if (flatpak_fancy_output ()) { if (j > 0) g_string_append_c (row_s, ' '); if (cell->span) g_string_append (row_s, cell->text); else if (cell->align < 0) { g_string_append (row_s, text); string_add_spaces (row_s, len - cell_width (text)); } else { string_add_spaces (row_s, lwidths[j] - cell->align); g_string_append (row_s, text); string_add_spaces (row_s, widths[j] - (lwidths[j] - cell->align) - cell_width (text)); } } else g_string_append_printf (row_s, "%s%s", cell->text, (j < row->cells->len - 1) ? "\t" : ""); } rows += print_row (row_s, FALSE, &skip, columns); } if (table_width) *table_width = width; if (table_height) *table_height = rows; } void flatpak_table_printer_print (FlatpakTablePrinter *printer) { int rows, cols; flatpak_get_window_size (&rows, &cols); flatpak_table_printer_print_full (printer, 0, cols, NULL, NULL); g_print ("\n"); } int flatpak_table_printer_get_current_row (FlatpakTablePrinter *printer) { return printer->rows->len; } static void set_cell (FlatpakTablePrinter *printer, int r, int c, const char *text, int align, int append) { Row *row; Cell *cell; char *old; row = (Row *) g_ptr_array_index (printer->rows, r); g_assert (row); cell = (Cell *) g_ptr_array_index (row->cells, c); g_assert (cell); old = cell->text; if (old != NULL && append) { if (append == 2 && *old != 0) cell->text = g_strconcat (old, ", ", text, NULL); else cell->text = g_strconcat (old, text, NULL); } else cell->text = g_strdup (text); cell->align = align; g_free (old); } void flatpak_table_printer_set_cell (FlatpakTablePrinter *printer, int r, int c, const char *text) { set_cell (printer, r, c, text, -1, 0); } void flatpak_table_printer_append_cell (FlatpakTablePrinter *printer, int r, int c, const char *text) { set_cell (printer, r, c, text, -1, 1); } void flatpak_table_printer_append_cell_with_comma (FlatpakTablePrinter *printer, int r, int c, const char *text) { set_cell (printer, r, c, text, -1, 2); } void flatpak_table_printer_append_cell_with_comma_unique (FlatpakTablePrinter *printer, int r, int c, const char *text) { Row *row; Cell *cell; row = (Row *) g_ptr_array_index (printer->rows, r); g_assert (row); cell = (Cell *) g_ptr_array_index (row->cells, c); g_assert (cell); /* Look for existing text in comma separated text */ if (cell->text != NULL && *text != 0) { gsize len = strlen (text); const char *match = cell->text; while ((match = strstr (match, text)) != NULL) { if (match[len] == 0 || match[len] == ',' ) return; /* Already in string, do nothing */ /* Look for next match */ match = match + len; } } set_cell (printer, r, c, text, -1, 2); } void flatpak_table_printer_set_decimal_cell (FlatpakTablePrinter *printer, int r, int c, const char *text) { int align = -1; const char *decimal = find_decimal_point (text); if (decimal) align = decimal - text; set_cell (printer, r, c, text, align, 0); } void flatpak_table_printer_set_column_expand (FlatpakTablePrinter *printer, int column, gboolean expand) { TableColumn *col = get_table_column (printer, column); col->expand = expand; } void flatpak_table_printer_set_column_ellipsize (FlatpakTablePrinter *printer, int column, FlatpakEllipsizeMode mode) { TableColumn *col = get_table_column (printer, column); col->ellipsize = mode; } /* Specifies that the column should be skipped if all values are the same */ void flatpak_table_printer_set_column_skip_unique (FlatpakTablePrinter *printer, int column, gboolean skip_unique) { TableColumn *col = get_table_column (printer, column); col->skip_unique = skip_unique; } /* This modifies set_column_skip_unique to also require that the * unique value of the column must be this particular string. Useful if you * want to e.g. skip the arch list if everything is for the primary arch, but * not if everything is for a non-standard arch. */ void flatpak_table_printer_set_column_skip_unique_string (FlatpakTablePrinter *printer, int column, const char *str) { TableColumn *col = get_table_column (printer, column); g_assert (col->skip_unique_str == NULL); col->skip_unique_str = g_strdup (str); }