vtgl

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

Commit: c86265503880612d85f924f794061aea978a8d25
Parent: 0940841f6decc5c9eeaca2997a06ad5eb4aa2438
Author: Randy Palamar
Date:   Sun,  7 Jul 2024 11:58:56 -0600

osc and window title handling

Diffstat:
Mconfig.def.h | 3+++
Mmain.c | 7++++---
Mterminal.c | 133++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mutil.c | 10++++++++++
Mutil.h | 9+++++++++
Mvtgl.c | 2+-
6 files changed, 149 insertions(+), 15 deletions(-)

diff --git a/config.def.h b/config.def.h @@ -1,3 +1,6 @@ +/* See LICENSE for copyright details */ +static s8 g_default_title = s8("vtgl"); + static u8 g_tabstop = 8; static Colour base16_colours[16] = { diff --git a/main.c b/main.c @@ -85,7 +85,7 @@ error_callback(int code, const char *desc) } static void -init_window(Term *t) +init_window(Term *t, Arena arena) { if (!glfwInit()) die("Failed to init GLFW\n"); @@ -106,7 +106,8 @@ init_window(Term *t) glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); - t->gl.window = glfwCreateWindow(t->gl.window_size.w, t->gl.window_size.h, "vtgl", NULL, NULL); + char *title = s8_to_cstr(&arena, g_default_title); + t->gl.window = glfwCreateWindow(t->gl.window_size.w, t->gl.window_size.h, title, NULL, NULL); if (!t->gl.window) { glfwTerminate(); die("Failed to spawn GLFW window\n"); @@ -284,7 +285,7 @@ main(void) static char *font_paths[] = { "/usr/share/fonts/gofont/Go-Mono.ttf", }; - init_window(&term); + init_window(&term, memory); init_fonts(&term, font_paths, ARRAY_COUNT(font_paths), 48, &memory); cursor_reset(&term); diff --git a/terminal.c b/terminal.c @@ -10,6 +10,12 @@ typedef struct { u8 priv; } CSI; +typedef struct { + s8 raw; + s8 arg; + i32 cmd; +} OSC; + static const u8 utf8overhangmask[32] = { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, @@ -17,6 +23,12 @@ static const u8 utf8overhangmask[32] = { 0, 0, 0, 0, 0, 0, 0, 0 }; +static void +set_window_title(GLFWwindow *win, Arena a, s8 title) +{ + glfwSetWindowTitle(win, s8_to_cstr(&a, title)); +} + static v2 get_cell_size(Term *t) { @@ -372,8 +384,8 @@ static void window_manipulation(Term *t, CSI *csi) { switch (csi->argv[0]) { - case 22: /* TODO: save title */ break; - case 23: /* TODO: restore title */ break; + case 22: t->saved_title = glfwGetWindowTitle(t->gl.window); break; + case 23: glfwSetWindowTitle(t->gl.window, t->saved_title); break; default: fprintf(stderr, "unhandled xtwinops: %d\n", csi->argv[0]); dump_csi(csi); @@ -444,12 +456,98 @@ handle_csi(Term *t, s8 *raw) } } +static OSC +parse_osc(s8 *raw) +{ + OSC osc = {0}; + + osc.raw.data = raw->data; + + /* NOTE: parse command then store the rest as a string */ + u32 cp; + while (raw->len) { + cp = get_ascii(raw); + osc.raw.len++; + if (!BETWEEN(cp, '0', '9')) + break; + osc.cmd *= 10; + osc.cmd += cp - '0'; + + /* TODO: Performance? */ + /* NOTE: The maximum OSC in xterm is 119 so if this + * exceeds that the whole sequence is malformed */ + if (osc.cmd > 1000) + break; + } + + if (cp != ';' || osc.cmd > 1000) + die("parse_osc: malformed\n"); + + osc.arg.data = raw->data; + while (raw->len) { + cp = get_ascii(raw); + osc.raw.len++; + if (cp == '\a') + break; + if (cp == 0x1B && peek(*raw, 0) == '\\') { + get_ascii(raw); + osc.raw.len++; + break; + } + osc.arg.len++; + } + + /* NOTE: if raw->len is 0 we ran out of characters */ + if (raw->len == 0) + osc.cmd = -1; + + return osc; +} + +static void +dump_osc(OSC *osc) +{ + fputs("ESC]", stderr); + for (size i = 0; i < osc->raw.len; i++) { + u8 cp = osc->raw.data[i]; + if (ISPRINT(cp)) + fputc(cp, stderr); + else if (cp == '\n') + fputs("\\n", stderr); + else if (cp == '\r') + fputs("\\r", stderr); + else if (cp == '\a') + fputs("\\a", stderr); + else + fprintf(stderr, "\\x%02X", cp); + } + fprintf(stderr, "\n\t.cmd = %d, .arg = {.len = %zd}\n", osc->cmd, osc->arg.len); +} + +static void +handle_osc(Term *t, s8 *raw, Arena a) +{ + OSC osc = parse_osc(raw); + + switch (osc.cmd) { + case 0: set_window_title(t->gl.window, a, osc.arg); break; + case 1: /* IGNORED: set icon name */ break; + case 2: set_window_title(t->gl.window, a, osc.arg); break; + case -1: + default: + fputs("unhandled osc cmd: ", stderr); + dump_osc(&osc); + break; + } +} + static void -handle_escape(Term *t, s8 *raw) +handle_escape(Term *t, s8 *raw, Arena a) { u32 cp = get_ascii(raw); switch(cp) { - case '[': handle_csi(t, raw); break; + case '[': handle_csi(t, raw); break; + case ']': handle_osc(t, raw, a); break; case '(': /* GZD4 -- set primary charset G0 */ case ')': /* G1D4 -- set secondary charset G1 */ case '*': /* G2D4 -- set tertiary charset G2 */ @@ -484,6 +582,16 @@ enum escape_moves_cursor_result { }; static enum escape_moves_cursor_result +validate_osc(Term *t, s8 *raw) +{ + enum escape_moves_cursor_result result = EMC_NORMAL_RETURN; + OSC osc = parse_osc(raw); + if (osc.cmd == -1) + return EMC_NEEDS_MORE_BYTES; + return result; +} + +static enum escape_moves_cursor_result check_if_csi_moves_cursor(Term *t, s8 *raw) { enum escape_moves_cursor_result result = EMC_NORMAL_RETURN; @@ -511,6 +619,9 @@ check_if_escape_moves_cursor(Term *t, s8 *raw) case '[': result = check_if_csi_moves_cursor(t, raw); break; + case ']': + result = validate_osc(t, raw); + break; case '(': /* GZD4 -- set primary charset G0 */ case ')': /* G1D4 -- set secondary charset G1 */ case '*': /* G2D4 -- set tertiary charset G2 */ @@ -632,7 +743,7 @@ push_tab(Term *t) } static void -push_line(Term *t, Line *line) +push_line(Term *t, Line *line, Arena a) { s8 l = line_to_s8(line); @@ -643,10 +754,10 @@ push_line(Term *t, Line *line) /* TODO: handle unicode case */ u32 cp = get_ascii(&l); switch (cp) { - case 0x1B: handle_escape(t, &l); break; - case '\r': t->cursor.col = 0; break; - case '\n': push_newline(t); break; - case '\t': push_tab(t); break; + case 0x1B: handle_escape(t, &l, a); break; + case '\r': t->cursor.col = 0; break; + case '\n': push_newline(t); break; + case '\t': push_tab(t); break; case '\b': cursor_move_to(t, t->cursor.row, t->cursor.col - 1); break; @@ -673,7 +784,7 @@ get_line_idx(LineBuf *lb, size off) } static void -blit_lines(Term *t) +blit_lines(Term *t, Arena a) { size line_count = t->size.h; if (line_count > t->log_lines.filled) @@ -687,6 +798,6 @@ blit_lines(Term *t) t->cursor.col = 0; for (size i = 0; i <= line_count; i++) { size line_idx = get_line_idx(&t->log_lines, -line_count + i); - push_line(t, t->log_lines.buf + line_idx); + push_line(t, t->log_lines.buf + line_idx, a); } } diff --git a/util.c b/util.c @@ -43,3 +43,13 @@ s8alloc(Arena *a, size len) { return (s8){.len = len, .data = alloc(a, u8, len)}; } + +static char * +s8_to_cstr(Arena *a, s8 s) +{ + char *cstr = alloc(a, char, s.len + 1); + for (size i = 0; i < s.len; i++) + cstr[i] = s.data[i]; + cstr[s.len] = 0; + return cstr; +} diff --git a/util.h b/util.h @@ -237,6 +237,11 @@ typedef struct { i32 deltay; } FontAtlas; +enum terminal_mode { + TM_ALTSCEEN = 1 << 0, + TM_REPLACE = 1 << 1, +}; + typedef struct { GLCtx gl; FontAtlas fa; @@ -251,6 +256,10 @@ typedef struct { uv2 size; Cursor cursor; + u32 mode; + + /* NOTE: owned by GLFW; doesn't need to be freed */ + const char *saved_title; FT_Library ftlib; } Term; diff --git a/vtgl.c b/vtgl.c @@ -382,7 +382,7 @@ do_terminal(Term *t, Arena a) clear_colour(); - blit_lines(t); + blit_lines(t, a); render_framebuffer(t, rpb); v2 cell_size = get_cell_size(t);