mirror of
https://github.com/tmux/tmux.git
synced 2026-10-15 07:44:24 +00:00
Use imagescroll feature.
This commit is contained in:
@@ -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,
|
||||
|
||||
26
image-grid.c
26
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,
|
||||
@@ -680,9 +688,17 @@ 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 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. */
|
||||
|
||||
@@ -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)
|
||||
|
||||
16
regress/image-scroll-no-erase.sh
Executable file
16
regress/image-scroll-no-erase.sh
Executable file
@@ -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
|
||||
17
regress/image-scroll-no-text-redraw.sh
Executable file
17
regress/image-scroll-no-text-redraw.sh
Executable file
@@ -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
|
||||
20
regress/image-scroll-uses-region.sh
Executable file
20
regress/image-scroll-uses-region.sh
Executable file
@@ -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
|
||||
21
regress/image-sixel-region-scroll.sh
Executable file
21
regress/image-sixel-region-scroll.sh
Executable file
@@ -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
|
||||
133
regress/image_scroll.py
Normal file
133
regress/image_scroll.py
Normal file
@@ -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)
|
||||
@@ -150,6 +150,9 @@ class Terminal:
|
||||
if rest[0] == 13:
|
||||
self.x = 0
|
||||
elif rest[0] == 10:
|
||||
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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 *);
|
||||
|
||||
Reference in New Issue
Block a user