vtgl

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

Commit: e552852145827c5f8bedd5b4d0405259f7cc93d7
Parent: e87c83d5e54f4a762d738a2485a674b10ae26840
Author: Randy Palamar
Date:   Tue, 27 Aug 2024 21:20:19 -0600

allow control characters to be embedded in csi sequences

Diffstat:
Mbuild.sh | 4++--
Mterminal.c | 212+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mtest.c | 39+++++++++++++++++++++++++++++++++++++--
Mutil.h | 25+++++++++++++++++++++++++
4 files changed, 174 insertions(+), 106 deletions(-)

diff --git a/build.sh b/build.sh @@ -16,10 +16,10 @@ testcflags="$cflags -O0 -ggdb -D_DEBUG -Wno-unused-function -Wno-undefined-inter if [ $debug ]; then # Hot Reloading/Debugging - cflags="$cflags -ggdb -O0 -D_DEBUG -Wno-unused-function -Wno-undefined-internal" + cflags="$cflags -D_DEBUG -Wno-unused-function -Wno-undefined-internal" #cflags="$cflags -fsanitize=address,undefined" - libcflags="$cflags -fPIC -Wno-unused-function" + libcflags="$cflags -ggdb -O0 -fPIC" libldflags="$ldflags -shared" ${cc} $libcflags vtgl.c -o vtgl.so $libldflags diff --git a/terminal.c b/terminal.c @@ -1,21 +1,5 @@ #include <immintrin.h> -#define ESC_ARG_SIZ 6 - -typedef struct { - s8 raw; - i32 argv[ESC_ARG_SIZ]; - i32 argc; - u8 mode; - 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, @@ -665,31 +649,25 @@ push_tab(Term *t, i32 n) static i32 parse_csi(s8 *r, CSI *csi) { - *csi = (CSI){0}; - csi->raw = (s8){.data = r->data}; - if (peek(*r, 0) == '?') { csi->priv = 1; - csi->raw.len++; get_ascii(r); } while (r->len) { u32 cp = get_ascii(r); - csi->raw.len++; - if (BETWEEN(cp, '0', '9')) { + if (ISCONTROL(cp)) { + continue; + } else if (BETWEEN(cp, '0', '9')) { csi->argv[csi->argc] *= 10; csi->argv[csi->argc] += cp - '0'; continue; } csi->argc++; + if (cp != ';' || csi->argc == ESC_ARG_SIZ) { - if (cp == ';') { - csi->mode = get_ascii(r); - csi->raw.len++; - } else { - csi->mode = cp; - } + if (cp == ';') csi->mode = get_ascii(r); + else csi->mode = cp; return 0; } } @@ -698,10 +676,10 @@ parse_csi(s8 *r, CSI *csi) } static void -handle_csi(Term *t, s8 *raw) +handle_csi(Term *t, CSI *csi) { - CSI csi; - i32 ret = parse_csi(raw, &csi); + s8 raw = csi->raw; + i32 ret = parse_csi(&raw, csi); ASSERT(ret != -1); #define ORONE(x) (x)? (x) : 1 @@ -709,37 +687,38 @@ handle_csi(Term *t, s8 *raw) iv2 p = t->cursor.pos; u8 next; - switch (csi.mode) { - case 'A': cursor_step_raw(t, ORONE(csi.argv[0]), -1, 0); break; - case 'B': cursor_step_raw(t, ORONE(csi.argv[0]), 1, 0); break; - case 'C': cursor_step_raw(t, ORONE(csi.argv[0]), 0, 1); break; - case 'D': cursor_step_raw(t, ORONE(csi.argv[0]), 0, -1); break; - case 'E': cursor_move_to(t, p.y + ORONE(csi.argv[0]), 0); break; - case 'F': cursor_move_to(t, p.y - ORONE(csi.argv[0]), 0); break; - case 'G': cursor_move_to(t, p.y, csi.argv[0] - 1); break; - case 'H': cursor_move_to(t, csi.argv[0] - 1, csi.argv[1] - 1); break; - case 'J': erase_in_display(t, &csi); break; - case 'K': erase_in_line(t, &csi); break; - case 'L': insert_blank_lines(t, ORONE(csi.argv[0])); break; - case 'M': erase_lines(t, ORONE(csi.argv[0])); break; - case 'P': erase_characters(t, ORONE(csi.argv[0])); break; - case 'X': erase_characters(t, ORONE(csi.argv[0])); break; - case 'Z': push_tab(t, -(ORONE(csi.argv[0]))); break; - case 'a': cursor_step_raw(t, ORONE(csi.argv[0]), 0, 1); break; - case 'd': cursor_move_to(t, csi.argv[0] - 1, p.x); break; - case 'e': cursor_step_raw(t, ORONE(csi.argv[0]), 1, 0); break; - case 'f': cursor_move_to(t, csi.argv[0] - 1, csi.argv[1] - 1); break; - case 'h': set_mode(t, &csi, 1); break; - case 'l': set_mode(t, &csi, 0); break; - case 'm': set_colours(t, &csi); break; + switch (csi->mode) { + case 'A': cursor_step_raw(t, ORONE(csi->argv[0]), -1, 0); break; + case 'B': cursor_step_raw(t, ORONE(csi->argv[0]), 1, 0); break; + case 'C': cursor_step_raw(t, ORONE(csi->argv[0]), 0, 1); break; + case 'D': cursor_step_raw(t, ORONE(csi->argv[0]), 0, -1); break; + case 'E': cursor_move_to(t, p.y + ORONE(csi->argv[0]), 0); break; + case 'F': cursor_move_to(t, p.y - ORONE(csi->argv[0]), 0); break; + case 'G': cursor_move_to(t, p.y, csi->argv[0] - 1); break; + case 'H': cursor_move_to(t, csi->argv[0] - 1, csi->argv[1] - 1); break; + case 'J': erase_in_display(t, csi); break; + case 'K': erase_in_line(t, csi); break; + case 'L': insert_blank_lines(t, ORONE(csi->argv[0])); break; + case 'M': erase_lines(t, ORONE(csi->argv[0])); break; + case 'P': erase_characters(t, ORONE(csi->argv[0])); break; + case 'X': erase_characters(t, ORONE(csi->argv[0])); break; + case 'T': insert_blank_lines(t, ORONE(csi->argv[0])); break; + case 'Z': push_tab(t, -(ORONE(csi->argv[0]))); break; + case 'a': cursor_step_raw(t, ORONE(csi->argv[0]), 0, 1); break; + case 'd': cursor_move_to(t, csi->argv[0] - 1, p.x); break; + case 'e': cursor_step_raw(t, ORONE(csi->argv[0]), 1, 0); break; + case 'f': cursor_move_to(t, csi->argv[0] - 1, csi->argv[1] - 1); break; + case 'h': set_mode(t, csi, 1); break; + case 'l': set_mode(t, csi, 0); break; + case 'm': set_colours(t, csi); break; case 'r': - if (csi.priv) + if (csi->priv) goto unknown; - set_scrolling_region(t, &csi); + set_scrolling_region(t, csi); break; - case 't': window_manipulation(t, &csi); break; + case 't': window_manipulation(t, csi); break; case '!': - next = get_ascii(raw); + next = get_ascii(&raw); if (next == 'p') { /* NOTE: DECSTR: soft terminal reset IGNORED */ break; @@ -748,7 +727,7 @@ handle_csi(Term *t, s8 *raw) default: unknown: fputs("unknown csi: ", stderr); - dump_csi(&csi); + dump_csi(csi); } } @@ -796,6 +775,13 @@ parse_osc(s8 *raw, OSC *osc) } static void +reset_csi(CSI *csi, s8 *raw) +{ + *csi = (CSI){0}; + csi->raw.data = raw->data; +} + +static void dump_osc(OSC *osc) { fputs("ESC]", stderr); @@ -837,8 +823,8 @@ static void handle_escape(Term *t, s8 *raw, Arena a) { u32 cp = get_ascii(raw); - switch(cp) { - case '[': handle_csi(t, raw); break; + switch (cp) { + case '[': reset_csi(&t->csi, raw); t->escape |= EM_CSI; break; case ']': handle_osc(t, raw, a); break; case '(': /* GZD4 -- set primary charset G0 */ case ')': /* G1D4 -- set secondary charset G1 */ @@ -873,6 +859,24 @@ handle_escape(Term *t, s8 *raw, Arena a) } } +static void +push_control(Term *t, s8 *line, u32 cp, Arena a) +{ + switch (cp) { + case 0x1B: handle_escape(t, line, a); break; + case '\r': t->cursor.pos.x = 0; break; + case '\n': push_newline(t, 0); break; + case '\t': push_tab(t, 1); break; + case '\a': /* TODO: ding ding? */ break; + case '\b': + cursor_move_to(t, t->cursor.pos.y, t->cursor.pos.x - 1); + break; + } + if (cp != 0x1B && t->escape & EM_CSI) { + t->csi.raw.len++; + } +} + enum escape_moves_cursor_result { EMC_NORMAL_RETURN, EMC_NEEDS_MORE_BYTES, @@ -894,7 +898,7 @@ static enum escape_moves_cursor_result check_if_csi_moves_cursor(Term *t, s8 *raw) { enum escape_moves_cursor_result result = EMC_NORMAL_RETURN; - CSI csi; + CSI csi = {0}; if (parse_csi(raw, &csi) == -1) return EMC_NEEDS_MORE_BYTES; @@ -1073,53 +1077,55 @@ push_line(Term *t, Line *line, Arena a) /* TODO: handle error case */ ASSERT(cp != (u32)-1); + if (ISCONTROL(cp)) { + push_control(t, &l, cp, a); + continue; + } else if (t->escape & EM_CSI) { + t->csi.raw.len++; + if (BETWEEN(cp, '@', '~')) { + handle_csi(t, &t->csi); + t->escape &= ~EM_CSI; + } + continue; + } + + if (t->mode & TM_AUTO_WRAP && t->cursor.state & CURSOR_WRAP_NEXT) + push_newline(t, 1); + i32 width; - switch (cp) { - case 0x1B: handle_escape(t, &l, a); break; - case '\r': t->cursor.pos.x = 0; break; - case '\n': push_newline(t, 0); break; - case '\t': push_tab(t, 1); break; - case '\a': /* TODO: ding ding? */ break; - case '\b': - cursor_move_to(t, t->cursor.pos.y, t->cursor.pos.x - 1); - break; - default: - if (t->mode & TM_AUTO_WRAP && t->cursor.state & CURSOR_WRAP_NEXT) + if (line->has_unicode) { + width = wcwidth(cp); + ASSERT(width != -1); + } else { + width = 1; + } + + if (t->cursor.pos.x + width > t->size.w) { + /* NOTE: make space for character if mode enabled else + * clobber whatever was on the end of the line */ + if (t->mode & TM_AUTO_WRAP) push_newline(t, 1); + else + cursor_move_to(t, t->cursor.pos.y, t->size.w - width); + } - if (line->has_unicode) { - width = wcwidth(cp); - ASSERT(width != -1); - } else { - width = 1; - } + c = &tv->fb.rows[t->cursor.pos.y][t->cursor.pos.x]; + c->cp = cp; + c->style = t->cursor.style; - if (t->cursor.pos.x + width > t->size.w) { - /* NOTE: make space for character if mode enabled else - * clobber whatever was on the end of the line */ - if (t->mode & TM_AUTO_WRAP) - push_newline(t, 1); - else - cursor_move_to(t, t->cursor.pos.y, t->size.w - width); + if (width == 2) { + c->style.attr |= ATTR_WIDE; + if (t->cursor.pos.x + 1 < t->size.w) { + Cell *nc = c + 1; + nc->style.attr |= ATTR_WDUMMY; } + } - c = &tv->fb.rows[t->cursor.pos.y][t->cursor.pos.x]; - c->cp = cp; - c->style = t->cursor.style; - - if (width == 2) { - c->style.attr |= ATTR_WIDE; - if (t->cursor.pos.x + 1 < t->size.w) { - Cell *nc = c + 1; - nc->style.attr |= ATTR_WDUMMY; - } - } + if (t->cursor.pos.x + width < t->size.w) + cursor_step_column(t, width); + else + t->cursor.state |= CURSOR_WRAP_NEXT; - if (t->cursor.pos.x + width < t->size.w) - cursor_step_column(t, width); - else - t->cursor.state |= CURSOR_WRAP_NEXT; - } if (is_selected(&t->selection, t->cursor.pos.x, t->cursor.pos.y)) selection_clear(&t->selection); } @@ -1154,6 +1160,8 @@ blit_lines(Term *t, Arena a, size line_count) tv->last_cursor_pos = t->cursor.pos; tv->last_line_idx = line_idx; push_line(t, tv->lines.buf + line_idx, a); + /* TODO: can we avoid this? */ + ASSERT(t->escape == 0); } t->gl.flags &= ~(NEEDS_FULL_REFILL|NEEDS_REFILL); diff --git a/test.c b/test.c @@ -17,6 +17,7 @@ struct test_result { b32 status; const char *info; }; typedef TEST_FN(Test_Fn); #define TESTS \ + X(csi_embedded_control) \ X(colour_setting) \ X(cursor_at_line_boundary) \ X(cursor_movement) \ @@ -87,6 +88,41 @@ launder_static_string(Term *term, s8 static_str) return raw; } +static TEST_FN(csi_embedded_control) +{ + struct test_result result = {.info = __FUNCTION__}; + + /* NOTE: print a '1' with default style then start changing the colour, + * but backspace within the escape sequence so the cursor is now over the + * '1', but then put a newline inside the sequence so that cursor is on + * the next line, finally print a ' 2' with the completed colour sequence. */ + launder_static_string(term, s8("1")); + launder_static_string(term, CSI(48;2;7\b5;63;4\n2m)); + s8 raw = launder_static_string(term, s8(" 2")); + size parsed_lines = split_raw_input_to_lines(term, raw); + blit_lines(term, arena, parsed_lines); + + #if 0 + dump_csi(&term->csi); + #endif + + Cell c1 = { .cp = '1', .style = { + .bg = g_colours.data[g_colours.bgidx], + .fg = g_colours.data[g_colours.fgidx], + .attr = (ATTR_NULL), + }}; + Cell c2 = { .cp = '2', .style = { + .bg = (Colour){.r = 75, .g = 63, .b = 42, .a = 0xFF}, + .fg = g_colours.data[3], + .attr = (ATTR_INVISIBLE|ATTR_STRUCK), + }}; + result.status = term->cursor.pos.y == 1 && term->cursor.pos.x == 3; + result.status |= check_cells_equal(&c1, &term->views[term->view_idx].fb.rows[0][0]); + result.status |= check_cells_equal(&c2, &term->views[term->view_idx].fb.rows[1][1]); + + return result; +} + static TEST_FN(colour_setting) { struct test_result result = {.info = __FUNCTION__}; @@ -239,8 +275,7 @@ main(void) for (u32 i = 0; i < ARRAY_COUNT(tests); i++) { buffer_reset(&term); - cursor_reset(&term); - cursor_move_to(&term, 0, 0); + term_reset(&term); struct test_result result = tests[i](&term, memory); if (result.status == 0) { failure_count++; diff --git a/util.h b/util.h @@ -31,6 +31,8 @@ #define MIN(a, b) ((a) <= (b) ? (a) : (b)) #define SGN(a) ((a) < 0 ? (-1) : (1)) +#define ISCONTROLC0(c) (BETWEEN((c), 0, 0x1F) || (c) == 0x7F) +#define ISCONTROL(c) (ISCONTROLC0(c)) #define ISSPACE(c) ((c) == ' ' || (c) == '\n' || (c) == '\t') #define ISPRINT(c) BETWEEN((c), ' ', '~') @@ -336,6 +338,26 @@ typedef struct { enum selection_states state; } Selection; +#define ESC_ARG_SIZ 6 +typedef struct { + s8 raw; + i32 argv[ESC_ARG_SIZ]; + i32 argc; + u8 mode; + u8 priv; +} CSI; + +typedef struct { + s8 raw; + s8 arg; + i32 cmd; +} OSC; + +enum escape_mode { + EM_CSI = 1 << 0, + EM_STR = 1 << 1, +}; + enum terminal_mode { TM_ALTSCREEN = 1 << 0, TM_REPLACE = 1 << 1, @@ -363,6 +385,9 @@ typedef struct { /* NOTE: scrolling region */ u32 top, bot; + CSI csi; + + enum escape_mode escape; enum terminal_mode mode; char saved_title[1024];