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:
M | debug.c | | | 18 | +++++++++--------- |
M | terminal.c | | | 88 | ++++++++++++++++++++++++++++++++++++++++---------------------------------------- |
M | test.c | | | 2 | +- |
M | util.h | | | 10 | +++++++++- |
M | vtgl.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;
}