diff options
Diffstat (limited to 'ui-terminal.c')
| -rw-r--r-- | ui-terminal.c | 727 |
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); +} |
