#include "vega.h" #include #include static const char *const tone_names[VEGA_TONES] = {"normal", "muted", "accent", "good", "warning", "danger"}; /* ---- CLI ------------------------------------------------------------ */ /* Text for a pipe: what would act on the output instead of being shown * becomes its picture (vega_cell), so a value never breaks a line nor turns * one around. */ static void put_text(FILE *out, const char *text, size_t length) { for (size_t at = 0; at < length;) { uint32_t codepoint; size_t size = vega_decode(text, length, at, &codepoint); if (vega_cell(codepoint) == codepoint) fwrite(text + at, 1, size, out); else { uint32_t picture = vega_cell(codepoint); fputc(0xE0 | (int)(picture >> 12), out); fputc(0x80 | (int)(picture >> 6 & 0x3F), out); fputc(0x80 | (int)(picture & 0x3F), out); } at += size; } } static void pad(FILE *out, unsigned count) { while (count--) fputc(' ', out); } static const char *const colors[VEGA_TONES] = {"", "\033[2m", "\033[1;36m", "\033[32m", "\033[33m", "\033[31m"}; static void tone_start(FILE *out, VegaTone tone, bool color) { if (color && tone) fputs(colors[tone], out); } static void tone_end(FILE *out, VegaTone tone, bool color) { if (color && tone) fputs("\033[0m", out); else if (!color && tone) fprintf(out, " (%s)", tone_names[tone]); } static void print_list(FILE *out, PureApp *app, const VegaPart *part, bool color) { char text[VEGA_TEXT]; size_t length, rows = vega_rows(app, part), focus = vega_focus(app, part); for (size_t row = 0; row < rows; ++row) { VegaTone tone = vega_tone(app, part, row); fputs(row == focus ? "> " : " ", out); tone_start(out, tone, color); if (vega_text(app, (uint32_t)part->label, row, text, &length)) put_text(out, text, length); if (part->badge >= 0 && vega_text(app, (uint32_t)part->badge, row, text, &length) && length) { fputs(" [", out); put_text(out, text, length); fputc(']', out); } if (part->detail >= 0 && vega_text(app, (uint32_t)part->detail, row, text, &length) && length) { fputs(" - ", out); put_text(out, text, length); } tone_end(out, tone, color); fputc('\n', out); } } static void print_table(FILE *out, PureApp *app, const VegaPart *part, bool color) { const PureModule *m = app->module; size_t rows = vega_rows(app, part), focus = vega_focus(app, part); unsigned widths[PURE_MODULE_VIEWS]; char text[VEGA_TEXT]; size_t length; for (unsigned c = 0; c < part->column_count; ++c) { const char *name = m->view_names[part->columns[c]]; widths[c] = vega_text_width(name, strlen(name)); for (size_t row = 0; row < rows; ++row) if (vega_text(app, part->columns[c], row, text, &length)) { unsigned width = vega_text_width(text, length); if (width > widths[c]) widths[c] = width; } } fputs(" ", out); for (unsigned c = 0; c < part->column_count; ++c) { const char *name = m->view_names[part->columns[c]]; fputs(name, out); if (c + 1 < part->column_count) pad(out, widths[c] - (unsigned)strlen(name) + 2); } fputc('\n', out); for (size_t row = 0; row < rows; ++row) { VegaTone tone = vega_tone(app, part, row); fputs(row == focus ? "> " : " ", out); tone_start(out, tone, color); for (unsigned c = 0; c < part->column_count; ++c) { bool ok = vega_text(app, part->columns[c], row, text, &length); if (!ok) { memcpy(text, "-", 2); length = 1; } put_text(out, text, length); if (c + 1 < part->column_count) pad(out, widths[c] - vega_text_width(text, length) + 2); } tone_end(out, tone, color); fputc('\n', out); } } void vega_print(FILE *out, PureApp *app, const VegaScreen *screen, bool color) { const PureModule *m = app->module; char text[VEGA_TEXT]; size_t length; if (screen->title >= 0 && vega_text(app, (uint32_t)screen->title, 0, text, &length)) { if (color) fputs("\033[1m", out); put_text(out, text, length); if (color) fputs("\033[0m", out); fputc('\n', out); } for (unsigned i = 0; i < screen->count; ++i) { const VegaPart *part = &screen->parts[i]; const char *name = m->view_names[part->view]; if (part->kind == VEGA_FIELD) { fprintf(out, "%s: ", name); if (vega_text(app, part->view, 0, text, &length)) put_text(out, text, length); else fputs("-", out); fputc('\n', out); continue; } fprintf(out, "%s (%zu)\n", name, vega_rows(app, part)); if (part->kind == VEGA_LIST) print_list(out, app, part, color); else print_table(out, app, part, color); } } /* ---- NUI ------------------------------------------------------------ */ /* A code point as it is said: what would act on the output (controls, line * and paragraph separators) as a space, the controls that turn text around * not at all (0), else itself. */ static uint32_t spoken(uint32_t codepoint) { if (vega_visible(codepoint) != codepoint) return ' '; return vega_cell(codepoint) != codepoint ? 0 : codepoint; } /* Spoken text: as spoken says, without the spaces it ends with. */ static void speak(FILE *out, const char *text, size_t length) { size_t end = 0; for (size_t at = 0; at < length;) { uint32_t codepoint, said; at += vega_decode(text, length, at, &codepoint); if ((said = spoken(codepoint)) && said != ' ') end = at; } for (size_t at = 0; at < end;) { uint32_t codepoint, said; size_t size = vega_decode(text, end, at, &codepoint); if ((said = spoken(codepoint)) == ' ') fputc(' ', out); else if (said) fwrite(text + at, 1, size, out); at += size; } } /* "Item 2 of 3: milk, done." Tones other than normal and muted are said. */ static void say_focus(FILE *out, PureApp *app, const VegaPart *part) { const PureModule *m = app->module; char text[VEGA_TEXT]; size_t length, rows = vega_rows(app, part); if (!rows) { fprintf(out, "%s is empty.\n", m->view_names[part->view]); return; } size_t focus = vega_focus(app, part); if (focus >= rows) { fprintf(out, "%s: none of %zu focused.\n", m->view_names[part->view], rows); return; } fprintf(out, "Item %zu of %zu: ", focus + 1, rows); VegaTone tone = vega_tone(app, part, focus); if (tone >= VEGA_ACCENT) fprintf(out, "%s, ", tone_names[tone]); if (part->kind == VEGA_LIST) { if (vega_text(app, (uint32_t)part->label, focus, text, &length)) speak(out, text, length); int more[] = {part->badge, part->detail}; for (unsigned k = 0; k < 2; ++k) if (more[k] >= 0 && vega_text(app, (uint32_t)more[k], focus, text, &length) && length) { fputs(", ", out); speak(out, text, length); } } else { for (unsigned c = 0; c < part->column_count; ++c) { fprintf(out, "%s%s ", c ? ", " : "", m->view_names[part->columns[c]]); if (vega_text(app, part->columns[c], focus, text, &length)) speak(out, text, length); else fputs("none", out); } } fputs(".\n", out); } void vega_say(FILE *out, PureApp *app, const VegaScreen *screen, bool all) { const PureModule *m = app->module; char text[VEGA_TEXT]; size_t length; if (!all) { /* The focused item where a list shows it, else the first list. */ const VegaPart *first = NULL; for (unsigned i = 0; i < screen->count; ++i) { const VegaPart *part = &screen->parts[i]; if (part->kind == VEGA_FIELD) continue; if (!first) first = part; if (vega_focus(app, part) < vega_rows(app, part)) { say_focus(out, app, part); return; } } if (first) say_focus(out, app, first); return; } if (screen->title >= 0 && vega_text(app, (uint32_t)screen->title, 0, text, &length)) { speak(out, text, length); fputs(".\n", out); } for (unsigned i = 0; i < screen->count; ++i) { const VegaPart *part = &screen->parts[i]; const char *name = m->view_names[part->view]; if (part->kind == VEGA_FIELD) { fprintf(out, "%s: ", name); if (vega_text(app, part->view, 0, text, &length)) speak(out, text, length); else fputs("none", out); fputs(".\n", out); continue; } size_t rows = vega_rows(app, part); fprintf(out, "%s, %zu item%s.\n", name, rows, rows == 1 ? "" : "s"); if (rows) say_focus(out, app, part); } } /* ---- TUI ------------------------------------------------------------ */ bool vega_grid(VegaGrid *grid, unsigned width, unsigned height) { grid->span_count = grid->text_used = 0; size_t count = (size_t)width * height; VegaCell *cells = calloc(count ? count : 1, sizeof(*cells)), *shown = calloc(count ? count : 1, sizeof(*shown)); struct VegaRow *rows = calloc(height ? height : 1, sizeof(*rows)); if (!cells || !shown || !rows) { free(cells); free(shown); free(rows); return false; } for (unsigned r = 0; r < height; ++r) rows[r].part = -1; free(grid->cells); free(grid->shown); free(grid->rows); grid->cells = cells; grid->shown = shown; grid->rows = rows; grid->painted = false; grid->width = width; grid->height = height; return true; } void vega_grid_free(VegaGrid *grid) { free(grid->cells); free(grid->shown); free(grid->rows); free(grid->spans); free(grid->text); *grid = (VegaGrid){0}; } /* Where text goes. reach is where the pen's last text stopped (written: it * wrote some): text written there follows it. right sets its text flush * with its end, when set as type. */ typedef struct { VegaGrid *grid; unsigned row, column, end; uint8_t tone; bool focus, strong, written, right; unsigned reach; } Pen; static Pen pen_at(VegaGrid *grid, unsigned row, unsigned column, unsigned end, uint8_t tone, bool focus, bool strong) { return (Pen){grid, row, column, end, tone, focus, strong, false, false, 0}; } /* The text as a span of the columns [pen's column, end), for a surface that * sets type (the cells get it too; after them, stop() says where they * reached). Out of memory, the span is lost: the cells still show it. */ static void span(Pen *pen, const char *text, size_t length, unsigned end) { VegaGrid *g = pen->grid; if (!length || pen->column >= end) return; if (g->span_count == g->spans_allocated) { size_t n = g->spans_allocated ? 2 * g->spans_allocated : 64; VegaSpan *grown = realloc(g->spans, n * sizeof(*grown)); if (!grown) return; g->spans = grown; g->spans_allocated = n; } if (g->text_used + length > g->text_allocated) { size_t n = g->text_allocated ? g->text_allocated : 4096; while (n < g->text_used + length) n *= 2; char *grown = realloc(g->text, n); if (!grown) return; g->text = grown; g->text_allocated = n; } memcpy(g->text + g->text_used, text, length); g->spans[g->span_count++] = (VegaSpan){pen->row, pen->column, end, pen->column, (uint32_t)g->text_used, (uint32_t)length, pen->tone, pen->focus, pen->strong, pen->written && pen->reach == pen->column, pen->right}; g->text_used += length; } /* The pen's text was written: where its cells reached. */ static void stop(Pen *pen, size_t spans) { VegaGrid *g = pen->grid; if (g->span_count > spans) g->spans[g->span_count - 1].reach = pen->column; pen->written = true; pen->reach = pen->column; } /* Write text from the pen's column up to its end; a wide character that * does not fit is left out, and a mark joins the character before it. */ static void put_cells(Pen *pen, const char *text, size_t length); static void write_text(Pen *pen, const char *text, size_t length) { size_t spans = pen->grid->span_count; span(pen, text, length, pen->end); put_cells(pen, text, length); stop(pen, spans); } static void put_cells(Pen *pen, const char *text, size_t length) { VegaCell *line = pen->grid->cells + (size_t)pen->row * pen->grid->width; for (size_t at = 0; at < length;) { uint32_t codepoint; at += vega_decode(text, length, at, &codepoint); codepoint = vega_cell(codepoint); unsigned width = vega_width(codepoint); if (!width) { if (pen->column && !line[pen->column - 1].mark) { unsigned base = pen->column - 1; while (base && !line[base].ch) --base; line[base].mark = codepoint; } continue; } if (pen->column + width > pen->end) { pen->column = pen->end; return; } line[pen->column] = (VegaCell){codepoint, 0, pen->tone, pen->focus, pen->strong}; if (width == 2) line[pen->column + 1] = (VegaCell){0, 0, pen->tone, pen->focus, pen->strong}; pen->column += width; } } static void write_cstr(Pen *pen, const char *text) { write_text(pen, text, strlen(text)); } /* Fill the rest of the pen's span with spaces in its style (a focused row * is highlighted across). */ static void fill(Pen *pen) { VegaCell *line = pen->grid->cells + (size_t)pen->row * pen->grid->width; while (pen->column < pen->end) line[pen->column++] = (VegaCell){' ', 0, pen->tone, pen->focus, pen->strong}; } /* Text cut to width with an ellipsis. */ static void write_fit(Pen *pen, const char *text, size_t length, unsigned width) { unsigned end = pen->end; size_t spans = pen->grid->span_count; if (pen->column + width < end) pen->end = pen->column + width; span(pen, text, length, pen->end); if (vega_text_width(text, length) > pen->end - pen->column && pen->end > pen->column) { --pen->end; put_cells(pen, text, length); pen->end = end; put_cells(pen, "\xe2\x80\xa6", 3); } else { put_cells(pen, text, length); pen->end = end; } stop(pen, spans); } static const char *key_symbol(const char *key) { static const struct { const char *key, *symbol; } symbols[] = { {"ArrowUp", "\xe2\x86\x91"}, {"ArrowDown", "\xe2\x86\x93"}, {"ArrowLeft", "\xe2\x86\x90"}, {"ArrowRight", "\xe2\x86\x92"}, {"Space", "Space"}, {"Enter", "Enter"}, {"Backspace", "Bksp"}, {"Delete", "Del"}, {"Escape", "Esc"}, {"PageUp", "PgUp"}, {"PageDown", "PgDn"}}; for (size_t i = 0; i < sizeof(symbols) / sizeof(*symbols); ++i) if (!strcmp(symbols[i].key, key)) return symbols[i].symbol; if (!strncmp(key, "Key", 3)) return key + 3; if (!strncmp(key, "Digit", 5)) return key + 5; return key; } static void draw_list(VegaGrid *grid, PureApp *app, const VegaPart *part, int index, size_t first, unsigned top, unsigned height) { char text[VEGA_TEXT]; size_t length, rows = vega_rows(app, part), focus = vega_focus(app, part); for (unsigned r = 0; r < height && first + r < rows; ++r) { size_t row = first + r; grid->rows[top + r] = (struct VegaRow){index, row}; bool focused = row == focus; VegaTone tone = vega_tone(app, part, row); Pen pen = pen_at(grid, top + r, 0, grid->width, (uint8_t)tone, focused, false); write_cstr(&pen, focused ? "> " : " "); pen.written = false; /* the label starts at its column, however wide the marker is set */ char badge[VEGA_TEXT]; size_t badge_length = 0; if (part->badge >= 0 && !vega_text(app, (uint32_t)part->badge, row, badge, &badge_length)) badge_length = 0; unsigned badge_width = badge_length ? vega_text_width(badge, badge_length) + 2 : 0; unsigned room = grid->width > pen.column + badge_width ? grid->width - pen.column - badge_width : 0; if (vega_text(app, (uint32_t)part->label, row, text, &length)) write_fit(&pen, text, length, room); if (part->detail >= 0 && vega_text(app, (uint32_t)part->detail, row, text, &length) && length) { unsigned used = pen.column; uint8_t saved = pen.tone; pen.tone = pen.tone ? pen.tone : VEGA_MUTED; write_cstr(&pen, " "); unsigned left = grid->width > badge_width + pen.column ? grid->width - badge_width - pen.column : 0; if (pen.column - used == 2) write_fit(&pen, text, length, left); pen.tone = saved; } if (badge_length && grid->width >= badge_width) { /* The badge ends at the last column, two cells after the label at least. */ unsigned start = grid->width - (badge_width - 2); if (pen.column < start) { Pen gap = pen_at(grid, pen.row, pen.column, start, pen.tone, focused, false); fill(&gap); } pen.column = start > pen.column ? start : pen.column; pen.written = false; pen.right = true; write_fit(&pen, badge, badge_length, badge_width - 2); } fill(&pen); } } static void draw_table(VegaGrid *grid, PureApp *app, const VegaPart *part, int index, size_t first, unsigned top, unsigned height) { const PureModule *m = app->module; char text[VEGA_TEXT]; size_t length, rows = vega_rows(app, part), focus = vega_focus(app, part); unsigned visible = height ? height - 1 : 0, widths[PURE_MODULE_VIEWS], total = 2; /* Columns as wide as their header and the rows on screen, shared out when too wide. */ for (unsigned c = 0; c < part->column_count; ++c) { const char *name = m->view_names[part->columns[c]]; widths[c] = vega_text_width(name, strlen(name)); for (unsigned r = 0; r < visible && first + r < rows; ++r) if (vega_text(app, part->columns[c], first + r, text, &length)) { unsigned width = vega_text_width(text, length); if (width > widths[c]) widths[c] = width; } total += widths[c] + 2; } if (part->column_count && total > grid->width) { unsigned share = grid->width > 2 + 2 * part->column_count ? (grid->width - 2) / part->column_count - 2 : 1; for (unsigned c = 0; c < part->column_count; ++c) if (widths[c] > share) widths[c] = share; } if (!height) return; Pen head = pen_at(grid, top, 2, grid->width, VEGA_MUTED, false, true); for (unsigned c = 0; c < part->column_count; ++c) { const char *name = m->view_names[part->columns[c]]; unsigned start = head.column; write_fit(&head, name, strlen(name), widths[c]); head.column = start + widths[c] + 2 < grid->width ? start + widths[c] + 2 : grid->width; } for (unsigned r = 0; r < visible && first + r < rows; ++r) { size_t row = first + r; bool focused = row == focus; grid->rows[top + 1 + r] = (struct VegaRow){index, row}; Pen pen = pen_at(grid, top + 1 + r, 0, grid->width, (uint8_t)vega_tone(app, part, row), focused, false); write_cstr(&pen, focused ? "> " : " "); pen.written = false; for (unsigned c = 0; c < part->column_count; ++c) { unsigned start = pen.column, end = start + widths[c] + 2 < grid->width ? start + widths[c] + 2 : grid->width; if (vega_text(app, part->columns[c], row, text, &length)) write_fit(&pen, text, length, widths[c]); else write_cstr(&pen, "-"); if (pen.column < end) { Pen gap = pen_at(grid, pen.row, pen.column, end, pen.tone, focused, false); fill(&gap); } pen.column = end; } fill(&pen); } } bool vega_hit(const VegaGrid *grid, unsigned line, unsigned *part, size_t *item) { if (line >= grid->height || grid->rows[line].part < 0) return false; *part = (unsigned)grid->rows[line].part; *item = grid->rows[line].item; return true; } void vega_draw(VegaGrid *grid, PureApp *app, const VegaScreen *screen, const char *prompt, size_t prompt_length, bool typing) { const PureModule *m = app->module; grid->span_count = grid->text_used = 0; for (size_t i = 0; i < (size_t)grid->width * grid->height; ++i) grid->cells[i] = (VegaCell){' ', 0, 0, false, false}; for (unsigned r = 0; r < grid->height; ++r) grid->rows[r].part = -1; grid->cursor_row = grid->cursor_column = -1; if (!grid->width || !grid->height) return; char text[VEGA_TEXT]; size_t length; unsigned row = 0, bottom = grid->height - 1 - (screen->prompt >= 0 && grid->height > 1); if (screen->title >= 0 && row < bottom && vega_text(app, (uint32_t)screen->title, 0, text, &length)) { Pen pen = pen_at(grid, row++, 0, grid->width, VEGA_NORMAL, false, true); write_fit(&pen, text, length, grid->width); } unsigned groups = 0; for (unsigned i = 0; i < screen->count; ++i) { const VegaPart *part = &screen->parts[i]; if (part->kind != VEGA_FIELD) { ++groups; continue; } if (row >= bottom) continue; Pen pen = pen_at(grid, row++, 0, grid->width, VEGA_MUTED, false, false); write_cstr(&pen, m->view_names[part->view]); write_cstr(&pen, " "); pen.tone = VEGA_NORMAL; if (vega_text(app, part->view, 0, text, &length)) write_fit(&pen, text, length, grid->width); else { pen.tone = VEGA_MUTED; write_cstr(&pen, "-"); } } /* Lists and tables share the rows left; each keeps its focused row in view * and only its visible rows are evaluated. */ unsigned left = bottom > row ? bottom - row : 0; for (unsigned i = 0, group = 0; i < screen->count; ++i) { const VegaPart *part = &screen->parts[i]; if (part->kind == VEGA_FIELD) continue; unsigned share = left / groups + (group < left % groups); ++group; if (!share) continue; size_t rows = vega_rows(app, part), focus = vega_focus(app, part); Pen head = pen_at(grid, row, 0, grid->width, VEGA_NORMAL, false, true); write_cstr(&head, m->view_names[part->view]); char count[32]; snprintf(count, sizeof(count), " (%zu)", rows); head.strong = false; head.tone = VEGA_MUTED; write_cstr(&head, count); unsigned area = share - 1, visible = part->kind == VEGA_TABLE && area ? area - 1 : area; size_t *first = &grid->first[i]; *first = vega_scroll(*first, focus, visible, rows); if (part->kind == VEGA_LIST) draw_list(grid, app, part, (int)i, *first, row + 1, area); else draw_table(grid, app, part, (int)i, *first, row + 1, area); row += share; } /* Keys the app answers to, then the text being typed. */ Pen help = pen_at(grid, bottom, 0, grid->width, VEGA_MUTED, false, false); if (screen->prompt >= 0) write_cstr(&help, typing ? "Tab keys " : "Tab type "); for (uint32_t i = 0; i < m->inputs; ++i) { if (m->input[i].binding != PURE_BIND_PRESS) continue; bool bound = false; for (uint32_t h = 0; h < m->handlers && !bound; ++h) bound = m->handler[h].event == i; if (!bound) continue; write_cstr(&help, key_symbol(m->input_keys[i])); write_cstr(&help, " "); write_cstr(&help, m->input_names[i]); write_cstr(&help, " "); } if (screen->prompt >= 0 && grid->height > 1) { Pen pen = pen_at(grid, grid->height - 1, 0, grid->width, typing ? VEGA_ACCENT : VEGA_MUTED, false, false); write_cstr(&pen, m->input_names[screen->prompt]); write_cstr(&pen, "> "); pen.tone = typing ? VEGA_NORMAL : VEGA_MUTED; /* Show the end of a long entry. */ size_t start = 0; unsigned room = grid->width > pen.column + 1 ? grid->width - pen.column - 1 : 0; while (start < prompt_length && vega_text_width(prompt + start, prompt_length - start) > room) { uint32_t skip; start += vega_decode(prompt, prompt_length, start, &skip); } write_text(&pen, prompt + start, prompt_length - start); if (typing) { grid->cursor_row = (int)pen.row; grid->cursor_column = (int)pen.column; } } }