vtgl

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

terminal.c (41162B)


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