diff --git a/CHANGELOG.md b/CHANGELOG.md index 48b8873a..7255e5a3 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..ecc6d141 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->scroll_region.end; + + /* 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 */