aboutsummaryrefslogtreecommitdiff
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)
downloadvis-9bcf2667e7e239873597b7ec2172206a9af18071.tar.gz
vis-9bcf2667e7e239873597b7ec2172206a9af18071.tar.xz
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);
+}