tui.c (10221B)
1 #define _XOPEN_SOURCE 2 #define _BSD_SOURCE 3 #include <wchar.h> 4 5 #include <assert.h> 6 #include <locale.h> 7 #include <poll.h> 8 #include <stdarg.h> 9 #include <stdio.h> 10 #include <stdlib.h> 11 #include <string.h> 12 #include <sys/ioctl.h> 13 #include <termios.h> 14 #include <unistd.h> 15 16 #include "utf8.h" 17 #include "ui.h" 18 19 #define ESC "\x1b" 20 #define CURPOS ESC"[%d;%dH" 21 #define CLEARRIGHT ESC"[0K" 22 #define CURHIDE ESC"[?25l" 23 #define CURSHOW ESC"[?25h" 24 //#define CLEARLEFT ESC"[1K" 25 //#define ERASECHAR ESC"[1X" 26 27 typedef struct { 28 char *buf; 29 int len; 30 int cap; 31 } Abuf; 32 33 /* globals */ 34 struct termios origti; 35 struct winsize ws; 36 Abuf frame; 37 int compat_mode; 38 int is_modern; 39 int vs16_double = 1; 40 41 /* TODO: edo.h? */ 42 extern void *ecalloc(size_t nmemb, size_t size); 43 extern void *erealloc(void *p, size_t size); 44 extern void die(const char *fmt, ...); 45 extern char *cell_get_text(Cell *cell, char *pool_base); 46 47 /* function declarations */ 48 void ab_free(Abuf *ab); 49 void ab_ensure_cap(Abuf *ab, size_t addlen); 50 void ab_write(Abuf *ab, const char *s, size_t len); 51 int ab_printf(Abuf *ab, const char *fmt, ...); 52 void ab_flush(Abuf *ab); 53 void tui_frame_start(void); 54 void tui_frame_flush(void); 55 int tui_text_width(char *s, int len, int x); 56 int tui_text_len(char *s, int len); 57 void tui_get_window_size(int *rows, int *cols); 58 void tui_exit(void); 59 void tui_move_cursor(int x, int y); 60 void tui_draw_line(UI *ui, int x, int y, Cell *cells, int screen_cols); 61 void tui_draw_symbol(int r, int c, Symbol sym); 62 void tui_init(void); 63 64 /* function implementations */ 65 void 66 ab_free(Abuf *ab) { 67 if(!ab->buf) 68 return; 69 free(ab->buf); 70 ab->buf = NULL; 71 ab->len = 0; 72 ab->cap = 0; 73 } 74 75 void 76 ab_ensure_cap(Abuf *ab, size_t addlen) { 77 size_t newlen = ab->len + addlen; 78 79 if(newlen <= ab->cap) 80 return; 81 while(newlen > ab->cap) 82 ab->cap = ab->cap ? ab->cap * 2 : 8; 83 ab->buf = erealloc(ab->buf, ab->cap); 84 } 85 86 void 87 ab_write(Abuf *ab, const char *s, size_t len) { 88 ab_ensure_cap(&frame, len); 89 memcpy(ab->buf + ab->len, s, len); 90 ab->len += len; 91 } 92 93 int 94 ab_printf(Abuf *ab, const char *fmt, ...) { 95 va_list ap; 96 int len; 97 98 va_start(ap, fmt); 99 len = vsnprintf(NULL, 0, fmt, ap); 100 assert(len >= 0); 101 va_end(ap); 102 103 ab_ensure_cap(ab, len + 1); 104 105 va_start(ap, fmt); 106 vsnprintf(ab->buf + ab->len, len + 1, fmt, ap); 107 va_end(ap); 108 109 ab->len += len; 110 return len; 111 } 112 113 void 114 ab_flush(Abuf *ab) { 115 write(STDOUT_FILENO, ab->buf, ab->len); 116 ab_free(ab); 117 } 118 119 void 120 tui_frame_start(void) { 121 ab_write(&frame, CURHIDE, sizeof CURHIDE - 1); 122 } 123 124 void 125 tui_frame_flush(void) { 126 ab_write(&frame, CURSHOW, sizeof CURSHOW - 1); 127 ab_flush(&frame); 128 } 129 130 /* XXX move to utils? */ 131 int 132 hexlen(unsigned int n) { 133 int len = 0; 134 135 do { 136 len++; 137 n >>= 4; 138 } while(n > 0); 139 return len; 140 } 141 142 int 143 tui_text_width(char *s, int len, int x) { 144 int tabstop = 8; 145 int w = 0, i; 146 int step, wc; 147 unsigned int cp; 148 149 for(i = 0; i < len; i += step) { 150 step = utf8_decode(s + i, len - i, &cp); 151 if(cp == '\t') { 152 w += tabstop - x % tabstop; 153 continue; 154 } 155 156 wc = -1; 157 158 /* force RIS to be 2-cells wide */ 159 if(compat_mode && IS_RIS(cp)) wc = 2; 160 161 /* color modifier is zero-width in modern terminals while legacy 162 * VTs are able to see the square color modifiers */ 163 if(is_modern && IS_CMOD(cp)) wc = 0; 164 165 /* force 2 cells width for emoji followed by VS16 */ 166 if(vs16_double && is_modern && wc == -1) { 167 int nxi = i + step; 168 if(nxi < len) { 169 unsigned int nxcp; 170 utf8_decode(s + nxi, len - nxi, &nxcp); 171 if(nxcp == 0xFE0F && ((cp >= 0x203C && cp <= 0x3299) || cp >= 0x1F000)) 172 wc = 2; 173 } 174 } 175 176 if(wc < 0) wc = wcwidth(cp); 177 assert(wc != -1); 178 179 if(wc > 0) w += wc; 180 else if(compat_mode && !utf8_is_combining(cp)) w += hexlen(cp) + 2; /* 2 for < and > */ 181 //else w += hexlen(cp) + 2; /* 2 for < and > */ 182 } 183 return w; 184 } 185 186 int 187 tui_text_len(char *s, int len) { 188 return compat_mode ? utf8_len_compat(s, len) : utf8_len(s, len); 189 } 190 191 void 192 tui_get_window_size(int *rows, int *cols) { 193 *rows = ws.ws_row; 194 *cols = ws.ws_col; 195 } 196 197 void 198 tui_exit(void) { 199 tcsetattr(0, TCSANOW, &origti); 200 printf(CURPOS CLEARRIGHT, ws.ws_row, 0); 201 } 202 203 void 204 tui_move_cursor(int c, int r) { 205 int x = c + 1, y = r + 1; /* TERM coords are 1-based */ 206 ab_printf(&frame, CURPOS, y, x); 207 } 208 209 void 210 tui_draw_line_compat(UI *ui, int x, int y, Cell *cells, int count) { 211 char *txt; 212 int i; 213 214 tui_move_cursor(x, y); 215 for(i = 0; i < count; i++) { 216 txt = cell_get_text(cells + i, ui->pool.data); 217 218 int w = 0; 219 int o = 0; 220 221 while(o < cells[i].len && w < cells[i].width) { 222 unsigned int cp; 223 int step = utf8_decode(txt + o, cells[i].len - o, &cp); 224 225 if(cp == '\t') { 226 while(w++ < cells[i].width) 227 ab_write(&frame, " ", 1); 228 break; 229 } 230 231 int cw = wcwidth(cp); 232 if(cw < 0) break; 233 234 int showhex = !cw && !IS_CMOD(cp) && !IS_VAR(cp) && !utf8_is_combining(cp) ? 1 : 0; 235 236 if(!showhex && is_modern && compat_mode && IS_CMOD(cp)) showhex = 1; 237 238 if(showhex) { 239 char tag[16]; 240 snprintf(tag, sizeof tag, "<%0x>", cp); 241 242 if(cells[i].flags & CELL_TRUNC_L) { 243 cw = tui_text_width(txt + o, cells[i].len - o, 0); 244 o = cw - cells[i].width; 245 if(o < 0) o = 0; 246 } 247 { const char t[] = ESC"[48;5;233m"; ab_write(&frame, t, sizeof t - 1); } 248 249 int j = 0; 250 251 while(w < cells[i].width && x+w < ws.ws_col) { 252 ab_write(&frame, tag + o + j++, 1); 253 ++w; 254 } 255 256 { const char t[] = ESC"[0m"; ab_write(&frame, t, sizeof t - 1); } 257 break; 258 } 259 260 /* to preserve coherence between terminals always split 261 * RIS so that we can see individual components. */ 262 if(is_modern && IS_RIS(cp)) 263 ab_write(&frame, ZWNJ, sizeof ZWNJ - 1); 264 265 if(!cw) { 266 ab_write(&frame, txt + o, step); 267 o += step; 268 continue; 269 } 270 271 272 if(cells[i].flags & CELL_TRUNC_L) { 273 ab_write(&frame, "<", 1); 274 ++w; 275 while(w++ < cells[i].width) ab_write(&frame, ".", 1); 276 break; 277 } 278 if(cells[i].flags & CELL_TRUNC_R) { 279 ab_write(&frame, ">", 1); 280 ++w; 281 while(w++ < cells[i].width) ab_write(&frame, ".", 1); 282 break; 283 } 284 285 if(x+cw > ws.ws_col) break; 286 ab_write(&frame, txt + o, step); 287 288 o += step; 289 w += cw; 290 } 291 //ab_write(&frame, txt, cells[i].len); 292 293 /* pad to ensure we always honor cells[i].width 294 * should only happens with RIS on legacy VTs */ 295 if(!is_modern && w < cells[i].width) { 296 while(w < cells[i].width && x+w < ws.ws_col) { 297 ab_write(&frame, " ", 1); 298 ++w; 299 } 300 } 301 302 x += w; 303 } 304 305 if(x < ws.ws_col) ab_write(&frame, CLEARRIGHT, strlen(CLEARRIGHT)); 306 } 307 308 void 309 tui_draw_line(UI *ui, int x, int y, Cell *cells, int count) { 310 assert(x < ws.ws_col && y < ws.ws_row); 311 312 if(compat_mode) { 313 tui_draw_line_compat(ui, x, y, cells, count); 314 return; 315 } 316 317 char *txt; 318 int i; 319 320 tui_move_cursor(x, y); 321 for(i = 0; i < count; i++) { 322 x += cells[i].width; 323 txt = cell_get_text(cells + i, ui->pool.data); 324 325 /* TODO: temp code for testing, we'll se how to deal with this later */ 326 if(txt[0] == '\t') { 327 for(int t = 0; t < cells[i].width; t++) 328 ab_write(&frame, " ", 1); 329 continue; 330 } 331 332 if(cells[i].flags & CELL_TRUNC_L) { 333 ab_write(&frame, "<", 1); 334 for(int j = 1; j < cells[i].width; ++j) 335 ab_write(&frame, ".", 1); 336 continue; 337 } 338 if(cells[i].flags & CELL_TRUNC_R) { 339 ab_write(&frame, ">", 1); 340 for(int j = 1; j < cells[i].width; ++j) 341 ab_write(&frame, ".", 1); 342 continue; 343 } 344 345 ab_write(&frame, txt, cells[i].len); 346 } 347 348 ab_write(&frame, CLEARRIGHT, strlen(CLEARRIGHT)); 349 } 350 351 void 352 tui_draw_symbol(int c, int r, Symbol sym) { 353 int symch; 354 355 switch(sym) { 356 case SYM_EMPTYLINE: symch = '~'; break; 357 default: symch = '?'; break; 358 } 359 360 tui_move_cursor(c, r); 361 ab_printf(&frame, "%c" CLEARRIGHT, symch); 362 } 363 364 int 365 detect_width(char *buf, int sz) { 366 int w = 1; /* default for legacy VTs */ 367 int len = 0; 368 char *s, *e = NULL; 369 370 /* write an emoji which should be width=1 on legacy VTs and 2 on modern 371 * ones. 372 * 373 * \r start of line 374 * \xe2\x9d\xa4\xef\xb8\x8f the heart emoji 375 * \x1b[6n get position 376 * \r\x1b[K back to the start and clear 377 * 378 * We do all in a single write to be more efficient and most important 379 * to prevent visual glitches. */ 380 const char out[] = "\r\xe2\x9d\xa4\xef\xb8\x8f\x1b[6n\r\x1b[K"; 381 write(STDOUT_FILENO, out, sizeof out - 1); 382 383 memset(buf, 0, sz); 384 385 /* quick loop max 100ms */ 386 struct pollfd fd = {STDIN_FILENO, POLLIN, 0}; 387 for(int i = 0; i < 10; i++) { 388 if(poll(&fd, 1, 10) <= 0) continue; 389 int n = read(STDIN_FILENO, buf + len, sz - len - 1); 390 if(n <= 0) continue; 391 len += n; 392 buf[len] = '\0'; 393 if((e = strchr(buf, 'R'))) break; 394 } 395 396 s = strstr(buf, "\x1b["); 397 if(s && e) { 398 /* if col is > 2 then emoji is 2-cells wide */ 399 char *sc = strchr(s, ';'); 400 if(sc && atoi(sc + 1) > 2) w = 2; 401 402 /* remove our test from user buffer */ 403 memmove(s, e + 1, len - (e - buf)); 404 } 405 return w; 406 } 407 408 void 409 tui_init(void) { 410 struct termios ti; 411 412 setlocale(LC_CTYPE, ""); 413 tcgetattr(0, &origti); 414 cfmakeraw(&ti); 415 416 ti.c_iflag |= ICRNL; 417 /* 418 ti.c_iflag &= ~(BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL|IXON); 419 ti.c_lflag &= ~(ECHO|ECHONL|ICANON|ISIG|IEXTEN); 420 ti.c_cflag &= ~(CSIZE|PARENB); 421 ti.c_cflag |= CS8; 422 */ 423 ti.c_cc[VMIN] = 1; 424 ti.c_cc[VTIME] = 0; 425 tcsetattr(0, TCSAFLUSH, &ti); 426 setbuf(stdout, NULL); 427 ioctl(0, TIOCGWINSZ, &ws); 428 429 /* auto-detect VT type */ 430 char user_input[1024]; 431 int emoji_width = detect_width(user_input, sizeof user_input); 432 433 is_modern = emoji_width == 2; 434 if(strlen(user_input)) { 435 die("TODO: user typed while initializing...\n"); 436 } 437 438 //die("Is%smodern VT (width=%d)\n", is_modern ? " " : " NOT ", emoji_width); 439 compat_mode = !is_modern; /* TODO: toggable (upward only) */ 440 compat_mode = 1; /* currently forced for development */ 441 } 442 443 int 444 tui_read_byte(void) { 445 char c; 446 447 if(read(STDIN_FILENO, &c, 1) == 1) 448 return c; 449 return -1; 450 } 451 452 Event 453 tui_next_event(void) { 454 Event ev; 455 int c = tui_read_byte(); 456 457 if(c == 0x1B) { 458 ev.type = EV_UKN; 459 return ev; 460 } 461 462 ev.type = EV_KEY; 463 ev.key = c; 464 return ev; 465 } 466 467 UI ui_tui = { 468 .name = "TUI", 469 .init = tui_init, 470 .exit = tui_exit, 471 .frame_start = tui_frame_start, 472 .frame_flush = tui_frame_flush, 473 .text_width = tui_text_width, 474 .text_len = tui_text_len, 475 .move_cursor = tui_move_cursor, 476 .draw_line = tui_draw_line, 477 .draw_symbol = tui_draw_symbol, 478 .get_window_size = tui_get_window_size, 479 .next_event = tui_next_event 480 };