vtgl

terminal emulator implemented in OpenGL
git clone anongit@rnpnr.xyz:vtgl.git
Log | Files | Refs | Feed | LICENSE

terminal.c (37782B)


      1 /* See LICENSE for copyright details */
      2 
      3 /* TODO: build own wide char tables */
      4 i32 wcwidth(u32 cp);
      5 
      6 static const u8 utf8overhangmask[32] = {
      7 	255, 255, 255, 255,  255, 255, 255, 255,
      8 	255, 255, 255, 255,  255, 255, 255, 255,
      9 	0, 0, 0, 0,  0, 0, 0, 0,
     10 	0, 0, 0, 0,  0, 0, 0, 0
     11 };
     12 
     13 #define SPLIT_LONG 4096L
     14 
     15 static iv2
     16 initialize_framebuffer(Framebuffer *fb, iv2 term_size)
     17 {
     18 	size cell_memory_size = sizeof(Cell) * term_size.h * term_size.w;
     19 	size rows_memory_size = sizeof(Row)  * term_size.h;
     20 
     21 	/* NOTE: make sure cell memory size is a multiple of pointer size */
     22 	cell_memory_size += (sizeof(void *) - cell_memory_size % sizeof(void *));
     23 
     24 	if (cell_memory_size + rows_memory_size >= fb->backing_store.size)
     25 		term_size.h = (fb->backing_store.size - cell_memory_size) / sizeof(Row);
     26 
     27 	fb->cells = fb->backing_store.memory;
     28 	fb->rows  = fb->backing_store.memory + cell_memory_size;
     29 
     30 	for (i32 i = 0; i < term_size.h; i++)
     31 		fb->rows[i] = fb->cells + i * term_size.w;
     32 
     33 	return term_size;
     34 }
     35 
     36 static Range
     37 get_word_around_cell(Term *t, iv2 cell)
     38 {
     39 	Range result = {.start = cell, .end = cell};
     40 	Cell *row    = t->views[t->view_idx].fb.rows[cell.y];
     41 
     42 	b32 isspace = ISSPACE(row[cell.x].cp) && !(row[cell.x].bg & ATTR_WDUMMY);
     43 	while (result.start.x > 0) {
     44 		Cell *nc = row + result.start.x - 1;
     45 		if (!(nc->bg & ATTR_WDUMMY) && isspace != ISSPACE(nc->cp))
     46 			break;
     47 		result.start.x--;
     48 	}
     49 	while (result.end.x < t->size.w - 1) {
     50 		Cell *nc = row + result.end.x + 1;
     51 		if (!(nc->bg & ATTR_WDUMMY) && isspace != ISSPACE(nc->cp))
     52 			break;
     53 		result.end.x++;
     54 	}
     55 
     56 	return result;
     57 }
     58 
     59 static Range
     60 get_char_around_cell(Term *t, iv2 cell)
     61 {
     62 	Range result = {.start = cell, .end = cell};
     63 
     64 	/* NOTE: if the cell is WDUMMY move back until we find the starting
     65 	 * WIDE cell. then set the range end to the last WDUMMY cell */
     66 	Cell *row = t->views[t->view_idx].fb.rows[cell.y];
     67 	if (row[cell.x].bg & ATTR_WDUMMY) {
     68 		ASSERT(cell.x - 1 >= 0);
     69 		cell.x--;
     70 		ASSERT(row[cell.x].bg & ATTR_WIDE);
     71 		result.start = cell;
     72 	} else if (row[cell.x].bg & ATTR_WIDE) {
     73 		ASSERT(cell.x + 1 < t->size.w);
     74 		cell.x++;
     75 		ASSERT(row[cell.x].bg & ATTR_WDUMMY);
     76 		result.end = cell;
     77 	}
     78 
     79 	return result;
     80 }
     81 
     82 static s8
     83 consume(s8 raw, size count)
     84 {
     85 	raw.data += count;
     86 	raw.len  -= count;
     87 	return raw;
     88 }
     89 
     90 static u8
     91 peek(s8 raw, size i)
     92 {
     93 	ASSERT(i < raw.len);
     94 	return raw.data[i];
     95 }
     96 
     97 static u32
     98 get_utf8(s8 *raw)
     99 {
    100 	u32 state = 0, cp;
    101 	size off = 0;
    102 	while (off < raw->len) {
    103 		if (!utf8_decode(&state, &cp, raw->data[off++])) {
    104 			*raw = consume(*raw, off);
    105 			return cp;
    106 		}
    107 	}
    108 	return (u32)-1;
    109 }
    110 
    111 static u32
    112 get_ascii(s8 *raw)
    113 {
    114 	ASSERT(raw->len > 0);
    115 	u32 result = raw->data[0];
    116 	*raw = consume(*raw, 1);
    117 	return result;
    118 }
    119 
    120 static size
    121 line_length(Line *l)
    122 {
    123 	ASSERT(l->start <= l->end);
    124 	return l->end - l->start;
    125 }
    126 
    127 static s8
    128 line_to_s8(Line *l, RingBuf *rb)
    129 {
    130 	ASSERT(l->start <= l->end);
    131 
    132 	s8 result = {.len = l->end - l->start, .data = l->start};
    133 	return result;
    134 }
    135 
    136 static void
    137 init_line(Line *l, u8 *position, CellStyle cursor_state)
    138 {
    139 	l->start        = position;
    140 	l->end          = position;
    141 	l->has_unicode  = 0;
    142 	l->cursor_state = cursor_state;
    143 }
    144 
    145 static size
    146 feed_line(LineBuf *lb, u8 *position, CellStyle cursor_state)
    147 {
    148 	size result = 0;
    149 	if (lb->buf[lb->widx].start != position) {
    150 		lb->buf[lb->widx++].end = position;
    151 		lb->widx    = lb->widx >= lb->cap ? 0 : lb->widx;
    152 		lb->filled += lb->filled <= lb->widx;
    153 		init_line(lb->buf + lb->widx, position, cursor_state);
    154 		result = 1;
    155 	}
    156 	return result;
    157 }
    158 
    159 static void
    160 selection_clear(Selection *s)
    161 {
    162 	s->range.end = INVALID_RANGE_END;
    163 	s->state     = SS_NONE;
    164 }
    165 
    166 static void
    167 selection_scroll(Term *t, u32 origin, i32 n)
    168 {
    169 	Selection *s = &t->selection;
    170 	if (!is_valid_range(s->range))
    171 		return;
    172 
    173 	b32 start_in_bounds = BETWEEN(s->range.start.y, origin, t->bot);
    174 	b32 end_in_bounds   = BETWEEN(s->range.end.y,   origin, t->bot);
    175 	if (start_in_bounds != end_in_bounds) {
    176 		selection_clear(s);
    177 	} else if (start_in_bounds) {
    178 		s->range.start.y += n;
    179 		s->range.end.y   += n;
    180 		if (s->range.start.y > t->bot || s->range.start.y < t->top ||
    181 		    s->range.end.y   > t->bot || s->range.end.y   < t->top)
    182 			selection_clear(s);
    183 	}
    184 }
    185 
    186 static b32
    187 is_selected(Selection *s, i32 x, i32 y)
    188 {
    189 	if (!is_valid_range(s->range))
    190 		return 0;
    191 
    192 	b32 result = BETWEEN(y, s->range.start.y, s->range.end.y) &&
    193 	             (y != s->range.start.y || x >= s->range.start.x) &&
    194 	             (y != s->range.end.y   || x <= s->range.end.x);
    195 	return result;
    196 }
    197 
    198 static b32
    199 selection_intersects_region(Selection *s, uv2 tl, uv2 br)
    200 {
    201 	/* TODO: maybe this can be further simplified (eg. with a k-map) */
    202 	Range r = s->range;
    203 	b32 valid   = is_valid_range(r);
    204 	b32 whole   = r.start.y <  tl.y && r.end.y   >  br.y;
    205 	b32 start_x = r.start.x >= tl.x && r.start.x <= br.x;
    206 	b32 start_y = r.start.y >= tl.y && r.start.y <= br.y;
    207 	b32 end_x   = r.end.x   >= tl.x && r.end.x   <= br.x;
    208 	b32 end_y   = r.end.y   >= tl.y && r.end.y   <= br.y;
    209 	b32 result  = valid && (whole || (start_y && start_x) || (end_y && end_x));
    210 	return result;
    211 }
    212 
    213 static void
    214 fb_clear_region(Term *t, u32 r1, u32 r2, u32 c1, u32 c2)
    215 {
    216 	BEGIN_TIMED_BLOCK();
    217 	u32 tmp;
    218 	if (r1 > r2) {
    219 		tmp = r1;
    220 		r1  = r2;
    221 		r2  = tmp;
    222 	}
    223 	if (c1 > c2) {
    224 		tmp = c1;
    225 		c1  = c2;
    226 		c2  = tmp;
    227 	}
    228 	CLAMP(c1, 0, t->size.w - 1);
    229 	CLAMP(c2, 0, t->size.w - 1);
    230 	CLAMP(r1, 0, t->size.h - 1);
    231 	CLAMP(r2, 0, t->size.h - 1);
    232 
    233 	uv2 top_left     = {.x = c1, .y = r1};
    234 	uv2 bottom_right = {.x = c2, .y = r2};
    235 	if (selection_intersects_region(&t->selection, top_left, bottom_right))
    236 		selection_clear(&t->selection);
    237 
    238 	Row *rows = t->views[t->view_idx].fb.rows;
    239 	CellStyle cursor_style = t->cursor.style;
    240 	u32 fg = SHADER_PACK_FG(cursor_style.fg.rgba, cursor_style.attr);
    241 	u32 bg = SHADER_PACK_BG(cursor_style.bg.rgba, cursor_style.attr);
    242 	for (u32 r = top_left.y; r <= bottom_right.y; r++) {
    243 		for (u32 c = top_left.x; c <= bottom_right.x; c++) {
    244 			rows[r][c].fg = fg;
    245 			rows[r][c].bg = bg;
    246 			rows[r][c].cp = ' ';
    247 		}
    248 	}
    249 
    250 	END_TIMED_BLOCK();
    251 }
    252 
    253 static void
    254 fb_scroll_down(Term *t, u32 top, u32 n)
    255 {
    256 	BEGIN_TIMED_BLOCK();
    257 	if (!BETWEEN(top, t->top, t->bot))
    258 		goto end;
    259 
    260 	TermView *tv = t->views + t->view_idx;
    261 	CLAMP(n, 0, t->bot - top + 1);
    262 
    263 	fb_clear_region(t, t->bot - n + 1, t->bot, 0, t->size.w);
    264 	for (u32 i = t->bot; i >= top + n; i--) {
    265 		Row tmp = tv->fb.rows[i];
    266 		tv->fb.rows[i]     = tv->fb.rows[i - n];
    267 		tv->fb.rows[i - n] = tmp;
    268 	}
    269 	selection_scroll(t, top, n);
    270 end:
    271 	END_TIMED_BLOCK();
    272 }
    273 
    274 static void
    275 fb_scroll_up(Term *t, u32 top, u32 n)
    276 {
    277 	BEGIN_TIMED_BLOCK();
    278 	if (!BETWEEN(top, t->top, t->bot))
    279 		goto end;
    280 
    281 	TermView *tv = t->views + t->view_idx;
    282 	CLAMP(n, 0, t->bot - top + 1);
    283 
    284 #if 0
    285 	size cell_count = (t->bot - n + 1) * t->size.w;
    286 	ASSERT(cell_count < tv->fb.cells_count);
    287 	mem_move(tv->fb.cells + t->size.w * n, tv->fb.cells, cell_count * sizeof(Cell));
    288 	fb_clear_region(t, t->bot - n + 1, t->bot, 0, t->size.w);
    289 #else
    290 	fb_clear_region(t, top, top + n - 1, 0, t->size.w);
    291 	for (u32 i = top; i <= t->bot - n; i++) {
    292 		Row tmp = tv->fb.rows[i];
    293 		tv->fb.rows[i]     = tv->fb.rows[i + n];
    294 		tv->fb.rows[i + n] = tmp;
    295 	}
    296 #endif
    297 	selection_scroll(t, top, -n);
    298 end:
    299 	END_TIMED_BLOCK();
    300 }
    301 
    302 static void
    303 swap_screen(Term *t)
    304 {
    305 	t->mode.term ^= TM_ALTSCREEN;
    306 	t->view_idx   = !!(t->mode.term & TM_ALTSCREEN);
    307 }
    308 
    309 static void
    310 cursor_reset(Term *t)
    311 {
    312 	//(Colour){.rgba = 0x1e9e33ff};
    313 	t->cursor.style.fg   = g_colours.data[g_colours.fgidx];
    314 	t->cursor.style.bg   = g_colours.data[g_colours.bgidx];
    315 	t->cursor.style.attr = ATTR_NULL;
    316 }
    317 
    318 static void
    319 cursor_move_to(Term *t, i32 row, i32 col)
    320 {
    321 	i32 minr = 0, maxr = t->size.h - 1;
    322 	if (t->cursor.state & CURSOR_ORIGIN) {
    323 		minr = t->top;
    324 		maxr = t->bot;
    325 	}
    326 	t->cursor.pos.y  = CLAMP(row, minr, maxr);
    327 	t->cursor.pos.x  = CLAMP(col, 0, t->size.w - 1);
    328 	t->cursor.state &= ~CURSOR_WRAP_NEXT;
    329 }
    330 
    331 static void
    332 cursor_move_abs_to(Term *t, i32 row, i32 col)
    333 {
    334 	if (t->cursor.state & CURSOR_ORIGIN)
    335 		row += t->top;
    336 	cursor_move_to(t, row, col);
    337 }
    338 
    339 static void
    340 cursor_alt(Term *t, b32 save)
    341 {
    342 	i32 mode = t->view_idx;
    343 	if (save) {
    344 		t->saved_cursors[mode] = t->cursor;
    345 	} else {
    346 		t->cursor = t->saved_cursors[mode];
    347 		cursor_move_to(t, t->cursor.pos.y, t->cursor.pos.x);
    348 	}
    349 }
    350 
    351 /* NOTE: advance the cursor by <n> cells; handles reverse movement */
    352 static void
    353 cursor_step_column(Term *t, i32 n)
    354 {
    355 	i32 col = t->cursor.pos.x + n;
    356 	i32 row = t->cursor.pos.y;
    357 	if (col >= t->size.w) {
    358 		row++;
    359 		col = 0;
    360 		if (row >= t->size.h)
    361 			fb_scroll_up(t, t->top, 1);
    362 	}
    363 	cursor_move_to(t, row, col);
    364 }
    365 
    366 /* NOTE: steps the cursor without causing a scroll */
    367 static void
    368 cursor_step_raw(Term *t, i32 step, i32 rows, i32 cols)
    369 {
    370 	rows *= step;
    371 	cols *= step;
    372 	cursor_move_to(t, t->cursor.pos.y + rows, t->cursor.pos.x + cols);
    373 }
    374 
    375 static i32
    376 next_tab_position(Term *t, b32 backwards)
    377 {
    378 	static_assert(ARRAY_COUNT(t->tabs) == 8 * sizeof(*t->tabs),
    379 	              "Term.tabs must be same length as the bitwidth of it's type");
    380 	u32 col  = t->cursor.pos.x;
    381 	u32 idx  = col / ARRAY_COUNT(t->tabs);
    382 	u32 bit  = col % ARRAY_COUNT(t->tabs);
    383 	u32 mask = safe_left_shift(1, bit - backwards) - 1;
    384 
    385 	u32 result = 32 * idx;
    386 	if (backwards) {
    387 		u32 zeroes = _lzcnt_u32(t->tabs[idx--] & mask);
    388 		while (idx < ARRAY_COUNT(t->tabs) && zeroes == 32)
    389 			zeroes = _lzcnt_u32(t->tabs[idx--]);
    390 		result = 32 * (idx + 1) + 32 - zeroes;
    391 	} else {
    392 		u32 zeroes = _tzcnt_u32(t->tabs[idx++] & ~mask);
    393 		while (idx < ARRAY_COUNT(t->tabs) && zeroes == 32)
    394 			zeroes = _tzcnt_u32(t->tabs[idx++]);
    395 		result = 32 * (idx - 1) + zeroes + 1;
    396 	}
    397 	ASSERT(result < t->size.w);
    398 
    399 	return result;
    400 }
    401 
    402 static void
    403 term_tab_col(Term *t, u32 col, b32 set)
    404 {
    405 	ASSERT(col < t->size.w);
    406 	u32 idx  = (col - 1) / ARRAY_COUNT(t->tabs);
    407 	u32 bit  = (col - 1) % ARRAY_COUNT(t->tabs);
    408 	u32 mask = 1u;
    409 	if (bit) mask = safe_left_shift(1, bit);
    410 	if (set) t->tabs[idx] |=  mask;
    411 	else     t->tabs[idx] &= ~mask;
    412 }
    413 
    414 static void
    415 term_reset(Term *t)
    416 {
    417 	i32 mode = t->mode.term & TM_ALTSCREEN;
    418 	t->cursor.state = CURSOR_NORMAL;
    419 	for (u32 i = 0; i < ARRAY_COUNT(t->saved_cursors); i++) {
    420 		cursor_reset(t);
    421 		t->cursor.charset_index = 0;
    422 		for (u32 i = 0; i < ARRAY_COUNT(t->cursor.charsets); i++)
    423 			t->cursor.charsets[i] = CS_USA;
    424 		cursor_move_to(t, 0, 0);
    425 		cursor_alt(t, 1);
    426 		swap_screen(t);
    427 		fb_clear_region(t, 0, t->size.h, 0, t->size.w);
    428 	}
    429 	for (u32 i = 0; i < ARRAY_COUNT(t->tabs); i++)
    430 		t->tabs[i] = 0;
    431 	for (u32 i = g_tabstop; i < t->size.w; i += g_tabstop)
    432 		term_tab_col(t, i, 1);
    433 
    434 	t->top  = 0;
    435 	t->bot  = t->size.h - 1;
    436 	/* TODO: why is term_reset() being called when we are in the altscreen */
    437 	t->mode.term = mode|TM_AUTO_WRAP|TM_UTF8;
    438 }
    439 
    440 static void
    441 dump_csi(CSI *csi, Stream *err)
    442 {
    443 	stream_push_s8(err, s8("raw: ESC["));
    444 	for (size i = 0; i < csi->raw.len; i++) {
    445 		u8 c = csi->raw.data[i];
    446 		if (ISPRINT(c)) {
    447 			stream_push_byte(err, csi->raw.data[i]);
    448 		} else if (c == '\n') {
    449 			stream_push_s8(err, s8("\\n"));
    450 		} else if (c == '\r') {
    451 			stream_push_s8(err, s8("\\r"));
    452 		} else {
    453 			stream_push_s8(err, s8("\\x"));
    454 			stream_push_hex_u64(err, c);
    455 		}
    456 	}
    457 	stream_push_s8(err, s8("\n\tparsed = { .priv = "));
    458 	stream_push_u64(err, csi->priv);
    459 	stream_push_s8(err, s8(" .mode = "));
    460 	if (ISPRINT(csi->mode)) {
    461 		stream_push_byte(err, csi->mode);
    462 	} else {
    463 		stream_push_s8(err, s8("\\x"));
    464 		stream_push_hex_u64(err, csi->mode);
    465 	}
    466 	stream_push_s8(err, s8(", .argc = "));
    467 	stream_push_u64(err, csi->argc);
    468 	stream_push_s8(err, s8(", .argv = {"));
    469 	for (i32 i = 0; i < csi->argc; i++) {
    470 		stream_push_byte(err, ' ');
    471 		stream_push_i64(err, csi->argv[i]);
    472 	}
    473 
    474 	stream_push_s8(err, s8(" } }\n"));
    475 	os_write_err_msg(stream_to_s8(err));
    476 	err->widx = 0;
    477 }
    478 
    479 /* ED/DECSED: Erase in Display */
    480 static void
    481 erase_in_display(Term *t, CSI *csi)
    482 {
    483 	iv2 cpos = t->cursor.pos;
    484 	switch (csi->argv[0]) {
    485 	case 0: /* Erase Below (default) */
    486 		fb_clear_region(t, cpos.y, cpos.y, cpos.x, t->size.w);
    487 		if (cpos.y < t->size.h - 1)
    488 			fb_clear_region(t, cpos.y + 1, t->size.h, 0, t->size.w);
    489 		break;
    490 	case 1: /* Erase Above */
    491 		if (cpos.y > 0)
    492 			fb_clear_region(t, 0, cpos.y - 1, 0, t->size.w);
    493 		fb_clear_region(t, cpos.y, cpos.y, 0, cpos.x);
    494 		break;
    495 	case 2: /* Erase All */
    496 		fb_clear_region(t, 0, t->size.h, 0, t->size.w);
    497 		break;
    498 	case 3: /* Erase Saved Lines (xterm) */
    499 		/* NOTE: ignored; we don't save lines in the way xterm does */
    500 		break;
    501 	default: ASSERT(0);
    502 	}
    503 }
    504 
    505 /* EL/DECSEL: Erase in Line */
    506 static void
    507 erase_in_line(Term *t, CSI *csi)
    508 {
    509 	iv2 cpos = t->cursor.pos;
    510 	switch (csi->argv[0]) {
    511 	case 0: /* Erase to Right */
    512 		fb_clear_region(t, cpos.y, cpos.y, cpos.x, t->size.w);
    513 		break;
    514 	case 1: /* Erase to Left */
    515 		fb_clear_region(t, cpos.y, cpos.y, 0, cpos.x);
    516 		break;
    517 	case 2: /* Erase All */
    518 		fb_clear_region(t, cpos.y, cpos.y, 0, t->size.w);
    519 		break;
    520 	default: ASSERT(0);
    521 	}
    522 }
    523 
    524 /* IL: Insert <count> blank lines */
    525 static void
    526 insert_blank_lines(Term *t, i32 count)
    527 {
    528 	fb_scroll_down(t, t->cursor.pos.y, count);
    529 }
    530 
    531 /* DL: Erase <count> lines */
    532 static void
    533 erase_lines(Term *t, i32 count)
    534 {
    535 	fb_scroll_up(t, t->cursor.pos.y, count);
    536 }
    537 
    538 /* DCH: Delete Characters (NOTE: DCH is technically different but we are ignoring that) */
    539 /* ECH: Erase Characters  */
    540 static void
    541 erase_characters(Term *t, i32 count)
    542 {
    543 	iv2 cpos  = t->cursor.pos;
    544 	fb_clear_region(t, cpos.y, cpos.y, cpos.x, cpos.x + count - 1);
    545 }
    546 
    547 /* TBC: Tabulation Clear */
    548 static void
    549 clear_term_tab(Term *t, i32 arg)
    550 {
    551 	/* TODO: case 1, 2? */
    552 	switch(arg) {
    553 	case 0:
    554 		term_tab_col(t, t->cursor.pos.x, 0);
    555 		break;
    556 	case 3:
    557 		for (u32 i = 0; i < ARRAY_COUNT(t->tabs); i++)
    558 			t->tabs[i] = 0;
    559 		break;
    560 	default:
    561 		stream_push_s8(&t->error_stream, s8("clear_term_tab: unhandled arg: "));
    562 		stream_push_i64(&t->error_stream, arg);
    563 		stream_push_byte(&t->error_stream, '\n');
    564 		os_write_err_msg(stream_to_s8(&t->error_stream));
    565 		t->error_stream.widx = 0;
    566 	}
    567 }
    568 
    569 /* SM/DECSET: Set Mode & RM/DECRST Reset Mode */
    570 static void
    571 set_mode(Term *t, CSI *csi, b32 set, ModeState src, ModeState *dest)
    572 {
    573 	BEGIN_TIMED_BLOCK();
    574 	i32 alt = t->view_idx;
    575 
    576 	/* TODO: this whole thing should be a lookup table */
    577 	#define PRIV(a) ((1 << 30) | (a))
    578 	for (i32 i = 0; i < csi->argc; i++) {
    579 		i32 arg = (csi->argv[i]) | ((csi->priv & 1) << 30);
    580 		switch (arg) {
    581 		case 4:          /* IRM: Insert/Replace Mode */
    582 			dest->term &= ~TM_REPLACE;
    583 			if (set) dest->term |= (src.term & TM_REPLACE);
    584 			break;
    585 		case 20:         /* LNM: Linefeed Assumes Carriage Return */
    586 			dest->term &= ~TM_CRLF;
    587 			if (set) dest->term |= (src.term & TM_CRLF);
    588 			break;
    589 		case PRIV(1):    /* DECCKM: use application cursor keys */
    590 			dest->win &= ~WM_APPCURSOR;
    591 			if (set) dest->win |= (src.win & WM_APPCURSOR);
    592 			break;
    593 		case PRIV(5):    /* DECSCNM: reverse/normal video mode */
    594 			dest->win &= ~WM_REVERSE;
    595 			if (set) dest->win |= (src.win & WM_REVERSE);
    596 			break;
    597 		case PRIV(6):    /* DECOM: Cursor Origin Mode */
    598 			if (set) t->cursor.state |=  CURSOR_ORIGIN;
    599 			else     t->cursor.state &= ~CURSOR_ORIGIN;
    600 			cursor_move_abs_to(t, 0, 0);
    601 			break;
    602 		case PRIV(7):    /* DECAWM: Auto-Wrap Mode */
    603 			dest->term &= ~TM_AUTO_WRAP;
    604 			if (set) dest->term |= (src.term & TM_AUTO_WRAP);
    605 			break;
    606 		case PRIV(1000): /* xterm: report mouse button presses */
    607 			dest->win &= ~WM_MOUSE_MASK;
    608 			if (set) dest->win |= (src.win & WM_MOUSE_BTN);
    609 			break;
    610 		case PRIV(1002): /* xterm: cell motion tracking */
    611 			dest->win &= ~WM_MOUSE_MASK;
    612 			if (set) dest->win |= (src.win & WM_MOUSE_TRK);
    613 			break;
    614 		case PRIV(1006): /* xterm: SGR mouse mode */
    615 			dest->win &= ~WM_MOUSE_SGR;
    616 			if (set) dest->win |= (src.win & WM_MOUSE_SGR);
    617 			break;
    618 		case PRIV(3):    /* DECCOLM: 132/80 Column Mode */
    619 		case PRIV(4):    /* DECSCLM: Fast/Slow Scroll */
    620 		case PRIV(8):    /* DECARM: Auto-Repeat Keys */
    621 		case PRIV(12):   /* AT&T 610: Start blinking cursor */
    622 		case PRIV(40):   /* xterm: (dis)allow 132/80 Column Mode */
    623 		case PRIV(45):   /* XTREVWRAP: xterm reverse wrap around */
    624 		case PRIV(1001): /* xterm: (broken) mouse highlight tracking; requires
    625 		                  * the requesting program to be competely functional or will
    626 		                  * hang the terminal by design */
    627 		case PRIV(1015): /* urxvt: (broken) mouse mode */
    628 			/* IGNORED */
    629 			break;
    630 		case PRIV(25):   /* DECTCEM: Show/Hide Cursor */
    631 			dest->win &= ~WM_HIDECURSOR;
    632 			if (!set) dest->win |= (src.win & WM_HIDECURSOR);
    633 			break;
    634 		case PRIV(1034): /* xterm: enable 8-bit input mode */
    635 			dest->win &= ~WM_8BIT;
    636 			if (set) dest->win |= (src.win & WM_8BIT);
    637 			break;
    638 		case PRIV(1049): /* xterm: swap cursor then swap screen */
    639 			cursor_alt(t, set);
    640 		case PRIV(47):   /* xterm: swap screen buffer */
    641 		case PRIV(1047): /* xterm: swap screen buffer */
    642 			if (alt)                  fb_clear_region(t, 0, t->size.h, 0, t->size.w);
    643 			if (set ^ alt)            swap_screen(t);
    644 			if (csi->argv[i] != 1049) break;
    645 			/* FALLTHROUGH */
    646 		case PRIV(1048): /* xterm: swap cursor */
    647 			cursor_alt(t, set);
    648 			break;
    649 		case PRIV(2004): /* xterm: bracketed paste mode */
    650 			dest->win &= ~WM_BRACKPASTE;
    651 			if (set) dest->win |= (src.win & WM_BRACKPASTE);
    652 			break;
    653 		case PRIV(2026): /* synchronized render (eg. wezterm) */
    654 			/* IGNORED: we aren't writing some slow garbage so we won't let some
    655 			 * broken program try and stall us because they think we can't operate
    656 			 * fast enough */
    657 			break;
    658 		default:
    659 			os_write_err_msg(s8("set_mode: unhandled mode: "));
    660 			dump_csi(csi, &t->error_stream);
    661 		}
    662 	}
    663 	#undef PRIV
    664 	END_TIMED_BLOCK();
    665 }
    666 
    667 /* NOTE: adapted from the perl script 256colres.pl in xterm src */
    668 static Colour
    669 indexed_colour(i32 index)
    670 {
    671 	Colour result;
    672 	if (index < 232) {
    673 		/* NOTE: 16-231 are colours off a 6x6x6 RGB cube */
    674 		index -= 16;
    675 		result.r = 40 * ((index / 36));
    676 		result.g = 40 * ((index % 36) / 6);
    677 		result.b = 40 * ((index %  6));
    678 		result.a = 0xFF;
    679 		if (result.r) result.r += 55;
    680 		if (result.g) result.g += 55;
    681 		if (result.b) result.b += 55;
    682 	} else {
    683 		/* NOTE: 232-255 are greyscale ramp */
    684 		u32 k = (10 * (index - 232) + 8) & 0xFF;
    685 		result.r = result.g = result.b = k;
    686 		result.a = 0xFF;
    687 	}
    688 	return result;
    689 }
    690 
    691 static struct conversion_result
    692 direct_colour(i32 *argv, i32 argc, i32 *idx, Stream *err)
    693 {
    694 	struct conversion_result result = {.status = CR_FAILURE};
    695 	switch (argv[*idx + 1]) {
    696 	case 2: /* NOTE: defined RGB colour */
    697 		if (*idx + 4 >= argc) {
    698 			stream_push_s8(err, s8("direct_colour: wrong parameter count: "));
    699 			stream_push_u64(err, argc);
    700 			stream_push_byte(err, '\n');
    701 			break;
    702 		}
    703 		u32 r = (u32)argv[*idx + 2];
    704 		u32 g = (u32)argv[*idx + 3];
    705 		u32 b = (u32)argv[*idx + 4];
    706 		*idx += 4;
    707 		if (r > 0xFF || g > 0xFF || b > 0xFF) {
    708 			stream_push_s8(err, s8("direct_colour: bad rgb colour: ("));
    709 			stream_push_u64(err, r);
    710 			stream_push_s8(err, s8(", "));
    711 			stream_push_u64(err, g);
    712 			stream_push_s8(err, s8(", "));
    713 			stream_push_u64(err, b);
    714 			stream_push_s8(err, s8(")\n"));
    715 			break;
    716 		}
    717 		result.colour = (Colour){.r = r, .g = g, .b = b, .a = 0xFF};
    718 		result.status = CR_SUCCESS;
    719 		break;
    720 	case 5: /* NOTE: indexed colour */
    721 		if (*idx + 2 >= argc) {
    722 			stream_push_s8(err, s8("direct_colour: wrong parameter count: "));
    723 			stream_push_u64(err, argc);
    724 			stream_push_byte(err, '\n');
    725 			break;
    726 		}
    727 		*idx += 2;
    728 		if (!BETWEEN(argv[*idx], 0, 255)) {
    729 			stream_push_s8(err, s8("direct_colour: index parameter out of range: "));
    730 			stream_push_i64(err, argv[*idx]);
    731 			stream_push_byte(err, '\n');
    732 			break;
    733 		}
    734 		if (BETWEEN(argv[*idx], 0, 16))
    735 			result.colour = g_colours.data[argv[*idx]];
    736 		else
    737 			result.colour = indexed_colour(argv[*idx]);
    738 		result.status = CR_SUCCESS;
    739 		break;
    740 	default:
    741 		stream_push_s8(err, s8("direct_colour: unknown argument: "));
    742 		stream_push_i64(err, argv[*idx + 1]);
    743 		stream_push_byte(err, '\n');
    744 	}
    745 
    746 	return result;
    747 }
    748 
    749 /* SGR: Select Graphic Rendition */
    750 static void
    751 set_colours(Term *t, CSI *csi)
    752 {
    753 	BEGIN_TIMED_BLOCK();
    754 	CellStyle *cs = &t->cursor.style;
    755 	struct conversion_result dcr;
    756 	for (i32 i = 0; i < csi->argc; i++) {
    757 		switch (csi->argv[i]) {
    758 		case  0: cursor_reset(t);                     break;
    759 		case  1: cs->attr |= ATTR_BOLD;               break;
    760 		case  2: cs->attr |= ATTR_FAINT;              break;
    761 		case  3: cs->attr |= ATTR_ITALIC;             break;
    762 		case  4: cs->attr |= ATTR_UNDERLINED;         break;
    763 		case  5: cs->attr |= ATTR_BLINK;              break;
    764 		case  7: cs->attr |= ATTR_INVERSE;            break;
    765 		case  8: cs->attr |= ATTR_INVISIBLE;          break;
    766 		case  9: cs->attr |= ATTR_STRUCK;             break;
    767 		case 22: cs->attr &= ~(ATTR_BOLD|ATTR_FAINT); break;
    768 		case 23: cs->attr &= ~ATTR_ITALIC;            break;
    769 		case 24: cs->attr &= ~ATTR_UNDERLINED;        break;
    770 		case 25: cs->attr &= ~ATTR_BLINK;             break;
    771 		case 27: cs->attr &= ~ATTR_INVERSE;           break;
    772 		case 28: cs->attr &= ~ATTR_INVISIBLE;         break;
    773 		case 29: cs->attr &= ~ATTR_STRUCK;            break;
    774 		case 38:
    775 			dcr = direct_colour(csi->argv, csi->argc, &i, &t->error_stream);
    776 			if (dcr.status == CR_SUCCESS) {
    777 				cs->fg = dcr.colour;
    778 			} else {
    779 				stream_push_s8(&t->error_stream, s8("set_colours: "));
    780 				dump_csi(csi, &t->error_stream);
    781 			}
    782 			break;
    783 
    784 		case 39: cs->fg = g_colours.data[g_colours.fgidx]; break;
    785 
    786 		case 48:
    787 			dcr = direct_colour(csi->argv, csi->argc, &i, &t->error_stream);
    788 			if (dcr.status == CR_SUCCESS) {
    789 				cs->bg = dcr.colour;
    790 			} else {
    791 				stream_push_s8(&t->error_stream, s8("set_colours: "));
    792 				dump_csi(csi, &t->error_stream);
    793 			}
    794 			break;
    795 
    796 		case 49: cs->bg = g_colours.data[g_colours.bgidx]; break;
    797 
    798 		default:
    799 			if (BETWEEN(csi->argv[i], 30, 37)) {
    800 				cs->fg = g_colours.data[csi->argv[i] - 30];
    801 			} else if (BETWEEN(csi->argv[i], 40, 47)) {
    802 				cs->bg = g_colours.data[csi->argv[i] - 40];
    803 			} else if (BETWEEN(csi->argv[i], 90, 97)) {
    804 				cs->fg = g_colours.data[csi->argv[i] - 82];
    805 			} else if (BETWEEN(csi->argv[i], 100, 107)) {
    806 				cs->bg = g_colours.data[csi->argv[i] - 92];
    807 			} else {
    808 				stream_push_s8(&t->error_stream, s8("unhandled colour arg: "));
    809 				stream_push_i64(&t->error_stream, csi->argv[i]);
    810 				stream_push_byte(&t->error_stream, '\n');
    811 				dump_csi(csi, &t->error_stream);
    812 			}
    813 		}
    814 	}
    815 	END_TIMED_BLOCK();
    816 }
    817 
    818 static void
    819 set_scrolling_region(Term *t, i32 top, i32 bot)
    820 {
    821 	CLAMP(top, 0, t->size.h - 1);
    822 	CLAMP(bot, 0, t->size.h - 1);
    823 	if (top > bot) {
    824 		i32 tmp = top;
    825 		top  = bot;
    826 		bot  = tmp;
    827 	}
    828 	t->top = top;
    829 	t->bot = bot;
    830 }
    831 
    832 static void
    833 window_manipulation(Term *t, CSI *csi)
    834 {
    835 	switch (csi->argv[0]) {
    836 	case 22: t->platform->get_window_title(&t->saved_title); break;
    837 	case 23: t->platform->set_window_title(&t->saved_title); break;
    838 	default:
    839 		stream_push_s8(&t->error_stream, s8("unhandled xtwinops: "));
    840 		stream_push_i64(&t->error_stream, csi->argv[0]);
    841 		stream_push_byte(&t->error_stream, '\n');
    842 		dump_csi(csi, &t->error_stream);
    843 	}
    844 }
    845 
    846 static void
    847 push_newline(Term *t, b32 move_to_first_col)
    848 {
    849 	i32 row = t->cursor.pos.y;
    850 	if (row == t->bot && t->scroll_offset == 0)
    851 		fb_scroll_up(t, t->top, 1);
    852 	else
    853 		row++;
    854 	cursor_move_to(t, row, move_to_first_col? 0 : t->cursor.pos.x);
    855 }
    856 
    857 static void
    858 push_tab(Term *t, i32 n)
    859 {
    860 	u32 end = ABS(n);
    861 	for (u32 i = 0; i < end; i++)
    862 		cursor_move_to(t, t->cursor.pos.y, next_tab_position(t, n < 0));
    863 }
    864 
    865 static i32
    866 parse_csi(s8 *r, CSI *csi)
    867 {
    868 	BEGIN_TIMED_BLOCK();
    869 	i32 result = 0;
    870 
    871 	if (peek(*r, 0) == '?') {
    872 		csi->priv = 1;
    873 		get_ascii(r);
    874 	}
    875 
    876 	while (r->len) {
    877 		u32 cp = get_ascii(r);
    878 		if (ISCONTROL(cp)) {
    879 			continue;
    880 		} else if (BETWEEN(cp, '0', '9')) {
    881 			csi->argv[csi->argc] *= 10;
    882 			csi->argv[csi->argc] += cp - '0';
    883 			continue;
    884 		}
    885 		csi->argc++;
    886 
    887 		if (cp != ';' || csi->argc == ESC_ARG_SIZ) {
    888 			if (cp == ';') csi->mode = get_ascii(r);
    889 			else           csi->mode = cp;
    890 			goto end;
    891 		}
    892 	}
    893 	/* NOTE: if we fell out of the loop then we ran out of characters */
    894 	result = -1;
    895 end:
    896 	END_TIMED_BLOCK();
    897 
    898 	return result;
    899 }
    900 
    901 static void
    902 handle_csi(Term *t, CSI *csi)
    903 {
    904 	BEGIN_TIMED_BLOCK();
    905 	s8  raw = csi->raw;
    906 	i32 ret = parse_csi(&raw, csi);
    907 	ASSERT(ret != -1);
    908 
    909 	#define ORONE(x) ((x)? (x) : 1)
    910 
    911 	iv2 p = t->cursor.pos;
    912 
    913 	switch (csi->mode) {
    914 	case 'A': cursor_step_raw(t, ORONE(csi->argv[0]), -1,  0);           break;
    915 	case 'B': cursor_step_raw(t, ORONE(csi->argv[0]),  1,  0);           break;
    916 	case 'C': cursor_step_raw(t, ORONE(csi->argv[0]),  0,  1);           break;
    917 	case 'D': cursor_step_raw(t, ORONE(csi->argv[0]),  0, -1);           break;
    918 	case 'E': cursor_move_to(t, p.y + ORONE(csi->argv[0]), 0);           break;
    919 	case 'F': cursor_move_to(t, p.y - ORONE(csi->argv[0]), 0);           break;
    920 	case 'G': cursor_move_to(t, p.y, csi->argv[0] - 1);                  break;
    921 	case 'H': cursor_move_abs_to(t, csi->argv[0] - 1, csi->argv[1] - 1); break;
    922 	case 'J': erase_in_display(t, csi);                                  break;
    923 	case 'K': erase_in_line(t, csi);                                     break;
    924 	case 'L': insert_blank_lines(t, ORONE(csi->argv[0]));                break;
    925 	case 'M': erase_lines(t, ORONE(csi->argv[0]));                       break;
    926 	case 'P': erase_characters(t, ORONE(csi->argv[0]));                  break;
    927 	case 'X': erase_characters(t, ORONE(csi->argv[0]));                  break;
    928 	case 'S': fb_scroll_up(t, t->top, ORONE(csi->argv[0]));              break;
    929 	case 'T': fb_scroll_down(t, t->top, ORONE(csi->argv[0]));            break;
    930 	case 'Z': push_tab(t, -(ORONE(csi->argv[0])));                       break;
    931 	case 'a': cursor_step_raw(t, ORONE(csi->argv[0]),  0,  1);           break;
    932 	case 'd': cursor_move_abs_to(t, csi->argv[0] - 1, p.x);              break;
    933 	case 'e': cursor_step_raw(t, ORONE(csi->argv[0]),  1,  0);           break;
    934 	case 'f': cursor_move_abs_to(t, csi->argv[0] - 1, csi->argv[1] - 1); break;
    935 	case 'g': clear_term_tab(t, csi->argv[0]);                           break;
    936 	case 'h': set_mode(t, csi, 1, MODE_STATE_ALL_MASK, &t->mode);        break;
    937 	case 'l': set_mode(t, csi, 0, MODE_STATE_ALL_MASK, &t->mode);        break;
    938 	case 'm': set_colours(t, csi);                                       break;
    939 	case 'r':
    940 		if (csi->priv) {
    941 			/* NOTE: XTRESTORE: restore the value of a private mode */
    942 			set_mode(t, csi, 1, t->saved_mode, &t->mode);
    943 		} else {
    944 			set_scrolling_region(t, ORONE(csi->argv[0]) - 1, ORONE(csi->argv[1]) - 1);
    945 			cursor_move_abs_to(t, 0, 0);
    946 		}
    947 		break;
    948 	case 's':
    949 		if (csi->priv) {
    950 			/* NOTE: XTSAVE: save the value of a private mode */
    951 			set_mode(t, csi, 1, t->mode, &t->saved_mode);
    952 		} else {
    953 			/* NOTE: SCOSC/ANSI.SYS: save the cursor */
    954 			cursor_alt(t, 1);
    955 		}
    956 		break;
    957 	case 't': window_manipulation(t, csi);                               break;
    958 	case '!':
    959 		if (csi->raw.data[csi->raw.len - 1] == 'p') {
    960 			/* NOTE: DECSTR: soft terminal reset IGNORED */
    961 			break;
    962 		}
    963 		goto unknown;
    964 	case '"':
    965 		if (csi->raw.data[csi->raw.len - 1] == 'p') {
    966 			/* TODO: we should look at the second parameter and
    967 			 * use it modify C1 control character mode */
    968 			/* NOTE: DECSCL: set conformance level IGNORED */
    969 			break;
    970 		}
    971 		goto unknown;
    972 	default:
    973 	unknown:
    974 		stream_push_s8(&t->error_stream, s8("unknown csi: "));
    975 		dump_csi(csi, &t->error_stream);
    976 	}
    977 	END_TIMED_BLOCK();
    978 }
    979 
    980 static i32
    981 parse_osc(s8 *raw, OSC *osc)
    982 {
    983 	BEGIN_TIMED_BLOCK();
    984 
    985 	i32 result = 0;
    986 
    987 	*osc          = (OSC){0};
    988 	osc->raw.data = raw->data;
    989 
    990 	/* NOTE: parse command then store the rest as a string */
    991 	u32 cp;
    992 	while (raw->len) {
    993 		cp = get_ascii(raw);
    994 		osc->raw.len++;
    995 		if (!BETWEEN(cp, '0', '9'))
    996 			break;
    997 		osc->cmd *= 10;
    998 		osc->cmd += cp - '0';
    999 
   1000 		/* TODO: Performance? */
   1001 		/* NOTE: The maximum OSC in xterm is 119 so if this
   1002 		 * exceeds that the whole sequence is malformed */
   1003 		if (osc->cmd > 1000)
   1004 			break;
   1005 	}
   1006 
   1007 	if (cp != ';' || osc->cmd > 1000)
   1008 		os_fatal(s8("parse_osc: malformed\n"));
   1009 
   1010 	osc->arg.data = raw->data;
   1011 	while (raw->len) {
   1012 		cp = get_ascii(raw);
   1013 		osc->raw.len++;
   1014 		if (cp == '\a')
   1015 			goto end;
   1016 		if (cp == 0x1B && peek(*raw, 0) == '\\') {
   1017 			get_ascii(raw);
   1018 			osc->raw.len++;
   1019 			goto end;
   1020 		}
   1021 		osc->arg.len++;
   1022 	}
   1023 	/* NOTE: if we fell out of the loop then we ran out of characters */
   1024 	result = -1;
   1025 end:
   1026 	END_TIMED_BLOCK();
   1027 
   1028 	return result;
   1029 }
   1030 
   1031 static void
   1032 reset_csi(CSI *csi, s8 *raw)
   1033 {
   1034 	*csi          = (CSI){0};
   1035 	csi->raw.data = raw->data;
   1036 }
   1037 
   1038 static void
   1039 dump_osc(OSC *osc, Stream *err)
   1040 {
   1041 	stream_push_s8(err, s8("ESC]"));
   1042 	for (size i = 0; i < osc->raw.len; i++) {
   1043 		u8 cp = osc->raw.data[i];
   1044 		if (ISPRINT(cp)) {
   1045 			stream_push_byte(err, cp);
   1046 		} else if (cp == '\n') {
   1047 			stream_push_s8(err, s8("\\n"));
   1048 		} else if (cp == '\r') {
   1049 			stream_push_s8(err, s8("\\r"));
   1050 		} else if (cp == '\a') {
   1051 			stream_push_s8(err, s8("\\a"));
   1052 		} else {
   1053 			stream_push_s8(err, s8("\\x"));
   1054 			stream_push_hex_u64(err, cp);
   1055 		}
   1056 	}
   1057 	stream_push_s8(err, s8("\n\t.cmd = "));
   1058 	stream_push_u64(err, osc->cmd);
   1059 	stream_push_s8(err, s8(", .arg = {.len = "));
   1060 	stream_push_i64(err, osc->arg.len);
   1061 	stream_push_s8(err, s8("}\n"));
   1062 	os_write_err_msg(stream_to_s8(err));
   1063 	err->widx = 0;
   1064 }
   1065 
   1066 static void
   1067 handle_osc(Term *t, s8 *raw, Arena a)
   1068 {
   1069 	BEGIN_TIMED_BLOCK();
   1070 	OSC osc;
   1071 	i32 ret = parse_osc(raw, &osc);
   1072 	ASSERT(ret != -1);
   1073 
   1074 	Stream buffer = arena_stream(a);
   1075 	switch (osc.cmd) {
   1076 	case  0: stream_push_s8(&buffer, osc.arg); t->platform->set_window_title(&buffer); break;
   1077 	case  1: break; /* IGNORED: set icon name */
   1078 	case  2: stream_push_s8(&buffer, osc.arg); t->platform->set_window_title(&buffer); break;
   1079 	default:
   1080 		stream_push_s8(&t->error_stream, s8("unhandled osc cmd: "));
   1081 		dump_osc(&osc, &t->error_stream);
   1082 		break;
   1083 	}
   1084 	END_TIMED_BLOCK();
   1085 }
   1086 
   1087 static i32
   1088 handle_escape(Term *t, s8 *raw, Arena a)
   1089 {
   1090 	BEGIN_TIMED_BLOCK();
   1091 	i32 result = 0;
   1092 	u32 cp = get_ascii(raw);
   1093 	switch (cp) {
   1094 	case '[': reset_csi(&t->csi, raw); t->escape |= EM_CSI; break;
   1095 	case ']': handle_osc(t, raw, a); break;
   1096 	case '%': /* utf-8 mode */
   1097 		/* TODO: should this really be done here? */
   1098 		if (!raw->len) {
   1099 			result = 1;
   1100 		} else {
   1101 			switch (get_ascii(raw)) {
   1102 			case 'G': t->mode.term |=  TM_UTF8; break;
   1103 			case '@': t->mode.term &= ~TM_UTF8; break;
   1104 			}
   1105 		}
   1106 		break;
   1107 	case '(':   /* GZD4 -- set primary charset G0 */
   1108 	case ')':   /* G1D4 -- set secondary charset G1 */
   1109 	case '*':   /* G2D4 -- set tertiary charset G2 */
   1110 	case '+': { /* G3D4 -- set quaternary charset G3 */
   1111 		i32 index = cp - '(';
   1112 		/* TODO: should this really be done here? */
   1113 		if (!raw->len) {
   1114 			result = 1;
   1115 		} else {
   1116 			u32 cs = get_ascii(raw);
   1117 			switch (cs) {
   1118 			case '0': t->cursor.charsets[index] = CS_GRAPHIC0; break;
   1119 			case 'B': t->cursor.charsets[index] = CS_USA;      break;
   1120 			default:
   1121 				stream_push_s8(&t->error_stream, s8("unhandled charset: "));
   1122 				stream_push_byte(&t->error_stream, cs);
   1123 				stream_push_byte(&t->error_stream, '\n');
   1124 				os_write_err_msg(stream_to_s8(&t->error_stream));
   1125 				t->error_stream.widx = 0;
   1126 				break;
   1127 			}
   1128 		}
   1129 	} break;
   1130 	case '=': /* DECPAM -- application keypad */
   1131 	case '>': /* DECPNM -- normal keypad mode */
   1132 		/* TODO: MODE_APPKEYPAD */
   1133 		break;
   1134 	case 'c': /* RIS -- Reset to Initial State */
   1135 		term_reset(t);
   1136 		break;
   1137 	case 'D': /* IND -- Linefeed */
   1138 		push_newline(t, 0);
   1139 		break;
   1140 	case 'E': /* NEL -- Next Line */
   1141 		push_newline(t, 1);
   1142 		break;
   1143 	case 'H': /* HTS -- Horizontal Tab Stop */
   1144 		term_tab_col(t, t->cursor.pos.x, 1);
   1145 		break;
   1146 	case 'M': /* RI  -- Reverse Index */
   1147 		if (t->cursor.pos.y == t->top) {
   1148 			fb_scroll_down(t, t->top, 1);
   1149 		} else {
   1150 			cursor_move_to(t, t->cursor.pos.y - 1, t->cursor.pos.x);
   1151 		}
   1152 		break;
   1153 	case '7': /* DECSC: Save Cursor */
   1154 		cursor_alt(t, 1);
   1155 		break;
   1156 	case '8': /* DECRC: Restore Cursor */
   1157 		cursor_alt(t, 0);
   1158 		break;
   1159 	default:
   1160 		stream_push_s8(&t->error_stream, s8("unknown escape sequence: ESC "));
   1161 		stream_push_byte(&t->error_stream, cp);
   1162 		stream_push_s8(&t->error_stream, s8(" (0x"));
   1163 		stream_push_hex_u64(&t->error_stream, cp);
   1164 		stream_push_s8(&t->error_stream, s8(")\n"));
   1165 		os_write_err_msg(stream_to_s8(&t->error_stream));
   1166 		t->error_stream.widx = 0;
   1167 		break;
   1168 	}
   1169 	END_TIMED_BLOCK();
   1170 	return result;
   1171 }
   1172 
   1173 static i32
   1174 push_control(Term *t, s8 *line, u32 cp, Arena a)
   1175 {
   1176 	i32 result = 0;
   1177 	switch (cp) {
   1178 	case 0x1B:
   1179 		if (!line->len) result = 1;
   1180 		else            result = handle_escape(t, line, a);
   1181 		break;
   1182 	case '\r': cursor_move_to(t, t->cursor.pos.y, 0);   break;
   1183 	case '\n': push_newline(t, t->mode.term & TM_CRLF); break;
   1184 	case '\t': push_tab(t, 1);                          break;
   1185 	case '\a': /* TODO: ding ding? */                   break;
   1186 	case '\b':
   1187 		cursor_move_to(t, t->cursor.pos.y, t->cursor.pos.x - 1);
   1188 		break;
   1189 	case 0x0E: /* SO (LS1: Locking Shift 1) */
   1190 	case 0x0F: /* SI (LS0: Locking Shift 0) */
   1191 		t->cursor.charset_index = 1 - (cp - 0x0E);
   1192 		break;
   1193 	default:
   1194 		stream_push_s8(&t->error_stream, s8("unknown control code: 0x"));
   1195 		stream_push_hex_u64(&t->error_stream, cp);
   1196 		stream_push_byte(&t->error_stream, '\n');
   1197 		os_write_err_msg(stream_to_s8(&t->error_stream));
   1198 		t->error_stream.widx = 0;
   1199 		break;
   1200 	}
   1201 	if (cp != 0x1B) {
   1202 		if (t->escape & EM_CSI) t->csi.raw.len++;
   1203 	}
   1204 	return result;
   1205 }
   1206 
   1207 static void
   1208 push_normal_cp(Term *t, TermView *tv, u32 cp)
   1209 {
   1210 	BEGIN_TIMED_BLOCK();
   1211 
   1212 	if (t->mode.term & TM_AUTO_WRAP && t->cursor.state & CURSOR_WRAP_NEXT)
   1213 		push_newline(t, 1);
   1214 
   1215 	if (t->cursor.charsets[t->cursor.charset_index] == CS_GRAPHIC0 &&
   1216 	    BETWEEN(cp, 0x41, 0x7e) && graphic_0[cp - 0x41])
   1217 		cp = graphic_0[cp - 0x41];
   1218 
   1219 	u32 width = 1;
   1220 	if (cp > 0x7F) {
   1221 		width = wcwidth(cp);
   1222 		ASSERT(width != -1);
   1223 	}
   1224 
   1225 	/* NOTE: make this '>=' for fun in vis */
   1226 	if (t->cursor.pos.x + width > t->size.w) {
   1227 		/* NOTE: make space for character if mode enabled else
   1228 		 * clobber whatever was on the end of the line */
   1229 		if (t->mode.term & TM_AUTO_WRAP)
   1230 			push_newline(t, 1);
   1231 		else
   1232 			cursor_move_to(t, t->cursor.pos.y, t->size.w - width);
   1233 	}
   1234 
   1235 	Cell *c = &tv->fb.rows[t->cursor.pos.y][t->cursor.pos.x];
   1236 	/* TODO: pack cell into ssbo */
   1237 	c->cp   = cp;
   1238 	c->fg   = SHADER_PACK_FG(t->cursor.style.fg.rgba, t->cursor.style.attr);
   1239 	c->bg   = SHADER_PACK_BG(t->cursor.style.bg.rgba, t->cursor.style.attr);
   1240 
   1241 	if (width > 1) {
   1242 		ASSERT(t->cursor.pos.x + (width - 1) < t->size.w);
   1243 		c[0].bg |= ATTR_WIDE;
   1244 		c[1].bg |= ATTR_WDUMMY;
   1245 	}
   1246 
   1247 	if (t->cursor.pos.x + width < t->size.w)
   1248 		cursor_step_column(t, width);
   1249 	else
   1250 		t->cursor.state |= CURSOR_WRAP_NEXT;
   1251 
   1252 	/* TODO: region detection */
   1253 	if (is_selected(&t->selection, t->cursor.pos.x, t->cursor.pos.y))
   1254 		selection_clear(&t->selection);
   1255 
   1256 	END_TIMED_BLOCK();
   1257 }
   1258 
   1259 static void
   1260 push_line(Term *t, Line *line, Arena a)
   1261 {
   1262 	BEGIN_TIMED_BLOCK();
   1263 
   1264 	TermView *tv    = t->views + t->view_idx;
   1265 	s8 l            = line_to_s8(line, &tv->log);
   1266 	t->cursor.style = line->cursor_state;
   1267 
   1268 	while (l.len) {
   1269 		u32 cp;
   1270 		if (line->has_unicode) cp = get_utf8(&l);
   1271 		else                   cp = get_ascii(&l);
   1272 
   1273 		ASSERT(cp != (u32)-1);
   1274 		if (ISCONTROL(cp)) {
   1275 			if (!(t->mode.term & TM_UTF8) || !ISCONTROLC1(cp))
   1276 				push_control(t, &l, cp, a);
   1277 			continue;
   1278 		} else if (t->escape & EM_CSI) {
   1279 			t->csi.raw.len++;
   1280 			if (BETWEEN(cp, '@', '~')) {
   1281 				handle_csi(t, &t->csi);
   1282 				t->escape &= ~EM_CSI;
   1283 			}
   1284 			continue;
   1285 		}
   1286 
   1287 		push_normal_cp(t, tv, cp);
   1288 	}
   1289 	END_TIMED_BLOCK();
   1290 }
   1291 
   1292 static size
   1293 get_line_idx(LineBuf *lb, size off)
   1294 {
   1295 	ASSERT(-off <= lb->filled);
   1296 	size result = lb->widx + off;
   1297 	if (result < 0)
   1298 		result += lb->filled;
   1299 	return result;
   1300 }
   1301 
   1302 static void
   1303 blit_lines(Term *t, Arena a)
   1304 {
   1305 	BEGIN_TIMED_BLOCK();
   1306 
   1307 	ASSERT(t->gl.flags & NEEDS_REFILL);
   1308 	term_reset(t);
   1309 
   1310 	TermView *tv    = t->views + t->view_idx;
   1311 	size line_count = t->size.h - 1;
   1312 	size off        = t->scroll_offset;
   1313 	CLAMP(line_count, 0, tv->lines.filled);
   1314 	for (size idx = -line_count; idx <= 0; idx++) {
   1315 		size line_idx = get_line_idx(&tv->lines, idx - off);
   1316 		push_line(t, tv->lines.buf + line_idx, a);
   1317 	}
   1318 
   1319 	t->gl.flags &= ~NEEDS_REFILL;
   1320 
   1321 	END_TIMED_BLOCK();
   1322 }
   1323 
   1324 static void
   1325 handle_input(Term *t, Arena a, s8 raw)
   1326 {
   1327 	BEGIN_TIMED_BLOCK();
   1328 
   1329 	TermView *tv = t->views + t->view_idx;
   1330 
   1331 	/* TODO: SIMD look ahead */
   1332 	while (raw.len) {
   1333 		size start_len = raw.len;
   1334 		u32 cp = peek(raw, 0);
   1335 		/* TODO: this could be a performance issue; may need seperate code path for
   1336 		 * terminal when not in UTF8 mode */
   1337 		if (cp > 0x7F && (t->mode.term & TM_UTF8)) {
   1338 			cp = get_utf8(&raw);
   1339 			tv->lines.buf[tv->lines.widx].has_unicode = 1;
   1340 			if (cp == (u32)-1) {
   1341 				/* NOTE: Need More Bytes! */
   1342 				raw.len = start_len;
   1343 				goto end;
   1344 			}
   1345 		} else {
   1346 			cp = get_ascii(&raw);
   1347 		}
   1348 
   1349 		ASSERT(cp != (u32)-1);
   1350 
   1351 		if (ISCONTROL(cp)) {
   1352 			if (!(t->mode.term & TM_UTF8) || !ISCONTROLC1(cp)) {
   1353 				i32 old_curs_y = t->cursor.pos.y;
   1354 				if (push_control(t, &raw, cp, a)) {
   1355 					raw.len = start_len;
   1356 					goto end;
   1357 				}
   1358 				if (!t->escape && (cp == '\n' || t->cursor.pos.y != old_curs_y))
   1359 					feed_line(&tv->lines, raw.data, t->cursor.style);
   1360 			}
   1361 			continue;
   1362 		} else if (t->escape & EM_CSI) {
   1363 			t->csi.raw.len++;
   1364 			if (BETWEEN(cp, '@', '~')) {
   1365 				i32 old_curs_y = t->cursor.pos.y;
   1366 				i32 mode = t->mode.term & TM_ALTSCREEN;
   1367 				handle_csi(t, &t->csi);
   1368 				t->escape &= ~EM_CSI;
   1369 				if ((t->mode.term & TM_ALTSCREEN) != mode) {
   1370 					u8 *old = raw.data - t->csi.raw.len - 2;
   1371 					ASSERT(*old == 0x1B);
   1372 					feed_line(&tv->lines, old, t->cursor.style);
   1373 					TermView *nv = t->views + t->view_idx;
   1374 					size nstart  = nv->log.widx;
   1375 					mem_copy(raw.data, nv->log.buf + nstart, raw.len);
   1376 					commit_to_rb(tv, -raw.len);
   1377 					commit_to_rb(nv,  raw.len);
   1378 					raw.data = nv->log.buf + nstart;
   1379 					init_line(nv->lines.buf + nv->lines.widx, raw.data,
   1380 					          t->cursor.style);
   1381 					tv = nv;
   1382 				} else if (t->cursor.pos.y != old_curs_y) {
   1383 					feed_line(&tv->lines, raw.data, t->cursor.style);
   1384 				}
   1385 			}
   1386 			continue;
   1387 		}
   1388 
   1389 		push_normal_cp(t, tv, cp);
   1390 
   1391 	}
   1392 end:
   1393 	tv->lines.buf[tv->lines.widx].end = raw.data;
   1394 
   1395 	/* TODO: this shouldn't be needed */
   1396 	if (tv->lines.buf[tv->lines.widx].end < tv->lines.buf[tv->lines.widx].start)
   1397 		tv->lines.buf[tv->lines.widx].start -= tv->log.cap;
   1398 
   1399 	if (!t->escape && line_length(tv->lines.buf + tv->lines.widx) > SPLIT_LONG)
   1400 		feed_line(&tv->lines, raw.data, t->cursor.style);
   1401 
   1402 	t->unprocessed_bytes = raw.len;
   1403 	END_TIMED_BLOCK();
   1404 }