vtgl

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

vtgl.c (26616B)


      1 /* See LICENSE for copyright details */
      2 #define GL_GLEXT_PROTOTYPES 1
      3 #include <GL/glcorearb.h>
      4 #include <GL/glext.h>
      5 #include <GLFW/glfw3.h>
      6 
      7 #include "util.h"
      8 #include "config.h"
      9 
     10 #include "font.c"
     11 #include "terminal.c"
     12 #ifdef _DEBUG
     13 #include "debug.c"
     14 #endif
     15 
     16 #define REVERSE_VIDEO_MASK (Colour){.r = 0xff, .g = 0xff, .b = 0xff}.rgba
     17 
     18 #define TEXTURE_GLYPH_COUNT PUSH_BUFFER_CAP
     19 
     20 #define X(name) "u_"#name,
     21 static char *render_uniform_names[] = { GL_RENDER_UNIFORMS };
     22 static char *post_uniform_names[]   = { GL_POST_UNIFORMS };
     23 #undef X
     24 static_assert(ARRAY_COUNT(render_uniform_names) == ARRAY_COUNT(((GLCtx *)0)->render.uniforms),
     25               "GLCtx.render.uniforms must be same length as GL_RENDER_UNIFORMS\n");
     26 static_assert(ARRAY_COUNT(post_uniform_names) == ARRAY_COUNT(((GLCtx *)0)->post.uniforms),
     27               "GLCtx.post.uniforms must be same length as GL_POST_UNIFORMS\n");
     28 
     29 static v4
     30 normalized_colour(Colour c)
     31 {
     32 	return (v4){.r = c.r / 255.0f, .g = c.g / 255.0f, .b = c.b / 255.0f, .a = c.a / 255.0f};
     33 }
     34 
     35 static void
     36 clear_colour(b32 reverse)
     37 {
     38 	Colour c = g_colours.data[g_colours.bgidx];
     39 	if (reverse) c.rgba ^= REVERSE_VIDEO_MASK;
     40 	v4 cc = normalized_colour(c);
     41 	glClearColor(cc.r, cc.g, cc.b, cc.a);
     42 	glClear(GL_COLOR_BUFFER_BIT);
     43 }
     44 
     45 static void
     46 set_projection_matrix(GLCtx *gl)
     47 {
     48 	f32 w = gl->window_size.w;
     49 	f32 h = gl->window_size.h;
     50 
     51 	f32 pmat[4 * 4] = {
     52 		2.0/w,   0.0,      0.0,    -1.0,
     53 		0.0,     2.0/h,    0.0,    -1.0,
     54 		0.0,     0.0,     -1.0,     0.0,
     55 		0.0,     0.0,      0.0,     1.0,
     56 	};
     57 
     58 	glUseProgram(gl->programs[SHADER_RENDER]);
     59 	glUniformMatrix4fv(gl->render.Pmat, 1, GL_TRUE, pmat);
     60 	glUseProgram(gl->programs[SHADER_POST]);
     61 	glUniformMatrix4fv(gl->post.Pmat, 1, GL_TRUE, pmat);
     62 }
     63 
     64 static v2
     65 get_cell_size(Term *t)
     66 {
     67 	v2 result = {.w = t->fa.size.w + g_cell_pad.w, .h = t->fa.size.h + g_cell_pad.h};
     68 	return result;
     69 }
     70 
     71 static v2
     72 get_cell_pad_off(void)
     73 {
     74 	v2 result = {.w = 0.5 * g_cell_pad.w, .h = 0.5 * g_cell_pad.h};
     75 	return result;
     76 }
     77 
     78 static v2
     79 get_occupied_size(Term *t)
     80 {
     81 	v2 cs     = get_cell_size(t);
     82 	v2 result = {.x = t->size.w * cs.w, .y = t->size.h * cs.h};
     83 	return result;
     84 }
     85 
     86 static v2
     87 get_terminal_top_left(Term *t)
     88 {
     89 	/* NOTE: There is trade-off here: you can center the usable area which looks good but
     90 	 * causes the contents to jump around when resizing; or you can only consider the global
     91 	 * padding and have the contents fixed while resizing. We are choosing the former! */
     92 	/* IMPORTANT: Assume the glyphs already have subpixel rendering so cells must be aligned
     93 	 * on pixels. No harm if they don't but if they do and we don't align on pixels they
     94 	 * will look like crap. */
     95 	v2 os     = get_occupied_size(t);
     96 	v2 delta  = {.x = t->gl.window_size.w - os.w, .y = t->gl.window_size.h - os.h};
     97 	v2 result = {.x = (u32)(delta.x / 2), .y = (u32)(t->gl.window_size.h - delta.y / 2)};
     98 	return result;
     99 }
    100 
    101 static v2
    102 get_terminal_bot_left(Term *t)
    103 {
    104 	v2 os     = get_occupied_size(t);
    105 	v2 delta  = {.x = t->gl.window_size.w - os.w, .y = t->gl.window_size.h - os.h};
    106 	v2 result = {.x = delta.x / 2, .y = delta.y / 2};
    107 	return result;
    108 }
    109 
    110 static void
    111 resize(Term *t)
    112 {
    113 	v2 ws  = t->gl.window_size;
    114 	ws.w  -= 2 * g_term_pad.w;
    115 	ws.h  -= 2 * g_term_pad.h;
    116 
    117 	uv2 old_size = t->size;
    118 	v2 cs        = get_cell_size(t);
    119 	t->size.w    = (u32)(ws.w / cs.w);
    120 	t->size.h    = (u32)(ws.h / cs.h);
    121 
    122 	if (!equal_uv2(old_size, t->size)) {
    123 		os_alloc_framebuffer(&t->views[0].fb, t->size.h, t->size.w);
    124 		os_alloc_framebuffer(&t->views[1].fb, t->size.h, t->size.w);
    125 		t->gl.flags |= NEEDS_FULL_REFILL;
    126 	}
    127 
    128 	os_set_term_size(t->child, t->size.h, t->size.w, ws.w, ws.h);
    129 	t->gl.flags &= ~NEEDS_RESIZE;
    130 }
    131 
    132 static void
    133 update_font_textures(GLCtx *gl, FontAtlas *fa)
    134 {
    135 	glActiveTexture(GL_TEXTURE0);
    136 	glBindTexture(GL_TEXTURE_2D_ARRAY, gl->glyph_tex);
    137 	glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8,
    138 	             MAX_FONT_SIZE, MAX_FONT_SIZE, TEXTURE_GLYPH_COUNT,
    139 	             0, GL_RED, GL_UNSIGNED_BYTE, 0);
    140 
    141 	font_atlas_update(fa);
    142 }
    143 
    144 static void
    145 update_uniforms(Term *t, enum shader_stages stage)
    146 {
    147 	switch (stage) {
    148 	case SHADER_RENDER:
    149 		for (u32 i = 0; i < ARRAY_COUNT(t->gl.render.uniforms); i++) {
    150 			t->gl.render.uniforms[i] = glGetUniformLocation(t->gl.programs[stage],
    151 			                                                render_uniform_names[i]);
    152 			//fprintf(stderr, "uniform (RENDER): %s; id %d\n",
    153 			//        render_uniform_names[i], t->gl.render.uniforms[i]);
    154 		}
    155 		t->gl.flags &= ~UPDATE_RENDER_UNIFORMS;
    156 		break;
    157 	case SHADER_POST:
    158 		for (u32 i = 0; i < ARRAY_COUNT(t->gl.post.uniforms); i++) {
    159 			t->gl.post.uniforms[i] = glGetUniformLocation(t->gl.programs[stage],
    160 			                                              post_uniform_names[i]);
    161 			//fprintf(stderr, "uniform (POST): %s; id %d\n",
    162 			//        post_uniform_names[i], t->gl.post.uniforms[i]);
    163 		}
    164 		t->gl.flags &= ~UPDATE_POST_UNIFORMS;
    165 		break;
    166 	case SHADER_LAST: ASSERT(0); break;
    167 	}
    168 
    169 	set_projection_matrix(&t->gl);
    170 	/* TODO: this doesn't need to be called so often */
    171 	update_font_textures(&t->gl, &t->fa);
    172 }
    173 
    174 static i32
    175 get_gpu_glyph_index(Arena a, FontAtlas *fa, u32 codepoint, enum face_style style, Glyph *out_glyph)
    176 {
    177 	u32 depth_idx;
    178 	CachedGlyph *cg;
    179 	u32 *data  = render_glyph(&a, fa, codepoint, style, &cg, &depth_idx);
    180 	*out_glyph = cg->g;
    181 	if (data) {
    182 		ASSERT(depth_idx);
    183 		glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, depth_idx,
    184 		                out_glyph->size.w, out_glyph->size.h, 1, GL_RGBA,
    185 		                GL_UNSIGNED_BYTE, data);
    186 		cg->uploaded_to_gpu = 1;
    187 	}
    188 	ASSERT(cg->uploaded_to_gpu);
    189 	return depth_idx;
    190 }
    191 
    192 static v2
    193 measure_text(Arena a, FontAtlas *fa, s8 text, b32 monospaced)
    194 {
    195 	v2  result = {0};
    196 	Glyph g;
    197 	get_gpu_glyph_index(a, fa, ' ', FS_NORMAL, &g);
    198 	f32 single_space_width = g.size.w;
    199 	for (size i = 0; i < text.len; i++) {
    200 		get_gpu_glyph_index(a, fa, text.data[i], FS_NORMAL, &g);
    201 		/* TODO: should we consider offset characters in y? */
    202 		if (g.size.h > result.y)
    203 			result.y = g.size.h;
    204 		result.x += monospaced? single_space_width : g.size.w;
    205 	}
    206 	return result;
    207 }
    208 
    209 static void
    210 flush_render_push_buffer(RenderPushBuffer *rpb, GLCtx *gl)
    211 {
    212 	glUniform2fv(gl->render.vertscale,  rpb->count, (f32 *)rpb->vertscales);
    213 	glUniform2fv(gl->render.vertoff,    rpb->count, (f32 *)rpb->vertoffsets);
    214 	glUniform2fv(gl->render.texscale,   rpb->count, (f32 *)rpb->texscales);
    215 	glUniform2uiv(gl->render.texcolour, rpb->count, (u32 *)rpb->texcolours);
    216 	glUniform1iv(gl->render.charmap,    rpb->count, (i32 *)rpb->charmap);
    217 	glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, rpb->count);
    218 	rpb->count = 0;
    219 }
    220 
    221 static u32
    222 get_render_push_buffer_idx(RenderPushBuffer *rpb, GLCtx *gl, u32 count)
    223 {
    224 	if (rpb->count + count > PUSH_BUFFER_CAP)
    225 		flush_render_push_buffer(rpb, gl);
    226 	u32 result  = rpb->count;
    227 	rpb->count += count;
    228 	return result;
    229 }
    230 
    231 static void
    232 push_char(RenderPushBuffer *rpb, GLCtx *gl, v2 vertscale, v2 vertoff, v2 texscale,
    233           uv2 colours, i32 char_idx)
    234 {
    235 	u32 idx = get_render_push_buffer_idx(rpb, gl, 1);
    236 	rpb->vertscales[idx]  = vertscale;
    237 	rpb->vertoffsets[idx] = vertoff;
    238 	rpb->texscales[idx]   = texscale;
    239 	rpb->texcolours[idx]  = colours;
    240 	rpb->charmap[idx]     = char_idx;
    241 }
    242 
    243 static void
    244 push_empty_cell_rect(RenderPushBuffer *rpb, Term *t, u32 minrow, u32 maxrow, u32 mincol, u32 maxcol)
    245 {
    246 	ASSERT(minrow <= maxrow && mincol <= maxcol);
    247 	ASSERT(maxrow <= t->size.h && maxcol <= t->size.w);
    248 
    249 	v2 cs   = get_cell_size(t);
    250 	v2 size = {.x = (maxcol - mincol + 1) * cs.w, .y = (maxrow - minrow + 1) * cs.h};
    251 	v2 pos  = {.x = mincol * cs.w, .y = t->gl.window_size.h - cs.h * (maxrow + 1)};
    252 
    253 	Colour colour = g_colours.data[g_colours.fgidx];
    254 	push_char(rpb, &t->gl, size, pos, (v2){0}, (uv2){.y = colour.rgba}, 0);
    255 }
    256 
    257 static void
    258 draw_text(RenderPushBuffer *rpb, GLCtx *gl, Arena a, FontAtlas *fa, s8 text, v2 position, Colour colour, b32 monospaced)
    259 {
    260 	Glyph g;
    261 	get_gpu_glyph_index(a, fa, ' ', FS_NORMAL, &g);
    262 	f32 single_space_width = g.size.w;
    263 	for (size i = 0; i < text.len; i++) {
    264 		u32 cp = text.data[i];
    265 		i32 glyph_idx = get_gpu_glyph_index(a, fa, cp, FS_NORMAL, &g);
    266 		v2 texscale   = {.x = g.size.w / MAX_FONT_SIZE, .y = g.size.h / MAX_FONT_SIZE};
    267 		v2 vertscale  = {.x = g.size.w, .y = g.size.h};
    268 		v2 vertoff    = {.x = position.x + g.delta.x, .y = position.y + g.delta.y};
    269 		push_char(rpb, gl, vertscale, vertoff, texscale, (uv2){.x = colour.rgba}, glyph_idx);
    270 		position.x += monospaced? single_space_width : g.size.w;
    271 	}
    272 }
    273 
    274 static void
    275 draw_rectangle(RenderPushBuffer *rpb, GLCtx *gl, Rect r, Colour colour)
    276 {
    277 	push_char(rpb, gl, r.size, r.pos, (v2){0}, (uv2){.y = colour.rgba}, 0);
    278 }
    279 
    280 static void
    281 push_cell(RenderPushBuffer *rpb, GLCtx *gl, Arena a, FontAtlas *fa, Cell c, Rect r, v2 cell_font_delta)
    282 {
    283 	u32 idx = get_render_push_buffer_idx(rpb, gl, 2);
    284 
    285 	ASSERT(c.cp);
    286 	Glyph g;
    287 	i32 style = FS_NORMAL;
    288 	/* TODO: performance */
    289 	if ((c.style.attr & (ATTR_BOLD|ATTR_ITALIC)) == (ATTR_BOLD|ATTR_ITALIC))
    290 		style = FS_BOLD_ITALIC;
    291 	else if (c.style.attr & ATTR_BOLD)
    292 		style = FS_BOLD;
    293 	else if (c.style.attr & ATTR_ITALIC)
    294 		style = FS_ITALIC;
    295 
    296 	i32 depth_idx = get_gpu_glyph_index(a, fa, c.cp, style, &g);
    297 
    298 	rpb->vertscales[idx + 0] = r.size;
    299 	rpb->vertscales[idx + 1] = (v2){.x = g.size.w, .y = g.size.h};
    300 
    301 	rpb->vertoffsets[idx + 0] = r.pos;
    302 	rpb->vertoffsets[idx + 1] = (v2){
    303 		.x = r.pos.x + g.delta.x + cell_font_delta.x,
    304 		.y = r.pos.y + g.delta.y + cell_font_delta.y,
    305 	};
    306 
    307 	rpb->texscales[idx + 0] = (v2){0};
    308 	rpb->texscales[idx + 1] = (v2){
    309 		.x = g.size.w / (f32)MAX_FONT_SIZE,
    310 		.y = g.size.h / (f32)MAX_FONT_SIZE,
    311 	};
    312 
    313 	rpb->charmap[idx + 0] = depth_idx;
    314 	rpb->charmap[idx + 1] = depth_idx;
    315 
    316 	CellStyle cs = c.style;
    317 	if (cs.attr & ATTR_FAINT) {
    318 		if (cs.attr & ATTR_INVERSE) cs.bg.a = 0.5 * 255;
    319 		else                        cs.fg.a = 0.5 * 255;
    320 	}
    321 
    322 	u32 fg    = (cs.attr & ATTR_INVERSE)? cs.bg.rgba : cs.fg.rgba;
    323 	u32 bg    = (cs.attr & ATTR_INVERSE)? cs.fg.rgba : cs.bg.rgba;
    324 	u32 rmask = (gl->mode & WIN_MODE_REVERSE)? REVERSE_VIDEO_MASK : 0;
    325 
    326 	rpb->texcolours[idx + 0].x = fg ^ rmask;
    327 	rpb->texcolours[idx + 0].y = bg ^ rmask;
    328 	rpb->texcolours[idx + 1].x = fg ^ rmask;
    329 	rpb->texcolours[idx + 1].y = bg ^ rmask;
    330 }
    331 
    332 static void
    333 push_cell_row(RenderPushBuffer *rpb, GLCtx *gl, Arena a, FontAtlas *fa, Cell *row, u32 len,
    334               b32 inverse, Rect cr, v2 cell_font_delta)
    335 {
    336 	ASSERT(inverse == 0 || inverse == 1);
    337 	v2 cs  = cr.size;
    338 	v2 csw = {.w = cs.w * 2, .h = cs.h};
    339 	for (u32 c = 0; c < len; c++) {
    340 		Cell cell = row[c];
    341 		if (cell.style.attr & ATTR_WDUMMY)
    342 			continue;
    343 		if (cell.style.attr & ATTR_WIDE) cr.size = csw;
    344 		else                             cr.size = cs;
    345 		cell.style.attr ^= inverse * ATTR_INVERSE;
    346 		push_cell(rpb, gl, a, fa, cell, cr, cell_font_delta);
    347 		cr.pos.x += cr.size.w;
    348 	}
    349 }
    350 
    351 /* NOTE: In this program we render to an offscreen render target that is later drawn
    352  * to the screen as a full window quad. Therefore render_framebuffer must take care
    353  * to handle all necessary padding (window and cell). Outside of here everyone should
    354  * simply care about the terminal in terms of rows and columns (t->size). */
    355 static void
    356 render_framebuffer(Term *t, RenderPushBuffer *rpb)
    357 {
    358 	v2 tl   = get_terminal_top_left(t);
    359 	v2 cs   = get_cell_size(t);
    360 	Rect cr = {.pos = {.x = tl.x, .y = tl.y - cs.h}, .size = cs};
    361 
    362 	v2 cell_font_delta  = get_cell_pad_off();
    363 	cell_font_delta.y  += t->fa.baseline;
    364 
    365 	TermView *tv = t->views + t->view_idx;
    366 	/* NOTE: draw whole framebuffer */
    367 	for (u32 r = 0; r < t->size.h; r++) {
    368 		push_cell_row(rpb, &t->gl, t->arena_for_frame, &t->fa, tv->fb.rows[r], t->size.w, 0,
    369 		              cr, cell_font_delta);
    370 		cr.pos.y -= cs.h;
    371 	}
    372 
    373 	/* NOTE: draw selection if active */
    374 	/* TODO: combine with original push_cell? */
    375 	if (is_valid_range(t->selection.range)) {
    376 		Range sel = t->selection.range;
    377 		iv2 curs  = sel.start;
    378 		iv2 end   = sel.end;
    379 		cr.pos    = (v2){.x = tl.x + cs.w * curs.x, .y = tl.h - cs.h * (curs.y + 1)};
    380 		/* NOTE: do full rows first */
    381 		for (; curs.y < end.y; curs.y++) {
    382 			u32 len = t->size.w - curs.x - 1;
    383 			push_cell_row(rpb, &t->gl, t->arena_for_frame, &t->fa, tv->fb.rows[curs.y] + curs.x, len, 1,
    384 			              cr, cell_font_delta);
    385 			curs.x    = 0;
    386 			cr.pos.x  = tl.x;
    387 			cr.pos.y -= cs.h;
    388 		}
    389 		/* NOTE: do the last row */
    390 		push_cell_row(rpb, &t->gl, t->arena_for_frame, &t->fa, tv->fb.rows[curs.y] + curs.x,
    391 		              end.x - curs.x + 1, 1, cr, cell_font_delta);
    392 	}
    393 
    394 	/* NOTE: draw cursor */
    395 	if (!(t->gl.mode & WIN_MODE_HIDECURSOR) && t->scroll_offset == 0) {
    396 		iv2 curs    = t->cursor.pos;
    397 		cr.pos      = (v2){.x = tl.x + cs.w * curs.x, .y = tl.h - cs.h * (curs.y + 1)};
    398 		Cell cursor = tv->fb.rows[t->cursor.pos.y][t->cursor.pos.x];
    399 		if (cursor.style.attr & ATTR_WDUMMY) {
    400 			cursor = tv->fb.rows[t->cursor.pos.y][t->cursor.pos.x - 1];
    401 			ASSERT(cursor.style.attr & ATTR_WIDE);
    402 		}
    403 		if (cursor.style.attr & ATTR_WIDE)
    404 			cr.size.w *= 2;
    405 		cursor.style.attr ^= ATTR_INVERSE;
    406 		push_cell(rpb, &t->gl, t->arena_for_frame, &t->fa, cursor, cr, cell_font_delta);
    407 	}
    408 }
    409 
    410 static iv2
    411 mouse_to_cell_space(Term *t, v2 mouse)
    412 {
    413 	iv2 result   = {0};
    414 	v2 cell_size = get_cell_size(t);
    415 	v2 bot_left  = get_terminal_bot_left(t);
    416 
    417 	result.x = (i32)((mouse.x - bot_left.x) / cell_size.w);
    418 	result.y = (i32)((mouse.y - bot_left.y) / cell_size.h);
    419 
    420 	CLAMP(result.x, 0, t->size.w - 1);
    421 	CLAMP(result.y, 0, t->size.h - 1);
    422 
    423 	return result;
    424 }
    425 
    426 static void
    427 update_selection(Term *t)
    428 {
    429 	Selection *sel = &t->selection;
    430 	b32 held = glfwGetMouseButton(t->gl.window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS;
    431 
    432 	sel->click_param -= t->gl.dt;
    433 	if (sel->click_param < 0) {
    434 		sel->click_param = 0;
    435 		if (!held)
    436 			sel->state = SS_NONE;
    437 	}
    438 
    439 	if (!held)
    440 		return;
    441 
    442 	f64 xpos, ypos;
    443 	glfwGetCursorPos(t->gl.window, &xpos, &ypos);
    444 	v2 mouse = {.x = xpos, .y = ypos};
    445 
    446 	if (sel->state != SS_WORDS && (mouse.x == sel->mouse_start.x && mouse.y == sel->mouse_start.y))
    447 		return;
    448 
    449 	iv2 new_p = mouse_to_cell_space(t, mouse);
    450 	if (t->views[t->view_idx].fb.rows[new_p.y][new_p.x].style.attr & ATTR_WDUMMY) {
    451 		ASSERT(t->views[t->view_idx].fb.rows[new_p.y][new_p.x - 1].style.attr & ATTR_WIDE);
    452 		new_p.x--;
    453 	}
    454 	if (sel->state != SS_WORDS) {
    455 		sel->range.start = sel->anchor.start;
    456 		sel->range.end = new_p;
    457 	} else {
    458 		Range word = get_word_around_cell(t, new_p);
    459 		if (sel->anchor.start.y < word.start.y) {
    460 			sel->range.start = sel->anchor.start;
    461 			sel->range.end   = word.end;
    462 		} else if (sel->anchor.start.y > word.start.y) {
    463 			sel->range.start = word.start;
    464 			sel->range.end   = sel->anchor.end;
    465 		} else {
    466 			if (word.start.x < sel->anchor.start.x) {
    467 				sel->range.start = word.start;
    468 				sel->range.end   = sel->anchor.end;
    469 			} else {
    470 				sel->range.start = sel->anchor.start;
    471 				sel->range.end   = word.end;
    472 			}
    473 		}
    474 	}
    475 	sel->range   = normalize_range(sel->range);
    476 	t->gl.flags |= NEEDS_BLIT;
    477 }
    478 
    479 KEYBIND_FN(copy)
    480 {
    481 	if (!is_valid_range(t->selection.range))
    482 		return 1;
    483 
    484 	TermView *tv = t->views + t->view_idx;
    485 	Range sel    = t->selection.range;
    486 	iv2 curs     = sel.start;
    487 	iv2 end      = sel.end;
    488 	i32 buf_curs = 0;
    489 
    490 	/* NOTE: super piggy but we are only holding onto it for the function duration */
    491 	Arena arena    = t->arena_for_frame;
    492 	size  buf_size = 1 * MEGABYTE;
    493 	char *buf      = alloc(&arena, char, buf_size);
    494 
    495 	/* NOTE: do full rows first */
    496 	u32 last_non_space_idx = 0;
    497 	for (; curs.y < end.y; curs.y++) {
    498 		for (; curs.x < t->size.w && buf_curs != buf_size; curs.x++) {
    499 			Cell c = tv->fb.rows[curs.y][curs.x];
    500 			if (c.style.attr & ATTR_WDUMMY)
    501 				continue;
    502 			if (!ISSPACE(c.cp))
    503 				last_non_space_idx = buf_curs;
    504 			s8 enc = utf8_encode(c.cp);
    505 			for (size i = 0; i < enc.len && buf_curs != buf_size; i++)
    506 				buf[buf_curs++] = enc.data[i];
    507 		}
    508 		buf[last_non_space_idx + 1] = '\n';
    509 		buf_curs = last_non_space_idx + 2;
    510 		curs.x = 0;
    511 	}
    512 
    513 	/* NOTE: do the last row */
    514 	for (; curs.x <= end.x && buf_curs != buf_size; curs.x++) {
    515 		Cell c = tv->fb.rows[curs.y][curs.x];
    516 		if (c.style.attr & ATTR_WDUMMY)
    517 			continue;
    518 		if (!ISSPACE(c.cp))
    519 			last_non_space_idx = buf_curs;
    520 		s8 enc = utf8_encode(c.cp);
    521 		for (size i = 0; i < enc.len && buf_curs != buf_size; i++)
    522 			buf[buf_curs++] = enc.data[i];
    523 	}
    524 
    525 	CLAMP(buf_curs, 0, buf_size - 1);
    526 	buf[buf_curs] = 0;
    527 	glfwSetClipboardString(0, buf);
    528 
    529 	return 1;
    530 }
    531 
    532 KEYBIND_FN(paste)
    533 {
    534 	s8 text = {.data = (u8 *)glfwGetClipboardString(0)};
    535 	b32 bracketed = t->gl.mode & WIN_MODE_BRACKPASTE;
    536 	/* TODO: we may need to replace '\n' with '\r' */
    537 	if (text.data) {
    538 		for (u8 *t = text.data; *t; t++)
    539 			text.len++;
    540 		if (bracketed) os_child_put_s8(t->child, s8("\033[200~"));
    541 		os_child_put_s8(t->child, text);
    542 		if (bracketed) os_child_put_s8(t->child, s8("\033[201~"));
    543 	}
    544 	return 1;
    545 }
    546 
    547 KEYBIND_FN(scroll)
    548 {
    549 	if (t->mode & TM_ALTSCREEN)
    550 		return 0;
    551 
    552 	TermView *tv = t->views + t->view_idx;
    553 
    554 	/* NOTE: do nothing if there aren't enough lines to scrollback */
    555 	if (tv->lines.filled < t->size.h)
    556 		return 1;
    557 
    558 	t->scroll_offset += a.i;
    559 	CLAMP(t->scroll_offset, 0, tv->lines.filled - (t->size.h - 1));
    560 
    561 	t->gl.flags |= NEEDS_FULL_REFILL;
    562 
    563 	return 1;
    564 }
    565 
    566 KEYBIND_FN(zoom)
    567 {
    568 	shift_font_sizes(&t->fa, a.i);
    569 	update_font_textures(&t->gl, &t->fa);
    570 	t->gl.flags |= NEEDS_RESIZE;
    571 	return 1;
    572 }
    573 
    574 /* NOTE: called when the window was resized */
    575 static void
    576 fb_callback(GLFWwindow *win, i32 w, i32 h)
    577 {
    578 	Term *t = glfwGetWindowUserPointer(win);
    579 
    580 	t->gl.window_size = (v2){.w = w, .h = h};
    581 
    582 	glViewport(0, 0, w, h);
    583 	set_projection_matrix(&t->gl);
    584 
    585 	glActiveTexture(GL_TEXTURE0 + t->gl.fb_tex_unit);
    586 	glBindTexture(GL_TEXTURE_2D, t->gl.fb_tex);
    587 	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
    588 
    589 	/* NOTE: reactive the glyph texture unit */
    590 	glActiveTexture(GL_TEXTURE0);
    591 
    592 	t->gl.flags |= NEEDS_RESIZE|NEEDS_BLIT;
    593 }
    594 
    595 static void
    596 key_callback(GLFWwindow *win, i32 key, i32 sc, i32 act, i32 mods)
    597 {
    598 	Term *t = glfwGetWindowUserPointer(win);
    599 
    600 	#ifdef _DEBUG
    601 	if (key == GLFW_KEY_F1 && act == GLFW_PRESS) {
    602 		dump_last_line_to_file(t);
    603 		return;
    604 	}
    605 	if (key == GLFW_KEY_F2 && act == GLFW_PRESS) {
    606 		dump_fb_to_file(t);
    607 		return;
    608 	}
    609 	if (key == GLFW_KEY_F3 && act == GLFW_PRESS) {
    610 		dump_glyph_cache_to_file(&t->fa);
    611 		return;
    612 	}
    613 	#endif
    614 
    615 	/* NOTE: handle mapped keybindings */
    616 	u32 enc = ENCODE_KEY(act, mods, key);
    617 	for (u32 i = 0; i < ARRAY_COUNT(g_hotkeys); i++) {
    618 		struct hotkey *hk = g_hotkeys + i;
    619 		if (hk->key == enc) {
    620 			b32 handled = hk->fn(t, hk->arg);
    621 			if (handled)
    622 				return;
    623 		}
    624 	}
    625 
    626 	/* NOTE: send control sequences */
    627 	if (mods & GLFW_MOD_CONTROL && act != GLFW_RELEASE) {
    628 		if (t->gl.mode & WIN_MODE_8BIT) {
    629 			if (key < 0x7F) {
    630 				s8 enc = utf8_encode(key | 0x80);
    631 				os_child_put_s8(t->child, enc);
    632 				return;
    633 			}
    634 		} else if (BETWEEN(key, 0x40, 0x5F)) {
    635 			os_child_put_char(t->child, key - 0x40);
    636 			return;
    637 		}
    638 	}
    639 
    640 
    641 	switch (ENCODE_KEY(act, 0, key)) {
    642 	case ENCODE_KEY(GLFW_PRESS,  0, GLFW_KEY_TAB):
    643 	case ENCODE_KEY(GLFW_REPEAT, 0, GLFW_KEY_TAB):
    644 		os_child_put_char(t->child, '\t');
    645 		break;
    646 	case ENCODE_KEY(GLFW_PRESS,  0, GLFW_KEY_ENTER):
    647 	case ENCODE_KEY(GLFW_REPEAT, 0, GLFW_KEY_ENTER):
    648 		os_child_put_char(t->child, '\r');
    649 		break;
    650 	case ENCODE_KEY(GLFW_PRESS,  0, GLFW_KEY_BACKSPACE):
    651 	case ENCODE_KEY(GLFW_REPEAT, 0, GLFW_KEY_BACKSPACE):
    652 		os_child_put_char(t->child, 0x7F);
    653 		break;
    654 	case ENCODE_KEY(GLFW_PRESS,  0, GLFW_KEY_UP):
    655 	case ENCODE_KEY(GLFW_REPEAT, 0, GLFW_KEY_UP):
    656 		if (t->gl.mode & WIN_MODE_APPCURSOR)
    657 			os_child_put_s8(t->child, s8("\x1BOA"));
    658 		else
    659 			os_child_put_s8(t->child, s8("\x1B[A"));
    660 		break;
    661 	case ENCODE_KEY(GLFW_PRESS,  0, GLFW_KEY_DOWN):
    662 	case ENCODE_KEY(GLFW_REPEAT, 0, GLFW_KEY_DOWN):
    663 		if (t->gl.mode & WIN_MODE_APPCURSOR)
    664 			os_child_put_s8(t->child, s8("\x1BOB"));
    665 		else
    666 			os_child_put_s8(t->child, s8("\x1B[B"));
    667 		break;
    668 	case ENCODE_KEY(GLFW_PRESS,  0, GLFW_KEY_RIGHT):
    669 	case ENCODE_KEY(GLFW_REPEAT, 0, GLFW_KEY_RIGHT):
    670 		if (t->gl.mode & WIN_MODE_APPCURSOR)
    671 			os_child_put_s8(t->child, s8("\x1BOC"));
    672 		else
    673 			os_child_put_s8(t->child, s8("\x1B[C"));
    674 		break;
    675 	case ENCODE_KEY(GLFW_PRESS,  0, GLFW_KEY_LEFT):
    676 	case ENCODE_KEY(GLFW_REPEAT, 0, GLFW_KEY_LEFT):
    677 		if (t->gl.mode & WIN_MODE_APPCURSOR)
    678 			os_child_put_s8(t->child, s8("\x1BOD"));
    679 		else
    680 			os_child_put_s8(t->child, s8("\x1B[D"));
    681 		break;
    682 	case ENCODE_KEY(GLFW_PRESS,  0, GLFW_KEY_PAGE_UP):
    683 	case ENCODE_KEY(GLFW_REPEAT, 0, GLFW_KEY_PAGE_UP):
    684 		if      (mods & GLFW_MOD_CONTROL) os_child_put_s8(t->child, s8("\x1B[5;5~"));
    685 		else if (mods & GLFW_MOD_SHIFT)   os_child_put_s8(t->child, s8("\x1B[5;2~"));
    686 		else                              os_child_put_s8(t->child, s8("\x1B[5~"));
    687 		break;
    688 	case ENCODE_KEY(GLFW_PRESS,  0, GLFW_KEY_PAGE_DOWN):
    689 	case ENCODE_KEY(GLFW_REPEAT, 0, GLFW_KEY_PAGE_DOWN):
    690 		if      (mods & GLFW_MOD_CONTROL) os_child_put_s8(t->child, s8("\x1B[6;5~"));
    691 		else if (mods & GLFW_MOD_SHIFT)   os_child_put_s8(t->child, s8("\x1B[6;2~"));
    692 		else                              os_child_put_s8(t->child, s8("\x1B[6~"));
    693 		break;
    694 	}
    695 }
    696 
    697 static void
    698 mouse_button_callback(GLFWwindow *win, i32 btn, i32 act, i32 mod)
    699 {
    700 	Term *t = glfwGetWindowUserPointer(win);
    701 	/* TODO: map other mouse buttons */
    702 	if (btn != GLFW_MOUSE_BUTTON_LEFT)
    703 		return;
    704 
    705 	if (act == GLFW_RELEASE)
    706 		return;
    707 
    708 	f64 xpos, ypos;
    709 	glfwGetCursorPos(win, &xpos, &ypos);
    710 	t->selection.range.end   = (iv2){.x = -1, .y = -1};
    711 	t->selection.mouse_start = (v2){.x = xpos, .y = ypos};
    712 	t->selection.click_param = DOUBLE_CLICK_TIME;
    713 
    714 	if (t->selection.state != SS_WORDS)
    715 		t->selection.state++;
    716 
    717 	iv2 cell = mouse_to_cell_space(t, t->selection.mouse_start);
    718 	if (t->views[t->view_idx].fb.rows[cell.y][cell.x].style.attr & ATTR_WDUMMY) {
    719 		ASSERT(t->views[t->view_idx].fb.rows[cell.y][cell.x - 1].style.attr & ATTR_WIDE);
    720 		cell.x--;
    721 	}
    722 
    723 	if (t->selection.state == SS_WORDS) {
    724 		t->selection.anchor = get_word_around_cell(t, cell);
    725 	} else {
    726 		t->selection.anchor    = (Range){.start = cell, .end = cell};
    727 		t->selection.range.end = INVALID_RANGE_END;
    728 	}
    729 }
    730 
    731 static void
    732 char_callback(GLFWwindow *win, u32 codepoint)
    733 {
    734 	Term *t = glfwGetWindowUserPointer(win);
    735 	if (t->scroll_offset) {
    736 		t->scroll_offset = 0;
    737 		t->gl.flags |= NEEDS_FULL_REFILL;
    738 	}
    739 	os_child_put_s8(t->child, utf8_encode(codepoint));
    740 }
    741 
    742 static void
    743 scroll_callback(GLFWwindow *win, f64 xoff, f64 yoff)
    744 {
    745 	(void)xoff;
    746 
    747 	Term *t = glfwGetWindowUserPointer(win);
    748 	if (t->mode & TM_ALTSCREEN) {
    749 		b32 left_shift_state  = glfwGetKey(win, GLFW_KEY_LEFT_SHIFT)  == GLFW_PRESS;
    750 		b32 right_shift_state = glfwGetKey(win, GLFW_KEY_RIGHT_SHIFT) == GLFW_PRESS;
    751 		b32 shift_down = left_shift_state || right_shift_state;
    752 		if (yoff > 0) {
    753 			if (shift_down) os_child_put_s8(t->child, s8("\x1B[5;2~"));
    754 			else            os_child_put_s8(t->child, s8("\x19"));
    755 		} else {
    756 			if (shift_down) os_child_put_s8(t->child, s8("\x1B[6;2~"));
    757 			else            os_child_put_s8(t->child, s8("\x05"));
    758 		}
    759 	} else {
    760 		Arg a = {.i = (i32)yoff};
    761 		if (glfwGetKey(win, GLFW_KEY_LEFT_SHIFT) ||
    762 		    glfwGetKey(win, GLFW_KEY_RIGHT_SHIFT))
    763 		    a.i *= 5;
    764 		scroll(t, a);
    765 	}
    766 }
    767 
    768 DEBUG_EXPORT iv2
    769 init_term(Term *t, Arena *a, iv2 cells)
    770 {
    771 	init_fonts(t, a);
    772 	for (u32 i = 0; i < ARRAY_COUNT(t->saved_cursors); i++) {
    773 		cursor_reset(t);
    774 		cursor_move_to(t, 0, 0);
    775 		cursor_alt(t, 1);
    776 	}
    777 	selection_clear(&t->selection);
    778 	v2 cs = get_cell_size(t);
    779 	iv2 requested_size = {
    780 		.x = cs.x * cells.x + 2 * g_term_pad.x,
    781 		.y = cs.y * cells.y + 2 * g_term_pad.y,
    782 	};
    783 	return requested_size;
    784 }
    785 
    786 DEBUG_EXPORT void
    787 init_callbacks(GLCtx *gl)
    788 {
    789 	glfwSetCharCallback(gl->window, char_callback);
    790 	glfwSetFramebufferSizeCallback(gl->window, fb_callback);
    791 	glfwSetKeyCallback(gl->window, key_callback);
    792 	glfwSetMouseButtonCallback(gl->window, mouse_button_callback);
    793 	//glfwSetWindowRefreshCallback(gl->window, refresh_callback);
    794 	glfwSetScrollCallback(gl->window, scroll_callback);
    795 }
    796 
    797 DEBUG_EXPORT void
    798 do_terminal(Term *t)
    799 {
    800 	static f32 last_frame_time;
    801 	f32 frame_start_time = (f32)glfwGetTime();
    802 
    803 	if (t->gl.flags & UPDATE_RENDER_UNIFORMS)
    804 		update_uniforms(t, SHADER_RENDER);
    805 	if (t->gl.flags & UPDATE_POST_UNIFORMS)
    806 		update_uniforms(t, SHADER_POST);
    807 
    808 	if (t->gl.flags & NEEDS_RESIZE)
    809 		resize(t);
    810 
    811 	size parsed_lines = 0;
    812 	if (os_child_data_available(t->child)) {
    813 		RingBuf *rb = &t->views[t->view_idx].log;
    814 		if (os_child_exited(t->child)) {
    815 			/* TODO: is there a reason to not immediately exit? */
    816 			glfwSetWindowShouldClose(t->gl.window, GL_TRUE);
    817 			return;
    818 		}
    819 		t->unprocessed_bytes += os_read_from_child(t->child, t->views + t->view_idx,
    820 		                                           t->unprocessed_bytes);
    821 		s8 raw = {
    822 			.len  = t->unprocessed_bytes,
    823 			.data = rb->buf + (rb->widx - t->unprocessed_bytes)
    824 		};
    825 		parsed_lines  = split_raw_input_to_lines(t, raw);
    826 		t->gl.flags  |= NEEDS_REFILL;
    827 	}
    828 
    829 	if (t->gl.flags & (NEEDS_REFILL|NEEDS_FULL_REFILL))
    830 		blit_lines(t, t->arena_for_frame, parsed_lines);
    831 
    832 	update_selection(t);
    833 
    834 	v2 ws = t->gl.window_size;
    835 
    836 	if (t->gl.flags & NEEDS_BLIT) {
    837 		glUseProgram(t->gl.programs[SHADER_RENDER]);
    838 		glUniform1i(t->gl.render.texslot, 0);
    839 		glBindFramebuffer(GL_FRAMEBUFFER, t->gl.fb);
    840 
    841 		RenderPushBuffer *rpb = alloc(&t->arena_for_frame, RenderPushBuffer, 1);
    842 		clear_colour(t->gl.mode & WIN_MODE_REVERSE);
    843 		render_framebuffer(t, rpb);
    844 
    845 		if (0) {
    846 			v2 cell_size  = get_cell_size(t);
    847 			v2 cursor_pos = {
    848 				.x = t->cursor.pos.x * cell_size.w,
    849 				.y = ws.h - cell_size.h * (t->cursor.pos.y + 1),
    850 			};
    851 
    852 			v2 src_bl = {0};
    853 			v2 src_tr = {.x = ws.w, .y = src_bl.y + ws.h};
    854 
    855 			if (t->cursor.pos.y > t->size.h) {
    856 				src_tr.y = cursor_pos.y + ws.h;
    857 				src_bl.y = cursor_pos.y;
    858 			}
    859 
    860 			s8 fps  = s8alloc(&t->arena_for_frame, 64);
    861 			fps.len = snprintf((char *)fps.data, fps.len, "Render Time: %0.02f ms/f",
    862 			                   last_frame_time * 1e3);
    863 			v2 ts   = measure_text(t->arena_for_frame, &t->fa, fps, 1);
    864 			v2 pos  = {.x = ws.w - ts.x - 10, .y = src_tr.y - ts.y - 10};
    865 
    866 			Rect r = {
    867 				.pos  = {.x = pos.x - 0.05 * ts.x, .y = pos.y - 0.25 * ts.y},
    868 				.size = {.w = ts.w * 1.1, .h = ts.h * 1.2},
    869 			};
    870 
    871 			draw_rectangle(rpb, &t->gl, r, (Colour){.rgba = 0x303030ff});
    872 			draw_text(rpb, &t->gl, t->arena_for_frame, &t->fa, fps, pos,
    873 			          (Colour){.rgba = 0x1e9e33ff}, 1);
    874 		}
    875 
    876 		flush_render_push_buffer(rpb, &t->gl);
    877 		t->gl.flags &= ~NEEDS_BLIT;
    878 	}
    879 
    880 	static f32 param = 0;
    881 	static f32 p_scale = 1;
    882 	param += p_scale * 0.005 * t->gl.dt;
    883 	if (param > 1.0f || param < 0)
    884 		p_scale *= -1.0f;
    885 
    886 	glBindFramebuffer(GL_FRAMEBUFFER, 0);
    887 
    888 	clear_colour(t->gl.mode & WIN_MODE_REVERSE);
    889 	glUseProgram(t->gl.programs[SHADER_POST]);
    890 	glUniform1i(t->gl.post.texslot, t->gl.fb_tex_unit);
    891 	glUniform1f(t->gl.post.param, param);
    892 	glUniform2fv(t->gl.post.vertscale, 1, (f32 []){ws.w, ws.h});
    893 	glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    894 
    895 	last_frame_time = (f32)glfwGetTime() - frame_start_time;
    896 }