]> git.hungrycats.org Git - bees/commitdiff
table: add a simple text table renderer
authorZygo Blaxell <bees@furryterror.org>
Sun, 29 Jan 2023 02:26:51 +0000 (21:26 -0500)
committerZygo Blaxell <bees@furryterror.org>
Sun, 1 Dec 2024 04:30:33 +0000 (23:30 -0500)
This should help clean up some of the uglier status outputs.

Supports:

 * multi-line table cells
 * character fills
 * sparse tables
 * insert, delete by row and column
 * vertical separators

and not much else.

Signed-off-by: Zygo Blaxell <bees@furryterror.org>
include/crucible/table.h [new file with mode: 0644]
lib/Makefile
lib/table.cc [new file with mode: 0644]
test/Makefile
test/table.cc [new file with mode: 0644]

diff --git a/include/crucible/table.h b/include/crucible/table.h
new file mode 100644 (file)
index 0000000..34eb90d
--- /dev/null
@@ -0,0 +1,106 @@
+#ifndef CRUCIBLE_TABLE_H
+#define CRUCIBLE_TABLE_H
+
+#include <functional>
+#include <limits>
+#include <map>
+#include <memory>
+#include <ostream>
+#include <sstream>
+#include <string>
+#include <vector>
+
+namespace crucible {
+       namespace Table {
+               using namespace std;
+
+               using Content = function<string(size_t width, size_t height)>;
+               const size_t endpos = numeric_limits<size_t>::max();
+
+               Content Fill(const char c);
+               Content Text(const string& s);
+
+               template <class T>
+               Content Number(const T& num)
+               {
+                       ostringstream oss;
+                       oss << num;
+                       return Text(oss.str());
+               }
+
+               class Cell {
+                       Content m_content;
+               public:
+                       Cell(const Content &fn = [](size_t, size_t) { return string(); } );
+                       Cell& operator=(const Content &fn);
+                       string text(size_t width, size_t height) const;
+               };
+
+               class Dimension {
+                       size_t m_next_pos = 0;
+                       vector<size_t> m_elements;
+               friend class Table;
+                       size_t at(size_t) const;
+               public:
+                       size_t size() const;
+                       size_t insert(size_t pos);
+                       void erase(size_t pos);
+               };
+
+               class Table {
+                       Dimension m_rows, m_cols;
+                       map<pair<size_t, size_t>, Cell> m_cells;
+                       string m_left = "|";
+                       string m_mid = "|";
+                       string m_right = "|";
+               public:
+                       Dimension &rows();
+                       const Dimension& rows() const;
+                       Dimension &cols();
+                       const Dimension& cols() const;
+                       Cell& at(size_t row, size_t col);
+                       const Cell& at(size_t row, size_t col) const;
+                       template <class T> void insert_row(size_t pos, const T& container);
+                       template <class T> void insert_col(size_t pos, const T& container);
+                       void left(const string &s);
+                       void mid(const string &s);
+                       void right(const string &s);
+                       const string& left() const;
+                       const string& mid() const;
+                       const string& right() const;
+               };
+
+               ostream& operator<<(ostream &os, const Table &table);
+
+               template <class T>
+               void
+               Table::insert_row(size_t pos, const T& container)
+               {
+                       const auto new_pos = m_rows.insert(pos);
+                       size_t col = 0;
+                       for (const auto &i : container) {
+                               if (col >= cols().size()) {
+                                       cols().insert(col);
+                               }
+                               at(new_pos, col++) = i;
+                       }
+               }
+
+               template <class T>
+               void
+               Table::insert_col(size_t pos, const T& container)
+               {
+                       const auto new_pos = m_cols.insert(pos);
+                       size_t row = 0;
+                       for (const auto &i : container) {
+                               if (row >= rows().size()) {
+                                       rows().insert(row);
+                               }
+                               at(row++, new_pos) = i;
+                       }
+               }
+
+       }
+}
+
+#endif // CRUCIBLE_TABLE_H
index 24cd309a36783a75635d252b999d8e6b9234cabe..cddea6ff3091a7ea8f28b402e8836e06a9d9a2b5 100644 (file)
@@ -17,6 +17,7 @@ CRUCIBLE_OBJS = \
        path.o \
        process.o \
        string.o \
+       table.o \
        task.o \
        time.o \
        uname.o \
diff --git a/lib/table.cc b/lib/table.cc
new file mode 100644 (file)
index 0000000..51d9e40
--- /dev/null
@@ -0,0 +1,254 @@
+#include "crucible/table.h"
+
+#include "crucible/string.h"
+
+namespace crucible {
+       namespace Table {
+               using namespace std;
+
+               Content
+               Fill(const char c)
+               {
+                       return [=](size_t width, size_t height) -> string {
+                               string rv;
+                               while (height--) {
+                                       rv += string(width, c);
+                                       if (height) {
+                                               rv += "\n";
+                                       }
+                               }
+                               return rv;
+                       };
+               }
+
+               Content
+               Text(const string &s)
+               {
+                       return [=](size_t width, size_t height) -> string {
+                               const auto lines = split("\n", s);
+                               string rv;
+                               size_t line_count = 0;
+                               for (const auto &i : lines) {
+                                       if (line_count++) {
+                                               rv += "\n";
+                                       }
+                                       if (i.length() < width) {
+                                               rv += string(width - i.length(), ' ');
+                                       }
+                                       rv += i;
+                               }
+                               while (line_count < height) {
+                                       if (line_count++) {
+                                               rv += "\n";
+                                       }
+                                       rv += string(width, ' ');
+                               }
+                               return rv;
+                       };
+               }
+
+               Content
+               Number(const string &s)
+               {
+                       return [=](size_t width, size_t height) -> string {
+                               const auto lines = split("\n", s);
+                               string rv;
+                               size_t line_count = 0;
+                               for (const auto &i : lines) {
+                                       if (line_count++) {
+                                               rv += "\n";
+                                       }
+                                       if (i.length() < width) {
+                                               rv += string(width - i.length(), ' ');
+                                       }
+                                       rv += i;
+                               }
+                               while (line_count < height) {
+                                       if (line_count++) {
+                                               rv += "\n";
+                                       }
+                                       rv += string(width, ' ');
+                               }
+                               return rv;
+                       };
+               }
+
+               Cell::Cell(const Content &fn) :
+                       m_content(fn)
+               {
+               }
+
+               Cell&
+               Cell::operator=(const Content &fn)
+               {
+                       m_content = fn;
+                       return *this;
+               }
+
+               string
+               Cell::text(size_t width, size_t height) const
+               {
+                       return m_content(width, height);
+               }
+
+               size_t
+               Dimension::size() const
+               {
+                       return m_elements.size();
+               }
+
+               size_t
+               Dimension::insert(size_t pos)
+               {
+                       ++m_next_pos;
+                       const auto insert_pos = min(m_elements.size(), pos);
+                       const auto it = m_elements.begin() + insert_pos;
+                       m_elements.insert(it, m_next_pos);
+                       return insert_pos;
+               }
+
+               void
+               Dimension::erase(size_t pos)
+               {
+                       const auto it = m_elements.begin() + min(m_elements.size(), pos);
+                       m_elements.erase(it);
+               }
+
+               size_t
+               Dimension::at(size_t pos) const
+               {
+                       return m_elements.at(pos);
+               }
+
+               Dimension&
+               Table::rows()
+               {
+                       return m_rows;
+               };
+
+               const Dimension&
+               Table::rows() const
+               {
+                       return m_rows;
+               };
+
+               Dimension&
+               Table::cols()
+               {
+                       return m_cols;
+               };
+
+               const Dimension&
+               Table::cols() const
+               {
+                       return m_cols;
+               };
+
+               const Cell&
+               Table::at(size_t row, size_t col) const
+               {
+                       const auto row_idx = m_rows.at(row);
+                       const auto col_idx = m_cols.at(col);
+                       const auto found = m_cells.find(make_pair(row_idx, col_idx));
+                       if (found == m_cells.end()) {
+                               static const Cell s_empty(Fill('.'));
+                               return s_empty;
+                       }
+                       return found->second;
+               };
+
+               Cell&
+               Table::at(size_t row, size_t col)
+               {
+                       const auto row_idx = m_rows.at(row);
+                       const auto col_idx = m_cols.at(col);
+                       return m_cells[make_pair(row_idx, col_idx)];
+               };
+
+               static
+               pair<size_t, size_t>
+               text_size(const string &s)
+               {
+                       const auto s_split = split("\n", s);
+                       size_t width = 0;
+                       for (const auto &i : s_split) {
+                               width = max(width, i.length());
+                       }
+                       return make_pair(width, s_split.size());
+               }
+
+               ostream& operator<<(ostream &os, const Table &table)
+               {
+                       const auto rows = table.rows().size();
+                       const auto cols = table.cols().size();
+                       vector<size_t> row_heights(rows, 1);
+                       vector<size_t> col_widths(cols, 1);
+                       // Get the size of all fixed- and minimum-sized content cells
+                       for (size_t row = 0; row < table.rows().size(); ++row) {
+                               vector<string> col_text;
+                               for (size_t col = 0; col < table.cols().size(); ++col) {
+                                       col_text.push_back(table.at(row, col).text(0, 0));
+                                       const auto tsize = text_size(*col_text.rbegin());
+                                       row_heights[row] = max(row_heights[row], tsize.second);
+                                       col_widths[col] = max(col_widths[col], tsize.first);
+                               }
+                       }
+                       // Render the table
+                       for (size_t row = 0; row < table.rows().size(); ++row) {
+                               vector<string> lines(row_heights[row], "");
+                               for (size_t col = 0; col < table.cols().size(); ++col) {
+                                       const auto& table_cell = table.at(row, col);
+                                       const auto table_text = table_cell.text(col_widths[col], row_heights[row]);
+                                       auto col_lines = split("\n", table_text);
+                                       col_lines.resize(row_heights[row], "");
+                                       for (size_t line = 0; line < row_heights[row]; ++line) {
+                                               if (col > 0) {
+                                                       lines[line] += table.mid();
+                                               }
+                                               lines[line] += col_lines[line];
+                                       }
+                               }
+                               for (const auto &line : lines) {
+                                       os << table.left() << line << table.right() << "\n";
+                               }
+                       }
+                       return os;
+               }
+
+               void
+               Table::left(const string &s)
+               {
+                       m_left = s;
+               }
+
+               void
+               Table::mid(const string &s)
+               {
+                       m_mid = s;
+               }
+
+               void
+               Table::right(const string &s)
+               {
+                       m_right = s;
+               }
+
+               const string&
+               Table::left() const
+               {
+                       return m_left;
+               }
+
+               const string&
+               Table::mid() const
+               {
+                       return m_mid;
+               }
+
+               const string&
+               Table::right() const
+               {
+                       return m_right;
+               }
+       }
+}
index f2e607e749ed0313d50cafb84ed97f0c217ec248..e8b8194e999655191dc719bb31be027454af917e 100644 (file)
@@ -8,6 +8,7 @@ PROGRAMS = \
        process \
        progress \
        seeker \
+       table \
        task \
 
 all: test
diff --git a/test/table.cc b/test/table.cc
new file mode 100644 (file)
index 0000000..7b8e910
--- /dev/null
@@ -0,0 +1,63 @@
+#include "tests.h"
+
+#include "crucible/table.h"
+
+using namespace crucible;
+using namespace std;
+
+void
+print_table(const Table::Table& t)
+{
+       cerr << "BEGIN TABLE\n";
+       cerr << t;
+       cerr << "END TABLE\n";
+       cerr << endl;
+}
+
+void
+test_table()
+{
+       Table::Table t;
+       t.insert_row(Table::endpos, vector<Table::Content> {
+               Table::Text("Hello, World!"),
+               Table::Text("2"),
+               Table::Text("3"),
+               Table::Text("4"),
+       });
+       print_table(t);
+       t.insert_row(Table::endpos, vector<Table::Content> {
+               Table::Text("Greeting"),
+               Table::Text("two"),
+               Table::Text("three"),
+               Table::Text("four"),
+       });
+       print_table(t);
+       t.insert_row(Table::endpos, vector<Table::Content> {
+               Table::Fill('-'),
+               Table::Text("ii"),
+               Table::Text("iii"),
+               Table::Text("iv"),
+       });
+       print_table(t);
+       t.mid(" | ");
+       t.left("| ");
+       t.right(" |");
+       print_table(t);
+       t.insert_col(1, vector<Table::Content> {
+               Table::Text("1"),
+               Table::Text("one"),
+               Table::Text("i"),
+               Table::Text("I"),
+       });
+       print_table(t);
+       t.at(2, 1) = Table::Text("Two\nLines");
+       print_table(t);
+}
+
+int
+main(int, char**)
+{
+       RUN_A_TEST(test_table());
+
+       exit(EXIT_SUCCESS);
+}