From 849427bf105a4bf5c251a5aed5940cecd7234032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 26 Feb 2021 09:28:03 +0100 Subject: [PATCH 1/2] sixel: implement private mode 80 - sixel scrolling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When enabled (the default), sixels behave much like normal output; the start where the cursor is, and the cursor moves with the sixel. I.e. after emitting a sixel the cursor is left after the image; either to the right, if private mode 8452 is enabled, or otherwise on the next line. Terminal content is scrolled up if the sixel is larger than the screen. When disabled, sixels *always* start at (0,0), the cursor never moves, and the terminal content never scrolls. In other words, the ‘disabled’ mode is a much simpler mode. All we need to do to support both modes is re-write the sixel-emitting loop to: * break early if we’re “out of rows”, i.e. we’ve reached the bottom of the screen. * not linefeed, or move the cursor when scrolling is disabled This patch also fixes a bug in the (new) implementation of private mode 8452. When emitting a sixel, we may break it up into smaller pieces, to ensure a single sixel (as tracked internally) does not cross the scrollback wrap-around. The code that checked if we should do a linefeed or not, would skip the linefeed on the last row of *each* such sixel piece. The correct thing to do is to skip it only on the last row of the *last* piece. I chose not to fix this bug in a separate patch since doing so would have meant re-writing it again when implementing private mode 80. --- CHANGELOG.md | 2 ++ csi.c | 7 ++++ sixel.c | 99 ++++++++++++++++++++++++++++++++-------------------- terminal.c | 1 + terminal.h | 7 ++-- 5 files changed, 75 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07d645ab..9fbd9957 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,8 @@ `footclient` (https://codeberg.org/dnkl/foot/issues/337). * `-D,--working-directory=DIR` to both `foot` and `footclient` (https://codeberg.org/dnkl/foot/issues/347) +* `DECSET 80` - sixel scrolling + (https://codeberg.org/dnkl/foot/issues/361). * `DECSET 1070` - sixel private color palette (https://codeberg.org/dnkl/foot/issues/362). * `DECSET 8452` - position cursor to the right of sixels diff --git a/csi.c b/csi.c index b05327b1..5b102f58 100644 --- a/csi.c +++ b/csi.c @@ -401,6 +401,10 @@ decset_decrst(struct terminal *term, unsigned param, bool enable) term->reverse_wrap = enable; break; + case 80: + term->sixel.scrolling = enable; + break; + case 1000: if (enable) term->mouse_tracking = MOUSE_CLICK; @@ -599,6 +603,7 @@ decrqm(const struct terminal *term, unsigned param, bool *enabled) case 12: *enabled = term->cursor_blink.decset; return true; case 25: *enabled = !term->hide_cursor; return true; case 45: *enabled = term->reverse_wrap; return true; + case 80: *enabled = term->sixel.scrolling; return true; case 1000: *enabled = term->mouse_tracking == MOUSE_CLICK; return true; case 1001: *enabled = false; return true; case 1002: *enabled = term->mouse_tracking == MOUSE_DRAG; return true; @@ -640,6 +645,7 @@ xtsave(struct terminal *term, unsigned param) case 25: term->xtsave.show_cursor = !term->hide_cursor; break; case 45: term->xtsave.reverse_wrap = term->reverse_wrap; break; case 47: term->xtsave.alt_screen = term->grid == &term->alt; break; + case 80: term->xtsave.sixel_scrolling = term->sixel.scrolling; break; case 1000: term->xtsave.mouse_click = term->mouse_tracking == MOUSE_CLICK; break; case 1001: break; case 1002: term->xtsave.mouse_drag = term->mouse_tracking == MOUSE_DRAG; break; @@ -680,6 +686,7 @@ xtrestore(struct terminal *term, unsigned param) case 25: enable = term->xtsave.show_cursor; break; case 45: enable = term->xtsave.reverse_wrap; break; case 47: enable = term->xtsave.alt_screen; break; + case 80: enable = term->xtsave.sixel_scrolling; break; case 1000: enable = term->xtsave.mouse_click; break; case 1001: return; case 1002: enable = term->xtsave.mouse_drag; break; diff --git a/sixel.c b/sixel.c index 4f41348c..06246203 100644 --- a/sixel.c +++ b/sixel.c @@ -701,24 +701,43 @@ sixel_unhook(struct terminal *term) const int stride = term->sixel.image.width * sizeof(uint32_t); /* - * Need to 'remember' current cursor column. + * When sixel scrolling is enabled (the default), sixels behave + * pretty much like normal output; the sixel starts at the current + * cursor position and the cursor is moved to a point after the + * sixel. * - * If we split up the sixel (to avoid scrollback wrap-around), we - * will emit a carriage-return (after several linefeeds), which - * will reset the cursor column to 0. If we use _that_ column for - * the subsequent image parts, the image will look sheared. + * Furthermore, if the sixel reaches the bottom of the scrolling + * region, the terminal content is scrolled. + * + * When scrolling is disabled, sixels always start at (0,0), the + * cursor is not moved at all, and the terminal content never + * scrolls. */ - const int start_col = term->grid->cursor.point.col; + + const bool do_scroll = term->sixel.scrolling; + + /* Number of rows we're allowed to use. + * + * When scrolling is enabled, we always allow the entire sixel to + * be emitted. + * + * When disabled, only the number of screen rows may be used. */ + int rows_avail = do_scroll + ? (term->sixel.image.height + term->cell_height - 1) / term->cell_height + : term->rows; + + /* Initial sixel coordinates */ + int start_row = do_scroll ? term->grid->cursor.point.row : 0; + const int start_col = do_scroll ? term->grid->cursor.point.col : 0; /* We do not allow sixels to cross the scrollback wrap-around, as * this makes intersection calculations much more complicated */ - while (pixel_rows_left > 0) { - const struct coord *cursor = &term->grid->cursor.point; + while (pixel_rows_left > 0 && rows_avail > 0) { + const int cur_row = (term->grid->offset + start_row) & (term->grid->num_rows - 1); + const int rows_left_until_wrap_around = term->grid->num_rows - cur_row; + const int usable_rows = min(rows_avail, rows_left_until_wrap_around); - const int cur_row = (term->grid->offset + cursor->row) & (term->grid->num_rows - 1); - const int rows_avail = term->grid->num_rows - cur_row; - - const int pixel_rows_avail = rows_avail * term->cell_height; + const int pixel_rows_avail = usable_rows * term->cell_height; const int width = term->sixel.image.width; const int height = min(pixel_rows_left, pixel_rows_avail); @@ -755,12 +774,15 @@ sixel_unhook(struct terminal *term) image.width, image.height, img_data, stride); - /* Allocate space *first* (by emitting line-feeds), then insert */ + pixel_row_idx += height; + pixel_rows_left -= height; + rows_avail -= image.rows; + + /* Dirty touched cells, and scroll terminal content if necessary */ for (size_t i = 0; i < image.rows; i++) { - struct row *row = term->grid->cur_row; + struct row *row = term->grid->rows[cur_row + i]; row->dirty = true; - /* Mark cells touched by the sixel as dirty */ for (int col = image.pos.col; col < min(image.pos.col + image.cols, term->cols); col++) @@ -768,36 +790,37 @@ sixel_unhook(struct terminal *term) row->cells[col].attrs.clean = 0; } - if (i < image.rows - 1 || !term->sixel.cursor_right_of_graphics) - term_linefeed(term); + if (do_scroll) { + /* + * Linefeed, *unless* we're on the very last row of + * the final image (not just this chunk) and private + * mode 8452 (leave cursor at the right of graphics) + * is enabled. + */ + if (term->sixel.cursor_right_of_graphics && + rows_avail == 0 && + i >= image.rows - 1) + { + term_cursor_to( + term, + term->grid->cursor.point.row, + min(image.pos.col + image.cols, term->cols - 1)); + } else { + term_linefeed(term); + term_carriage_return(term); + } + } } - /* - * Position cursor - * - * Private mode 8452 controls where we leave the cursor after - * emitting a sixel: - * - * When disabled (the default), the cursor is positioned on a - * new line. - * - * When enabled, the cursor is positioned to the right of the - * sixel. - */ - term_cursor_to( - term, - term->grid->cursor.point.row, - (term->sixel.cursor_right_of_graphics - ? min(image.pos.col + image.cols, term->cols - 1) - : 0)); - _sixel_overwrite_by_rectangle( term, image.pos.row, image.pos.col, image.rows, image.cols); sixel_insert(term, image); - pixel_row_idx += height; - pixel_rows_left -= height; + if (do_scroll) + start_row = term->grid->cursor.point.row; + else + start_row -= image.rows; } term->sixel.image.data = NULL; diff --git a/terminal.c b/terminal.c index dc210fbc..5cbb7fcf 100644 --- a/terminal.c +++ b/terminal.c @@ -1178,6 +1178,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .upper_fd = delay_upper_fd, }, .sixel = { + .scrolling = true, .use_private_palette = true, .palette_size = SIXEL_MAX_COLORS, .max_width = SIXEL_MAX_WIDTH, diff --git a/terminal.h b/terminal.h index 2a6ae0b1..a7f2ed36 100644 --- a/terminal.h +++ b/terminal.h @@ -360,6 +360,7 @@ struct terminal { bool modify_escape_key:1; bool ime:1; + bool sixel_scrolling:1; bool sixel_private_palette:1; bool sixel_cursor_right_of_graphics:1; } xtsave; @@ -533,14 +534,14 @@ struct terminal { bool autosize; } image; - bool use_private_palette:1; /* Private mode 1070 */ + bool scrolling:1; /* Private mode 80 */ + bool use_private_palette:1; /* Private mode 1070 */ + bool cursor_right_of_graphics:1; /* Private mode 8452 */ unsigned params[5]; /* Collected parameters, for RASTER, COLOR_SPEC */ unsigned param; /* Currently collecting parameter, for RASTER, COLOR_SPEC and REPEAT */ unsigned param_idx; /* Parameters seen */ - bool cursor_right_of_graphics:1; /* Private mode 8452 */ - /* Application configurable */ unsigned palette_size; /* Number of colors in palette */ unsigned max_width; /* Maximum image width, in pixels */ From 1563fecc2040b6cbc622e5d0da6b7837fde768a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 26 Feb 2021 14:20:00 +0100 Subject: [PATCH 2/2] =?UTF-8?q?sixel:=20don=E2=80=99t=20go=20past=20the=20?= =?UTF-8?q?bottom=20scroll=20margin=20when=20sixel=20scrolling=20is=20disa?= =?UTF-8?q?bled?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When sixel scrolling is disabled (private mode 80 is off), and scroll margins have been set, XTerm seems to ignore the top margin (sixel still begins at (0,0)), but does not go past the bottom margin. This patch implements the same behavior in foot. --- sixel.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sixel.c b/sixel.c index 06246203..ecc6d141 100644 --- a/sixel.c +++ b/sixel.c @@ -724,7 +724,7 @@ sixel_unhook(struct terminal *term) * When disabled, only the number of screen rows may be used. */ int rows_avail = do_scroll ? (term->sixel.image.height + term->cell_height - 1) / term->cell_height - : term->rows; + : term->scroll_region.end; /* Initial sixel coordinates */ int start_row = do_scroll ? term->grid->cursor.point.row : 0;