diff --git a/include/emergent/Console.hpp b/include/emergent/Console.hpp index e101b93..db59957 100644 --- a/include/emergent/Console.hpp +++ b/include/emergent/Console.hpp @@ -4,6 +4,7 @@ #include #include #include +#include // #include #ifdef __linux @@ -12,6 +13,8 @@ #endif +// #include + namespace emergent { // Console helper functions @@ -107,4 +110,338 @@ namespace emergent // Console commands static constexpr const char *Erase = "\x1B[2K\r"; }; + + + namespace console + { + // Draw a hozizontal line of a given length + struct Line { size_t length = 0; }; + + inline std::ostream &operator <<(std::ostream &dst, const Line &l) + { + for (size_t i=0; i c.item.length()) + // { + // const size_t space = c.width - c.item.length(); + + // dst << std::setw(space - space / 2) << "" + // << c.item + // << std::setw(space / 2) << ""; + // } + // else + // { + // dst << c.item.substr(0, c.width); + // } + + // return dst; + // } + + + // Return the size of the largest item in the values container + template inline size_t MaxItemSize(const T &values) + { + if (values.empty()) + { + return 0; + } + + #if defined(__cpp_lib_ranges) + return std::ranges::max(values, {}, &T::value_type::size).size(); + #else + return std::max_element( + values.begin(), values.end(), + [](auto a, auto b) { return a.size() < b.size(); } + )->size(); + #endif + } + + + // Helper class for printing a table of data. + // Column and row headers can be defined where required. + // Each cell can contain multiple strings - placed on separate lines. + // Column width can be fixed or calculated automatically. + template struct Table + { + struct Row + { + std::string header; + std::array, Columns> cells; + + + Row() = default; + Row(const std::string &header) : header(header) {} + + size_t Height() const + { + // Not actually widest item here, but largest vector in the cells array + return MaxItemSize(cells); + } + }; + + std::array headers; + std::vector rows; + size_t width = 0; // fixed column width, leave as 0 for auto column sizing + size_t padding = 1; // horizontal cell padding + + Table &Width(size_t value) + { + this->width = value; + return *this; + } + + Table &Padding(size_t value) + { + this->padding = value; + return *this; + } + + Table &Headers(const std::array &values) + { + this->headers = values; + return *this; + } + + Row &AddRow(const std::string &header = "") + { + return this->rows.emplace_back(header); + } + + + size_t Widest(const size_t column) const + { + if (this->width) + { + return this->width; + } + + size_t max = this->headers[column].size(); + + for (auto &r : this->rows) + { + max = std::max(max, MaxItemSize(r.cells[column])); + } + + return max; + } + + + std::array ColumnWidths() const + { + std::array widths = { 0 }; + + // find the maximum row header width + for (auto &r : this->rows) + { + widths[0] = std::max(widths[0], r.header.size()); + } + + // find the maximum string width for each column + for (size_t i=0; iWidest(i); + } + + return widths; + } + + + void ColumnHeaders(std::ostream &dst, const std::array &widths, const bool rowHeader) const + { + if (MaxItemSize(this->headers) == 0) + { + // There are no column headers, so return + return; + } + + dst << Console::Blue << "\u2502"; + + if (rowHeader) + { + dst << std::setw(widths[0] + 2 * this->padding) << "" << "\u2502"; + } + + for (size_t i=0; ipadding) << "" + // << Centre { this->headers[i], widths[i+1] } + // << std::setw(this->padding) << ""; + + dst << std::setw(widths[i + 1] + this->padding) + << this->headers[i] + << std::setw(this->padding) + << ""; + } + + dst << "\u2502\n"; + } + + + void RowHeader(std::ostream &dst, const std::array &widths, const bool first, std::optional header) const + { + dst << Console::Blue << "\u2502"; + + if (header) + { + dst << std::setw(widths[0] + this->padding) + << (first ? header.value() : "") + << std::setw(this->padding) << "" << "\u2502"; + } + + dst << Console::Reset; + } + + + static inline std::string_view RowItem(std::string_view item, const size_t width) + { + // truncate the string for when using fixed with columns + return item.substr(0, width); + } + + + void RowMain(std::ostream &dst, const std::array &widths, const Row &r, const size_t item) const + { + static constexpr std::array PALLETTE = { + Console::Reset, Console::Yellow, Console::Cyan, Console::Magenta + }; + + for (size_t i=0; ipadding) + << RowItem(cell[item], widths[i+1]) + << std::setw(this->padding) << ""; + } + else + { + // you could potentially have different amounts of data + // in each cell - so leave blank entries for anything missing + dst << std::setw(widths[i+1] + 2 * this->padding) << ""; + } + } + + dst << Console::Blue << "\u2502\n"; + } + + + std::ostream &Render(std::ostream &dst) const + { + const auto widths = this->ColumnWidths(); + const size_t bothPads = this->padding * 2; + const size_t total = std::accumulate(widths.begin(), widths.end(), widths.size() * bothPads); + const size_t header = widths[0] ? widths[0] + bothPads : 0; + const size_t main = total - (header ? header : bothPads); + + Table::Separator(dst, header, main, "\u250c", "\u252c", "\u2510"); + + this->ColumnHeaders(dst, widths, header); + + for (auto &r : this->rows) + { + Table::Separator(dst, header, main, "\u251c", "\u253c", "\u2524"); + + const size_t height = r.Height(); + + for (size_t y=0; yRowHeader(dst, widths, y == 0, header + ? std::make_optional(r.header) + : std::nullopt + ); + + this->RowMain(dst, widths, r, y); + } + } + + Table::Separator(dst, header, main, "\u2514", "\u2534", "\u2518"); + + return dst; + } + + + // Draw a table row separator - the left/middle/right indicate which unicode box drawing symbols + // to use so that it can be customised to also print the top and bottom of the table + static void Separator(std::ostream &dst, const size_t header, const size_t main, std::string_view left, std::string_view middle, std::string_view right) + { + dst << Console::Blue << left; + + if (header) + { + dst << Line { .length = header } << middle; + } + + dst << Line { .length = main } << right << '\n' << Console::Reset; + } + + + // Produce a table from a vector of values. The column headers will be the column index and the row headers will be the + // offset in the vector. The formatters will convert a value for a specific cell, multiple formatters will result in multiple + // representations within a cell drawn on different lines. + // For example: + // + // std::cout << Table<10>::From(data, { "%d", "0x%04x" }); + // + // which given a vector of uint16_t will print the decimal and hex values in each cell as follows + // ┌────┬────────────────────────────────────────────────────────────────────────────────┐ + // │ │ 0 1 2 3 4 5 6 7 8 9 │ + // ├────┼────────────────────────────────────────────────────────────────────────────────┤ + // │ 0 │ 1 2 3 4095 42 8192 12 435 223 1 │ + // │ │ 0x0001 0x0002 0x0003 0x0fff 0x002a 0x2000 0x000c 0x01b3 0x00df 0x0001 │ + // ├────┼────────────────────────────────────────────────────────────────────────────────┤ + // │ 10 │ 2 3 4095 42 8192 12 435 223 1 2 │ + // │ │ 0x0002 0x0003 0x0fff 0x002a 0x2000 0x000c 0x01b3 0x00df 0x0001 0x0002 │ + // ... + template static Table From(const T &values, std::initializer_list formatters) + { + const size_t size = values.size(); + + Table table; + + for (size_t i=0; i std::ostream &operator <<(std::ostream &dst, const Table &table) + { + return table.Render(dst); + } + } + }