commit 46972dd2694b2b7ba4395861787c6a63d6dd4d53
Author: Louis Burda <quent.burda@gmail.com>
Date: Sat, 18 Mar 2023 18:42:12 +0100
Initial version
Diffstat:
11 files changed, 1124 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,3 @@
+compile_commands.json
+build
+.cache
diff --git a/.gitmodules b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "lib/liballoc"]
+ path = lib/liballoc
+ url = git@sinitax.com:snx/liballoc
diff --git a/Makefile b/Makefile
@@ -0,0 +1,48 @@
+PREFIX ?= /usr/local
+LIBDIR ?= /lib
+INCLDIR ?= /include
+
+CFLAGS = -I src -I include -I lib/liballoc/include -Wformat
+CFLAGS += -Wconversion -Wunused-variable -Wunused-function
+
+ifeq "$(DEBUG)" "1"
+CFLAGS += -g
+endif
+
+SRC = src/tabular.c src/util.c
+
+all: build/libtabular.a build/libtabular.so build/test
+
+clean:
+ rm -rf build
+
+build:
+ mkdir build
+
+lib/liballoc/build/liballoc.a:
+ make -C lib/liballoc build/liballoc.a
+
+build/libtabular.a: $(SRC) include/tabular.h libtabular.api | build
+ $(CC) -o build/tmp.o $(SRC) $(CFLAGS) -r
+ objcopy --keep-global-symbols=libtabular.api build/tmp.o build/fixed.o
+ ar rcs $@ build/fixed.o
+
+build/libtabular.so: $(SRC) include/tabular.h libtabular.lds | build
+ $(CC) -o $@ $(SRC) -fPIC $(CFLAGS) \
+ -shared -Wl,-version-script libtabular.lds
+
+build/test: src/test.c build/libtabular.a \
+ lib/liballoc/build/liballoc.a | build
+ $(CC) -o $@ $^ $(CFLAGS)
+
+install:
+ install -m644 include/tabular.h -t "$(DESTDIR)$(PREFIX)$(INCLDIR)"
+ install -m755 build/libtabular.so -t "$(DESTDIR)$(PREFIX)$(LIBDIR)"
+ install -m755 build/libtabular.a -t "$(DESTDIR)$(PREFIX)$(LIBDIR)"
+
+uninstall:
+ rm -f "$(DESTDIR)$(PREFIX)$(INCLDIR)/tabular.h"
+ rm -f "$(DESTDIR)$(PREFIX)$(LIBDIR)/libtabular.so"
+ rm -f "$(DESTDIR)$(PREFIX)$(LIBDIR)/libtabular.a"
+
+.PHONY: all clean install uninstall
diff --git a/include/tabular.h b/include/tabular.h
@@ -0,0 +1,102 @@
+#pragma once
+
+#include "allocator.h"
+
+#include <wchar.h>
+#include <stdio.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <wctype.h>
+
+enum {
+ TABULAR_ALIGN_LEFT,
+ TABULAR_ALIGN_CENTER,
+ TABULAR_ALIGN_RIGHT
+};
+
+enum {
+ TABULAR_TRUNC,
+ TABULAR_SQUASH,
+ TABULAR_SQUASH_WORDAWARE
+};
+
+struct tabular_user {
+ union {
+ void *ptr;
+ size_t id;
+ };
+};
+
+struct tabular_entry {
+ char *str;
+ bool avail;
+ size_t ulen;
+};
+
+struct tabular_row {
+ struct tabular_user user;
+ struct tabular_entry *entries;
+
+ struct tabular_row *next;
+};
+
+struct tabular_col {
+ const char *name;
+
+ struct tabular_user user;
+
+ char *(*to_str)(const struct tabular_user *id);
+ bool (*is_hidden)(const struct tabular_user *id);
+
+ size_t minwidth, maxwidth;
+ size_t lpad, rpad;
+
+ int align;
+
+ int strategy;
+
+ bool essential;
+};
+
+struct tabular_cfg {
+ const struct tabular_col *columns;
+ size_t column_count;
+
+ /* color mode: 1, 16, 256 */
+ int colors;
+
+ /* fit rows to output size */
+ bool fit_rows;
+
+ /* amount of available lines to skip */
+ size_t skip_lines;
+
+ /* output dimensions */
+ size_t outw, outh;
+
+ /* entry separators */
+ const char *hsep, *vsep, *xsep;
+
+ /* total lpad, rpad */
+ size_t lpad, rpad;
+
+ bool (*print_style)(FILE *file, const struct tabular_cfg *cfg,
+ const struct tabular_row *row, const struct tabular_col *col);
+
+ const struct allocator *allocator;
+};
+
+struct tabular_stats {
+ bool rows_truncated;
+ bool cols_truncated;
+ size_t rows_displayed;
+ size_t lines_used;
+};
+
+int tabular_format(FILE *file, const struct tabular_cfg *cfg,
+ struct tabular_stats *stat, struct tabular_row *rows);
+
+struct tabular_row **tabular_alloc_row(const struct tabular_cfg *cfg,
+ int *error, struct tabular_row **rows, struct tabular_user user);
+int tabular_free_rows(const struct tabular_cfg *cfg, struct tabular_row *rows);
diff --git a/lib/liballoc b/lib/liballoc
@@ -0,0 +1 @@
+Subproject commit 22d274eb7a43de119af51cf46a892aa6be6a7d91
diff --git a/libtabular.api b/libtabular.api
@@ -0,0 +1,3 @@
+tabular_format
+tabular_alloc_row
+tabular_free_rows
diff --git a/libtabular.lds b/libtabular.lds
@@ -0,0 +1,7 @@
+LIBTABULAR_1.0 {
+ global:
+ tabular_format;
+ tabular_alloc_row;
+ tabular_free_rows;
+ local: *;
+};
diff --git a/src/tabular.c b/src/tabular.c
@@ -0,0 +1,636 @@
+#define _XOPEN_SOURCE
+
+#include "tabular.h"
+#include "util.h"
+
+#include <sys/ioctl.h>
+#include <wchar.h>
+#include <errno.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdint.h>
+#include <stddef.h>
+
+struct col_state {
+ size_t width;
+ size_t written;
+ size_t maxlen;
+ size_t lpad, rpad;
+ bool hidden;
+};
+
+struct fmt_state {
+ struct col_state *columns;
+ size_t column_count;
+
+ size_t hseplen;
+ size_t vseplen;
+ size_t xseplen;
+ size_t mseplen;
+
+ size_t line_limit;
+ size_t row_limit;
+
+ bool header_line;
+};
+
+static void calc_word_aware(struct tabular_entry *entry, size_t maxwidth,
+ size_t *offset, size_t *width, size_t *lines);
+static size_t recalc_col_word_aware(const struct tabular_cfg *cfg,
+ struct tabular_row *rows, size_t limit,
+ size_t col, size_t maxwidth, size_t remaining);
+static size_t calc_row_width(struct col_state *cols, size_t count);
+static size_t calc_output_lines(const struct tabular_cfg *cfg,
+ struct fmt_state *fmt, struct tabular_row *rows, size_t limit);
+static bool recalc_cols(const struct tabular_cfg *cfg,
+ struct tabular_stats *stats, struct fmt_state *fmt,
+ struct tabular_row *rows, size_t limit);
+
+static int calc_params_row(const struct tabular_cfg *cfg,
+ struct tabular_stats *stats, struct tabular_row *rows,
+ struct tabular_row *row, struct fmt_state *fmt, size_t limit);
+static int calc_params(const struct tabular_cfg *cfg,
+ struct tabular_stats *stat, struct tabular_row *rows,
+ struct fmt_state *fmt);
+
+static void output_header(FILE *file, const struct tabular_cfg *cfg,
+ struct tabular_stats *stat, const struct fmt_state *fmt);
+static void output_row(FILE *file, const struct tabular_cfg *cfg,
+ struct tabular_stats *stat, const struct tabular_row *row,
+ struct fmt_state *fmt);
+static int output_rows(FILE *file, const struct tabular_cfg *cfg,
+ struct tabular_stats *stats, struct tabular_row *rows,
+ struct fmt_state *fmt);
+
+const char *
+calc_word_aware_line(const char *str, size_t maxwidth,
+ size_t *out_offset, size_t *out_width)
+{
+ const char *sep;
+ size_t width, nwidth;
+ size_t offset, wlen;
+
+ if (!str) {
+ if (out_offset) *out_offset = 0;
+ if (out_width) *out_width = 0;
+ return NULL;
+ }
+
+ offset = strspn(str, " ");
+ str += offset;
+
+ nwidth = width = 0;
+ while (nwidth <= maxwidth) {
+ if (!str) break;
+ sep = strchr(str, ' ');
+ if (!sep) {
+ wlen = u8strlen(str);
+ str = NULL;
+ } else {
+ wlen = u8strnlen(str, (size_t) (sep - str));
+ str = sep + 1;
+ }
+ nwidth = width + (width > 0) + wlen;
+ }
+
+ if (out_offset) *out_offset = offset;
+ if (out_width) {
+ /* single word > maxwidth */
+ if (!width && nwidth)
+ *out_width = maxwidth;
+ else
+ *out_offset = offset;
+ }
+
+ return str;
+}
+
+void
+calc_word_aware(struct tabular_entry *entry, size_t maxwidth,
+ size_t *out_offset, size_t *out_width, size_t *out_lines)
+{
+ const char *str;
+ size_t width, mwidth;
+ size_t lines;
+
+ if (out_offset) *out_offset = 0;
+
+ if (!entry->str) {
+ if (out_width) *out_width = 0;
+ if (out_lines) *out_lines = 1;
+ return;
+ }
+
+ lines = 0;
+ mwidth = 0;
+ str = entry->str;
+ while (str) {
+ str = calc_word_aware_line(str, maxwidth,
+ str == entry->str ? out_offset : NULL, &width);
+ lines++;
+ mwidth = MAX(mwidth, width);
+ }
+
+ if (out_width) *out_width = mwidth;
+ if (out_lines) *out_lines = lines;
+}
+
+size_t
+recalc_col_word_aware(const struct tabular_cfg *cfg,
+ struct tabular_row *rows, size_t limit, size_t col,
+ size_t maxwidth, size_t remaining)
+{
+ size_t off, wwidth, max_wwidth, rowcnt;
+ struct tabular_row *row;
+
+ max_wwidth = cfg->columns[col].minwidth;
+
+ rowcnt = 0;
+ for (row = rows; row; row = row->next) {
+ calc_word_aware(&row->entries[col],
+ maxwidth, &off, &wwidth, NULL);
+ max_wwidth = MAX(max_wwidth, wwidth);
+
+ rowcnt += 1;
+ if (rowcnt == limit) break;
+ }
+
+ return max_wwidth;
+}
+
+size_t
+calc_row_width(struct col_state *cols, size_t count)
+{
+ size_t sum, i;
+
+ sum = 0;
+ for (i = 0; i < count; i++) {
+ if (cols[i].hidden) continue;
+ sum += cols[i].width + (sum ? 1 : 0);
+ }
+
+ return sum;
+}
+
+size_t
+calc_output_lines(const struct tabular_cfg *cfg, struct fmt_state *fmt,
+ struct tabular_row *rows, size_t limit)
+{
+ struct tabular_row *row;
+ size_t row_index, row_lines;
+ size_t entry_lines;
+ size_t lines, i;
+ size_t width;
+
+ lines = 0;
+ row_index = 0;
+ for (row = rows; row; row = row->next) {
+ if (row_index == limit) break;
+
+ row_lines = 0;
+ for (i = 0; i < cfg->column_count; i++) {
+ if (fmt->columns[i].hidden) continue;
+ width = fmt->columns[i].width
+ - fmt->columns[i].lpad - fmt->columns[i].rpad;
+ switch (cfg->columns[i].strategy) {
+ case TABULAR_TRUNC:
+ entry_lines = 1;
+ break;
+ case TABULAR_SQUASH:
+ entry_lines = CEILDIV(row->entries[i].ulen, width);
+ break;
+ case TABULAR_SQUASH_WORDAWARE:
+ calc_word_aware(&row->entries[i],
+ width, NULL, NULL, &entry_lines);
+ break;
+ }
+ row_lines = MAX(row_lines, entry_lines);
+ }
+ lines += row_lines;
+
+ row_index += 1;
+ }
+
+ return lines;
+}
+
+bool
+recalc_cols(const struct tabular_cfg *cfg, struct tabular_stats *stats,
+ struct fmt_state *fmt, struct tabular_row *rows, size_t limit)
+{
+ size_t width, fullwidth;
+ size_t i, remaining;
+
+ fullwidth = cfg->outw - cfg->lpad - cfg->rpad;
+
+ /* reset widths to minimum requirement */
+ for (i = 0; i < cfg->column_count; i++) {
+ if (fmt->columns[i].hidden) continue;
+ fmt->columns[i].width = cfg->columns[i].minwidth;
+ }
+
+ /* could not fit all necessary columns at minimum width */
+ width = calc_row_width(fmt->columns, fmt->column_count);
+ while (width > fullwidth) {
+ /* remove non-essential columns */
+ for (i = cfg->column_count - 1; i >= 0; i--) {
+ if (!cfg->columns[i].essential
+ && !fmt->columns[i].hidden) {
+ fmt->columns[i].hidden = true;
+ break;
+ }
+ }
+
+ /* failed to remove any more */
+ if (i < 0) return false;
+
+ stats->cols_truncated = true;
+ width = calc_row_width(fmt->columns, fmt->column_count);
+ }
+
+ /* redistribute excess width to columns, left to right */
+ remaining = fullwidth - width;
+ for (i = 0; remaining > 0 && i < cfg->column_count; i++) {
+ if (fmt->columns[i].hidden) continue;
+ width = MIN(fmt->columns[i].maxlen, cfg->columns[i].maxwidth);
+ width = MIN(width, fmt->columns[i].width + remaining);
+ width = width - fmt->columns[i].lpad - fmt->columns[i].rpad;
+ if (cfg->columns[i].strategy == TABULAR_SQUASH_WORDAWARE)
+ width = recalc_col_word_aware(cfg, rows,
+ limit, i, width, remaining);
+ width = MAX(width + fmt->columns[i].lpad + fmt->columns[i].rpad,
+ fmt->columns[i].width);
+ remaining -= width - fmt->columns[i].width;
+ fmt->columns[i].width = width;
+ }
+
+ return true;
+}
+
+int
+calc_params_row(const struct tabular_cfg *cfg, struct tabular_stats *stats,
+ struct tabular_row *rows, struct tabular_row *row,
+ struct fmt_state *fmt, size_t limit)
+{
+ struct colinfo *copy;
+ size_t i, height;
+ int rc;
+
+ /* make copy of current width in case the next row does not fit */
+ rc = cfg->allocator->alloc((void **)©,
+ sizeof(struct col_state) * cfg->column_count);
+ if (rc) return -rc;
+ memcpy(copy, fmt->columns, sizeof(struct col_state) * cfg->column_count);
+
+ /* unhide cols and update maxlen */
+ for (i = 0; i < cfg->column_count; i++) {
+ if (fmt->columns[i].hidden && cfg->columns[i].to_str) {
+ if (cfg->columns[i].is_hidden) {
+ fmt->columns[i].hidden =
+ cfg->columns[i].is_hidden(&row->user);
+ } else {
+ fmt->columns[i].hidden = false;
+ }
+ }
+ if (fmt->columns[i].hidden) continue;
+
+ if (!row->entries[i].avail) {
+ row->entries[i].str = cfg->columns[i].to_str(&row->user);
+ row->entries[i].ulen = (uint32_t) u8strlen(row->entries[i].str);
+ row->entries[i].avail = true;
+ }
+
+ fmt->columns[i].maxlen = MAX(fmt->columns[i].maxlen,
+ row->entries[i].ulen);
+ }
+
+ /* recalc column sizes to fit new column */
+ if (!recalc_cols(cfg, stats, fmt, rows, limit))
+ goto undo;
+
+ height = calc_output_lines(cfg, fmt, rows, limit);
+
+ /* check the line limit if applicable */
+ if (fmt->line_limit > 0 && height > fmt->line_limit)
+ goto undo;
+
+ cfg->allocator->free(copy);
+
+ return 0;
+
+undo:
+ memcpy(fmt->columns, copy, sizeof(struct col_state) * fmt->column_count);
+ cfg->allocator->free(copy);
+
+ return 1;
+}
+
+int
+calc_params(const struct tabular_cfg *cfg, struct tabular_stats *stats,
+ struct tabular_row *rows, struct fmt_state *fmt)
+{
+ struct tabular_row *row;
+ ssize_t first, last;
+ size_t i;
+ int rc;
+
+ fmt->header_line = (cfg->vsep != 0 && cfg->xsep != 0);
+
+ fmt->line_limit = 0;
+ if (cfg->fit_rows) {
+ fmt->line_limit = MAX(0, cfg->outh
+ - cfg->skip_lines - 1 - fmt->header_line);
+ }
+
+ fmt->hseplen = u8strlen(cfg->hsep);
+ fmt->vseplen = u8strlen(cfg->vsep);
+ fmt->xseplen = u8strlen(cfg->xsep);
+ fmt->mseplen = MAX(fmt->hseplen, fmt->xseplen);
+
+ fmt->column_count = cfg->column_count;
+ rc = cfg->allocator->alloc((void **)&fmt->columns,
+ fmt->column_count * sizeof(struct col_state));
+ if (rc) return -rc;
+
+ for (i = 0; i < cfg->column_count; i++) {
+ fmt->columns[i].lpad = cfg->columns[i].lpad;
+ fmt->columns[i].rpad = cfg->columns[i].rpad;
+ fmt->columns[i].hidden = true;
+ fmt->columns[i].width = cfg->columns[i].minwidth;
+ fmt->columns[i].maxlen = u8strlen(cfg->columns[i].name)
+ + fmt->columns[i].lpad + fmt->columns[i].rpad;
+ if (fmt->columns[i].maxlen > cfg->columns[i].minwidth)
+ return 1;
+ fmt->columns[i].written = 0;
+ }
+
+ if (!recalc_cols(cfg, stats, fmt, rows, 0)) {
+ stats->cols_truncated = true;
+ stats->rows_truncated = true;
+ return 0;
+ }
+
+ fmt->row_limit = 0;
+ for (row = rows; row; row = row->next) {
+ rc = calc_params_row(cfg, stats, rows,
+ row, fmt, fmt->row_limit + 1);
+ if (rc < 0) return rc;
+ if (rc > 0) break;
+ fmt->row_limit++;
+ }
+
+ first = last = -1;
+ for (i = 0; i < cfg->column_count; i++) {
+ if (fmt->columns[i].hidden) continue;
+ if (first < 0) first = (ssize_t) i;
+ last = (ssize_t) i;
+ }
+
+ if (first >= 0 && last >= 0) {
+ fmt->columns[first].lpad += cfg->lpad;
+ fmt->columns[first].width += cfg->lpad;
+ fmt->columns[last].rpad += cfg->rpad;
+ fmt->columns[last].width += cfg->rpad;
+ }
+
+ return 0;
+}
+
+void
+output_header(FILE *file, const struct tabular_cfg *cfg,
+ struct tabular_stats *stats, const struct fmt_state *fmt)
+{
+ size_t i, k, width;
+ bool dirty, first;
+
+ first = true;
+ for (i = 0; i < cfg->column_count; i++) {
+ if (fmt->columns[i].hidden) continue;
+ if (!first) {
+ dirty = cfg->print_style(file, cfg, NULL, NULL);
+ fprintf(file, "%s%*.s", cfg->hsep,
+ (int) (fmt->mseplen - fmt->hseplen), "");
+ if (dirty) fprintf(file, "\x1b[0m");
+ }
+ first = false;
+
+ width = fmt->columns[i].width
+ - fmt->columns[i].lpad - fmt->columns[i].rpad;
+
+ dirty = cfg->print_style(file, cfg,
+ NULL, &cfg->columns[i]);
+ print_pad(file, fmt->columns[i].lpad);
+ print_left(file, cfg->columns[i].name, width, width);
+ print_pad(file, fmt->columns[i].rpad);
+ if (dirty) fprintf(file, "\x1b[0m");
+ }
+
+ fprintf(file, "\n");
+ stats->lines_used += 1;
+
+ if (fmt->header_line) {
+ first = true;
+ dirty = cfg->print_style(file, cfg, NULL, NULL);
+ for (i = 0; i < cfg->column_count; i++) {
+ if (fmt->columns[i].hidden) continue;
+ if (!first) {
+ fprintf(file, "%s%*.s", cfg->xsep,
+ (int) (fmt->mseplen - fmt->xseplen), "");
+ }
+ for (k = 0; k < fmt->columns[i].width; k++)
+ fprintf(file, "%s", cfg->vsep);
+ first = false;
+ }
+ if (dirty) fprintf(file, "\x1b[0m");
+ fprintf(file, "\n");
+ stats->lines_used += 1;
+ }
+}
+
+void
+output_row(FILE *file, const struct tabular_cfg *cfg,
+ struct tabular_stats *stat, const struct tabular_row *row,
+ struct fmt_state *fmt)
+{
+ size_t wwidth, padwidth, outlen;
+ size_t i, off, width;
+ bool first, done, dirty;
+ char *entry;
+
+ for (i = 0; i < cfg->column_count; i++)
+ fmt->columns[i].written = 0;
+
+ do {
+ done = true;
+ first = true;
+ for (i = 0; i < cfg->column_count; i++) {
+ if (fmt->columns[i].hidden) continue;
+
+ if (!first) {
+ dirty = cfg->print_style(file, cfg, NULL, NULL);
+ fprintf(file, "%s", cfg->hsep);
+ if (dirty) fprintf(file, "\x1b[0m");
+ }
+ first = false;
+
+ entry = row->entries[i].str + fmt->columns[i].written;
+
+ if (cfg->columns[i].strategy == TABULAR_TRUNC) {
+ if (u8strlen(entry) > fmt->columns[i].width) {
+ width = fmt->columns[i].width;
+ entry[MAX(width - 2, 0)] = '.';
+ entry[MAX(width - 1, 0)] = '.';
+ entry[MAX(width - 0, 0)] = '\0';
+ }
+ }
+
+ dirty = cfg->print_style(file, cfg, row, &cfg->columns[i]);
+
+ off = 0;
+ width = fmt->columns[i].width
+ - fmt->columns[i].lpad - fmt->columns[i].rpad;
+ padwidth = width;
+ if (cfg->columns[i].strategy == TABULAR_SQUASH_WORDAWARE) {
+ calc_word_aware_line(entry, width, &off, &wwidth);
+ entry += off;
+ width = wwidth;
+ }
+
+ print_pad(file, fmt->columns[i].lpad);
+ if (cfg->columns[i].align == TABULAR_ALIGN_LEFT) {
+ print_left(file, entry, width, padwidth);
+ } else if (cfg->columns[i].align == TABULAR_ALIGN_RIGHT) {
+ print_right(file, entry, width, padwidth);
+ } else {
+ print_center(file, entry, width, padwidth);
+ }
+ print_pad(file, fmt->columns[i].rpad);
+
+ if (dirty) fprintf(file, "\x1b[0m");
+
+ if (cfg->columns[i].strategy == TABULAR_TRUNC) {
+ fmt->columns[i].written += u8strlen(entry);
+ continue;
+ }
+
+ outlen = MIN(u8strlen(entry), width + off);
+ fmt->columns[i].written += u8rawlen(entry, outlen);
+
+ /* check if anything other than whitespace left */
+ entry += u8rawlen(entry, outlen);
+ if (strspn(entry, " \t\v\n") != u8strlen(entry))
+ done = false;
+ }
+ fprintf(file, "\n");
+ stat->lines_used += 1;
+ } while (!done);
+
+ stat->rows_displayed += 1;
+}
+
+int
+output_rows(FILE *file, const struct tabular_cfg *cfg,
+ struct tabular_stats *stats, struct tabular_row *rows,
+ struct fmt_state *fmt)
+{
+ struct tabular_row *row;
+ size_t count;
+
+ if (fmt->row_limit == 0) return 0;
+
+ output_header(file, cfg, stats, fmt);
+
+ count = 0;
+ for (row = rows; row; row = row->next) {
+ if (count == fmt->row_limit)
+ break;
+ output_row(file, cfg, stats, row, fmt);
+ count += 1;
+ }
+
+ stats->rows_truncated = !row;
+
+ return 0;
+}
+
+int
+tabular_format(FILE *file, const struct tabular_cfg *cfg,
+ struct tabular_stats *stats, struct tabular_row *rows)
+{
+ struct fmt_state fmt;
+ size_t i;
+ int rc;
+
+ stats->rows_displayed = 0;
+ stats->rows_truncated = false;
+ stats->cols_truncated = false;
+ stats->lines_used = 0;
+
+ if (!cfg->column_count || !rows)
+ return 0;
+
+ for (i = 0; i < cfg->column_count; i++) {
+ if (cfg->columns[i].minwidth > cfg->columns[i].maxwidth)
+ return 1;
+ if (cfg->columns[i].lpad + cfg->columns[i].rpad
+ >= cfg->columns[i].minwidth)
+ return 1;
+ if (cfg->columns[i].strategy != TABULAR_TRUNC
+ && cfg->columns[i].strategy != TABULAR_SQUASH)
+ return 1;
+ }
+
+ rc = calc_params(cfg, stats, rows, &fmt);
+ if (rc) return rc;
+
+ rc = output_rows(file, cfg, stats, rows, &fmt);
+ if (rc) return rc;
+
+ rc = cfg->allocator->free(fmt.columns);
+ if (rc) return -rc;
+
+ return 0;
+}
+
+struct tabular_row **
+tabular_alloc_row(const struct tabular_cfg *cfg, int *rc,
+ struct tabular_row **end, struct tabular_user user)
+{
+ struct tabular_row *row;
+
+ *rc = -cfg->allocator->alloc((void **)end, sizeof(struct tabular_row));
+ if (*rc) return NULL;
+
+ row = *end;
+ row->next = NULL;
+ row->user = user;
+ *rc = -cfg->allocator->alloc((void **)&row->entries,
+ sizeof(struct tabular_entry) * cfg->column_count);
+ if (*rc) return NULL;
+ memset(row->entries, 0, sizeof(struct tabular_entry) * cfg->column_count);
+
+ return &row->next;
+}
+
+int
+tabular_free_rows(const struct tabular_cfg *cfg, struct tabular_row *rows)
+{
+ struct tabular_row *iter, *next;
+ size_t i;
+ int rc;
+
+ for (iter = rows; iter; iter = next) {
+ next = iter->next;
+ for (i = 0; i < cfg->column_count; i++) {
+ rc = cfg->allocator->free(iter->entries[i].str);
+ if (rc) return -rc;
+ }
+ rc = cfg->allocator->free(iter->entries);
+ if (rc) return -rc;
+ rc = cfg->allocator->free(iter);
+ if (rc) return -rc;
+ }
+
+ return 0;
+}
diff --git a/src/test.c b/src/test.c
@@ -0,0 +1,153 @@
+#include "tabular.h"
+#include "allocator.h"
+
+#include <sys/ioctl.h>
+#include <err.h>
+#include <string.h>
+#include <stdbool.h>
+#include <stdio.h>
+
+#define ARRLEN(x) (sizeof(x)/sizeof(*(x)))
+
+bool print_style(FILE *file, const struct tabular_cfg *cfg,
+ const struct tabular_row *row, const struct tabular_col *col);
+
+char *col_pos_str(const struct tabular_user *user);
+bool col_pos_hidden(const struct tabular_user *user);
+
+char *col_name_str(const struct tabular_user *user);
+bool col_name_hidden(const struct tabular_user *user);
+
+static const char **argv = NULL;
+
+static struct tabular_col columns[] = {
+ {
+ .name = "Pos",
+
+ .to_str = col_pos_str,
+ .is_hidden = NULL,
+
+ .minwidth = 3,
+ .maxwidth = 3,
+ .lpad = 0,
+ .rpad = 0,
+
+ .align = TABULAR_ALIGN_CENTER,
+
+ .strategy = TABULAR_TRUNC,
+ .essential = true,
+ },
+ {
+ .name = "Value",
+
+ .to_str = col_name_str,
+ .is_hidden = NULL,
+
+ .minwidth = 5,
+ .maxwidth = 20,
+ .lpad = 0,
+ .rpad = 0,
+
+ .align = TABULAR_ALIGN_LEFT,
+
+ .strategy = TABULAR_SQUASH,
+ .essential = true,
+ },
+};
+
+static struct tabular_cfg cfg = {
+ .colors = 256,
+
+ .columns = columns,
+ .column_count = ARRLEN(columns),
+
+ .fit_rows = true,
+
+ .hsep = "│ ",
+ .vsep = "─",
+ .xsep = "┼─",
+
+ .outw = 0,
+ .outh = 0,
+
+ .lpad = 1,
+ .rpad = 1,
+
+ .print_style = print_style,
+
+ .skip_lines = 3,
+
+ .allocator = &stdlib_heap_allocator
+};
+
+bool
+print_style(FILE *file, const struct tabular_cfg *cfg,
+ const struct tabular_row *row, const struct tabular_col *col)
+{
+ if (cfg->colors == 256) {
+ if (!col) { /* separators */
+ fprintf(file, "\x1b[90m");
+ return true;
+ } else if (!row) { /* header */
+ fprintf(file, "\x1b[1m");
+ return true;
+ } else if (!strcmp(col->name, "Name")) {
+ fprintf(file, "\x1b[35m");
+ return true;
+ }
+ }
+
+ return false;
+}
+
+char *
+col_pos_str(const struct tabular_user *user)
+{
+ char buf[16];
+
+ snprintf(buf, sizeof(buf), "%li", user->id);
+
+ return strdup(buf);
+}
+
+char *
+col_name_str(const struct tabular_user *user)
+{
+ return strdup(argv[user->id]);
+}
+
+int
+main(int argc, const char **_argv)
+{
+ struct tabular_row *rows, **end;
+ struct tabular_stats stats;
+ struct winsize ws;
+ int i, rc;
+
+ argv = _argv;
+
+ rc = ioctl(1, TIOCGWINSZ, &ws);
+ if (!rc) {
+ cfg.outw = ws.ws_col;
+ cfg.outh = ws.ws_row;
+ } else {
+ cfg.outw = 80;
+ cfg.outh = 26;
+ }
+
+ rows = NULL;
+ end = &rows;
+ for (i = 0; i < argc; i++) {
+ end = tabular_alloc_row(&cfg, &rc, end,
+ (struct tabular_user) { .id = (size_t) i });
+ if (!end) errx(1, "tabular_append_row");
+ }
+
+ rc = tabular_format(stdout, &cfg, &stats, rows);
+ if (rc) errx(1, "tabular_format");
+
+ printf("\n%lu lines, %lu rows\n",
+ stats.lines_used, stats.rows_displayed);
+
+ tabular_free_rows(&cfg, rows);
+}
diff --git a/src/util.c b/src/util.c
@@ -0,0 +1,143 @@
+#include "util.h"
+
+#include <stddef.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+size_t
+u8strlen(const char *str)
+{
+ size_t i, len;
+
+ if (!str) return 0;
+
+ len = 0;
+ for (i = 0; str[i]; i++) {
+ if ((str[i] & 0xC0) != 0x80)
+ len += 1;
+ }
+
+ return len;
+}
+
+size_t
+u8strnlen(const char *str, size_t max)
+{
+ size_t i, len;
+
+ if (!str) return 0;
+
+ len = 0;
+ for (i = 0; str[i] && i < max; i++) {
+ if ((str[i] & 0xC0) != 0x80)
+ len += 1;
+ }
+
+ return len;
+}
+
+size_t
+u8rawlen(const char *str, size_t max)
+{
+ size_t i, len;
+
+ len = 0;
+ for (i = 0; str[i] && len < max; i++)
+ if ((str[i] & 0xC0) != 0x80) {
+ len += 1;
+ }
+
+ return i;
+}
+
+size_t
+print_pad(FILE *file, size_t len)
+{
+ fprintf(file, "%*.s", (int) len, "");
+ return len;
+}
+
+size_t
+print_hex(FILE *file, const void *bytes, size_t len)
+{
+ size_t i;
+
+ for (i = 0; i < len; i++)
+ fprintf(file, "%02X", *(uint8_t *)(bytes + i));
+
+ return len * 2;
+}
+
+size_t
+print_trunc(FILE *file, const char *str, size_t width)
+{
+ size_t len;
+
+ len = u8strlen(str);
+ if (len > width && width >= 2) {
+ fprintf(file, "%*.*s..", (int) width - 2, (int) width - 2, str);
+ } else if (len > width&& width < 2) {
+ fprintf(file, "%*.*s", (int) width, (int) width, "..");
+ } else {
+ fprintf(file, "%s", str);
+ }
+
+ return MIN(len, width);
+}
+
+size_t
+print_left(FILE *file, const char *str, size_t width, size_t padwidth)
+{
+ size_t rawlen, u8len;
+ ssize_t ret;
+
+ u8len = MIN(u8strlen(str), width);
+ rawlen = u8rawlen(str, u8len);
+
+ /* make up for invisible bytes */
+ padwidth += rawlen - u8len;
+
+ ret = fprintf(file, "%-*.*s", (int) padwidth, (int) rawlen, str);
+
+ return (size_t) MAX(0, ret);
+}
+
+size_t
+print_center(FILE *file, const char *str, size_t width, size_t padwidth)
+{
+ size_t u8len, rawlen;
+ size_t lpad, rpad;
+ ssize_t ret;
+
+ u8len = MIN(width, u8strlen(str));
+ rawlen = u8rawlen(str, u8len);
+ lpad = MAX(padwidth - u8len, 0) / 2;
+ rpad = MAX(padwidth - u8len - lpad, 0);
+
+ /* make up for invisible bytes */
+ rpad += rawlen - u8len;
+
+ ret = fprintf(file, "%*s%*.*s%*s", (int) lpad, "",
+ (int) rawlen, (int) rawlen, str, (int) rpad, "");
+
+ return (size_t) MAX(0, ret);
+}
+
+size_t
+print_right(FILE *file, const char *str, size_t width, size_t padwidth)
+{
+ size_t rawlen, u8len;
+ ssize_t ret;
+
+ u8len = MIN(width, u8strlen(str));
+ rawlen = u8rawlen(str, u8len);
+
+ /* make up for invisible bytes */
+ padwidth += rawlen - u8len;
+
+ ret = fprintf(file, "%*.*s", (int) padwidth, (int) rawlen, str);
+
+ return (size_t) MAX(0, ret);
+}
+
diff --git a/src/util.h b/src/util.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+#define MAX(a, b) ((a) < (b) ? (b) : (a))
+#define CEILDIV(a, b) (((a) / (b)) + !!((a) % (b)))
+
+size_t u8strlen(const char *str);
+size_t u8strnlen(const char *str, size_t max);
+size_t u8rawlen(const char *str, size_t max);
+
+size_t print_pad(FILE *file, size_t len);
+size_t print_hex(FILE *file, const void *in, size_t len);
+
+size_t print_trunc(FILE *file, const char *str, size_t width);
+size_t print_left(FILE *file, const char *str,
+ size_t width, size_t padwidth);
+size_t print_center(FILE *file, const char *str,
+ size_t width, size_t padwidth);
+size_t print_right(FILE *file, const char *str,
+ size_t width, size_t padwidth);
+
+