vtgl

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

Commit: 22cc7c97755d177b070650ec2e9781a05ba81d11
Parent: 81d8897805130bf3b53ad044ec06bff0a71019ec
Author: Randy Palamar
Date:   Fri, 16 Aug 2024 07:42:47 -0600

restore cursor position when replaying a line

If we replay a line that wraps and causes the cursor to advance a
row we will get incorrect when replaying it. Instead we should
keep track of position when the line was started and go back when
the same line is replayed.

Diffstat:
Mdebug.c | 18+++++++++---------
Mterminal.c | 88++++++++++++++++++++++++++++++++++++++++----------------------------------------
Mtest.c | 2+-
Mutil.h | 10+++++++++-
Mvtgl.c | 14+++++++-------
5 files changed, 70 insertions(+), 62 deletions(-)

diff --git a/debug.c b/debug.c @@ -13,16 +13,16 @@ simulate_line(Term *t, Line *line) if (line->has_unicode) cp = get_utf8(&l); else cp = get_ascii(&l); - u32 advance = g_tabstop - (t->cursor.col % g_tabstop); + u32 advance = g_tabstop - (t->cursor.pos.x % g_tabstop); switch (cp) { - case 0x1B: check_if_escape_moves_cursor(t, &l); break; - case '\r': t->cursor.col = 0; break; - case '\n': cursor_move_to(t, t->cursor.row + 1, t->cursor.col); break; - case '\t': cursor_move_to(t, t->cursor.row, t->cursor.col + advance); break; - case '\b': cursor_move_to(t, t->cursor.row, t->cursor.col - 1); break; - case '\a': break; - default: cursor_move_to(t, t->cursor.row, t->cursor.col + 1); break; + case 0x1B: check_if_escape_moves_cursor(t, &l); break; + case '\r': t->cursor.pos.x = 0; break; + case '\n': cursor_move_to(t, t->cursor.pos.y + 1, t->cursor.pos.x); break; + case '\t': cursor_move_to(t, t->cursor.pos.y, t->cursor.pos.x + advance); break; + case '\b': cursor_move_to(t, t->cursor.pos.y, t->cursor.pos.x - 1); break; + case '\a': break; + default: cursor_move_to(t, t->cursor.pos.y, t->cursor.pos.x + 1); break; } } @@ -38,7 +38,7 @@ fput_cursor_info(FILE *f, Cursor c) fprintf(f, "\tFG: 0x%08x\n", c.state.fg.rgba); fprintf(f, "\tBG: 0x%08x\n", c.state.bg.rgba); fprintf(f, "\tAttr: 0x%08x\n", c.state.attr); - fprintf(f, "\tPos: {%d, %d}\n", c.row, c.col); + fprintf(f, "\tPos: {%d, %d}\n", c.pos.y, c.pos.x); } static void diff --git a/terminal.c b/terminal.c @@ -180,6 +180,7 @@ swap_screen(Term *t) { t->mode ^= TM_ALTSCREEN; t->view_idx = !!(t->mode & TM_ALTSCREEN); + t->gl.flags |= NEEDS_FULL_BLIT; } static void @@ -194,8 +195,8 @@ cursor_reset(Term *t) static void cursor_move_to(Term *t, i32 row, i32 col) { - t->cursor.row = CLAMP(row, 0, t->size.h - 1); - t->cursor.col = CLAMP(col, 0, t->size.w - 1); + t->cursor.pos.y = CLAMP(row, 0, t->size.h - 1); + t->cursor.pos.x = CLAMP(col, 0, t->size.w - 1); } static void @@ -210,14 +211,14 @@ cursor_alt(Term *t, b32 save) static void cursor_step_column(Term *t, i32 step) { - t->cursor.col += step; - if (t->cursor.col < t->size.w) + t->cursor.pos.x += step; + if (t->cursor.pos.x < t->size.w) return; - t->cursor.col = 0; - t->cursor.row++; - if (t->cursor.row < t->size.h) + t->cursor.pos.x = 0; + t->cursor.pos.y++; + if (t->cursor.pos.y < t->size.h) return; - t->cursor.row = t->size.h - 1; + t->cursor.pos.y = t->size.h - 1; fb_scroll_up(t, 0, 1); } @@ -263,17 +264,17 @@ dump_csi(CSI *csi) static void erase_in_display(Term *t, CSI *csi) { - Cursor *c = &t->cursor; + uv2 cpos = t->cursor.pos; switch (csi->argv[0]) { case 0: /* Erase Below (default) */ - fb_clear_region(t, c->row, c->row, c->col, t->size.w); - if (c->row < t->size.h - 1) - fb_clear_region(t, c->row + 1, t->size.h, 0, t->size.w); + fb_clear_region(t, cpos.y, cpos.y, cpos.x, t->size.w); + if (cpos.y < t->size.h - 1) + fb_clear_region(t, cpos.y + 1, t->size.h, 0, t->size.w); break; case 1: /* Erase Above */ - if (c->row > 0) - fb_clear_region(t, 0, c->row - 1, 0, t->size.w); - fb_clear_region(t, c->row, c->row, 0, c->col); + if (cpos.y > 0) + fb_clear_region(t, 0, cpos.y - 1, 0, t->size.w); + fb_clear_region(t, cpos.y, cpos.y, 0, cpos.x); break; case 2: /* Erase All */ fb_clear_region(t, 0, t->size.h, 0, t->size.w); @@ -289,16 +290,16 @@ erase_in_display(Term *t, CSI *csi) static void erase_in_line(Term *t, CSI *csi) { - Cursor *c = &t->cursor; + uv2 cpos = t->cursor.pos; switch (csi->argv[0]) { case 0: /* Erase to Right */ - fb_clear_region(t, c->row, c->row, c->col, t->size.w); + fb_clear_region(t, cpos.y, cpos.y, cpos.x, t->size.w); break; case 1: /* Erase to Left */ - fb_clear_region(t, c->row, c->row, 0, c->col); + fb_clear_region(t, cpos.y, cpos.y, 0, cpos.x); break; case 2: /* Erase All */ - fb_clear_region(t, c->row, c->row, 0, t->size.w); + fb_clear_region(t, cpos.y, cpos.y, 0, t->size.w); break; default: ASSERT(0); } @@ -516,7 +517,7 @@ handle_csi(Term *t, s8 *raw) u8 next; switch (csi.mode) { - case 'G': cursor_move_to(t, t->cursor.row, csi.argv[0] - 1); break; + case 'G': cursor_move_to(t, t->cursor.pos.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; @@ -644,10 +645,10 @@ handle_escape(Term *t, s8 *raw, Arena a) term_reset(t); break; case 'M': /* RI -- Reverse Index */ - if (t->cursor.row == 0) { + if (t->cursor.pos.y == 0) { fb_scroll_down(t, 0, 1); } else { - cursor_move_to(t, t->cursor.row - 1, t->cursor.col); + cursor_move_to(t, t->cursor.pos.y - 1, t->cursor.pos.x); } break; default: @@ -660,6 +661,7 @@ enum escape_moves_cursor_result { EMC_NORMAL_RETURN, EMC_NEEDS_MORE_BYTES, EMC_CURSOR_MOVED, + EMC_SWAPPED_SCREEN, }; static enum escape_moves_cursor_result @@ -691,7 +693,7 @@ check_if_csi_moves_cursor(Term *t, s8 *raw) } if (term_mode != t->mode) - result = EMC_CURSOR_MOVED; + result = EMC_SWAPPED_SCREEN; return result; } @@ -721,7 +723,7 @@ check_if_escape_moves_cursor(Term *t, s8 *raw) result = EMC_CURSOR_MOVED; break; case 'M': /* RI -- Reverse Index */ - if (t->cursor.row != 0) + if (t->cursor.pos.y != 0) result = EMC_CURSOR_MOVED; break; default: break; @@ -780,9 +782,10 @@ split_raw_input_to_lines(Term *t, s8 raw) case EMC_CURSOR_MOVED: parsed_lines++; feed_line(&tv->lines, old.data, t->cursor.state); - if ((t->views + t->view_idx) == tv) - break; - /* NOTE: screen swapped */ + break; + case EMC_SWAPPED_SCREEN: + parsed_lines++; + feed_line(&tv->lines, old.data, t->cursor.state); TermView *nv = t->views + t->view_idx; size nstart = nv->log.widx; mem_copy(raw, (s8){nv->log.cap, nv->log.buf + nstart}); @@ -826,9 +829,9 @@ split_raw_input_to_lines(Term *t, s8 raw) static void push_newline(Term *t) { - t->cursor.row++; - if (t->cursor.row == t->size.h) { - t->cursor.row = t->size.h - 1; + t->cursor.pos.y++; + if (t->cursor.pos.y == t->size.h) { + t->cursor.pos.y = t->size.h - 1; fb_scroll_up(t, 0, 1); } } @@ -836,8 +839,8 @@ push_newline(Term *t) static void push_tab(Term *t) { - u32 advance = g_tabstop - (t->cursor.col % g_tabstop); - fb_clear_region(t, t->cursor.row, t->cursor.row, t->cursor.col, t->cursor.col + advance); + u32 advance = g_tabstop - (t->cursor.pos.x % g_tabstop); + fb_clear_region(t, t->cursor.pos.y, t->cursor.pos.y, t->cursor.pos.x, t->cursor.pos.x + advance); cursor_step_column(t, advance); } @@ -860,12 +863,12 @@ push_line(Term *t, Line *line, Arena a) switch (cp) { case 0x1B: handle_escape(t, &l, a); break; - case '\r': t->cursor.col = 0; break; + case '\r': t->cursor.pos.x = 0; break; case '\n': push_newline(t); break; case '\t': push_tab(t); break; case '\a': /* TODO: ding ding? */ break; case '\b': - cursor_move_to(t, t->cursor.row, t->cursor.col - 1); + cursor_move_to(t, t->cursor.pos.y, t->cursor.pos.x - 1); break; default: if (wrap_next) @@ -873,16 +876,16 @@ push_line(Term *t, Line *line, Arena a) /* TODO properly make sure characters are printable */ CLAMP(cp, ' ', '~'); - c = &tv->fb.rows[t->cursor.row][t->cursor.col]; + c = &tv->fb.rows[t->cursor.pos.y][t->cursor.pos.x]; c->cp = cp; c->style = t->cursor.state; - wrap_next = t->cursor.col + 1 == t->size.w; + wrap_next = t->cursor.pos.x + 1 == t->size.w; if (!wrap_next) cursor_step_column(t, 1); } } - if (wrap_next && (t->cursor.row != t->size.h - 1)) + if (wrap_next && (t->cursor.pos.y != t->size.h - 1)) cursor_step_column(t, 1); } @@ -899,17 +902,14 @@ get_line_idx(LineBuf *lb, size off) static void blit_lines(Term *t, Arena a, size line_count) { - /* TODO: this can't be correct; it could cause an unintentional screen scroll. - * need some way of determining if we are replaying a line and if so restore the - * cursor to position the line originally started at */ - /* NOTE: assume altscreen programs are managing the cursor correctly on their own */ - //t->cursor.col = t->mode & TM_ALTSCREEN ? t->cursor.col : 0; - t->cursor.col = 0; - TermView *tv = t->views + t->view_idx; CLAMP(line_count, 0, tv->lines.filled); for (size i = 0; i <= line_count; i++) { size line_idx = get_line_idx(&tv->lines, -line_count + i); + if (line_idx == tv->last_line_idx) + t->cursor.pos = tv->last_cursor_pos; + tv->last_cursor_pos = t->cursor.pos; + tv->last_line_idx = line_idx; push_line(t, tv->lines.buf + line_idx, a); } } diff --git a/test.c b/test.c @@ -96,7 +96,7 @@ static TEST_FN(cursor_movement) parsed_lines = split_raw_input_to_lines(term, raw); blit_lines(term, arena, parsed_lines); - result.status = term->cursor.row == 16 && term->cursor.col == 6; + result.status = term->cursor.pos.y == 16 && term->cursor.pos.x == 6; return result; } diff --git a/util.h b/util.h @@ -108,7 +108,7 @@ typedef struct { } Cell; typedef struct { - u32 row, col; + uv2 pos; CellStyle state; } Cursor; @@ -149,6 +149,14 @@ typedef struct { RingBuf log; LineBuf lines; Framebuffer fb; + /* NOTE: the position of the cursor the last time a new line was blitted + * and the index of the line. This is needed because we blit whole lines + * at a time unlike traditional terminal emulators which just operate as + * a state machine. Any time a line hasn't played to completion we must + * restart it from the original location lest it unintentionally cause a + * screen scroll. */ + uv2 last_cursor_pos; + size last_line_idx; } TermView; typedef struct { diff --git a/vtgl.c b/vtgl.c @@ -272,9 +272,9 @@ render_framebuffer(Term *t, RenderPushBuffer *rpb) /* TODO: hide cursor doesn't get reset properly */ //if (!(t->gl.mode & WIN_MODE_HIDECURSOR)) { - Cell cursor = tv->fb.rows[t->cursor.row][t->cursor.col]; - cr.pos.y = t->gl.window_size.h - cs.h * (t->cursor.row + 1); - cr.pos.x = t->cursor.col * cs.w; + Cell cursor = tv->fb.rows[t->cursor.pos.y][t->cursor.pos.x]; + cr.pos.y = t->gl.window_size.h - cs.h * (t->cursor.pos.y+ 1); + cr.pos.x = t->cursor.pos.x* cs.w; cursor.style.attr ^= ATTR_INVERSE; push_cell(rpb, &t->gl, cursor, cr, t->fa.deltay); } @@ -412,9 +412,9 @@ do_terminal(Term *t, Arena a) /* NOTE: this could cause extra work if there is new data and we need a full * blit on the same frame but for now we will ignore that */ if (t->gl.flags & NEEDS_FULL_BLIT) { - t->gl.flags &= ~NEEDS_FULL_BLIT; term_reset(t); blit_lines(t, a, t->size.h - 1); + t->gl.flags &= ~NEEDS_FULL_BLIT; } if (os_child_data_available(t->child)) { @@ -444,14 +444,14 @@ do_terminal(Term *t, Arena a) v2 ws = t->gl.window_size; v2 cell_size = get_cell_size(t); v2 cursor_pos = { - .x = t->cursor.col * cell_size.w, - .y = ws.h - cell_size.h * (t->cursor.row + 1), + .x = t->cursor.pos.x * cell_size.w, + .y = ws.h - cell_size.h * (t->cursor.pos.y + 1), }; v2 src_bl = {0}; v2 src_tr = { .x = ws.w, .y = src_bl.y + ws.h }; - if (t->cursor.row > t->size.h) { + if (t->cursor.pos.y > t->size.h) { src_tr.y = cursor_pos.y + ws.h; src_bl.y = cursor_pos.y; }