From a59a02174f45ee82f0785e3e04572a39debd0265 Mon Sep 17 00:00:00 2001 From: Zygo Blaxell Date: Sat, 28 Jan 2023 21:26:51 -0500 Subject: [PATCH] table: add a simple text table renderer 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 --- include/crucible/table.h | 106 ++++++++++++++++ lib/Makefile | 1 + lib/table.cc | 254 +++++++++++++++++++++++++++++++++++++++ test/Makefile | 1 + test/table.cc | 63 ++++++++++ 5 files changed, 425 insertions(+) create mode 100644 include/crucible/table.h create mode 100644 lib/table.cc create mode 100644 test/table.cc diff --git a/include/crucible/table.h b/include/crucible/table.h new file mode 100644 index 0000000..34eb90d --- /dev/null +++ b/include/crucible/table.h @@ -0,0 +1,106 @@ +#ifndef CRUCIBLE_TABLE_H +#define CRUCIBLE_TABLE_H + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace crucible { + namespace Table { + using namespace std; + + using Content = function; + const size_t endpos = numeric_limits::max(); + + Content Fill(const char c); + Content Text(const string& s); + + template + 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 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, 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 void insert_row(size_t pos, const T& container); + template 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 + 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 + 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 diff --git a/lib/Makefile b/lib/Makefile index 24cd309..cddea6f 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -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 index 0000000..51d9e40 --- /dev/null +++ b/lib/table.cc @@ -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 + 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 row_heights(rows, 1); + vector 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 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 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; + } + } +} diff --git a/test/Makefile b/test/Makefile index f2e607e..e8b8194 100644 --- a/test/Makefile +++ b/test/Makefile @@ -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 index 0000000..7b8e910 --- /dev/null +++ b/test/table.cc @@ -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::Text("Hello, World!"), + Table::Text("2"), + Table::Text("3"), + Table::Text("4"), + }); + print_table(t); + t.insert_row(Table::endpos, vector { + Table::Text("Greeting"), + Table::Text("two"), + Table::Text("three"), + Table::Text("four"), + }); + print_table(t); + t.insert_row(Table::endpos, vector { + 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::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); +}