tui.c (7101B)
1 #define _XOPEN_SOURCE 2 #define _BSD_SOURCE 3 #include <wchar.h> 4 5 #include <assert.h> 6 #include <stdarg.h> 7 #include <stdio.h> 8 #include <locale.h> 9 #include <stdlib.h> 10 #include <string.h> 11 #include <sys/ioctl.h> 12 #include <termios.h> 13 #include <unistd.h> 14 15 #include "utf8.h" 16 #include "ui.h" 17 18 #define CURPOS "\33[%d;%dH" 19 //#define CLEARLEFT "\33[1K" 20 #define CLEARRIGHT "\33[0K" 21 #define CURHIDE "\33[?25l" 22 #define CURSHOW "\33[?25h" 23 #define ERASECHAR "\33[1X" 24 25 #define IS_RIS(c) ((c) >= 0x1F1E6 && (c) <= 0x1F1FF) 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 39 /* TODO: edo.h? */ 40 extern void *ecalloc(size_t nmemb, size_t size); 41 extern void *erealloc(void *p, size_t size); 42 extern void die(const char *fmt, ...); 43 extern char *cell_get_text(Cell *cell, char *pool_base); 44 45 /* function declarations */ 46 void ab_free(Abuf *ab); 47 void ab_ensure_cap(Abuf *ab, size_t addlen); 48 void ab_write(Abuf *ab, const char *s, size_t len); 49 int ab_printf(Abuf *ab, const char *fmt, ...); 50 void ab_flush(Abuf *ab); 51 void tui_frame_start(void); 52 void tui_frame_flush(void); 53 int tui_text_width(char *s, int len, int x); 54 int tui_text_len(char *s, int len); 55 void tui_get_window_size(int *rows, int *cols); 56 void tui_exit(void); 57 void tui_move_cursor(int x, int y); 58 void tui_draw_line(UI *ui, int x, int y, Cell *cells, int screen_cols); 59 void tui_draw_symbol(int r, int c, Symbol sym); 60 void tui_init(void); 61 62 /* function implementations */ 63 void 64 ab_free(Abuf *ab) { 65 if(!ab->buf) 66 return; 67 free(ab->buf); 68 ab->buf = NULL; 69 ab->len = 0; 70 ab->cap = 0; 71 } 72 73 void 74 ab_ensure_cap(Abuf *ab, size_t addlen) { 75 size_t newlen = ab->len + addlen; 76 77 if(newlen <= ab->cap) 78 return; 79 while(newlen > ab->cap) 80 ab->cap = ab->cap ? ab->cap * 2 : 8; 81 ab->buf = erealloc(ab->buf, ab->cap); 82 } 83 84 void 85 ab_write(Abuf *ab, const char *s, size_t len) { 86 ab_ensure_cap(&frame, len); 87 memcpy(ab->buf + ab->len, s, len); 88 ab->len += len; 89 } 90 91 int 92 ab_printf(Abuf *ab, const char *fmt, ...) { 93 va_list ap; 94 int len; 95 96 va_start(ap, fmt); 97 len = vsnprintf(NULL, 0, fmt, ap); 98 assert(len >= 0); 99 va_end(ap); 100 101 ab_ensure_cap(ab, len + 1); 102 103 va_start(ap, fmt); 104 vsnprintf(ab->buf + ab->len, len + 1, fmt, ap); 105 va_end(ap); 106 107 ab->len += len; 108 return len; 109 } 110 111 void 112 ab_flush(Abuf *ab) { 113 write(STDOUT_FILENO, ab->buf, ab->len); 114 ab_free(ab); 115 } 116 117 void 118 tui_frame_start(void) { 119 ab_printf(&frame, CURHIDE); 120 } 121 122 void 123 tui_frame_flush(void) { 124 ab_printf(&frame, CURSHOW); 125 ab_flush(&frame); 126 } 127 128 int 129 tui_text_width(char *s, int len, int x) { 130 int tabstop = 8; 131 int w = 0, i; 132 int step, wc; 133 unsigned int cp; 134 135 for(i = 0; i < len; i += step) { 136 step = utf8_decode(s + i, len - i, &cp); 137 if(cp == '\t') { 138 w += tabstop - x % tabstop; 139 continue; 140 } 141 142 wc = -1; 143 if(compat_mode) { 144 if(cp == 0x200D) { 145 w += 6; 146 continue; 147 } 148 if(IS_RIS(cp)) wc = 2; 149 } else { 150 /* force 2 cells width for emoji followed by VS16 */ 151 int nxi = i + step; 152 if(nxi < len) { 153 unsigned int nxcp; 154 utf8_decode(s + nxi, len - nxi, &nxcp); 155 156 if(nxcp == 0xFE0F && ((cp >= 0x203C && cp <= 0x3299) || cp >= 0x1F000)) 157 wc = 2; 158 } 159 } 160 161 if(wc == -1) wc = wcwidth(cp); 162 if(wc > 0) w += wc; 163 } 164 return w; 165 } 166 167 int 168 tui_text_len(char *s, int len) { 169 return compat_mode ? utf8_len_compat(s, len) : utf8_len(s, len); 170 } 171 172 void 173 tui_get_window_size(int *rows, int *cols) { 174 *rows = ws.ws_row; 175 *cols = ws.ws_col; 176 } 177 178 void 179 tui_exit(void) { 180 tcsetattr(0, TCSANOW, &origti); 181 printf(CURPOS CLEARRIGHT, ws.ws_row, 0); 182 } 183 184 void 185 tui_move_cursor(int c, int r) { 186 int x = c + 1, y = r + 1; /* TERM coords are 1-based */ 187 ab_printf(&frame, CURPOS, y, x); 188 } 189 190 void 191 tui_draw_line_compat(UI *ui, int x, int y, Cell *cells, int count) { 192 char *txt; 193 unsigned int cp = 0; 194 int was_emoji = 0; 195 int i; 196 197 tui_move_cursor(x, y); 198 for(i = 0; i < count; i++) { 199 x += cells[i].width; 200 txt = cell_get_text(cells + i, ui->pool.data); 201 202 int cw = cells[i].width; 203 204 /* TODO: temp code for testing, we'll se how to deal with this later */ 205 if(txt[0] == '\t') { 206 ab_printf(&frame, "%*s", cells[i].width, " "); 207 } else { 208 int o = 0; 209 210 while(o < cells[i].len) { 211 int step = utf8_decode(txt + o, cells[i].len - o, &cp); 212 213 if(cp == 0x200D) { 214 ab_write(&frame, "<200d>", cells[i].width); 215 } else { 216 int w = wcwidth(cp); 217 if(w > 0) cw = w; 218 219 if(was_emoji) { 220 /* ERASECHAR to clear eventual garbage state */ 221 const char t[] = "\x1b[48;5;232m"ERASECHAR; 222 ab_write(&frame, t, sizeof t - 1); 223 } 224 225 ab_write(&frame, txt + o, step); 226 227 if(was_emoji) { 228 const char t[] = "\x1b[0m"; 229 ab_write(&frame, t, sizeof t - 1); 230 } 231 } 232 o += step; 233 } 234 } 235 236 /* pad if needed */ 237 if(cw < cells[i].width) { 238 tui_move_cursor(x - cells[i].width + cw, y); 239 240 const char t[] = "\x1b[48;5;233m"; 241 ab_write(&frame, t, sizeof t - 1); 242 243 while(cw++ < cells[i].width) ab_write(&frame, " ", 1); 244 245 const char t2[] = "\x1b[0m"; 246 ab_write(&frame, t2, sizeof t2 - 1); 247 } 248 249 was_emoji = cells[i].len > 1 || IS_RIS(cp); 250 if(was_emoji) tui_move_cursor(x, y); 251 } 252 253 /* TODO: this only happens with ambi characters? */ 254 if(was_emoji) { 255 const char t[] = "\x1b[48;5;232m \x1b[0m"; 256 ab_write(&frame, t, sizeof t - 1); 257 } 258 259 ab_write(&frame, CLEARRIGHT, strlen(CLEARRIGHT)); 260 } 261 262 263 void 264 tui_draw_line(UI *ui, int x, int y, Cell *cells, int count) { 265 assert(x < ws.ws_col && y < ws.ws_row); 266 267 if(compat_mode) { 268 tui_draw_line_compat(ui, x, y, cells, count); 269 return; 270 } 271 272 char *txt; 273 int i; 274 275 tui_move_cursor(x, y); 276 for(i = 0; i < count; i++) { 277 x += cells[i].width; 278 txt = cell_get_text(cells + i, ui->pool.data); 279 280 /* TODO: temp code for testing, we'll se how to deal with this later */ 281 if(txt[0] == '\t') 282 ab_printf(&frame, "%*s", cells[i].width, " "); 283 else 284 ab_write(&frame, txt, cells[i].len); 285 } 286 ab_write(&frame, CLEARRIGHT, strlen(CLEARRIGHT)); 287 } 288 289 void 290 tui_draw_symbol(int c, int r, Symbol sym) { 291 int symch; 292 293 switch(sym) { 294 case SYM_EMPTYLINE: symch = '~'; break; 295 default: symch = '?'; break; 296 } 297 298 tui_move_cursor(c, r); 299 ab_printf(&frame, "%c" CLEARRIGHT, symch); 300 } 301 302 void 303 tui_init(void) { 304 struct termios ti; 305 306 setlocale(LC_CTYPE, ""); 307 tcgetattr(0, &origti); 308 cfmakeraw(&ti); 309 ti.c_iflag |= ICRNL; 310 ti.c_cc[VMIN] = 1; 311 ti.c_cc[VTIME] = 0; 312 tcsetattr(0, TCSAFLUSH, &ti); 313 setbuf(stdout, NULL); 314 ioctl(0, TIOCGWINSZ, &ws); 315 316 /* check for compat mode */ 317 /* TODO: auto-detect */ 318 compat_mode = 1; 319 } 320 321 int 322 tui_read_byte(void) { 323 char c; 324 325 if(read(STDIN_FILENO, &c, 1) == 1) 326 return c; 327 return -1; 328 } 329 330 Event 331 tui_next_event(void) { 332 Event ev; 333 int c = tui_read_byte(); 334 335 if(c == 0x1B) { 336 ev.type = EV_UKN; 337 return ev; 338 } 339 340 ev.type = EV_KEY; 341 ev.key = c; 342 return ev; 343 } 344 345 UI ui_tui = { 346 .name = "TUI", 347 .init = tui_init, 348 .exit = tui_exit, 349 .frame_start = tui_frame_start, 350 .frame_flush = tui_frame_flush, 351 .text_width = tui_text_width, 352 .text_len = tui_text_len, 353 .move_cursor = tui_move_cursor, 354 .draw_line = tui_draw_line, 355 .draw_symbol = tui_draw_symbol, 356 .get_window_size = tui_get_window_size, 357 .next_event = tui_next_event 358 };