diff --git a/image-fallback.c b/image-fallback.c index f974049c6..d8888903b 100644 --- a/image-fallback.c +++ b/image-fallback.c @@ -41,6 +41,13 @@ fallback_select(__unused struct tty *tty) return (1); } +/* Text images move with terminal scrolling. */ +int +fallback_get_flags(__unused struct tty *tty) +{ + return (IMAGE_BACKEND_SCROLLS); +} + /* Get the average colour and brightness of an area. */ static int fallback_sample(struct image *im, uint64_t px, uint64_t py, uint64_t pw, diff --git a/image-grid.c b/image-grid.c index e5afe730a..cd3e5d24e 100644 --- a/image-grid.c +++ b/image-grid.c @@ -86,7 +86,7 @@ struct image_recover_row { /* A backend to draw an image. */ struct image_backend { const char *name; - int flags; + int (*get_flags)(struct tty *); int (*select)(struct tty *); const struct grid_cell *(*map_cell)(struct tty *, struct grid *, u_int, u_int, const struct grid_cell *, struct grid_cell *, @@ -100,7 +100,7 @@ struct image_backend { /* Backend definitions. */ static struct image_backend image_backend_fallback = { "fallback", - IMAGE_BACKEND_SCROLLS, + fallback_get_flags, fallback_select, fallback_map_cell, NULL, @@ -110,7 +110,7 @@ static struct image_backend image_backend_fallback = { }; static struct image_backend image_backend_sixel = { "sixel", - IMAGE_BACKEND_GRAPHICAL, + sixel_get_flags, sixel_select, NULL, sixel_draw_rect, @@ -147,6 +147,14 @@ image_tty_update(struct tty *tty) return (1); } +/* Get the flags for a terminal's image backend. */ +int +image_backend_get_flags(struct tty *tty) +{ + image_tty_update(tty); + return (tty->image_backend->get_flags(tty)); +} + /* Map an image grid cell to text for the selected backend. */ const struct grid_cell * image_map_cell(struct tty *tty, struct grid *gd, u_int x, u_int y, @@ -679,10 +687,18 @@ image_redraw_all(struct screen_write_ctx *ctx) void image_redraw_scroll(struct screen_write_ctx *ctx) { - struct screen *s = ctx->s; - u_int sx = screen_size_x(s), sy = s->rlower - s->rupper + 1; + struct screen *s = ctx->s; + struct window_pane *wp = ctx->wp; + u_int sx = screen_size_x(s); + u_int sy = s->rlower - s->rupper + 1; - image_redraw_area(ctx, 0, s->rupper, sx, sy); + if (wp == NULL) + return; + if (!image_grid_area_has_images(s->grid, 0, + s->grid->hsize + s->rupper, sx, sy)) + return; + redraw_damage_scroll(wp->window, wp->xoff, + wp->yoff + s->rupper, sx, sy); } /* Redraw images under changed cells. */ diff --git a/image-sixel.c b/image-sixel.c index c70f23bc5..4d500a690 100644 --- a/image-sixel.c +++ b/image-sixel.c @@ -1300,6 +1300,17 @@ sixel_select(struct tty *tty) return (0); } +/* Get SIXEL backend flags. */ +int +sixel_get_flags(struct tty *tty) +{ + int flags = IMAGE_BACKEND_GRAPHICAL; + + if (tty_term_flag(tty->term, TTYC_ISCRL)) + flags |= IMAGE_BACKEND_SCROLLS; + return (flags); +} + /* Get pending SIXEL rows for a terminal. */ static struct sixel_pending * sixel_get_pending(struct tty *tty) diff --git a/regress/image-scroll-no-erase.sh b/regress/image-scroll-no-erase.sh new file mode 100755 index 000000000..85d8b7d7a --- /dev/null +++ b/regress/image-scroll-no-erase.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# The PR's trusted-scroll erase regression: inspect terminal erases directly +# rather than depending on a particular debug log message. + +PATH=/bin:/usr/bin +TERM=screen +export TERM +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +python3 -B - "$TEST_TMUX" <<'PY' +import sys +from image_scroll import check_scroll + +check_scroll(sys.argv[1], True, no_erase=True) +PY diff --git a/regress/image-scroll-no-text-redraw.sh b/regress/image-scroll-no-text-redraw.sh new file mode 100755 index 000000000..aeae2570c --- /dev/null +++ b/regress/image-scroll-no-text-redraw.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +# Adapt the PR's Kitty marker check to SIXEL. Native image scrolling must +# skip the surrounding text repaint as well as image retransmission. + +PATH=/bin:/usr/bin +TERM=screen +export TERM +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +python3 -B - "$TEST_TMUX" <<'PY' +import sys +from image_scroll import check_scroll + +check_scroll(sys.argv[1], True) +check_scroll(sys.argv[1], False) +PY diff --git a/regress/image-scroll-uses-region.sh b/regress/image-scroll-uses-region.sh new file mode 100755 index 000000000..495de0958 --- /dev/null +++ b/regress/image-scroll-uses-region.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# The PR's narrow-pane scroll check. Margins permit native scrolling; +# without them imagescroll must not suppress a necessary repaint. +# Different clients must make that decision independently. + +PATH=/bin:/usr/bin +TERM=screen +export TERM +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +python3 -B - "$TEST_TMUX" <<'PY' +import sys +from image_scroll import check_scroll, check_clients + +check_scroll(sys.argv[1], True, margins=True) +check_scroll(sys.argv[1], True, margins=False) +check_clients(sys.argv[1]) +check_clients(sys.argv[1], narrow=True) +PY diff --git a/regress/image-sixel-region-scroll.sh b/regress/image-sixel-region-scroll.sh new file mode 100755 index 000000000..33f1a30fa --- /dev/null +++ b/regress/image-sixel-region-scroll.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# The PR's SIXEL imagescroll check, with known cell pixels and no shell race. +# Check the default repaint and an overwrite merged with trusted scroll damage. + +PATH=/bin:/usr/bin +TERM=screen +export TERM +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +python3 -B - "$TEST_TMUX" <<'PY' +import sys +from image_scroll import check_scroll, check_capability + +for command in ('linefeed', 'up', 'down', 'reverse'): + check_scroll(sys.argv[1], True, command=command) + check_scroll(sys.argv[1], False, command=command) +check_scroll(sys.argv[1], True, mixed=True) +check_capability(sys.argv[1], True) +check_capability(sys.argv[1], False) +PY diff --git a/regress/image_scroll.py b/regress/image_scroll.py new file mode 100644 index 000000000..4de254405 --- /dev/null +++ b/regress/image_scroll.py @@ -0,0 +1,133 @@ +"""PTY checks adapted from the PR's image scroll regressions.""" + +import re +from image_test import image_test, sixel, check_image +from image_terminal import Terminal + + +MARKER = b'SCROLLMARKER' +WIRE = sixel(16, 16, [(100, 0, 0), (0, 100, 0)], + [x >= 8 for y in range(16) for x in range(16)]) + + +def attach(test, scrolls, margins=None, term='xterm-256color'): + """Choose scroll features before attaching a client.""" + features = [term, 'imagescroll' if scrolls else 'imagescroll@'] + if margins is not None: + features.append('margins' if margins else 'margins@') + test.run('set-option', '-as', 'terminal-features', ':'.join(features)) + client = test.attach(term=term) + prefix = str(client.pid) + ':' + actual = next(line[len(prefix):] for line in test.run('list-clients', + '-F', '#{client_pid}:#{client_termfeatures}').splitlines() + if line.startswith(prefix)).split(',') + assert ('imagescroll' in actual) == scrolls, actual + if margins is not None: + assert ('margins' in actual) == margins, actual + return client + + +def place(test): + """Place marker text and a two-colour image away from the scroll edges.""" + test.emit(b'\033[7;1H' + MARKER + b'\033[9;3H' + WIRE + b'\033[16;1H') + for client in test.clients: + images = client.images() + assert len(images) == 1, 'initial image was not sent' + check_image(images[0], 16, 16, + [(255, 0, 0) if x < 8 else (0, 255, 0) + for y in range(16) for x in range(16)]) + + +def check_output(client, replay): + """Check whether scroll damage replayed the image and surrounding text.""" + data = bytes(client.data) + if replay: + assert client.images(), 'image was not replayed after a manual repaint' + assert MARKER in data, 'scroll damage did not repaint surrounding text' + else: + assert not client.images(), 'native scroll replayed the image' + assert MARKER not in data, 'native scroll repainted existing text' + return data + + +def check_position(test, mixed=False, down=False): + """A later full redraw must show the scrolled image at its new position.""" + test.redraw() + row = 9 if down else 7 + expected = {(x, y): (255, 0, 0) if x < 24 else (0, 255, 0) + for y in range(row * 16, (row + 1) * 16) for x in range(16, 32) + if not mixed or x >= 24} + for client in test.clients: + terminal = Terminal() + terminal.feed(client.data) + terminal.check(expected) + + +def check_scroll(tmux, scrolls, margins=None, no_erase=False, mixed=False, + command='linefeed'): + """Check one client's native scroll or fallback repaint.""" + with image_test(tmux) as test: + test.run('set-option', '-w', 'automatic-rename', 'off') + if margins is not None: + test.run('set-option', '-w', 'pane-scrollbars', 'on') + client = attach(test, scrolls, margins) + place(test) + if no_erase: + test.redraw() + terminal = Terminal() + terminal.feed(client.data) + operation = {'linefeed': b'\n', 'up': b'\033[S', 'down': b'\033[T', + 'reverse': b'\033[H\033M'}[command] + if mixed: + # An ordinary overwrite must prevent skipping merged scroll damage. + operation += b'\033[8;3HX' + test.emit(operation) + replay = not scrolls or margins is False or mixed + data = check_output(client, replay) + if no_erase: + # Erasing the newly exposed bottom row is valid. Check that + # none of the moved image pixels were erased or drawn over. + terminal.feed(data) + terminal.check({(x, y): (255, 0, 0) if x < 24 else (0, 255, 0) + for y in range(7 * 16, 8 * 16) for x in range(16, 32)}) + if scrolls and margins: + assert re.search(rb'\033\[\d+;\d+s', data), ( + 'no horizontal scrolling region was set', data) + check_position(test, mixed, command in ('down', 'reverse')) + + +def check_clients(tmux, narrow=False): + """Check different client capabilities and shared fallback damage.""" + with image_test(tmux) as test: + test.run('set-option', '-w', 'automatic-rename', 'off') + if narrow: + test.run('set-option', '-w', 'pane-scrollbars', 'on') + native = attach(test, True, True) + repaint = attach(test, True, False, term='xterm') + else: + native = attach(test, True) + repaint = attach(test, False, term='xterm') + place(test) + test.emit(b'\n') + # A large fallback repaint adds ordinary window damage, which must + # be drawn on both clients even if the first scrolled natively. + check_output(native, narrow) + check_output(repaint, True) + check_position(test) + + +def check_capability(tmux, enabled): + """Iscrl grants image scrolling, and Iscrl@ overrides an enabled feature.""" + with image_test(tmux) as test: + test.run('set-option', '-w', 'automatic-rename', 'off') + capability = 'Iscrl' if enabled else 'Iscrl@' + test.run('set-option', '-as', 'terminal-overrides', + 'xterm-256color:' + capability) + if not enabled: + test.run('set-option', '-as', 'terminal-features', + 'xterm-256color:imagescroll') + client = test.attach() + place(test) + test.emit(b'\n') + check_output(client, not enabled) + check_position(test) diff --git a/regress/image_terminal.py b/regress/image_terminal.py index f31434bd8..493c6ade5 100644 --- a/regress/image_terminal.py +++ b/regress/image_terminal.py @@ -150,7 +150,10 @@ class Terminal: if rest[0] == 13: self.x = 0 elif rest[0] == 10: - self.y += 1 + if self.y == self.bottom: + self.scroll(self.top, self.bottom, 1) + else: + self.y += 1 elif rest[0] == 8: self.x = max(0, self.x - 1) pos += 1 diff --git a/screen-redraw.c b/screen-redraw.c index 122b14eb5..3e14f7e28 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -2257,6 +2257,7 @@ void redraw_client_damage(struct client *c) { struct window *w = c->session->curw->window; + struct tty *tty = &c->tty; struct window_pane *wp; struct redraw_scene *scene; struct redraw_draw_ctx dctx; @@ -2278,10 +2279,18 @@ redraw_client_damage(struct client *c) wp->active_border_gc_set = 0; } - tty_sync_start(&c->tty); - tty_update_mode(&c->tty, c->tty.mode & ~CURSOR_MODES, NULL); + tty_sync_start(tty); + tty_update_mode(tty, tty->mode & ~CURSOR_MODES, NULL); TAILQ_FOREACH(rd, &w->damage, entry) { +#ifdef ENABLE_IMAGES + if ((rd->flags & REDRAW_DAMAGE_SCROLL) && + (image_backend_get_flags(tty) & IMAGE_BACKEND_SCROLLS) && + tty->scroll_window == w && + tty->scroll_generation == w->scroll_generation && + !tty->scroll_failed) + continue; +#endif x0 = (rd->x > ox) ? rd->x : ox; y0 = (rd->y > oy) ? rd->y : oy; x1 = (rd->x + rd->sx < ox + sx) ? rd->x + rd->sx : ox + sx; diff --git a/tmux-image.h b/tmux-image.h index df024f329..e3873a8e7 100644 --- a/tmux-image.h +++ b/tmux-image.h @@ -132,17 +132,20 @@ void image_draw_line(struct tty *, struct screen *, u_int, u_int, void image_redraw_start(struct tty *, u_int, u_int, u_int, u_int); void image_draw_flush(struct tty *); int image_tty_update(struct tty *); +int image_backend_get_flags(struct tty *); void image_tty_size_changed(struct tty *); void image_tty_free(struct tty *); /* image-fallback.c */ int fallback_select(struct tty *); +int fallback_get_flags(struct tty *); const struct grid_cell *fallback_map_cell(struct tty *, struct grid *, u_int, u_int, const struct grid_cell *, struct grid_cell *, int *); /* image-sixel.c */ #define SIXEL_COLOUR_REGISTERS 1024 int sixel_select(struct tty *); +int sixel_get_flags(struct tty *); struct sixel_image *sixel_parse(const char *, size_t, u_int, u_int, u_int, u_int); void sixel_free(struct sixel_image *);