10 #include <sys/types.h>
16 #include "ui-terminal.h"
21 #include "text-util.h"
28 #define debug(...) do { printf(__VA_ARGS__); fflush(stdout); } while (0)
30 #define debug(...) do { } while (0)
33 #define MAX_WIDTH 1024
34 #define MAX_HEIGHT 1024
35 typedef struct UiTermWin UiTermWin
;
38 Ui ui
; /* generic ui interface, has to be the first struct member */
39 Vis
*vis
; /* editor instance to which this ui belongs */
40 UiTermWin
*windows
; /* all windows managed by this ui */
41 UiTermWin
*selwin
; /* the currently selected layout */
42 char info
[MAX_WIDTH
]; /* info message displayed at the bottom of the screen */
43 int width
, height
; /* terminal dimensions available for all windows */
44 enum UiLayout layout
; /* whether windows are displayed horizontally or vertically */
45 TermKey
*termkey
; /* libtermkey instance to handle keyboard input (stdin or /dev/tty) */
46 size_t ids
; /* bit mask of in use window ids */
47 size_t styles_size
; /* #bytes allocated for styles array */
48 CellStyle
*styles
; /* each window has UI_STYLE_MAX different style definitions */
49 size_t cells_size
; /* #bytes allocated for 2D grid (grows only) */
50 Cell
*cells
; /* 2D grid of cells, at least as large as current terminal size */
54 UiWin uiwin
; /* generic interface, has to be the first struct member */
55 UiTerm
*ui
; /* ui which manages this window */
56 Win
*win
; /* editor window being displayed */
57 int id
; /* unique identifier for this window */
58 int width
, height
; /* window dimension including status bar */
59 int x
, y
; /* window position */
60 int sidebar_width
; /* width of the sidebar showing line numbers etc. */
61 UiTermWin
*next
, *prev
; /* pointers to neighbouring windows */
62 enum UiOption options
; /* display settings for this window */
66 #include "ui-terminal-curses.c"
68 #include "ui-terminal-vt100.c"
71 __attribute__((noreturn
)) static void ui_die(Ui
*ui
, const char *msg
, va_list ap
) {
72 UiTerm
*tui
= (UiTerm
*)ui
;
73 ui_term_backend_free(tui
);
75 termkey_stop(tui
->termkey
);
76 vfprintf(stderr
, msg
, ap
);
80 __attribute__((noreturn
)) static void ui_die_msg(Ui
*ui
, const char *msg
, ...) {
87 static void ui_window_resize(UiTermWin
*win
, int width
, int height
) {
88 debug("ui-win-resize[%s]: %dx%d\n", win
->win
->file
->name
? win
->win
->file
->name
: "noname", width
, height
);
89 bool status
= win
->options
& UI_OPTION_STATUSBAR
;
92 view_resize(win
->win
->view
, width
- win
->sidebar_width
, status
? height
- 1 : height
);
95 static void ui_window_move(UiTermWin
*win
, int x
, int y
) {
96 debug("ui-win-move[%s]: (%d, %d)\n", win
->win
->file
->name
? win
->win
->file
->name
: "noname", x
, y
);
101 static bool color_fromstring(UiTerm
*ui
, CellColor
*color
, const char *s
)
105 if (*s
== '#' && strlen(s
) == 7) {
107 unsigned char r
, g
, b
;
108 for (cp
= s
+ 1; isxdigit((unsigned char)*cp
); cp
++);
111 int n
= sscanf(s
+ 1, "%2hhx%2hhx%2hhx", &r
, &g
, &b
);
114 *color
= color_rgb(ui
, r
, g
, b
);
116 } else if ('0' <= *s
&& *s
<= '9') {
118 if (index
<= 0 || index
> 255)
120 *color
= color_terminal(ui
, index
);
124 static const struct {
128 { "black", CELL_COLOR_BLACK
},
129 { "red", CELL_COLOR_RED
},
130 { "green", CELL_COLOR_GREEN
},
131 { "yellow", CELL_COLOR_YELLOW
},
132 { "blue", CELL_COLOR_BLUE
},
133 { "magenta", CELL_COLOR_MAGENTA
},
134 { "cyan", CELL_COLOR_CYAN
},
135 { "white", CELL_COLOR_WHITE
},
136 { "default", CELL_COLOR_DEFAULT
},
139 for (size_t i
= 0; i
< LENGTH(color_names
); i
++) {
140 if (strcasecmp(color_names
[i
].name
, s
) == 0) {
141 *color
= color_names
[i
].color
;
149 static bool ui_style_define(UiWin
*w
, int id
, const char *style
) {
150 UiTermWin
*win
= (UiTermWin
*)w
;
151 UiTerm
*tui
= win
->ui
;
152 if (id
>= UI_STYLE_MAX
)
156 CellStyle cell_style
= tui
->styles
[win
->id
* UI_STYLE_MAX
+ UI_STYLE_DEFAULT
];
157 char *style_copy
= strdup(style
), *option
= style_copy
;
159 while (*option
== ' ')
161 char *next
= strchr(option
, ',');
164 char *value
= strchr(option
, ':');
166 for (*value
++ = '\0'; *value
== ' '; value
++);
167 if (!strcasecmp(option
, "reverse")) {
168 cell_style
.attr
|= CELL_ATTR_REVERSE
;
169 } else if (!strcasecmp(option
, "bold")) {
170 cell_style
.attr
|= CELL_ATTR_BOLD
;
171 } else if (!strcasecmp(option
, "notbold")) {
172 cell_style
.attr
&= ~CELL_ATTR_BOLD
;
173 } else if (!strcasecmp(option
, "italics")) {
174 cell_style
.attr
|= CELL_ATTR_ITALIC
;
175 } else if (!strcasecmp(option
, "notitalics")) {
176 cell_style
.attr
&= ~CELL_ATTR_ITALIC
;
177 } else if (!strcasecmp(option
, "underlined")) {
178 cell_style
.attr
|= CELL_ATTR_UNDERLINE
;
179 } else if (!strcasecmp(option
, "notunderlined")) {
180 cell_style
.attr
&= ~CELL_ATTR_UNDERLINE
;
181 } else if (!strcasecmp(option
, "blink")) {
182 cell_style
.attr
|= CELL_ATTR_BLINK
;
183 } else if (!strcasecmp(option
, "notblink")) {
184 cell_style
.attr
&= ~CELL_ATTR_BLINK
;
185 } else if (!strcasecmp(option
, "fore")) {
186 color_fromstring(win
->ui
, &cell_style
.fg
, value
);
187 } else if (!strcasecmp(option
, "back")) {
188 color_fromstring(win
->ui
, &cell_style
.bg
, value
);
192 tui
->styles
[win
->id
* UI_STYLE_MAX
+ id
] = cell_style
;
197 static void ui_draw_line(UiTerm
*tui
, int x
, int y
, char c
, enum UiStyle style_id
) {
198 if (x
< 0 || x
>= tui
->width
|| y
< 0 || y
>= tui
->height
)
200 CellStyle style
= tui
->styles
[style_id
];
201 Cell
*cells
= tui
->cells
+ y
* tui
->width
;
202 while (x
< tui
->width
) {
203 cells
[x
].data
[0] = c
;
204 cells
[x
].data
[1] = '\0';
205 cells
[x
].style
= style
;
210 static void ui_draw_string(UiTerm
*tui
, int x
, int y
, const char *str
, UiTermWin
*win
, enum UiStyle style_id
) {
211 debug("draw-string: [%d][%d]\n", y
, x
);
212 if (x
< 0 || x
>= tui
->width
|| y
< 0 || y
>= tui
->height
)
214 CellStyle style
= tui
->styles
[(win
? win
->id
: 0)*UI_STYLE_MAX
+ style_id
];
215 // FIXME: does not handle double width characters etc, share code with view.c?
216 Cell
*cells
= tui
->cells
+ y
* tui
->width
;
217 const size_t cell_size
= sizeof(cells
[0].data
)-1;
218 for (const char *next
= str
; *str
&& x
< tui
->width
; str
= next
) {
219 do next
++; while (!ISUTF8(*next
));
220 size_t len
= next
- str
;
223 len
= MIN(len
, cell_size
);
224 strncpy(cells
[x
].data
, str
, len
);
225 cells
[x
].data
[len
] = '\0';
226 cells
[x
].style
= style
;
231 static void ui_window_draw(UiWin
*w
) {
232 UiTermWin
*win
= (UiTermWin
*)w
;
233 UiTerm
*ui
= win
->ui
;
234 View
*view
= win
->win
->view
;
235 int width
= win
->width
, height
= win
->height
;
236 const Line
*line
= view_lines_first(view
);
237 bool status
= win
->options
& UI_OPTION_STATUSBAR
;
238 bool nu
= win
->options
& UI_OPTION_LINE_NUMBERS_ABSOLUTE
;
239 bool rnu
= win
->options
& UI_OPTION_LINE_NUMBERS_RELATIVE
;
240 bool sidebar
= nu
|| rnu
;
241 int sidebar_width
= sidebar
? snprintf(NULL
, 0, "%zd ", line
->lineno
+ height
- 2) : 0;
242 if (sidebar_width
!= win
->sidebar_width
) {
243 view_resize(view
, width
- sidebar_width
, status
? height
- 1 : height
);
244 win
->sidebar_width
= sidebar_width
;
246 vis_window_draw(win
->win
);
247 line
= view_lines_first(view
);
248 size_t prev_lineno
= 0;
249 Selection
*sel
= view_selections_primary_get(view
);
250 const Line
*cursor_line
= view_cursors_line_get(sel
);
251 size_t cursor_lineno
= cursor_line
->lineno
;
252 char buf
[(sizeof(size_t) * CHAR_BIT
+ 2) / 3 + 1 + 1];
253 int x
= win
->x
, y
= win
->y
;
254 int view_width
= view_width_get(view
);
255 Cell
*cells
= ui
->cells
+ y
* ui
->width
;
256 if (x
+ sidebar_width
+ view_width
> ui
->width
)
257 view_width
= ui
->width
- x
- sidebar_width
;
258 for (const Line
*l
= line
; l
; l
= l
->next
, y
++) {
260 if (!l
->lineno
|| !l
->len
|| l
->lineno
== prev_lineno
) {
261 memset(buf
, ' ', sizeof(buf
));
262 buf
[sidebar_width
] = '\0';
264 size_t number
= l
->lineno
;
266 number
= (win
->options
& UI_OPTION_LARGE_FILE
) ? 0 : l
->lineno
;
267 if (l
->lineno
> cursor_lineno
)
268 number
= l
->lineno
- cursor_lineno
;
269 else if (l
->lineno
< cursor_lineno
)
270 number
= cursor_lineno
- l
->lineno
;
272 snprintf(buf
, sizeof buf
, "%*zu ", sidebar_width
-1, number
);
274 ui_draw_string(ui
, x
, y
, buf
, win
,
275 (l
->lineno
== cursor_lineno
) ? UI_STYLE_LINENUMBER_CURSOR
: UI_STYLE_LINENUMBER
);
276 prev_lineno
= l
->lineno
;
278 debug("draw-window: [%d][%d] ... cells[%d][%d]\n", y
, x
+sidebar_width
, y
, view_width
);
279 memcpy(&cells
[x
+sidebar_width
], l
->cells
, sizeof(Cell
) * view_width
);
284 static CellStyle
ui_window_style_get(UiWin
*w
, enum UiStyle style
) {
285 UiTermWin
*win
= (UiTermWin
*)w
;
286 UiTerm
*tui
= win
->ui
;
287 return tui
->styles
[win
->id
* UI_STYLE_MAX
+ style
];
290 static void ui_window_status(UiWin
*w
, const char *status
) {
291 UiTermWin
*win
= (UiTermWin
*)w
;
292 if (!(win
->options
& UI_OPTION_STATUSBAR
))
294 UiTerm
*ui
= win
->ui
;
295 enum UiStyle style
= ui
->selwin
== win
? UI_STYLE_STATUS_FOCUSED
: UI_STYLE_STATUS
;
296 ui_draw_string(ui
, win
->x
, win
->y
+ win
->height
- 1, status
, win
, style
);
299 static void ui_arrange(Ui
*ui
, enum UiLayout layout
) {
300 debug("ui-arrange\n");
301 UiTerm
*tui
= (UiTerm
*)ui
;
302 tui
->layout
= layout
;
303 int n
= 0, m
= !!tui
->info
[0], x
= 0, y
= 0;
304 for (UiTermWin
*win
= tui
->windows
; win
; win
= win
->next
) {
305 if (win
->options
& UI_OPTION_ONELINE
)
310 int max_height
= tui
->height
- m
;
311 int width
= (tui
->width
/ MAX(1, n
)) - 1;
312 int height
= max_height
/ MAX(1, n
);
313 for (UiTermWin
*win
= tui
->windows
; win
; win
= win
->next
) {
314 if (win
->options
& UI_OPTION_ONELINE
)
317 if (layout
== UI_LAYOUT_HORIZONTAL
) {
318 int h
= n
? height
: max_height
- y
;
319 ui_window_resize(win
, tui
->width
, h
);
320 ui_window_move(win
, x
, y
);
323 int w
= n
? width
: tui
->width
- x
;
324 ui_window_resize(win
, w
, max_height
);
325 ui_window_move(win
, x
, y
);
328 Cell
*cells
= tui
->cells
;
329 for (int i
= 0; i
< max_height
; i
++) {
330 strcpy(cells
[x
].data
,"│");
331 cells
[x
].style
= tui
->styles
[UI_STYLE_SEPARATOR
];
339 if (layout
== UI_LAYOUT_VERTICAL
)
342 for (UiTermWin
*win
= tui
->windows
; win
; win
= win
->next
) {
343 if (!(win
->options
& UI_OPTION_ONELINE
))
345 ui_window_resize(win
, tui
->width
, 1);
346 ui_window_move(win
, 0, y
++);
350 static void ui_draw(Ui
*ui
) {
352 UiTerm
*tui
= (UiTerm
*)ui
;
353 ui_arrange(ui
, tui
->layout
);
354 for (UiTermWin
*win
= tui
->windows
; win
; win
= win
->next
)
355 ui_window_draw((UiWin
*)win
);
357 ui_draw_string(tui
, 0, tui
->height
-1, tui
->info
, NULL
, UI_STYLE_INFO
);
358 ui_term_backend_blit(tui
);
361 static void ui_redraw(Ui
*ui
) {
362 UiTerm
*tui
= (UiTerm
*)ui
;
363 ui_term_backend_clear(tui
);
364 for (UiTermWin
*win
= tui
->windows
; win
; win
= win
->next
)
365 view_invalidate(win
->win
->view
);
368 static void ui_resize(Ui
*ui
) {
369 UiTerm
*tui
= (UiTerm
*)ui
;
371 int width
= 80, height
= 24;
373 if (ioctl(STDERR_FILENO
, TIOCGWINSZ
, &ws
) != -1) {
380 width
= MIN(width
, MAX_WIDTH
);
381 height
= MIN(height
, MAX_HEIGHT
);
382 if (!ui_term_backend_resize(tui
, width
, height
))
385 size_t size
= width
*height
*sizeof(Cell
);
386 if (size
> tui
->cells_size
) {
387 Cell
*cells
= realloc(tui
->cells
, size
);
390 memset((char*)cells
+tui
->cells_size
, 0, size
- tui
->cells_size
);
391 tui
->cells_size
= size
;
395 tui
->height
= height
;
398 static void ui_window_free(UiWin
*w
) {
399 UiTermWin
*win
= (UiTermWin
*)w
;
402 UiTerm
*tui
= win
->ui
;
404 win
->prev
->next
= win
->next
;
406 win
->next
->prev
= win
->prev
;
407 if (tui
->windows
== win
)
408 tui
->windows
= win
->next
;
409 if (tui
->selwin
== win
)
411 win
->next
= win
->prev
= NULL
;
412 tui
->ids
&= ~(1UL << win
->id
);
416 static void ui_window_focus(UiWin
*w
) {
417 UiTermWin
*new = (UiTermWin
*)w
;
418 UiTermWin
*old
= new->ui
->selwin
;
419 if (new->options
& UI_OPTION_STATUSBAR
)
420 new->ui
->selwin
= new;
422 view_invalidate(old
->win
->view
);
423 view_invalidate(new->win
->view
);
426 static void ui_window_options_set(UiWin
*w
, enum UiOption options
) {
427 UiTermWin
*win
= (UiTermWin
*)w
;
428 win
->options
= options
;
429 if (options
& UI_OPTION_ONELINE
) {
430 /* move the new window to the end of the list */
431 UiTerm
*tui
= win
->ui
;
432 UiTermWin
*last
= tui
->windows
;
437 win
->prev
->next
= win
->next
;
439 win
->next
->prev
= win
->prev
;
440 if (tui
->windows
== win
)
441 tui
->windows
= win
->next
;
447 ui_draw((Ui
*)win
->ui
);
450 static enum UiOption
ui_window_options_get(UiWin
*win
) {
451 return ((UiTermWin
*)win
)->options
;
454 static int ui_window_width(UiWin
*win
) {
455 return ((UiTermWin
*)win
)->width
;
458 static int ui_window_height(UiWin
*win
) {
459 return ((UiTermWin
*)win
)->height
;
462 static void ui_window_swap(UiWin
*aw
, UiWin
*bw
) {
463 UiTermWin
*a
= (UiTermWin
*)aw
;
464 UiTermWin
*b
= (UiTermWin
*)bw
;
465 if (a
== b
|| !a
|| !b
)
468 UiTermWin
*tmp
= a
->next
;
482 if (tui
->windows
== a
)
484 else if (tui
->windows
== b
)
486 if (tui
->selwin
== a
)
488 else if (tui
->selwin
== b
)
492 static UiWin
*ui_window_new(Ui
*ui
, Win
*w
, enum UiOption options
) {
493 UiTerm
*tui
= (UiTerm
*)ui
;
494 /* get rightmost zero bit, i.e. highest available id */
495 size_t bit
= ~tui
->ids
& (tui
->ids
+ 1);
497 for (size_t tmp
= bit
; tmp
>>= 1; id
++);
498 if (id
>= sizeof(size_t) * 8)
500 size_t styles_size
= (id
+ 1) * UI_STYLE_MAX
* sizeof(CellStyle
);
501 if (styles_size
> tui
->styles_size
) {
502 CellStyle
*styles
= realloc(tui
->styles
, styles_size
);
505 tui
->styles
= styles
;
506 tui
->styles_size
= styles_size
;
508 UiTermWin
*win
= calloc(1, sizeof(UiTermWin
));
512 win
->uiwin
= (UiWin
) {
513 .style_get
= ui_window_style_get
,
514 .status
= ui_window_status
,
515 .options_set
= ui_window_options_set
,
516 .options_get
= ui_window_options_get
,
517 .style_define
= ui_style_define
,
518 .window_width
= ui_window_width
,
519 .window_height
= ui_window_height
,
527 CellStyle
*styles
= &tui
->styles
[win
->id
* UI_STYLE_MAX
];
528 for (int i
= 0; i
< UI_STYLE_MAX
; i
++) {
529 styles
[i
] = (CellStyle
) {
530 .fg
= CELL_COLOR_DEFAULT
,
531 .bg
= CELL_COLOR_DEFAULT
,
532 .attr
= CELL_ATTR_NORMAL
,
536 styles
[UI_STYLE_CURSOR
].attr
|= CELL_ATTR_REVERSE
;
537 styles
[UI_STYLE_CURSOR_PRIMARY
].attr
|= CELL_ATTR_REVERSE
|CELL_ATTR_BLINK
;
538 styles
[UI_STYLE_SELECTION
].attr
|= CELL_ATTR_REVERSE
;
539 styles
[UI_STYLE_COLOR_COLUMN
].attr
|= CELL_ATTR_REVERSE
;
540 styles
[UI_STYLE_STATUS
].attr
|= CELL_ATTR_REVERSE
;
541 styles
[UI_STYLE_STATUS_FOCUSED
].attr
|= CELL_ATTR_REVERSE
|CELL_ATTR_BOLD
;
542 styles
[UI_STYLE_INFO
].attr
|= CELL_ATTR_BOLD
;
543 view_ui(w
->view
, &win
->uiwin
);
546 tui
->windows
->prev
= win
;
547 win
->next
= tui
->windows
;
550 if (text_size(w
->file
->text
) > UI_LARGE_FILE_SIZE
) {
551 options
|= UI_OPTION_LARGE_FILE
;
552 options
&= ~UI_OPTION_LINE_NUMBERS_ABSOLUTE
;
555 ui_window_options_set((UiWin
*)win
, options
);
560 static void ui_info(Ui
*ui
, const char *msg
, va_list ap
) {
561 UiTerm
*tui
= (UiTerm
*)ui
;
562 ui_draw_line(tui
, 0, tui
->height
-1, ' ', UI_STYLE_INFO
);
563 vsnprintf(tui
->info
, sizeof(tui
->info
), msg
, ap
);
566 static void ui_info_hide(Ui
*ui
) {
567 UiTerm
*tui
= (UiTerm
*)ui
;
572 static TermKey
*ui_termkey_new(int fd
) {
573 TermKey
*termkey
= termkey_new(fd
, UI_TERMKEY_FLAGS
);
575 termkey_set_canonflags(termkey
, TERMKEY_CANON_DELBS
);
579 static TermKey
*ui_termkey_reopen(Ui
*ui
, int fd
) {
580 int tty
= open("/dev/tty", O_RDWR
);
583 if (tty
!= fd
&& dup2(tty
, fd
) == -1) {
588 return ui_termkey_new(fd
);
591 static TermKey
*ui_termkey_get(Ui
*ui
) {
592 UiTerm
*tui
= (UiTerm
*)ui
;
596 static void ui_suspend(Ui
*ui
) {
597 UiTerm
*tui
= (UiTerm
*)ui
;
598 ui_term_backend_suspend(tui
);
602 static void ui_resume(Ui
*ui
) {
603 UiTerm
*tui
= (UiTerm
*)ui
;
604 ui_term_backend_resume(tui
);
607 static bool ui_getkey(Ui
*ui
, TermKeyKey
*key
) {
608 UiTerm
*tui
= (UiTerm
*)ui
;
609 TermKeyResult ret
= termkey_getkey(tui
->termkey
, key
);
611 if (ret
== TERMKEY_RES_EOF
) {
612 termkey_destroy(tui
->termkey
);
614 if (!(tui
->termkey
= ui_termkey_reopen(ui
, STDIN_FILENO
)))
615 ui_die_msg(ui
, "Failed to re-open stdin as /dev/tty: %s\n", errno
!= 0 ? strerror(errno
) : "");
619 if (ret
== TERMKEY_RES_AGAIN
) {
621 fd
.fd
= STDIN_FILENO
;
623 if (poll(&fd
, 1, termkey_get_waittime(tui
->termkey
)) == 0)
624 ret
= termkey_getkey_force(tui
->termkey
, key
);
627 return ret
== TERMKEY_RES_KEY
;
630 static void ui_terminal_save(Ui
*ui
) {
631 UiTerm
*tui
= (UiTerm
*)ui
;
632 ui_term_backend_save(tui
);
633 termkey_stop(tui
->termkey
);
636 static void ui_terminal_restore(Ui
*ui
) {
637 UiTerm
*tui
= (UiTerm
*)ui
;
638 termkey_start(tui
->termkey
);
639 ui_term_backend_restore(tui
);
642 static bool ui_init(Ui
*ui
, Vis
*vis
) {
643 UiTerm
*tui
= (UiTerm
*)ui
;
646 setlocale(LC_CTYPE
, "");
648 char *term
= getenv("TERM");
651 setenv("TERM", term
, 1);
655 if (!(tui
->termkey
= ui_termkey_new(STDIN_FILENO
))) {
656 /* work around libtermkey bug which fails if stdin is /dev/null */
657 if (errno
== EBADF
) {
659 if (!(tui
->termkey
= ui_termkey_reopen(ui
, STDIN_FILENO
)) && errno
== ENXIO
)
660 tui
->termkey
= termkey_new_abstract(term
, UI_TERMKEY_FLAGS
);
666 if (!ui_term_backend_init(tui
, term
))
671 ui_die_msg(ui
, "Failed to start curses interface: %s\n", errno
!= 0 ? strerror(errno
) : "");
675 Ui
*ui_term_new(void) {
676 size_t styles_size
= UI_STYLE_MAX
* sizeof(CellStyle
);
677 CellStyle
*styles
= calloc(1, styles_size
);
680 UiTerm
*tui
= ui_term_backend_new();
685 tui
->styles_size
= styles_size
;
686 tui
->styles
= styles
;
690 .free
= ui_term_free
,
691 .termkey_get
= ui_termkey_get
,
692 .suspend
= ui_suspend
,
695 .window_new
= ui_window_new
,
696 .window_free
= ui_window_free
,
697 .window_focus
= ui_window_focus
,
698 .window_swap
= ui_window_swap
,
701 .arrange
= ui_arrange
,
704 .info_hide
= ui_info_hide
,
706 .terminal_save
= ui_terminal_save
,
707 .terminal_restore
= ui_terminal_restore
,
708 .colors
= ui_term_backend_colors
,
714 void ui_term_free(Ui
*ui
) {
715 UiTerm
*tui
= (UiTerm
*)ui
;
719 ui_window_free((UiWin
*)tui
->windows
);
720 ui_term_backend_free(tui
);
722 termkey_destroy(tui
->termkey
);