Таблички рисуем

Давно приходилось выводить чего-нибудь в табличной форме на экран терминала? Ничего не расползалось? :).

Придумал крошечную библиотечку, которая умеет рисовать таблички.

Типа таких:

Ниже - код, который ее нарисовал (simple_table_example.c):

#include <stddef.h>
#include "simple_table.h"

// Пример:
// Нарисуем табличку , состоящую из 4 столбцов, которые мы подпишем как
// "Column 1", "Colun 2",  "Col 3" и "Colum Number Four", как то так:
//
//  Column 1|Colun 2|Col 3|Colum Number Four
//  --------+-------+-----+-----------------
//          |       |     |
//
// Заполним табличку разной чепухой
//
int main() {

  // Дескриптор таблички.
  table_layout_t t;

  // Задаем настройки: использовать символы UTF8 для рамок, печатать "% " (процент и пробел)
  // перед каждой новой строчкой и символ # с пробелом в конце каждой строчки
  table_layout(&t, true, "% ", " #\r\n");

  // Рисуем заголовок нашей таблички с названиями стобцов
  table_header(&t, "Column 1", "Colun 2","Col 3", "Colum Number Four", NULL);

  // Начинаем заполнять данными. Заполнение идет слева направо, сверху вниз.
  // Если значение не влазит по ширине, то будет обрезано
  table_data(&t, "Long Text");
  table_data(&t, "Short");
  table_data(&t, "Exact");
  table_data(&t, "Very Long Text");

  table_data(&t, 10000.123456f);
  table_data(&t, 655355555);
  table_data(&t, -65535);
  table_data(&t, "Another quite long text");

  table_data(&t, 1);
  table_data(&t, -1);
  table_data(&t, 666.666f);
  table_data(&t, 777.777);


  // На экране (в терминале) должно появится что-то вроде этого:
  //
  // % Column 1│Colun 2│Col 3│Colum Number Four #
  // % ────────┼───────┼─────┼───────────────── #
  // % Long Tex│Short  │Exact│Very Long Text    #
  // % 10000.12│6553555│-6553│Another quite lon #
  // % 1       │-1     │666.6│777.776978        #
  //
  // Если вместо символов рамок на экране отображается не пойми что, то замените второй аргумент 
  // в вызове table_layout() на false, что означает - не использовать UTF8, а использовать +,- и|
  // для рисования табличек
  //
  return 0;
}

Используемая “библиотека“ написана на Си, должна сожительствовать без проблем с Си++ кодом

simple_table.h:

#ifndef simple_table_h
#define simple_table_h

#include <stdbool.h>

#define TABLE_COLUMN_MAX         16   // Максимальное число столбцов
#define TABLE_COLUMN_MAX_LENGTH  64   // Максимальная ширина одного столбца


// Прототипы. Запиханы под extern "C", на всякий случай.
//
#ifdef __cplusplus
extern "C" {
#endif

// Всякая таблица начинается с ее дескриптора - структуры для временного хранения настроек таблицы.
// Все функции работы с таблицой своим первым аргументом ожидают указатель на экземпляр этой структуры.
// Пример определения и использования:
//
//  table_layout_t t;          // Дескриптор. Можно, но не нужно инициализировать.
//                             // Можно использовать многократно, для вывода разных таблиц. Scratch area
//
//  table_layout( &t, ... );   // Задаем параметры таблицы
//  table_header( &t, ... );   // Рисуем заголовок
//  table_data( &t, ... );     // Посылаем данные..
//  table_data( &t, ... );     // Посылаем данные..
//  table_data( &t, ... );     // Посылаем данные..
//  ....
typedef struct {
  unsigned char cols;         
  unsigned char cur;
  unsigned char width[TABLE_COLUMN_MAX];  
  const char   *hvc[3];
  const char   *tr[2];
} table_layout_t;


// Общие настройки таблички. Эту функцию можно вызывать в любое время на любом этапе печати таблички, 
// но нужно вызвать хотя бы один раз ДО вызова остальных функций библиотечки (см. пример simple_table_example.c)
//
// Можно менять настройки для каждой выводимой строки таблицы, если вызывать эту функцию перед выводом очередной
// строки таблицы.
//
// Аргументы: 
//
//   t           : Указатель на переменную типа table_layout_t, можно - неинициализированную.
//   utf8_enable : Использовать +-| (false) или псевдографику (true)
//   pre         : Указатель на строку, которая будет печататься _перед_ каждой новой строкой таблицы 
//                 (включая заголовок таблицы). Может быть пустой строкой или NULL, что то же самое, 
//                 что и пустая строка, может содержать ANSI ESC последовательности для управления цветом
//                 выводимого текста и его атрибуты, может содержать UTF8 символы (смайлики, стрелочки и т.п.
//                 например)
//   post        : Указатель на строку, которая будет печататься _после_ каждой новой строкой таблицы 
//                 (включая заголовок таблицы). Обычно тут что-то вроде "\r\n" или NULL, что то же самое,
//                 что и "\r\n", но может быть что угодно, главное, не забыть \r\n в конце а то таблица 
//                 превратится в строчку
//
// Пример:
//   table_layout_t t;
//   table_layout(&t, false, NULL, NULL);    // используем ASCII рамки 
//   table_layout(&t, true, "# ", " #\r\n"); // используем utf8 рамки, рисуем # в начале и конце каждой 
//                                           // строки таблицы
//
void table_layout(table_layout_t *t,bool utf8_enable,const char *pre,const char *post);

// Печать заголовка таблички - названия столбцов с разделителями и горизонтальной линией
// Количество столбцов определяется тем, сколько аргументов было использовано, например:
//
//   table_header(&t, "Column 1", "Colun 2","Col 3", "Colum Number Four", NULL);
//
// Этот код нарисует заголовок таблицы, состоящей из четырех колонок, ширина каждой колонки
// определяется длинной (в байтах) названия колонки. В примере выше ширина первой колонки
// будет 8 символов, а третьей - пять.
//
// Аргументы:
//  t        : Указатель на табличку, 
// first_col : Название первого столбца таблички
// ...       : произвольное количество const char * аргументов, задающих названия столбцов
//
//  Последний аргумент всегда должен быть NULL, а то все грохнется
//
void table_header( table_layout_t *t, const char *first_col, ... );


// Эти четыре функции определены в table.c через самодельный "темплейт" table_data_template()
// Когда компилятор будет встречать вызов вида table_data(...) то будет вызываться одна из функций ниже
// (какая именно - определяется типом аргмента)
//
void table_data_asciiz(table_layout_t *t,const char *data);
void table_data_unsigned(table_layout_t *t,unsigned int data);
void table_data_signed(table_layout_t *t,signed int data);
void table_data_float(table_layout_t *t,float data);

void __attribute__((noreturn)) table_data_bug(table_layout_t *t,unsigned int data);

#ifdef __cplusplus
};
#endif

// void table_data(table_layout_t *t, ARG)
//
// В зависимости от типа входных данных (ARG), на этапе компиляции, вызов table_data(...) будет преобразован
// к одному из вызовов ниже.
//
// Пример:
//   int a = 10; float b = 2.2f; const char *c = "!!!"
//   table_layout_t t;
//   table_layout(&t, true,NULL,NULL);
//   table_header(&t, "Col1", "Col2", "Col3", NULL);
//   table_data(&t, a);
//   table_data(&t, b);
//   table_data(&t, c);
// 
// Код ниже компилируется GCC и совместимыми с ним компиляторами, с поддержкой C11
// Ключевое слово _Generic выбирает выражение из списка по типу переменной _Item:
// Если _Item является float-переменной, то будет выбрано table_data_float и т.д.
// Если тип переменной _Item такой, что ничего из списка не подходит (например, _Item 
// у нас определен как uint64_t ), то будет вызвана table_data_bug(), которая вызывает abort()
//
#define table_data(_Table, _Item) _Generic((_Item), \
    const char *   : table_data_asciiz, \
    char *         : table_data_asciiz, \
    unsigned int   : table_data_unsigned, \
    unsigned short : table_data_unsigned, \
    unsigned long  : table_data_unsigned, \
    unsigned char  : table_data_unsigned, \
    signed int     : table_data_signed, \
    signed short   : table_data_signed, \
    signed long    : table_data_signed, \
    signed char    : table_data_signed, \
    float          : table_data_float, \
    double         : table_data_float, \
    default        : table_data_bug \
)(_Table, _Item)

#endif // simple_table_h

simple_table.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdarg.h>

#include "simple_table.h"


// Хелпер #1: Печать обычной Си-строчки на экран. В данном случае - на stdout 
// (на ESP32 так можно, на других платформах - понятия не имею)
//
// переопределите putsn(), если fputs(..., stdout) не поддерживается на вашей платформе, 
// например, так:
//
// #define putsn(_String)   printf("%s", _String)
//
#define putsn(_String)   fputs(_String, stdout)


// Функция-шаблон. Ну, такие вот у нас шаблоны в Си.
// Генерирует функцию печати элемента данных таблицы.
//
#define table_data_template(_Name, _TypeName, _Format) \
\
  void table_data_ ## _Name (table_layout_t *t, _TypeName col ) { \
\
    char tmp[16]; \
    char tmp2[TABLE_COLUMN_MAX_LENGTH]; \
\
    if (!t) \
      return ; \
\
  if (!t->cur) \
    putsn(t->tr[0]); \
\
  sprintf(tmp, _Format, t->width[t->cur], t->width[t->cur]); \
  snprintf(tmp2,t->width[t->cur] + 1,tmp,col); \
  putsn(tmp2); \
\
  t->cur++; \
  if (t->cur >= t->cols) { \
    t->cur = 0; \
    putsn(t->tr[1]); \
  } else \
    putsn(t->hvc[1]); \
}

// Создадим несколько функций по шаблону выше: для печати строчки, знаковых,
// беззнаковых и чисел с плавающей точкой. Если сильно приспичит - можно добавить
// еще какой-нибудь свой собственный тип, по образу и подобию (см. ниже)
//
// Неиспользуемые функции не будут слинкованы в результирующий бинарник 
// (по крайней мере в среде Arduino)
//
//
#pragma GCC diagnostic ignored "-Wformat-extra-args"  // Выключаем предупреждение о лишнем аргументе в *printf

table_data_template(asciiz, const char *, "%%-%u.%us");   //table_data_asciiz()
table_data_template(unsigned, unsigned int, "%%-%uu");    //table_data_unsigned()
table_data_template(signed, signed int, "%%-%ud");        //table_data_signed()
table_data_template(float, float, "%%-%uf");              //table_data_float()

#pragma GCC diagnostic warning "-Wformat-extra-args"  // Включаем назад

// Будет время - придумаю что-нибудь получше. А пока пусть будет так: 
// при попытке добавить очередной элемент, если тип данных не поддерживается, то будет вызван abort()
//
void table_data_bug(table_layout_t *t, unsigned int col) {
  t = t;
  col = col;
  abort();
}

//
//
void table_layout(table_layout_t *t,
                  bool            utf8_enable,
                  const char     *pre,
                  const char     *post) {
  if (t) {
    t->hvc[0] = utf8_enable ? "─" : "-";
    t->hvc[1] = utf8_enable ? "│" : "|";
    t->hvc[2] = utf8_enable ? "┼" : "+";

    t->tr[0] = pre ? pre : "";
    t->tr[1] = post ? post : "\r\n";
  }
}

//
//
void table_header( table_layout_t *t, const char *first_col, ... ) {

  int i = 0;
  const char *col = first_col;

  va_list arg;
  va_start(arg, first_col);

  putsn(t->tr[0]);
  
  while(col) {
    putsn(col);
    t->width[i++] = strlen(col);
    if (i >= sizeof(t->width)/sizeof(t->width[0]))
      col = NULL;
    else
      col = va_arg(arg, const char *);
    if (col)
      putsn(t->hvc[1]); // | - vertical bar
  }
  t->cols = i;
  t->cur = 0;
  va_end(arg);

  putsn(t->tr[1]);
  putsn(t->tr[0]);

  for (i = 0; i < t->cols; i++) {
    for (int j=0;j<t->width[i];j++)
      putsn(t->hvc[0]); // horisontal bar
    if (i + 1 < t->cols)
      putsn(t->hvc[2]); // cross
  }                       
  putsn(t->tr[1]);
}

Скомпилировать simple_table_example.c можно так:

gcc -Wall simple_table.c simple_table_example.c

Чтобы в Arduino Serial Monitor рисовать таблички, нужно заменить /true/ на /false/ в вызове table_layout(…)

Удачи в рисовании табличек!

1 лайк

оно конечно хорошо, но в ESP32 вообще нет меню по установке опций компиляции, а в STM32 этой опции в меню нет, а мы жеж очень ленивые чтобы куда-то лезть, что-то ручками менять

Ну нет, так нет. Не нужна она. Это для красоты, чтобы все варнинги показывать.

Тут твоя прелесть находится. В файлах c_flags и cpp_flags:

Ещё со времен ms dos были эти рисовалки псевдографики.

Историю почитайте :man_facepalming:

чё её читать-то, мы ее и так помним :slight_smile:

Вопрос то какой у ТС? Данная тема, по моему, для раздела Проекты. Не?

На проект не тянет :slight_smile:

Чорт, забыл вопрос задать.

Исправляюсь:

“Функция для рисования табличек простеньких кому-нибудь нужна?“

В хозяйстве пригодится

2 лайка