commit e6233ef4f875bd1420469a0fdfab36f3ba12336b
parent ca75f09d87624b5a01a667491ad81e386df7b189
Author: Brian Swetland <swetland@frotz.net>
Date: Wed, 1 Mar 2023 07:37:06 -0800
tui: initial checkin of a simple Text UI
- provide a log area and commandline, divided by an infobar
- provide mechanisms to append log lines
- provide a callback for commands
Diffstat:
A | tui/test.c | | | 19 | +++++++++++++++++++ |
A | tui/tui.c | | | 273 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | tui/tui.h | | | 34 | ++++++++++++++++++++++++++++++++++ |
3 files changed, 326 insertions(+), 0 deletions(-)
diff --git a/tui/test.c b/tui/test.c
@@ -0,0 +1,19 @@
+// Copyright 2023, Brian Swetland <swetland@frotz.net>
+// Licensed under the Apache License, Version 2.0.
+
+#include <tui.h>
+#include <string.h>
+
+void handle_line(char* line, unsigned len) {
+ if (len) {
+ tui_printf("? %s\n", line);
+ }
+ if (!strcmp(line, "exit")) {
+ tui_exit();
+ }
+}
+
+int main(int argc, char** argv) {
+ tui_loop(handle_line);
+ return 0;
+}
diff --git a/tui/tui.c b/tui/tui.c
@@ -0,0 +1,273 @@
+// Copyright 2023, Brian Swetland <swetland@frotz.net>
+// Licensed under the Apache License, Version 2.0.
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+
+#include <tui.h>
+#include <termbox.h>
+
+#define MAXWIDTH 128
+
+typedef struct line LINE;
+typedef struct ux UX;
+
+struct line {
+ LINE* prev;
+ LINE* next;
+ uint16_t len;
+ uint8_t fg;
+ uint8_t bg;
+ uint8_t text[MAXWIDTH];
+};
+
+struct ux {
+ int w;
+ int h;
+ int invalid;
+
+ uint8_t cmd[MAXWIDTH + 1];
+ int len;
+
+ LINE list;
+};
+
+static void paint(int x, int y, const char* str) {
+ while (*str) {
+ tb_change_cell(x++, y, *str++, TB_DEFAULT, TB_DEFAULT);
+ }
+}
+
+static void paint_titlebar(UX *ux) {
+}
+
+static void paint_infobar(UX *ux) {
+ for (int x = 0; x < ux->w; x++) {
+ tb_change_cell(x, ux->h - 2, '-', TB_DEFAULT | TB_REVERSE, TB_DEFAULT);
+ }
+}
+
+static void paint_cmdline(UX *ux) {
+ uint8_t* ch = ux->cmd;
+ int y = ux->h - 1;
+ int w = ux->w;
+ for (int x = 0; x < w; x++) {
+ if (x < ux->len) {
+ tb_change_cell(x, y, *ch++, TB_DEFAULT, TB_DEFAULT);
+ } else {
+ tb_change_cell(x, y, ' ', TB_DEFAULT, TB_DEFAULT);
+ }
+ }
+ tb_set_cursor(ux->len >= w ? w - 1 : ux->len, y);
+}
+
+static void paint_log(UX *ux) {
+ LINE* list = &ux->list;
+ int y = ux->h - 3;
+ int w = ux->w;
+ uint8_t c;
+
+ for (LINE* line = list->prev; (line != list) && (y >= 0); line = line->prev) {
+ for (int x = 0; x < w; x++) {
+ c = (x < line->len) ? line->text[x] : ' ';
+ tb_change_cell(x, y, c, line->fg, line->bg);
+ }
+ y--;
+ }
+}
+
+static int repaint(UX* ux) {
+ // clear entire display and adjust to any resize events
+ tb_clear();
+ ux->w = tb_width();
+ ux->h = tb_height();
+
+ if ((ux->w < 40) || (ux->h < 8)) {
+ paint(0, 0, "WINDOW TOO SMALL");
+ return 1;
+ }
+
+ paint_titlebar(ux);
+ paint_infobar(ux);
+ paint_cmdline(ux);
+ paint_log(ux);
+ return 0;
+}
+
+static int handle_event(UX* ux, void (*cb)(char*, unsigned) ) {
+ struct tb_event ev;
+ tb_present();
+
+ if ((tb_poll_event(&ev) < 0) ||
+ (ev.key == TB_KEY_CTRL_C)) {
+ return -1;
+ }
+
+ // always process full repaints due to resize or user request
+ if ((ev.type == TB_EVENT_RESIZE) ||
+ (ev.key == TB_KEY_CTRL_L)) {
+ ux->invalid = repaint(ux);
+ return 0;
+ }
+
+ // ignore other input of the window is too small
+ if (ux->invalid) {
+ return 0;
+ }
+
+ switch (ev.key) {
+ case 0: // printable character
+ // ignore fancy unicode characters
+ if (ev.ch > 255) {
+ break;
+ }
+ if (ux->len >= MAXWIDTH) {
+ break;
+ }
+ ux->cmd[ux->len++] = ev.ch;
+ paint_cmdline(ux);
+ break;
+ case TB_KEY_BACKSPACE:
+ case TB_KEY_BACKSPACE2:
+ if (ux->len > 0 ) {
+ ux->len--;
+ paint_cmdline(ux);
+ }
+ break;
+ case TB_KEY_ENTER: {
+ unsigned len = ux->len;
+ // clear command line
+ ux->len = 0;
+ paint_cmdline(ux);
+ tb_present();
+ // process command
+ ux->cmd[len] = 0;
+ cb((void*) ux->cmd, len);
+ break;
+ }
+ default:
+#if 0 // debug unexpected keys
+ if (ux->len < (MAXWIDTH - 6)) {
+ char tmp[5];
+ sprintf(tmp, "%04x", ev.key);
+ ux->cmd[ux->len++] = '<';
+ ux->cmd[ux->len++] = tmp[0];
+ ux->cmd[ux->len++] = tmp[1];
+ ux->cmd[ux->len++] = tmp[2];
+ ux->cmd[ux->len++] = tmp[3];
+ ux->cmd[ux->len++] = '>';
+ paint_cmdline(ux);
+ }
+#endif
+ break;
+ }
+ return 0;
+}
+
+static int running = 1;
+
+// TODO: handle calls from outside of tui_handle();
+void tui_exit(void) {
+ running = 0;
+}
+
+static UX ux = {
+ .list = {
+ .prev = &ux.list,
+ .next = &ux.list,
+ },
+};
+
+void tui_loop(void (*cb)(char*, unsigned)) {
+ if (tb_init()) {
+ fprintf(stderr, "termbox init failed\n");
+ return;
+ }
+
+ tb_select_input_mode(TB_INPUT_ESC | TB_INPUT_SPACE);
+
+ repaint(&ux);
+
+ while (running && (handle_event(&ux, cb) == 0)) ;
+
+ tb_shutdown();
+}
+
+static void tui_logline(uint8_t* text, unsigned len) {
+ LINE* line = malloc(sizeof(LINE));
+ if (line == NULL) return;
+ memcpy(line->text, text, len);
+ line->len = len;
+ line->fg = TB_DEFAULT;
+ line->bg = TB_DEFAULT;
+
+ // add line to the log
+ line->prev = ux.list.prev;
+ line->next = &ux.list;
+ line->prev->next = line;
+ ux.list.prev = line;
+
+ // refresh the log
+ paint_log(&ux);
+ tb_present();
+}
+
+struct tui_ch {
+ unsigned len;
+ uint8_t buffer[MAXWIDTH];
+};
+
+int tui_ch_create(tui_ch_t** out, unsigned flags) {
+ tui_ch_t* ch = calloc(1, sizeof(tui_ch_t));
+ if (ch == NULL) {
+ return -1;
+ } else {
+ *out = ch;
+ return 0;
+ }
+}
+
+void tui_ch_destroy(tui_ch_t* ch) {
+ free(ch);
+}
+
+void tui_ch_vaprintf(tui_ch_t* ch, const char* fmt, va_list ap) {
+ char tmp[1024];
+ int n = vsnprintf(tmp, sizeof(tmp), fmt, ap);
+ char *x = tmp;
+ while (n > 0) {
+ uint8_t c = *x++;
+ n--;
+ if ((c == '\n') && (ch->len > 0)) {
+ tui_logline(ch->buffer, ch->len);
+ ch->len = 0;
+ continue;
+ }
+ if ((c < ' ') || (c > 0x7e)) {
+ continue;
+ }
+ if (ch->len < MAXWIDTH) {
+ ch->buffer[ch->len++] = c;
+ }
+ }
+}
+
+void tui_ch_printf(tui_ch_t* ch, const char* fmt, ...) {
+ va_list ap;
+ va_start(ap, fmt);
+ tui_ch_vaprintf(ch, fmt, ap);
+ va_end(ap);
+}
+
+void tui_printf(const char* fmt, ...) {
+ va_list ap;
+ tui_ch_t ch = { 0 };
+ va_start(ap, fmt);
+ tui_ch_vaprintf(&ch, fmt, ap);
+ va_end(ap);
+ if (ch.len > 0) {
+ tui_logline(ch.buffer, ch.len);
+ }
+}
diff --git a/tui/tui.h b/tui/tui.h
@@ -0,0 +1,34 @@
+// Copyright 2023, Brian Swetland <swetland@frotz.net>
+// Licensed under the Apache License, Version 2.0.
+
+#pragma once
+
+#include <stdarg.h>
+
+void tui_loop(void (*callback)(char* line, unsigned len));
+
+void tui_exit(void);
+
+// Write a line (or multiple lines separated by '\n') to the TUI log
+// Non-printing and non-ascii characters are ignored.
+// Lines larger than the TUI log max width (128) are truncated.
+void tui_printf(const char* fmt, ...);
+
+
+// TUI Channels provide a way for different entities to use a
+// printf() interface to send log lines to the TUI without
+// interleaving partial log lines.
+
+// They contain a line assembly buffer and only send lines to
+// the TUI log when a newline character is encountered.
+
+// It is safe for different threads to use different channels
+// to simultaneously send log lines, but it is NOT safe for
+// different threads to simultaneously use the same channel.
+typedef struct tui_ch tui_ch_t;
+
+int tui_ch_create(tui_ch_t** ch, unsigned flags);
+void tui_ch_destroy(tui_ch_t* ch);
+void tui_ch_printf(tui_ch_t* ch, const char* fmt, ...);
+void tui_ch_vaprintf(tui_ch_t* ch, const char* fmt, va_list ap);
+