diff --git a/image.c b/image.c index 0c240d90f..11b3edcd4 100644 --- a/image.c +++ b/image.c @@ -100,7 +100,8 @@ static const struct image_backend image_backend_fallback = { "fallback", IMAGE_BACKEND_SCROLLS, NULL, NULL }; static const struct image_backend image_backend_kitty = { - "kitty", IMAGE_BACKEND_GRAPHICAL|IMAGE_BACKEND_SCROLLS, + "kitty", + IMAGE_BACKEND_GRAPHICAL|IMAGE_BACKEND_SCROLLS|IMAGE_BACKEND_CLIPPED, kitty_draw_rect, kitty_free_output_state }; static const struct image_backend image_backend_sixel = { @@ -168,8 +169,22 @@ image_draw_flush(struct tty *tty) int image_backend_flags(struct tty *tty) { + int flags; + image_tty_update(tty); - return (tty->image_backend->flags); + flags = tty->image_backend->flags; + + /* + * There is no way to ask a terminal whether it moves SIXEL image + * content along with the rest of a scrolling region, so this is an + * assumption the user can turn off with sixel-region-scrolling if + * their terminal gets it wrong. + */ + if (tty->image_backend == &image_backend_sixel && + options_get_number(global_options, "sixel-region-scrolling")) + flags |= IMAGE_BACKEND_SCROLLS; + + return (flags); } /* Discard image backend state after a terminal geometry change. */ diff --git a/options-table.c b/options-table.c index 83bd0a5fa..aec39f849 100644 --- a/options-table.c +++ b/options-table.c @@ -538,6 +538,18 @@ const struct options_table_entry options_table[] = { "paste buffers with an escape sequence ('on' only)." }, + { .name = "sixel-region-scrolling", + .type = OPTIONS_TABLE_FLAG, + .scope = OPTIONS_TABLE_SERVER, + .default_num = 1, + .text = "Whether to assume the terminal moves SIXEL image content " + "along with the rest of a scrolling region, rather than " + "redrawing the image after every scroll. There is no way " + "to detect this, so if a terminal does not do it, images " + "may be left in the wrong place after scrolling - turn " + "this off in that case." + }, + { .name = "terminal-overrides", .type = OPTIONS_TABLE_STRING, .scope = OPTIONS_TABLE_SERVER, diff --git a/regress/image-sixel-region-scroll.sh b/regress/image-sixel-region-scroll.sh new file mode 100755 index 000000000..9a78acf31 --- /dev/null +++ b/regress/image-sixel-region-scroll.sh @@ -0,0 +1,95 @@ +#!/bin/sh + +# Regression test for the sixel-region-scrolling option (options-table.c): +# scrolling a pane that has a SIXEL image in it must not retransmit the +# image when the option is on - tmux trusts the terminal to have moved the +# image along with the rest of the scrolling region, the same way it +# already trusts Kitty placements to move themselves (IMAGE_BACKEND_SCROLLS +# in image.c). With the option off, today's always-redraw-on-scroll +# behaviour is unchanged. +# +# There is no way to query a terminal for whether it actually moves SIXEL +# pixels along with a scroll, so this only proves tmux's own decision to +# skip or redraw is wired correctly - not that any particular terminal +# renders the result correctly. That needs a human, on real terminals. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" + +cleanup() +{ + $TMUX kill-server >/dev/null 2>&1 + $TMUX2 kill-server >/dev/null 2>&1 +} +fail() +{ + echo "$*" >&2 + cleanup + exit 1 +} + +cleanup + +TMP=$(mktemp) +trap "cleanup; rm -f $TMP" 0 1 15 + +HEADER='\033Pq"1;1;26;26#0;2;100;100;100#0!26~-!26~-!26~-!26~-!26B\033\\' + +# A small image near the top of a tall pane, then enough plain output below +# it to force several ordinary (no DECSTBM sub-region) linefeed scrolls, +# each one shifting the image rows along with everything else. +$TMUX new-session -d -s inner -x 40 -y 20 "printf '$HEADER'; exec sh" || + exit 1 +sleep 0.3 + +[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0 +$TMUX set -as terminal-features ',*:sixel' || exit 1 + +$TMUX2 new-session -d -x 40 -y 20 || exit 1 +OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1) +[ -n "$OUTER" ] || fail "No outer pane." +$TMUX2 set -as terminal-features ',*:sixel@' || fail "disable outer sixel failed" +$TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed" +$TMUX2 send-keys -t "$OUTER" -l "$TMUX attach -t inner" || fail "send attach failed" +$TMUX2 send-keys -t "$OUTER" Enter || fail "send enter failed" +sleep 2 + +grep -qa '"1;1;26;26' "$TMP" || fail "sanity: image never reached the client" + +# --- Phase 1: sixel-region-scrolling on (the default) - expect no DCS. --- +$TMUX set -s sixel-region-scrolling on || fail "set option on failed" +sleep 0.5 +: >"$TMP" +$TMUX send-keys -t inner -l "yes | head -n 30" || fail "send scroll failed" +$TMUX send-keys -t inner Enter || fail "send enter failed" +sleep 1 + +n_on=$(grep -ac "$(printf '\033P')" "$TMP") +[ "$n_on" -eq 0 ] || + fail "image was retransmitted ($n_on times) scrolling with sixel-region-scrolling on" + +# --- Phase 2: sixel-region-scrolling off - expect the image back, then +# more scrolling to redraw it. --- +$TMUX send-keys -t inner -l "printf '$HEADER'" || fail "resend image failed" +$TMUX send-keys -t inner Enter || fail "send enter failed" +sleep 1.5 +grep -qa '"1;1;26;26' "$TMP" || fail "sanity: image did not reappear before phase 2" + +$TMUX set -s sixel-region-scrolling off || fail "set option off failed" +sleep 0.5 +: >"$TMP" +$TMUX send-keys -t inner -l "yes | head -n 30" || fail "send scroll failed" +$TMUX send-keys -t inner Enter || fail "send enter failed" +sleep 1 + +n_off=$(grep -ac "$(printf '\033P')" "$TMP") +[ "$n_off" -gt 0 ] || + fail "image was not retransmitted scrolling with sixel-region-scrolling off - expected the old always-redraw behaviour" + +exit 0 diff --git a/screen-redraw.c b/screen-redraw.c index c591269aa..e0e53323a 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -2063,7 +2063,7 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags) #ifdef ENABLE_IMAGES if ((flags & REDRAW_PANE) && (image_backend_flags(tty) & - (IMAGE_BACKEND_GRAPHICAL|IMAGE_BACKEND_SCROLLS)) == + (IMAGE_BACKEND_GRAPHICAL|IMAGE_BACKEND_CLIPPED)) == IMAGE_BACKEND_GRAPHICAL) redraw_damage_window_pane_status(scene->w); #endif diff --git a/tmux.h b/tmux.h index afffe2311..7dfa53c49 100644 --- a/tmux.h +++ b/tmux.h @@ -4316,6 +4316,7 @@ char *regsub(const char *, const char *, const char *, int); /* image.c */ #define IMAGE_BACKEND_GRAPHICAL 0x1 #define IMAGE_BACKEND_SCROLLS 0x2 +#define IMAGE_BACKEND_CLIPPED 0x4 struct image *image_create(u_int, u_int, u_int, u_int, u_int, u_int, u_char *); diff --git a/tools/sixel-ruler.six b/tools/sixel-ruler.six new file mode 100644 index 000000000..c457d5dd8 --- /dev/null +++ b/tools/sixel-ruler.six @@ -0,0 +1 @@ +P0;0;0q"1;1;160;200#0;2;0;0;0#1;2;4;0;0#2;2;0;4;0#3;2;0;0;4#4;2;5;0;5#5;2;5;5;5#6;2;8;0;0#7;2;11;0;0#8;2;0;7;0#9;2;0;11;0#10;2;7;7;0#11;2;9;9;0#12;2;0;0;9#13;2;7;0;7#14;2;11;0;11#15;2;0;9;9#16;2;7;7;7#17;2;11;11;11#18;2;15;0;0#19;2;20;0;0#20;2;14;9;0#21;2;18;11;0#22;2;0;14;0#23;2;0;20;0#24;2;0;23;0#25;2;18;18;0#26;2;20;13;0#27;2;20;20;0#28;2;0;0;14#29;2;0;0;16#30;2;15;0;15#31;2;17;0;17#32;2;20;0;20#33;2;24;0;24#34;2;0;16;16#35;2;0;19;19#36;2;14;14;14#37;2;18;18;18#38;2;20;20;20#39;2;24;24;24#40;2;25;0;0#41;2;36;0;0#42;2;40;0;0#43;2;47;0;0#44;2;0;27;0#45;2;0;33;0#46;2;0;36;0#47;2;0;39;0#48;2;0;43;0#49;2;0;45;0#50;2;0;49;0#51;2;27;27;0#52;2;35;35;0#53;2;39;39;0#54;2;0;0;27#55;2;0;0;31#56;2;0;0;36#57;2;0;0;40#58;2;0;0;42#59;2;0;0;45#60;2;0;0;47#61;2;27;0;27#62;2;31;0;31#63;2;34;0;34#64;2;36;0;36#65;2;40;0;40#66;2;42;0;42#67;2;45;0;45#68;2;49;0;49#69;2;0;32;32#70;2;0;41;41#71;2;0;49;49#72;2;28;28;28#73;2;30;30;30#74;2;33;33;33#75;2;35;35;35#76;2;38;38;38#77;2;42;42;42#78;2;45;45;45#79;2;49;49;49#80;2;60;0;0#81;2;64;0;0#82;2;68;0;0#83;2;71;0;0#84;2;74;0;0#85;2;76;0;0#86;2;80;0;0#87;2;89;0;0#88;2;92;0;0#89;2;96;0;0#90;2;100;0;0#91;2;54;35;0#92;2;63;41;0#93;2;68;44;0#94;2;0;50;0#95;2;55;55;0#96;2;71;71;0#97;2;74;74;0#98;2;82;53;0#99;2;91;58;0#100;2;100;65;0#101;2;77;77;0#102;2;80;80;0#103;2;87;87;0#104;2;89;89;0#105;2;93;93;0#106;2;96;96;0#107;2;100;100;0#108;2;0;0;50#109;2;0;0;68#110;2;0;0;71#111;2;0;0;73#112;2;0;0;76#113;2;0;0;78#114;2;0;0;83#115;2;0;0;87#116;2;0;0;89#117;2;0;0;96#118;2;0;0;100#119;2;50;0;50#120;2;58;0;58#121;2;64;0;64#122;2;71;0;71#123;2;78;0;78#124;2;78;0;78#125;2;86;0;86#126;2;93;0;93#127;2;96;0;96#128;2;100;0;100#129;2;0;52;52#130;2;0;55;55#131;2;0;58;58#132;2;0;62;62#133;2;0;73;73#134;2;0;77;77#135;2;0;83;83#136;2;0;89;89#137;2;0;93;93#138;2;0;97;97#139;2;0;100;100#140;2;51;51;51#141;2;64;64;64#142;2;69;69;69#143;2;76;76;76#144;2;80;80;80#145;2;82;82;82#146;2;87;87;87#147;2;89;89;89#148;2;93;93;93#149;2;94;94;94#150;2;100;100;100#90!160~-!4~NBbrrBF!149~$#85!4?_?O#6C??_$#88!4?O#1_???G#40O$#42!5?GC#86G#18C#83_G$#0!5?O#19G#82?G#43O$#89!5?C#80???C-#90!4~o?FNN?_{!148~$#81!4?B#89_!5?B$#84!4?C#0G!4?B$#88!4?G#1C!4?C$#41!5?O_#6_?O#82O$#7!5?B#40O???G$#86!6?G??B$#87!7?O#18_#43G$#85!8?O#80_$#83!9?C-#90!160B$#100!160{-!5~^^NF!151~$#99!5?_??G$#93!6?_#21_#20o$#98!7?O-#100!5~}}??!151~$#91!5?@#26@#20@~$#92!7?}-#100!7NKK!151N$#107!160o$#92!7?B#20B-#107!160~-!4~row[K??r!148~$#102!4?C??A?C#106_$#103!4?G#105@C???@K$#0!5?C!4?K$#25!5?A???O$#95!5?G?_#10_#27A$#51!6?A???A$#52!6?@??_O$#11!7?@@#53@$#101!8?A#97G$#104!8?O-#107!4~rooqqrrv!148~$#95!4?G#0K!5G#105G$#96!4?C#25A#27@@#101@$#104!5?@#53A#102!4C$#97!6?C-#94!160~-!4~FBbrRBB!149~$#50!4?g?O?_?C$#47!4?OC#23G??O#44G$#2!5?G??CG$#0!5?O?C#45G#22_O$#48!5?_?G#24?C_$#9!6?C-#94!4~bBNKK??r!148~$#44!4?G?O?A#24_A$#49!4?CC?O#8_A#50`K$#48!4?O#0O!4?G$#9!5?G???P#23O$#47!5?_?AO#46K$#22!6?_@$#2!7?_@?C-#94!160B$#139!160{-!7~NFF!150~$#70!7?_#15_#35o$#138!7?O#34O#130G$#135!8?G-#139!4~F@?K??NN!148~$#137!4?_A#133@!4?O$#70!4?O#0!6O$#132!4?G#136___??_$#35!5?G???N$#69!5?C#138G!4?_$#15!6?A#34@?_$#71!6?C#134A#129`$#131!8?M-#118!160o$#139!8NKK!150N$#131!8?B#35B-#118!160~-!4~n?_ccCC^!148~$#115!4?O!5?A$#117!5?_!4?G_$#0!5?O!4@_$#55!5?AG#113AAA#56P$#59!5?@O#12GG#28O$#3!5?G#58A??G$#29!5?C#109C#114O#111O#110_-#118!4~xprrrow}!148~$#116!4?C!4?@#0@#115@$#109!4?A#3C?G??A$#114!5?G?C#28G#29C#60C$#54!5?AG#112?C#108G$#57!6?C#111??A-#119!160~-!4~NFBrRbb!149~$#68!4?O?_?_?C$#65!4?_#4OG?C#32CW$#61!5?GC#13C?G$#0!5?_#64OGGO-#119!4~o??KK??r!148~$#66!4?G??O?C$#62!4?A#68_K???_G$#63!4?@#0M??@?K$#64!4?C#31O!4?O$#4!5?@?`_#14OA$#30!6?O??B$#32!6?a??_$#33!6?@#67AA?@C$#65!8?OG-#119!160B$#128!160{-!4~!8N!148~$#126!4?_!6?_$#121!4?O#0!6O#122O$#124!5?!4_#120_#14_-#128!6~NB`w}!149~$#121!6?_#4O#13C#31@#122@$#127!6?O#30_#65G#64A$#67!7?G#119A#125C$#126!7?C#123O-#128!6NKK!152N$#150!160o$#13!6?A#66@$#63!6?@#121A-#150!160~-!4~Z??CC?P!149~$#148!4?c#36_???G#76_$#0!5?C?OO#39oC$#37!5?A#79_???A$#38!5?G#5O@#17@A#140G$#146!5?P#73B#144AG#78D$#75!6?G#147g#142A$#143!6?C#145?_-#150!4~worrroo{!148~$#148!4?C!6?A$#141!4?B#145G???@#149G@$#16!5?C#77C??G#0B$#36!5?B#72G#5G#144CA#74C$#146!7?C#17G#39C-#79!160~-!4~FBrrrBF!149~$#73!4?_!4?CG$#74!4?O!4?O#0_$#78!4?G#16_??C?O$#5!5?W?C?G$#77!5?C?G#75G_$#36!6?C$#38!6?G-#79!4~uCCLK?_}!148~$#78!4?G?G!4?@$#77!4?@_?O@#38G#76O$#0!5?@?a?OB$#37!5?GO???G$#75!5?A???c$#16!5?OA#5?A?C$#17!6?_?_@$#72!6?@??A$#73!8?O-#79!160B-\ \ No newline at end of file diff --git a/tools/sixel-scroll-region-test.sh b/tools/sixel-scroll-region-test.sh new file mode 100755 index 000000000..eb758db80 --- /dev/null +++ b/tools/sixel-scroll-region-test.sh @@ -0,0 +1,44 @@ +#!/bin/sh + +# Visual test for sixel-region-scrolling: does this terminal actually move +# SIXEL pixels along with tmux's own scroll-region escapes (DECSTBM/DECSLRM +# + IND/RI/RIN), or does tmux need to fall back to redrawing the image on +# every scroll? +# +# There is no way to query a terminal for this, so it has to be judged by +# eye. Run this from inside the tmux session you want to test (attached +# from whichever terminal emulator you're checking), and watch the +# numbered colour bands in the floating pane while the counter below it +# scrolls slowly. Run it once with the option on, once with it off, and +# compare: +# +# - "off" is the known-good baseline: tmux redraws the whole image every +# scroll, so the bands must stay perfectly aligned with the counter no +# matter what the terminal does on its own. +# - "on" trusts the terminal to have moved the pixels itself. If it +# looks identical to "off", this terminal is fine. If a band freezes, +# duplicates, tears, or drifts out of sync with the counter, this +# terminal does not scroll SIXEL regions correctly and +# sixel-region-scrolling should stay off for it. +# +# Usage: sh tools/sixel-scroll-region-test.sh [on|off] + +set -eu +cd "$(dirname "$0")/.." + +MODE=${1:-on} +case "$MODE" in +on|off) ;; +*) echo "usage: $0 [on|off]" >&2; exit 1 ;; +esac + +tmux set -s sixel-region-scrolling "$MODE" +tmux set -as terminal-features ',*:sixel' + +echo "Testing with sixel-region-scrolling=$MODE. Watch the coloured bands" >&2 +echo "in the floating pane - they should stay locked to the counter." >&2 +sleep 2 + +PANE=$(tmux new-pane -d -x 24 -y 22 -PF '#{pane_id}' \ + "cat '$(pwd)/tools/sixel-ruler.six'; for i in \$(seq 1 50); do echo line \$i; sleep 0.3; done; echo done - press enter; read _") +tmux select-pane -t "$PANE"