diff --git a/CHANGELOG.md b/CHANGELOG.md index f12a8b53..11dce7f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,9 @@ * Scrollback indicator being incorrectly rendered when window size is very small. * Reduced memory usage in URL mode. +* Crash when the `E3` escape (`\E[3J`) was executed, and there was a + selection in the scrollback + (https://codeberg.org/dnkl/foot/issues/633). ### Security diff --git a/csi.c b/csi.c index 80ee75cd..dc5a018a 100644 --- a/csi.c +++ b/csi.c @@ -917,26 +917,7 @@ csi_dispatch(struct terminal *term, uint8_t final) case 3: { /* Erase scrollback */ - int end = (term->grid->offset + term->rows - 1) % term->grid->num_rows; - for (size_t i = 0; i < term->grid->num_rows; i++) { - if (end >= term->grid->offset) { - /* Not wrapped */ - if (i >= term->grid->offset && i <= end) - continue; - } else { - /* Wrapped */ - if (i >= term->grid->offset || i <= end) - continue; - } - - if (term->render.last_cursor.row == term->grid->rows[i]) - term->render.last_cursor.row = NULL; - - grid_row_free(term->grid->rows[i]); - term->grid->rows[i] = NULL; - } - term->grid->view = term->grid->offset; - term_damage_view(term); + term_erase_scrollback(term); break; } diff --git a/selection.c b/selection.c index 57ab8bf6..537614f6 100644 --- a/selection.c +++ b/selection.c @@ -69,10 +69,8 @@ selection_on_rows(const struct terminal *term, int row_start, int row_end) end = tmp; } - if (row_start >= start->row && row_end <= end->row) { - LOG_INFO("ON ROWS"); + if (row_start >= start->row && row_end <= end->row) return true; - } return false; } diff --git a/terminal.c b/terminal.c index 72ab1f2b..b16cf19a 100644 --- a/terminal.c +++ b/terminal.c @@ -2011,6 +2011,166 @@ term_erase(struct terminal *term, const struct coord *start, const struct coord sixel_overwrite_by_row(term, end->row, 0, end->col + 1); } +void +term_erase_scrollback(struct terminal *term) +{ + const int mask = term->grid->num_rows - 1; + const int start = (term->grid->offset + term->rows) & mask; + const int end = (term->grid->offset - 1) & mask; + const int sel_start = term->selection.start.row; + const int sel_end = term->selection.end.row; + + if (sel_end >= 0) { + /* + * Cancel selection if it touches any of the rows in the + * scrollback, since we can’t have the selection reference + * soon-to-be deleted rows. + * + * This is done by range checking the selection range against + * the scrollback range. + * + * To make this comparison simpler, the start/end absolute row + * numbers are “rebased” against the scrollback start, where + * row 0 is the *first* row in the scrollback. A high number + * thus means the row is further *down* in the scrollback, + * closer to the screen bottom. + */ + int scrollback_start = term->grid->offset + term->rows; + + int rel_sel_start = sel_start - scrollback_start + term->grid->num_rows; + int rel_sel_end = sel_end - scrollback_start + term->grid->num_rows; + + int rel_start = start - scrollback_start + term->grid->num_rows; + int rel_end = end - scrollback_start + term->grid->num_rows; + + rel_sel_start &= mask; + rel_sel_end &= mask; + rel_start &= mask; + rel_end &= mask; + + if ((rel_sel_start <= rel_start && rel_sel_end >= rel_start) || + (rel_sel_start <= rel_end && rel_sel_end >= rel_end) || + (rel_sel_start >= rel_start && rel_sel_end <= rel_end)) + { + selection_cancel(term); + } + } + + for (int i = start;; i = (i + 1) & mask) { + struct row *row = term->grid->rows[i]; + if (row != NULL) { + if (term->render.last_cursor.row == row) + term->render.last_cursor.row = NULL; + + grid_row_free(row); + term->grid->rows[i] = NULL; + } + + if (i == end) + break; + } + + term->grid->view = term->grid->offset; + term_damage_view(term); +} + +UNITTEST +{ + const int scrollback_rows = 16; + const int term_rows = 5; + const int cols = 5; + + struct fdm *fdm = fdm_init(); + xassert(fdm != NULL); + + struct terminal term = { + .fdm = fdm, + .rows = term_rows, + .cols = cols, + .normal = { + .rows = xcalloc(scrollback_rows, sizeof(term.normal.rows[0])), + .num_rows = scrollback_rows, + .num_cols = cols, + }, + .grid = &term.normal, + .selection = { + .start = {-1, -1}, + .end = {-1, -1}, + .kind = SELECTION_NONE, + .auto_scroll = { + .fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK), + }, + }, + }; + + xassert(term.selection.auto_scroll.fd >= 0); + +#define populate_scrollback() do { \ + for (int i = 0; i < scrollback_rows; i++) { \ + if (term.normal.rows[i] == NULL) { \ + struct row *r = xcalloc(1, sizeof(*term.normal.rows[i])); \ + r->cells = xcalloc(cols, sizeof(r->cells[0])); \ + term.normal.rows[i] = r; \ + } \ + } \ + } while (0) + + /* + * Test case 1 - no selection, just verify all rows except those + * on screen have been deleted. + */ + + populate_scrollback(); + term.normal.offset = 11; + term_erase_scrollback(&term); + for (int i = 0; i < scrollback_rows; i++) { + if (i >= term.normal.offset && i < term.normal.offset + term_rows) + xassert(term.normal.rows[i] != NULL); + else + xassert(term.normal.rows[i] == NULL); + } + + /* + * Test case 2 - selection that touches the scrollback. Verify the + * selection is cancelled. + */ + + term.normal.offset = 14; /* Screen covers rows 14,15,0,1,2 */ + + /* Selection covers rows 15,0,1,2,3 */ + term.selection.start = (struct coord){.row = 15}; + term.selection.end = (struct coord){.row = 19}; + term.selection.kind = SELECTION_CHAR_WISE; + + populate_scrollback(); + term_erase_scrollback(&term); + xassert(term.selection.start.row < 0); + xassert(term.selection.end.row < 0); + xassert(term.selection.kind == SELECTION_NONE); + + /* + * Test case 3 - selection that does *not* touch the + * scrollback. Verify the selection is *not* cancelled. + */ + + /* Selection covers rows 15,0 */ + term.selection.start = (struct coord){.row = 15}; + term.selection.end = (struct coord){.row = 16}; + term.selection.kind = SELECTION_CHAR_WISE; + + populate_scrollback(); + term_erase_scrollback(&term); + xassert(term.selection.start.row == 15); + xassert(term.selection.end.row == 16); + xassert(term.selection.kind == SELECTION_CHAR_WISE); + + close(term.selection.auto_scroll.fd); + for (int i = 0; i < scrollback_rows; i++) + grid_row_free(term.normal.rows[i]); + free(term.normal.rows); + fdm_destroy(fdm); +} + int term_row_rel_to_abs(const struct terminal *term, int row) { diff --git a/terminal.h b/terminal.h index b02036a2..d699d644 100644 --- a/terminal.h +++ b/terminal.h @@ -662,6 +662,7 @@ void term_damage_scroll( void term_erase( struct terminal *term, const struct coord *start, const struct coord *end); +void term_erase_scrollback(struct terminal *term); int term_row_rel_to_abs(const struct terminal *term, int row); void term_cursor_home(struct terminal *term);