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
->width
] = (void*)tui
->cells
;
202 while (x
< tui
->width
) {
203 cells
[y
][x
].data
[0] = c
;
204 cells
[y
][x
].data
[1] = '\0';
205 cells
[y
][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
->width
] = (void*)tui
->cells
;
217 const size_t cell_size
= sizeof(cells
[0][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
[y
][x
].data
, str
, len
);
225 cells
[y
][x
].data
[len
] = '\0';
226 cells
[y
][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 Cell (*cells
)[ui
->width
] = (void*)ui
->cells
;
236 int width
= win
->width
, height
= win
->height
;
237 const Line
*line
= view_lines_first(view
);
238 bool status
= win
->options
& UI_OPTION_STATUSBAR
;
239 bool nu
= win
->options
& UI_OPTION_LINE_NUMBERS_ABSOLUTE
;
240 bool rnu
= win
->options
& UI_OPTION_LINE_NUMBERS_RELATIVE
;
241 bool sidebar
= nu
|| rnu
;
242 int sidebar_width
= sidebar
? snprintf(NULL
, 0, "%zd ", line
->lineno
+ height
- 2) : 0;
243 if (sidebar_width
!= win
->sidebar_width
) {
244 view_resize(view
, width
- sidebar_width
, status
? height
- 1 : height
);
245 win
->sidebar_width
= sidebar_width
;
247 vis_window_draw(win
->win
);
248 line
= view_lines_first(view
);
249 size_t prev_lineno
= 0;
250 Cursor
*cursor
= view_selections_primary_get(view
);
251 const Line
*cursor_line
= view_cursors_line_get(cursor
);
252 size_t cursor_lineno
= cursor_line
->lineno
;
253 char buf
[sidebar_width
+1];
254 int x
= win
->x
, y
= win
->y
;
255 int view_width
= view_width_get(view
);
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
) {
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
, UI_STYLE_LINENUMBER
);
275 prev_lineno
= l
->lineno
;
277 debug("draw-window: [%d][%d] ... cells[%d][%d]\n", y
, x
+sidebar_width
, y
, view_width
);
278 memcpy(&cells
[y
++][x
+sidebar_width
], l
->cells
, sizeof(Cell
) * view_width
);
282 static CellStyle
ui_window_style_get(UiWin
*w
, enum UiStyle style
) {
283 UiTermWin
*win
= (UiTermWin
*)w
;
284 UiTerm
*tui
= win
->ui
;
285 return tui
->styles
[win
->id
* UI_STYLE_MAX
+ style
];
288 static void ui_window_status(UiWin
*w
, const char *status
) {
289 UiTermWin
*win
= (UiTermWin
*)w
;
290 if (!(win
->options
& UI_OPTION_STATUSBAR
))
292 UiTerm
*ui
= win
->ui
;
293 enum UiStyle style
= ui
->selwin
== win
? UI_STYLE_STATUS_FOCUSED
: UI_STYLE_STATUS
;
294 ui_draw_string(ui
, win
->x
, win
->y
+ win
->height
- 1, status
, win
, style
);
297 static void ui_arrange(Ui
*ui
, enum UiLayout layout
) {
298 debug("ui-arrange\n");
299 UiTerm
*tui
= (UiTerm
*)ui
;
300 tui
->layout
= layout
;
301 Cell (*cells
)[tui
->width
] = (void*)tui
->cells
;
302 int n
= 0, m
= !!tui
->info
[0], x
= 0, y
= 0;
303 for (UiTermWin
*win
= tui
->windows
; win
; win
= win
->next
) {
304 if (win
->options
& UI_OPTION_ONELINE
)
309 int max_height
= tui
->height
- m
;
310 int width
= (tui
->width
/ MAX(1, n
)) - 1;
311 int height
= max_height
/ MAX(1, n
);
312 for (UiTermWin
*win
= tui
->windows
; win
; win
= win
->next
) {
313 if (win
->options
& UI_OPTION_ONELINE
)
316 if (layout
== UI_LAYOUT_HORIZONTAL
) {
317 int h
= n
? height
: max_height
- y
;
318 ui_window_resize(win
, tui
->width
, h
);
319 ui_window_move(win
, x
, y
);
322 int w
= n
? width
: tui
->width
- x
;
323 ui_window_resize(win
, w
, max_height
);
324 ui_window_move(win
, x
, y
);
327 for (int i
= 0; i
< max_height
; i
++) {
328 strcpy(cells
[i
][x
].data
,"│");
329 cells
[i
][x
].style
= tui
->styles
[UI_STYLE_SEPARATOR
];
336 if (layout
== UI_LAYOUT_VERTICAL
)
339 for (UiTermWin
*win
= tui
->windows
; win
; win
= win
->next
) {
340 if (!(win
->options
& UI_OPTION_ONELINE
))
342 ui_window_resize(win
, tui
->width
, 1);
343 ui_window_move(win
, 0, y
++);
347 static void ui_draw(Ui
*ui
) {
349 UiTerm
*tui
= (UiTerm
*)ui
;
350 ui_arrange(ui
, tui
->layout
);
351 for (UiTermWin
*win
= tui
->windows
; win
; win
= win
->next
)
352 ui_window_draw((UiWin
*)win
);
354 ui_draw_string(tui
, 0, tui
->height
-1, tui
->info
, NULL
, UI_STYLE_INFO
);
355 ui_term_backend_blit(tui
);
358 static void ui_redraw(Ui
*ui
) {
359 UiTerm
*tui
= (UiTerm
*)ui
;
360 ui_term_backend_clear(tui
);
361 for (UiTermWin
*win
= tui
->windows
; win
; win
= win
->next
)
362 view_invalidate(win
->win
->view
);
365 static void ui_resize(Ui
*ui
) {
366 UiTerm
*tui
= (UiTerm
*)ui
;
368 int width
= 80, height
= 24;
370 if (ioctl(STDERR_FILENO
, TIOCGWINSZ
, &ws
) != -1) {
375 width
= MAX(width
, 1);
376 width
= MIN(width
, MAX_WIDTH
);
377 height
= MAX(height
, 1);
378 height
= MIN(height
, MAX_HEIGHT
);
379 if (!ui_term_backend_resize(tui
, width
, height
))
382 size_t size
= width
*height
*sizeof(Cell
);
383 if (size
> tui
->cells_size
) {
384 Cell
*cells
= realloc(tui
->cells
, size
);
387 memset((char*)cells
+tui
->cells_size
, 0, size
- tui
->cells_size
);
388 tui
->cells_size
= size
;
392 tui
->height
= height
;
395 static void ui_window_free(UiWin
*w
) {
396 UiTermWin
*win
= (UiTermWin
*)w
;
399 UiTerm
*tui
= win
->ui
;
401 win
->prev
->next
= win
->next
;
403 win
->next
->prev
= win
->prev
;
404 if (tui
->windows
== win
)
405 tui
->windows
= win
->next
;
406 if (tui
->selwin
== win
)
408 win
->next
= win
->prev
= NULL
;
409 tui
->ids
&= ~(1UL << win
->id
);
413 static void ui_window_focus(UiWin
*w
) {
414 UiTermWin
*new = (UiTermWin
*)w
;
415 UiTermWin
*old
= new->ui
->selwin
;
416 if (new->options
& UI_OPTION_STATUSBAR
)
417 new->ui
->selwin
= new;
419 view_invalidate(old
->win
->view
);
420 view_invalidate(new->win
->view
);
423 static void ui_window_options_set(UiWin
*w
, enum UiOption options
) {
424 UiTermWin
*win
= (UiTermWin
*)w
;
425 win
->options
= options
;
426 if (options
& UI_OPTION_ONELINE
) {
427 /* move the new window to the end of the list */
428 UiTerm
*tui
= win
->ui
;
429 UiTermWin
*last
= tui
->windows
;
434 win
->prev
->next
= win
->next
;
436 win
->next
->prev
= win
->prev
;
437 if (tui
->windows
== win
)
438 tui
->windows
= win
->next
;
444 ui_draw((Ui
*)win
->ui
);
447 static enum UiOption
ui_window_options_get(UiWin
*win
) {
448 return ((UiTermWin
*)win
)->options
;
451 static int ui_window_width(UiWin
*win
) {
452 return ((UiTermWin
*)win
)->width
;
455 static int ui_window_height(UiWin
*win
) {
456 return ((UiTermWin
*)win
)->height
;
459 static void ui_window_swap(UiWin
*aw
, UiWin
*bw
) {
460 UiTermWin
*a
= (UiTermWin
*)aw
;
461 UiTermWin
*b
= (UiTermWin
*)bw
;
462 if (a
== b
|| !a
|| !b
)
465 UiTermWin
*tmp
= a
->next
;
479 if (tui
->windows
== a
)
481 else if (tui
->windows
== b
)
483 if (tui
->selwin
== a
)
485 else if (tui
->selwin
== b
)
489 static UiWin
*ui_window_new(Ui
*ui
, Win
*w
, enum UiOption options
) {
490 UiTerm
*tui
= (UiTerm
*)ui
;
491 /* get rightmost zero bit, i.e. highest available id */
492 size_t bit
= ~tui
->ids
& (tui
->ids
+ 1);
494 for (size_t tmp
= bit
; tmp
>>= 1; id
++);
495 if (id
>= sizeof(size_t) * 8)
497 size_t styles_size
= (id
+ 1) * UI_STYLE_MAX
* sizeof(CellStyle
);
498 if (styles_size
> tui
->styles_size
) {
499 CellStyle
*styles
= realloc(tui
->styles
, styles_size
);
502 tui
->styles
= styles
;
503 tui
->styles_size
= styles_size
;
505 UiTermWin
*win
= calloc(1, sizeof(UiTermWin
));
509 win
->uiwin
= (UiWin
) {
510 .style_get
= ui_window_style_get
,
511 .status
= ui_window_status
,
512 .options_set
= ui_window_options_set
,
513 .options_get
= ui_window_options_get
,
514 .style_define
= ui_style_define
,
515 .window_width
= ui_window_width
,
516 .window_height
= ui_window_height
,
524 CellStyle
*styles
= &tui
->styles
[win
->id
* UI_STYLE_MAX
];
525 for (int i
= 0; i
< UI_STYLE_MAX
; i
++) {
526 styles
[i
] = (CellStyle
) {
527 .fg
= CELL_COLOR_DEFAULT
,
528 .bg
= CELL_COLOR_DEFAULT
,
529 .attr
= CELL_ATTR_NORMAL
,
533 styles
[UI_STYLE_CURSOR
].attr
|= CELL_ATTR_REVERSE
;
534 styles
[UI_STYLE_CURSOR_PRIMARY
].attr
|= CELL_ATTR_REVERSE
|CELL_ATTR_BLINK
;
535 styles
[UI_STYLE_SELECTION
].attr
|= CELL_ATTR_REVERSE
;
536 styles
[UI_STYLE_COLOR_COLUMN
].attr
|= CELL_ATTR_REVERSE
;
537 styles
[UI_STYLE_STATUS
].attr
|= CELL_ATTR_REVERSE
;
538 styles
[UI_STYLE_STATUS_FOCUSED
].attr
|= CELL_ATTR_REVERSE
|CELL_ATTR_BOLD
;
539 styles
[UI_STYLE_INFO
].attr
|= CELL_ATTR_BOLD
;
540 view_ui(w
->view
, &win
->uiwin
);
543 tui
->windows
->prev
= win
;
544 win
->next
= tui
->windows
;
547 if (text_size(w
->file
->text
) > UI_LARGE_FILE_SIZE
) {
548 options
|= UI_OPTION_LARGE_FILE
;
549 options
&= ~UI_OPTION_LINE_NUMBERS_ABSOLUTE
;
552 ui_window_options_set((UiWin
*)win
, options
);
557 static void ui_info(Ui
*ui
, const char *msg
, va_list ap
) {
558 UiTerm
*tui
= (UiTerm
*)ui
;
559 ui_draw_line(tui
, 0, tui
->height
-1, ' ', UI_STYLE_INFO
);
560 vsnprintf(tui
->info
, sizeof(tui
->info
), msg
, ap
);
563 static void ui_info_hide(Ui
*ui
) {
564 UiTerm
*tui
= (UiTerm
*)ui
;
569 static TermKey
*ui_termkey_new(int fd
) {
570 TermKey
*termkey
= termkey_new(fd
, UI_TERMKEY_FLAGS
);
572 termkey_set_canonflags(termkey
, TERMKEY_CANON_DELBS
);
576 static TermKey
*ui_termkey_reopen(Ui
*ui
, int fd
) {
577 int tty
= open("/dev/tty", O_RDWR
);
580 if (tty
!= fd
&& dup2(tty
, fd
) == -1) {
585 return ui_termkey_new(fd
);
588 static TermKey
*ui_termkey_get(Ui
*ui
) {
589 UiTerm
*tui
= (UiTerm
*)ui
;
593 static void ui_suspend(Ui
*ui
) {
594 UiTerm
*tui
= (UiTerm
*)ui
;
595 ui_term_backend_suspend(tui
);
599 static void ui_resume(Ui
*ui
) {
600 UiTerm
*tui
= (UiTerm
*)ui
;
601 ui_term_backend_resume(tui
);
604 static bool ui_getkey(Ui
*ui
, TermKeyKey
*key
) {
605 UiTerm
*tui
= (UiTerm
*)ui
;
606 TermKeyResult ret
= termkey_getkey(tui
->termkey
, key
);
608 if (ret
== TERMKEY_RES_EOF
) {
609 termkey_destroy(tui
->termkey
);
611 if (!(tui
->termkey
= ui_termkey_reopen(ui
, STDIN_FILENO
)))
612 ui_die_msg(ui
, "Failed to re-open stdin as /dev/tty: %s\n", errno
!= 0 ? strerror(errno
) : "");
616 if (ret
== TERMKEY_RES_AGAIN
) {
618 fd
.fd
= STDIN_FILENO
;
620 if (poll(&fd
, 1, termkey_get_waittime(tui
->termkey
)) == 0)
621 ret
= termkey_getkey_force(tui
->termkey
, key
);
624 return ret
== TERMKEY_RES_KEY
;
627 static void ui_terminal_save(Ui
*ui
) {
628 UiTerm
*tui
= (UiTerm
*)ui
;
629 ui_term_backend_save(tui
);
630 termkey_stop(tui
->termkey
);
633 static void ui_terminal_restore(Ui
*ui
) {
634 UiTerm
*tui
= (UiTerm
*)ui
;
635 termkey_start(tui
->termkey
);
636 ui_term_backend_restore(tui
);
639 static bool ui_init(Ui
*ui
, Vis
*vis
) {
640 UiTerm
*tui
= (UiTerm
*)ui
;
643 setlocale(LC_CTYPE
, "");
645 char *term
= getenv("TERM");
650 if (!(tui
->termkey
= ui_termkey_new(STDIN_FILENO
))) {
651 /* work around libtermkey bug which fails if stdin is /dev/null */
652 if (errno
== EBADF
) {
654 if (!(tui
->termkey
= ui_termkey_reopen(ui
, STDIN_FILENO
)) && errno
== ENXIO
)
655 tui
->termkey
= termkey_new_abstract(term
, UI_TERMKEY_FLAGS
);
661 if (!ui_term_backend_init(tui
, term
))
666 ui_die_msg(ui
, "Failed to start curses interface: %s\n", errno
!= 0 ? strerror(errno
) : "");
670 Ui
*ui_term_new(void) {
671 size_t styles_size
= UI_STYLE_MAX
* sizeof(CellStyle
);
672 CellStyle
*styles
= calloc(1, styles_size
);
675 UiTerm
*tui
= ui_term_backend_new();
680 tui
->styles_size
= styles_size
;
681 tui
->styles
= styles
;
685 .free
= ui_term_free
,
686 .termkey_get
= ui_termkey_get
,
687 .suspend
= ui_suspend
,
690 .window_new
= ui_window_new
,
691 .window_free
= ui_window_free
,
692 .window_focus
= ui_window_focus
,
693 .window_swap
= ui_window_swap
,
696 .arrange
= ui_arrange
,
699 .info_hide
= ui_info_hide
,
701 .terminal_save
= ui_terminal_save
,
702 .terminal_restore
= ui_terminal_restore
,
703 .colors
= ui_term_backend_colors
,
709 void ui_term_free(Ui
*ui
) {
710 UiTerm
*tui
= (UiTerm
*)ui
;
714 ui_window_free((UiWin
*)tui
->windows
);
715 ui_term_backend_free(tui
);
717 termkey_destroy(tui
->termkey
);