vis
a vi-like editor based on Plan 9's structural regular expressions
git clone https://9o.is/git/vis.git
ui-terminal.c
(17313B)
1 #include <unistd.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <strings.h>
5 #include <limits.h>
6 #include <ctype.h>
7 #include <locale.h>
8 #include <poll.h>
9 #include <sys/ioctl.h>
10 #include <sys/types.h>
11 #include <sys/stat.h>
12 #include <fcntl.h>
13 #include <termios.h>
14 #include <errno.h>
15
16 #include "vis.h"
17 #include "vis-core.h"
18 #include "text.h"
19 #include "util.h"
20 #include "text-util.h"
21
22 #ifndef DEBUG_UI
23 #define DEBUG_UI 0
24 #endif
25
26 #if DEBUG_UI
27 #define debug(...) do { printf(__VA_ARGS__); fflush(stdout); } while (0)
28 #else
29 #define debug(...) do { } while (0)
30 #endif
31
32 #if CONFIG_CURSES
33 #include "ui-terminal-curses.c"
34 #else
35 #include "ui-terminal-vt100.c"
36 #endif
37
38 /* helper macro for handling UiTerm.cells */
39 #define CELL_AT_POS(UI, X, Y) (((UI)->cells) + (X) + ((Y) * (UI)->width));
40
41 #define CELL_STYLE_DEFAULT (CellStyle){.fg = CELL_COLOR_DEFAULT, .bg = CELL_COLOR_DEFAULT, .attr = CELL_ATTR_NORMAL}
42
43 static bool is_default_fg(CellColor c) {
44 return is_default_color(c);
45 }
46
47 static bool is_default_bg(CellColor c) {
48 return is_default_color(c);
49 }
50
51 void ui_die(Ui *tui, const char *msg, va_list ap) {
52 ui_terminal_free(tui);
53 if (tui->termkey)
54 termkey_stop(tui->termkey);
55 vfprintf(stderr, msg, ap);
56 exit(EXIT_FAILURE);
57 }
58
59 static void ui_die_msg(Ui *ui, const char *msg, ...) {
60 va_list ap;
61 va_start(ap, msg);
62 ui_die(ui, msg, ap);
63 va_end(ap);
64 }
65
66 static void ui_window_resize(Win *win, int width, int height) {
67 debug("ui-win-resize[%s]: %dx%d\n", win->file->name ? win->file->name : "noname", width, height);
68 bool status = win->options & UI_OPTION_STATUSBAR;
69 win->width = width;
70 win->height = height;
71 view_resize(&win->view, width - win->sidebar_width, status ? height - 1 : height);
72 }
73
74 static void ui_window_move(Win *win, int x, int y) {
75 debug("ui-win-move[%s]: (%d, %d)\n", win->file->name ? win->file->name : "noname", x, y);
76 win->x = x;
77 win->y = y;
78 }
79
80 static bool color_fromstring(Ui *ui, CellColor *color, const char *s)
81 {
82 if (!s)
83 return false;
84 if (*s == '#' && strlen(s) == 7) {
85 const char *cp;
86 unsigned char r, g, b;
87 for (cp = s + 1; isxdigit((unsigned char)*cp); cp++);
88 if (*cp != '\0')
89 return false;
90 int n = sscanf(s + 1, "%2hhx%2hhx%2hhx", &r, &g, &b);
91 if (n != 3)
92 return false;
93 *color = color_rgb(ui, r, g, b);
94 return true;
95 } else if ('0' <= *s && *s <= '9') {
96 int index = atoi(s);
97 if (index <= 0 || index > 255)
98 return false;
99 *color = color_terminal(ui, index);
100 return true;
101 }
102
103 static const struct {
104 const char *name;
105 CellColor color;
106 } color_names[] = {
107 { "black", CELL_COLOR_BLACK },
108 { "red", CELL_COLOR_RED },
109 { "green", CELL_COLOR_GREEN },
110 { "yellow", CELL_COLOR_YELLOW },
111 { "blue", CELL_COLOR_BLUE },
112 { "magenta", CELL_COLOR_MAGENTA },
113 { "cyan", CELL_COLOR_CYAN },
114 { "white", CELL_COLOR_WHITE },
115 { "default", CELL_COLOR_DEFAULT },
116 };
117
118 for (size_t i = 0; i < LENGTH(color_names); i++) {
119 if (strcasecmp(color_names[i].name, s) == 0) {
120 *color = color_names[i].color;
121 return true;
122 }
123 }
124
125 return false;
126 }
127
128 void ui_showcursor(bool show) {
129 cursor_visible(show);
130 }
131
132 bool ui_style_define(Win *win, int id, const char *style) {
133 Ui *tui = &win->vis->ui;
134 if (id >= UI_STYLE_MAX)
135 return false;
136 if (!style)
137 return true;
138
139 CellStyle cell_style = CELL_STYLE_DEFAULT;
140 char *style_copy = strdup(style), *option = style_copy;
141 while (option) {
142 while (*option == ' ')
143 option++;
144 char *next = strchr(option, ',');
145 if (next)
146 *next++ = '\0';
147 char *value = strchr(option, ':');
148 if (value)
149 for (*value++ = '\0'; *value == ' '; value++);
150 if (!strcasecmp(option, "reverse")) {
151 cell_style.attr |= CELL_ATTR_REVERSE;
152 } else if (!strcasecmp(option, "notreverse")) {
153 cell_style.attr &= CELL_ATTR_REVERSE;
154 } else if (!strcasecmp(option, "bold")) {
155 cell_style.attr |= CELL_ATTR_BOLD;
156 } else if (!strcasecmp(option, "notbold")) {
157 cell_style.attr &= ~CELL_ATTR_BOLD;
158 } else if (!strcasecmp(option, "dim")) {
159 cell_style.attr |= CELL_ATTR_DIM;
160 } else if (!strcasecmp(option, "notdim")) {
161 cell_style.attr &= ~CELL_ATTR_DIM;
162 } else if (!strcasecmp(option, "italics")) {
163 cell_style.attr |= CELL_ATTR_ITALIC;
164 } else if (!strcasecmp(option, "notitalics")) {
165 cell_style.attr &= ~CELL_ATTR_ITALIC;
166 } else if (!strcasecmp(option, "underlined")) {
167 cell_style.attr |= CELL_ATTR_UNDERLINE;
168 } else if (!strcasecmp(option, "notunderlined")) {
169 cell_style.attr &= ~CELL_ATTR_UNDERLINE;
170 } else if (!strcasecmp(option, "blink")) {
171 cell_style.attr |= CELL_ATTR_BLINK;
172 } else if (!strcasecmp(option, "notblink")) {
173 cell_style.attr &= ~CELL_ATTR_BLINK;
174 } else if (!strcasecmp(option, "fore")) {
175 color_fromstring(&win->vis->ui, &cell_style.fg, value);
176 } else if (!strcasecmp(option, "back")) {
177 color_fromstring(&win->vis->ui, &cell_style.bg, value);
178 }
179 option = next;
180 }
181 tui->styles[win->id * UI_STYLE_MAX + id] = cell_style;
182 free(style_copy);
183 return true;
184 }
185
186 static void ui_draw_line(Ui *tui, int x, int y, char c, enum UiStyle style_id) {
187 if (x < 0 || x >= tui->width || y < 0 || y >= tui->height)
188 return;
189 CellStyle style = tui->styles[style_id];
190 Cell *cells = tui->cells + y * tui->width;
191 while (x < tui->width) {
192 cells[x].data[0] = c;
193 cells[x].data[1] = '\0';
194 cells[x].style = style;
195 x++;
196 }
197 }
198
199 static void ui_draw_string(Ui *tui, int x, int y, const char *str, int win_id, enum UiStyle style_id) {
200 debug("draw-string: [%d][%d]\n", y, x);
201 if (x < 0 || x >= tui->width || y < 0 || y >= tui->height)
202 return;
203
204 /* NOTE: the style that style_id refers to may contain unset values; we need to properly
205 * clear the cell first then go through ui_window_style_set to get the correct style */
206 CellStyle default_style = tui->styles[UI_STYLE_MAX * win_id + UI_STYLE_DEFAULT];
207 // FIXME: does not handle double width characters etc, share code with view.c?
208 Cell *cells = tui->cells + y * tui->width;
209 const size_t cell_size = sizeof(cells[0].data)-1;
210 for (const char *next = str; *str && x < tui->width; str = next) {
211 do next++; while (!ISUTF8(*next));
212 size_t len = next - str;
213 if (!len)
214 break;
215 len = MIN(len, cell_size);
216 strncpy(cells[x].data, str, len);
217 cells[x].data[len] = '\0';
218 cells[x].style = default_style;
219 ui_window_style_set(tui, win_id, cells + x++, style_id, false);
220 }
221 }
222
223 static void ui_window_draw(Win *win) {
224 Ui *ui = &win->vis->ui;
225 View *view = &win->view;
226 const Line *line = win->view.topline;
227
228 bool status = win->options & UI_OPTION_STATUSBAR;
229 bool nu = win->options & UI_OPTION_LINE_NUMBERS_ABSOLUTE;
230 bool rnu = win->options & UI_OPTION_LINE_NUMBERS_RELATIVE;
231 bool sidebar = nu || rnu;
232
233 int width = win->width, height = win->height;
234 int sidebar_width = sidebar ? snprintf(NULL, 0, "%zd ", line->lineno + height - 2) : 0;
235 if (sidebar_width != win->sidebar_width) {
236 view_resize(view, width - sidebar_width, status ? height - 1 : height);
237 win->sidebar_width = sidebar_width;
238 }
239 vis_window_draw(win);
240
241 Selection *sel = view_selections_primary_get(view);
242 size_t prev_lineno = 0, cursor_lineno = sel->line->lineno;
243 char buf[(sizeof(size_t) * CHAR_BIT + 2) / 3 + 1 + 1];
244 int x = win->x, y = win->y;
245 int view_width = view->width;
246 Cell *cells = ui->cells + y * ui->width;
247 if (x + sidebar_width + view_width > ui->width)
248 view_width = ui->width - x - sidebar_width;
249 for (const Line *l = line; l; l = l->next, y++) {
250 if (sidebar) {
251 if (!l->lineno || !l->len || l->lineno == prev_lineno) {
252 memset(buf, ' ', sizeof(buf));
253 buf[sidebar_width] = '\0';
254 } else {
255 size_t number = l->lineno;
256 if (rnu) {
257 number = (win->options & UI_OPTION_LARGE_FILE) ? 0 : l->lineno;
258 if (l->lineno > cursor_lineno)
259 number = l->lineno - cursor_lineno;
260 else if (l->lineno < cursor_lineno)
261 number = cursor_lineno - l->lineno;
262 }
263 snprintf(buf, sizeof buf, "%*zu ", sidebar_width-1, number);
264 }
265 ui_draw_string(ui, x, y, buf, win->id,
266 (l->lineno == cursor_lineno) ? UI_STYLE_LINENUMBER_CURSOR :
267 UI_STYLE_LINENUMBER);
268 prev_lineno = l->lineno;
269 }
270 debug("draw-window: [%d][%d] ... cells[%d][%d]\n", y, x+sidebar_width, y, view_width);
271 memcpy(cells + x + sidebar_width, l->cells, sizeof(Cell) * view_width);
272 cells += ui->width;
273 }
274 }
275
276 void ui_window_style_set(Ui *tui, int win_id, Cell *cell, enum UiStyle id, bool keep_non_default) {
277 CellStyle set = tui->styles[win_id * UI_STYLE_MAX + id];
278
279 if (id != UI_STYLE_DEFAULT) {
280 if (keep_non_default) {
281 CellStyle default_style = tui->styles[win_id * UI_STYLE_MAX + UI_STYLE_DEFAULT];
282 if (!cell_color_equal(cell->style.fg, default_style.fg))
283 set.fg = cell->style.fg;
284 if (!cell_color_equal(cell->style.bg, default_style.bg))
285 set.bg = cell->style.bg;
286 }
287 set.fg = is_default_fg(set.fg)? cell->style.fg : set.fg;
288 set.bg = is_default_bg(set.bg)? cell->style.bg : set.bg;
289 set.attr = cell->style.attr | set.attr;
290 }
291
292 cell->style = set;
293 }
294
295 bool ui_window_style_set_pos(Win *win, int x, int y, enum UiStyle id, bool keep_non_default) {
296 Ui *tui = &win->vis->ui;
297 if (x < 0 || y < 0 || y >= win->height || x >= win->width) {
298 return false;
299 }
300 Cell *cell = CELL_AT_POS(tui, win->x + x, win->y + y)
301 ui_window_style_set(tui, win->id, cell, id, keep_non_default);
302 return true;
303 }
304
305 void ui_window_status(Win *win, const char *status) {
306 if (!(win->options & UI_OPTION_STATUSBAR))
307 return;
308 Ui *ui = &win->vis->ui;
309 enum UiStyle style = ui->selwin == win ? UI_STYLE_STATUS_FOCUSED : UI_STYLE_STATUS;
310 ui_draw_string(ui, win->x, win->y + win->height - 1, status, win->id, style);
311 }
312
313 void ui_arrange(Ui *tui, enum UiLayout layout) {
314 debug("ui-arrange\n");
315 tui->layout = layout;
316 int n = 0, m = !!tui->info[0], x = 0, y = 0;
317 for (Win *win = tui->windows; win; win = win->next) {
318 if (win->options & UI_OPTION_ONELINE)
319 m++;
320 else
321 n++;
322 }
323 int max_height = tui->height - m;
324 int width = (tui->width / MAX(1, n)) - 1;
325 int height = max_height / MAX(1, n);
326 for (Win *win = tui->windows; win; win = win->next) {
327 if (win->options & UI_OPTION_ONELINE)
328 continue;
329 n--;
330 if (layout == UI_LAYOUT_HORIZONTAL) {
331 int h = n ? height : max_height - y;
332 ui_window_resize(win, tui->width, h);
333 ui_window_move(win, x, y);
334 y += h;
335 } else {
336 int w = n ? width : tui->width - x;
337 ui_window_resize(win, w, max_height);
338 ui_window_move(win, x, y);
339 x += w;
340 if (n) {
341 Cell *cells = tui->cells;
342 for (int i = 0; i < max_height; i++) {
343 strcpy(cells[x].data,"│");
344 cells[x].style = tui->styles[UI_STYLE_SEPARATOR];
345 cells += tui->width;
346 }
347 x++;
348 }
349 }
350 }
351
352 if (layout == UI_LAYOUT_VERTICAL)
353 y = max_height;
354
355 for (Win *win = tui->windows; win; win = win->next) {
356 if (!(win->options & UI_OPTION_ONELINE))
357 continue;
358 ui_window_resize(win, tui->width, 1);
359 ui_window_move(win, 0, y++);
360 }
361 }
362
363 void ui_draw(Ui *tui) {
364 debug("ui-draw\n");
365 ui_arrange(tui, tui->layout);
366 for (Win *win = tui->windows; win; win = win->next)
367 ui_window_draw(win);
368 if (tui->info[0])
369 ui_draw_string(tui, 0, tui->height-1, tui->info, 0, UI_STYLE_INFO);
370 vis_event_emit(tui->vis, VIS_EVENT_UI_DRAW);
371 ui_term_backend_blit(tui);
372 }
373
374 void ui_redraw(Ui *tui) {
375 ui_term_backend_clear(tui);
376 for (Win *win = tui->windows; win; win = win->next)
377 win->view.need_update = true;
378 }
379
380 void ui_resize(Ui *tui) {
381 struct winsize ws;
382 int width = 80, height = 24;
383
384 if (ioctl(STDERR_FILENO, TIOCGWINSZ, &ws) != -1) {
385 if (ws.ws_col > 0)
386 width = ws.ws_col;
387 if (ws.ws_row > 0)
388 height = ws.ws_row;
389 }
390
391 width = MIN(width, UI_MAX_WIDTH);
392 height = MIN(height, UI_MAX_HEIGHT);
393 if (!ui_term_backend_resize(tui, width, height))
394 return;
395
396 size_t size = width*height*sizeof(Cell);
397 if (size > tui->cells_size) {
398 Cell *cells = realloc(tui->cells, size);
399 if (!cells)
400 return;
401 memset((char*)cells+tui->cells_size, 0, size - tui->cells_size);
402 tui->cells_size = size;
403 tui->cells = cells;
404 }
405 tui->width = width;
406 tui->height = height;
407 }
408
409 void ui_window_release(Ui *tui, Win *win) {
410 if (!win)
411 return;
412 if (tui->windows == win)
413 tui->windows = win->next;
414 if (tui->selwin == win)
415 tui->selwin = NULL;
416 tui->ids &= ~(1UL << win->id);
417 }
418
419 void ui_window_focus(Win *new) {
420 Win *old = new->vis->ui.selwin;
421 if (new->options & UI_OPTION_STATUSBAR)
422 new->vis->ui.selwin = new;
423 if (old)
424 old->view.need_update = true;
425 new->view.need_update = true;
426 }
427
428 void ui_window_options_set(Win *win, enum UiOption options) {
429 win->options = options;
430 if (options & UI_OPTION_ONELINE) {
431 /* move the new window to the end of the list */
432 Ui *tui = &win->vis->ui;
433 Win *last = tui->windows;
434 while (last->next)
435 last = last->next;
436 if (last != win) {
437 if (tui->windows == win)
438 tui->windows = win->next;
439 last->next = win;
440 }
441 }
442 ui_draw(&win->vis->ui);
443 }
444
445 void ui_window_swap(Win *a, Win *b) {
446 if (a == b || !a || !b)
447 return;
448 Ui *tui = &a->vis->ui;
449 if (tui->windows == a)
450 tui->windows = b;
451 else if (tui->windows == b)
452 tui->windows = a;
453 if (tui->selwin == a)
454 ui_window_focus(b);
455 else if (tui->selwin == b)
456 ui_window_focus(a);
457 }
458
459 bool ui_window_init(Ui *tui, Win *w, enum UiOption options) {
460 /* get rightmost zero bit, i.e. highest available id */
461 size_t bit = ~tui->ids & (tui->ids + 1);
462 size_t id = 0;
463 for (size_t tmp = bit; tmp >>= 1; id++);
464 if (id >= sizeof(size_t) * 8)
465 return NULL;
466 size_t styles_size = (id + 1) * UI_STYLE_MAX * sizeof(CellStyle);
467 if (styles_size > tui->styles_size) {
468 CellStyle *styles = realloc(tui->styles, styles_size);
469 if (!styles)
470 return NULL;
471 tui->styles = styles;
472 tui->styles_size = styles_size;
473 }
474
475 tui->ids |= bit;
476 w->id = id;
477
478 CellStyle *styles = &tui->styles[w->id * UI_STYLE_MAX];
479 for (int i = 0; i < UI_STYLE_MAX; i++) {
480 styles[i] = CELL_STYLE_DEFAULT;
481 }
482
483 styles[UI_STYLE_CURSOR].attr |= CELL_ATTR_REVERSE;
484 styles[UI_STYLE_CURSOR_PRIMARY].attr |= CELL_ATTR_REVERSE|CELL_ATTR_BLINK;
485 styles[UI_STYLE_SELECTION].attr |= CELL_ATTR_REVERSE;
486 styles[UI_STYLE_COLOR_COLUMN].attr |= CELL_ATTR_REVERSE;
487 styles[UI_STYLE_STATUS].attr |= CELL_ATTR_REVERSE;
488 styles[UI_STYLE_STATUS_FOCUSED].attr |= CELL_ATTR_REVERSE|CELL_ATTR_BOLD;
489 styles[UI_STYLE_INFO].attr |= CELL_ATTR_BOLD;
490
491 if (tui->windows)
492 tui->windows->prev = w->prev;
493 tui->windows = w;
494
495 if (text_size(w->file->text) > UI_LARGE_FILE_SIZE) {
496 options |= UI_OPTION_LARGE_FILE;
497 options &= ~UI_OPTION_LINE_NUMBERS_ABSOLUTE;
498 }
499
500 win_options_set(w, options);
501
502 return true;
503 }
504
505 void ui_info_show(Ui *tui, const char *msg, va_list ap) {
506 ui_draw_line(tui, 0, tui->height-1, ' ', UI_STYLE_INFO);
507 vsnprintf(tui->info, sizeof(tui->info), msg, ap);
508 }
509
510 void ui_info_hide(Ui *tui) {
511 if (tui->info[0])
512 tui->info[0] = '\0';
513 }
514
515 static TermKey *ui_termkey_new(int fd) {
516 TermKey *termkey = termkey_new(fd, UI_TERMKEY_FLAGS);
517 if (termkey)
518 termkey_set_canonflags(termkey, TERMKEY_CANON_DELBS);
519 return termkey;
520 }
521
522 static TermKey *ui_termkey_reopen(Ui *ui, int fd) {
523 int tty = open("/dev/tty", O_RDWR);
524 if (tty == -1)
525 return NULL;
526 if (tty != fd && dup2(tty, fd) == -1) {
527 close(tty);
528 return NULL;
529 }
530 close(tty);
531 return ui_termkey_new(fd);
532 }
533
534 void ui_terminal_suspend(Ui *tui) {
535 ui_term_backend_suspend(tui);
536 kill(0, SIGTSTP);
537 }
538
539 bool ui_getkey(Ui *tui, TermKeyKey *key) {
540 TermKeyResult ret = termkey_getkey(tui->termkey, key);
541
542 if (ret == TERMKEY_RES_EOF) {
543 termkey_destroy(tui->termkey);
544 errno = 0;
545 if (!(tui->termkey = ui_termkey_reopen(tui, STDIN_FILENO)))
546 ui_die_msg(tui, "Failed to re-open stdin as /dev/tty: %s\n", errno != 0 ? strerror(errno) : "");
547 return false;
548 }
549
550 if (ret == TERMKEY_RES_AGAIN) {
551 struct pollfd fd;
552 fd.fd = STDIN_FILENO;
553 fd.events = POLLIN;
554 if (poll(&fd, 1, termkey_get_waittime(tui->termkey)) == 0)
555 ret = termkey_getkey_force(tui->termkey, key);
556 }
557
558 return ret == TERMKEY_RES_KEY;
559 }
560
561 void ui_terminal_save(Ui *tui, bool fscr) {
562 ui_term_backend_save(tui, fscr);
563 termkey_stop(tui->termkey);
564 }
565
566 void ui_terminal_restore(Ui *tui) {
567 termkey_start(tui->termkey);
568 ui_term_backend_restore(tui);
569 }
570
571 bool ui_init(Ui *tui, Vis *vis) {
572 tui->vis = vis;
573
574 setlocale(LC_CTYPE, "");
575
576 char *term = getenv("TERM");
577 if (!term) {
578 term = "xterm";
579 setenv("TERM", term, 1);
580 }
581
582 errno = 0;
583 if (!(tui->termkey = ui_termkey_new(STDIN_FILENO))) {
584 /* work around libtermkey bug which fails if stdin is /dev/null */
585 if (errno == EBADF) {
586 errno = 0;
587 if (!(tui->termkey = ui_termkey_reopen(tui, STDIN_FILENO)) && errno == ENXIO)
588 tui->termkey = termkey_new_abstract(term, UI_TERMKEY_FLAGS);
589 }
590 if (!tui->termkey)
591 goto err;
592 }
593
594 if (!ui_term_backend_init(tui, term))
595 goto err;
596 ui_resize(tui);
597 return true;
598 err:
599 ui_die_msg(tui, "Failed to start curses interface: %s\n", errno != 0 ? strerror(errno) : "");
600 return false;
601 }
602
603 bool ui_terminal_init(Ui *tui) {
604 size_t styles_size = UI_STYLE_MAX * sizeof(CellStyle);
605 CellStyle *styles = calloc(1, styles_size);
606 if (!styles)
607 return false;
608 if (!ui_backend_init(tui)) {
609 free(styles);
610 return false;
611 }
612 tui->styles_size = styles_size;
613 tui->styles = styles;
614 tui->doupdate = true;
615 return true;
616 }
617
618 void ui_terminal_free(Ui *tui) {
619 if (!tui)
620 return;
621 while (tui->windows)
622 ui_window_release(tui, tui->windows);
623 ui_term_backend_free(tui);
624 if (tui->termkey)
625 termkey_destroy(tui->termkey);
626 free(tui->cells);
627 free(tui->styles);
628 }