diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ceab4d5..4712a299 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,8 +62,12 @@ * Support for OSC-176, _"Set App-ID"_ (https://gist.github.com/delthas/d451e2cc1573bb2364839849c7117239). * Support for `DECRQM` queries with ANSI/ECMA-48 modes (`CSI Ps $ p`). +* Rectangular edit functions: `DECCARA`, `DECRARA`, `DECCRA`, `DECFRA` + and `DECERA` ([#1633][1633]). +* `Rect` capability to terminfo. [1348]: https://codeberg.org/dnkl/foot/issues/1348 +[1633]: https://codeberg.org/dnkl/foot/issues/1633 ### Changed diff --git a/csi.c b/csi.c index 78fdf4ef..d03ae40d 100644 --- a/csi.c +++ b/csi.c @@ -673,6 +673,24 @@ xtrestore(struct terminal *term, unsigned param) decset_decrst(term, param, enable); } +static bool +params_to_rectangular_area(const struct terminal *term, int first_idx, + int *top, int *left, int *bottom, int *right) +{ + int rel_top = vt_param_get(term, first_idx + 0, 1) - 1; + *left = min(vt_param_get(term, first_idx + 1, 1) - 1, term->cols - 1); + int rel_bottom = vt_param_get(term, first_idx + 2, term->rows) - 1; + *right = min(vt_param_get(term, first_idx + 3, term->cols) - 1, term->cols - 1); + + if (rel_top > rel_bottom || *left > *right) + return false; + + *top = term_row_rel_to_abs(term, rel_top); + *bottom = term_row_rel_to_abs(term, rel_bottom); + + return true; +} + void csi_dispatch(struct terminal *term, uint8_t final) { @@ -743,10 +761,10 @@ csi_dispatch(struct terminal *term, uint8_t final) * Note: tertiary DA responds with "FOOT". */ if (term->conf->tweak.sixel) { - static const char reply[] = "\033[?62;4;22c"; + static const char reply[] = "\033[?62;4;22;28c"; term_to_slave(term, reply, sizeof(reply) - 1); } else { - static const char reply[] = "\033[?62;22c"; + static const char reply[] = "\033[?62;22;28c"; term_to_slave(term, reply, sizeof(reply) - 1); } break; @@ -1752,6 +1770,214 @@ csi_dispatch(struct terminal *term, uint8_t final) break; /* private[0] == '=' */ } + case '$': { + switch (final) { + case 'r': { /* DECCARA */ + int top, left, bottom, right; + if (!params_to_rectangular_area( + term, 0, &top, &left, &bottom, &right)) + { + break; + } + + for (int r = top; r <= bottom; r++) { + struct row *row = grid_row(term->grid, r); + row->dirty = true; + + for (int c = left; c <= right; c++) { + struct attributes *a = &row->cells[c].attrs; + a->clean = 0; + + for (size_t i = 4; i < term->vt.params.idx; i++) { + const int param = term->vt.params.v[i].value; + + /* DECCARA only supports a sub-set of SGR parameters */ + switch (param) { + case 0: + a->bold = false; + a->underline = false; + a->blink = false; + a->reverse = false; + break; + + case 1: a->bold = true; break; + case 4: a->underline = true; break; + case 5: a->blink = true; break; + case 7: a->reverse = true; break; + + case 22: a->bold = false; break; + case 24: a->underline = false; break; + case 25: a->blink = false; break; + case 27: a->reverse = false; break; + } + } + } + } + break; + } + + case 't': { /* DECRARA */ + int top, left, bottom, right; + if (!params_to_rectangular_area( + term, 0, &top, &left, &bottom, &right)) + { + break; + } + + for (int r = top; r <= bottom; r++) { + struct row *row = grid_row(term->grid, r); + row->dirty = true; + + for (int c = left; c <= right; c++) { + struct attributes *a = &row->cells[c].attrs; + a->clean = 0; + + for (size_t i = 4; i < term->vt.params.idx; i++) { + const int param = term->vt.params.v[i].value; + + /* DECRARA only supports a sub-set of SGR parameters */ + switch (param) { + case 0: + a->bold = !a->bold; + a->underline = !a->underline; + a->blink = !a->blink; + a->reverse = !a->reverse; + break; + + case 1: a->bold = !a->bold; break; + case 4: a->underline = !a->underline; break; + case 5: a->blink = !a->blink; break; + case 7: a->reverse = !a->reverse; break; + } + } + } + } + break; + } + + case 'v': { /* DECCRA */ + int src_top, src_left, src_bottom, src_right; + if (!params_to_rectangular_area( + term, 0, &src_top, &src_left, &src_bottom, &src_right)) + { + break; + } + + int src_page = vt_param_get(term, 4, 1); + + int dst_rel_top = vt_param_get(term, 5, 1) - 1; + int dst_left = vt_param_get(term, 6, 1) - 1; + int dst_page = vt_param_get(term, 7, 1); + + if (unlikely(src_page != 1 || dst_page != 1)) { + /* We don’t support “pages” */ + break; + } + + int dst_rel_bottom = dst_rel_top + (src_bottom - src_top); + int dst_right = min(dst_left + (src_right - src_left), term->cols - 1); + + int dst_top = term_row_rel_to_abs(term, dst_rel_top); + int dst_bottom = term_row_rel_to_abs(term, dst_rel_bottom); + + /* Target area outside the screen is clipped */ + const size_t row_count = min(src_bottom - src_top, + dst_bottom - dst_top) + 1; + const size_t cell_count = min(src_right - src_left, + dst_right - dst_left) + 1; + + sixel_overwrite_by_rectangle( + term, dst_top, dst_left, row_count, cell_count); + + /* + * Copy source area + * + * Note: since source and destination may overlap, we need + * to copy out the entire source region first, and _then_ + * write the destination. I.e. this is similar to how + * memmove() behaves, but adapted to our row/cell + * structure. + */ + struct cell **copy = xmalloc(row_count * sizeof(copy[0])); + for (int r = 0; r < row_count; r++) { + copy[r] = xmalloc(cell_count * sizeof(copy[r][0])); + + const struct row *row = grid_row(term->grid, src_top + r); + const struct cell *cell = &row->cells[src_left]; + memcpy(copy[r], cell, cell_count * sizeof(copy[r][0])); + } + + /* Paste into destination area */ + for (int r = 0; r < row_count; r++) { + struct row *row = grid_row(term->grid, dst_top + r); + row->dirty = true; + + struct cell *cell = &row->cells[dst_left]; + memcpy(cell, copy[r], cell_count * sizeof(copy[r][0])); + free(copy[r]); + + for (;cell < &row->cells[dst_left + cell_count]; cell++) + cell->attrs.clean = 0; + + if (unlikely(row->extra != NULL)) { + /* TODO: technically, we should copy the source URIs... */ + grid_row_uri_range_erase(row, dst_left, dst_right); + } + } + free(copy); + break; + } + + case 'x': { /* DECFRA */ + const uint8_t c = vt_param_get(term, 0, 0); + + if (unlikely(!((c >= 32 && c < 126) || c >= 160))) + break; + + int top, left, bottom, right; + if (!params_to_rectangular_area( + term, 1, &top, &left, &bottom, &right)) + { + break; + } + + /* Erase the entire region at once (MUCH cheaper than + * doing it row by row, or even character by + * character). */ + sixel_overwrite_by_rectangle( + term, top, left, bottom - top + 1, right - left + 1); + + for (int r = top; r <= bottom; r++) + term_fill(term, r, left, c, right - left + 1, true); + + break; + } + + case 'z': { /* DECERA */ + int top, left, bottom, right; + if (!params_to_rectangular_area( + term, 0, &top, &left, &bottom, &right)) + { + break; + } + + /* + * Note: term_erase() _also_ erases sixels, but since + * we’re forced to erase one row at a time, erasing the + * entire sixel here is more efficient. + */ + sixel_overwrite_by_rectangle( + term, top, left, bottom - top + 1, right - left + 1); + + for (int r = top; r <= bottom; r++) + term_erase(term, r, left, r, right); + break; + } + } + + break; /* private[0] == ‘$’ */ + } + case 0x243f: /* ?$ */ switch (final) { case 'p': { diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index 0d8d5d79..38742aa3 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -495,6 +495,31 @@ manipulation sequences. The generic format is: : VT320 : Request status of ECMA-48/ANSI mode. See the descriptions for SM/RM above for recognized _Ps_ values. +| \\E[ _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ ; _Pm_ $ r +: DECCARA +: VT400 +: Change attributes in rectangular area. _Pt_, _Pl_, _Pb_ and _Pr_ + denotes the rectangle, _Pm_ denotes the SGR attributes. +| \\E[ _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ ; _Pm_ $ t +: DECRARA +: VT400 +: Invert attributes in rectangular area. _Pt_, _Pl_, _Pb_ and _Pr_ + denotes the rectangle, _Pm_ denotes the SGR attributes. +| \\E[ _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ ; _Pp_ ; _Pt_ ; _Pl_ ; _Pp_ $ v +: DECCRA +: VT400 +: Copy rectangular area. _Pt_, _Pl_, _Pb_ and _Pr_ denotes the + rectangle, _Pt_ and _Pl_ denotes the target location. +| \\E[ _Pc_ ; _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ $ x +: DECFRA +: VT420 +: Fill rectangular area. _Pc_ is the character to use, _Pt_, _Pl_, + _Pb_ and _Pr_ denotes the rectangle. +| \\E[ _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ $ z +: DECERA +: VT400 +: Erase rectangular area. _Pt_, _Pl_, _Pb_ and _Pr_ denotes the + rectangle. | \\E[ _Ps_ T : SD : VT420 diff --git a/foot.info b/foot.info index 3bab3266..319d0781 100644 --- a/foot.info +++ b/foot.info @@ -38,6 +38,7 @@ PE=\E[201~, PS=\E[200~, RV=\E[>c, + Rect=\E[%p1%d;%p2%d;%p3%d;%p4%d;%p5%d$x, Se=\E[ q, Ss=\E[%p1%d q, Sync=\E[?2026%?%p1%{1}%-%tl%eh%;, diff --git a/grid.h b/grid.h index 0664409c..8ea5200b 100644 --- a/grid.h +++ b/grid.h @@ -86,7 +86,6 @@ grid_row_in_view(struct grid *grid, int row_no) void grid_row_uri_range_put( struct row *row, int col, const char *uri, uint64_t id); -void grid_row_uri_range_add(struct row *row, struct row_uri_range range); void grid_row_uri_range_erase(struct row *row, int start, int end); static inline void diff --git a/terminal.c b/terminal.c index d9d66bac..2248bd02 100644 --- a/terminal.c +++ b/terminal.c @@ -3498,6 +3498,54 @@ print_spacer(struct terminal *term, int col, int remaining) cell->attrs = term->vt.attrs; } +/* + * Puts a character on the grid. Coordinates are in screen coordinates + * (i.e. ‘cursor’ coordinates). + * + * Does NOT: + * - update the cursor + * - linewrap + * - erase sixels + * + * Limitations: + * - double width characters not supported + */ +void +term_fill(struct terminal *term, int r, int c, uint8_t data, size_t count, + bool use_sgr_attrs) +{ + struct row *row = grid_row(term->grid, r); + row->dirty = true; + + xassert(c + count <= term->cols); + + struct attributes attrs = use_sgr_attrs + ? term->vt.attrs + : (struct attributes){0}; + + const struct cell *last = &row->cells[c + count]; + for (struct cell *cell = &row->cells[c]; cell < last; cell++) { + cell->wc = data; + cell->attrs = attrs; + + if (unlikely(term->vt.osc8.uri != NULL)) { + grid_row_uri_range_put(row, c, term->vt.osc8.uri, term->vt.osc8.id); + + switch (term->conf->url.osc8_underline) { + case OSC8_UNDERLINE_ALWAYS: + cell->attrs.url = true; + break; + + case OSC8_UNDERLINE_URL_MODE: + break; + } + } + } + + if (unlikely(row->extra != NULL)) + grid_row_uri_range_erase(row, c, c + count - 1); +} + void term_print(struct terminal *term, char32_t wc, int width) { @@ -3566,7 +3614,7 @@ term_print(struct terminal *term, char32_t wc, int width) grid_row_uri_range_erase(row, col, col + width - 1); /* Advance cursor the 'additional' columns while dirty:ing the cells */ - for (int i = 1; i < width && col < term->cols - 1; i++) { + for (int i = 1; i < width && col < term->cols; i++) { col++; print_spacer(term, col, width - i); } diff --git a/terminal.h b/terminal.h index ec2ff963..0dd40c51 100644 --- a/terminal.h +++ b/terminal.h @@ -798,6 +798,8 @@ void term_cursor_down(struct terminal *term, int count); void term_cursor_blink_update(struct terminal *term); void term_print(struct terminal *term, char32_t wc, int width); +void term_fill(struct terminal *term, int row, int col, uint8_t c, size_t count, + bool use_sgr_attrs); void term_scroll(struct terminal *term, int rows); void term_scroll_reverse(struct terminal *term, int rows); diff --git a/vt.c b/vt.c index caa53e62..a8a0f0fb 100644 --- a/vt.c +++ b/vt.c @@ -18,6 +18,7 @@ #include "debug.h" #include "grid.h" #include "osc.h" +#include "sixel.h" #include "util.h" #include "xmalloc.h" @@ -560,15 +561,16 @@ action_esc_dispatch(struct terminal *term, uint8_t final) case '#': switch (final) { - case '8': - for (int r = 0; r < term->rows; r++) { - struct row *row = grid_row(term->grid, r); - for (int c = 0; c < term->cols; c++) { - row->cells[c].wc = U'E'; - row->cells[c].attrs = (struct attributes){0}; - } - row->dirty = true; - } + case '8': /* DECALN */ + sixel_overwrite_by_rectangle(term, 0, 0, term->rows, term->cols); + + term->scroll_region.start = 0; + term->scroll_region.end = term->rows; + + for (int r = 0; r < term->rows; r++) + term_fill(term, r, 0, 'E', term->cols, false); + + term_cursor_home(term); break; } break; /* private[0] == '#' */