vtgl

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

terminal.c (40925B)


      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, i32 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, iv2 tl, iv2 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, i32 r1, i32 r2, i32 c1, i32 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 	iv2 top_left     = {.x = c1, .y = r1};
    234 	iv2 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 (i32 r = top_left.y; r <= bottom_right.y; r++) {
    243 		for (i32 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, i32 top, i32 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);
    262 
    263 	fb_clear_region(t, t->bot - n + 1, t->bot, 0, t->size.w);
    264 	for (i32 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, i32 top, i32 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);
    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 (i32 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 	rows = MIN(rows, I32_MAX - t->cursor.pos.y);
    373 	cols = MIN(cols, I32_MAX - t->cursor.pos.x);
    374 	cursor_move_to(t, t->cursor.pos.y + rows, t->cursor.pos.x + cols);
    375 }
    376 
    377 static void
    378 cursor_up(Term *t, i32 requested_count)
    379 {
    380 	i32 cursor_y = t->cursor.pos.y;
    381 	i32 max      = cursor_y >= t->top ? (cursor_y - t->top) : cursor_y;
    382 	i32 count    = MIN(max, MAX(requested_count, 1));
    383 	cursor_move_to(t, t->cursor.pos.y - count, t->cursor.pos.x);
    384 }
    385 
    386 static void
    387 cursor_down(Term *t, i32 requested_count)
    388 {
    389 	i32 cursor_y = t->cursor.pos.y;
    390 	i32 max      = cursor_y <= t->bot ? (t->bot - cursor_y) : (t->size.h - cursor_y - 1);
    391 	i32 count    = MIN(max, MAX(requested_count, 1));
    392 	cursor_move_to(t, t->cursor.pos.y + count, t->cursor.pos.x);
    393 }
    394 
    395 static i32
    396 next_tab_position(Term *t, b32 backwards)
    397 {
    398 	static_assert(ARRAY_COUNT(t->tabs) == 8 * sizeof(*t->tabs),
    399 	              "Term.tabs must be same length as the bitwidth of it's type");
    400 	u32 col  = t->cursor.pos.x;
    401 	u32 idx  = col / ARRAY_COUNT(t->tabs);
    402 	u32 bit  = col % ARRAY_COUNT(t->tabs);
    403 	u32 mask = safe_left_shift(1, bit - backwards) - 1;
    404 
    405 	i32 result = 32 * idx;
    406 	if (backwards) {
    407 		u32 zeroes = clz_u32(t->tabs[idx--] & mask);
    408 		while (idx < ARRAY_COUNT(t->tabs) && zeroes == 32)
    409 			zeroes = clz_u32(t->tabs[idx--]);
    410 		result  = 32 * (idx + 1) + 32 - zeroes;
    411 		result *= col != 0;
    412 	} else {
    413 		u32 zeroes = ctz_u32(t->tabs[idx++] & ~mask);
    414 		while (idx < ARRAY_COUNT(t->tabs) && zeroes == 32)
    415 			zeroes = ctz_u32(t->tabs[idx++]);
    416 		result = 32 * (idx - 1) + zeroes + 1;
    417 	}
    418 	/* TODO(rnp): is clamping this correct? */
    419 	//ASSERT(result < t->size.w);
    420 	result = MIN(result, t->size.w - 1);
    421 
    422 	return result;
    423 }
    424 
    425 static void
    426 term_tab_col(Term *t, i32 col, b32 set)
    427 {
    428 	ASSERT(col < t->size.w);
    429 	u32 idx  = (col > 0) ? ((col - 1) / ARRAY_COUNT(t->tabs)) : 0;
    430 	u32 bit  = (col > 0) ? ((col - 1) % ARRAY_COUNT(t->tabs)) : 0;
    431 	u32 mask = 1u;
    432 	if (bit) mask = safe_left_shift(1, bit);
    433 	if (set) t->tabs[idx] |=  mask;
    434 	else     t->tabs[idx] &= ~mask;
    435 }
    436 
    437 static void
    438 reset_tabs(Term *t, u32 column_step)
    439 {
    440 	for (i32 i = 0; i < ARRAY_COUNT(t->tabs); i++)
    441 		t->tabs[i] = 0;
    442 	for (i32 i = column_step; i < t->size.w; i += column_step)
    443 		term_tab_col(t, i, 1);
    444 }
    445 
    446 static void
    447 term_reset(Term *t)
    448 {
    449 	i32 mode = t->mode.term & TM_ALTSCREEN;
    450 	t->cursor.state = CURSOR_NORMAL;
    451 	for (u32 i = 0; i < ARRAY_COUNT(t->saved_cursors); i++) {
    452 		cursor_reset(t);
    453 		t->cursor.charset_index = 0;
    454 		for (u32 i = 0; i < ARRAY_COUNT(t->cursor.charsets); i++)
    455 			t->cursor.charsets[i] = CS_USA;
    456 		cursor_move_to(t, 0, 0);
    457 		cursor_alt(t, 1);
    458 		swap_screen(t);
    459 		fb_clear_region(t, 0, t->size.h, 0, t->size.w);
    460 	}
    461 	reset_tabs(t, g_tabstop);
    462 
    463 	t->top  = 0;
    464 	t->bot  = t->size.h - 1;
    465 	/* TODO: why is term_reset() being called when we are in the altscreen */
    466 	t->mode.term = mode|TM_AUTO_WRAP|TM_UTF8;
    467 }
    468 
    469 static void
    470 stream_push_csi(Stream *s, CSI *csi)
    471 {
    472 	stream_push_s8(s, s8("ESC ["));
    473 	for (size i = 0; i < csi->raw.len; i++) {
    474 		u8 c = csi->raw.data[i];
    475 		if (ISPRINT(c)) {
    476 			stream_push_byte(s, csi->raw.data[i]);
    477 		} else if (c == '\n') {
    478 			stream_push_s8(s, s8("\\n"));
    479 		} else if (c == '\r') {
    480 			stream_push_s8(s, s8("\\r"));
    481 		} else {
    482 			stream_push_s8(s, s8("\\x"));
    483 			stream_push_hex_u64(s, c);
    484 		}
    485 	}
    486 	stream_push_s8(s, s8("\n\t{ .priv = "));
    487 	stream_push_u64(s, csi->priv);
    488 	stream_push_s8(s, s8(" .mode = "));
    489 	if (ISPRINT(csi->mode)) {
    490 		stream_push_byte(s, csi->mode);
    491 	} else {
    492 		stream_push_s8(s, s8("\\x"));
    493 		stream_push_hex_u64(s, csi->mode);
    494 	}
    495 
    496 	stream_push_s8(s, s8(", .argc = "));
    497 	stream_push_u64(s, csi->argc);
    498 	stream_push_s8(s, s8(", .argv = {"));
    499 	for (i32 i = 0; i < csi->argc; i++) {
    500 		stream_push_byte(s, ' ');
    501 		stream_push_i64(s, csi->argv[i]);
    502 	}
    503 	stream_push_s8(s, s8(" } }\n"));
    504 }
    505 
    506 /* ED/DECSED: Erase in Display */
    507 static void
    508 erase_in_display(Term *t, CSI *csi)
    509 {
    510 	iv2 cpos = t->cursor.pos;
    511 	switch (csi->argv[0]) {
    512 	case 0: /* Erase Below (default) */
    513 		fb_clear_region(t, cpos.y, cpos.y, cpos.x, t->size.w);
    514 		if (cpos.y < t->size.h - 1)
    515 			fb_clear_region(t, cpos.y + 1, t->size.h, 0, t->size.w);
    516 		break;
    517 	case 1: /* Erase Above */
    518 		if (cpos.y > 0)
    519 			fb_clear_region(t, 0, cpos.y - 1, 0, t->size.w);
    520 		fb_clear_region(t, cpos.y, cpos.y, 0, cpos.x);
    521 		break;
    522 	case 2: /* Erase All */
    523 		fb_clear_region(t, 0, t->size.h, 0, t->size.w);
    524 		break;
    525 	case 3: /* Erase Saved Lines (xterm) */
    526 		/* NOTE: ignored; we don't save lines in the way xterm does */
    527 		break;
    528 	default: /* TODO(rnp): warn about invalid argument */ ;
    529 	}
    530 }
    531 
    532 /* EL/DECSEL: Erase in Line */
    533 static void
    534 erase_in_line(Term *t, CSI *csi)
    535 {
    536 	iv2 cpos = t->cursor.pos;
    537 	switch (csi->argv[0]) {
    538 	case 0: /* Erase to Right */
    539 		fb_clear_region(t, cpos.y, cpos.y, cpos.x, t->size.w);
    540 		break;
    541 	case 1: /* Erase to Left */
    542 		fb_clear_region(t, cpos.y, cpos.y, 0, cpos.x);
    543 		break;
    544 	case 2: /* Erase All */
    545 		fb_clear_region(t, cpos.y, cpos.y, 0, t->size.w);
    546 		break;
    547 	default: /* TODO(rnp): warn about invalid argument */ ;
    548 	}
    549 }
    550 
    551 /* IL: Insert <count> blank lines */
    552 static void
    553 insert_blank_lines(Term *t, i32 count)
    554 {
    555 	fb_scroll_down(t, t->cursor.pos.y, count);
    556 }
    557 
    558 /* DL: Erase <count> lines */
    559 static void
    560 erase_lines(Term *t, i32 count)
    561 {
    562 	fb_scroll_up(t, t->cursor.pos.y, count);
    563 }
    564 
    565 /* DCH: Delete <count> Characters */
    566 static void
    567 delete_characters(Term *t, i32 requested_count)
    568 {
    569 	iv2 cpos = t->cursor.pos;
    570 	/* TODO(rnp): if the cursor is outside the left/right margins do nothing */
    571 
    572 	TermView *tv  = t->views + t->view_idx;
    573 	i32 count     = MAX(0, MIN(requested_count, t->size.w - cpos.x));
    574 	i32 total     = t->size.w - count;
    575 
    576 	Cell *dst = tv->fb.rows[cpos.y] + cpos.x;
    577 	if (dst->bg & ATTR_WDUMMY) dst--;
    578 	Cell *src = dst + count;
    579 
    580 	i32 dst_off = 0, src_off = 0;
    581 	for (i32 i = 0; i < total; i++) {
    582 		b32 dst_wide = (dst[i].bg & ATTR_WIDE) != 0;
    583 		b32 src_wide = (src[i].bg & ATTR_WIDE) != 0;
    584 
    585 		if (dst_wide && !src_wide)
    586 			dst[i + 1].bg &= ~ATTR_WDUMMY;
    587 
    588 		dst[i + dst_off] = src[i + src_off];
    589 
    590 		if (src_wide && !dst_wide) {
    591 			dst[i + 1] = src[i + 1];
    592 			i++;
    593 		}
    594 	}
    595 
    596 	fb_clear_region(t, cpos.y, cpos.y, t->size.w - count, t->size.w);
    597 }
    598 
    599 /* ECH: Erase <count> Characters */
    600 static void
    601 erase_characters(Term *t, i32 count)
    602 {
    603 	iv2 cpos  = t->cursor.pos;
    604 	fb_clear_region(t, cpos.y, cpos.y, cpos.x, cpos.x + count - 1);
    605 }
    606 
    607 /* TBC: Tabulation Clear */
    608 static void
    609 clear_term_tab(Term *t, i32 arg)
    610 {
    611 	/* TODO: case 1, 2? */
    612 	switch(arg) {
    613 	case 0:
    614 		term_tab_col(t, t->cursor.pos.x, 0);
    615 		break;
    616 	case 3:
    617 		for (u32 i = 0; i < ARRAY_COUNT(t->tabs); i++)
    618 			t->tabs[i] = 0;
    619 		break;
    620 	default:
    621 		stream_push_s8(&t->error_stream, s8("clear_term_tab: unhandled arg: "));
    622 		stream_push_i64(&t->error_stream, arg);
    623 		stream_push_byte(&t->error_stream, '\n');
    624 		os_write_err_msg(stream_to_s8(&t->error_stream));
    625 		t->error_stream.widx = 0;
    626 	}
    627 }
    628 
    629 /* SM/DECSET: Set Mode & RM/DECRST Reset Mode */
    630 static void
    631 set_mode(Term *t, CSI *csi, b32 set, ModeState src, ModeState *dest)
    632 {
    633 	BEGIN_TIMED_BLOCK();
    634 	i32 alt = t->view_idx;
    635 
    636 	/* TODO: this whole thing should be a lookup table */
    637 	#define PRIV(a) ((1 << 30) | (a))
    638 	for (i32 i = 0; i < csi->argc; i++) {
    639 		i32 arg = (csi->argv[i]) | ((csi->priv & 1) << 30);
    640 		switch (arg) {
    641 		case 4:          /* IRM: Insert/Replace Mode */
    642 			dest->term &= ~TM_REPLACE;
    643 			if (set) dest->term |= (src.term & TM_REPLACE);
    644 			break;
    645 		case 20:         /* LNM: Linefeed Assumes Carriage Return */
    646 			dest->term &= ~TM_CRLF;
    647 			if (set) dest->term |= (src.term & TM_CRLF);
    648 			break;
    649 		case PRIV(1):    /* DECCKM: use application cursor keys */
    650 			dest->win &= ~WM_APPCURSOR;
    651 			if (set) dest->win |= (src.win & WM_APPCURSOR);
    652 			break;
    653 		case PRIV(5):    /* DECSCNM: reverse/normal video mode */
    654 			dest->win &= ~WM_REVERSE;
    655 			if (set) dest->win |= (src.win & WM_REVERSE);
    656 			break;
    657 		case PRIV(6):    /* DECOM: Cursor Origin Mode */
    658 			if (set) t->cursor.state |=  CURSOR_ORIGIN;
    659 			else     t->cursor.state &= ~CURSOR_ORIGIN;
    660 			cursor_move_abs_to(t, 0, 0);
    661 			break;
    662 		case PRIV(7):    /* DECAWM: Auto-Wrap Mode */
    663 			dest->term &= ~TM_AUTO_WRAP;
    664 			if (set) dest->term |= (src.term & TM_AUTO_WRAP);
    665 			break;
    666 		case PRIV(1000): /* xterm: report mouse button presses */
    667 			dest->win &= ~WM_MOUSE_MASK;
    668 			if (set) dest->win |= (src.win & WM_MOUSE_BTN);
    669 			break;
    670 		case PRIV(1002): /* xterm: cell motion tracking */
    671 			dest->win &= ~WM_MOUSE_MASK;
    672 			if (set) dest->win |= (src.win & WM_MOUSE_TRK);
    673 			break;
    674 		case PRIV(1006): /* xterm: SGR mouse mode */
    675 			dest->win &= ~WM_MOUSE_SGR;
    676 			if (set) dest->win |= (src.win & WM_MOUSE_SGR);
    677 			break;
    678 		case PRIV(3):    /* DECCOLM: 132/80 Column Mode */
    679 		case PRIV(4):    /* DECSCLM: Fast/Slow Scroll */
    680 		case PRIV(8):    /* DECARM: Auto-Repeat Keys */
    681 		case PRIV(12):   /* AT&T 610: Start blinking cursor */
    682 		case PRIV(40):   /* xterm: (dis)allow 132/80 Column Mode */
    683 		case PRIV(45):   /* XTREVWRAP: xterm reverse wrap around */
    684 		case PRIV(1001): /* xterm: (broken) mouse highlight tracking; requires
    685 		                  * the requesting program to be competely functional or will
    686 		                  * hang the terminal by design */
    687 		case PRIV(1015): /* urxvt: (broken) mouse mode */
    688 			/* IGNORED */
    689 			break;
    690 		case PRIV(25):   /* DECTCEM: Show/Hide Cursor */
    691 			dest->win &= ~WM_HIDECURSOR;
    692 			if (!set) dest->win |= (src.win & WM_HIDECURSOR);
    693 			break;
    694 		case PRIV(1034): /* xterm: enable 8-bit input mode */
    695 			dest->win &= ~WM_8BIT;
    696 			if (set) dest->win |= (src.win & WM_8BIT);
    697 			break;
    698 		case PRIV(1049): /* xterm: swap cursor then swap screen */
    699 			cursor_alt(t, set);
    700 			/* FALLTHROUGH */
    701 		case PRIV(47):   /* xterm: swap screen buffer */
    702 		case PRIV(1047): /* xterm: swap screen buffer */
    703 			if (alt)                  fb_clear_region(t, 0, t->size.h, 0, t->size.w);
    704 			if (set ^ alt)            swap_screen(t);
    705 			if (csi->argv[i] != 1049) break;
    706 			/* FALLTHROUGH */
    707 		case PRIV(1048): /* xterm: swap cursor */
    708 			cursor_alt(t, set);
    709 			break;
    710 		case PRIV(2004): /* xterm: bracketed paste mode */
    711 			dest->win &= ~WM_BRACKPASTE;
    712 			if (set) dest->win |= (src.win & WM_BRACKPASTE);
    713 			break;
    714 		case PRIV(2026): /* synchronized render (eg. wezterm) */
    715 			/* IGNORED: we aren't writing some slow garbage so we won't let some
    716 			 * broken program try and stall us because they think we can't operate
    717 			 * fast enough */
    718 			break;
    719 		default:
    720 			stream_push_s8(&t->error_stream, s8("set_mode: unhandled mode: "));
    721 			stream_push_csi(&t->error_stream, csi);
    722 			os_write_err_msg(stream_to_s8(&t->error_stream));
    723 			t->error_stream.widx = 0;
    724 		}
    725 	}
    726 	#undef PRIV
    727 	END_TIMED_BLOCK();
    728 }
    729 
    730 /* NOTE: adapted from the perl script 256colres.pl in xterm src */
    731 static Colour
    732 indexed_colour(i32 index)
    733 {
    734 	Colour result;
    735 	if (index < 232) {
    736 		/* NOTE: 16-231 are colours off a 6x6x6 RGB cube */
    737 		index -= 16;
    738 		result.r = 40 * ((index / 36));
    739 		result.g = 40 * ((index % 36) / 6);
    740 		result.b = 40 * ((index %  6));
    741 		result.a = 0xFF;
    742 		if (result.r) result.r += 55;
    743 		if (result.g) result.g += 55;
    744 		if (result.b) result.b += 55;
    745 	} else {
    746 		/* NOTE: 232-255 are greyscale ramp */
    747 		u32 k = (10 * (index - 232) + 8) & 0xFF;
    748 		result.r = result.g = result.b = k;
    749 		result.a = 0xFF;
    750 	}
    751 	return result;
    752 }
    753 
    754 static struct conversion_result
    755 direct_colour(i32 *argv, i32 argc, i32 *idx, Stream *err)
    756 {
    757 	struct conversion_result result = {.status = CR_FAILURE};
    758 	switch (argv[*idx + 1]) {
    759 	case 2: /* NOTE: defined RGB colour */
    760 		if (*idx + 4 >= argc) {
    761 			stream_push_s8(err, s8("direct_colour: wrong parameter count: "));
    762 			stream_push_u64(err, argc);
    763 			stream_push_byte(err, '\n');
    764 			break;
    765 		}
    766 		u32 r = (u32)argv[*idx + 2];
    767 		u32 g = (u32)argv[*idx + 3];
    768 		u32 b = (u32)argv[*idx + 4];
    769 		*idx += 4;
    770 		if (r > 0xFF || g > 0xFF || b > 0xFF) {
    771 			stream_push_s8(err, s8("direct_colour: bad rgb colour: ("));
    772 			stream_push_u64(err, r);
    773 			stream_push_s8(err, s8(", "));
    774 			stream_push_u64(err, g);
    775 			stream_push_s8(err, s8(", "));
    776 			stream_push_u64(err, b);
    777 			stream_push_s8(err, s8(")\n"));
    778 			break;
    779 		}
    780 		result.colour = (Colour){.r = r, .g = g, .b = b, .a = 0xFF};
    781 		result.status = CR_SUCCESS;
    782 		break;
    783 	case 5: /* NOTE: indexed colour */
    784 		if (*idx + 2 >= argc) {
    785 			stream_push_s8(err, s8("direct_colour: wrong parameter count: "));
    786 			stream_push_u64(err, argc);
    787 			stream_push_byte(err, '\n');
    788 			break;
    789 		}
    790 		*idx += 2;
    791 		if (!BETWEEN(argv[*idx], 0, 255)) {
    792 			stream_push_s8(err, s8("direct_colour: index parameter out of range: "));
    793 			stream_push_i64(err, argv[*idx]);
    794 			stream_push_byte(err, '\n');
    795 			break;
    796 		}
    797 		if (BETWEEN(argv[*idx], 0, 16))
    798 			result.colour = g_colours.data[argv[*idx]];
    799 		else
    800 			result.colour = indexed_colour(argv[*idx]);
    801 		result.status = CR_SUCCESS;
    802 		break;
    803 	default:
    804 		stream_push_s8(err, s8("direct_colour: unknown argument: "));
    805 		stream_push_i64(err, argv[*idx + 1]);
    806 		stream_push_byte(err, '\n');
    807 	}
    808 
    809 	return result;
    810 }
    811 
    812 /* SGR: Select Graphic Rendition */
    813 static void
    814 set_colours(Term *t, CSI *csi)
    815 {
    816 	BEGIN_TIMED_BLOCK();
    817 	CellStyle *cs = &t->cursor.style;
    818 	struct conversion_result dcr;
    819 	for (i32 i = 0; i < csi->argc; i++) {
    820 		switch (csi->argv[i]) {
    821 		case  0: cursor_reset(t);                     break;
    822 		case  1: cs->attr |= ATTR_BOLD;               break;
    823 		case  2: cs->attr |= ATTR_FAINT;              break;
    824 		case  3: cs->attr |= ATTR_ITALIC;             break;
    825 		case  4: cs->attr |= ATTR_UNDERLINED;         break;
    826 		case  5: cs->attr |= ATTR_BLINK;              break;
    827 		case  7: cs->attr |= ATTR_INVERSE;            break;
    828 		case  8: cs->attr |= ATTR_INVISIBLE;          break;
    829 		case  9: cs->attr |= ATTR_STRUCK;             break;
    830 		case 22: cs->attr &= ~(ATTR_BOLD|ATTR_FAINT); break;
    831 		case 23: cs->attr &= ~ATTR_ITALIC;            break;
    832 		case 24: cs->attr &= ~ATTR_UNDERLINED;        break;
    833 		case 25: cs->attr &= ~ATTR_BLINK;             break;
    834 		case 27: cs->attr &= ~ATTR_INVERSE;           break;
    835 		case 28: cs->attr &= ~ATTR_INVISIBLE;         break;
    836 		case 29: cs->attr &= ~ATTR_STRUCK;            break;
    837 		case 38:
    838 			dcr = direct_colour(csi->argv, csi->argc, &i, &t->error_stream);
    839 			if (dcr.status == CR_SUCCESS) {
    840 				cs->fg = dcr.colour;
    841 			} else {
    842 				stream_push_s8(&t->error_stream, s8("set_colours: "));
    843 				stream_push_csi(&t->error_stream, csi);
    844 				os_write_err_msg(stream_to_s8(&t->error_stream));
    845 				t->error_stream.widx = 0;
    846 			}
    847 			break;
    848 
    849 		case 39: cs->fg = g_colours.data[g_colours.fgidx]; break;
    850 
    851 		case 48:
    852 			dcr = direct_colour(csi->argv, csi->argc, &i, &t->error_stream);
    853 			if (dcr.status == CR_SUCCESS) {
    854 				cs->bg = dcr.colour;
    855 			} else {
    856 				stream_push_s8(&t->error_stream, s8("set_colours: "));
    857 				stream_push_csi(&t->error_stream, csi);
    858 				os_write_err_msg(stream_to_s8(&t->error_stream));
    859 				t->error_stream.widx = 0;
    860 			}
    861 			break;
    862 
    863 		case 49: cs->bg = g_colours.data[g_colours.bgidx]; break;
    864 
    865 		default:
    866 			if (BETWEEN(csi->argv[i], 30, 37)) {
    867 				cs->fg = g_colours.data[csi->argv[i] - 30];
    868 			} else if (BETWEEN(csi->argv[i], 40, 47)) {
    869 				cs->bg = g_colours.data[csi->argv[i] - 40];
    870 			} else if (BETWEEN(csi->argv[i], 90, 97)) {
    871 				cs->fg = g_colours.data[csi->argv[i] - 82];
    872 			} else if (BETWEEN(csi->argv[i], 100, 107)) {
    873 				cs->bg = g_colours.data[csi->argv[i] - 92];
    874 			} else {
    875 				stream_push_s8(&t->error_stream, s8("unhandled colour arg: "));
    876 				stream_push_i64(&t->error_stream, csi->argv[i]);
    877 				stream_push_byte(&t->error_stream, '\n');
    878 				stream_push_csi(&t->error_stream, csi);
    879 				os_write_err_msg(stream_to_s8(&t->error_stream));
    880 				t->error_stream.widx = 0;
    881 			}
    882 		}
    883 	}
    884 	END_TIMED_BLOCK();
    885 }
    886 
    887 static void
    888 set_top_bottom_margins(Term *t, i32 requested_top, i32 requested_bottom)
    889 {
    890 	i32 top    = MIN(MAX(1, requested_top), t->size.h);
    891 	i32 bottom = MIN(t->size.h, (requested_bottom ? requested_bottom : t->size.h));
    892 	if (top < bottom) {
    893 		t->top = top - 1;
    894 		t->bot = bottom - 1;
    895 		cursor_move_abs_to(t, 0, 0);
    896 	}
    897 }
    898 
    899 static void
    900 window_manipulation(Term *t, CSI *csi)
    901 {
    902 	switch (csi->argv[0]) {
    903 	case 22: t->platform->get_window_title(&t->saved_title); break;
    904 	case 23: t->platform->set_window_title(&t->saved_title); break;
    905 	default:
    906 		stream_push_s8(&t->error_stream, s8("unhandled xtwinops: "));
    907 		stream_push_i64(&t->error_stream, csi->argv[0]);
    908 		stream_push_byte(&t->error_stream, '\n');
    909 		stream_push_csi(&t->error_stream, csi);
    910 		os_write_err_msg(stream_to_s8(&t->error_stream));
    911 		t->error_stream.widx = 0;
    912 	}
    913 }
    914 
    915 static void
    916 push_newline(Term *t, b32 move_to_first_col)
    917 {
    918 	i32 row = t->cursor.pos.y;
    919 	if (row == t->bot && t->scroll_offset == 0)
    920 		fb_scroll_up(t, t->top, 1);
    921 	else
    922 		row++;
    923 	cursor_move_to(t, row, move_to_first_col? 0 : t->cursor.pos.x);
    924 }
    925 
    926 static void
    927 push_tab(Term *t, i32 n)
    928 {
    929 	u32 end = ABS(n);
    930 	for (u32 i = 0; i < end; i++)
    931 		cursor_move_to(t, t->cursor.pos.y, next_tab_position(t, n < 0));
    932 }
    933 
    934 static b32
    935 parse_csi(s8 *r, CSI *csi)
    936 {
    937 	BEGIN_TIMED_BLOCK();
    938 	b32 result = 1;
    939 
    940 	if (peek(*r, 0) == '?') {
    941 		csi->priv = 1;
    942 		get_ascii(r);
    943 	}
    944 
    945 	struct conversion_result arg = {0};
    946 	while (r->len) {
    947 		s8_parse_i32_accum(&arg, *r);
    948 		*r = arg.unparsed;
    949 		if (r->len) {
    950 			u32 cp = get_ascii(r);
    951 
    952 			if (ISCONTROL(cp))
    953 				continue;
    954 
    955 			csi->argv[csi->argc++] = arg.i;
    956 			zero_struct(&arg);
    957 
    958 			if (cp != ';' || csi->argc == ESC_ARG_SIZ) {
    959 				if (cp == ';') csi->mode = get_ascii(r);
    960 				else           csi->mode = cp;
    961 				goto end;
    962 			}
    963 		}
    964 	}
    965 	/* NOTE: if we fell out of the loop then we ran out of characters */
    966 	result = 0;
    967 end:
    968 	END_TIMED_BLOCK();
    969 
    970 	return result;
    971 }
    972 
    973 static void
    974 handle_csi(Term *t, CSI *csi)
    975 {
    976 	BEGIN_TIMED_BLOCK();
    977 	s8  raw = csi->raw;
    978 	b32 ret = parse_csi(&raw, csi);
    979 	if (!ret) goto unknown;
    980 
    981 	#define ORONE(x) ((x)? (x) : 1)
    982 
    983 	iv2 p = t->cursor.pos;
    984 
    985 	switch (csi->mode) {
    986 	case 'A': cursor_up(t,   csi->argv[0]);                              break;
    987 	case 'B': cursor_down(t, csi->argv[0]);                              break;
    988 	case 'C': cursor_step_raw(t, ORONE(csi->argv[0]),  0,  1);           break;
    989 	case 'D': cursor_step_raw(t, ORONE(csi->argv[0]),  0, -1);           break;
    990 	case 'E': cursor_move_to(t, p.y + ORONE(csi->argv[0]), 0);           break;
    991 	case 'F': cursor_move_to(t, p.y - ORONE(csi->argv[0]), 0);           break;
    992 	case 'G': cursor_move_to(t, p.y, csi->argv[0] - 1);                  break;
    993 	case 'H': cursor_move_abs_to(t, csi->argv[0] - 1, csi->argv[1] - 1); break;
    994 	case 'J': erase_in_display(t, csi);                                  break;
    995 	case 'K': erase_in_line(t, csi);                                     break;
    996 	case 'L': insert_blank_lines(t, ORONE(csi->argv[0]));                break;
    997 	case 'M': erase_lines(t, ORONE(csi->argv[0]));                       break;
    998 	case 'P': delete_characters(t, ORONE(csi->argv[0]));                 break;
    999 	case 'X': erase_characters(t, ORONE(csi->argv[0]));                  break;
   1000 	case 'S': fb_scroll_up(t, t->top, ORONE(csi->argv[0]));              break;
   1001 	case 'T': fb_scroll_down(t, t->top, ORONE(csi->argv[0]));            break;
   1002 	case 'W': if (csi->priv) reset_tabs(t, 8); else goto unknown;        break;
   1003 	case 'Z': push_tab(t, -(ORONE(csi->argv[0])));                       break;
   1004 	case 'a': cursor_step_raw(t, ORONE(csi->argv[0]),  0,  1);           break;
   1005 	case 'd': cursor_move_abs_to(t, csi->argv[0] - 1, p.x);              break;
   1006 	case 'e': cursor_step_raw(t, ORONE(csi->argv[0]),  1,  0);           break;
   1007 	case 'f': cursor_move_abs_to(t, csi->argv[0] - 1, csi->argv[1] - 1); break;
   1008 	case 'g': clear_term_tab(t, csi->argv[0]);                           break;
   1009 	case 'h': set_mode(t, csi, 1, MODE_STATE_ALL_MASK, &t->mode);        break;
   1010 	case 'l': set_mode(t, csi, 0, MODE_STATE_ALL_MASK, &t->mode);        break;
   1011 	case 'm': set_colours(t, csi);                                       break;
   1012 	case 'n': {
   1013 		switch (csi->argv[0]) {
   1014 		case 5: /* NOTE: DSR V-1: Operating Status */
   1015 			t->platform->write(t->child, s8("\x1B[0n"));
   1016 			break;
   1017 		case 6: /* NOTE: DSR V-2: Cursor Position */ {
   1018 			iv2 cpos = t->cursor.pos;
   1019 			if (t->cursor.state & CURSOR_ORIGIN) {
   1020 				/* TODO(rnp): left/right margin */
   1021 				//cpos.x = MAX(0, cpos.x - t->margin.left);
   1022 				cpos.y = MAX(0, cpos.y - t->top);
   1023 			}
   1024 			Stream buf = arena_stream(t->arena_for_frame);
   1025 			stream_push_s8(&buf, s8("\x1B["));
   1026 			stream_push_i64(&buf, cpos.y + 1);
   1027 			stream_push_byte(&buf, ';');
   1028 			stream_push_i64(&buf, cpos.x + 1);
   1029 			stream_push_byte(&buf, 'R');
   1030 			t->platform->write(t->child, stream_to_s8(&buf));
   1031 		} break;
   1032 		default: goto unknown;
   1033 		}
   1034 	} break;
   1035 	case 'r':
   1036 		if (csi->priv) {
   1037 			/* NOTE: XTRESTORE: restore the value of a private mode */
   1038 			set_mode(t, csi, 1, t->saved_mode, &t->mode);
   1039 		} else {
   1040 			set_top_bottom_margins(t, csi->argv[0], csi->argv[1]);
   1041 		}
   1042 		break;
   1043 	case 's':
   1044 		if (csi->priv) {
   1045 			/* NOTE: XTSAVE: save the value of a private mode */
   1046 			set_mode(t, csi, 1, t->mode, &t->saved_mode);
   1047 		} else if (csi->argc == 1) {
   1048 			/* NOTE: SCOSC/ANSI.SYS: save the cursor */
   1049 			cursor_alt(t, 1);
   1050 		} else {
   1051 			goto unknown;
   1052 		}
   1053 		break;
   1054 	case 't': window_manipulation(t, csi); break;
   1055 	case '!':
   1056 		if (csi->raw.data[csi->raw.len - 1] == 'p') {
   1057 			/* NOTE: DECSTR: soft terminal reset IGNORED */
   1058 			break;
   1059 		}
   1060 		goto unknown;
   1061 	case '"':
   1062 		if (csi->raw.data[csi->raw.len - 1] == 'p') {
   1063 			/* TODO: we should look at the second parameter and
   1064 			 * use it modify C1 control character mode */
   1065 			/* NOTE: DECSCL: set conformance level IGNORED */
   1066 			break;
   1067 		}
   1068 		goto unknown;
   1069 	default:
   1070 	unknown:
   1071 		stream_push_s8(&t->error_stream, s8("unknown csi: "));
   1072 		stream_push_csi(&t->error_stream, csi);
   1073 		os_write_err_msg(stream_to_s8(&t->error_stream));
   1074 		t->error_stream.widx = 0;
   1075 	}
   1076 	END_TIMED_BLOCK();
   1077 }
   1078 
   1079 static b32
   1080 parse_osc(s8 *raw, OSC *osc)
   1081 {
   1082 	BEGIN_TIMED_BLOCK();
   1083 
   1084 	b32 result = 0;
   1085 	/* TODO(rnp): make this whole function re-entrant */
   1086 	zero_struct(osc);
   1087 	osc->raw.data = raw->data;
   1088 
   1089 	struct conversion_result cmd = s8_parse_i32_until(*raw, ';');
   1090 	if (cmd.status != CR_FAILURE) {
   1091 		osc->cmd     = cmd.i;
   1092 		osc->arg     = cmd.unparsed;
   1093 		osc->raw.len = osc->arg.data - raw->data;
   1094 		*raw         = consume(*raw, osc->raw.len);
   1095 	}
   1096 
   1097 	if (osc->arg.len && peek(osc->arg, 0) == ';') {
   1098 		osc->arg.data++;
   1099 		osc->arg.len = 0;
   1100 		osc->raw.len++;
   1101 		get_ascii(raw);
   1102 		while (raw->len) {
   1103 			u32 cp = get_ascii(raw);
   1104 			osc->raw.len++;
   1105 			if (cp == '\a') {
   1106 				result = 1;
   1107 				break;
   1108 			}
   1109 			if (cp == 0x1B && (raw->len && peek(*raw, 0) == '\\')) {
   1110 				get_ascii(raw);
   1111 				osc->raw.len++;
   1112 				result = 1;
   1113 				break;
   1114 			}
   1115 			osc->arg.len++;
   1116 		}
   1117 	}
   1118 
   1119 	END_TIMED_BLOCK();
   1120 
   1121 	return result;
   1122 }
   1123 
   1124 static void
   1125 reset_csi(CSI *csi, s8 *raw)
   1126 {
   1127 	*csi          = (CSI){0};
   1128 	csi->raw.data = raw->data;
   1129 }
   1130 
   1131 static void
   1132 dump_osc(OSC *osc, Stream *err)
   1133 {
   1134 	stream_push_s8(err, s8("ESC]"));
   1135 	for (size i = 0; i < osc->raw.len; i++) {
   1136 		u8 cp = osc->raw.data[i];
   1137 		if (ISPRINT(cp)) {
   1138 			stream_push_byte(err, cp);
   1139 		} else if (cp == '\n') {
   1140 			stream_push_s8(err, s8("\\n"));
   1141 		} else if (cp == '\r') {
   1142 			stream_push_s8(err, s8("\\r"));
   1143 		} else if (cp == '\a') {
   1144 			stream_push_s8(err, s8("\\a"));
   1145 		} else {
   1146 			stream_push_s8(err, s8("\\x"));
   1147 			stream_push_hex_u64(err, cp);
   1148 		}
   1149 	}
   1150 	stream_push_s8(err, s8("\n\t.cmd = "));
   1151 	stream_push_u64(err, osc->cmd);
   1152 	stream_push_s8(err, s8(", .arg = {.len = "));
   1153 	stream_push_i64(err, osc->arg.len);
   1154 	stream_push_s8(err, s8("}\n"));
   1155 	os_write_err_msg(stream_to_s8(err));
   1156 	err->widx = 0;
   1157 }
   1158 
   1159 static void
   1160 handle_osc(Term *t, s8 *raw, Arena a)
   1161 {
   1162 	BEGIN_TIMED_BLOCK();
   1163 	OSC osc;
   1164 	if (!parse_osc(raw, &osc))
   1165 		goto unknown;
   1166 
   1167 	Stream buffer = arena_stream(a);
   1168 	switch (osc.cmd) {
   1169 	case  0: stream_push_s8(&buffer, osc.arg); t->platform->set_window_title(&buffer); break;
   1170 	case  1: break; /* IGNORED: set icon name */
   1171 	case  2: stream_push_s8(&buffer, osc.arg); t->platform->set_window_title(&buffer); break;
   1172 	default:
   1173 	unknown:
   1174 		stream_push_s8(&t->error_stream, s8("unhandled osc cmd: "));
   1175 		dump_osc(&osc, &t->error_stream);
   1176 		break;
   1177 	}
   1178 	END_TIMED_BLOCK();
   1179 }
   1180 
   1181 static i32
   1182 handle_escape(Term *t, s8 *raw, Arena a)
   1183 {
   1184 	BEGIN_TIMED_BLOCK();
   1185 	i32 result = 0;
   1186 	u32 cp = get_ascii(raw);
   1187 	switch (cp) {
   1188 	case '[': reset_csi(&t->csi, raw); t->escape |= EM_CSI; break;
   1189 	case ']': handle_osc(t, raw, a); break;
   1190 	case '%': /* utf-8 mode */
   1191 		/* TODO: should this really be done here? */
   1192 		if (!raw->len) {
   1193 			result = 1;
   1194 		} else {
   1195 			switch (get_ascii(raw)) {
   1196 			case 'G': t->mode.term |=  TM_UTF8; break;
   1197 			case '@': t->mode.term &= ~TM_UTF8; break;
   1198 			}
   1199 		}
   1200 		break;
   1201 	case '(':   /* GZD4 -- set primary charset G0 */
   1202 	case ')':   /* G1D4 -- set secondary charset G1 */
   1203 	case '*':   /* G2D4 -- set tertiary charset G2 */
   1204 	case '+': { /* G3D4 -- set quaternary charset G3 */
   1205 		i32 index = cp - '(';
   1206 		/* TODO: should this really be done here? */
   1207 		if (!raw->len) {
   1208 			result = 1;
   1209 		} else {
   1210 			u32 cs = get_ascii(raw);
   1211 			switch (cs) {
   1212 			case '0': t->cursor.charsets[index] = CS_GRAPHIC0; break;
   1213 			case 'B': t->cursor.charsets[index] = CS_USA;      break;
   1214 			default:
   1215 				stream_push_s8(&t->error_stream, s8("unhandled charset: "));
   1216 				stream_push_byte(&t->error_stream, cs);
   1217 				stream_push_byte(&t->error_stream, '\n');
   1218 				os_write_err_msg(stream_to_s8(&t->error_stream));
   1219 				t->error_stream.widx = 0;
   1220 				break;
   1221 			}
   1222 		}
   1223 	} break;
   1224 	case '=': /* DECPAM -- application keypad */
   1225 	case '>': /* DECPNM -- normal keypad mode */
   1226 		/* TODO: MODE_APPKEYPAD */
   1227 		break;
   1228 	case 'c': /* RIS -- Reset to Initial State */
   1229 		term_reset(t);
   1230 		break;
   1231 	case 'D': /* IND -- Linefeed */
   1232 		push_newline(t, 0);
   1233 		break;
   1234 	case 'E': /* NEL -- Next Line */
   1235 		push_newline(t, 1);
   1236 		break;
   1237 	case 'H': /* HTS -- Horizontal Tab Stop */
   1238 		term_tab_col(t, t->cursor.pos.x, 1);
   1239 		break;
   1240 	case 'M': /* RI  -- Reverse Index */
   1241 		if (t->cursor.pos.y == t->top) {
   1242 			fb_scroll_down(t, t->top, 1);
   1243 		} else {
   1244 			cursor_move_to(t, t->cursor.pos.y - 1, t->cursor.pos.x);
   1245 		}
   1246 		break;
   1247 	case '7': /* DECSC: Save Cursor */
   1248 		cursor_alt(t, 1);
   1249 		break;
   1250 	case '8': /* DECRC: Restore Cursor */
   1251 		cursor_alt(t, 0);
   1252 		break;
   1253 	default:
   1254 		stream_push_s8(&t->error_stream, s8("unknown escape sequence: ESC "));
   1255 		stream_push_byte(&t->error_stream, cp);
   1256 		stream_push_s8(&t->error_stream, s8(" (0x"));
   1257 		stream_push_hex_u64(&t->error_stream, cp);
   1258 		stream_push_s8(&t->error_stream, s8(")\n"));
   1259 		os_write_err_msg(stream_to_s8(&t->error_stream));
   1260 		t->error_stream.widx = 0;
   1261 		break;
   1262 	}
   1263 	END_TIMED_BLOCK();
   1264 	return result;
   1265 }
   1266 
   1267 static i32
   1268 push_control(Term *t, s8 *line, u32 cp, Arena a)
   1269 {
   1270 	i32 result = 0;
   1271 	switch (cp) {
   1272 	case 0x1B:
   1273 		if (!line->len) result = 1;
   1274 		else            result = handle_escape(t, line, a);
   1275 		break;
   1276 	case '\r': cursor_move_to(t, t->cursor.pos.y, 0);   break;
   1277 	case '\n': push_newline(t, t->mode.term & TM_CRLF); break;
   1278 	case '\t': push_tab(t, 1);                          break;
   1279 	case '\a': /* TODO: ding ding? */                   break;
   1280 	case '\b':
   1281 		cursor_move_to(t, t->cursor.pos.y, t->cursor.pos.x - 1);
   1282 		break;
   1283 	case 0x0E: /* SO (LS1: Locking Shift 1) */
   1284 	case 0x0F: /* SI (LS0: Locking Shift 0) */
   1285 		t->cursor.charset_index = 1 - (cp - 0x0E);
   1286 		break;
   1287 	default:
   1288 		stream_push_s8(&t->error_stream, s8("unknown control code: 0x"));
   1289 		stream_push_hex_u64(&t->error_stream, cp);
   1290 		stream_push_byte(&t->error_stream, '\n');
   1291 		os_write_err_msg(stream_to_s8(&t->error_stream));
   1292 		t->error_stream.widx = 0;
   1293 		break;
   1294 	}
   1295 	if (cp != 0x1B) {
   1296 		if (t->escape & EM_CSI) t->csi.raw.len++;
   1297 	}
   1298 	return result;
   1299 }
   1300 
   1301 static void
   1302 push_normal_cp(Term *t, TermView *tv, u32 cp)
   1303 {
   1304 	BEGIN_TIMED_BLOCK();
   1305 
   1306 	if (t->mode.term & TM_AUTO_WRAP && t->cursor.state & CURSOR_WRAP_NEXT)
   1307 		push_newline(t, 1);
   1308 
   1309 	if (t->cursor.charsets[t->cursor.charset_index] == CS_GRAPHIC0 && BETWEEN(cp, 0x60, 0x7e))
   1310 		cp = graphic_0[cp - 0x60];
   1311 
   1312 	i32 width = 1;
   1313 	if (cp > 0x7F) {
   1314 		width = wcwidth(cp);
   1315 		ASSERT(width != -1);
   1316 	}
   1317 
   1318 	/* NOTE: make this '>=' for fun in vis */
   1319 	if (t->cursor.pos.x + width > t->size.w) {
   1320 		/* NOTE: make space for character if mode enabled else
   1321 		 * clobber whatever was on the end of the line */
   1322 		if (t->mode.term & TM_AUTO_WRAP)
   1323 			push_newline(t, 1);
   1324 		else
   1325 			cursor_move_to(t, t->cursor.pos.y, t->size.w - width);
   1326 	}
   1327 
   1328 	Cell *c = &tv->fb.rows[t->cursor.pos.y][t->cursor.pos.x];
   1329 	/* TODO: pack cell into ssbo */
   1330 	c->cp   = cp;
   1331 	c->fg   = SHADER_PACK_FG(t->cursor.style.fg.rgba, t->cursor.style.attr);
   1332 	c->bg   = SHADER_PACK_BG(t->cursor.style.bg.rgba, t->cursor.style.attr);
   1333 
   1334 	if (width > 1) {
   1335 		ASSERT(t->cursor.pos.x + (width - 1) < t->size.w);
   1336 		c[0].bg |= ATTR_WIDE;
   1337 		c[1].bg |= ATTR_WDUMMY;
   1338 	}
   1339 
   1340 	if (t->cursor.pos.x + width < t->size.w)
   1341 		cursor_step_column(t, width);
   1342 	else
   1343 		t->cursor.state |= CURSOR_WRAP_NEXT;
   1344 
   1345 	/* TODO: region detection */
   1346 	if (is_selected(&t->selection, t->cursor.pos.x, t->cursor.pos.y))
   1347 		selection_clear(&t->selection);
   1348 
   1349 	END_TIMED_BLOCK();
   1350 }
   1351 
   1352 static void
   1353 push_line(Term *t, Line *line, Arena a)
   1354 {
   1355 	BEGIN_TIMED_BLOCK();
   1356 
   1357 	TermView *tv    = t->views + t->view_idx;
   1358 	s8 l            = line_to_s8(line, &tv->log);
   1359 	t->cursor.style = line->cursor_state;
   1360 
   1361 	while (l.len) {
   1362 		u32 cp;
   1363 		if (line->has_unicode) cp = get_utf8(&l);
   1364 		else                   cp = get_ascii(&l);
   1365 
   1366 		ASSERT(cp != (u32)-1);
   1367 		if (ISCONTROL(cp)) {
   1368 			if (!(t->mode.term & TM_UTF8) || !ISCONTROLC1(cp))
   1369 				push_control(t, &l, cp, a);
   1370 			continue;
   1371 		} else if (t->escape & EM_CSI) {
   1372 			t->csi.raw.len++;
   1373 			if (BETWEEN(cp, '@', '~')) {
   1374 				handle_csi(t, &t->csi);
   1375 				t->escape &= ~EM_CSI;
   1376 			}
   1377 			continue;
   1378 		}
   1379 
   1380 		push_normal_cp(t, tv, cp);
   1381 	}
   1382 	END_TIMED_BLOCK();
   1383 }
   1384 
   1385 static size
   1386 get_line_idx(LineBuf *lb, size off)
   1387 {
   1388 	ASSERT(-off <= lb->filled);
   1389 	size result = lb->widx + off;
   1390 	if (result < 0)
   1391 		result += lb->filled;
   1392 	return result;
   1393 }
   1394 
   1395 static void
   1396 blit_lines(Term *t, Arena a)
   1397 {
   1398 	BEGIN_TIMED_BLOCK();
   1399 
   1400 	ASSERT(t->gl.flags & NEEDS_REFILL);
   1401 	term_reset(t);
   1402 
   1403 	TermView *tv    = t->views + t->view_idx;
   1404 	size line_count = t->size.h - 1;
   1405 	size off        = t->scroll_offset;
   1406 	CLAMP(line_count, 0, tv->lines.filled);
   1407 	for (size idx = -line_count; idx <= 0; idx++) {
   1408 		size line_idx = get_line_idx(&tv->lines, idx - off);
   1409 		push_line(t, tv->lines.buf + line_idx, a);
   1410 	}
   1411 
   1412 	t->gl.flags &= ~NEEDS_REFILL;
   1413 
   1414 	END_TIMED_BLOCK();
   1415 }
   1416 
   1417 static void
   1418 handle_input(Term *t, Arena a, s8 raw)
   1419 {
   1420 	BEGIN_TIMED_BLOCK();
   1421 
   1422 	TermView *tv = t->views + t->view_idx;
   1423 
   1424 	/* TODO: SIMD look ahead */
   1425 	while (raw.len) {
   1426 		size start_len = raw.len;
   1427 		u32 cp = peek(raw, 0);
   1428 		/* TODO: this could be a performance issue; may need seperate code path for
   1429 		 * terminal when not in UTF8 mode */
   1430 		if (cp > 0x7F && (t->mode.term & TM_UTF8)) {
   1431 			cp = get_utf8(&raw);
   1432 			if (cp == (u32)-1 && start_len < 4) {
   1433 				/* NOTE: Need More Bytes! */
   1434 				goto end;
   1435 			} else if (cp == (u32)-1 && start_len >= 4) {
   1436 				/* NOTE(rnp): invalid/garbage cp; treat as ASCII control char */
   1437 				cp = get_ascii(&raw);
   1438 			} else if (cp != (u32)-1) {
   1439 				tv->lines.buf[tv->lines.widx].has_unicode = 1;
   1440 			}
   1441 		} else {
   1442 			cp = get_ascii(&raw);
   1443 		}
   1444 
   1445 		ASSERT(cp != (u32)-1);
   1446 
   1447 		if (ISCONTROL(cp)) {
   1448 			if (!(t->mode.term & TM_UTF8) || !ISCONTROLC1(cp)) {
   1449 				i32 old_curs_y = t->cursor.pos.y;
   1450 				if (push_control(t, &raw, cp, a)) {
   1451 					raw.len = start_len;
   1452 					goto end;
   1453 				}
   1454 				if (!t->escape && (cp == '\n' || t->cursor.pos.y != old_curs_y))
   1455 					feed_line(&tv->lines, raw.data, t->cursor.style);
   1456 			}
   1457 			continue;
   1458 		} else if (t->escape & EM_CSI) {
   1459 			t->csi.raw.len++;
   1460 			if (BETWEEN(cp, '@', '~')) {
   1461 				i32 old_curs_y = t->cursor.pos.y;
   1462 				u32 mode = t->mode.term & TM_ALTSCREEN;
   1463 				handle_csi(t, &t->csi);
   1464 				t->escape &= ~EM_CSI;
   1465 				if ((t->mode.term & TM_ALTSCREEN) != mode) {
   1466 					u8 *old = raw.data - t->csi.raw.len - 2;
   1467 					ASSERT(*old == 0x1B);
   1468 					feed_line(&tv->lines, old, t->cursor.style);
   1469 					TermView *nv = t->views + t->view_idx;
   1470 					size nstart  = nv->log.widx;
   1471 					mem_copy(raw.data, nv->log.buf + nstart, raw.len);
   1472 					commit_to_rb(tv, -raw.len);
   1473 					commit_to_rb(nv,  raw.len);
   1474 					raw.data = nv->log.buf + nstart;
   1475 					init_line(nv->lines.buf + nv->lines.widx, raw.data,
   1476 					          t->cursor.style);
   1477 					tv = nv;
   1478 				} else if (t->cursor.pos.y != old_curs_y) {
   1479 					feed_line(&tv->lines, raw.data, t->cursor.style);
   1480 				}
   1481 			}
   1482 			continue;
   1483 		}
   1484 
   1485 		push_normal_cp(t, tv, cp);
   1486 
   1487 	}
   1488 end:
   1489 	tv->lines.buf[tv->lines.widx].end = raw.data;
   1490 
   1491 	/* TODO: this shouldn't be needed */
   1492 	if (tv->lines.buf[tv->lines.widx].end < tv->lines.buf[tv->lines.widx].start)
   1493 		tv->lines.buf[tv->lines.widx].start -= tv->log.cap;
   1494 
   1495 	if (!t->escape && line_length(tv->lines.buf + tv->lines.widx) > SPLIT_LONG)
   1496 		feed_line(&tv->lines, raw.data, t->cursor.style);
   1497 
   1498 	t->unprocessed_bytes = raw.len;
   1499 	END_TIMED_BLOCK();
   1500 }