summaryrefslogtreecommitdiff
path: root/ui-terminal.c
diff options
context:
space:
mode:
authorMarc André Tanner <mat@brain-dump.org>2017-03-14 16:53:53 +0100
committerMarc André Tanner <mat@brain-dump.org>2017-03-14 19:04:21 +0100
commit9bcf2667e7e239873597b7ec2172206a9af18071 (patch)
tree7e9ccb42fa665ba01be65b93fc995fa76719aaf7 /ui-terminal.c
parentbed289a96e1ed17e4b9fa4f9e22227fcf13cc818 (diff)
Restructure display code
Use pull instead of push based model for display code. Previously view.c was calling into the ui frontend code, with the new scheme this switches around: the necessary data is fetched by the ui as necessary. The UI independent display code is moved out of view.c/ui-curses.c into vis.c. The cell styles are now directly embedded into the Cell struct. New UI styles are introduced for: - status bar (focused / non-focused) - info message - window separator - EOF symbol You will have to update your color themes. The terminal output code is further abstracted into a generic ui-terminal.c part which keeps track of the whole in-memory cell matrix and #includes ui-terminal-curses.c for the actual terminal output. This architecture currently assumes that there are no overlapping windows. It will also allow non-curses based terminal user interfaces.
Diffstat (limited to 'ui-terminal.c')
-rw-r--r--ui-terminal.c727
1 files changed, 727 insertions, 0 deletions
diff --git a/ui-terminal.c b/ui-terminal.c
new file mode 100644
index 0000000..67cabba
--- /dev/null
+++ b/ui-terminal.c
@@ -0,0 +1,727 @@
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <limits.h>
+#include <ctype.h>
+#include <locale.h>
+#include <poll.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <termios.h>
+#include <errno.h>
+
+#include "ui-terminal.h"
+#include "vis.h"
+#include "vis-core.h"
+#include "text.h"
+#include "util.h"
+#include "text-util.h"
+
+#ifndef DEBUG_UI
+#define DEBUG_UI 0
+#endif
+
+#if DEBUG_UI
+#define debug(...) do { printf(__VA_ARGS__); fflush(stdout); } while (0)
+#else
+#define debug(...) do { } while (0)
+#endif
+
+#define MAX_WIDTH 1024
+#define MAX_HEIGHT 1024
+typedef struct UiTermWin UiTermWin;
+
+typedef struct {
+ Ui ui; /* generic ui interface, has to be the first struct member */
+ Vis *vis; /* editor instance to which this ui belongs */
+ UiTermWin *windows; /* all windows managed by this ui */
+ UiTermWin *selwin; /* the currently selected layout */
+ char info[MAX_WIDTH]; /* info message displayed at the bottom of the screen */
+ int width, height; /* terminal dimensions available for all windows */
+ enum UiLayout layout; /* whether windows are displayed horizontally or vertically */
+ TermKey *termkey; /* libtermkey instance to handle keyboard input (stdin or /dev/tty) */
+ size_t ids; /* bit mask of in use window ids */
+ size_t styles_size; /* #bytes allocated for styles array */
+ CellStyle *styles; /* each window has UI_STYLE_MAX different style definitions */
+ size_t cells_size; /* #bytes allocated for 2D grid (grows only) */
+ Cell *cells; /* 2D grid of cells, at least as large as current terminal size */
+} UiTerm;
+
+struct UiTermWin {
+ UiWin uiwin; /* generic interface, has to be the first struct member */
+ UiTerm *ui; /* ui which manages this window */
+ Win *win; /* editor window being displayed */
+ int id; /* unique identifier for this window */
+ int width, height; /* window dimension including status bar */
+ int x, y; /* window position */
+ int sidebar_width; /* width of the sidebar showing line numbers etc. */
+ UiTermWin *next, *prev; /* pointers to neighbouring windows */
+ enum UiOption options; /* display settings for this window */
+};
+
+#include "ui-terminal-curses.c"
+
+__attribute__((noreturn)) static void ui_die(Ui *ui, const char *msg, va_list ap) {
+ UiTerm *tui = (UiTerm*)ui;
+ ui_term_backend_free(tui);
+ if (tui->termkey)
+ termkey_stop(tui->termkey);
+ vfprintf(stderr, msg, ap);
+ exit(EXIT_FAILURE);
+}
+
+__attribute__((noreturn)) static void ui_die_msg(Ui *ui, const char *msg, ...) {
+ va_list ap;
+ va_start(ap, msg);
+ ui_die(ui, msg, ap);
+ va_end(ap);
+}
+
+static void ui_window_resize(UiTermWin *win, int width, int height) {
+ debug("ui-win-resize[%s]: %dx%d\n", win->win->file->name ? win->win->file->name : "noname", width, height);
+ bool status = win->options & UI_OPTION_STATUSBAR;
+ win->width = width;
+ win->height = height;
+ view_resize(win->win->view, width - win->sidebar_width, status ? height - 1 : height);
+}
+
+static void ui_window_move(UiTermWin *win, int x, int y) {
+ debug("ui-win-move[%s]: (%d, %d)\n", win->win->file->name ? win->win->file->name : "noname", x, y);
+ win->x = x;
+ win->y = y;
+}
+
+/* Convert color from string. */
+static bool color_fromstring(UiTerm *ui, CellColor *color, const char *s)
+{
+ if (!s)
+ return false;
+ if (*s == '#' && strlen(s) == 7) {
+ const char *cp;
+ unsigned char r, g, b;
+ for (cp = s + 1; isxdigit((unsigned char)*cp); cp++);
+ if (*cp != '\0')
+ return false;
+ int n = sscanf(s + 1, "%2hhx%2hhx%2hhx", &r, &g, &b);
+ if (n != 3)
+ return false;
+ *color = color_rgb(ui, r, g, b);
+ return true;
+ } else if ('0' <= *s && *s <= '9') {
+ int index = atoi(s);
+ if (index <= 0 || index > 255)
+ return false;
+ *color = color_terminal(ui, index);
+ return true;
+ }
+
+ struct {
+ const char *name;
+ CellColor color;
+ } color_names[] = {
+ { "black", CELL_COLOR_BLACK },
+ { "red", CELL_COLOR_RED },
+ { "green", CELL_COLOR_GREEN },
+ { "yellow", CELL_COLOR_YELLOW },
+ { "blue", CELL_COLOR_BLUE },
+ { "magenta", CELL_COLOR_MAGENTA },
+ { "cyan", CELL_COLOR_CYAN },
+ { "white", CELL_COLOR_WHITE },
+ { "default", CELL_COLOR_DEFAULT },
+ };
+
+ for (size_t i = 0; i < LENGTH(color_names); i++) {
+ if (strcasecmp(color_names[i].name, s) == 0) {
+ *color = color_names[i].color;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static bool ui_style_define(UiWin *w, int id, const char *style) {
+ UiTermWin *win = (UiTermWin*)w;
+ UiTerm *tui = win->ui;
+ if (id >= UI_STYLE_MAX)
+ return false;
+ if (!style)
+ return true;
+ CellStyle cell_style = tui->styles[win->id * UI_STYLE_MAX + UI_STYLE_DEFAULT];
+ char *style_copy = strdup(style), *option = style_copy, *next, *p;
+ while (option) {
+ if ((next = strchr(option, ',')))
+ *next++ = '\0';
+ if ((p = strchr(option, ':')))
+ *p++ = '\0';
+ if (!strcasecmp(option, "reverse")) {
+ cell_style.attr |= CELL_ATTR_REVERSE;
+ } else if (!strcasecmp(option, "bold")) {
+ cell_style.attr |= CELL_ATTR_BOLD;
+ } else if (!strcasecmp(option, "notbold")) {
+ cell_style.attr &= ~CELL_ATTR_BOLD;
+ } else if (!strcasecmp(option, "italics")) {
+ cell_style.attr |= CELL_ATTR_ITALIC;
+ } else if (!strcasecmp(option, "notitalics")) {
+ cell_style.attr &= ~CELL_ATTR_ITALIC;
+ } else if (!strcasecmp(option, "underlined")) {
+ cell_style.attr |= CELL_ATTR_UNDERLINE;
+ } else if (!strcasecmp(option, "notunderlined")) {
+ cell_style.attr &= ~CELL_ATTR_UNDERLINE;
+ } else if (!strcasecmp(option, "blink")) {
+ cell_style.attr |= CELL_ATTR_BLINK;
+ } else if (!strcasecmp(option, "notblink")) {
+ cell_style.attr &= ~CELL_ATTR_BLINK;
+ } else if (!strcasecmp(option, "fore")) {
+ color_fromstring(win->ui, &cell_style.fg, p);
+ } else if (!strcasecmp(option, "back")) {
+ color_fromstring(win->ui, &cell_style.bg, p);
+ }
+ option = next;
+ }
+ tui->styles[win->id * UI_STYLE_MAX + id] = cell_style;
+ free(style_copy);
+ return true;
+}
+
+static void ui_style(UiTerm *tui, int x, int y, int len, UiTermWin *win, enum UiStyle style_id) {
+ CellStyle style = tui->styles[(win ? win->id : 0)*UI_STYLE_MAX + style_id];
+ Cell (*cells)[tui->width] = (void*)tui->cells;
+ int end = x + len;
+ if (end > tui->width)
+ end = tui->width;
+ while (x < end)
+ cells[y][x++].style = style;
+}
+
+static void ui_draw_string(UiTerm *tui, int x, int y, const char *str, UiTermWin *win, enum UiStyle style_id) {
+ debug("draw-string: [%d][%d]\n", y, x);
+ CellStyle style = tui->styles[(win ? win->id : 0)*UI_STYLE_MAX + style_id];
+ // FIXME: does not handle double width characters etc, share code with view.c?
+ Cell (*cells)[tui->width] = (void*)tui->cells;
+ const size_t cell_size = sizeof(cells[0][0].data)-1;
+ for (const char *next = str; *str && x < tui->width; str = next) {
+ do next++; while (!ISUTF8(*next));
+ size_t len = next - str;
+ if (!len)
+ break;
+ len = MIN(len, cell_size);
+ strncpy(cells[y][x].data, str, len);
+ cells[y][x].data[len] = '\0';
+ cells[y][x].style = style;
+ x++;
+ }
+}
+
+static void ui_window_draw(UiWin *w) {
+ UiTermWin *win = (UiTermWin*)w;
+ UiTerm *ui = win->ui;
+ View *view = win->win->view;
+ Cell (*cells)[ui->width] = (void*)ui->cells;
+ int width = win->width, height = win->height;
+ const Line *line = view_lines_first(view);
+ bool status = win->options & UI_OPTION_STATUSBAR;
+ bool nu = win->options & UI_OPTION_LINE_NUMBERS_ABSOLUTE;
+ bool rnu = win->options & UI_OPTION_LINE_NUMBERS_RELATIVE;
+ bool sidebar = nu || rnu;
+ int sidebar_width = sidebar ? snprintf(NULL, 0, "%zd ", line->lineno + height - 2) : 0;
+ if (sidebar_width != win->sidebar_width) {
+ view_resize(view, width - sidebar_width, status ? height - 1 : height);
+ win->sidebar_width = sidebar_width;
+ }
+ vis_window_draw(win->win);
+ line = view_lines_first(view);
+ size_t prev_lineno = 0;
+ Cursor *cursor = view_cursors_primary_get(view);
+ const Line *cursor_line = view_cursors_line_get(cursor);
+ size_t cursor_lineno = cursor_line->lineno;
+ char buf[sidebar_width+1];
+ int x = win->x, y = win->y;
+ int view_width = view_width_get(view);
+ if (x + sidebar_width + view_width > ui->width)
+ view_width = ui->width - x - sidebar_width;
+ for (const Line *l = line; l; l = l->next) {
+ if (sidebar) {
+ if (!l->lineno || !l->len || l->lineno == prev_lineno) {
+ memset(buf, ' ', sizeof(buf));
+ buf[sidebar_width] = '\0';
+ } else {
+ size_t number = l->lineno;
+ if (rnu) {
+ number = (win->options & UI_OPTION_LARGE_FILE) ? 0 : l->lineno;
+ if (l->lineno > cursor_lineno)
+ number = l->lineno - cursor_lineno;
+ else if (l->lineno < cursor_lineno)
+ number = cursor_lineno - l->lineno;
+ }
+ snprintf(buf, sizeof buf, "%*zu ", sidebar_width-1, number);
+ }
+ ui_draw_string(ui, x, y, buf, win, UI_STYLE_LINENUMBER);
+ prev_lineno = l->lineno;
+ }
+ debug("draw-window: [%d][%d] ... cells[%d][%d]\n", y, x+sidebar_width, y, view_width);
+ memcpy(&cells[y++][x+sidebar_width], l->cells, sizeof(Cell) * view_width);
+ }
+}
+
+static CellStyle ui_window_style_get(UiWin *w, enum UiStyle style) {
+ UiTermWin *win = (UiTermWin*)w;
+ UiTerm *tui = win->ui;
+ return tui->styles[win->id * UI_STYLE_MAX + style];
+}
+
+static void ui_window_status(UiWin *w, const char *status) {
+ UiTermWin *win = (UiTermWin*)w;
+ if (!(win->options & UI_OPTION_STATUSBAR))
+ return;
+ UiTerm *ui = win->ui;
+ enum UiStyle style = ui->selwin == win ? UI_STYLE_STATUS_FOCUSED : UI_STYLE_STATUS;
+ ui_draw_string(ui, win->x, win->y + win->height - 1, status, win, style);
+}
+
+static void ui_arrange(Ui *ui, enum UiLayout layout) {
+ debug("ui-arrange\n");
+ UiTerm *tui = (UiTerm*)ui;
+ tui->layout = layout;
+ Cell (*cells)[tui->width] = (void*)tui->cells;
+ int n = 0, m = !!tui->info[0], x = 0, y = 0;
+ for (UiTermWin *win = tui->windows; win; win = win->next) {
+ if (win->options & UI_OPTION_ONELINE)
+ m++;
+ else
+ n++;
+ }
+ int max_height = tui->height - m;
+ int width = (tui->width / MAX(1, n)) - 1;
+ int height = max_height / MAX(1, n);
+ for (UiTermWin *win = tui->windows; win; win = win->next) {
+ if (win->options & UI_OPTION_ONELINE)
+ continue;
+ n--;
+ if (layout == UI_LAYOUT_HORIZONTAL) {
+ int h = n ? height : max_height - y;
+ ui_window_resize(win, tui->width, h);
+ ui_window_move(win, x, y);
+ y += h;
+ } else {
+ int w = n ? width : tui->width - x;
+ ui_window_resize(win, w, max_height);
+ ui_window_move(win, x, y);
+ x += w;
+ if (n) {
+ for (int i = 0; i < max_height; i++) {
+ strcpy(cells[i][x].data,"│");
+ cells[i][x].style = tui->styles[UI_STYLE_SEPARATOR];
+ }
+ x++;
+ }
+ }
+ }
+
+ if (layout == UI_LAYOUT_VERTICAL)
+ y = max_height;
+
+ for (UiTermWin *win = tui->windows; win; win = win->next) {
+ if (!(win->options & UI_OPTION_ONELINE))
+ continue;
+ ui_window_resize(win, tui->width, 1);
+ ui_window_move(win, 0, y++);
+ }
+}
+
+static void ui_draw(Ui *ui) {
+ debug("ui-draw\n");
+ UiTerm *tui = (UiTerm*)ui;
+ ui_arrange(ui, tui->layout);
+
+ for (UiTermWin *win = tui->windows; win; win = win->next)
+ ui_window_draw((UiWin*)win);
+ if (tui->info[0]) {
+ ui_draw_string(tui, 0, tui->height-1, tui->info, NULL, UI_STYLE_INFO);
+ ui_style(tui, 0, tui->height-1, tui->width, NULL, UI_STYLE_INFO);
+ }
+}
+
+static void ui_redraw(Ui *ui) {
+ UiTerm *tui = (UiTerm*)ui;
+ ui_term_backend_clear(tui);
+ for (UiTermWin *win = tui->windows; win; win = win->next)
+ view_dirty(win->win->view);
+}
+
+static bool ui_resize_to(Ui *ui, int width, int height) {
+ UiTerm *tui = (UiTerm*)ui;
+ width = MAX(width, 1);
+ width = MIN(width, MAX_WIDTH);
+ height = MAX(height, 1);
+ height = MIN(height, MAX_HEIGHT);
+ ui_term_backend_resize(tui, width, height);
+ size_t size = width*height*sizeof(Cell);
+ if (size > tui->cells_size) {
+ Cell *cells = realloc(tui->cells, size);
+ if (!cells)
+ return false;
+ tui->cells_size = size;
+ tui->cells = cells;
+ }
+ tui->width = width;
+ tui->height = height;
+ ui_draw(ui);
+ return true;
+}
+
+static void ui_resize(Ui *ui) {
+ struct winsize ws;
+ int width = 80, height = 24;
+
+ if (ioctl(STDERR_FILENO, TIOCGWINSZ, &ws) != -1) {
+ width = ws.ws_col;
+ height = ws.ws_row;
+ }
+
+ ui_resize_to(ui, width, height);
+}
+
+static void ui_update(Ui *ui) {
+ debug("ui-doupdate\n");
+ UiTerm *tui = (UiTerm*)ui;
+ ui_draw(ui);
+ ui_term_backend_blit(tui);
+}
+
+static void ui_window_free(UiWin *w) {
+ UiTermWin *win = (UiTermWin*)w;
+ if (!win)
+ return;
+ UiTerm *tui = win->ui;
+ if (win->prev)
+ win->prev->next = win->next;
+ if (win->next)
+ win->next->prev = win->prev;
+ if (tui->windows == win)
+ tui->windows = win->next;
+ if (tui->selwin == win)
+ tui->selwin = NULL;
+ win->next = win->prev = NULL;
+ tui->ids &= ~(1UL << win->id);
+ free(win);
+}
+
+static void ui_window_focus(UiWin *w) {
+ UiTermWin *new = (UiTermWin*)w;
+ UiTermWin *old = new->ui->selwin;
+ if (new->options & UI_OPTION_STATUSBAR)
+ new->ui->selwin = new;
+ if (old)
+ view_dirty(old->win->view);
+ view_dirty(new->win->view);
+}
+
+static void ui_window_options_set(UiWin *w, enum UiOption options) {
+ UiTermWin *win = (UiTermWin*)w;
+ win->options = options;
+ if (options & UI_OPTION_ONELINE) {
+ /* move the new window to the end of the list */
+ UiTerm *tui = win->ui;
+ UiTermWin *last = tui->windows;
+ while (last->next)
+ last = last->next;
+ if (last != win) {
+ if (win->prev)
+ win->prev->next = win->next;
+ if (win->next)
+ win->next->prev = win->prev;
+ if (tui->windows == win)
+ tui->windows = win->next;
+ last->next = win;
+ win->prev = last;
+ win->next = NULL;
+ }
+ }
+
+ ui_draw((Ui*)win->ui);
+}
+
+static enum UiOption ui_window_options_get(UiWin *win) {
+ return ((UiTermWin*)win)->options;
+}
+
+static int ui_window_width(UiWin *win) {
+ return ((UiTermWin*)win)->width;
+}
+
+static int ui_window_height(UiWin *win) {
+ return ((UiTermWin*)win)->height;
+}
+
+static void ui_window_swap(UiWin *aw, UiWin *bw) {
+ UiTermWin *a = (UiTermWin*)aw;
+ UiTermWin *b = (UiTermWin*)bw;
+ if (a == b || !a || !b)
+ return;
+ UiTerm *tui = a->ui;
+ UiTermWin *tmp = a->next;
+ a->next = b->next;
+ b->next = tmp;
+ if (a->next)
+ a->next->prev = a;
+ if (b->next)
+ b->next->prev = b;
+ tmp = a->prev;
+ a->prev = b->prev;
+ b->prev = tmp;
+ if (a->prev)
+ a->prev->next = a;
+ if (b->prev)
+ b->prev->next = b;
+ if (tui->windows == a)
+ tui->windows = b;
+ else if (tui->windows == b)
+ tui->windows = a;
+ if (tui->selwin == a)
+ ui_window_focus(bw);
+ else if (tui->selwin == b)
+ ui_window_focus(aw);
+}
+
+static UiWin *ui_window_new(Ui *ui, Win *w, enum UiOption options) {
+ UiTerm *tui = (UiTerm*)ui;
+ /* get rightmost zero bit, i.e. highest available id */
+ size_t bit = ~tui->ids & (tui->ids + 1);
+ size_t id = 0;
+ for (size_t tmp = bit; tmp >>= 1; id++);
+ if (id >= sizeof(size_t) * 8)
+ return NULL;
+ size_t styles_size = (id + 1) * UI_STYLE_MAX * sizeof(CellStyle);
+ if (styles_size > tui->styles_size) {
+ CellStyle *styles = realloc(tui->styles, styles_size);
+ if (!styles)
+ return NULL;
+ tui->styles = styles;
+ tui->styles_size = styles_size;
+ }
+ UiTermWin *win = calloc(1, sizeof(UiTermWin));
+ if (!win)
+ return NULL;
+
+ win->uiwin = (UiWin) {
+ .style_get = ui_window_style_get,
+ .status = ui_window_status,
+ .options_set = ui_window_options_set,
+ .options_get = ui_window_options_get,
+ .style_define = ui_style_define,
+ .window_width = ui_window_width,
+ .window_height = ui_window_height,
+ };
+
+ tui->ids |= bit;
+ win->id = id;
+ win->ui = tui;
+ win->win = w;
+
+ CellStyle *styles = &tui->styles[win->id * UI_STYLE_MAX];
+ for (int i = 0; i < UI_STYLE_MAX; i++) {
+ styles[i] = (CellStyle) {
+ .fg = CELL_COLOR_DEFAULT,
+ .bg = CELL_COLOR_DEFAULT,
+ .attr = CELL_ATTR_NORMAL,
+ };
+ }
+
+ styles[UI_STYLE_CURSOR].attr |= CELL_ATTR_REVERSE;
+ styles[UI_STYLE_CURSOR_PRIMARY].attr |= CELL_ATTR_REVERSE|CELL_ATTR_BLINK;
+ styles[UI_STYLE_SELECTION].attr |= CELL_ATTR_REVERSE;
+ styles[UI_STYLE_COLOR_COLUMN].attr |= CELL_ATTR_REVERSE;
+ styles[UI_STYLE_STATUS].attr |= CELL_ATTR_REVERSE;
+ styles[UI_STYLE_STATUS_FOCUSED].attr |= CELL_ATTR_REVERSE|CELL_ATTR_BOLD;
+ styles[UI_STYLE_INFO].attr |= CELL_ATTR_BOLD;
+ view_ui(w->view, &win->uiwin);
+
+ if (tui->windows)
+ tui->windows->prev = win;
+ win->next = tui->windows;
+ tui->windows = win;
+
+ if (text_size(w->file->text) > UI_LARGE_FILE_SIZE) {
+ options |= UI_OPTION_LARGE_FILE;
+ options &= ~UI_OPTION_LINE_NUMBERS_ABSOLUTE;
+ }
+
+ ui_window_options_set((UiWin*)win, options);
+
+ return &win->uiwin;
+}
+
+static void ui_info(Ui *ui, const char *msg, va_list ap) {
+ UiTerm *tui = (UiTerm*)ui;
+ vsnprintf(tui->info, sizeof(tui->info), msg, ap);
+ ui_draw(ui);
+}
+
+static void ui_info_hide(Ui *ui) {
+ UiTerm *tui = (UiTerm*)ui;
+ if (tui->info[0]) {
+ tui->info[0] = '\0';
+ ui_draw(ui);
+ }
+}
+
+static TermKey *ui_termkey_new(int fd) {
+ TermKey *termkey = termkey_new(fd, TERMKEY_FLAG_UTF8);
+ if (termkey)
+ termkey_set_canonflags(termkey, TERMKEY_CANON_DELBS);
+ return termkey;
+}
+
+static TermKey *ui_termkey_reopen(Ui *ui, int fd) {
+ int tty = open("/dev/tty", O_RDWR);
+ if (tty == -1)
+ return NULL;
+ if (tty != fd && dup2(tty, fd) == -1) {
+ close(tty);
+ return NULL;
+ }
+ close(tty);
+ return ui_termkey_new(fd);
+}
+
+static TermKey *ui_termkey_get(Ui *ui) {
+ UiTerm *tui = (UiTerm*)ui;
+ return tui->termkey;
+}
+
+static void ui_suspend(Ui *ui) {
+ UiTerm *tui = (UiTerm*)ui;
+ ui_term_backend_suspend(tui);
+ termkey_stop(tui->termkey);
+ kill(0, SIGSTOP);
+}
+
+static void ui_resume(Ui *ui) {
+ UiTerm *tui = (UiTerm*)ui;
+ termkey_start(tui->termkey);
+ ui_term_backend_resume(tui);
+}
+
+static bool ui_getkey(Ui *ui, TermKeyKey *key) {
+ UiTerm *tui = (UiTerm*)ui;
+ TermKeyResult ret = termkey_getkey(tui->termkey, key);
+
+ if (ret == TERMKEY_RES_EOF) {
+ termkey_destroy(tui->termkey);
+ errno = 0;
+ if (!(tui->termkey = ui_termkey_reopen(ui, STDIN_FILENO)))
+ ui_die_msg(ui, "Failed to re-open stdin as /dev/tty: %s\n", errno != 0 ? strerror(errno) : "");
+ return false;
+ }
+
+ if (ret == TERMKEY_RES_AGAIN) {
+ struct pollfd fd;
+ fd.fd = STDIN_FILENO;
+ fd.events = POLLIN;
+ if (poll(&fd, 1, termkey_get_waittime(tui->termkey)) == 0)
+ ret = termkey_getkey_force(tui->termkey, key);
+ }
+
+ return ret == TERMKEY_RES_KEY;
+}
+
+static void ui_terminal_save(Ui *ui) {
+ UiTerm *tui = (UiTerm*)ui;
+ ui_term_backend_save(tui);
+ termkey_stop(tui->termkey);
+}
+
+static void ui_terminal_restore(Ui *ui) {
+ UiTerm *tui = (UiTerm*)ui;
+ termkey_start(tui->termkey);
+ ui_term_backend_restore(tui);
+}
+
+static bool ui_init(Ui *ui, Vis *vis) {
+ UiTerm *tui = (UiTerm*)ui;
+ tui->vis = vis;
+
+ setlocale(LC_CTYPE, "");
+
+ char *term = getenv("TERM");
+ if (!term)
+ term = "xterm";
+
+ errno = 0;
+ if (!(tui->termkey = ui_termkey_new(STDIN_FILENO))) {
+ /* work around libtermkey bug which fails if stdin is /dev/null */
+ if (errno == EBADF && !isatty(STDIN_FILENO)) {
+ errno = 0;
+ if (!(tui->termkey = ui_termkey_reopen(ui, STDIN_FILENO)) && errno == ENXIO)
+ tui->termkey = termkey_new_abstract(term, TERMKEY_FLAG_UTF8);
+ }
+ if (!tui->termkey)
+ goto err;
+ }
+
+ if (!ui_term_backend_init(tui, term))
+ goto err;
+ ui_resize(ui);
+ return true;
+err:
+ ui_die_msg(ui, "Failed to start curses interface: %s\n", errno != 0 ? strerror(errno) : "");
+ return false;
+}
+
+Ui *ui_term_new(void) {
+ size_t styles_size = UI_STYLE_MAX * sizeof(CellStyle);
+ CellStyle *styles = calloc(1, styles_size);
+ if (!styles)
+ return NULL;
+ UiTerm *tui = ui_term_backend_new();
+ if (!tui) {
+ free(styles);
+ return NULL;
+ }
+ tui->styles_size = styles_size;
+ tui->styles = styles;
+ Ui *ui = (Ui*)tui;
+ *ui = (Ui) {
+ .init = ui_init,
+ .free = ui_term_free,
+ .termkey_get = ui_termkey_get,
+ .suspend = ui_suspend,
+ .resume = ui_resume,
+ .resize = ui_resize,
+ .update = ui_update,
+ .window_new = ui_window_new,
+ .window_free = ui_window_free,
+ .window_focus = ui_window_focus,
+ .window_swap = ui_window_swap,
+ .draw = ui_draw,
+ .redraw = ui_redraw,
+ .arrange = ui_arrange,
+ .die = ui_die,
+ .info = ui_info,
+ .info_hide = ui_info_hide,
+ .getkey = ui_getkey,
+ .terminal_save = ui_terminal_save,
+ .terminal_restore = ui_terminal_restore,
+ .colors = ui_term_backend_colors,
+ };
+
+ return ui;
+}
+
+void ui_term_free(Ui *ui) {
+ UiTerm *tui = (UiTerm*)ui;
+ if (!tui)
+ return;
+ while (tui->windows)
+ ui_window_free((UiWin*)tui->windows);
+ ui_term_backend_free(tui);
+ if (tui->termkey)
+ termkey_destroy(tui->termkey);
+ free(tui->cells);
+ free(tui->styles);
+ free(tui);
+}