--- /dev/null
+/*#############################################################################
+# #
+# Pakfire - The IPFire package management system #
+# Copyright (C) 2025 Pakfire development team #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+#############################################################################*/
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/queue.h>
+
+#include <pakfire/string.h>
+
+#include "table.h"
+#include "terminal.h"
+
+typedef struct cli_table_col {
+ STAILQ_ENTRY(cli_table_col) nodes;
+
+ // Name
+ const char* name;
+
+ // Type
+ cli_table_col_type type;
+
+ // Alignment
+ cli_table_col_align align;
+
+ // Required Width
+ size_t width;
+} cli_table_col;
+
+typedef struct cli_table_row {
+ STAILQ_ENTRY(cli_table_row) nodes;
+
+ // Values for all cols
+ char** cols;
+} cli_table_row;
+
+struct cli_table {
+ // Terminal dimensions
+ struct {
+ int rows;
+ int cols;
+ } terminal;
+
+ // Columns
+ STAILQ_HEAD(cols, cli_table_col) cols;
+ unsigned int num_cols;
+
+ // Rows
+ STAILQ_HEAD(rows, cli_table_row) rows;
+};
+
+int cli_table_create(cli_table** table) {
+ cli_table* self = NULL;
+ int r;
+
+ // Allocate a new table
+ self = calloc(1, sizeof(*self));
+ if (!self)
+ return -errno;
+
+ // Initialize cols
+ STAILQ_INIT(&self->cols);
+
+ // Initialize rows
+ STAILQ_INIT(&self->rows);
+
+ // Fetch the terminal dimensions
+ r = cli_term_get_dimensions(&self->terminal.rows, &self->terminal.cols);
+ if (r < 0)
+ goto ERROR;
+
+ // Return the pointer
+ *table = self;
+ return 0;
+
+ERROR:
+ if (self)
+ cli_table_free(self);
+
+ return r;
+}
+
+static void cli_table_col_free(cli_table_col* col) {
+ free(col);
+}
+
+static void cli_table_row_free(cli_table_row* row) {
+ if (row->cols)
+ pakfire_strings_free(row->cols);
+ free(row);
+}
+
+void cli_table_free(cli_table* self) {
+ cli_table_col* col = NULL;
+ cli_table_row* row = NULL;
+
+ // Free all cols
+ while (!STAILQ_EMPTY(&self->cols)) {
+ col = STAILQ_FIRST(&self->cols);
+ if (!col)
+ break;
+
+ STAILQ_REMOVE_HEAD(&self->cols, nodes);
+
+ // Free the cols
+ cli_table_col_free(col);
+ }
+
+ // Free all rows
+ while (!STAILQ_EMPTY(&self->rows)) {
+ row = STAILQ_FIRST(&self->rows);
+ if (!row)
+ break;
+
+ STAILQ_REMOVE_HEAD(&self->rows, nodes);
+
+ // Free the rows
+ cli_table_row_free(row);
+ }
+
+ free(self);
+}
+
+int cli_table_add_col(cli_table* self,
+ const char* name, cli_table_col_type type, cli_table_col_align align) {
+ cli_table_col* col = NULL;
+
+ // Allocate a new col
+ col = calloc(1, sizeof(*col));
+ if (!col)
+ return -errno;
+
+ // Store the name
+ col->name = name;
+
+ // Store the type
+ col->type = type;
+
+ // Store the alignment
+ col->align = align;
+
+ // The column needs to be wide enough to fit the title
+ col->width = strlen(col->name);
+
+ // Append the column
+ STAILQ_INSERT_TAIL(&self->cols, col, nodes);
+ self->num_cols++;
+
+ return 0;
+}
+
+int cli_table_add_row(cli_table* self, ...) {
+ cli_table_col* col = NULL;
+ cli_table_row* row = NULL;
+ char buffer[1024];
+ size_t width = 0;
+ va_list args;
+ int r = 0;
+
+ va_start(args, self);
+
+ // Allocate a new row
+ row = calloc(1, sizeof(*row));
+ if (!row)
+ return -errno;
+
+ // Format all arguments
+ STAILQ_FOREACH(col, &self->cols, nodes) {
+ switch (col->type) {
+ case CLI_TABLE_STRING:
+ r = pakfire_string_format(buffer, "%s", va_arg(args, const char*));
+ break;
+
+ case CLI_TABLE_INTEGER:
+ r = pakfire_string_format(buffer, "%d", va_arg(args, int));
+ break;
+
+ case CLI_TABLE_FLOAT:
+ r = pakfire_string_format(buffer, "%f", va_arg(args, double));
+ break;
+ }
+
+ // Break if we could not format the argument
+ if (r < 0)
+ goto ERROR;
+
+ // Append the string
+ r = pakfire_strings_append(&row->cols, buffer);
+ if (r < 0)
+ goto ERROR;
+
+ // Determine the width of the argument
+ width = strlen(buffer);
+
+ // Adjust the required width of the column
+ if (width > col->width)
+ col->width = width;
+ }
+
+ va_end(args);
+
+ // Append the row
+ STAILQ_INSERT_TAIL(&self->rows, row, nodes);
+
+ return 0;
+
+ERROR:
+ if (row)
+ cli_table_row_free(row);
+
+ return r;
+}
+
+static size_t cli_table_width(cli_table* self) {
+ cli_table_col* col = NULL;
+ size_t width = 0;
+
+ // An empty table has no width
+ if (STAILQ_EMPTY(&self->cols))
+ return 0;
+
+ STAILQ_FOREACH(col, &self->cols, nodes)
+ width += col->width + 1;
+
+ return width - 1;
+}
+
+static int cli_table_col_is_last(cli_table* self, cli_table_col* col) {
+ cli_table_col* c = NULL;
+ unsigned int i = 0;
+
+ // Count all columns until we have found the column in question
+ STAILQ_FOREACH(c, &self->cols, nodes) {
+ i++;
+
+ if (col == c)
+ break;
+ }
+
+ return (i >= self->num_cols);
+}
+
+static int cli_table_print_newline(cli_table* self, FILE* f) {
+ int r;
+
+ // Terminate the line
+ r = fputc('\n', f);
+ if (r < 0)
+ return -errno;
+
+ return 0;
+}
+
+static int cli_table_print_cell(cli_table* self, cli_table_col* col, FILE* f, const char* s) {
+ int r = 0;
+
+ // Print the cell
+ switch (col->align) {
+ case CLI_TABLE_ALIGN_LEFT:
+ r = fprintf(f, "%-*s", (int)col->width, s);
+ break;
+
+ case CLI_TABLE_ALIGN_RIGHT:
+ r = fprintf(f, "%*s", (int)col->width, s);
+ break;
+ }
+
+ // Return any errors
+ if (r < 0)
+ return -errno;
+
+ // Add an extra space unless this is the last column
+ if (!cli_table_col_is_last(self, col)) {
+ r = fputc(' ', f);
+ if (r < 0)
+ return -errno;
+ }
+
+ return 0;
+}
+
+static int cli_table_print_header(cli_table* self, FILE* f) {
+ cli_table_col* col = NULL;
+ int r;
+
+ // Print the header
+ STAILQ_FOREACH(col, &self->cols, nodes) {
+ r = cli_table_print_cell(self, col, f, col->name);
+ if (r < 0)
+ return r;
+ }
+
+ // Terminate the line
+ return cli_table_print_newline(self, f);
+}
+
+static int cli_table_print_separator(cli_table* self, FILE* f) {
+ cli_table_col* col = NULL;
+ int r;
+
+ // Print the separator
+ STAILQ_FOREACH(col, &self->cols, nodes) {
+ for (size_t i = 0; i < col->width; i++) {
+ r = fputc('-', f);
+ if (r < 0)
+ return -errno;
+ }
+
+ r = fputc(' ', f);
+ if (r < 0)
+ return -errno;
+ }
+
+ // Terminate the line
+ return cli_table_print_newline(self, f);
+}
+
+static int cli_table_print_rows(cli_table* self, FILE* f) {
+ cli_table_col* col = NULL;
+ cli_table_row* row = NULL;
+ const char* cell = NULL;
+ int i;
+ int r;
+
+ STAILQ_FOREACH(row, &self->rows, nodes) {
+ // Reset the index of the columns
+ i = 0;
+
+ STAILQ_FOREACH(col, &self->cols, nodes) {
+ cell = row->cols[i++];
+ if (!cell)
+ break;
+
+ // Print the cell
+ r = cli_table_print_cell(self, col, f, cell);
+ if (r < 0)
+ return r;
+ }
+
+ // Terminate the line
+ r = cli_table_print_newline(self, f);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+int cli_table_print(cli_table* self, FILE* f) {
+ int r;
+
+ // We don't need to print a table with no columns
+ if (STAILQ_EMPTY(&self->cols))
+ return 0;
+
+ // Determine the minimum width that we
+ size_t width = cli_table_width(self);
+
+ // XXX What do we do when we are larger than the terminal is wide?
+
+ // Print the header
+ r = cli_table_print_header(self, f);
+ if (r < 0)
+ return r;
+
+ // Print a separator
+ r = cli_table_print_separator(self, f);
+ if (r < 0)
+ return r;
+
+ // Print the rows
+ r = cli_table_print_rows(self, f);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
--- /dev/null
+/*#############################################################################
+# #
+# Pakfire - The IPFire package management system #
+# Copyright (C) 2025 Pakfire development team #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+#############################################################################*/
+
+#ifndef PAKFIRE_CLI_TABLE_H
+#define PAKFIRE_CLI_TABLE_H
+
+#include <stdio.h>
+
+typedef struct cli_table cli_table;
+
+int cli_table_create(cli_table** table);
+
+void cli_table_free(cli_table* self);
+
+typedef enum {
+ CLI_TABLE_STRING,
+ CLI_TABLE_INTEGER,
+ CLI_TABLE_FLOAT,
+} cli_table_col_type;
+
+typedef enum {
+ CLI_TABLE_ALIGN_LEFT,
+ CLI_TABLE_ALIGN_RIGHT,
+} cli_table_col_align;
+
+int cli_table_add_col(cli_table* self,
+ const char* name, cli_table_col_type type, cli_table_col_align align);
+
+int cli_table_add_row(cli_table* self, ...);
+
+int cli_table_print(cli_table* self, FILE* f);
+
+#endif /* PAKFIRE_CLI_TABLE_H */