vtgl

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

terminal.c (31396B)


      1 #include <immintrin.h>
      2 
      3 static const u8 utf8overhangmask[32] = {
      4 	255, 255, 255, 255,  255, 255, 255, 255,
      5 	255, 255, 255, 255,  255, 255, 255, 255,
      6 	0, 0, 0, 0,  0, 0, 0, 0,
      7 	0, 0, 0, 0,  0, 0, 0, 0
      8 };
      9 
     10 static Range
     11 get_word_around_cell(Term *t, iv2 cell)
     12 {
     13 	Range result = {.start = cell, .end = cell};
     14 	Cell *row    = t->views[t->view_idx].fb.rows[cell.y];
     15 
     16 	b32 isspace = ISSPACE(row[cell.x].cp);
     17 	while (result.start.x > 0) {
     18 		Cell nc = row[result.start.x - 1];
     19 		if (!(nc.style.attr & ATTR_WDUMMY) && isspace != ISSPACE(nc.cp))
     20 			break;
     21 		result.start.x--;
     22 	}
     23 	while (result.end.x < t->size.w - 1) {
     24 		Cell nc = row[result.end.x + 1];
     25 		if (!(nc.style.attr & ATTR_WDUMMY) && isspace != ISSPACE(nc.cp))
     26 			break;
     27 		result.end.x++;
     28 	}
     29 
     30 	/* NOTE: ATTR_WDUMMY is invalid for start and end of range */
     31 	if (row[result.start.x].style.attr & ATTR_WDUMMY) result.start.x++;
     32 	if (row[result.end.x].style.attr   & ATTR_WDUMMY) result.end.x--;
     33 	return result;
     34 }
     35 
     36 static void
     37 set_window_title(GLFWwindow *win, Arena a, s8 title)
     38 {
     39 	glfwSetWindowTitle(win, s8_to_cstr(&a, title));
     40 }
     41 
     42 static s8
     43 consume(s8 raw, size count)
     44 {
     45 	raw.data += count;
     46 	raw.len  -= count;
     47 	return raw;
     48 }
     49 
     50 static u8
     51 peek(s8 raw, size i)
     52 {
     53 	ASSERT(i < raw.len);
     54 	return raw.data[i];
     55 }
     56 
     57 static u32
     58 get_utf8(s8 *raw)
     59 {
     60 	u32 state = 0, cp;
     61 	size off = 0;
     62 	while (off < raw->len) {
     63 		if (!utf8_decode(&state, &cp, raw->data[off++])) {
     64 			*raw = consume(*raw, off);
     65 			return cp;
     66 		}
     67 	}
     68 	return (u32)-1;
     69 }
     70 
     71 static u32
     72 get_ascii(s8 *raw)
     73 {
     74 	ASSERT(raw->len > 0);
     75 	u32 result = raw->data[0];
     76 	*raw = consume(*raw, 1);
     77 	return result;
     78 }
     79 
     80 static size
     81 line_length(Line *l)
     82 {
     83 	ASSERT(l->start <= l->end);
     84 	return l->end - l->start;
     85 }
     86 
     87 static s8
     88 line_to_s8(Line *l, RingBuf *rb)
     89 {
     90 	ASSERT(l->start <= l->end);
     91 
     92 	s8 result = {.len = l->end - l->start, .data = l->start};
     93 	return result;
     94 }
     95 
     96 static void
     97 init_line(Line *l, u8 *position, CellStyle cursor_state)
     98 {
     99 	l->start        = position;
    100 	l->end          = position;
    101 	l->has_unicode  = 0;
    102 	l->cursor_state = cursor_state;
    103 }
    104 
    105 static void
    106 feed_line(LineBuf *lb, u8 *position, CellStyle cursor_state)
    107 {
    108 	lb->buf[lb->widx++].end = position;
    109 	lb->widx    = lb->widx >= lb->cap ? 0 : lb->widx;
    110 	lb->filled += lb->filled <= lb->widx;
    111 	init_line(lb->buf + lb->widx, position, cursor_state);
    112 }
    113 
    114 static void
    115 selection_clear(Selection *s)
    116 {
    117 	s->range.end = INVALID_RANGE_END;
    118 	s->state     = SS_NONE;
    119 }
    120 
    121 static void
    122 selection_scroll(Term *t, u32 origin, i32 n)
    123 {
    124 	Selection *s = &t->selection;
    125 	if (!is_valid_range(s->range))
    126 		return;
    127 
    128 	b32 start_in_bounds = BETWEEN(s->range.start.y, origin, t->bot);
    129 	b32 end_in_bounds   = BETWEEN(s->range.end.y,   origin, t->bot);
    130 	if (start_in_bounds != end_in_bounds) {
    131 		selection_clear(s);
    132 	} else if (start_in_bounds) {
    133 		s->range.start.y += n;
    134 		s->range.end.y   += n;
    135 		if (s->range.start.y > t->bot || s->range.start.y < t->top ||
    136 		    s->range.end.y   > t->bot || s->range.end.y   < t->top)
    137 			selection_clear(s);
    138 	}
    139 }
    140 
    141 static b32
    142 is_selected(Selection *s, i32 x, i32 y)
    143 {
    144 	if (!is_valid_range(s->range))
    145 		return 0;
    146 
    147 	b32 result = BETWEEN(y, s->range.start.y, s->range.end.y) &&
    148 	             (y != s->range.start.y || x >= s->range.start.x) &&
    149 	             (y != s->range.end.y   || x <= s->range.end.x);
    150 	return result;
    151 }
    152 
    153 static void
    154 fb_clear_region(Term *t, u32 r1, u32 r2, u32 c1, u32 c2)
    155 {
    156 	u32 tmp;
    157 	if (r1 > r2) {
    158 		tmp = r1;
    159 		r1  = r2;
    160 		r2  = tmp;
    161 	}
    162 	if (c1 > c2) {
    163 		tmp = c1;
    164 		c1  = c2;
    165 		c2  = tmp;
    166 	}
    167 	CLAMP(c1, 0, t->size.w - 1);
    168 	CLAMP(c2, 0, t->size.w - 1);
    169 	CLAMP(r1, 0, t->size.h - 1);
    170 	CLAMP(r2, 0, t->size.h - 1);
    171 
    172 	TermView *tv = t->views + t->view_idx;
    173 	for (u32 r = r1; r <= r2; r++) {
    174 		for (u32 c = c1; c <= c2; c++) {
    175 			tv->fb.rows[r][c].style = t->cursor.style;
    176 			tv->fb.rows[r][c].cp    = ' ';
    177 			if (is_selected(&t->selection, c, r))
    178 				selection_clear(&t->selection);
    179 		}
    180 	}
    181 }
    182 
    183 static void
    184 fb_scroll_down(Term *t, u32 top, u32 n)
    185 {
    186 	if (!BETWEEN(top, t->top, t->bot))
    187 		return;
    188 
    189 	TermView *tv = t->views + t->view_idx;
    190 	CLAMP(n, 0, t->bot - top + 1);
    191 	fb_clear_region(t, t->bot - n + 1, t->bot, 0, t->size.w);
    192 	for (u32 i = t->bot; i >= top + n; i--) {
    193 		Row tmp = tv->fb.rows[i];
    194 		tv->fb.rows[i]     = tv->fb.rows[i - n];
    195 		tv->fb.rows[i - n] = tmp;
    196 	}
    197 	selection_scroll(t, top, n);
    198 }
    199 
    200 static void
    201 fb_scroll_up(Term *t, u32 top, u32 n)
    202 {
    203 	if (!BETWEEN(top, t->top, t->bot))
    204 		return;
    205 
    206 	TermView *tv = t->views + t->view_idx;
    207 	CLAMP(n, 0, t->bot - top + 1);
    208 	fb_clear_region(t, top, top + n - 1, 0, t->size.w);
    209 	for (u32 i = top; i <= t->bot - n; i++) {
    210 		Row tmp = tv->fb.rows[i];
    211 		tv->fb.rows[i]     = tv->fb.rows[i + n];
    212 		tv->fb.rows[i + n] = tmp;
    213 	}
    214 	selection_scroll(t, top, -n);
    215 }
    216 
    217 static void
    218 swap_screen(Term *t)
    219 {
    220 	t->mode     ^= TM_ALTSCREEN;
    221 	t->view_idx  = !!(t->mode & TM_ALTSCREEN);
    222 	t->gl.flags |= NEEDS_FULL_REFILL;
    223 }
    224 
    225 static void
    226 cursor_reset(Term *t)
    227 {
    228 	//(Colour){.rgba = 0x1e9e33ff};
    229 	t->cursor.style.fg   = g_colours.data[g_colours.fgidx];
    230 	t->cursor.style.bg   = g_colours.data[g_colours.bgidx];
    231 	t->cursor.style.attr = ATTR_NULL;
    232 	t->cursor.state      = CURSOR_NORMAL;
    233 }
    234 
    235 static void
    236 cursor_move_to(Term *t, i32 row, i32 col)
    237 {
    238 	i32 minr = 0, maxr = t->size.h - 1;
    239 	if (t->cursor.state & CURSOR_ORIGIN) {
    240 		minr = t->top;
    241 		maxr = t->bot;
    242 	}
    243 	t->cursor.pos.y  = CLAMP(row, minr, maxr);
    244 	t->cursor.pos.x  = CLAMP(col, 0, t->size.w - 1);
    245 	t->cursor.state &= ~CURSOR_WRAP_NEXT;
    246 }
    247 
    248 static void
    249 cursor_move_abs_to(Term *t, i32 row, i32 col)
    250 {
    251 	if (t->cursor.state & CURSOR_ORIGIN)
    252 		row += t->top;
    253 	cursor_move_to(t, row, col);
    254 }
    255 
    256 static void
    257 cursor_alt(Term *t, b32 save)
    258 {
    259 	i32 mode = t->view_idx;
    260 	if (save) {
    261 		t->saved_cursors[mode] = t->cursor;
    262 	} else {
    263 		t->cursor = t->saved_cursors[mode];
    264 		cursor_move_to(t, t->cursor.pos.y, t->cursor.pos.x);
    265 	}
    266 }
    267 
    268 /* NOTE: advance the cursor by <n> cells; handles reverse movement */
    269 static void
    270 cursor_step_column(Term *t, i32 n)
    271 {
    272 	i32 col = t->cursor.pos.x + n;
    273 	i32 row = t->cursor.pos.y;
    274 	if (col >= t->size.w) {
    275 		row++;
    276 		col = 0;
    277 		if (row >= t->size.h)
    278 			fb_scroll_up(t, t->top, 1);
    279 	}
    280 	cursor_move_to(t, row, col);
    281 }
    282 
    283 /* NOTE: steps the cursor without causing a scroll */
    284 static void
    285 cursor_step_raw(Term *t, i32 step, i32 rows, i32 cols)
    286 {
    287 	rows *= step;
    288 	cols *= step;
    289 	cursor_move_to(t, t->cursor.pos.y + rows, t->cursor.pos.x + cols);
    290 }
    291 
    292 static void
    293 term_reset(Term *t)
    294 {
    295 	i32 mode = t->mode & TM_ALTSCREEN;
    296 	for (u32 i = 0; i < ARRAY_COUNT(t->saved_cursors); i++) {
    297 		cursor_reset(t);
    298 		cursor_move_to(t, 0, 0);
    299 		cursor_alt(t, 1);
    300 		swap_screen(t);
    301 		fb_clear_region(t, 0, t->size.h, 0, t->size.w);
    302 	}
    303 	t->top  = 0;
    304 	t->bot  = t->size.h - 1;
    305 	/* TODO: why is term_reset() being called when we are in the altscreen */
    306 	t->mode = mode|TM_AUTO_WRAP;
    307 }
    308 
    309 static void
    310 dump_csi(CSI *csi)
    311 {
    312 	os_write_err_msg(s8("raw: ESC["));
    313 	for (size i = 0; i < csi->raw.len; i++) {
    314 		u8 c = csi->raw.data[i];
    315 		if (ISPRINT(c))
    316 			os_write_err_msg((s8){.len = 1, .data = csi->raw.data + i});
    317 		else if (c == '\n')
    318 			os_write_err_msg(s8("\\n"));
    319 		else if (c == '\r')
    320 			os_write_err_msg(s8("\\r"));
    321 		else
    322 			fprintf(stderr, "\\x%02X", c);
    323 	}
    324 	fprintf(stderr, "\n\tparsed = { .priv = %d, .mode = ", csi->priv);
    325 	if (ISPRINT(csi->mode)) {
    326 		u8 buf[1] = {csi->mode};
    327 		os_write_err_msg((s8){.len = 1, .data = buf});
    328 	} else {
    329 		fprintf(stderr, "\\x%02X", csi->mode);
    330 	}
    331 	fprintf(stderr, ", .argc = %d, .argv = {", csi->argc);
    332 	for (i32 i = 0; i < csi->argc; i++)
    333 		fprintf(stderr, " %d", csi->argv[i]);
    334 	os_write_err_msg(s8(" } }\n"));
    335 }
    336 
    337 /* ED/DECSED: Erase in Display */
    338 static void
    339 erase_in_display(Term *t, CSI *csi)
    340 {
    341 	iv2 cpos = t->cursor.pos;
    342 	switch (csi->argv[0]) {
    343 	case 0: /* Erase Below (default) */
    344 		fb_clear_region(t, cpos.y, cpos.y, cpos.x, t->size.w);
    345 		if (cpos.y < t->size.h - 1)
    346 			fb_clear_region(t, cpos.y + 1, t->size.h, 0, t->size.w);
    347 		break;
    348 	case 1: /* Erase Above */
    349 		if (cpos.y > 0)
    350 			fb_clear_region(t, 0, cpos.y - 1, 0, t->size.w);
    351 		fb_clear_region(t, cpos.y, cpos.y, 0, cpos.x);
    352 		break;
    353 	case 2: /* Erase All */
    354 		fb_clear_region(t, 0, t->size.h, 0, t->size.w);
    355 		break;
    356 	case 3: /* Erase Saved Lines (xterm) */
    357 		/* NOTE: ignored; we don't save lines in the way xterm does */
    358 		break;
    359 	default: ASSERT(0);
    360 	}
    361 }
    362 
    363 /* EL/DECSEL: Erase in Line */
    364 static void
    365 erase_in_line(Term *t, CSI *csi)
    366 {
    367 	iv2 cpos = t->cursor.pos;
    368 	switch (csi->argv[0]) {
    369 	case 0: /* Erase to Right */
    370 		fb_clear_region(t, cpos.y, cpos.y, cpos.x, t->size.w);
    371 		break;
    372 	case 1: /* Erase to Left */
    373 		fb_clear_region(t, cpos.y, cpos.y, 0, cpos.x);
    374 		break;
    375 	case 2: /* Erase All */
    376 		fb_clear_region(t, cpos.y, cpos.y, 0, t->size.w);
    377 		break;
    378 	default: ASSERT(0);
    379 	}
    380 }
    381 
    382 /* IL: Insert <count> blank lines */
    383 static void
    384 insert_blank_lines(Term *t, i32 count)
    385 {
    386 	fb_scroll_down(t, t->cursor.pos.y, count);
    387 }
    388 
    389 /* DL: Erase <count> lines */
    390 static void
    391 erase_lines(Term *t, i32 count)
    392 {
    393 	fb_scroll_up(t, t->cursor.pos.y, count);
    394 }
    395 
    396 /* DCH: Delete Characters (NOTE: DCH is technically different but we are ignoring that) */
    397 /* ECH: Erase Characters  */
    398 static void
    399 erase_characters(Term *t, i32 count)
    400 {
    401 	iv2 cpos  = t->cursor.pos;
    402 	fb_clear_region(t, cpos.y, cpos.y, cpos.x, cpos.x + count - 1);
    403 }
    404 
    405 /* SM/DECSET: Set Mode & RM/DECRST Reset Mode */
    406 static void
    407 set_mode(Term *t, CSI *csi, b32 set)
    408 {
    409 	i32 alt = t->view_idx;
    410 	#define PRIV(a) ((1 << 30) | (a))
    411 	for (i32 i = 0; i < csi->argc; i++) {
    412 		i32 arg = (csi->argv[i]) | ((csi->priv & 1) << 30);
    413 		switch (arg) {
    414 		case 4:          /* IRM: Insert/Replace Mode */
    415 			if (set) t->mode |=  TM_REPLACE;
    416 			else     t->mode &= ~TM_REPLACE;
    417 			break;
    418 		case 20:         /* LNM: Linefeed Assumes Carriage Return */
    419 			if (set) t->mode |=  TM_CRLF;
    420 			else     t->mode &= ~TM_CRLF;
    421 			break;
    422 		case PRIV(1):    /* DECCKM: use application cursor keys */
    423 			if (set) t->gl.mode |=  WIN_MODE_APPCURSOR;
    424 			else     t->gl.mode &= ~WIN_MODE_APPCURSOR;
    425 			break;
    426 		case PRIV(5):    /* DECSCNM: reverse/normal video mode */
    427 			if (set) t->gl.mode |=  WIN_MODE_REVERSE;
    428 			else     t->gl.mode &= ~WIN_MODE_REVERSE;
    429 			break;
    430 		case PRIV(6):    /* DECOM: Cursor Origin Mode */
    431 			if (set) t->cursor.state |=  CURSOR_ORIGIN;
    432 			else     t->cursor.state &= ~CURSOR_ORIGIN;
    433 			cursor_move_abs_to(t, 0, 0);
    434 			break;
    435 		case PRIV(7):    /* DECAWM: Auto-Wrap Mode */
    436 			if (set) t->mode |=  TM_AUTO_WRAP;
    437 			else     t->mode &= ~TM_AUTO_WRAP;
    438 			break;
    439 		case PRIV(3):    /* DECCOLM: 132/80 Column Mode */
    440 		case PRIV(4):    /* DECSCLM: Fast/Slow Scroll */
    441 		case PRIV(8):    /* DECARM: Auto-Repeat Keys */
    442 		case PRIV(12):   /* AT&T 610: Start blinking cursor */
    443 		case PRIV(40):   /* xterm: (dis)allow 132/80 Column Mode */
    444 		case PRIV(45):   /* XTREVWRAP: xterm reverse wrap around */
    445 		case PRIV(1015): /* urxvt: (broken) mouse mode */
    446 			/* IGNORED */
    447 			break;
    448 		case PRIV(25):   /* DECTCEM: Show/Hide Cursor */
    449 			if (!set) t->gl.mode |=  WIN_MODE_HIDECURSOR;
    450 			else      t->gl.mode &= ~WIN_MODE_HIDECURSOR;
    451 			break;
    452 		case PRIV(1034): /* xterm: enable 8-bit input mode */
    453 			if (set) t->gl.mode |=  WIN_MODE_8BIT;
    454 			else     t->gl.mode &= ~WIN_MODE_8BIT;
    455 			break;
    456 		case PRIV(1049): /* xterm: swap cursor then swap screen */
    457 			cursor_alt(t, set);
    458 		case PRIV(47):   /* xterm: swap screen buffer */
    459 		case PRIV(1047): /* xterm: swap screen buffer */
    460 			if (alt)                  fb_clear_region(t, 0, t->size.h, 0, t->size.w);
    461 			if (set ^ alt)            swap_screen(t);
    462 			if (csi->argv[i] != 1049) break;
    463 			/* FALLTHROUGH */
    464 		case PRIV(1048): /* xterm: swap cursor */
    465 			cursor_alt(t, set);
    466 			break;
    467 		case PRIV(2004): /* xterm: bracketed paste mode */
    468 			if (set) t->gl.mode |=  WIN_MODE_BRACKPASTE;
    469 			else     t->gl.mode &= ~WIN_MODE_BRACKPASTE;
    470 			break;
    471 		default:
    472 			os_write_err_msg(s8("set_mode: unhandled mode: "));
    473 			dump_csi(csi);
    474 		}
    475 	}
    476 	#undef PRIV
    477 }
    478 
    479 /* NOTE: adapted from the perl script 256colres.pl in xterm src */
    480 static Colour
    481 indexed_colour(i32 index)
    482 {
    483 	Colour result;
    484 	if (index < 232) {
    485 		/* NOTE: 16-231 are colours off a 6x6x6 RGB cube */
    486 		index -= 16;
    487 		result.r = 40 * ((index / 36));
    488 		result.g = 40 * ((index % 36) / 6);
    489 		result.b = 40 * ((index %  6));
    490 		result.a = 0xFF;
    491 		if (result.r) result.r += 55;
    492 		if (result.g) result.g += 55;
    493 		if (result.b) result.b += 55;
    494 	} else {
    495 		/* NOTE: 232-255 are greyscale ramp */
    496 		u32 k = (10 * (index - 232) + 8) & 0xFF;
    497 		result.r = result.g = result.b = k;
    498 		result.a = 0xFF;
    499 	}
    500 	return result;
    501 }
    502 
    503 static struct conversion_result
    504 direct_colour(i32 *argv, i32 argc, i32 *idx)
    505 {
    506 	struct conversion_result result = {.status = CR_FAILURE};
    507 	switch (argv[*idx + 1]) {
    508 	case 2: /* NOTE: defined RGB colour */
    509 		if (*idx + 4 >= argc) {
    510 			fprintf(stderr, "direct_colour: wrong paramater count: %d\n", argc);
    511 			break;
    512 		}
    513 		u32 r = (u32)argv[*idx + 2];
    514 		u32 g = (u32)argv[*idx + 3];
    515 		u32 b = (u32)argv[*idx + 4];
    516 		*idx += 4;
    517 		if (r > 0xFF || g > 0xFF || b > 0xFF) {
    518 			fprintf(stderr, "direct_colour: bad rgb colour: (%u, %u, %u)\n", r, g, b);
    519 			break;
    520 		}
    521 		result.colour = (Colour){.r = r, .g = g, .b = b, .a = 0xFF};
    522 		result.status = CR_SUCCESS;
    523 		break;
    524 	case 5: /* NOTE: indexed colour */
    525 		if (*idx + 2 >= argc) {
    526 			fprintf(stderr, "direct_colour: wrong paramater count: %d\n", argc);
    527 			break;
    528 		}
    529 		*idx += 2;
    530 		if (!BETWEEN(argv[*idx], 0, 255)) {
    531 			fprintf(stderr, "direct_colour: index parameter out of range: %d\n",
    532 			        argv[*idx]);
    533 			break;
    534 		}
    535 		if (BETWEEN(argv[*idx], 0, 16))
    536 			result.colour = g_colours.data[argv[*idx]];
    537 		else
    538 			result.colour = indexed_colour(argv[*idx]);
    539 		result.status = CR_SUCCESS;
    540 		break;
    541 	default:
    542 		fprintf(stderr, "define_colour: unknown argument: %d\n", argv[*idx + 1]);
    543 	}
    544 	return result;
    545 }
    546 
    547 /* SGR: Select Graphic Rendition */
    548 static void
    549 set_colours(Term *t, CSI *csi)
    550 {
    551 	CellStyle *cs = &t->cursor.style;
    552 	struct conversion_result dcr;
    553 	for (i32 i = 0; i < csi->argc; i++) {
    554 		switch (csi->argv[i]) {
    555 		case  0: cursor_reset(t);                     break;
    556 		case  1: cs->attr |= ATTR_BOLD;               break;
    557 		case  2: cs->attr |= ATTR_FAINT;              break;
    558 		case  3: cs->attr |= ATTR_ITALIC;             break;
    559 		case  4: cs->attr |= ATTR_UNDERLINED;         break;
    560 		case  5: cs->attr |= ATTR_BLINK;              break;
    561 		case  7: cs->attr |= ATTR_INVERSE;            break;
    562 		case  8: cs->attr |= ATTR_INVISIBLE;          break;
    563 		case  9: cs->attr |= ATTR_STRUCK;             break;
    564 		case 22: cs->attr &= ~(ATTR_BOLD|ATTR_FAINT); break;
    565 		case 23: cs->attr &= ~ATTR_ITALIC;            break;
    566 		case 24: cs->attr &= ~ATTR_UNDERLINED;        break;
    567 		case 25: cs->attr &= ~ATTR_BLINK;             break;
    568 		case 27: cs->attr &= ~ATTR_INVERSE;           break;
    569 		case 28: cs->attr &= ~ATTR_INVISIBLE;         break;
    570 		case 29: cs->attr &= ~ATTR_STRUCK;            break;
    571 		case 38:
    572 			dcr = direct_colour(csi->argv, csi->argc, &i);
    573 			if (dcr.status == CR_SUCCESS) {
    574 				cs->fg = dcr.colour;
    575 			} else {
    576 				os_write_err_msg(s8("set_colours: "));
    577 				dump_csi(csi);
    578 			}
    579 			break;
    580 
    581 		case 39: cs->fg = g_colours.data[g_colours.fgidx]; break;
    582 
    583 		case 48:
    584 			dcr = direct_colour(csi->argv, csi->argc, &i);
    585 			if (dcr.status == CR_SUCCESS) {
    586 				cs->bg = dcr.colour;
    587 			} else {
    588 				os_write_err_msg(s8("set_colours: "));
    589 				dump_csi(csi);
    590 			}
    591 			break;
    592 
    593 		case 49: cs->bg = g_colours.data[g_colours.bgidx]; break;
    594 
    595 		default:
    596 			if (BETWEEN(csi->argv[i], 30, 37)) {
    597 				cs->fg = g_colours.data[csi->argv[i] - 30];
    598 			} else if (BETWEEN(csi->argv[i], 40, 47)) {
    599 				cs->bg = g_colours.data[csi->argv[i] - 40];
    600 			} else if (BETWEEN(csi->argv[i], 90, 97)) {
    601 				cs->fg = g_colours.data[csi->argv[i] - 82];
    602 			} else if (BETWEEN(csi->argv[i], 100, 107)) {
    603 				cs->bg = g_colours.data[csi->argv[i] - 92];
    604 			} else {
    605 				fprintf(stderr, "unhandled colour arg: %d\n", csi->argv[i]);
    606 				dump_csi(csi);
    607 			}
    608 		}
    609 	}
    610 }
    611 
    612 static void
    613 set_scrolling_region(Term *t, CSI *csi)
    614 {
    615 	t->top = csi->argv[0]? csi->argv[0] : 0;
    616 	t->bot = csi->argv[1]? csi->argv[1] : t->size.h - 1;
    617 	CLAMP(t->top, 0, t->size.h - 1);
    618 	CLAMP(t->bot, 0, t->size.h - 1);
    619 	if (t->top > t->bot) {
    620 		i32 tmp = t->top;
    621 		t->top  = t->bot;
    622 		t->bot  = tmp;
    623 	}
    624 	cursor_move_to(t, 0, 0);
    625 }
    626 
    627 static void
    628 window_manipulation(Term *t, CSI *csi)
    629 {
    630 	const char *s;
    631 	u32 i;
    632 
    633 	switch (csi->argv[0]) {
    634 	case 22:
    635 		s = glfwGetWindowTitle(t->gl.window);
    636 		for (i = 0; i < ARRAY_COUNT(t->saved_title) - 1 && s[i]; i++)
    637 			t->saved_title[i] = s[i];
    638 		t->saved_title[i] = 0;
    639 		break;
    640 	case 23: glfwSetWindowTitle(t->gl.window, t->saved_title); break;
    641 	default:
    642 		fprintf(stderr, "unhandled xtwinops: %d\n", csi->argv[0]);
    643 		dump_csi(csi);
    644 	}
    645 }
    646 
    647 static void
    648 push_newline(Term *t, b32 move_to_first_col)
    649 {
    650 	i32 row = t->cursor.pos.y;
    651 	if (row == t->bot && t->scroll_offset == 0)
    652 		fb_scroll_up(t, t->top, 1);
    653 	else
    654 		row++;
    655 	cursor_move_to(t, row, move_to_first_col? 0 : t->cursor.pos.x);
    656 }
    657 
    658 static void
    659 push_tab(Term *t, i32 n)
    660 {
    661 	i32 n_abs   = ABS(n);
    662 	i32 n_sgn   = SGN(n);
    663 	i32 advance = n_abs * g_tabstop - (t->cursor.pos.x % g_tabstop);
    664 	cursor_step_column(t, n_sgn * advance);
    665 }
    666 
    667 static i32
    668 parse_csi(s8 *r, CSI *csi)
    669 {
    670 	if (peek(*r, 0) == '?') {
    671 		csi->priv = 1;
    672 		get_ascii(r);
    673 	}
    674 
    675 	while (r->len) {
    676 		u32 cp = get_ascii(r);
    677 		if (ISCONTROL(cp)) {
    678 			continue;
    679 		} else if (BETWEEN(cp, '0', '9')) {
    680 			csi->argv[csi->argc] *= 10;
    681 			csi->argv[csi->argc] += cp - '0';
    682 			continue;
    683 		}
    684 		csi->argc++;
    685 
    686 		if (cp != ';' || csi->argc == ESC_ARG_SIZ) {
    687 			if (cp == ';') csi->mode = get_ascii(r);
    688 			else           csi->mode = cp;
    689 			return 0;
    690 		}
    691 	}
    692 	/* NOTE: if we fell out of the loop then we ran out of characters */
    693 	return -1;
    694 }
    695 
    696 static void
    697 handle_csi(Term *t, CSI *csi)
    698 {
    699 	s8  raw = csi->raw;
    700 	i32 ret = parse_csi(&raw, csi);
    701 	ASSERT(ret != -1);
    702 
    703 	#define ORONE(x) (x)? (x) : 1
    704 
    705 	iv2 p = t->cursor.pos;
    706 
    707 	u8 next;
    708 	switch (csi->mode) {
    709 	case 'A': cursor_step_raw(t, ORONE(csi->argv[0]), -1,  0);           break;
    710 	case 'B': cursor_step_raw(t, ORONE(csi->argv[0]),  1,  0);           break;
    711 	case 'C': cursor_step_raw(t, ORONE(csi->argv[0]),  0,  1);           break;
    712 	case 'D': cursor_step_raw(t, ORONE(csi->argv[0]),  0, -1);           break;
    713 	case 'E': cursor_move_to(t, p.y + ORONE(csi->argv[0]), 0);           break;
    714 	case 'F': cursor_move_to(t, p.y - ORONE(csi->argv[0]), 0);           break;
    715 	case 'G': cursor_move_to(t, p.y, csi->argv[0] - 1);                  break;
    716 	case 'H': cursor_move_abs_to(t, csi->argv[0] - 1, csi->argv[1] - 1); break;
    717 	case 'J': erase_in_display(t, csi);                                  break;
    718 	case 'K': erase_in_line(t, csi);                                     break;
    719 	case 'L': insert_blank_lines(t, ORONE(csi->argv[0]));                break;
    720 	case 'M': erase_lines(t, ORONE(csi->argv[0]));                       break;
    721 	case 'P': erase_characters(t, ORONE(csi->argv[0]));                  break;
    722 	case 'X': erase_characters(t, ORONE(csi->argv[0]));                  break;
    723 	case 'T': insert_blank_lines(t, ORONE(csi->argv[0]));                break;
    724 	case 'Z': push_tab(t, -(ORONE(csi->argv[0])));                       break;
    725 	case 'a': cursor_step_raw(t, ORONE(csi->argv[0]),  0,  1);           break;
    726 	case 'd': cursor_move_abs_to(t, csi->argv[0] - 1, p.x);              break;
    727 	case 'e': cursor_step_raw(t, ORONE(csi->argv[0]),  1,  0);           break;
    728 	case 'f': cursor_move_abs_to(t, csi->argv[0] - 1, csi->argv[1] - 1); break;
    729 	case 'h': set_mode(t, csi, 1);                                       break;
    730 	case 'l': set_mode(t, csi, 0);                                       break;
    731 	case 'm': set_colours(t, csi);                                       break;
    732 	case 'r':
    733 		if (csi->priv)
    734 			goto unknown;
    735 		set_scrolling_region(t, csi);
    736 		cursor_move_abs_to(t, 0, 0);
    737 		break;
    738 	case 't': window_manipulation(t, csi);                           break;
    739 	case '!':
    740 		next = get_ascii(&raw);
    741 		if (next == 'p') {
    742 			/* NOTE: DECSTR: soft terminal reset IGNORED */
    743 			break;
    744 		}
    745 		/* FALLTHROUGH */
    746 	default:
    747 	unknown:
    748 		os_write_err_msg(s8("unknown csi: "));
    749 		dump_csi(csi);
    750 	}
    751 }
    752 
    753 static i32
    754 parse_osc(s8 *raw, OSC *osc)
    755 {
    756 	*osc          = (OSC){0};
    757 	osc->raw.data = raw->data;
    758 
    759 	/* NOTE: parse command then store the rest as a string */
    760 	u32 cp;
    761 	while (raw->len) {
    762 		cp = get_ascii(raw);
    763 		osc->raw.len++;
    764 		if (!BETWEEN(cp, '0', '9'))
    765 			break;
    766 		osc->cmd *= 10;
    767 		osc->cmd += cp - '0';
    768 
    769 		/* TODO: Performance? */
    770 		/* NOTE: The maximum OSC in xterm is 119 so if this
    771 		 * exceeds that the whole sequence is malformed */
    772 		if (osc->cmd > 1000)
    773 			break;
    774 	}
    775 
    776 	if (cp != ';' || osc->cmd > 1000)
    777 		os_die("parse_osc: malformed\n");
    778 
    779 	osc->arg.data = raw->data;
    780 	while (raw->len) {
    781 		cp = get_ascii(raw);
    782 		osc->raw.len++;
    783 		if (cp == '\a')
    784 			return 0;
    785 		if (cp == 0x1B && peek(*raw, 0) == '\\') {
    786 			get_ascii(raw);
    787 			osc->raw.len++;
    788 			return 0;
    789 		}
    790 		osc->arg.len++;
    791 	}
    792 	/* NOTE: if we fell out of the loop then we ran out of characters */
    793 	return -1;
    794 }
    795 
    796 static void
    797 reset_csi(CSI *csi, s8 *raw)
    798 {
    799 	*csi          = (CSI){0};
    800 	csi->raw.data = raw->data;
    801 }
    802 
    803 static void
    804 dump_osc(OSC *osc)
    805 {
    806 	os_write_err_msg(s8("ESC]"));
    807 	for (size i = 0; i < osc->raw.len; i++) {
    808 		u8 cp = osc->raw.data[i];
    809 		if (ISPRINT(cp))
    810 			os_write_err_msg((s8){.len = 1, .data = osc->raw.data + i});
    811 		else if (cp == '\n')
    812 			os_write_err_msg(s8("\\n"));
    813 		else if (cp == '\r')
    814 			os_write_err_msg(s8("\\r"));
    815 		else if (cp == '\a')
    816 			os_write_err_msg(s8("\\a"));
    817 		else
    818 			fprintf(stderr, "\\x%02X", cp);
    819 	}
    820 	fprintf(stderr, "\n\t.cmd = %d, .arg = {.len = %zd}\n", osc->cmd, osc->arg.len);
    821 }
    822 
    823 static void
    824 handle_osc(Term *t, s8 *raw, Arena a)
    825 {
    826 	OSC osc;
    827 	i32 ret = parse_osc(raw, &osc);
    828 	ASSERT(ret != -1);
    829 
    830 	switch (osc.cmd) {
    831 	case  0: set_window_title(t->gl.window, a, osc.arg); break;
    832 	case  1: /* IGNORED: set icon name */                break;
    833 	case  2: set_window_title(t->gl.window, a, osc.arg); break;
    834 	default:
    835 		os_write_err_msg(s8("unhandled osc cmd: "));
    836 		dump_osc(&osc);
    837 		break;
    838 	}
    839 }
    840 
    841 static void
    842 handle_escape(Term *t, s8 *raw, Arena a)
    843 {
    844 	u32 cp = get_ascii(raw);
    845 	switch (cp) {
    846 	case '[': reset_csi(&t->csi, raw); t->escape |= EM_CSI; break;
    847 	case ']': handle_osc(t, raw, a); break;
    848 	case '(': /* GZD4 -- set primary charset G0 */
    849 	case ')': /* G1D4 -- set secondary charset G1 */
    850 	case '*': /* G2D4 -- set tertiary charset G2 */
    851 	case '+': /* G3D4 -- set quaternary charset G3 */
    852 	case '%': /* utf-8 mode */
    853 		get_ascii(raw);
    854 		break;
    855 	case '=': /* DECPAM -- application keypad */
    856 	case '>': /* DECPNM -- normal keypad mode */
    857 		/* TODO: MODE_APPKEYPAD */
    858 		break;
    859 	case 'c': /* RIS -- Reset to Initial State */
    860 		term_reset(t);
    861 		break;
    862 	case 'D': /* IND -- Linefeed */
    863 		push_newline(t, 0);
    864 		break;
    865 	case 'E': /* NEL -- Next Line */
    866 		push_newline(t, 1);
    867 		break;
    868 	case 'M': /* RI  -- Reverse Index */
    869 		if (t->cursor.pos.y == t->top) {
    870 			fb_scroll_down(t, t->top, 1);
    871 		} else {
    872 			cursor_move_to(t, t->cursor.pos.y - 1, t->cursor.pos.x);
    873 		}
    874 		break;
    875 	case '7': /* DECSC: Save Cursor */
    876 		cursor_alt(t, 1);
    877 		break;
    878 	case '8': /* DECRC: Restore Cursor */
    879 		cursor_alt(t, 0);
    880 		break;
    881 	default:
    882 		fprintf(stderr, "unknown escape sequence: ESC %c (0x%02x)\n", cp, cp);
    883 		break;
    884 	}
    885 }
    886 
    887 static void
    888 push_control(Term *t, s8 *line, u32 cp, Arena a)
    889 {
    890 	switch (cp) {
    891 	case 0x1B: handle_escape(t, line, a);          break;
    892 	case '\r': t->cursor.pos.x = 0;                break;
    893 	case '\n': push_newline(t, t->mode & TM_CRLF); break;
    894 	case '\t': push_tab(t, 1);                     break;
    895 	case '\a': /* TODO: ding ding? */              break;
    896 	case '\b':
    897 		cursor_move_to(t, t->cursor.pos.y, t->cursor.pos.x - 1);
    898 		break;
    899 	}
    900 	if (cp != 0x1B && t->escape & EM_CSI) {
    901 		t->csi.raw.len++;
    902 	}
    903 }
    904 
    905 enum escape_moves_cursor_result {
    906 	EMC_NORMAL_RETURN,
    907 	EMC_NEEDS_MORE_BYTES,
    908 	EMC_CURSOR_MOVED,
    909 	EMC_SWAPPED_SCREEN,
    910 };
    911 
    912 static enum escape_moves_cursor_result
    913 validate_osc(Term *t, s8 *raw)
    914 {
    915 	enum escape_moves_cursor_result result = EMC_NORMAL_RETURN;
    916 	OSC osc;
    917 	if (parse_osc(raw, &osc) == -1)
    918 		return EMC_NEEDS_MORE_BYTES;
    919 	return result;
    920 }
    921 
    922 static enum escape_moves_cursor_result
    923 check_if_csi_moves_cursor(Term *t, s8 *raw)
    924 {
    925 	enum escape_moves_cursor_result result = EMC_NORMAL_RETURN;
    926 	CSI csi = {0};
    927 	if (parse_csi(raw, &csi) == -1)
    928 		return EMC_NEEDS_MORE_BYTES;
    929 
    930 	i32 mode = t->mode & TM_ALTSCREEN;
    931 	switch (csi.mode) {
    932 	case 'A': result = EMC_CURSOR_MOVED; break;
    933 	case 'B': result = EMC_CURSOR_MOVED; break;
    934 	case 'C': result = EMC_CURSOR_MOVED; break;
    935 	case 'D': result = EMC_CURSOR_MOVED; break;
    936 	case 'E': result = EMC_CURSOR_MOVED; break;
    937 	case 'F': result = EMC_CURSOR_MOVED; break;
    938 	case 'G': result = EMC_CURSOR_MOVED; break;
    939 	case 'H': result = EMC_CURSOR_MOVED; break;
    940 	case 'Z': result = EMC_CURSOR_MOVED; break;
    941 	case 'a': result = EMC_CURSOR_MOVED; break;
    942 	case 'd': result = EMC_CURSOR_MOVED; break;
    943 	case 'e': result = EMC_CURSOR_MOVED; break;
    944 	case 'f': result = EMC_CURSOR_MOVED; break;
    945 	case 'h': set_mode(t, &csi, 1);      break;
    946 	case 'l': set_mode(t, &csi, 0);      break;
    947 	case 'm': set_colours(t, &csi);      break;
    948 	default:  break;
    949 	}
    950 
    951 	if (mode != (t->mode & TM_ALTSCREEN))
    952 		result = EMC_SWAPPED_SCREEN;
    953 
    954 	return result;
    955 }
    956 
    957 static enum escape_moves_cursor_result
    958 check_if_escape_moves_cursor(Term *t, s8 *raw)
    959 {
    960 	enum escape_moves_cursor_result result = EMC_NORMAL_RETURN;
    961 	if (raw->len < 2)
    962 		return EMC_NEEDS_MORE_BYTES;
    963 	u32 cp = get_ascii(raw);
    964 	switch(cp) {
    965 	case '[':
    966 		result = check_if_csi_moves_cursor(t, raw);
    967 		break;
    968 	case ']':
    969 		result = validate_osc(t, raw);
    970 		break;
    971 	case '(': /* GZD4 -- set primary charset G0 */
    972 	case ')': /* G1D4 -- set secondary charset G1 */
    973 	case '*': /* G2D4 -- set tertiary charset G2 */
    974 	case '+': /* G3D4 -- set quaternary charset G3 */
    975 	case '%': /* utf-8 mode */
    976 		get_ascii(raw);
    977 		break;
    978 	case 'c': /* RIS -- Reset to Initial State */
    979 		result = EMC_CURSOR_MOVED;
    980 		break;
    981 	case 'D': /* IND -- Linefeed */
    982 	case 'E': /* NEL -- Next Line */
    983 		result = EMC_CURSOR_MOVED;
    984 		break;
    985 	case 'M': /* RI -- Reverse Index */
    986 		if (t->cursor.pos.y != 0)
    987 			result = EMC_CURSOR_MOVED;
    988 		break;
    989 	case '7': break;
    990 	case '8':
    991 		if (!equal_iv2(t->cursor.pos, t->saved_cursors[t->view_idx].pos))
    992 			result = EMC_CURSOR_MOVED;
    993 		break;
    994 	default: break;
    995 	}
    996 	return result;
    997 }
    998 
    999 static size
   1000 split_raw_input_to_lines(Term *t, s8 raw)
   1001 {
   1002 	TermView *tv = t->views + t->view_idx;
   1003 	size parsed_lines = 0;
   1004 	__m128i nl  = _mm_set1_epi8('\n');
   1005 	__m128i esc = _mm_set1_epi8(0x1B);
   1006 	__m128i uni = _mm_set1_epi8(0x80);
   1007 
   1008 	#define SPLIT_LONG 4096L
   1009 	while (raw.len) {
   1010 		__m128i hasutf8 = _mm_setzero_si128();
   1011 		size count = raw.len > SPLIT_LONG ? SPLIT_LONG : raw.len;
   1012 		u8 *data = raw.data;
   1013 		while (count >= 16) {
   1014 			__m128i vdat    = _mm_loadu_si128((__m128i_u *)data);
   1015 			__m128i hasnl   = _mm_cmpeq_epi8(vdat, nl);
   1016 			__m128i hasesc  = _mm_cmpeq_epi8(vdat, esc);
   1017 			__m128i hasuni  = _mm_and_si128(vdat,  uni);
   1018 			__m128i hasproc = _mm_or_si128(hasuni, _mm_or_si128(hasnl,  hasesc));
   1019 			i32 needsproc   = _mm_movemask_epi8(hasproc);
   1020 
   1021 			if (needsproc) {
   1022 				u32 advance = _tzcnt_u32(needsproc);
   1023 				__m128i utf8mask = _mm_loadu_si128((__m128i_u *)(utf8overhangmask + 16 - advance));
   1024 				hasuni  = _mm_and_si128(utf8mask, hasuni);
   1025 				hasutf8 = _mm_or_si128(hasutf8, hasuni);
   1026 				count -= advance;
   1027 				data  += advance;
   1028 				break;
   1029 			}
   1030 
   1031 			hasutf8 = _mm_or_si128(hasutf8, hasuni);
   1032 			count  -= 16;
   1033 			data   += 16;
   1034 		}
   1035 		tv->lines.buf[tv->lines.widx].has_unicode |= _mm_movemask_epi8(hasutf8);
   1036 		raw = consume(raw, data - raw.data);
   1037 
   1038 		if (raw.len) {
   1039 			u32 cp = peek(raw, 0);
   1040 			if (cp == 0x1B) {
   1041 				s8 old = raw;
   1042 				raw = consume(raw, 1);
   1043 				switch (check_if_escape_moves_cursor(t, &raw)) {
   1044 				case EMC_NEEDS_MORE_BYTES:
   1045 					t->unprocessed_bytes = old.len;
   1046 					return parsed_lines;
   1047 				case EMC_CURSOR_MOVED:
   1048 					if (line_length(tv->lines.buf + tv->lines.widx)) {
   1049 						parsed_lines++;
   1050 						feed_line(&tv->lines, raw.data, t->cursor.style);
   1051 					}
   1052 					break;
   1053 				case EMC_SWAPPED_SCREEN:
   1054 					parsed_lines++;
   1055 					feed_line(&tv->lines, old.data, t->cursor.style);
   1056 					TermView *nv = t->views + t->view_idx;
   1057 					size nstart  = nv->log.widx;
   1058 					mem_copy(raw, (s8){nv->log.cap, nv->log.buf + nstart});
   1059 					commit_to_rb(tv, -raw.len);
   1060 					commit_to_rb(nv,  raw.len);
   1061 					raw.data = nv->log.buf + nstart;
   1062 					init_line(nv->lines.buf + nv->lines.widx, raw.data, t->cursor.style);
   1063 					tv = nv;
   1064 					break;
   1065 				default: break;
   1066 				}
   1067 			} else if (cp == '\n') {
   1068 				raw = consume(raw, 1);
   1069 				parsed_lines++;
   1070 				feed_line(&tv->lines, raw.data, t->cursor.style);
   1071 			} else if (cp & 0x80) {
   1072 				tv->lines.buf[tv->lines.widx].has_unicode = 1;
   1073 				/* TODO: this is probably slow */
   1074 				size old_len = raw.len;
   1075 				if (get_utf8(&raw) == (u32)-1) {
   1076 					/* NOTE: Need More Bytes! */
   1077 					t->unprocessed_bytes = old_len;
   1078 					return parsed_lines;
   1079 				}
   1080 			} else {
   1081 				raw = consume(raw, 1);
   1082 			}
   1083 		}
   1084 
   1085 		tv->lines.buf[tv->lines.widx].end = raw.data;
   1086 
   1087 		if (line_length(tv->lines.buf + tv->lines.widx) > SPLIT_LONG) {
   1088 			parsed_lines++;
   1089 			feed_line(&tv->lines, raw.data, t->cursor.style);
   1090 		}
   1091 	}
   1092 	t->unprocessed_bytes = 0;
   1093 	return parsed_lines;
   1094 }
   1095 
   1096 static void
   1097 push_line(Term *t, Line *line, Arena a)
   1098 {
   1099 	TermView *tv    = t->views + t->view_idx;
   1100 	s8 l            = line_to_s8(line, &tv->log);
   1101 	t->cursor.style = line->cursor_state;
   1102 
   1103 	Cell *c;
   1104 	while (l.len) {
   1105 		u32 cp;
   1106 		if (line->has_unicode) cp = get_utf8(&l);
   1107 		else                   cp = get_ascii(&l);
   1108 
   1109 		/* TODO: handle error case */
   1110 		ASSERT(cp != (u32)-1);
   1111 
   1112 		if (ISCONTROL(cp)) {
   1113 			push_control(t, &l, cp, a);
   1114 			continue;
   1115 		} else if (t->escape & EM_CSI) {
   1116 			t->csi.raw.len++;
   1117 			if (BETWEEN(cp, '@', '~')) {
   1118 				handle_csi(t, &t->csi);
   1119 				t->escape &= ~EM_CSI;
   1120 			}
   1121 			continue;
   1122 		}
   1123 
   1124 		if (t->mode & TM_AUTO_WRAP && t->cursor.state & CURSOR_WRAP_NEXT)
   1125 			push_newline(t, 1);
   1126 
   1127 		i32 width;
   1128 		if (line->has_unicode) {
   1129 			width = wcwidth(cp);
   1130 			ASSERT(width != -1);
   1131 		} else {
   1132 			width = 1;
   1133 		}
   1134 
   1135 		if (t->cursor.pos.x + width > t->size.w) {
   1136 			/* NOTE: make space for character if mode enabled else
   1137 			 * clobber whatever was on the end of the line */
   1138 			if (t->mode & TM_AUTO_WRAP)
   1139 				push_newline(t, 1);
   1140 			else
   1141 				cursor_move_to(t, t->cursor.pos.y, t->size.w - width);
   1142 		}
   1143 
   1144 		c = &tv->fb.rows[t->cursor.pos.y][t->cursor.pos.x];
   1145 		c->cp    = cp;
   1146 		c->style = t->cursor.style;
   1147 
   1148 		if (width == 2) {
   1149 			c->style.attr |= ATTR_WIDE;
   1150 			if (t->cursor.pos.x + 1 < t->size.w) {
   1151 				Cell *nc = c + 1;
   1152 				nc->style.attr |= ATTR_WDUMMY;
   1153 			}
   1154 		}
   1155 
   1156 		if (t->cursor.pos.x + width < t->size.w)
   1157 			cursor_step_column(t, width);
   1158 		else
   1159 			t->cursor.state |= CURSOR_WRAP_NEXT;
   1160 
   1161 		if (is_selected(&t->selection, t->cursor.pos.x, t->cursor.pos.y))
   1162 			selection_clear(&t->selection);
   1163 	}
   1164 }
   1165 
   1166 static size
   1167 get_line_idx(LineBuf *lb, size off)
   1168 {
   1169 	ASSERT(-off <= lb->filled);
   1170 	size result = lb->widx + off;
   1171 	if (result < 0)
   1172 		result += lb->filled;
   1173 	return result;
   1174 }
   1175 
   1176 static void
   1177 blit_lines(Term *t, Arena a, size line_count)
   1178 {
   1179 	TermView *tv = t->views + t->view_idx;
   1180 
   1181 	if (t->gl.flags & NEEDS_FULL_REFILL) {
   1182 		term_reset(t);
   1183 		line_count = t->size.h - 1;
   1184 	}
   1185 
   1186 	size off = t->scroll_offset;
   1187 	CLAMP(line_count, 0, tv->lines.filled);
   1188 	for (size idx = -line_count; idx <= 0; idx++) {
   1189 		size line_idx = get_line_idx(&tv->lines, idx - off);
   1190 		if (line_idx == tv->last_line_idx)
   1191 			t->cursor.pos = tv->last_cursor_pos;
   1192 		tv->last_cursor_pos = t->cursor.pos;
   1193 		tv->last_line_idx   = line_idx;
   1194 		push_line(t, tv->lines.buf + line_idx, a);
   1195 		/* TODO: can we avoid this? */
   1196 		ASSERT(t->escape == 0);
   1197 	}
   1198 
   1199 	t->gl.flags &= ~(NEEDS_FULL_REFILL|NEEDS_REFILL);
   1200 	t->gl.flags |= NEEDS_BLIT;
   1201 }