edo

Experimental text editor.
Log | Files | Refs | LICENSE

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 };